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()