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