• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
7See e.g.:
8  http://developer.android.com/reference/android/content/SharedPreferences.html
9"""
10
11import logging
12import posixpath
13
14from xml.etree import ElementTree
15
16
17_XML_DECLARATION = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
18
19
20class 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
60class 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
78class 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
91class 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
104class 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
115class 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
132class 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
164class 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