1#!/usr/bin/env python3 2# 3# Copyright 2019, The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17from argparse import ArgumentParser as AP, RawDescriptionHelpFormatter 18import os 19import sys 20import re 21import subprocess 22import time 23from hashlib import sha1 24 25if sys.version_info[0] != 3: 26 print("Must use python 3") 27 sys.exit(1) 28 29def hex_to_letters(hex): 30 """Converts numbers in a hex string to letters. 31 32 Example: 0beec7b5 -> aBEEChBf""" 33 hex = hex.upper() 34 chars = [] 35 for char in hex: 36 if ord('0') <= ord(char) <= ord('9'): 37 # Convert 0-9 to a-j 38 chars.append(chr(ord(char) - ord('0') + ord('a'))) 39 else: 40 chars.append(char) 41 return ''.join(chars) 42 43def get_package_name(args): 44 """Generates a package name for the quickrro. 45 46 The name is quickrro.<hash>. The hash is based on 47 all of the inputs to the RRO. (package/targetName to overlay and resources) 48 The hash will be entirely lowercase/uppercase letters, since 49 android package names can't have numbers.""" 50 hash = sha1(args.package.encode('UTF-8')) 51 if args.target_name: 52 hash.update(args.target_name.encode('UTF-8')) 53 54 if args.resources is not None: 55 args.resources.sort() 56 hash.update(''.join(args.resources).encode('UTF-8')) 57 else: 58 for root, dirs, files in os.walk(args.dir): 59 for file in files: 60 path = os.path.join(root, file) 61 hash.update(path.encode('UTF-8')) 62 with open(path, 'rb') as f: 63 while True: 64 buf = f.read(4096) 65 if not buf: 66 break 67 hash.update(buf) 68 69 result = 'quickrro.' + hex_to_letters(hash.hexdigest()) 70 return result 71 72def run_command(command_args): 73 """Returns the stdout of a command, and throws an exception if the command fails""" 74 result = subprocess.Popen(command_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 75 stdout, stderr = result.communicate() 76 77 stdout = str(stdout) 78 stderr = str(stderr) 79 80 if result.returncode != 0: 81 err = 'command failed: ' + ' '.join(command_args) 82 if len(stdout) > 0: 83 err += '\n' + stdout.strip() 84 if len(stderr) > 0: 85 err += '\n' + stderr.strip() 86 raise Exception(err) 87 88 return stdout 89 90def get_android_dir_priority(dir): 91 """Given the name of a directory under ~/Android/Sdk/platforms, returns an integer priority. 92 93 The directory with the highest priority will be used. Currently android-stable is higest, 94 and then after that the api level is the priority. eg android-28 has priority 28.""" 95 if len(dir) == 0: 96 return -1 97 if 'stable' in dir: 98 return 999 99 100 try: 101 return int(dir.split('-')[1]) 102 except Exception: 103 pass 104 105 return 0 106 107def find_android_jar(path=None): 108 """Returns the path to framework-res.apk or android.jar, throwing an Exception when not found. 109 110 First looks in the given path. Then looks in $OUT/system/framework/framework-res.apk. 111 Finally, looks in ~/Android/Sdk/platforms.""" 112 if path is not None: 113 if os.path.isfile(path): 114 return path 115 else: 116 raise Exception('Invalid path: ' + path) 117 118 framework_res_path = os.path.join(os.environ['OUT'], 'system/framework/framework-res.apk') 119 if os.path.isfile(framework_res_path): 120 return framework_res_path 121 122 sdk_dir = os.path.expanduser('~/Android/Sdk/platforms') 123 best_dir = '' 124 for dir in os.listdir(sdk_dir): 125 if os.path.isdir(os.path.join(sdk_dir, dir)): 126 if get_android_dir_priority(dir) > get_android_dir_priority(best_dir): 127 best_dir = dir 128 129 if len(best_dir) == 0: 130 raise Exception("Couldn't find android.jar") 131 132 android_jar_path = os.path.join(sdk_dir, best_dir, 'android.jar') 133 134 if not os.path.isfile(android_jar_path): 135 raise Exception("Couldn't find android.jar") 136 137 return android_jar_path 138 139def uninstall_all(): 140 """Uninstall all RROs starting with 'quickrro'""" 141 packages = re.findall('quickrro[a-zA-Z.]+', 142 run_command(['adb', 'shell', 'cmd', 'overlay', 'list'])) 143 144 for package in packages: 145 print('Uninstalling ' + package) 146 run_command(['adb', 'uninstall', package]) 147 148 if len(packages) == 0: 149 print('No quick RROs to uninstall') 150 151def delete_flat_files(path): 152 """Deletes all .flat files under `path`""" 153 for filename in os.listdir(path): 154 if filename.endswith('.flat'): 155 os.remove(os.path.join(path, filename)) 156 157def build(args, package_name): 158 """Builds the RRO apk""" 159 try: 160 android_jar_path = find_android_jar(args.I) 161 except: 162 print('Unable to find framework-res.apk / android.jar. Please build android, ' 163 'install an SDK via android studio, or supply a valid -I') 164 sys.exit(1) 165 166 print('Building...') 167 root_folder = os.path.join(args.workspace, 'quick_rro') 168 manifest_file = os.path.join(root_folder, 'AndroidManifest.xml') 169 resource_folder = args.dir or os.path.join(root_folder, 'res') 170 unsigned_apk = os.path.join(root_folder, package_name + '.apk.unsigned') 171 signed_apk = os.path.join(root_folder, package_name + '.apk') 172 173 if not os.path.exists(root_folder): 174 os.makedirs(root_folder) 175 176 if args.resources is not None: 177 values_folder = os.path.join(resource_folder, 'values') 178 resource_file = os.path.join(values_folder, 'values.xml') 179 180 if not os.path.exists(values_folder): 181 os.makedirs(values_folder) 182 183 resources = map(lambda x: x.split(','), args.resources) 184 for resource in resources: 185 if len(resource) != 3: 186 print("Resource format is type,name,value") 187 sys.exit(1) 188 189 with open(resource_file, 'w') as f: 190 f.write('<?xml version="1.0" encoding="utf-8"?>\n') 191 f.write('<resources>\n') 192 for resource in resources: 193 f.write(' <item type="' + resource[0] + '" name="' 194 + resource[1] + '">' + resource[2] + '</item>\n') 195 f.write('</resources>\n') 196 197 with open(manifest_file, 'w') as f: 198 f.write('<?xml version="1.0" encoding="utf-8"?>\n') 199 f.write('<manifest xmlns:android="http://schemas.android.com/apk/res/android"\n') 200 f.write(' package="' + package_name + '">\n') 201 f.write(' <application android:hasCode="false"/>\n') 202 f.write(' <overlay android:priority="99"\n') 203 f.write(' android:targetPackage="' + args.package + '"\n') 204 if args.target_name is not None: 205 f.write(' android:targetName="' + args.target_name + '"\n') 206 f.write(' />\n') 207 f.write('</manifest>\n') 208 209 run_command(['aapt2', 'compile', '-o', os.path.join(root_folder, 'compiled.zip'), 210 '--dir', resource_folder]) 211 212 delete_flat_files(root_folder) 213 214 run_command(['unzip', os.path.join(root_folder, 'compiled.zip'), 215 '-d', root_folder]) 216 217 link_command = ['aapt2', 'link', '--auto-add-overlay', 218 '-o', unsigned_apk, '--manifest', manifest_file, 219 '-I', android_jar_path] 220 for filename in os.listdir(root_folder): 221 if filename.endswith('.flat'): 222 link_command.extend(['-R', os.path.join(root_folder, filename)]) 223 run_command(link_command) 224 225 # For some reason signapk.jar requires a relative path to out/soong/host/linux-x86/lib64 226 os.chdir(os.environ['ANDROID_BUILD_TOP']) 227 run_command(['java', '-Djava.library.path=out/soong/host/linux-x86/lib64', 228 '-jar', 'out/soong/host/linux-x86/framework/signapk.jar', 229 'build/target/product/security/platform.x509.pem', 230 'build/target/product/security/platform.pk8', 231 unsigned_apk, signed_apk]) 232 233 # No need to delete anything, but the unsigned apks might take a lot of space 234 try: 235 run_command(['rm', unsigned_apk]) 236 except Exception: 237 pass 238 239 print('Built ' + signed_apk) 240 241def main(): 242 parser = AP(description="Create and deploy a RRO (Runtime Resource Overlay)", 243 epilog='Examples:\n' 244 ' quick_rro.py -r bool,car_ui_scrollbar_enable,false\n' 245 ' quick_rro.py -r bool,car_ui_scrollbar_enable,false' 246 ' -p com.android.car.ui.paintbooth\n' 247 ' quick_rro.py -d vendor/auto/embedded/car-ui/sample1/rro/res\n' 248 ' quick_rro.py --uninstall-all\n', 249 formatter_class=RawDescriptionHelpFormatter) 250 parser.add_argument('-r', '--resources', action='append', nargs='+', 251 help='A resource in the form type,name,value. ' 252 'ex: -r bool,car_ui_scrollbar_enable,false') 253 parser.add_argument('-d', '--dir', 254 help='res folder rro') 255 parser.add_argument('-p', '--package', default='com.android.car.ui.paintbooth', 256 help='The package to override. Defaults to paintbooth.') 257 parser.add_argument('-t', '--target-name', 258 help='The name of the overlayable entry to RRO, if any.') 259 parser.add_argument('--uninstall-all', action='store_true', 260 help='Uninstall all RROs created by this script') 261 parser.add_argument('-I', 262 help='Path to android.jar or framework-res.apk. If not provided, will ' 263 'attempt to auto locate in $OUT/system/framework/framework-res.apk, ' 264 'and then in ~/Android/Sdk/') 265 parser.add_argument('--workspace', default='/tmp', 266 help='The location where temporary files are made. Defaults to /tmp. ' 267 'Will make a "quickrro" folder here.') 268 args = parser.parse_args() 269 270 if args.resources is not None: 271 # flatten 2d list 272 args.resources = [x for sub in args.resources for x in sub] 273 274 if args.uninstall_all: 275 return uninstall_all() 276 277 if args.dir is None and args.resources is None: 278 print('Must include one of --resources, --dir, or --uninstall-all') 279 parser.print_help() 280 sys.exit(1) 281 282 if args.dir is not None and args.resources is not None: 283 print('Cannot specify both --resources and --dir') 284 sys.exit(1) 285 286 if not os.path.isdir(args.workspace): 287 print(str(args.workspace) + ': No such directory') 288 sys.exit(1) 289 290 if 'ANDROID_BUILD_TOP' not in os.environ: 291 print("Please run lunch first") 292 sys.exit(1) 293 294 if not os.path.isfile(os.path.join( 295 os.environ['ANDROID_BUILD_TOP'], 'out/soong/host/linux-x86/framework/signapk.jar')): 296 print('out/soong/host/linux-x86/framework/signapk.jar missing, please do an android build first') 297 sys.exit(1) 298 299 package_name = get_package_name(args) 300 signed_apk = os.path.join(args.workspace, 'quick_rro', package_name + '.apk') 301 302 if os.path.isfile(signed_apk): 303 print("Found cached RRO: " + signed_apk) 304 else: 305 build(args, package_name) 306 307 print('Installing...') 308 run_command(['adb', 'install', '-r', signed_apk]) 309 310 print('Enabling...') 311 # Enabling RROs sometimes fails shortly after installing them 312 time.sleep(1) 313 run_command(['adb', 'shell', 'cmd', 'overlay', 'enable', '--user', 'current', package_name]) 314 315 print('Done!') 316 317if __name__ == "__main__": 318 main() 319