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