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 5"""Module containing utilities for apk packages.""" 6 7import re 8 9from devil import base_error 10from devil.android.sdk import aapt 11 12 13_MANIFEST_ATTRIBUTE_RE = re.compile( 14 r'\s*A: ([^\(\)= ]*)(?:\([^\(\)= ]*\))?=' 15 r'(?:"(.*)" \(Raw: .*\)|\(type.*?\)(.*))$') 16_MANIFEST_ELEMENT_RE = re.compile(r'\s*(?:E|N): (\S*) .*$') 17 18 19def GetPackageName(apk_path): 20 """Returns the package name of the apk.""" 21 return ApkHelper(apk_path).GetPackageName() 22 23 24# TODO(jbudorick): Deprecate and remove this function once callers have been 25# converted to ApkHelper.GetInstrumentationName 26def GetInstrumentationName(apk_path): 27 """Returns the name of the Instrumentation in the apk.""" 28 return ApkHelper(apk_path).GetInstrumentationName() 29 30 31def ToHelper(path_or_helper): 32 """Creates an ApkHelper unless one is already given.""" 33 if isinstance(path_or_helper, basestring): 34 return ApkHelper(path_or_helper) 35 return path_or_helper 36 37 38# To parse the manifest, the function uses a node stack where at each level of 39# the stack it keeps the currently in focus node at that level (of indentation 40# in the xmltree output, ie. depth in the tree). The height of the stack is 41# determinded by line indentation. When indentation is increased so is the stack 42# (by pushing a new empty node on to the stack). When indentation is decreased 43# the top of the stack is popped (sometimes multiple times, until indentation 44# matches the height of the stack). Each line parsed (either an attribute or an 45# element) is added to the node at the top of the stack (after the stack has 46# been popped/pushed due to indentation). 47def _ParseManifestFromApk(apk_path): 48 aapt_output = aapt.Dump('xmltree', apk_path, 'AndroidManifest.xml') 49 50 parsed_manifest = {} 51 node_stack = [parsed_manifest] 52 indent = ' ' 53 54 if aapt_output[0].startswith('N'): 55 # if the first line is a namespace then the root manifest is indented, and 56 # we need to add a dummy namespace node, then skip the first line (we dont 57 # care about namespaces). 58 node_stack.insert(0, {}) 59 output_to_parse = aapt_output[1:] 60 else: 61 output_to_parse = aapt_output 62 63 for line in output_to_parse: 64 if len(line) == 0: 65 continue 66 67 # If namespaces are stripped, aapt still outputs the full url to the 68 # namespace and appends it to the attribute names. 69 line = line.replace('http://schemas.android.com/apk/res/android:', 'android:') 70 71 indent_depth = 0 72 while line[(len(indent) * indent_depth):].startswith(indent): 73 indent_depth += 1 74 75 # Pop the stack until the height of the stack is the same is the depth of 76 # the current line within the tree. 77 node_stack = node_stack[:indent_depth + 1] 78 node = node_stack[-1] 79 80 # Element nodes are a list of python dicts while attributes are just a dict. 81 # This is because multiple elements, at the same depth of tree and the same 82 # name, are all added to the same list keyed under the element name. 83 m = _MANIFEST_ELEMENT_RE.match(line[len(indent) * indent_depth:]) 84 if m: 85 manifest_key = m.group(1) 86 if manifest_key in node: 87 node[manifest_key] += [{}] 88 else: 89 node[manifest_key] = [{}] 90 node_stack += [node[manifest_key][-1]] 91 continue 92 93 m = _MANIFEST_ATTRIBUTE_RE.match(line[len(indent) * indent_depth:]) 94 if m: 95 manifest_key = m.group(1) 96 if manifest_key in node: 97 raise base_error.BaseError( 98 "A single attribute should have one key and one value") 99 else: 100 node[manifest_key] = m.group(2) or m.group(3) 101 continue 102 103 return parsed_manifest 104 105 106def _ParseNumericKey(obj, key, default=0): 107 val = obj.get(key) 108 if val is None: 109 return default 110 return int(val, 0) 111 112 113class _ExportedActivity(object): 114 def __init__(self, name): 115 self.name = name 116 self.actions = set() 117 self.categories = set() 118 self.schemes = set() 119 120 121def _IterateExportedActivities(manifest_info): 122 app_node = manifest_info['manifest'][0]['application'][0] 123 activities = app_node.get('activity', []) + app_node.get('activity-alias', []) 124 for activity_node in activities: 125 # Presence of intent filters make an activity exported by default. 126 has_intent_filter = 'intent-filter' in activity_node 127 if not _ParseNumericKey( 128 activity_node, 'android:exported', default=has_intent_filter): 129 continue 130 131 activity = _ExportedActivity(activity_node.get('android:name')) 132 # Merge all intent-filters into a single set because there is not 133 # currently a need to keep them separate. 134 for intent_filter in activity_node.get('intent-filter', []): 135 for action in intent_filter.get('action', []): 136 activity.actions.add(action.get('android:name')) 137 for category in intent_filter.get('category', []): 138 activity.categories.add(category.get('android:name')) 139 for data in intent_filter.get('data', []): 140 activity.schemes.add(data.get('android:scheme')) 141 yield activity 142 143 144class ApkHelper(object): 145 146 def __init__(self, path): 147 self._apk_path = path 148 self._manifest = None 149 150 @property 151 def path(self): 152 return self._apk_path 153 154 def GetActivityName(self): 155 """Returns the name of the first launcher Activity in the apk.""" 156 manifest_info = self._GetManifest() 157 for activity in _IterateExportedActivities(manifest_info): 158 if ('android.intent.action.MAIN' in activity.actions and 159 'android.intent.category.LAUNCHER' in activity.categories): 160 return self._ResolveName(activity.name) 161 return None 162 163 def GetViewActivityName(self): 164 """Returns name of the first action=View Activity that can handle http.""" 165 manifest_info = self._GetManifest() 166 for activity in _IterateExportedActivities(manifest_info): 167 if ('android.intent.action.VIEW' in activity.actions and 168 'http' in activity.schemes): 169 return self._ResolveName(activity.name) 170 return None 171 172 def GetInstrumentationName( 173 self, default='android.test.InstrumentationTestRunner'): 174 """Returns the name of the Instrumentation in the apk.""" 175 all_instrumentations = self.GetAllInstrumentations(default=default) 176 if len(all_instrumentations) != 1: 177 raise base_error.BaseError( 178 'There is more than one instrumentation. Expected one.') 179 else: 180 return self._ResolveName(all_instrumentations[0]['android:name']) 181 182 def GetAllInstrumentations( 183 self, default='android.test.InstrumentationTestRunner'): 184 """Returns a list of all Instrumentations in the apk.""" 185 try: 186 return self._GetManifest()['manifest'][0]['instrumentation'] 187 except KeyError: 188 return [{'android:name': default}] 189 190 def GetPackageName(self): 191 """Returns the package name of the apk.""" 192 manifest_info = self._GetManifest() 193 try: 194 return manifest_info['manifest'][0]['package'] 195 except KeyError: 196 raise Exception('Failed to determine package name of %s' % self._apk_path) 197 198 def GetPermissions(self): 199 manifest_info = self._GetManifest() 200 try: 201 return [p['android:name'] for 202 p in manifest_info['manifest'][0]['uses-permission']] 203 except KeyError: 204 return [] 205 206 def GetSplitName(self): 207 """Returns the name of the split of the apk.""" 208 manifest_info = self._GetManifest() 209 try: 210 return manifest_info['manifest'][0]['split'] 211 except KeyError: 212 return None 213 214 def HasIsolatedProcesses(self): 215 """Returns whether any services exist that use isolatedProcess=true.""" 216 manifest_info = self._GetManifest() 217 try: 218 application = manifest_info['manifest'][0]['application'][0] 219 services = application['service'] 220 return any( 221 _ParseNumericKey(s, 'android:isolatedProcess') for s in services) 222 except KeyError: 223 return False 224 225 def GetAllMetadata(self): 226 """Returns a list meta-data tags as (name, value) tuples.""" 227 manifest_info = self._GetManifest() 228 try: 229 application = manifest_info['manifest'][0]['application'][0] 230 metadata = application['meta-data'] 231 return [(x.get('android:name'), x.get('android:value')) for x in metadata] 232 except KeyError: 233 return [] 234 235 def _GetManifest(self): 236 if not self._manifest: 237 self._manifest = _ParseManifestFromApk(self._apk_path) 238 return self._manifest 239 240 def _ResolveName(self, name): 241 name = name.lstrip('.') 242 if '.' not in name: 243 return '%s.%s' % (self.GetPackageName(), name) 244 return name 245