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