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
13from xml.etree import ElementTree
14
15from devil.android import device_errors
16from devil.android.sdk import version_codes
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, use_encrypted_path=False):
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      use_encrypted_path: Whether to read and write to the shared prefs location
200        in the device-encrypted path (/data/user_de) instead of the older,
201        unencrypted path (/data/data). Only supported on N+, but falls back to
202        the unencrypted path if the encrypted path is not supported on the given
203        device.
204    """
205    self._device = device
206    self._xml = None
207    self._package = package
208    self._filename = filename
209    self._unencrypted_path = '/data/data/%s/shared_prefs/%s' % (package,
210                                                                filename)
211    self._encrypted_path = '/data/user_de/0/%s/shared_prefs/%s' % (package,
212                                                                   filename)
213    self._path = self._unencrypted_path
214    self._encrypted = use_encrypted_path
215    if use_encrypted_path:
216      if self._device.build_version_sdk < version_codes.NOUGAT:
217        logging.info('SharedPrefs set to use encrypted path, but given device '
218                     'is not running N+. Falling back to unencrypted path')
219        self._encrypted = False
220      else:
221        self._path = self._encrypted_path
222    self._changed = False
223
224  def __repr__(self):
225    """Get a useful printable representation of the object."""
226    return '<{cls} file {filename} for {package} on {device}>'.format(
227      cls=type(self).__name__, filename=self.filename, package=self.package,
228      device=str(self._device))
229
230  def __str__(self):
231    """Get the underlying xml document as a string."""
232    return _XML_DECLARATION + ElementTree.tostring(self.xml)
233
234  @property
235  def package(self):
236    """Get the package name of the app that owns the shared preferences."""
237    return self._package
238
239  @property
240  def filename(self):
241    """Get the filename of the shared preferences file."""
242    return self._filename
243
244  @property
245  def path(self):
246    """Get the full path to the shared preferences file on the device."""
247    return self._path
248
249  @property
250  def changed(self):
251    """True if properties have changed and a commit would be needed."""
252    return self._changed
253
254  @property
255  def xml(self):
256    """Get the underlying xml document as an ElementTree object."""
257    if self._xml is None:
258      self._xml = ElementTree.Element('map')
259    return self._xml
260
261  def Load(self):
262    """Load the shared preferences file from the device.
263
264    A empty xml document, which may be modified and saved on |commit|, is
265    created if the file does not already exist.
266    """
267    if self._device.FileExists(self.path):
268      self._xml = ElementTree.fromstring(
269          self._device.ReadFile(self.path, as_root=True))
270      assert self._xml.tag == 'map'
271    else:
272      self._xml = None
273    self._changed = False
274
275  def Clear(self):
276    """Clear all of the preferences contained in this object."""
277    if self._xml is not None and len(self):  # only clear if not already empty
278      self._xml = None
279      self._changed = True
280
281  def Commit(self, force_commit=False):
282    """Save the current set of preferences to the device.
283
284    Only actually saves if some preferences have been modified or force_commit
285    is set to True.
286
287    Args:
288      force_commit: Commit even if no changes have been made to the SharedPrefs
289        instance.
290    """
291    if not (self.changed or force_commit):
292      return
293    self._device.RunShellCommand(
294        ['mkdir', '-p', posixpath.dirname(self.path)],
295        as_root=True, check_return=True)
296    self._device.WriteFile(self.path, str(self), as_root=True)
297    # Creating the directory/file can cause issues with SELinux if they did
298    # not already exist. As a workaround, apply the package's security context
299    # to the shared_prefs directory, which mimics the behavior of a file
300    # created by the app itself
301    if self._device.build_version_sdk >= version_codes.MARSHMALLOW:
302      security_context = self._device.GetSecurityContextForPackage(self.package,
303          encrypted=self._encrypted)
304      if security_context is None:
305        raise device_errors.CommandFailedError(
306            'Failed to get security context for %s' % self.package)
307      paths = [posixpath.dirname(self.path), self.path]
308      self._device.ChangeSecurityContext(security_context, paths)
309
310    # Ensure that there isn't both an encrypted and unencrypted version of the
311    # file on the device at the same time.
312    if self._device.build_version_sdk >= version_codes.NOUGAT:
313      remove_path = (self._unencrypted_path if self._encrypted
314                     else self._encrypted_path)
315      if self._device.PathExists(remove_path, as_root=True):
316        logging.warning('Found an equivalent shared prefs file at %s, removing',
317            remove_path)
318        self._device.RemovePath(remove_path, as_root=True)
319
320    self._device.KillAll(self.package, exact=True, as_root=True, quiet=True)
321    self._changed = False
322
323  def __len__(self):
324    """Get the number of preferences in this collection."""
325    return len(self.xml)
326
327  def PropertyType(self, key):
328    """Get the type (i.e. tag name) of a property in the collection."""
329    return self._GetChild(key).tag
330
331  def HasProperty(self, key):
332    try:
333      self._GetChild(key)
334      return True
335    except KeyError:
336      return False
337
338  def GetBoolean(self, key):
339    """Get a boolean property."""
340    return BooleanPref(self._GetChild(key)).get()
341
342  def SetBoolean(self, key, value):
343    """Set a boolean property."""
344    self._SetPrefValue(key, value, BooleanPref)
345
346  def GetFloat(self, key):
347    """Get a float property."""
348    return FloatPref(self._GetChild(key)).get()
349
350  def SetFloat(self, key, value):
351    """Set a float property."""
352    self._SetPrefValue(key, value, FloatPref)
353
354  def GetInt(self, key):
355    """Get an int property."""
356    return IntPref(self._GetChild(key)).get()
357
358  def SetInt(self, key, value):
359    """Set an int property."""
360    self._SetPrefValue(key, value, IntPref)
361
362  def GetLong(self, key):
363    """Get a long property."""
364    return LongPref(self._GetChild(key)).get()
365
366  def SetLong(self, key, value):
367    """Set a long property."""
368    self._SetPrefValue(key, value, LongPref)
369
370  def GetString(self, key):
371    """Get a string property."""
372    return StringPref(self._GetChild(key)).get()
373
374  def SetString(self, key, value):
375    """Set a string property."""
376    self._SetPrefValue(key, value, StringPref)
377
378  def GetStringSet(self, key):
379    """Get a string set property."""
380    return StringSetPref(self._GetChild(key)).get()
381
382  def SetStringSet(self, key, value):
383    """Set a string set property."""
384    self._SetPrefValue(key, value, StringSetPref)
385
386  def Remove(self, key):
387    """Remove a preference from the collection."""
388    self.xml.remove(self._GetChild(key))
389
390  def AsDict(self):
391    """Return the properties and their values as a dictionary."""
392    d = {}
393    for child in self.xml:
394      pref = _PREF_TYPES[child.tag](child)
395      d[child.get('name')] = pref.get()
396    return d
397
398  def __enter__(self):
399    """Load preferences file from the device when entering a context."""
400    self.Load()
401    return self
402
403  def __exit__(self, exc_type, _exc_value, _traceback):
404    """Save preferences file to the device when leaving a context."""
405    if not exc_type:
406      self.Commit()
407
408  def _GetChild(self, key):
409    """Get the underlying xml node that holds the property of a given key.
410
411    Raises:
412      KeyError when the key is not found in the collection.
413    """
414    for child in self.xml:
415      if child.get('name') == key:
416        return child
417    raise KeyError(key)
418
419  def _SetPrefValue(self, key, value, pref_cls):
420    """Set the value of a property.
421
422    Args:
423      key: The key of the property to set.
424      value: The new value of the property.
425      pref_cls: A subclass of BasePref used to access the property.
426
427    Raises:
428      TypeError when the key already exists but with a different type.
429    """
430    try:
431      pref = pref_cls(self._GetChild(key))
432      old_value = pref.get()
433    except KeyError:
434      pref = pref_cls(ElementTree.SubElement(
435          self.xml, pref_cls.tag_name, {'name': key}))
436      old_value = None
437    if old_value != value:
438      pref.set(value)
439      self._changed = True
440      logger.info('Setting property: %s', pref)
441