Add Apps.AppListSearchQueryLength UMA histogram.
[chromium-blink-merge.git] / build / android / pylib / device / shared_prefs.py
blob32cef4b535b87ce87171323fe70793ac2bf23ab6
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.
5 """Helper object to read and modify Shared Preferences from Android apps.
7 See e.g.:
8 http://developer.android.com/reference/android/content/SharedPreferences.html
9 """
11 import collections
12 import logging
13 import posixpath
15 from xml.etree import ElementTree
18 _XML_DECLARATION = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
21 class BasePref(object):
22 """Base class for getting/setting the value of a specific preference type.
24 Should not be instantiated directly. The SharedPrefs collection will
25 instantiate the appropriate subclasses, which directly manipulate the
26 underlying xml document, to parse and serialize values according to their
27 type.
29 Args:
30 elem: An xml ElementTree object holding the preference data.
32 Properties:
33 tag_name: A string with the tag that must be used for this preference type.
34 """
35 tag_name = None
37 def __init__(self, elem):
38 if elem.tag != type(self).tag_name:
39 raise TypeError('Property %r has type %r, but trying to access as %r' %
40 (elem.get('name'), elem.tag, type(self).tag_name))
41 self._elem = elem
43 def __str__(self):
44 """Get the underlying xml element as a string."""
45 return ElementTree.tostring(self._elem)
47 def get(self):
48 """Get the value of this preference."""
49 return self._elem.get('value')
51 def set(self, value):
52 """Set from a value casted as a string."""
53 self._elem.set('value', str(value))
55 @property
56 def has_value(self):
57 """Check whether the element has a value."""
58 return self._elem.get('value') is not None
61 class BooleanPref(BasePref):
62 """Class for getting/setting a preference with a boolean value.
64 The underlying xml element has the form, e.g.:
65 <boolean name="featureEnabled" value="false" />
66 """
67 tag_name = 'boolean'
68 VALUES = {'true': True, 'false': False}
70 def get(self):
71 """Get the value as a Python bool."""
72 return type(self).VALUES[super(BooleanPref, self).get()]
74 def set(self, value):
75 """Set from a value casted as a bool."""
76 super(BooleanPref, self).set('true' if value else 'false')
79 class FloatPref(BasePref):
80 """Class for getting/setting a preference with a float value.
82 The underlying xml element has the form, e.g.:
83 <float name="someMetric" value="4.7" />
84 """
85 tag_name = 'float'
87 def get(self):
88 """Get the value as a Python float."""
89 return float(super(FloatPref, self).get())
92 class IntPref(BasePref):
93 """Class for getting/setting a preference with an int value.
95 The underlying xml element has the form, e.g.:
96 <int name="aCounter" value="1234" />
97 """
98 tag_name = 'int'
100 def get(self):
101 """Get the value as a Python int."""
102 return int(super(IntPref, self).get())
105 class LongPref(IntPref):
106 """Class for getting/setting a preference with a long value.
108 The underlying xml element has the form, e.g.:
109 <long name="aLongCounter" value="1234" />
111 We use the same implementation from IntPref.
113 tag_name = 'long'
116 class StringPref(BasePref):
117 """Class for getting/setting a preference with a string value.
119 The underlying xml element has the form, e.g.:
120 <string name="someHashValue">249b3e5af13d4db2</string>
122 tag_name = 'string'
124 def get(self):
125 """Get the value as a Python string."""
126 return self._elem.text
128 def set(self, value):
129 """Set from a value casted as a string."""
130 self._elem.text = str(value)
133 class StringSetPref(StringPref):
134 """Class for getting/setting a preference with a set of string values.
136 The underlying xml element has the form, e.g.:
137 <set name="managed_apps">
138 <string>com.mine.app1</string>
139 <string>com.mine.app2</string>
140 <string>com.mine.app3</string>
141 </set>
143 tag_name = 'set'
145 def get(self):
146 """Get a list with the string values contained."""
147 value = []
148 for child in self._elem:
149 assert child.tag == 'string'
150 value.append(child.text)
151 return value
153 def set(self, value):
154 """Set from a sequence of values, each casted as a string."""
155 for child in list(self._elem):
156 self._elem.remove(child)
157 for item in value:
158 ElementTree.SubElement(self._elem, 'string').text = str(item)
161 _PREF_TYPES = {c.tag_name: c for c in [BooleanPref, FloatPref, IntPref,
162 LongPref, StringPref, StringSetPref]}
165 class SharedPrefs(object):
166 def __init__(self, device, package, filename):
167 """Helper object to read and update "Shared Prefs" of Android apps.
169 Such files typically look like, e.g.:
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>
178 Example usage:
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()
187 The object may also be used as a context manager to automatically load and
188 commit, respectively, upon entering and leaving the context.
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.
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
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))
209 def __str__(self):
210 """Get the underlying xml document as a string."""
211 return _XML_DECLARATION + ElementTree.tostring(self.xml)
213 @property
214 def package(self):
215 """Get the package name of the app that owns the shared preferences."""
216 return self._package
218 @property
219 def filename(self):
220 """Get the filename of the shared preferences file."""
221 return self._filename
223 @property
224 def path(self):
225 """Get the full path to the shared preferences file on the device."""
226 return self._path
228 @property
229 def changed(self):
230 """True if properties have changed and a commit would be needed."""
231 return self._changed
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
240 def Load(self):
241 """Load the shared preferences file from the device.
243 A empty xml document, which may be modified and saved on |commit|, is
244 created if the file does not already exist.
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
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
260 def Commit(self):
261 """Save the current set of preferences to the device.
263 Only actually saves if some preferences have been modified.
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, as_root=True, quiet=True)
272 self._changed = False
274 def __len__(self):
275 """Get the number of preferences in this collection."""
276 return len(self.xml)
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
282 def HasProperty(self, key):
283 try:
284 self._GetChild(key)
285 return True
286 except KeyError:
287 return False
289 def GetBoolean(self, key):
290 """Get a boolean property."""
291 return BooleanPref(self._GetChild(key)).get()
293 def SetBoolean(self, key, value):
294 """Set a boolean property."""
295 self._SetPrefValue(key, value, BooleanPref)
297 def GetFloat(self, key):
298 """Get a float property."""
299 return FloatPref(self._GetChild(key)).get()
301 def SetFloat(self, key, value):
302 """Set a float property."""
303 self._SetPrefValue(key, value, FloatPref)
305 def GetInt(self, key):
306 """Get an int property."""
307 return IntPref(self._GetChild(key)).get()
309 def SetInt(self, key, value):
310 """Set an int property."""
311 self._SetPrefValue(key, value, IntPref)
313 def GetLong(self, key):
314 """Get a long property."""
315 return LongPref(self._GetChild(key)).get()
317 def SetLong(self, key, value):
318 """Set a long property."""
319 self._SetPrefValue(key, value, LongPref)
321 def GetString(self, key):
322 """Get a string property."""
323 return StringPref(self._GetChild(key)).get()
325 def SetString(self, key, value):
326 """Set a string property."""
327 self._SetPrefValue(key, value, StringPref)
329 def GetStringSet(self, key):
330 """Get a string set property."""
331 return StringSetPref(self._GetChild(key)).get()
333 def SetStringSet(self, key, value):
334 """Set a string set property."""
335 self._SetPrefValue(key, value, StringSetPref)
337 def Remove(self, key):
338 """Remove a preference from the collection."""
339 self.xml.remove(self._GetChild(key))
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
349 def __enter__(self):
350 """Load preferences file from the device when entering a context."""
351 self.Load()
352 return self
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()
359 def _GetChild(self, key):
360 """Get the underlying xml node that holds the property of a given key.
362 Raises:
363 KeyError when the key is not found in the collection.
365 for child in self.xml:
366 if child.get('name') == key:
367 return child
368 raise KeyError(key)
370 def _SetPrefValue(self, key, value, pref_cls):
371 """Set the value of a property.
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.
378 Raises:
379 TypeError when the key already exists but with a different type.
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)