aboutsummaryrefslogtreecommitdiff
path: root/catapult/devil/devil/android/apk_helper.py
diff options
context:
space:
mode:
Diffstat (limited to 'catapult/devil/devil/android/apk_helper.py')
-rw-r--r--catapult/devil/devil/android/apk_helper.py130
1 files changed, 121 insertions, 9 deletions
diff --git a/catapult/devil/devil/android/apk_helper.py b/catapult/devil/devil/android/apk_helper.py
index ab7649f8..abdf9071 100644
--- a/catapult/devil/devil/android/apk_helper.py
+++ b/catapult/devil/devil/android/apk_helper.py
@@ -5,10 +5,13 @@
"""Module containing utilities for apk packages."""
import re
+import xml.etree.ElementTree
import zipfile
from devil import base_error
+from devil.android.ndk import abis
from devil.android.sdk import aapt
+from devil.utils import cmd_helper
_MANIFEST_ATTRIBUTE_RE = re.compile(
@@ -45,9 +48,8 @@ def ToHelper(path_or_helper):
# 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')
-
+def _ParseManifestFromApk(apk):
+ aapt_output = aapt.Dump('xmltree', apk.path, 'AndroidManifest.xml')
parsed_manifest = {}
node_stack = [parsed_manifest]
indent = ' '
@@ -96,7 +98,8 @@ def _ParseManifestFromApk(apk_path):
manifest_key = m.group(1)
if manifest_key in node:
raise base_error.BaseError(
- "A single attribute should have one key and one value")
+ "A single attribute should have one key and one value: {}"
+ .format(line))
else:
node[manifest_key] = m.group(2) or m.group(3)
continue
@@ -104,6 +107,47 @@ def _ParseManifestFromApk(apk_path):
return parsed_manifest
+def _ParseManifestFromBundle(bundle):
+ cmd = [bundle.path, 'dump-manifest']
+ status, stdout, stderr = cmd_helper.GetCmdStatusOutputAndError(cmd)
+ if status != 0:
+ raise Exception('Failed running {} with output\n{}\n{}'.format(
+ ' '.join(cmd), stdout, stderr))
+ return ParseManifestFromXml(stdout)
+
+
+def ParseManifestFromXml(xml_str):
+ """Parse an android bundle manifest.
+
+ As ParseManifestFromAapt, but uses the xml output from bundletool. Each
+ element is a dict, mapping attribute or children by name. Attributes map to
+ a dict (as they are unique), children map to a list of dicts (as there may
+ be multiple children with the same name).
+
+ Args:
+ xml_str (str) An xml string that is an android manifest.
+
+ Returns:
+ A dict holding the parsed manifest, as with ParseManifestFromAapt.
+ """
+ root = xml.etree.ElementTree.fromstring(xml_str)
+ return {root.tag: [_ParseManifestXMLNode(root)]}
+
+
+def _ParseManifestXMLNode(node):
+ out = {}
+ for name, value in node.attrib.items():
+ cleaned_name = name.replace(
+ '{http://schemas.android.com/apk/res/android}',
+ 'android:').replace(
+ '{http://schemas.android.com/tools}',
+ 'tools:')
+ out[cleaned_name] = value
+ for child in node:
+ out.setdefault(child.tag, []).append(_ParseManifestXMLNode(child))
+ return out
+
+
def _ParseNumericKey(obj, key, default=0):
val = obj.get(key)
if val is None:
@@ -152,6 +196,10 @@ class ApkHelper(object):
def path(self):
return self._apk_path
+ @property
+ def is_bundle(self):
+ return self._apk_path.endswith('_bundle')
+
def GetActivityName(self):
"""Returns the name of the first launcher Activity in the apk."""
manifest_info = self._GetManifest()
@@ -233,9 +281,73 @@ class ApkHelper(object):
except KeyError:
return []
+ def GetVersionCode(self):
+ """Returns the versionCode as an integer, or None if not available."""
+ manifest_info = self._GetManifest()
+ try:
+ version_code = manifest_info['manifest'][0]['android:versionCode']
+ return int(version_code, 16)
+ except KeyError:
+ return None
+
+ def GetVersionName(self):
+ """Returns the versionName as a string."""
+ manifest_info = self._GetManifest()
+ try:
+ version_name = manifest_info['manifest'][0]['android:versionName']
+ return version_name
+ except KeyError:
+ return ''
+
+ def GetMinSdkVersion(self):
+ """Returns the minSdkVersion as a string, or None if not available.
+
+ Note: this cannot always be cast to an integer."""
+ manifest_info = self._GetManifest()
+ try:
+ uses_sdk = manifest_info['manifest'][0]['uses-sdk'][0]
+ min_sdk_version = uses_sdk['android:minSdkVersion']
+ try:
+ # The common case is for this to be an integer. Convert to decimal
+ # notation (rather than hexadecimal) for readability, but convert back
+ # to a string for type consistency with the general case.
+ return str(int(min_sdk_version, 16))
+ except ValueError:
+ # In general (ex. apps with minSdkVersion set to pre-release Android
+ # versions), minSdkVersion can be a string (usually, the OS codename
+ # letter). For simplicity, don't do any validation on the value.
+ return min_sdk_version
+ except KeyError:
+ return None
+
+ def GetTargetSdkVersion(self):
+ """Returns the targetSdkVersion as a string, or None if not available.
+
+ Note: this cannot always be cast to an integer."""
+ manifest_info = self._GetManifest()
+ try:
+ uses_sdk = manifest_info['manifest'][0]['uses-sdk'][0]
+ target_sdk_version = uses_sdk['android:targetSdkVersion']
+ try:
+ # The common case is for this to be an integer. Convert to decimal
+ # notation (rather than hexadecimal) for readability, but convert back
+ # to a string for type consistency with the general case.
+ return str(int(target_sdk_version, 16))
+ except ValueError:
+ # In general (ex. apps targeting pre-release Android versions),
+ # targetSdkVersion can be a string (usually, the OS codename letter).
+ # For simplicity, don't do any validation on the value.
+ return target_sdk_version
+ except KeyError:
+ return None
+
def _GetManifest(self):
if not self._manifest:
- self._manifest = _ParseManifestFromApk(self._apk_path)
+ app = ToHelper(self._apk_path)
+ if app.is_bundle:
+ self._manifest = _ParseManifestFromBundle(app)
+ else:
+ self._manifest = _ParseManifestFromApk(app)
return self._manifest
def _ResolveName(self, name):
@@ -257,10 +369,10 @@ class ApkHelper(object):
if len(path_tokens) >= 2 and path_tokens[0] == 'lib':
libs.add(path_tokens[1])
lib_to_abi = {
- 'armeabi-v7a': ['armeabi-v7a', 'arm64-v8a'],
- 'arm64-v8a': ['arm64-v8a'],
- 'x86': ['x86', 'x64'],
- 'x64': ['x64']
+ abis.ARM: [abis.ARM, abis.ARM_64],
+ abis.ARM_64: [abis.ARM_64],
+ abis.X86: [abis.X86, abis.X86_64],
+ abis.X86_64: [abis.X86_64]
}
try:
output = set()