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