• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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