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, ActivityAlias, IntentFilter, Service, Metadata, Receiver
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    android_app.label = application_element.attributes.get("label", "")
144    android_app.cross_profile = application_element.attributes.get("crossProfile", "false") == "true"
145
146    parse_activity_aliases(application_element, android_app)
147    parse_activities(application_element, android_app)
148    parse_services(application_element, android_app)
149    parse_metadata(application_element, android_app)
150    parse_receiver(application_element, android_app)
151
152    return android_app
153
154def parse_uses_sdk(root, android_app):
155    uses_sdk_element = find_single_element(root.children, "uses-sdk")
156    if uses_sdk_element:
157        if "minSdkVersion" in uses_sdk_element.attributes:
158            try:
159                android_app.uses_sdk.minSdkVersion = int(uses_sdk_element.attributes["minSdkVersion"])
160            except ValueError:
161                pass
162        if "maxSdkVersion" in uses_sdk_element.attributes:
163            try:
164                android_app.uses_sdk.maxSdkVersion = int(uses_sdk_element.attributes["maxSdkVersion"])
165            except ValueError:
166                pass
167        if "targetSdkVersion" in uses_sdk_element.attributes:
168            try:
169                android_app.uses_sdk.targetSdkVersion = int(uses_sdk_element.attributes["targetSdkVersion"])
170            except ValueError:
171                pass
172
173def parse_permissions(root, android_app):
174    for permission_element in find_elements(root.children, "uses-permission"):
175        permission = Permission()
176        permission.name = permission_element.attributes["name"]
177        android_app.permissions.append(permission)
178
179def parse_activities(application_element, android_app):
180    for activity_element in find_elements(application_element.children, "activity"):
181        activity = Activity()
182
183        activity.name = activity_element.attributes["name"]
184        if activity.name.startswith("androidx"):
185            continue # Special case: androidx adds non-logging activities
186
187        activity.exported = activity_element.attributes.get("exported", "false") == "true"
188        activity.permission = activity_element.attributes.get("permission", "")
189
190        parse_intent_filters(activity_element, activity)
191        android_app.activities.append(activity)
192
193def parse_activity_aliases(application_element, android_app):
194    for activity_alias_element in find_elements(application_element.children, "activity-alias"):
195        activity_alias = ActivityAlias()
196
197        activity_alias.name = activity_alias_element.attributes["name"]
198        if activity_alias.name.startswith("androidx"):
199            continue # Special case: androidx adds non-logging activity-aliases
200
201        activity_alias.exported = activity_alias_element.attributes.get("exported", "false") == "true"
202        activity_alias.permission = activity_alias_element.attributes.get("permission", "")
203
204        parse_intent_filters(activity_alias_element, activity_alias)
205        android_app.activityAliases.append(activity_alias)
206
207def parse_intent_filters(element, parent):
208    for intent_filter_element in find_elements(element.children, "intent-filter"):
209        intent_filter = IntentFilter()
210
211        parse_intent_filter_actions(intent_filter_element, intent_filter)
212        parse_intent_filter_category(intent_filter_element, intent_filter)
213        parent.intent_filters.append(intent_filter)
214
215def parse_intent_filter_actions(intent_filter_element, intent_filter):
216    for action_element in find_elements(intent_filter_element.children, "action"):
217        action = action_element.attributes["name"]
218        intent_filter.actions.append(action)
219
220def parse_intent_filter_category(intent_filter_element, intent_filter):
221    for category_element in find_elements(intent_filter_element.children, "category"):
222        category = category_element.attributes["name"]
223        intent_filter.categories.append(category)
224
225def parse_services(application_element, android_app):
226    for service_element in find_elements(application_element.children, "service"):
227        service = Service()
228        service.name = service_element.attributes["name"]
229        parse_intent_filters(service_element, service)
230        android_app.services.append(service)
231
232def parse_metadata(application_element, android_app):
233    for meta_data_element in find_elements(application_element.children, "meta-data"):
234        metadata = Metadata()
235        metadata.name = meta_data_element.attributes["name"]
236
237        if "value" in meta_data_element.attributes:
238            # This forces every value into a string
239            metadata.value = meta_data_element.attributes["value"]
240
241        android_app.metadata.append(metadata)
242
243def parse_receiver(application_element, android_app):
244    for receiver_element in find_elements(application_element.children, "receiver"):
245        parse_metadata(receiver_element, android_app)
246
247        receiver = Receiver()
248        receiver.name = receiver_element.attributes["name"]
249        receiver.permission = receiver_element.attributes.get("permission", "")
250        receiver.exported = receiver_element.attributes.get("exported", "false") == "true"
251        parse_metadata(receiver_element, receiver)
252        parse_intent_filters(receiver_element, receiver)
253        android_app.receivers.append(receiver)
254
255def find_single_element(element_collection, element_name):
256    for e in element_collection:
257        if e.name == element_name:
258            return e
259
260def find_elements(element_collection, element_name):
261    return [e for e in element_collection if e.name == element_name]
262
263if __name__ == "__main__":
264    main()