Home | History | Annotate | Download | only in sdk
      1 # Copyright 2015 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """Helper object to read and modify Shared Preferences from Android apps.
      6 
      7 See e.g.:
      8   http://developer.android.com/reference/android/content/SharedPreferences.html
      9 """
     10 
     11 import logging
     12 import posixpath
     13 
     14 from xml.etree import ElementTree
     15 
     16 
     17 _XML_DECLARATION = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
     18 
     19 
     20 class BasePref(object):
     21   """Base class for getting/setting the value of a specific preference type.
     22 
     23   Should not be instantiated directly. The SharedPrefs collection will
     24   instantiate the appropriate subclasses, which directly manipulate the
     25   underlying xml document, to parse and serialize values according to their
     26   type.
     27 
     28   Args:
     29     elem: An xml ElementTree object holding the preference data.
     30 
     31   Properties:
     32     tag_name: A string with the tag that must be used for this preference type.
     33   """
     34   tag_name = None
     35 
     36   def __init__(self, elem):
     37     if elem.tag != type(self).tag_name:
     38       raise TypeError('Property %r has type %r, but trying to access as %r' %
     39                       (elem.get('name'), elem.tag, type(self).tag_name))
     40     self._elem = elem
     41 
     42   def __str__(self):
     43     """Get the underlying xml element as a string."""
     44     return ElementTree.tostring(self._elem)
     45 
     46   def get(self):
     47     """Get the value of this preference."""
     48     return self._elem.get('value')
     49 
     50   def set(self, value):
     51     """Set from a value casted as a string."""
     52     self._elem.set('value', str(value))
     53 
     54   @property
     55   def has_value(self):
     56     """Check whether the element has a value."""
     57     return self._elem.get('value') is not None
     58 
     59 
     60 class BooleanPref(BasePref):
     61   """Class for getting/setting a preference with a boolean value.
     62 
     63   The underlying xml element has the form, e.g.:
     64       <boolean name="featureEnabled" value="false" />
     65   """
     66   tag_name = 'boolean'
     67   VALUES = {'true': True, 'false': False}
     68 
     69   def get(self):
     70     """Get the value as a Python bool."""
     71     return type(self).VALUES[super(BooleanPref, self).get()]
     72 
     73   def set(self, value):
     74     """Set from a value casted as a bool."""
     75     super(BooleanPref, self).set('true' if value else 'false')
     76 
     77 
     78 class FloatPref(BasePref):
     79   """Class for getting/setting a preference with a float value.
     80 
     81   The underlying xml element has the form, e.g.:
     82       <float name="someMetric" value="4.7" />
     83   """
     84   tag_name = 'float'
     85 
     86   def get(self):
     87     """Get the value as a Python float."""
     88     return float(super(FloatPref, self).get())
     89 
     90 
     91 class IntPref(BasePref):
     92   """Class for getting/setting a preference with an int value.
     93 
     94   The underlying xml element has the form, e.g.:
     95       <int name="aCounter" value="1234" />
     96   """
     97   tag_name = 'int'
     98 
     99   def get(self):
    100     """Get the value as a Python int."""
    101     return int(super(IntPref, self).get())
    102 
    103 
    104 class LongPref(IntPref):
    105   """Class for getting/setting a preference with a long value.
    106 
    107   The underlying xml element has the form, e.g.:
    108       <long name="aLongCounter" value="1234" />
    109 
    110   We use the same implementation from IntPref.
    111   """
    112   tag_name = 'long'
    113 
    114 
    115 class StringPref(BasePref):
    116   """Class for getting/setting a preference with a string value.
    117 
    118   The underlying xml element has the form, e.g.:
    119       <string name="someHashValue">249b3e5af13d4db2</string>
    120   """
    121   tag_name = 'string'
    122 
    123   def get(self):
    124     """Get the value as a Python string."""
    125     return self._elem.text
    126 
    127   def set(self, value):
    128     """Set from a value casted as a string."""
    129     self._elem.text = str(value)
    130 
    131 
    132 class StringSetPref(StringPref):
    133   """Class for getting/setting a preference with a set of string values.
    134 
    135   The underlying xml element has the form, e.g.:
    136       <set name="managed_apps">
    137           <string>com.mine.app1</string>
    138           <string>com.mine.app2</string>
    139           <string>com.mine.app3</string>
    140       </set>
    141   """
    142   tag_name = 'set'
    143 
    144   def get(self):
    145     """Get a list with the string values contained."""
    146     value = []
    147     for child in self._elem:
    148       assert child.tag == 'string'
    149       value.append(child.text)
    150     return value
    151 
    152   def set(self, value):
    153     """Set from a sequence of values, each casted as a string."""
    154     for child in list(self._elem):
    155       self._elem.remove(child)
    156     for item in value:
    157       ElementTree.SubElement(self._elem, 'string').text = str(item)
    158 
    159 
    160 _PREF_TYPES = {c.tag_name: c for c in [BooleanPref, FloatPref, IntPref,
    161                                        LongPref, StringPref, StringSetPref]}
    162 
    163 
    164 class SharedPrefs(object):
    165 
    166   def __init__(self, device, package, filename):
    167     """Helper object to read and update "Shared Prefs" of Android apps.
    168 
    169     Such files typically look like, e.g.:
    170 
    171         <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
    172         <map>
    173           <int name="databaseVersion" value="107" />
    174           <boolean name="featureEnabled" value="false" />
    175           <string name="someHashValue">249b3e5af13d4db2</string>
    176         </map>
    177 
    178     Example usage:
    179 
    180         prefs = shared_prefs.SharedPrefs(device, 'com.my.app', 'my_prefs.xml')
    181         prefs.Load()
    182         prefs.GetString('someHashValue') # => '249b3e5af13d4db2'
    183         prefs.SetInt('databaseVersion', 42)
    184         prefs.Remove('featureEnabled')
    185         prefs.Commit()
    186 
    187     The object may also be used as a context manager to automatically load and
    188     commit, respectively, upon entering and leaving the context.
    189 
    190     Args:
    191       device: A DeviceUtils object.
    192       package: A string with the package name of the app that owns the shared
    193         preferences file.
    194       filename: A string with the name of the preferences file to read/write.
    195     """
    196     self._device = device
    197     self._xml = None
    198     self._package = package
    199     self._filename = filename
    200     self._path = '/data/data/%s/shared_prefs/%s' % (package, filename)
    201     self._changed = False
    202 
    203   def __repr__(self):
    204     """Get a useful printable representation of the object."""
    205     return '<{cls} file {filename} for {package} on {device}>'.format(
    206       cls=type(self).__name__, filename=self.filename, package=self.package,
    207       device=str(self._device))
    208 
    209   def __str__(self):
    210     """Get the underlying xml document as a string."""
    211     return _XML_DECLARATION + ElementTree.tostring(self.xml)
    212 
    213   @property
    214   def package(self):
    215     """Get the package name of the app that owns the shared preferences."""
    216     return self._package
    217 
    218   @property
    219   def filename(self):
    220     """Get the filename of the shared preferences file."""
    221     return self._filename
    222 
    223   @property
    224   def path(self):
    225     """Get the full path to the shared preferences file on the device."""
    226     return self._path
    227 
    228   @property
    229   def changed(self):
    230     """True if properties have changed and a commit would be needed."""
    231     return self._changed
    232 
    233   @property
    234   def xml(self):
    235     """Get the underlying xml document as an ElementTree object."""
    236     if self._xml is None:
    237       self._xml = ElementTree.Element('map')
    238     return self._xml
    239 
    240   def Load(self):
    241     """Load the shared preferences file from the device.
    242 
    243     A empty xml document, which may be modified and saved on |commit|, is
    244     created if the file does not already exist.
    245     """
    246     if self._device.FileExists(self.path):
    247       self._xml = ElementTree.fromstring(
    248           self._device.ReadFile(self.path, as_root=True))
    249       assert self._xml.tag == 'map'
    250     else:
    251       self._xml = None
    252     self._changed = False
    253 
    254   def Clear(self):
    255     """Clear all of the preferences contained in this object."""
    256     if self._xml is not None and len(self):  # only clear if not already empty
    257       self._xml = None
    258       self._changed = True
    259 
    260   def Commit(self):
    261     """Save the current set of preferences to the device.
    262 
    263     Only actually saves if some preferences have been modified.
    264     """
    265     if not self.changed:
    266       return
    267     self._device.RunShellCommand(
    268         ['mkdir', '-p', posixpath.dirname(self.path)],
    269         as_root=True, check_return=True)
    270     self._device.WriteFile(self.path, str(self), as_root=True)
    271     self._device.KillAll(self.package, exact=True, as_root=True, quiet=True)
    272     self._changed = False
    273 
    274   def __len__(self):
    275     """Get the number of preferences in this collection."""
    276     return len(self.xml)
    277 
    278   def PropertyType(self, key):
    279     """Get the type (i.e. tag name) of a property in the collection."""
    280     return self._GetChild(key).tag
    281 
    282   def HasProperty(self, key):
    283     try:
    284       self._GetChild(key)
    285       return True
    286     except KeyError:
    287       return False
    288 
    289   def GetBoolean(self, key):
    290     """Get a boolean property."""
    291     return BooleanPref(self._GetChild(key)).get()
    292 
    293   def SetBoolean(self, key, value):
    294     """Set a boolean property."""
    295     self._SetPrefValue(key, value, BooleanPref)
    296 
    297   def GetFloat(self, key):
    298     """Get a float property."""
    299     return FloatPref(self._GetChild(key)).get()
    300 
    301   def SetFloat(self, key, value):
    302     """Set a float property."""
    303     self._SetPrefValue(key, value, FloatPref)
    304 
    305   def GetInt(self, key):
    306     """Get an int property."""
    307     return IntPref(self._GetChild(key)).get()
    308 
    309   def SetInt(self, key, value):
    310     """Set an int property."""
    311     self._SetPrefValue(key, value, IntPref)
    312 
    313   def GetLong(self, key):
    314     """Get a long property."""
    315     return LongPref(self._GetChild(key)).get()
    316 
    317   def SetLong(self, key, value):
    318     """Set a long property."""
    319     self._SetPrefValue(key, value, LongPref)
    320 
    321   def GetString(self, key):
    322     """Get a string property."""
    323     return StringPref(self._GetChild(key)).get()
    324 
    325   def SetString(self, key, value):
    326     """Set a string property."""
    327     self._SetPrefValue(key, value, StringPref)
    328 
    329   def GetStringSet(self, key):
    330     """Get a string set property."""
    331     return StringSetPref(self._GetChild(key)).get()
    332 
    333   def SetStringSet(self, key, value):
    334     """Set a string set property."""
    335     self._SetPrefValue(key, value, StringSetPref)
    336 
    337   def Remove(self, key):
    338     """Remove a preference from the collection."""
    339     self.xml.remove(self._GetChild(key))
    340 
    341   def AsDict(self):
    342     """Return the properties and their values as a dictionary."""
    343     d = {}
    344     for child in self.xml:
    345       pref = _PREF_TYPES[child.tag](child)
    346       d[child.get('name')] = pref.get()
    347     return d
    348 
    349   def __enter__(self):
    350     """Load preferences file from the device when entering a context."""
    351     self.Load()
    352     return self
    353 
    354   def __exit__(self, exc_type, _exc_value, _traceback):
    355     """Save preferences file to the device when leaving a context."""
    356     if not exc_type:
    357       self.Commit()
    358 
    359   def _GetChild(self, key):
    360     """Get the underlying xml node that holds the property of a given key.
    361 
    362     Raises:
    363       KeyError when the key is not found in the collection.
    364     """
    365     for child in self.xml:
    366       if child.get('name') == key:
    367         return child
    368     raise KeyError(key)
    369 
    370   def _SetPrefValue(self, key, value, pref_cls):
    371     """Set the value of a property.
    372 
    373     Args:
    374       key: The key of the property to set.
    375       value: The new value of the property.
    376       pref_cls: A subclass of BasePref used to access the property.
    377 
    378     Raises:
    379       TypeError when the key already exists but with a different type.
    380     """
    381     try:
    382       pref = pref_cls(self._GetChild(key))
    383       old_value = pref.get()
    384     except KeyError:
    385       pref = pref_cls(ElementTree.SubElement(
    386           self.xml, pref_cls.tag_name, {'name': key}))
    387       old_value = None
    388     if old_value != value:
    389       pref.set(value)
    390       self._changed = True
    391       logging.info('Setting property: %s', pref)
    392