1#  Copyright (C) 2021 The Android Open Source Project
2#
3#  Licensed under the Apache License, Version 2.0 (the "License");
4#  you may not use this file except in compliance with the License.
5#  You may obtain a copy of the License at
6#
7#       http://www.apache.org/licenses/LICENSE-2.0
8#
9#  Unless required by applicable law or agreed to in writing, software
10#  distributed under the License is distributed on an "AS IS" BASIS,
11#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12#  See the License for the specific language governing permissions and
13#  limitations under the License.
14
15import argparse
16from pathlib import Path
17import subprocess
18import queue
19from src.library.main.proto.testapp_protos_pb2 import TestAppIndex, AndroidApp, UsesSdk,\
20    Permission, Activity, IntentFilter, Service, Metadata
21
22ELEMENT = "E"
23ATTRIBUTE = "A"
24
25def main():
26    args_parser = argparse.ArgumentParser(description='Generate index for test apps')
27    args_parser.add_argument('--directory', help='Directory containing test apps')
28    args_parser.add_argument('--aapt2', help='The path to aapt2')
29    args = args_parser.parse_args()
30
31    pathlist = Path(args.directory).rglob('*.apk')
32    file_names = [p.name for p in pathlist]
33
34    index = TestAppIndex()
35
36    for file_name in file_names:
37        aapt2_command = [
38            args.aapt2, 'd', 'xmltree', '--file', 'AndroidManifest.xml', args.directory + "/" + file_name]
39        index.apps.append(parse(str(subprocess.check_output(aapt2_command)), file_name))
40
41    with open(args.directory + "/index.txt", "wb") as fd:
42        fd.write(index.SerializeToString())
43
44class XmlTreeLine:
45    """ A single line taken from the aapt2 xmltree output. """
46
47    def __init__(self, line, children):
48        self.line = line
49        self.children = children
50
51    def __str__(self):
52        return str(self.line) + "{" + ", ".join([str(s) for s in self.children]) + "}"
53
54class Element:
55    """ An XML element. """
56
57    def __init__(self, name, attributes, children):
58        self.name = name
59        self.attributes = attributes
60        self.children = children
61
62    def __str__(self):
63        return "Element(" + self.name +  " " + str(self.attributes) + ")"
64
65def parse_lines(manifest_content):
66    return parse_line(manifest_content, 0)[1]
67
68def parse_line(manifest_content, ptr, incoming_indentation = -1):
69    line = manifest_content[ptr]
70    line_without_indentation = line.lstrip(" ")
71    indentation_size = len(line) - len(line_without_indentation)
72
73    if (indentation_size <= incoming_indentation):
74        return ptr, None
75
76    ptr += 1
77    children = []
78
79    while (ptr < len(manifest_content)):
80        ptr, next_child = parse_line(manifest_content, ptr, indentation_size)
81        if next_child:
82            children.append(next_child)
83        else:
84            break
85
86    return ptr, XmlTreeLine(line_without_indentation, children)
87
88def augment(element):
89    """ Convert a XmlTreeLine and descendants into an Element with descendants. """
90    name = None
91    if element.line:
92        name = element.line[3:].split(" ", 1)[0]
93    attributes = {}
94    children = []
95
96    children_to_process = queue.Queue()
97    for c in element.children:
98        children_to_process.put(c)
99
100    while not children_to_process.empty():
101        c = children_to_process.get()
102        if c.line.startswith("E"):
103            # Is an element
104            children.append(augment(c))
105        elif c.line.startswith("A"):
106            # Is an attribute
107            attribute_name = c.line[3:].split("=", 1)[0]
108            if ":" in attribute_name:
109                attribute_name = attribute_name.rsplit(":", 1)[1]
110            attribute_name = attribute_name.split("(", 1)[0]
111            attribute_value = c.line.split("=", 1)[1].split(" (Raw", 1)[0]
112            if attribute_value[0] == '"':
113                attribute_value = attribute_value[1:-1]
114            attributes[attribute_name] = attribute_value
115
116            # Children of the attribute are actually children of the element itself
117            for child in c.children:
118                children_to_process.put(child)
119        else:
120            raise Exception("Unknown line type for line: " + c.line)
121
122    return Element(name, attributes, children)
123
124def parse(manifest_content, file_name):
125    manifest_content = manifest_content.split("\\n")
126    # strip namespaces as not important for our uses
127    # Also strip the last line which is a quotation mark because of the way it's imported
128    manifest_content = [m for m in manifest_content if not "N: " in m][:-1]
129
130    simple_root = parse_lines(manifest_content)
131    root = augment(simple_root)
132
133    android_app = AndroidApp()
134    android_app.apk_name = file_name
135    android_app.package_name = root.attributes["package"]
136    android_app.sharedUserId = root.attributes.get("sharedUserId", "")
137
138    parse_uses_sdk(root, android_app)
139    parse_permissions(root, android_app)
140
141    application_element = find_single_element(root.children, "application")
142    android_app.test_only = application_element.attributes.get("testOnly", "false") == "true"
143
144    parse_activities(application_element, android_app)
145    parse_services(application_element, android_app)
146    parse_metadata(application_element, android_app)
147
148    return android_app
149
150def parse_uses_sdk(root, android_app):
151    uses_sdk_element = find_single_element(root.children, "uses-sdk")
152    if uses_sdk_element:
153        if "minSdkVersion" in uses_sdk_element.attributes:
154            try:
155                android_app.uses_sdk.minSdkVersion = int(uses_sdk_element.attributes["minSdkVersion"])
156            except ValueError:
157                pass
158        if "maxSdkVersion" in uses_sdk_element.attributes:
159            try:
160                android_app.uses_sdk.maxSdkVersion = int(uses_sdk_element.attributes["maxSdkVersion"])
161            except ValueError:
162                pass
163        if "targetSdkVersion" in uses_sdk_element.attributes:
164            try:
165                android_app.uses_sdk.targetSdkVersion = int(uses_sdk_element.attributes["targetSdkVersion"])
166            except ValueError:
167                pass
168
169def parse_permissions(root, android_app):
170    for permission_element in find_elements(root.children, "uses-permission"):
171        permission = Permission()
172        permission.name = permission_element.attributes["name"]
173        android_app.permissions.append(permission)
174
175def parse_activities(application_element, android_app):
176    for activity_element in find_elements(application_element.children, "activity"):
177        activity = Activity()
178
179        activity.name = activity_element.attributes["name"]
180        if activity.name.startswith("androidx"):
181            continue # Special case: androidx adds non-logging activities
182
183        activity.exported = activity_element.attributes.get("exported", "false") == "true"
184
185        parse_intent_filters(activity_element, activity)
186        android_app.activities.append(activity)
187
188def parse_intent_filters(element, parent):
189    for intent_filter_element in find_elements(element.children, "intent-filter"):
190        intent_filter = IntentFilter()
191
192        parse_intent_filter_actions(intent_filter_element, intent_filter)
193        parse_intent_filter_category(intent_filter_element, intent_filter)
194        parent.intent_filters.append(intent_filter)
195
196def parse_intent_filter_actions(intent_filter_element, intent_filter):
197    for action_element in find_elements(intent_filter_element.children, "action"):
198        action = action_element.attributes["name"]
199        intent_filter.actions.append(action)
200
201def parse_intent_filter_category(intent_filter_element, intent_filter):
202    for category_element in find_elements(intent_filter_element.children, "category"):
203        category = category_element.attributes["name"]
204        intent_filter.categories.append(category)
205
206def parse_services(application_element, android_app):
207    for service_element in find_elements(application_element.children, "service"):
208        service = Service()
209        service.name = service_element.attributes["name"]
210        parse_intent_filters(service_element, service)
211        android_app.services.append(service)
212
213def parse_metadata(application_element, android_app):
214    for meta_data_element in find_elements(application_element.children, "meta-data"):
215        metadata = Metadata()
216        metadata.name = meta_data_element.attributes["name"]
217        # This forces every value into a string
218        metadata.value = meta_data_element.attributes["value"]
219        android_app.metadata.append(metadata)
220
221def find_single_element(element_collection, element_name):
222    for e in element_collection:
223        if e.name == element_name:
224            return e
225
226def find_elements(element_collection, element_name):
227    return [e for e in element_collection if e.name == element_name]
228
229if __name__ == "__main__":
230    main()