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]
32genDir = sys.argv[5]
33inputFiles = sys.argv[6:]
34
35tempDir = f"{genDir}/temp"
36outDir = f"{genDir}/out"
37
38os.makedirs(tempDir, exist_ok=True)
39os.makedirs(outDir, exist_ok=True)
40
41packageNamesOutput = open(f"{outDir}/{GENERATED_APK_PACKAGE_NAMES_FILE}", "w")
42
43usedHashCodes = {}
44
45for inputFile in inputFiles:
46    text = open(inputFile, "r").read()
47
48    annotationIndex = 0
49    while True:
50        try:
51            annotationIndex = text.index(ANNOTATION, annotationIndex)
52            if annotationIndex < 0:
53                break
54        except:
55            break
56
57        annotationIndex += len(ANNOTATION)
58        startIndex = text.index(TRIPLE_QUOTE, annotationIndex)
59        if startIndex < 0:
60            continue
61
62        endIndex = text.index(TRIPLE_QUOTE, startIndex + len(TRIPLE_QUOTE))
63        if endIndex < 0:
64            continue
65
66        string = text[startIndex + len(TRIPLE_QUOTE): endIndex]
67        hashCode = java_string_hashcode(string)
68
69        if hashCode in usedHashCodes:
70            if usedHashCodes[hashCode] != string:
71                sys.exit(f"Manifest XML with hash code {hashCode} already used: {string}")
72        usedHashCodes[hashCode] = string
73
74        if ANDROID_NAMESPACE not in string:
75            string = string.replace("<manifest", f"<manifest {ANDROID_NAMESPACE}\n", 1)
76
77        packageName = PACKAGE_NAME_PREFIX + hashCode
78        string = string.replace(">", f"\npackage=\"{packageName}\"\n>", 1)
79        string = string.replace("<application", "<application\nandroid:hasCode=\"false\"\n")
80
81        outputPath = f"{tempDir}/{hashCode}.xml"
82        outputFile = open(outputPath, "w")
83        outputFile.write(string)
84        outputFile.close()
85
86        packageNamesOutput.write(packageName)
87        packageNamesOutput.write("\n")
88
89        apkPath = f"{outDir}/{hashCode}.apk"
90
91        subprocess.run([
92            aapt2, "link",
93            "-I", frameworkRes,
94            "--manifest", outputPath,
95            "--warn-manifest-validation",
96            "--rename-manifest-package", packageName,
97            "-o", apkPath
98        ], check = True)
99
100        subprocess.run([
101            apkSigner, "sign",
102            "--ks", keyStore,
103            "--ks-pass", "pass:password",
104            apkPath
105        ], check = True)
106
107        # apksigner will generate an idsig file, but it's not useful for the test, so get rid of it
108        idsigPath = f"{outDir}/{hashCode}.idsig"
109        if os.path.isfile(idsigPath):
110            os.remove(idsigPath)
111