Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(4)

Side by Side Diff: build/android/pylib/android_commands.py

Issue 99713002: Factor out a system_properties interface for interacting with getprop/setprop. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Use shlex.split() Created 7 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be 2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file. 3 # found in the LICENSE file.
4 4
5 """Provides an interface to communicate with the device via the adb command. 5 """Provides an interface to communicate with the device via the adb command.
6 6
7 Assumes adb binary is currently on system path. 7 Assumes adb binary is currently on system path.
8 """ 8 """
9 9
10 import collections 10 import collections
11 import datetime 11 import datetime
12 import inspect
12 import logging 13 import logging
13 import os 14 import os
14 import re 15 import re
15 import shlex 16 import shlex
16 import signal 17 import signal
17 import subprocess 18 import subprocess
18 import sys 19 import sys
19 import tempfile 20 import tempfile
20 import time 21 import time
21 22
22 import cmd_helper 23 import cmd_helper
23 import constants 24 import constants
24 import screenshot 25 import screenshot
26 import system_properties
25 27
26 from utils import host_path_finder 28 from utils import host_path_finder
27 29
28 try: 30 try:
29 from pylib import pexpect 31 from pylib import pexpect
30 except: 32 except:
31 pexpect = None 33 pexpect = None
32 34
33 sys.path.append(os.path.join( 35 sys.path.append(os.path.join(
34 constants.DIR_SOURCE_ROOT, 'third_party', 'android_testrunner')) 36 constants.DIR_SOURCE_ROOT, 'third_party', 'android_testrunner'))
(...skipping 186 matching lines...) Expand 10 before | Expand all | Expand 10 after
221 """Returns the timestamp of the given |log_line| in the given year.""" 223 """Returns the timestamp of the given |log_line| in the given year."""
222 try: 224 try:
223 return datetime.datetime.strptime('%s-%s' % (year, log_line[:18]), 225 return datetime.datetime.strptime('%s-%s' % (year, log_line[:18]),
224 '%Y-%m-%d %H:%M:%S.%f') 226 '%Y-%m-%d %H:%M:%S.%f')
225 except (ValueError, IndexError): 227 except (ValueError, IndexError):
226 logging.critical('Error reading timestamp from ' + log_line) 228 logging.critical('Error reading timestamp from ' + log_line)
227 return None 229 return None
228 230
229 231
230 class AndroidCommands(object): 232 class AndroidCommands(object):
231 """Helper class for communicating with Android device via adb. 233 """Helper class for communicating with Android device via adb."""
232 234
233 Args: 235 def __init__(self, device=None, api_strict_mode=False):
234 device: If given, adb commands are only send to the device of this ID. 236 """Constructor.
235 Otherwise commands are sent to all attached devices.
236 """
237 237
238 def __init__(self, device=None): 238 Args:
239 device: If given, adb commands are only send to the device of this ID.
240 Otherwise commands are sent to all attached devices.
241 api_strict_mode: A boolean indicating whether fatal errors should be
242 raised if this API is used improperly.
243 """
239 adb_dir = os.path.dirname(constants.ADB_PATH) 244 adb_dir = os.path.dirname(constants.ADB_PATH)
240 if adb_dir and adb_dir not in os.environ['PATH'].split(os.pathsep): 245 if adb_dir and adb_dir not in os.environ['PATH'].split(os.pathsep):
241 # Required by third_party/android_testrunner to call directly 'adb'. 246 # Required by third_party/android_testrunner to call directly 'adb'.
242 os.environ['PATH'] += os.pathsep + adb_dir 247 os.environ['PATH'] += os.pathsep + adb_dir
243 self._adb = adb_interface.AdbInterface() 248 self._adb = adb_interface.AdbInterface()
244 if device: 249 if device:
245 self._adb.SetTargetSerial(device) 250 self._adb.SetTargetSerial(device)
246 self._device = device 251 self._device = device
247 self._logcat = None 252 self._logcat = None
248 self.logcat_process = None 253 self.logcat_process = None
249 self._logcat_tmpoutfile = None 254 self._logcat_tmpoutfile = None
250 self._pushed_files = [] 255 self._pushed_files = []
251 self._device_utc_offset = None 256 self._device_utc_offset = None
252 self._potential_push_size = 0 257 self._potential_push_size = 0
253 self._actual_push_size = 0 258 self._actual_push_size = 0
254 self._external_storage = '' 259 self._external_storage = ''
255 self._util_wrapper = '' 260 self._util_wrapper = ''
261 self._api_strict_mode = api_strict_mode
262 self._system_properties = system_properties.SystemProperties(self.Adb())
263
264 if not self._api_strict_mode:
265 logging.warning(
266 'API STRICT MODE IS DISABLED.\n'
267 'It should be enabled as soon as possible as it will eventually '
268 'become the default.')
269
270 @property
271 def system_properties(self):
272 return self._system_properties
256 273
257 def _LogShell(self, cmd): 274 def _LogShell(self, cmd):
258 """Logs the adb shell command.""" 275 """Logs the adb shell command."""
259 if self._device: 276 if self._device:
260 device_repr = self._device[-4:] 277 device_repr = self._device[-4:]
261 else: 278 else:
262 device_repr = '????' 279 device_repr = '????'
263 logging.info('[%s]> %s', device_repr, cmd) 280 logging.info('[%s]> %s', device_repr, cmd)
264 281
265 def Adb(self): 282 def Adb(self):
266 """Returns our AdbInterface to avoid us wrapping all its methods.""" 283 """Returns our AdbInterface to avoid us wrapping all its methods."""
284 # TODO(tonyg): Disable this method when in _api_strict_mode.
267 return self._adb 285 return self._adb
268 286
269 def GetDevice(self): 287 def GetDevice(self):
270 """Returns the device serial.""" 288 """Returns the device serial."""
271 return self._device 289 return self._device
272 290
273 def IsOnline(self): 291 def IsOnline(self):
274 """Checks whether the device is online. 292 """Checks whether the device is online.
275 293
276 Returns: 294 Returns:
(...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after
345 Args: 363 Args:
346 full_reboot: Whether to fully reboot the device or just restart the shell. 364 full_reboot: Whether to fully reboot the device or just restart the shell.
347 """ 365 """
348 # TODO(torne): hive can't reboot the device either way without breaking the 366 # TODO(torne): hive can't reboot the device either way without breaking the
349 # connection; work out if we can handle this better 367 # connection; work out if we can handle this better
350 if os.environ.get('USING_HIVE'): 368 if os.environ.get('USING_HIVE'):
351 logging.warning('Ignoring reboot request as we are on hive') 369 logging.warning('Ignoring reboot request as we are on hive')
352 return 370 return
353 if full_reboot or not self.IsRootEnabled(): 371 if full_reboot or not self.IsRootEnabled():
354 self._adb.SendCommand('reboot') 372 self._adb.SendCommand('reboot')
373 self._system_properties = system_properties.SystemProperties(self.Adb())
355 timeout = 300 374 timeout = 300
356 retries = 1 375 retries = 1
357 # Wait for the device to disappear. 376 # Wait for the device to disappear.
358 while retries < 10 and self.IsOnline(): 377 while retries < 10 and self.IsOnline():
359 time.sleep(1) 378 time.sleep(1)
360 retries += 1 379 retries += 1
361 else: 380 else:
362 self.RestartShell() 381 self.RestartShell()
363 timeout = 120 382 timeout = 120
364 # To run tests we need at least the package manager and the sd card (or 383 # To run tests we need at least the package manager and the sd card (or
365 # other external storage) to be ready. 384 # other external storage) to be ready.
366 self.WaitForDevicePm() 385 self.WaitForDevicePm()
367 self.WaitForSdCardReady(timeout) 386 self.WaitForSdCardReady(timeout)
368 387
369 def Shutdown(self): 388 def Shutdown(self):
370 """Shuts down the device.""" 389 """Shuts down the device."""
371 self._adb.SendCommand('reboot -p') 390 self._adb.SendCommand('reboot -p')
391 self._system_properties = system_properties.SystemProperties(self.Adb())
372 392
373 def Uninstall(self, package): 393 def Uninstall(self, package):
374 """Uninstalls the specified package from the device. 394 """Uninstalls the specified package from the device.
375 395
376 Args: 396 Args:
377 package: Name of the package to remove. 397 package: Name of the package to remove.
378 398
379 Returns: 399 Returns:
380 A status string returned by adb uninstall 400 A status string returned by adb uninstall
381 """ 401 """
(...skipping 147 matching lines...) Expand 10 before | Expand all | Expand 10 after
529 set. 549 set.
530 """ 550 """
531 logging.info('Waiting for system boot completed...') 551 logging.info('Waiting for system boot completed...')
532 self._adb.SendCommand('wait-for-device') 552 self._adb.SendCommand('wait-for-device')
533 # Now the device is there, but system not boot completed. 553 # Now the device is there, but system not boot completed.
534 # Query the sys.boot_completed flag with a basic command 554 # Query the sys.boot_completed flag with a basic command
535 boot_completed = False 555 boot_completed = False
536 attempts = 0 556 attempts = 0
537 wait_period = 5 557 wait_period = 5
538 while not boot_completed and (attempts * wait_period) < wait_time: 558 while not boot_completed and (attempts * wait_period) < wait_time:
539 output = self._adb.SendShellCommand('getprop sys.boot_completed', 559 output = self.system_properties['sys.boot_completed']
540 retry_count=1)
541 output = output.strip() 560 output = output.strip()
542 if output == '1': 561 if output == '1':
543 boot_completed = True 562 boot_completed = True
544 else: 563 else:
545 # If 'error: xxx' returned when querying the flag, it means 564 # If 'error: xxx' returned when querying the flag, it means
546 # adb server lost the connection to the emulator, so restart the adb 565 # adb server lost the connection to the emulator, so restart the adb
547 # server. 566 # server.
548 if 'error:' in output: 567 if 'error:' in output:
549 self.RestartAdbServer() 568 self.RestartAdbServer()
550 time.sleep(wait_period) 569 time.sleep(wait_period)
(...skipping 13 matching lines...) Expand all
564 output = self.RunShellCommand('ls ' + external_storage) 583 output = self.RunShellCommand('ls ' + external_storage)
565 if output: 584 if output:
566 sdcard_ready = True 585 sdcard_ready = True
567 else: 586 else:
568 time.sleep(wait_period) 587 time.sleep(wait_period)
569 attempts += 1 588 attempts += 1
570 if not sdcard_ready: 589 if not sdcard_ready:
571 raise errors.WaitForResponseTimedOutError( 590 raise errors.WaitForResponseTimedOutError(
572 'SD card not ready after %s seconds' % timeout_time) 591 'SD card not ready after %s seconds' % timeout_time)
573 592
593 def _CheckCommandIsValid(self, command):
594 """Raises a ValueError if the command is not valid."""
595
596 # A dict of commands the user should not run directly and a mapping to the
597 # API they should use instead.
598 preferred_apis = {
599 'getprop': 'system_properties[<PROPERTY>]',
600 'setprop': 'system_properties[<PROPERTY>]',
601 'su': 'RunShellCommandWithSU()',
602 }
603
604 # A dict of commands to methods that may call them.
605 whitelisted_callers = {
606 'su': 'RunShellCommandWithSU',
607 }
608
609 base_command = shlex.split(command)[0]
610 if (base_command in preferred_apis and
611 (base_command not in whitelisted_callers or
612 whitelisted_callers[base_command] not in [
613 f[3] for f in inspect.stack()])):
614 error_msg = ('%s cannot be run directly. Instead use: %s' %
615 (base_command, preferred_apis[base_command]))
616 if self._api_strict_mode:
617 raise ValueError(error_msg)
618 else:
619 logging.warning(error_msg)
620
574 # It is tempting to turn this function into a generator, however this is not 621 # It is tempting to turn this function into a generator, however this is not
575 # possible without using a private (local) adb_shell instance (to ensure no 622 # possible without using a private (local) adb_shell instance (to ensure no
576 # other command interleaves usage of it), which would defeat the main aim of 623 # other command interleaves usage of it), which would defeat the main aim of
577 # being able to reuse the adb shell instance across commands. 624 # being able to reuse the adb shell instance across commands.
578 def RunShellCommand(self, command, timeout_time=20, log_result=False): 625 def RunShellCommand(self, command, timeout_time=20, log_result=False):
579 """Send a command to the adb shell and return the result. 626 """Send a command to the adb shell and return the result.
580 627
581 Args: 628 Args:
582 command: String containing the shell command to send. Must not include 629 command: String containing the shell command to send. Must not include
583 the single quotes as we use them to escape the whole command. 630 the single quotes as we use them to escape the whole command.
584 timeout_time: Number of seconds to wait for command to respond before 631 timeout_time: Number of seconds to wait for command to respond before
585 retrying, used by AdbInterface.SendShellCommand. 632 retrying, used by AdbInterface.SendShellCommand.
586 log_result: Boolean to indicate whether we should log the result of the 633 log_result: Boolean to indicate whether we should log the result of the
587 shell command. 634 shell command.
588 635
589 Returns: 636 Returns:
590 list containing the lines of output received from running the command 637 list containing the lines of output received from running the command
591 """ 638 """
639 self._CheckCommandIsValid(command)
592 self._LogShell(command) 640 self._LogShell(command)
593 if "'" in command: logging.warning(command + " contains ' quotes") 641 if "'" in command: logging.warning(command + " contains ' quotes")
594 result = self._adb.SendShellCommand( 642 result = self._adb.SendShellCommand(
595 "'%s'" % command, timeout_time).splitlines() 643 "'%s'" % command, timeout_time).splitlines()
596 if ['error: device not found'] == result: 644 if ['error: device not found'] == result:
597 raise errors.DeviceUnresponsiveError('device not found') 645 raise errors.DeviceUnresponsiveError('device not found')
598 if log_result: 646 if log_result:
599 self._LogShell('\n'.join(result)) 647 self._LogShell('\n'.join(result))
600 return result 648 return result
601 649
(...skipping 391 matching lines...) Expand 10 before | Expand all | Expand 10 after
993 _TEMP_SCRIPT_FILE_BASE_FMT = 'temp_script_file_%d.sh' 1041 _TEMP_SCRIPT_FILE_BASE_FMT = 'temp_script_file_%d.sh'
994 1042
995 def _GetDeviceTempFileName(self, base_name): 1043 def _GetDeviceTempFileName(self, base_name):
996 i = 0 1044 i = 0
997 while self.FileExistsOnDevice( 1045 while self.FileExistsOnDevice(
998 self.GetExternalStorage() + '/' + base_name % i): 1046 self.GetExternalStorage() + '/' + base_name % i):
999 i += 1 1047 i += 1
1000 return self.GetExternalStorage() + '/' + base_name % i 1048 return self.GetExternalStorage() + '/' + base_name % i
1001 1049
1002 def RunShellCommandWithSU(self, command, timeout_time=20, log_result=False): 1050 def RunShellCommandWithSU(self, command, timeout_time=20, log_result=False):
1003 return self.RunShellCommand('su -c %s' % command, 1051 return self.RunShellCommand('su -c %s' % command, timeout_time, log_result)
1004 timeout_time=timeout_time,
1005 log_result=log_result)
1006 1052
1007 def CanAccessProtectedFileContents(self): 1053 def CanAccessProtectedFileContents(self):
1008 """Returns True if Get/SetProtectedFileContents would work via "su". 1054 """Returns True if Get/SetProtectedFileContents would work via "su".
1009 1055
1010 Devices running user builds don't have adb root, but may provide "su" which 1056 Devices running user builds don't have adb root, but may provide "su" which
1011 can be used for accessing protected files. 1057 can be used for accessing protected files.
1012 """ 1058 """
1013 r = self.RunShellCommandWithSU('cat /dev/null') 1059 r = self.RunShellCommandWithSU('cat /dev/null')
1014 return r == [] or r[0].strip() == '' 1060 return r == [] or r[0].strip() == ''
1015 1061
(...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after
1096 r'\s*=\s*\w+\s*$', re.MULTILINE) 1142 r'\s*=\s*\w+\s*$', re.MULTILINE)
1097 properties = re.sub(re_replace, '', properties) 1143 properties = re.sub(re_replace, '', properties)
1098 if enable: 1144 if enable:
1099 properties += '\n%s=all\n' % JAVA_ASSERT_PROPERTY 1145 properties += '\n%s=all\n' % JAVA_ASSERT_PROPERTY
1100 1146
1101 file(temp_props_file.name, 'w').write(properties) 1147 file(temp_props_file.name, 'w').write(properties)
1102 self._adb.Push(temp_props_file.name, LOCAL_PROPERTIES_PATH) 1148 self._adb.Push(temp_props_file.name, LOCAL_PROPERTIES_PATH)
1103 1149
1104 # Next, check the current runtime value is what we need, and 1150 # Next, check the current runtime value is what we need, and
1105 # if not, set it and report that a reboot is required. 1151 # if not, set it and report that a reboot is required.
1106 was_set = 'all' in self.RunShellCommand('getprop ' + JAVA_ASSERT_PROPERTY) 1152 was_set = 'all' in self.system_properties[JAVA_ASSERT_PROPERTY]
1107 if was_set == enable: 1153 if was_set == enable:
1108 return False 1154 return False
1109 1155 self.system_properties[JAVA_ASSERT_PROPERTY] = enable and 'all' or ''
1110 self.RunShellCommand('setprop %s "%s"' % (JAVA_ASSERT_PROPERTY,
1111 enable and 'all' or ''))
1112 return True 1156 return True
1113 1157
1114 def GetBuildId(self): 1158 def GetBuildId(self):
1115 """Returns the build ID of the system (e.g. JRM79C).""" 1159 """Returns the build ID of the system (e.g. JRM79C)."""
1116 build_id = self.RunShellCommand('getprop ro.build.id')[0] 1160 build_id = self.system_properties['ro.build.id']
1117 assert build_id 1161 assert build_id
1118 return build_id 1162 return build_id
1119 1163
1120 def GetBuildType(self): 1164 def GetBuildType(self):
1121 """Returns the build type of the system (e.g. eng).""" 1165 """Returns the build type of the system (e.g. eng)."""
1122 build_type = self.RunShellCommand('getprop ro.build.type')[0] 1166 build_type = self.system_properties['ro.build.type']
1123 assert build_type 1167 assert build_type
1124 return build_type 1168 return build_type
1125 1169
1126 def GetBuildProduct(self): 1170 def GetBuildProduct(self):
1127 """Returns the build product of the device (e.g. maguro).""" 1171 """Returns the build product of the device (e.g. maguro)."""
1128 build_product = self.RunShellCommand('getprop ro.build.product')[0] 1172 build_product = self.system_properties['ro.build.product']
1129 assert build_product 1173 assert build_product
1130 return build_product 1174 return build_product
1131 1175
1132 def GetProductName(self): 1176 def GetProductName(self):
1133 """Returns the product name of the device (e.g. takju).""" 1177 """Returns the product name of the device (e.g. takju)."""
1134 name = self.RunShellCommand('getprop ro.product.name')[0] 1178 name = self.system_properties['ro.product.name']
1135 assert name 1179 assert name
1136 return name 1180 return name
1137 1181
1138 def GetBuildFingerprint(self): 1182 def GetBuildFingerprint(self):
1139 """Returns the build fingerprint of the device.""" 1183 """Returns the build fingerprint of the device."""
1140 build_fingerprint = self.RunShellCommand('getprop ro.build.fingerprint')[0] 1184 build_fingerprint = self.system_properties['ro.build.fingerprint']
1141 assert build_fingerprint 1185 assert build_fingerprint
1142 return build_fingerprint 1186 return build_fingerprint
1143 1187
1144 def GetDescription(self): 1188 def GetDescription(self):
1145 """Returns the description of the system. 1189 """Returns the description of the system.
1146 1190
1147 For example, "yakju-userdebug 4.1 JRN54F 364167 dev-keys". 1191 For example, "yakju-userdebug 4.1 JRN54F 364167 dev-keys".
1148 """ 1192 """
1149 description = self.RunShellCommand('getprop ro.build.description')[0] 1193 description = self.system_properties['ro.build.description']
1150 assert description 1194 assert description
1151 return description 1195 return description
1152 1196
1153 def GetProductModel(self): 1197 def GetProductModel(self):
1154 """Returns the name of the product model (e.g. "Galaxy Nexus") """ 1198 """Returns the name of the product model (e.g. "Galaxy Nexus") """
1155 model = self.RunShellCommand('getprop ro.product.model')[0] 1199 model = self.system_properties['ro.product.model']
1156 assert model 1200 assert model
1157 return model 1201 return model
1158 1202
1159 def GetWifiIP(self): 1203 def GetWifiIP(self):
1160 """Returns the wifi IP on the device.""" 1204 """Returns the wifi IP on the device."""
1161 wifi_ip = self.RunShellCommand('getprop dhcp.wlan0.ipaddress')[0] 1205 wifi_ip = self.system_properties['dhcp.wlan0.ipaddress']
1162 # Do not assert here. Devices (e.g. emulators) may not have a WifiIP. 1206 # Do not assert here. Devices (e.g. emulators) may not have a WifiIP.
1163 return wifi_ip 1207 return wifi_ip
1164 1208
1165 def GetSubscriberInfo(self): 1209 def GetSubscriberInfo(self):
1166 """Returns the device subscriber info (e.g. GSM and device ID) as string.""" 1210 """Returns the device subscriber info (e.g. GSM and device ID) as string."""
1167 iphone_sub = self.RunShellCommand('dumpsys iphonesubinfo') 1211 iphone_sub = self.RunShellCommand('dumpsys iphonesubinfo')
1168 assert iphone_sub 1212 assert iphone_sub
1169 return '\n'.join(iphone_sub) 1213 return '\n'.join(iphone_sub)
1170 1214
1171 def GetBatteryInfo(self): 1215 def GetBatteryInfo(self):
1172 """Returns the device battery info (e.g. status, level, etc) as string.""" 1216 """Returns the device battery info (e.g. status, level, etc) as string."""
1173 battery = self.RunShellCommand('dumpsys battery') 1217 battery = self.RunShellCommand('dumpsys battery')
1174 assert battery 1218 assert battery
1175 return '\n'.join(battery) 1219 return '\n'.join(battery)
1176 1220
1177 def GetSetupWizardStatus(self): 1221 def GetSetupWizardStatus(self):
1178 """Returns the status of the device setup wizard (e.g. DISABLED).""" 1222 """Returns the status of the device setup wizard (e.g. DISABLED)."""
1179 status = self.RunShellCommand('getprop ro.setupwizard.mode')[0] 1223 status = self.system_properties['ro.setupwizard.mode']
1180 # On some devices, the status is empty if not otherwise set. In such cases 1224 # On some devices, the status is empty if not otherwise set. In such cases
1181 # the caller should expect an empty string to be returned. 1225 # the caller should expect an empty string to be returned.
1182 return status 1226 return status
1183 1227
1184 def StartMonitoringLogcat(self, clear=True, logfile=None, filters=None): 1228 def StartMonitoringLogcat(self, clear=True, logfile=None, filters=None):
1185 """Starts monitoring the output of logcat, for use with WaitForLogMatch. 1229 """Starts monitoring the output of logcat, for use with WaitForLogMatch.
1186 1230
1187 Args: 1231 Args:
1188 clear: If True the existing logcat output will be cleared, to avoiding 1232 clear: If True the existing logcat output will be cleared, to avoiding
1189 matching historical output lurking in the log. 1233 matching historical output lurking in the log.
(...skipping 523 matching lines...) Expand 10 before | Expand all | Expand 10 after
1713 """ 1757 """
1714 def __init__(self, output): 1758 def __init__(self, output):
1715 self._output = output 1759 self._output = output
1716 1760
1717 def write(self, data): 1761 def write(self, data):
1718 data = data.replace('\r\r\n', '\n') 1762 data = data.replace('\r\r\n', '\n')
1719 self._output.write(data) 1763 self._output.write(data)
1720 1764
1721 def flush(self): 1765 def flush(self):
1722 self._output.flush() 1766 self._output.flush()
OLDNEW
« no previous file with comments | « no previous file | build/android/pylib/system_properties.py » ('j') | build/android/pylib/system_properties.py » ('J')

Powered by Google App Engine
This is Rietveld 408576698