blob: 3d7d9eaba53b9b9668f48a46ad91c7ec79d3fa4e [file] [log] [blame]
Priyank Singh051c06b2019-12-20 04:12:02 -08001#!/usr/bin/env python
2#
Cole Faust48679c92019-11-08 13:49:26 -08003# Copyright 2019, The Android Open Source Project
4#
Priyank Singh051c06b2019-12-20 04:12:02 -08005# 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 Faust48679c92019-11-08 13:49:26 -08008#
Priyank Singh051c06b2019-12-20 04:12:02 -08009# http://www.apache.org/licenses/LICENSE-2.0
Cole Faust48679c92019-11-08 13:49:26 -080010#
Priyank Singh051c06b2019-12-20 04:12:02 -080011# 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 Faust48679c92019-11-08 13:49:26 -080016
17from argparse import ArgumentParser as AP, RawDescriptionHelpFormatter
18import os
19import sys
20import re
21import subprocess
22import time
23from hashlib import sha1
24
25def 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
39def 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
66def 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
84def 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
101def 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
133def 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
145def 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
151def 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
232def 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
306if __name__ == "__main__":
307 main()