• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""
2Parses @AndroidManifestXml annotations to pre-generate APKs for tests
3
4Assumes a Kotlin triple quoted string literal which is prepended by a custom annotation. See the
5AndroidManifestXml annotation for more information.
6"""
7
8import subprocess
9import sys
10import os
11
12ANNOTATION = "@AndroidManifestXml"
13TRIPLE_QUOTE = "\"\"\""
14PACKAGE_NAME_PREFIX = "android.content.pm.parsing.cts.generated"
15GENERATED_APK_PACKAGE_NAMES_FILE = "GeneratedApkPackageNames.txt"
16ANDROID_NAMESPACE = "xmlns:android=\"http://schemas.android.com/apk/res/android\""
17
18def java_string_hashcode(string):
19    """
20        Simulates Java's hashCode so that APKs can be looked up at runtime by using the manifest
21        hashCode as the file name.
22    """
23    hash = 0
24    for char in string:
25        hash = int((((31 * hash + ord(char)) ^ 0x80000000) & 0xFFFFFFFF) - 0x80000000)
26    return str(abs(hash))
27
28aapt2 = sys.argv[1]
29frameworkRes = sys.argv[2]
30apkSigner = sys.argv[3]
31keyStore = sys.argv[4]
32platformSdkVersion = sys.argv[5]
33genDir = sys.argv[6]
34inputFiles = sys.argv[7:]
35
36tempDir = f"{genDir}/temp"
37outDir = f"{genDir}/out"
38
39os.makedirs(tempDir, exist_ok=True)
40os.makedirs(outDir, exist_ok=True)
41
42packageNamesOutput = open(f"{outDir}/{GENERATED_APK_PACKAGE_NAMES_FILE}", "w")
43
44usedHashCodes = {}
45
46for inputFile in inputFiles:
47    text = open(inputFile, "r").read()
48
49    annotationIndex = 0
50    while True:
51        try:
52            annotationIndex = text.index(ANNOTATION, annotationIndex)
53            if annotationIndex < 0:
54                break
55        except:
56            break
57
58        annotationIndex += len(ANNOTATION)
59        startIndex = text.index(TRIPLE_QUOTE, annotationIndex)
60        if startIndex < 0:
61            continue
62
63        endIndex = text.index(TRIPLE_QUOTE, startIndex + len(TRIPLE_QUOTE))
64        if endIndex < 0:
65            continue
66
67        string = text[startIndex + len(TRIPLE_QUOTE): endIndex]
68        hashCode = java_string_hashcode(string)
69
70        if hashCode in usedHashCodes:
71            if usedHashCodes[hashCode] != string:
72                sys.exit(f"Manifest XML with hash code {hashCode} already used: {string}")
73        usedHashCodes[hashCode] = string
74
75        if ANDROID_NAMESPACE not in string:
76            string = string.replace("<manifest", f"<manifest {ANDROID_NAMESPACE}\n", 1)
77
78        packageName = PACKAGE_NAME_PREFIX + hashCode
79        string = string.replace(">", f"\npackage=\"{packageName}\"\n>", 1)
80        string = string.replace("<application", "<application\nandroid:hasCode=\"false\"\n")
81        major_version = platformSdkVersion.split(".")[0]
82        string = string.replace("platformSdkVersion", major_version)
83
84        outputPath = f"{tempDir}/{hashCode}.xml"
85        outputFile = open(outputPath, "w")
86        outputFile.write(string)
87        outputFile.close()
88
89        packageNamesOutput.write(packageName)
90        packageNamesOutput.write("\n")
91
92        apkPath = f"{outDir}/{hashCode}.apk"
93
94        subprocess.run([
95            aapt2, "link",
96            "-I", frameworkRes,
97            "--manifest", outputPath,
98            "--warn-manifest-validation",
99            "--rename-manifest-package", packageName,
100            "-o", apkPath
101        ], check = True)
102
103        subprocess.run([
104            apkSigner, "sign",
105            "--ks", keyStore,
106            "--ks-pass", "pass:password",
107            apkPath
108        ], check = True)
109
110        # apksigner will generate an idsig file, but it's not useful for the test, so get rid of it
111        idsigPath = f"{outDir}/{hashCode}.idsig"
112        if os.path.isfile(idsigPath):
113            os.remove(idsigPath)
114