1# Copyright (c) 2013 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"""Module containing utilities for apk packages.""" 5 6import contextlib 7import logging 8import os 9import re 10import shutil 11import tempfile 12import zipfile 13 14from devil import base_error 15from devil.android.ndk import abis 16from devil.android.sdk import aapt 17from devil.android.sdk import bundletool 18from devil.android.sdk import split_select 19from devil.utils import cmd_helper 20 21_logger = logging.getLogger(__name__) 22 23_MANIFEST_ATTRIBUTE_RE = re.compile(r'\s*A: ([^\(\)= ]*)(?:\([^\(\)= ]*\))?=' 24 r'(?:"(.*)" \(Raw: .*\)|\(type.*?\)(.*))$') 25_MANIFEST_ELEMENT_RE = re.compile(r'\s*(?:E|N): (\S*) .*$') 26_BASE_APK_APKS_RE = re.compile(r'^splits/base-master.*\.apk$') 27 28 29class ApkHelperError(base_error.BaseError): 30 """Exception for APK helper failures.""" 31 32 def __init__(self, message): 33 super(ApkHelperError, self).__init__(message) 34 35 36@contextlib.contextmanager 37def _DeleteHelper(files, to_delete): 38 """Context manager that returns |files| and deletes |to_delete| on exit.""" 39 try: 40 yield files 41 finally: 42 paths = to_delete if isinstance(to_delete, list) else [to_delete] 43 for path in paths: 44 if os.path.isfile(path): 45 os.remove(path) 46 elif os.path.isdir(path): 47 shutil.rmtree(path) 48 else: 49 raise ApkHelperError('Cannot delete %s' % path) 50 51 52@contextlib.contextmanager 53def _NoopFileHelper(files): 54 """Context manager that returns |files|.""" 55 yield files 56 57 58def GetPackageName(apk_path): 59 """Returns the package name of the apk.""" 60 return ToHelper(apk_path).GetPackageName() 61 62 63# TODO(jbudorick): Deprecate and remove this function once callers have been 64# converted to ApkHelper.GetInstrumentationName 65def GetInstrumentationName(apk_path): 66 """Returns the name of the Instrumentation in the apk.""" 67 return ToHelper(apk_path).GetInstrumentationName() 68 69 70def ToHelper(path_or_helper): 71 """Creates an ApkHelper unless one is already given.""" 72 if not isinstance(path_or_helper, basestring): 73 return path_or_helper 74 elif path_or_helper.endswith('.apk'): 75 return ApkHelper(path_or_helper) 76 elif path_or_helper.endswith('.apks'): 77 return ApksHelper(path_or_helper) 78 elif path_or_helper.endswith('_bundle'): 79 return BundleScriptHelper(path_or_helper) 80 81 raise ApkHelperError('Unrecognized APK format %s' % path_or_helper) 82 83 84def ToSplitHelper(path_or_helper, split_apks): 85 if isinstance(path_or_helper, SplitApkHelper): 86 if sorted(path_or_helper.split_apk_paths) != sorted(split_apks): 87 raise ApkHelperError('Helper has different split APKs') 88 return path_or_helper 89 elif (isinstance(path_or_helper, basestring) 90 and path_or_helper.endswith('.apk')): 91 return SplitApkHelper(path_or_helper, split_apks) 92 93 raise ApkHelperError( 94 'Unrecognized APK format %s, %s' % (path_or_helper, split_apks)) 95 96 97# To parse the manifest, the function uses a node stack where at each level of 98# the stack it keeps the currently in focus node at that level (of indentation 99# in the xmltree output, ie. depth in the tree). The height of the stack is 100# determinded by line indentation. When indentation is increased so is the stack 101# (by pushing a new empty node on to the stack). When indentation is decreased 102# the top of the stack is popped (sometimes multiple times, until indentation 103# matches the height of the stack). Each line parsed (either an attribute or an 104# element) is added to the node at the top of the stack (after the stack has 105# been popped/pushed due to indentation). 106def _ParseManifestFromApk(apk_path): 107 aapt_output = aapt.Dump('xmltree', apk_path, 'AndroidManifest.xml') 108 parsed_manifest = {} 109 node_stack = [parsed_manifest] 110 indent = ' ' 111 112 if aapt_output[0].startswith('N'): 113 # if the first line is a namespace then the root manifest is indented, and 114 # we need to add a dummy namespace node, then skip the first line (we dont 115 # care about namespaces). 116 node_stack.insert(0, {}) 117 output_to_parse = aapt_output[1:] 118 else: 119 output_to_parse = aapt_output 120 121 for line in output_to_parse: 122 if len(line) == 0: 123 continue 124 125 # If namespaces are stripped, aapt still outputs the full url to the 126 # namespace and appends it to the attribute names. 127 line = line.replace('http://schemas.android.com/apk/res/android:', 128 'android:') 129 130 indent_depth = 0 131 while line[(len(indent) * indent_depth):].startswith(indent): 132 indent_depth += 1 133 134 # Pop the stack until the height of the stack is the same is the depth of 135 # the current line within the tree. 136 node_stack = node_stack[:indent_depth + 1] 137 node = node_stack[-1] 138 139 # Element nodes are a list of python dicts while attributes are just a dict. 140 # This is because multiple elements, at the same depth of tree and the same 141 # name, are all added to the same list keyed under the element name. 142 m = _MANIFEST_ELEMENT_RE.match(line[len(indent) * indent_depth:]) 143 if m: 144 manifest_key = m.group(1) 145 if manifest_key in node: 146 node[manifest_key] += [{}] 147 else: 148 node[manifest_key] = [{}] 149 node_stack += [node[manifest_key][-1]] 150 continue 151 152 m = _MANIFEST_ATTRIBUTE_RE.match(line[len(indent) * indent_depth:]) 153 if m: 154 manifest_key = m.group(1) 155 if manifest_key in node: 156 raise ApkHelperError( 157 "A single attribute should have one key and one value: {}".format( 158 line)) 159 else: 160 node[manifest_key] = m.group(2) or m.group(3) 161 continue 162 163 return parsed_manifest 164 165 166def _ParseNumericKey(obj, key, default=0): 167 val = obj.get(key) 168 if val is None: 169 return default 170 return int(val, 0) 171 172 173def _SplitLocaleString(locale): 174 split_locale = locale.split('-') 175 if len(split_locale) != 2: 176 raise ApkHelperError('Locale has incorrect format: {}'.format(locale)) 177 return tuple(split_locale) 178 179 180class _ExportedActivity(object): 181 def __init__(self, name): 182 self.name = name 183 self.actions = set() 184 self.categories = set() 185 self.schemes = set() 186 187 188def _IterateExportedActivities(manifest_info): 189 app_node = manifest_info['manifest'][0]['application'][0] 190 activities = app_node.get('activity', []) + app_node.get('activity-alias', []) 191 for activity_node in activities: 192 # Presence of intent filters make an activity exported by default. 193 has_intent_filter = 'intent-filter' in activity_node 194 if not _ParseNumericKey( 195 activity_node, 'android:exported', default=has_intent_filter): 196 continue 197 198 activity = _ExportedActivity(activity_node.get('android:name')) 199 # Merge all intent-filters into a single set because there is not 200 # currently a need to keep them separate. 201 for intent_filter in activity_node.get('intent-filter', []): 202 for action in intent_filter.get('action', []): 203 activity.actions.add(action.get('android:name')) 204 for category in intent_filter.get('category', []): 205 activity.categories.add(category.get('android:name')) 206 for data in intent_filter.get('data', []): 207 activity.schemes.add(data.get('android:scheme')) 208 yield activity 209 210 211class BaseApkHelper(object): 212 """Abstract base class representing an installable Android app.""" 213 214 def __init__(self): 215 self._manifest = None 216 217 @property 218 def path(self): 219 raise NotImplementedError() 220 221 def __repr__(self): 222 return '%s(%s)' % (self.__class__.__name__, self.path) 223 224 def _GetBaseApkPath(self): 225 """Returns context manager providing path to this app's base APK. 226 227 Must be implemented by subclasses. 228 """ 229 raise NotImplementedError() 230 231 def GetActivityName(self): 232 """Returns the name of the first launcher Activity in the apk.""" 233 manifest_info = self._GetManifest() 234 for activity in _IterateExportedActivities(manifest_info): 235 if ('android.intent.action.MAIN' in activity.actions 236 and 'android.intent.category.LAUNCHER' in activity.categories): 237 return self._ResolveName(activity.name) 238 return None 239 240 def GetViewActivityName(self): 241 """Returns name of the first action=View Activity that can handle http.""" 242 manifest_info = self._GetManifest() 243 for activity in _IterateExportedActivities(manifest_info): 244 if ('android.intent.action.VIEW' in activity.actions 245 and 'http' in activity.schemes): 246 return self._ResolveName(activity.name) 247 return None 248 249 def GetInstrumentationName(self, 250 default='android.test.InstrumentationTestRunner'): 251 """Returns the name of the Instrumentation in the apk.""" 252 all_instrumentations = self.GetAllInstrumentations(default=default) 253 if len(all_instrumentations) != 1: 254 raise ApkHelperError( 255 'There is more than one instrumentation. Expected one.') 256 else: 257 return self._ResolveName(all_instrumentations[0]['android:name']) 258 259 def GetAllInstrumentations(self, 260 default='android.test.InstrumentationTestRunner'): 261 """Returns a list of all Instrumentations in the apk.""" 262 try: 263 return self._GetManifest()['manifest'][0]['instrumentation'] 264 except KeyError: 265 return [{'android:name': default}] 266 267 def GetPackageName(self): 268 """Returns the package name of the apk.""" 269 manifest_info = self._GetManifest() 270 try: 271 return manifest_info['manifest'][0]['package'] 272 except KeyError: 273 raise ApkHelperError('Failed to determine package name of %s' % self.path) 274 275 def GetPermissions(self): 276 manifest_info = self._GetManifest() 277 try: 278 return [ 279 p['android:name'] 280 for p in manifest_info['manifest'][0]['uses-permission'] 281 ] 282 except KeyError: 283 return [] 284 285 def GetSplitName(self): 286 """Returns the name of the split of the apk.""" 287 manifest_info = self._GetManifest() 288 try: 289 return manifest_info['manifest'][0]['split'] 290 except KeyError: 291 return None 292 293 def HasIsolatedProcesses(self): 294 """Returns whether any services exist that use isolatedProcess=true.""" 295 manifest_info = self._GetManifest() 296 try: 297 application = manifest_info['manifest'][0]['application'][0] 298 services = application['service'] 299 return any( 300 _ParseNumericKey(s, 'android:isolatedProcess') for s in services) 301 except KeyError: 302 return False 303 304 def GetAllMetadata(self): 305 """Returns a list meta-data tags as (name, value) tuples.""" 306 manifest_info = self._GetManifest() 307 try: 308 application = manifest_info['manifest'][0]['application'][0] 309 metadata = application['meta-data'] 310 return [(x.get('android:name'), x.get('android:value')) for x in metadata] 311 except KeyError: 312 return [] 313 314 def GetVersionCode(self): 315 """Returns the versionCode as an integer, or None if not available.""" 316 manifest_info = self._GetManifest() 317 try: 318 version_code = manifest_info['manifest'][0]['android:versionCode'] 319 return int(version_code, 16) 320 except KeyError: 321 return None 322 323 def GetVersionName(self): 324 """Returns the versionName as a string.""" 325 manifest_info = self._GetManifest() 326 try: 327 version_name = manifest_info['manifest'][0]['android:versionName'] 328 return version_name 329 except KeyError: 330 return '' 331 332 def GetMinSdkVersion(self): 333 """Returns the minSdkVersion as a string, or None if not available. 334 335 Note: this cannot always be cast to an integer.""" 336 manifest_info = self._GetManifest() 337 try: 338 uses_sdk = manifest_info['manifest'][0]['uses-sdk'][0] 339 min_sdk_version = uses_sdk['android:minSdkVersion'] 340 try: 341 # The common case is for this to be an integer. Convert to decimal 342 # notation (rather than hexadecimal) for readability, but convert back 343 # to a string for type consistency with the general case. 344 return str(int(min_sdk_version, 16)) 345 except ValueError: 346 # In general (ex. apps with minSdkVersion set to pre-release Android 347 # versions), minSdkVersion can be a string (usually, the OS codename 348 # letter). For simplicity, don't do any validation on the value. 349 return min_sdk_version 350 except KeyError: 351 return None 352 353 def GetTargetSdkVersion(self): 354 """Returns the targetSdkVersion as a string, or None if not available. 355 356 Note: this cannot always be cast to an integer. If this application targets 357 a pre-release SDK, this returns the SDK codename instead (ex. "R"). 358 """ 359 manifest_info = self._GetManifest() 360 try: 361 uses_sdk = manifest_info['manifest'][0]['uses-sdk'][0] 362 target_sdk_version = uses_sdk['android:targetSdkVersion'] 363 try: 364 # The common case is for this to be an integer. Convert to decimal 365 # notation (rather than hexadecimal) for readability, but convert back 366 # to a string for type consistency with the general case. 367 return str(int(target_sdk_version, 16)) 368 except ValueError: 369 # In general (ex. apps targeting pre-release Android versions), 370 # targetSdkVersion can be a string (usually, the OS codename letter). 371 # For simplicity, don't do any validation on the value. 372 return target_sdk_version 373 except KeyError: 374 return None 375 376 def _GetManifest(self): 377 if not self._manifest: 378 with self._GetBaseApkPath() as base_apk_path: 379 self._manifest = _ParseManifestFromApk(base_apk_path) 380 return self._manifest 381 382 def _ResolveName(self, name): 383 name = name.lstrip('.') 384 if '.' not in name: 385 return '%s.%s' % (self.GetPackageName(), name) 386 return name 387 388 def _ListApkPaths(self): 389 with self._GetBaseApkPath() as base_apk_path: 390 with zipfile.ZipFile(base_apk_path) as z: 391 return z.namelist() 392 393 def GetAbis(self): 394 """Returns a list of ABIs in the apk (empty list if no native code).""" 395 # Use lib/* to determine the compatible ABIs. 396 libs = set() 397 for path in self._ListApkPaths(): 398 path_tokens = path.split('/') 399 if len(path_tokens) >= 2 and path_tokens[0] == 'lib': 400 libs.add(path_tokens[1]) 401 lib_to_abi = { 402 abis.ARM: [abis.ARM, abis.ARM_64], 403 abis.ARM_64: [abis.ARM_64], 404 abis.X86: [abis.X86, abis.X86_64], 405 abis.X86_64: [abis.X86_64] 406 } 407 try: 408 output = set() 409 for lib in libs: 410 for abi in lib_to_abi[lib]: 411 output.add(abi) 412 return sorted(output) 413 except KeyError: 414 raise ApkHelperError('Unexpected ABI in lib/* folder.') 415 416 def GetApkPaths(self, 417 device, 418 modules=None, 419 allow_cached_props=False, 420 additional_locales=None): 421 """Returns context manager providing list of split APK paths for |device|. 422 423 The paths may be deleted when the context manager exits. Must be implemented 424 by subclasses. 425 426 args: 427 device: The device for which to return split APKs. 428 modules: Extra feature modules to install. 429 allow_cached_props: Allow using cache when querying propery values from 430 |device|. 431 """ 432 # pylint: disable=unused-argument 433 raise NotImplementedError() 434 435 @staticmethod 436 def SupportsSplits(): 437 return False 438 439 440class ApkHelper(BaseApkHelper): 441 """Represents a single APK Android app.""" 442 443 def __init__(self, apk_path): 444 super(ApkHelper, self).__init__() 445 self._apk_path = apk_path 446 447 @property 448 def path(self): 449 return self._apk_path 450 451 def _GetBaseApkPath(self): 452 return _NoopFileHelper(self._apk_path) 453 454 def GetApkPaths(self, 455 device, 456 modules=None, 457 allow_cached_props=False, 458 additional_locales=None): 459 if modules: 460 raise ApkHelperError('Cannot install modules when installing single APK') 461 return _NoopFileHelper([self._apk_path]) 462 463 464class SplitApkHelper(BaseApkHelper): 465 """Represents a multi APK Android app.""" 466 467 def __init__(self, base_apk_path, split_apk_paths): 468 super(SplitApkHelper, self).__init__() 469 self._base_apk_path = base_apk_path 470 self._split_apk_paths = split_apk_paths 471 472 @property 473 def path(self): 474 return self._base_apk_path 475 476 @property 477 def split_apk_paths(self): 478 return self._split_apk_paths 479 480 def __repr__(self): 481 return '%s(%s, %s)' % (self.__class__.__name__, self.path, 482 self.split_apk_paths) 483 484 def _GetBaseApkPath(self): 485 return _NoopFileHelper(self._base_apk_path) 486 487 def GetApkPaths(self, 488 device, 489 modules=None, 490 allow_cached_props=False, 491 additional_locales=None): 492 if modules: 493 raise ApkHelperError('Cannot install modules when installing single APK') 494 splits = split_select.SelectSplits( 495 device, 496 self.path, 497 self.split_apk_paths, 498 allow_cached_props=allow_cached_props) 499 if len(splits) == 1: 500 _logger.warning('split-select did not select any from %s', splits) 501 return _NoopFileHelper([self._base_apk_path] + splits) 502 503 #override 504 @staticmethod 505 def SupportsSplits(): 506 return True 507 508 509class BaseBundleHelper(BaseApkHelper): 510 """Abstract base class representing an Android app bundle.""" 511 512 def _GetApksPath(self): 513 """Returns context manager providing path to the bundle's APKS archive. 514 515 Must be implemented by subclasses. 516 """ 517 raise NotImplementedError() 518 519 def _GetBaseApkPath(self): 520 try: 521 base_apk_path = tempfile.mkdtemp() 522 with self._GetApksPath() as apks_path: 523 with zipfile.ZipFile(apks_path) as z: 524 base_apks = [s for s in z.namelist() if _BASE_APK_APKS_RE.match(s)] 525 if len(base_apks) < 1: 526 raise ApkHelperError('Cannot find base APK in %s' % self.path) 527 z.extract(base_apks[0], base_apk_path) 528 return _DeleteHelper( 529 os.path.join(base_apk_path, base_apks[0]), base_apk_path) 530 except: 531 shutil.rmtree(base_apk_path) 532 raise 533 534 def GetApkPaths(self, 535 device, 536 modules=None, 537 allow_cached_props=False, 538 additional_locales=None): 539 locales = [device.GetLocale()] 540 if additional_locales: 541 locales.extend(_SplitLocaleString(l) for l in additional_locales) 542 with self._GetApksPath() as apks_path: 543 try: 544 split_dir = tempfile.mkdtemp() 545 # TODO(tiborg): Support all locales. 546 bundletool.ExtractApks(split_dir, apks_path, 547 device.product_cpu_abis, locales, 548 device.GetFeatures(), device.pixel_density, 549 device.build_version_sdk, modules) 550 splits = [os.path.join(split_dir, p) for p in os.listdir(split_dir)] 551 return _DeleteHelper(splits, split_dir) 552 except: 553 shutil.rmtree(split_dir) 554 raise 555 556 #override 557 @staticmethod 558 def SupportsSplits(): 559 return True 560 561 562class ApksHelper(BaseBundleHelper): 563 """Represents a bundle's APKS archive.""" 564 565 def __init__(self, apks_path): 566 super(ApksHelper, self).__init__() 567 self._apks_path = apks_path 568 569 @property 570 def path(self): 571 return self._apks_path 572 573 def _GetApksPath(self): 574 return _NoopFileHelper(self._apks_path) 575 576 577class BundleScriptHelper(BaseBundleHelper): 578 """Represents a bundle install script.""" 579 580 def __init__(self, bundle_script_path): 581 super(BundleScriptHelper, self).__init__() 582 self._bundle_script_path = bundle_script_path 583 584 @property 585 def path(self): 586 return self._bundle_script_path 587 588 def _GetApksPath(self): 589 apks_path = None 590 try: 591 fd, apks_path = tempfile.mkstemp(suffix='.apks') 592 os.close(fd) 593 cmd = [ 594 self._bundle_script_path, 595 'build-bundle-apks', 596 '--output-apks', 597 apks_path, 598 ] 599 status, stdout, stderr = cmd_helper.GetCmdStatusOutputAndError(cmd) 600 if status != 0: 601 raise ApkHelperError('Failed running {} with output\n{}\n{}'.format( 602 ' '.join(cmd), stdout, stderr)) 603 return _DeleteHelper(apks_path, apks_path) 604 except: 605 if apks_path: 606 os.remove(apks_path) 607 raise 608