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