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