Add an util to get and set Android Shared Preference files.
And a function to update gboad shared preferences.
Note that this requires root permission.
Bug: chromium:443782461
Change-Id: Ief475a258c8e6dad0c71874027b89f4e1c7d04f7
Reviewed-on: https://chromium-review.googlesource.com/c/catapult/+/7008791
Commit-Queue: Haiyang Pan <hypan@google.com>
Reviewed-by: Ben Pastene <bpastene@chromium.org>
diff --git a/devil/devil/android/device_utils.py b/devil/devil/android/device_utils.py
index 3e687ea..d87e8d7 100644
--- a/devil/devil/android/device_utils.py
+++ b/devil/devil/android/device_utils.py
@@ -39,6 +39,7 @@
from devil.android.sdk import adb_wrapper
from devil.android.sdk import intent
from devil.android.sdk import keyevent
+from devil.android.sdk import shared_prefs
from devil.android.sdk import version_codes
from devil.utils import parallelizer
from devil.utils import reraiser_thread
@@ -339,6 +340,11 @@
# * on non-hsum, main user is the system user (user 0)
_USER_FLAG_MAIN = 0x00004000
+# A string template pointing to file that stores the gboard preference
+# for a given user.
+_GBOARD_PKG = 'com.google.android.inputmethod.latin'
+_GBOARD_PREF_FILENAME = f'{_GBOARD_PKG}_preferences.xml'
+
# Namespaces for settings
class SettingsNamespace:
@@ -3838,6 +3844,27 @@
finally:
self.SetEnforce(not enabled, timeout=timeout, retries=retries)
+ @contextlib.contextmanager
+ def GboardPreferences(self, timeout=None, retries=None):
+ """Get the gboard preferences.
+
+ For users to read or modify the desired preferences.
+
+ Args:
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ A shared_prefs.SharedPrefs object
+ """
+ with shared_prefs.SharedPrefs(self,
+ _GBOARD_PKG,
+ _GBOARD_PREF_FILENAME,
+ user_id=self.GetCurrentUser(),
+ use_encrypted_path=True) as prefs:
+ yield prefs
+
+
@decorators.WithTimeoutAndRetriesFromInstance()
def GetWebViewProvider(self, timeout=None, retries=None):
"""Returns the webview_provider setting from global settings.
diff --git a/devil/devil/android/sdk/shared_prefs.py b/devil/devil/android/sdk/shared_prefs.py
index 0c77ae4..a4180ef 100644
--- a/devil/devil/android/sdk/shared_prefs.py
+++ b/devil/devil/android/sdk/shared_prefs.py
@@ -172,7 +172,13 @@
class SharedPrefs(object):
- def __init__(self, device, package, filename, use_encrypted_path=False):
+
+ def __init__(self,
+ device,
+ package,
+ filename,
+ user_id=0,
+ use_encrypted_path=False):
"""Helper object to read and update "Shared Prefs" of Android apps.
Such files typically look like, e.g.:
@@ -201,6 +207,7 @@
package: A string with the package name of the app that owns the shared
preferences file.
filename: A string with the name of the preferences file to read/write.
+ user_id: The id of the targeting user, as an int. Default to 0.
use_encrypted_path: Whether to read and write to the shared prefs location
in the device-encrypted path (/data/user_de) instead of the older,
unencrypted path (/data/data). Only supported on N+, but falls back to
@@ -213,8 +220,8 @@
self._filename = filename
self._unencrypted_path = '/data/data/%s/shared_prefs/%s' % (package,
filename)
- self._encrypted_path = '/data/user_de/0/%s/shared_prefs/%s' % (package,
- filename)
+ self._encrypted_path = '/data/user_de/%d/%s/shared_prefs/%s' % (
+ user_id, package, filename)
self._path = self._unencrypted_path
self._encrypted = use_encrypted_path
if use_encrypted_path:
@@ -275,7 +282,7 @@
A empty xml document, which may be modified and saved on |commit|, is
created if the file does not already exist.
"""
- if self._device.FileExists(self.path):
+ if self._device.PathExists(self.path, as_root=True):
self._xml = ElementTree.fromstring(
self._device.ReadFile(self.path, as_root=True))
assert self._xml.tag == 'map'
diff --git a/devil/devil/android/sdk/shared_prefs_test.py b/devil/devil/android/sdk/shared_prefs_test.py
index 13cb282..8734afd 100755
--- a/devil/devil/android/sdk/shared_prefs_test.py
+++ b/devil/devil/android/sdk/shared_prefs_test.py
@@ -27,7 +27,7 @@
if files is None:
files = {}
- def file_exists(path):
+ def path_exists(path, **_kwargs):
return path in files
def write_file(path, contents, **_kwargs):
@@ -37,7 +37,7 @@
return files[path]
device = mock.MagicMock(spec=device_utils.DeviceUtils)
- device.FileExists = mock.Mock(side_effect=file_exists)
+ device.PathExists = mock.Mock(side_effect=path_exists)
device.WriteFile = mock.Mock(side_effect=write_file)
device.ReadFile = mock.Mock(side_effect=read_file)
return device
@@ -102,17 +102,17 @@
return_value=version_codes.LOLLIPOP_MR1)
prefs = shared_prefs.SharedPrefs(self.device, 'com.some.package',
'other_prefs.xml')
- self.assertFalse(self.device.FileExists(prefs.path)) # file does not exist
+ self.assertFalse(self.device.PathExists(prefs.path)) # file does not exist
prefs.Load()
self.assertEqual(len(prefs), 0) # file did not exist, collection is empty
prefs.SetInt('magicNumber', 42)
prefs.SetFloat('myMetric', 3.14)
prefs.SetLong('bigNumner', 6000000000)
prefs.SetStringSet('apps', ['gmail', 'chrome', 'music'])
- self.assertFalse(self.device.FileExists(prefs.path)) # still does not exist
+ self.assertFalse(self.device.PathExists(prefs.path)) # still does not exist
self.assertTrue(prefs.changed)
prefs.Commit()
- self.assertTrue(self.device.FileExists(prefs.path)) # should exist now
+ self.assertTrue(self.device.PathExists(prefs.path)) # should exist now
self.device.KillAll.assert_called_once_with(
prefs.package, exact=True, as_root=True, quiet=True)
self.assertFalse(prefs.changed)
diff --git a/devil/devil/android/sdk/version_codes.py b/devil/devil/android/sdk/version_codes.py
index 35c3c6e..4d91b37 100644
--- a/devil/devil/android/sdk/version_codes.py
+++ b/devil/devil/android/sdk/version_codes.py
@@ -26,3 +26,4 @@
TIRAMISU = 33
UPSIDE_DOWN_CAKE = 34
VANILLA_ICE_CREAM = 35
+BAKLAVA = 36