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