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 xml.etree import ElementTree 15 16 17_XML_DECLARATION = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" 18 19 20class BasePref(object): 21 """Base class for getting/setting the value of a specific preference type. 22 23 Should not be instantiated directly. The SharedPrefs collection will 24 instantiate the appropriate subclasses, which directly manipulate the 25 underlying xml document, to parse and serialize values according to their 26 type. 27 28 Args: 29 elem: An xml ElementTree object holding the preference data. 30 31 Properties: 32 tag_name: A string with the tag that must be used for this preference type. 33 """ 34 tag_name = None 35 36 def __init__(self, elem): 37 if elem.tag != type(self).tag_name: 38 raise TypeError('Property %r has type %r, but trying to access as %r' % 39 (elem.get('name'), elem.tag, type(self).tag_name)) 40 self._elem = elem 41 42 def __str__(self): 43 """Get the underlying xml element as a string.""" 44 return ElementTree.tostring(self._elem) 45 46 def get(self): 47 """Get the value of this preference.""" 48 return self._elem.get('value') 49 50 def set(self, value): 51 """Set from a value casted as a string.""" 52 self._elem.set('value', str(value)) 53 54 @property 55 def has_value(self): 56 """Check whether the element has a value.""" 57 return self._elem.get('value') is not None 58 59 60class BooleanPref(BasePref): 61 """Class for getting/setting a preference with a boolean value. 62 63 The underlying xml element has the form, e.g.: 64 <boolean name="featureEnabled" value="false" /> 65 """ 66 tag_name = 'boolean' 67 VALUES = {'true': True, 'false': False} 68 69 def get(self): 70 """Get the value as a Python bool.""" 71 return type(self).VALUES[super(BooleanPref, self).get()] 72 73 def set(self, value): 74 """Set from a value casted as a bool.""" 75 super(BooleanPref, self).set('true' if value else 'false') 76 77 78class FloatPref(BasePref): 79 """Class for getting/setting a preference with a float value. 80 81 The underlying xml element has the form, e.g.: 82 <float name="someMetric" value="4.7" /> 83 """ 84 tag_name = 'float' 85 86 def get(self): 87 """Get the value as a Python float.""" 88 return float(super(FloatPref, self).get()) 89 90 91class IntPref(BasePref): 92 """Class for getting/setting a preference with an int value. 93 94 The underlying xml element has the form, e.g.: 95 <int name="aCounter" value="1234" /> 96 """ 97 tag_name = 'int' 98 99 def get(self): 100 """Get the value as a Python int.""" 101 return int(super(IntPref, self).get()) 102 103 104class LongPref(IntPref): 105 """Class for getting/setting a preference with a long value. 106 107 The underlying xml element has the form, e.g.: 108 <long name="aLongCounter" value="1234" /> 109 110 We use the same implementation from IntPref. 111 """ 112 tag_name = 'long' 113 114 115class StringPref(BasePref): 116 """Class for getting/setting a preference with a string value. 117 118 The underlying xml element has the form, e.g.: 119 <string name="someHashValue">249b3e5af13d4db2</string> 120 """ 121 tag_name = 'string' 122 123 def get(self): 124 """Get the value as a Python string.""" 125 return self._elem.text 126 127 def set(self, value): 128 """Set from a value casted as a string.""" 129 self._elem.text = str(value) 130 131 132class StringSetPref(StringPref): 133 """Class for getting/setting a preference with a set of string values. 134 135 The underlying xml element has the form, e.g.: 136 <set name="managed_apps"> 137 <string>com.mine.app1</string> 138 <string>com.mine.app2</string> 139 <string>com.mine.app3</string> 140 </set> 141 """ 142 tag_name = 'set' 143 144 def get(self): 145 """Get a list with the string values contained.""" 146 value = [] 147 for child in self._elem: 148 assert child.tag == 'string' 149 value.append(child.text) 150 return value 151 152 def set(self, value): 153 """Set from a sequence of values, each casted as a string.""" 154 for child in list(self._elem): 155 self._elem.remove(child) 156 for item in value: 157 ElementTree.SubElement(self._elem, 'string').text = str(item) 158 159 160_PREF_TYPES = {c.tag_name: c for c in [BooleanPref, FloatPref, IntPref, 161 LongPref, StringPref, StringSetPref]} 162 163 164class SharedPrefs(object): 165 166 def __init__(self, device, package, filename): 167 """Helper object to read and update "Shared Prefs" of Android apps. 168 169 Such files typically look like, e.g.: 170 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> 177 178 Example usage: 179 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() 186 187 The object may also be used as a context manager to automatically load and 188 commit, respectively, upon entering and leaving the context. 189 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. 195 """ 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 202 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)) 208 209 def __str__(self): 210 """Get the underlying xml document as a string.""" 211 return _XML_DECLARATION + ElementTree.tostring(self.xml) 212 213 @property 214 def package(self): 215 """Get the package name of the app that owns the shared preferences.""" 216 return self._package 217 218 @property 219 def filename(self): 220 """Get the filename of the shared preferences file.""" 221 return self._filename 222 223 @property 224 def path(self): 225 """Get the full path to the shared preferences file on the device.""" 226 return self._path 227 228 @property 229 def changed(self): 230 """True if properties have changed and a commit would be needed.""" 231 return self._changed 232 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 239 240 def Load(self): 241 """Load the shared preferences file from the device. 242 243 A empty xml document, which may be modified and saved on |commit|, is 244 created if the file does not already exist. 245 """ 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 253 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 259 260 def Commit(self): 261 """Save the current set of preferences to the device. 262 263 Only actually saves if some preferences have been modified. 264 """ 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, exact=True, as_root=True, quiet=True) 272 self._changed = False 273 274 def __len__(self): 275 """Get the number of preferences in this collection.""" 276 return len(self.xml) 277 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 281 282 def HasProperty(self, key): 283 try: 284 self._GetChild(key) 285 return True 286 except KeyError: 287 return False 288 289 def GetBoolean(self, key): 290 """Get a boolean property.""" 291 return BooleanPref(self._GetChild(key)).get() 292 293 def SetBoolean(self, key, value): 294 """Set a boolean property.""" 295 self._SetPrefValue(key, value, BooleanPref) 296 297 def GetFloat(self, key): 298 """Get a float property.""" 299 return FloatPref(self._GetChild(key)).get() 300 301 def SetFloat(self, key, value): 302 """Set a float property.""" 303 self._SetPrefValue(key, value, FloatPref) 304 305 def GetInt(self, key): 306 """Get an int property.""" 307 return IntPref(self._GetChild(key)).get() 308 309 def SetInt(self, key, value): 310 """Set an int property.""" 311 self._SetPrefValue(key, value, IntPref) 312 313 def GetLong(self, key): 314 """Get a long property.""" 315 return LongPref(self._GetChild(key)).get() 316 317 def SetLong(self, key, value): 318 """Set a long property.""" 319 self._SetPrefValue(key, value, LongPref) 320 321 def GetString(self, key): 322 """Get a string property.""" 323 return StringPref(self._GetChild(key)).get() 324 325 def SetString(self, key, value): 326 """Set a string property.""" 327 self._SetPrefValue(key, value, StringPref) 328 329 def GetStringSet(self, key): 330 """Get a string set property.""" 331 return StringSetPref(self._GetChild(key)).get() 332 333 def SetStringSet(self, key, value): 334 """Set a string set property.""" 335 self._SetPrefValue(key, value, StringSetPref) 336 337 def Remove(self, key): 338 """Remove a preference from the collection.""" 339 self.xml.remove(self._GetChild(key)) 340 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 348 349 def __enter__(self): 350 """Load preferences file from the device when entering a context.""" 351 self.Load() 352 return self 353 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() 358 359 def _GetChild(self, key): 360 """Get the underlying xml node that holds the property of a given key. 361 362 Raises: 363 KeyError when the key is not found in the collection. 364 """ 365 for child in self.xml: 366 if child.get('name') == key: 367 return child 368 raise KeyError(key) 369 370 def _SetPrefValue(self, key, value, pref_cls): 371 """Set the value of a property. 372 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. 377 378 Raises: 379 TypeError when the key already exists but with a different type. 380 """ 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) 392