Index: build/android/pylib/device/shared_prefs.py |
diff --git a/build/android/pylib/device/shared_prefs.py b/build/android/pylib/device/shared_prefs.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..32cef4b535b87ce87171323fe70793ac2bf23ab6 |
--- /dev/null |
+++ b/build/android/pylib/device/shared_prefs.py |
@@ -0,0 +1,391 @@ |
+# Copyright 2015 The Chromium Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+ |
+"""Helper object to read and modify Shared Preferences from Android apps. |
+ |
+See e.g.: |
+ http://developer.android.com/reference/android/content/SharedPreferences.html |
+""" |
+ |
+import collections |
+import logging |
+import posixpath |
+ |
+from xml.etree import ElementTree |
+ |
+ |
+_XML_DECLARATION = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" |
+ |
+ |
+class BasePref(object): |
+ """Base class for getting/setting the value of a specific preference type. |
+ |
+ Should not be instantiated directly. The SharedPrefs collection will |
+ instantiate the appropriate subclasses, which directly manipulate the |
+ underlying xml document, to parse and serialize values according to their |
+ type. |
+ |
+ Args: |
+ elem: An xml ElementTree object holding the preference data. |
+ |
+ Properties: |
+ tag_name: A string with the tag that must be used for this preference type. |
+ """ |
+ tag_name = None |
+ |
+ def __init__(self, elem): |
+ if elem.tag != type(self).tag_name: |
+ raise TypeError('Property %r has type %r, but trying to access as %r' % |
+ (elem.get('name'), elem.tag, type(self).tag_name)) |
+ self._elem = elem |
+ |
+ def __str__(self): |
+ """Get the underlying xml element as a string.""" |
+ return ElementTree.tostring(self._elem) |
+ |
+ def get(self): |
+ """Get the value of this preference.""" |
+ return self._elem.get('value') |
+ |
+ def set(self, value): |
+ """Set from a value casted as a string.""" |
+ self._elem.set('value', str(value)) |
+ |
+ @property |
+ def has_value(self): |
+ """Check whether the element has a value.""" |
+ return self._elem.get('value') is not None |
+ |
+ |
+class BooleanPref(BasePref): |
+ """Class for getting/setting a preference with a boolean value. |
+ |
+ The underlying xml element has the form, e.g.: |
+ <boolean name="featureEnabled" value="false" /> |
+ """ |
+ tag_name = 'boolean' |
+ VALUES = {'true': True, 'false': False} |
+ |
+ def get(self): |
+ """Get the value as a Python bool.""" |
+ return type(self).VALUES[super(BooleanPref, self).get()] |
+ |
+ def set(self, value): |
+ """Set from a value casted as a bool.""" |
+ super(BooleanPref, self).set('true' if value else 'false') |
+ |
+ |
+class FloatPref(BasePref): |
+ """Class for getting/setting a preference with a float value. |
+ |
+ The underlying xml element has the form, e.g.: |
+ <float name="someMetric" value="4.7" /> |
+ """ |
+ tag_name = 'float' |
+ |
+ def get(self): |
+ """Get the value as a Python float.""" |
+ return float(super(FloatPref, self).get()) |
+ |
+ |
+class IntPref(BasePref): |
+ """Class for getting/setting a preference with an int value. |
+ |
+ The underlying xml element has the form, e.g.: |
+ <int name="aCounter" value="1234" /> |
+ """ |
+ tag_name = 'int' |
+ |
+ def get(self): |
+ """Get the value as a Python int.""" |
+ return int(super(IntPref, self).get()) |
+ |
+ |
+class LongPref(IntPref): |
+ """Class for getting/setting a preference with a long value. |
+ |
+ The underlying xml element has the form, e.g.: |
+ <long name="aLongCounter" value="1234" /> |
+ |
+ We use the same implementation from IntPref. |
+ """ |
+ tag_name = 'long' |
+ |
+ |
+class StringPref(BasePref): |
+ """Class for getting/setting a preference with a string value. |
+ |
+ The underlying xml element has the form, e.g.: |
+ <string name="someHashValue">249b3e5af13d4db2</string> |
+ """ |
+ tag_name = 'string' |
+ |
+ def get(self): |
+ """Get the value as a Python string.""" |
+ return self._elem.text |
+ |
+ def set(self, value): |
+ """Set from a value casted as a string.""" |
+ self._elem.text = str(value) |
+ |
+ |
+class StringSetPref(StringPref): |
+ """Class for getting/setting a preference with a set of string values. |
+ |
+ The underlying xml element has the form, e.g.: |
+ <set name="managed_apps"> |
+ <string>com.mine.app1</string> |
+ <string>com.mine.app2</string> |
+ <string>com.mine.app3</string> |
+ </set> |
+ """ |
+ tag_name = 'set' |
+ |
+ def get(self): |
+ """Get a list with the string values contained.""" |
+ value = [] |
+ for child in self._elem: |
+ assert child.tag == 'string' |
+ value.append(child.text) |
+ return value |
+ |
+ def set(self, value): |
+ """Set from a sequence of values, each casted as a string.""" |
+ for child in list(self._elem): |
+ self._elem.remove(child) |
+ for item in value: |
+ ElementTree.SubElement(self._elem, 'string').text = str(item) |
+ |
+ |
+_PREF_TYPES = {c.tag_name: c for c in [BooleanPref, FloatPref, IntPref, |
+ LongPref, StringPref, StringSetPref]} |
+ |
+ |
+class SharedPrefs(object): |
+ def __init__(self, device, package, filename): |
+ """Helper object to read and update "Shared Prefs" of Android apps. |
+ |
+ Such files typically look like, e.g.: |
+ |
+ <?xml version='1.0' encoding='utf-8' standalone='yes' ?> |
+ <map> |
+ <int name="databaseVersion" value="107" /> |
+ <boolean name="featureEnabled" value="false" /> |
+ <string name="someHashValue">249b3e5af13d4db2</string> |
+ </map> |
+ |
+ Example usage: |
+ |
+ prefs = shared_prefs.SharedPrefs(device, 'com.my.app', 'my_prefs.xml') |
+ prefs.Load() |
+ prefs.GetString('someHashValue') # => '249b3e5af13d4db2' |
+ prefs.SetInt('databaseVersion', 42) |
+ prefs.Remove('featureEnabled') |
+ prefs.Commit() |
+ |
+ The object may also be used as a context manager to automatically load and |
+ commit, respectively, upon entering and leaving the context. |
+ |
+ Args: |
+ device: A DeviceUtils object. |
+ 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. |
+ """ |
+ self._device = device |
+ self._xml = None |
+ self._package = package |
+ self._filename = filename |
+ self._path = '/data/data/%s/shared_prefs/%s' % (package, filename) |
+ self._changed = False |
+ |
+ def __repr__(self): |
+ """Get a useful printable representation of the object.""" |
+ return '<{cls} file {filename} for {package} on {device}>'.format( |
+ cls=type(self).__name__, filename=self.filename, package=self.package, |
+ device=str(self._device)) |
+ |
+ def __str__(self): |
+ """Get the underlying xml document as a string.""" |
+ return _XML_DECLARATION + ElementTree.tostring(self.xml) |
+ |
+ @property |
+ def package(self): |
+ """Get the package name of the app that owns the shared preferences.""" |
+ return self._package |
+ |
+ @property |
+ def filename(self): |
+ """Get the filename of the shared preferences file.""" |
+ return self._filename |
+ |
+ @property |
+ def path(self): |
+ """Get the full path to the shared preferences file on the device.""" |
+ return self._path |
+ |
+ @property |
+ def changed(self): |
+ """True if properties have changed and a commit would be needed.""" |
+ return self._changed |
+ |
+ @property |
+ def xml(self): |
+ """Get the underlying xml document as an ElementTree object.""" |
+ if self._xml is None: |
+ self._xml = ElementTree.Element('map') |
+ return self._xml |
+ |
+ def Load(self): |
+ """Load the shared preferences file from the device. |
+ |
+ 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): |
+ self._xml = ElementTree.fromstring( |
+ self._device.ReadFile(self.path, as_root=True)) |
+ assert self._xml.tag == 'map' |
+ else: |
+ self._xml = None |
+ self._changed = False |
+ |
+ def Clear(self): |
+ """Clear all of the preferences contained in this object.""" |
+ if self._xml is not None and len(self): # only clear if not already empty |
+ self._xml = None |
+ self._changed = True |
+ |
+ def Commit(self): |
+ """Save the current set of preferences to the device. |
+ |
+ Only actually saves if some preferences have been modified. |
+ """ |
+ if not self.changed: |
+ return |
+ self._device.RunShellCommand( |
+ ['mkdir', '-p', posixpath.dirname(self.path)], |
+ as_root=True, check_return=True) |
+ self._device.WriteFile(self.path, str(self), as_root=True) |
+ self._device.KillAll(self.package, as_root=True, quiet=True) |
+ self._changed = False |
+ |
+ def __len__(self): |
+ """Get the number of preferences in this collection.""" |
+ return len(self.xml) |
+ |
+ def PropertyType(self, key): |
+ """Get the type (i.e. tag name) of a property in the collection.""" |
+ return self._GetChild(key).tag |
+ |
+ def HasProperty(self, key): |
+ try: |
+ self._GetChild(key) |
+ return True |
+ except KeyError: |
+ return False |
+ |
+ def GetBoolean(self, key): |
+ """Get a boolean property.""" |
+ return BooleanPref(self._GetChild(key)).get() |
+ |
+ def SetBoolean(self, key, value): |
+ """Set a boolean property.""" |
+ self._SetPrefValue(key, value, BooleanPref) |
+ |
+ def GetFloat(self, key): |
+ """Get a float property.""" |
+ return FloatPref(self._GetChild(key)).get() |
+ |
+ def SetFloat(self, key, value): |
+ """Set a float property.""" |
+ self._SetPrefValue(key, value, FloatPref) |
+ |
+ def GetInt(self, key): |
+ """Get an int property.""" |
+ return IntPref(self._GetChild(key)).get() |
+ |
+ def SetInt(self, key, value): |
+ """Set an int property.""" |
+ self._SetPrefValue(key, value, IntPref) |
+ |
+ def GetLong(self, key): |
+ """Get a long property.""" |
+ return LongPref(self._GetChild(key)).get() |
+ |
+ def SetLong(self, key, value): |
+ """Set a long property.""" |
+ self._SetPrefValue(key, value, LongPref) |
+ |
+ def GetString(self, key): |
+ """Get a string property.""" |
+ return StringPref(self._GetChild(key)).get() |
+ |
+ def SetString(self, key, value): |
+ """Set a string property.""" |
+ self._SetPrefValue(key, value, StringPref) |
+ |
+ def GetStringSet(self, key): |
+ """Get a string set property.""" |
+ return StringSetPref(self._GetChild(key)).get() |
+ |
+ def SetStringSet(self, key, value): |
+ """Set a string set property.""" |
+ self._SetPrefValue(key, value, StringSetPref) |
+ |
+ def Remove(self, key): |
+ """Remove a preference from the collection.""" |
+ self.xml.remove(self._GetChild(key)) |
+ |
+ def AsDict(self): |
+ """Return the properties and their values as a dictionary.""" |
+ d = {} |
+ for child in self.xml: |
+ pref = _PREF_TYPES[child.tag](child) |
+ d[child.get('name')] = pref.get() |
+ return d |
+ |
+ def __enter__(self): |
+ """Load preferences file from the device when entering a context.""" |
+ self.Load() |
+ return self |
+ |
+ def __exit__(self, exc_type, _exc_value, _traceback): |
+ """Save preferences file to the device when leaving a context.""" |
+ if not exc_type: |
+ self.Commit() |
+ |
+ def _GetChild(self, key): |
+ """Get the underlying xml node that holds the property of a given key. |
+ |
+ Raises: |
+ KeyError when the key is not found in the collection. |
+ """ |
+ for child in self.xml: |
+ if child.get('name') == key: |
+ return child |
+ raise KeyError(key) |
+ |
+ def _SetPrefValue(self, key, value, pref_cls): |
+ """Set the value of a property. |
+ |
+ Args: |
+ key: The key of the property to set. |
+ value: The new value of the property. |
+ pref_cls: A subclass of BasePref used to access the property. |
+ |
+ Raises: |
+ TypeError when the key already exists but with a different type. |
+ """ |
+ try: |
+ pref = pref_cls(self._GetChild(key)) |
+ old_value = pref.get() |
+ except KeyError: |
+ pref = pref_cls(ElementTree.SubElement( |
+ self.xml, pref_cls.tag_name, {'name': key})) |
+ old_value = None |
+ if old_value != value: |
+ pref.set(value) |
+ self._changed = True |
+ logging.info('Setting property: %s', pref) |