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