diff options
Diffstat (limited to 'catapult/devil/devil/android/apk_helper.py')
-rw-r--r-- | catapult/devil/devil/android/apk_helper.py | 122 |
1 files changed, 101 insertions, 21 deletions
diff --git a/catapult/devil/devil/android/apk_helper.py b/catapult/devil/devil/android/apk_helper.py index 1a9b8c55..8acb41e6 100644 --- a/catapult/devil/devil/android/apk_helper.py +++ b/catapult/devil/devil/android/apk_helper.py @@ -4,7 +4,6 @@ """Module containing utilities for apk packages.""" -import itertools import re from devil import base_error @@ -36,6 +35,15 @@ def ToHelper(path_or_helper): return path_or_helper +# To parse the manifest, the function uses a node stack where at each level of +# the stack it keeps the currently in focus node at that level (of indentation +# in the xmltree output, ie. depth in the tree). The height of the stack is +# determinded by line indentation. When indentation is increased so is the stack +# (by pushing a new empty node on to the stack). When indentation is decreased +# the top of the stack is popped (sometimes multiple times, until indentation +# matches the height of the stack). Each line parsed (either an attribute or an +# element) is added to the node at the top of the stack (after the stack has +# been popped/pushed due to indentation). def _ParseManifestFromApk(apk_path): aapt_output = aapt.Dump('xmltree', apk_path, 'AndroidManifest.xml') @@ -43,17 +51,35 @@ def _ParseManifestFromApk(apk_path): node_stack = [parsed_manifest] indent = ' ' - for line in aapt_output[1:]: + if aapt_output[0].startswith('N'): + # if the first line is a namespace then the root manifest is indented, and + # we need to add a dummy namespace node, then skip the first line (we dont + # care about namespaces). + node_stack.insert(0, {}) + output_to_parse = aapt_output[1:] + else: + output_to_parse = aapt_output + + for line in output_to_parse: if len(line) == 0: continue + # If namespaces are stripped, aapt still outputs the full url to the + # namespace and appends it to the attribute names. + line = line.replace('http://schemas.android.com/apk/res/android:', 'android:') + indent_depth = 0 while line[(len(indent) * indent_depth):].startswith(indent): indent_depth += 1 - node_stack = node_stack[:indent_depth] + # Pop the stack until the height of the stack is the same is the depth of + # the current line within the tree. + node_stack = node_stack[:indent_depth + 1] node = node_stack[-1] + # Element nodes are a list of python dicts while attributes are just a dict. + # This is because multiple elements, at the same depth of tree and the same + # name, are all added to the same list keyed under the element name. m = _MANIFEST_ELEMENT_RE.match(line[len(indent) * indent_depth:]) if m: manifest_key = m.group(1) @@ -77,6 +103,44 @@ def _ParseManifestFromApk(apk_path): return parsed_manifest +def _ParseNumericKey(obj, key, default=0): + val = obj.get(key) + if val is None: + return default + return int(val, 0) + + +class _ExportedActivity(object): + def __init__(self, name): + self.name = name + self.actions = set() + self.categories = set() + self.schemes = set() + + +def _IterateExportedActivities(manifest_info): + app_node = manifest_info['manifest'][0]['application'][0] + activities = app_node.get('activity', []) + app_node.get('activity-alias', []) + for activity_node in activities: + # Presence of intent filters make an activity exported by default. + has_intent_filter = 'intent-filter' in activity_node + if not _ParseNumericKey( + activity_node, 'android:exported', default=has_intent_filter): + continue + + activity = _ExportedActivity(activity_node.get('android:name')) + # Merge all intent-filters into a single set because there is not + # currently a need to keep them separate. + for intent_filter in activity_node.get('intent-filter', []): + for action in intent_filter.get('action', []): + activity.actions.add(action.get('android:name')) + for category in intent_filter.get('category', []): + activity.categories.add(category.get('android:name')) + for data in intent_filter.get('data', []): + activity.schemes.add(data.get('android:scheme')) + yield activity + + class ApkHelper(object): def __init__(self, path): @@ -88,19 +152,22 @@ class ApkHelper(object): return self._apk_path def GetActivityName(self): - """Returns the name of the Activity in the apk.""" + """Returns the name of the first launcher Activity in the apk.""" manifest_info = self._GetManifest() - try: - activity = ( - manifest_info['manifest'][0]['application'][0]['activity'][0] - ['android:name']) - except KeyError: - return None - if '.' not in activity: - activity = '%s.%s' % (self.GetPackageName(), activity) - elif activity.startswith('.'): - activity = '%s%s' % (self.GetPackageName(), activity) - return activity + for activity in _IterateExportedActivities(manifest_info): + if ('android.intent.action.MAIN' in activity.actions and + 'android.intent.category.LAUNCHER' in activity.categories): + return self._ResolveName(activity.name) + return None + + def GetViewActivityName(self): + """Returns name of the first action=View Activity that can handle http.""" + manifest_info = self._GetManifest() + for activity in _IterateExportedActivities(manifest_info): + if ('android.intent.action.VIEW' in activity.actions and + 'http' in activity.schemes): + return self._ResolveName(activity.name) + return None def GetInstrumentationName( self, default='android.test.InstrumentationTestRunner'): @@ -110,7 +177,7 @@ class ApkHelper(object): raise base_error.BaseError( 'There is more than one instrumentation. Expected one.') else: - return all_instrumentations[0]['android:name'] + return self._ResolveName(all_instrumentations[0]['android:name']) def GetAllInstrumentations( self, default='android.test.InstrumentationTestRunner'): @@ -148,17 +215,30 @@ class ApkHelper(object): """Returns whether any services exist that use isolatedProcess=true.""" manifest_info = self._GetManifest() try: - applications = manifest_info['manifest'][0].get('application', []) - services = itertools.chain( - *(application.get('service', []) for application in applications)) + application = manifest_info['manifest'][0]['application'][0] + services = application['service'] return any( - int(s.get('android:isolatedProcess', '0'), 0) - for s in services) + _ParseNumericKey(s, 'android:isolatedProcess') for s in services) except KeyError: return False + def GetAllMetadata(self): + """Returns a list meta-data tags as (name, value) tuples.""" + manifest_info = self._GetManifest() + try: + application = manifest_info['manifest'][0]['application'][0] + metadata = application['meta-data'] + return [(x.get('android:name'), x.get('android:value')) for x in metadata] + except KeyError: + return [] + def _GetManifest(self): if not self._manifest: self._manifest = _ParseManifestFromApk(self._apk_path) return self._manifest + def _ResolveName(self, name): + name = name.lstrip('.') + if '.' not in name: + return '%s.%s' % (self.GetPackageName(), name) + return name |