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