1#!/usr/bin/env python3
2#
3# Copyright (C) 2018 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
17"""List all pre-installed Android Apps with `sharedUserId` in their
18`AndroidManifest.xml`."""
19
20import argparse
21import collections
22import csv
23import json
24import os
25import re
26import subprocess
27import sys
28
29
30_SHARED_UID_PATTERN = re.compile('sharedUserId="([^"\\r\\n]*)"')
31
32
33def load_module_paths(module_json):
34    """Load module source paths."""
35    result = {}
36    with open(module_json, 'r') as json_file:
37        modules = json.load(json_file)
38    for name, module in modules.items():
39        try:
40            result[name] = module['path'][0]
41        except IndexError:
42            continue
43    return result
44
45
46def find_shared_uid(manifest_path):
47    """Extract shared UID from AndroidManifest.xml."""
48    try:
49        with open(manifest_path, 'r') as manifest_file:
50            content = manifest_file.read()
51    except UnicodeDecodeError:
52        return []
53    return sorted(_SHARED_UID_PATTERN.findall(content))
54
55
56def find_file(product_out, app_name):
57    """Find the APK file for the app."""
58    product_out = os.path.abspath(product_out)
59    prefix_len = len(product_out) + 1
60    partitions = (
61        'data', 'odm', 'oem', 'product', 'system', 'system_ext', 'system_other',
62        'vendor',)
63    for partition in partitions:
64        partition_dir = os.path.join(product_out, partition)
65        for base, _, filenames in os.walk(partition_dir):
66            for filename in filenames:
67                name, ext = os.path.splitext(filename)
68                if name == app_name and ext in {'.apk', '.jar'}:
69                    return os.path.join(base, filename)[prefix_len:]
70    return ''
71
72
73AppInfo = collections.namedtuple(
74    'AppInfo', 'name shared_uid installed_path source_path')
75
76
77def collect_apps_with_shared_uid(product_out, module_paths):
78    """Collect apps with shared UID."""
79    apps_dir = os.path.join(product_out, 'obj', 'APPS')
80    result = []
81    for app_dir_name in os.listdir(apps_dir):
82        app_name = re.sub('_intermediates$', '', app_dir_name)
83        app_dir = os.path.join(apps_dir, app_dir_name)
84
85        apk_file = os.path.join(app_dir, 'package.apk')
86        if not os.path.exists(apk_file):
87            print('error: Failed to find:', apk_file, file=sys.stderr)
88            continue
89
90        apk_unpacked = os.path.join(app_dir, 'package')
91        if not os.path.exists(apk_unpacked):
92            ret = subprocess.call(['apktool', 'd', 'package.apk'], cwd=app_dir)
93            if ret != 0:
94                print('error: Failed to unpack:', apk_file, file=sys.stderr)
95                continue
96
97        manifest_file = os.path.join(apk_unpacked, 'AndroidManifest.xml')
98        if not os.path.exists(manifest_file):
99            print('error: Failed to find:', manifest_file, file=sys.stderr)
100            continue
101
102        shared_uid = find_shared_uid(manifest_file)
103        if not shared_uid:
104            continue
105
106        result.append(AppInfo(
107            app_name, shared_uid, find_file(product_out, app_name),
108            module_paths.get(app_name, '')))
109    return result
110
111
112def _parse_args():
113    """Parse command line options."""
114    parser = argparse.ArgumentParser()
115    parser.add_argument('product_out')
116    parser.add_argument('-o', '--output', required=True)
117    return parser.parse_args()
118
119
120def main():
121    """Main function."""
122    args = _parse_args()
123
124    module_paths = load_module_paths(
125        os.path.join(args.product_out, 'module-info.json'))
126
127    result = collect_apps_with_shared_uid(args.product_out, module_paths)
128
129    def _generate_sort_key(app):
130        has_android_uid = any(
131            uid.startswith('android.uid') for uid in app.shared_uid)
132        return (not has_android_uid, app.installed_path.startswith('system'),
133                app.installed_path)
134
135    result.sort(key=_generate_sort_key)
136
137    with open(args.output, 'w') as output_file:
138        writer = csv.writer(output_file)
139        writer.writerow(('App Name', 'UID', 'Installation Path', 'Source Path'))
140        for app in result:
141            writer.writerow((app.name, ' '.join(app.shared_uid),
142                             app.installed_path, app.source_path))
143
144
145if __name__ == '__main__':
146    main()
147