• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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