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