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.
8 http://developer.android.com/reference/android/content/SharedPreferences.html
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
30 elem: An xml ElementTree object holding the preference data.
33 tag_name: A string with the tag that must be used for this preference type.
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
))
44 """Get the underlying xml element as a string."""
45 return ElementTree
.tostring(self
._elem
)
48 """Get the value of this preference."""
49 return self
._elem
.get('value')
52 """Set from a value casted as a string."""
53 self
._elem
.set('value', str(value
))
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" />
68 VALUES
= {'true': True, 'false': False}
71 """Get the value as a Python bool."""
72 return type(self
).VALUES
[super(BooleanPref
, self
).get()]
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" />
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" />
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.
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>
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>
146 """Get a list with the string values contained."""
148 for child
in self
._elem
:
149 assert child
.tag
== 'string'
150 value
.append(child
.text
)
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
)
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' ?>
173 <int name="databaseVersion" value="107" />
174 <boolean name="featureEnabled" value="false" />
175 <string name="someHashValue">249b3e5af13d4db2</string>
180 prefs = shared_prefs.SharedPrefs(device, 'com.my.app', 'my_prefs.xml')
182 prefs.GetString('someHashValue') # => '249b3e5af13d4db2'
183 prefs.SetInt('databaseVersion', 42)
184 prefs.Remove('featureEnabled')
187 The object may also be used as a context manager to automatically load and
188 commit, respectively, upon entering and leaving the context.
191 device: A DeviceUtils object.
192 package: A string with the package name of the app that owns the shared
194 filename: A string with the name of the preferences file to read/write.
196 self
._device
= device
198 self
._package
= package
199 self
._filename
= filename
200 self
._path
= '/data/data/%s/shared_prefs/%s' % (package
, filename
)
201 self
._changed
= False
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
))
210 """Get the underlying xml document as a string."""
211 return _XML_DECLARATION
+ ElementTree
.tostring(self
.xml
)
215 """Get the package name of the app that owns the shared preferences."""
220 """Get the filename of the shared preferences file."""
221 return self
._filename
225 """Get the full path to the shared preferences file on the device."""
230 """True if properties have changed and a commit would be needed."""
235 """Get the underlying xml document as an ElementTree object."""
236 if self
._xml
is None:
237 self
._xml
= ElementTree
.Element('map')
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'
252 self
._changed
= False
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
261 """Save the current set of preferences to the device.
263 Only actually saves if some preferences have been modified.
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
275 """Get the number of preferences in this collection."""
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
):
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
))
342 """Return the properties and their values as a dictionary."""
344 for child
in self
.xml
:
345 pref
= _PREF_TYPES
[child
.tag
](child
)
346 d
[child
.get('name')] = pref
.get()
350 """Load preferences file from the device when entering a context."""
354 def __exit__(self
, exc_type
, _exc_value
, _traceback
):
355 """Save preferences file to the device when leaving a context."""
359 def _GetChild(self
, key
):
360 """Get the underlying xml node that holds the property of a given key.
363 KeyError when the key is not found in the collection.
365 for child
in self
.xml
:
366 if child
.get('name') == key
:
370 def _SetPrefValue(self
, key
, value
, pref_cls
):
371 """Set the value of a property.
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.
379 TypeError when the key already exists but with a different type.
382 pref
= pref_cls(self
._GetChild
(key
))
383 old_value
= pref
.get()
385 pref
= pref_cls(ElementTree
.SubElement(
386 self
.xml
, pref_cls
.tag_name
, {'name': key
}))
388 if old_value
!= value
:
391 logging
.info('Setting property: %s', pref
)