xref: /aosp_15_r20/cts/common/device-side/bedstead/testapp/tools/index/index_testapps.py (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
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
19try:
20    from src.library.main.proto.testapp_protos_pb2 import TestAppIndex, AndroidApp, UsesSdk,\
21    Permission, Activity, ActivityAlias, IntentFilter, Service, Metadata, Receiver
22except ImportError:
23    from google3.third_party.java.bedstead.testapp.src.library.main.proto.testapp_protos_pb2\
24    import TestAppIndex, AndroidApp, UsesSdk,\
25        Permission, Activity, ActivityAlias, IntentFilter, Service, Metadata, Receiver
26
27ELEMENT = "E"
28ATTRIBUTE = "A"
29
30def main():
31    args_parser = argparse.ArgumentParser(description='Generate index for test apps and resources')
32    args_parser.add_argument('--directory', help='Directory containing raw resources')
33    args_parser.add_argument('--aapt2', help='The path to aapt2')
34    args = args_parser.parse_args()
35
36    pathlist = Path(args.directory).rglob('*.apk')
37    file_names = [p.name for p in pathlist]
38
39    index = TestAppIndex()
40
41    for file_name in file_names:
42        aapt2_command = [
43            args.aapt2, 'd', 'xmltree', '--file', 'AndroidManifest.xml', args.directory + "/" + file_name]
44        index.apps.append(parse(str(subprocess.check_output(aapt2_command)), args.directory, file_name, args.aapt2))
45
46    with open(args.directory + "/index.txt", "wb") as fd:
47        fd.write(index.SerializeToString())
48
49class XmlTreeLine:
50    """ A single line taken from the aapt2 xmltree output. """
51
52    def __init__(self, line, children):
53        self.line = line
54        self.children = children
55
56    def __str__(self):
57        return str(self.line) + "{" + ", ".join([str(s) for s in self.children]) + "}"
58
59class Element:
60    """ An XML element. """
61
62    def __init__(self, name, attributes, children):
63        self.name = name
64        self.attributes = attributes
65        self.children = children
66
67    def __str__(self):
68        return "Element(" + self.name +  " " + str(self.attributes) + ")"
69
70def parse_lines(manifest_content):
71    return parse_line(manifest_content, 0)[1]
72
73def parse_line(manifest_content, ptr, incoming_indentation = -1):
74    line = manifest_content[ptr]
75    line_without_indentation = line.lstrip(" ")
76    indentation_size = len(line) - len(line_without_indentation)
77
78    if (indentation_size <= incoming_indentation):
79        return ptr, None
80
81    ptr += 1
82    children = []
83
84    while (ptr < len(manifest_content)):
85        ptr, next_child = parse_line(manifest_content, ptr, indentation_size)
86        if next_child:
87            children.append(next_child)
88        else:
89            break
90
91    return ptr, XmlTreeLine(line_without_indentation, children)
92
93def augment(element):
94    """ Convert a XmlTreeLine and descendants into an Element with descendants. """
95    name = None
96    if element.line:
97        name = element.line[3:].split(" ", 1)[0]
98    attributes = {}
99    children = []
100
101    children_to_process = queue.Queue()
102    for c in element.children:
103        children_to_process.put(c)
104
105    while not children_to_process.empty():
106        c = children_to_process.get()
107        if c.line.startswith("E"):
108            # Is an element
109            children.append(augment(c))
110        elif c.line.startswith("A"):
111            # Is an attribute
112            attribute_name = c.line[3:].split("=", 1)[0]
113            if ":" in attribute_name:
114                attribute_name = attribute_name.rsplit(":", 1)[1]
115            attribute_name = attribute_name.split("(", 1)[0]
116            attribute_value = c.line.split("=", 1)[1].split(" (Raw", 1)[0]
117            if attribute_value[0] == '"':
118                attribute_value = attribute_value[1:-1]
119            attributes[attribute_name] = attribute_value
120
121            # Children of the attribute are actually children of the element itself
122            for child in c.children:
123                children_to_process.put(child)
124        else:
125            raise Exception("Unknown line type for line: " + c.line)
126
127    return Element(name, attributes, children)
128
129def parse(manifest_content, raw_dir, file_name, aapt2):
130    manifest_content = manifest_content.split("\\n")
131    # strip namespaces as not important for our uses
132    # Also strip the last line which is a quotation mark because of the way it's imported
133    manifest_content = [m for m in manifest_content if not "N: " in m][:-1]
134
135    simple_root = parse_lines(manifest_content)
136    root = augment(simple_root)
137
138    android_app = AndroidApp()
139    android_app.apk_name = file_name
140    android_app.package_name = root.attributes["package"]
141    android_app.sharedUserId = root.attributes.get("sharedUserId", "")
142
143    parse_uses_sdk(root, android_app)
144    parse_permissions(root, android_app)
145
146    application_element = find_single_element(root.children, "application")
147    android_app.test_only = application_element.attributes.get("testOnly", "false") == "true"
148    android_app.label = application_element.attributes.get("label", "")
149    android_app.cross_profile = application_element.attributes.get("crossProfile", "false") == "true"
150
151    parse_activity_aliases(application_element, android_app)
152    parse_activities(application_element, android_app)
153    parse_services(application_element, android_app)
154    parse_metadata(application_element, android_app)
155    parse_receiver(application_element, android_app)
156
157    return android_app
158
159def parse_uses_sdk(root, android_app):
160    uses_sdk_element = find_single_element(root.children, "uses-sdk")
161    if uses_sdk_element:
162        if "minSdkVersion" in uses_sdk_element.attributes:
163            try:
164                android_app.uses_sdk.minSdkVersion = int(uses_sdk_element.attributes["minSdkVersion"])
165            except ValueError:
166                pass
167        if "maxSdkVersion" in uses_sdk_element.attributes:
168            try:
169                android_app.uses_sdk.maxSdkVersion = int(uses_sdk_element.attributes["maxSdkVersion"])
170            except ValueError:
171                pass
172        if "targetSdkVersion" in uses_sdk_element.attributes:
173            try:
174                android_app.uses_sdk.targetSdkVersion = int(uses_sdk_element.attributes["targetSdkVersion"])
175            except ValueError:
176                pass
177
178def parse_permissions(root, android_app):
179    for permission_element in find_elements(root.children, "uses-permission"):
180        permission = Permission()
181        permission.name = permission_element.attributes["name"]
182        android_app.permissions.append(permission)
183
184def parse_activities(application_element, android_app):
185    for activity_element in find_elements(application_element.children, "activity"):
186        activity = Activity()
187
188        activity.name = activity_element.attributes["name"]
189        if activity.name.startswith("androidx"):
190            continue # Special case: androidx adds non-logging activities
191
192        activity.exported = activity_element.attributes.get("exported", "false") == "true"
193        activity.permission = activity_element.attributes.get("permission", "")
194
195        parse_intent_filters(activity_element, activity)
196        android_app.activities.append(activity)
197
198def parse_activity_aliases(application_element, android_app):
199    for activity_alias_element in find_elements(application_element.children, "activity-alias"):
200        activity_alias = ActivityAlias()
201
202        activity_alias.name = activity_alias_element.attributes["name"]
203        if activity_alias.name.startswith("androidx"):
204            continue # Special case: androidx adds non-logging activity-aliases
205
206        activity_alias.exported = activity_alias_element.attributes.get("exported", "false") == "true"
207        activity_alias.permission = activity_alias_element.attributes.get("permission", "")
208
209        parse_intent_filters(activity_alias_element, activity_alias)
210        android_app.activityAliases.append(activity_alias)
211
212def parse_intent_filters(element, parent):
213    for intent_filter_element in find_elements(element.children, "intent-filter"):
214        intent_filter = IntentFilter()
215
216        parse_intent_filter_actions(intent_filter_element, intent_filter)
217        parse_intent_filter_category(intent_filter_element, intent_filter)
218        parent.intent_filters.append(intent_filter)
219
220def parse_intent_filter_actions(intent_filter_element, intent_filter):
221    for action_element in find_elements(intent_filter_element.children, "action"):
222        action = action_element.attributes["name"]
223        intent_filter.actions.append(action)
224
225def parse_intent_filter_category(intent_filter_element, intent_filter):
226    for category_element in find_elements(intent_filter_element.children, "category"):
227        category = category_element.attributes["name"]
228        intent_filter.categories.append(category)
229
230def parse_services(application_element, android_app):
231    for service_element in find_elements(application_element.children, "service"):
232        parse_metadata(service_element, android_app)
233
234        service = Service()
235        service.name = service_element.attributes["name"]
236        parse_intent_filters(service_element, service)
237        parse_metadata(service_element, service)
238        android_app.services.append(service)
239
240def parse_metadata(application_element, android_app):
241    for meta_data_element in find_elements(application_element.children, "meta-data"):
242        metadata = Metadata()
243        metadata.name = meta_data_element.attributes["name"]
244
245        if "value" in meta_data_element.attributes:
246            # This forces every value into a string
247            metadata.value = meta_data_element.attributes["value"]
248        if "resource" in meta_data_element.attributes:
249            metadata.resource = meta_data_element.attributes["resource"]
250
251        android_app.metadata.append(metadata)
252
253def parse_receiver(application_element, android_app):
254    for receiver_element in find_elements(application_element.children, "receiver"):
255        parse_metadata(receiver_element, android_app)
256
257        receiver = Receiver()
258        receiver.name = receiver_element.attributes["name"]
259        receiver.permission = receiver_element.attributes.get("permission", "")
260        receiver.exported = receiver_element.attributes.get("exported", "false") == "true"
261        parse_metadata(receiver_element, receiver)
262        parse_intent_filters(receiver_element, receiver)
263        android_app.receivers.append(receiver)
264
265def find_single_element(element_collection, element_name):
266    for e in element_collection:
267        if e.name == element_name:
268            return e
269
270def find_elements(element_collection, element_name):
271    return [e for e in element_collection if e.name == element_name]
272
273if __name__ == "__main__":
274    main()