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
17import argparse
18import os
19import sys
20import re
21from resource_utils import get_all_resources, get_resources_from_single_file, add_resource_to_set, Resource, merge_resources
22from git_utils import has_chassis_changes
23from datetime import datetime
24import urllib.request
25
26if sys.version_info[0] != 3:
27    print("Must use python 3")
28    sys.exit(1)
29
30# path to 'packages/apps/Car/libs/car-ui-lib/'
31ROOT_FOLDER = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../'))
32OUTPUT_FILE_PATH = os.path.join(ROOT_FOLDER, 'tests/apitest/')
33
34
35COPYRIGHT_STR = """Copyright (C) %s The Android Open Source Project
36
37Licensed under the Apache License, Version 2.0 (the "License");
38you may not use this file except in compliance with the License.
39You may obtain a copy of the License at
40
41  http://www.apache.org/licenses/LICENSE-2.0
42
43Unless required by applicable law or agreed to in writing, software
44distributed under the License is distributed on an "AS IS" BASIS,
45WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
46See the License for the specific language governing permissions and
47limitations under the License.""" % (datetime.today().strftime("%Y"))
48
49
50AUTOGENERATION_NOTICE_STR = """ THIS FILE WAS AUTO GENERATED, DO NOT EDIT MANUALLY. """
51
52"""
53Script used to update the 'current.xml' file. This is being used as part of pre-submits to
54verify whether resources previously exposed to OEMs are being changed by a CL, potentially
55breaking existing customizations.
56
57Example usage: python auto-generate-resources.py current.xml
58"""
59def main():
60    parser = argparse.ArgumentParser(description='Check if any existing resources are modified.')
61    parser.add_argument('--sha', help='Git hash of current changes. This script will not run if this is provided and there are no chassis changes.')
62    parser.add_argument('-f', '--file', default='current.xml', help='Name of output file.')
63    parser.add_argument('-c', '--compare', action='store_true',
64                        help='Pass this flag if resources need to be compared.')
65    args = parser.parse_args()
66
67    if not has_chassis_changes(args.sha):
68        # Don't run because there were no chassis changes
69        return
70
71    resources = get_all_resources(os.path.join(ROOT_FOLDER, 'car-ui-lib/src/main/res'))
72    check_resource_names(resources, get_resources_from_single_file(os.path.join(OUTPUT_FILE_PATH, 'resource_name_allowed.xml')))
73    removed_resources = get_resources_from_single_file(os.path.join(ROOT_FOLDER, 'car-ui-lib/src/main/res-overlayable/values/removed_resources.xml'))
74    OVERLAYABLE_OUTPUT_FILE_PATH = os.path.join(ROOT_FOLDER, 'car-ui-lib/src/main/res-overlayable/values/overlayable.xml')
75    output_file = args.file or 'current.xml'
76
77    if args.compare:
78        check_removed_resources(resources, removed_resources)
79        merge_resources(resources, removed_resources)
80
81        old_mapping = get_resources_from_single_file(os.path.join(OUTPUT_FILE_PATH, 'current.xml'))
82        compare_resources(old_mapping, resources, os.path.join(OUTPUT_FILE_PATH, 'current.xml'))
83
84        old_mapping = get_resources_from_single_file(OVERLAYABLE_OUTPUT_FILE_PATH)
85        add_constraintlayout_resources(resources)
86        compare_resources(old_mapping, resources, OVERLAYABLE_OUTPUT_FILE_PATH)
87    else:
88        merge_resources(resources, removed_resources)
89        generate_current_file(resources, output_file)
90        generate_overlayable_file(resources, OVERLAYABLE_OUTPUT_FILE_PATH)
91
92def generate_current_file(resources, output_file='current.xml'):
93    resources = sorted(resources, key=lambda x: x.type + x.name)
94
95    # defer importing lxml to here so that people who aren't editing chassis don't have to have
96    # lxml installed
97    import lxml.etree as etree
98
99    root = etree.Element('resources')
100
101    root.addprevious(etree.Comment(AUTOGENERATION_NOTICE_STR))
102    for resource in resources:
103        item = etree.SubElement(root, 'public')
104        item.set('type', resource.type)
105        item.set('name', resource.name)
106
107    data = etree.ElementTree(root)
108
109    with open(os.path.join(OUTPUT_FILE_PATH, output_file), 'wb') as f:
110        data.write(f, pretty_print=True, xml_declaration=True, encoding='utf-8')
111
112def generate_overlayable_file(resources, output_file='overlayable.xml'):
113    add_constraintlayout_resources(resources)
114    resources = sorted(resources, key=lambda x: x.type + x.name)
115
116    # defer importing lxml to here so that people who aren't editing chassis don't have to have
117    # lxml installed
118    import lxml.etree as etree
119
120    root = etree.Element('resources')
121
122    root.addprevious(etree.Comment(COPYRIGHT_STR))
123    root.addprevious(etree.Comment(AUTOGENERATION_NOTICE_STR))
124
125    overlayable = etree.SubElement(root, 'overlayable')
126    overlayable.set('name', 'car-ui-lib')
127
128    policy = etree.SubElement(overlayable, 'policy')
129    # everything except public. source: frameworks/base/cmds/idmap2/libidmap2_policies/include/idmap2/Policies.h
130    policy.set('type', 'odm|oem|product|signature|system|vendor')
131
132    for resource in resources:
133        item = etree.SubElement(policy, 'item')
134        item.set('type', resource.type)
135        item.set('name', resource.name)
136
137    data = etree.ElementTree(root)
138
139    with open(output_file, 'wb') as f:
140        data.write(f, pretty_print=True, xml_declaration=True, encoding='utf-8')
141
142def add_constraintlayout_resources(resources):
143    # We need these to be able to use base layouts in RROs
144    # This should become unnecessary some time in future?
145    # Please keep this in-sync with res/raw/car_ui_keep.xml
146    url = "https://raw.githubusercontent.com/androidx/constraintlayout/main/constraintlayout/constraintlayout/src/main/res/values/attrs.xml"
147    import xml.etree.ElementTree as ET
148    tree = ET.parse(urllib.request.urlopen(url))
149    root = tree.getroot()
150
151    # The source here always points to the latest version of attrs.xml from androidx repo
152    # and since platform is not using the latest one, some of the tags should be excluded.
153    unsupported_attrs = [
154        "circularflow_radiusInDP",
155        "constraint_referenced_tags",
156        "layout_goneMarginBaseline",
157        "circularflow_angles",
158        "circularflow_defaultRadius",
159        "circularflow_defaultAngle",
160        "layout_marginBaseline",
161        "circularflow_viewCenter",
162        "layout_wrapBehaviorInParent",
163        "layout_constraintWidth",
164        "layout_constraintBaseline_toBottomOf",
165        "layout_constraintHeight",
166        "layout_constraintBaseline_toTopOf",
167    ]
168
169    attrs = root.findall("./declare-styleable[@name='ConstraintLayout_Layout']/attr")
170    for attr in attrs:
171      if "android:" not in attr.get('name') and attr.get('name') not in unsupported_attrs:
172        add_resource_to_set(resources, Resource(attr.get('name'), 'attr'))
173
174
175def check_resource_names(resources, allowed):
176    newlist = resources.difference(allowed)
177    failed=False
178    for resource in newlist:
179        resourceType= resource.type
180        resourceName = resource.name
181        if resourceType == 'attr' and not re.match("^CarUi", resourceName, re.IGNORECASE):
182            print(f"Please consider changing {resourceType}/{resourceName} to something like CarUi{resourceName}")
183            failed=True
184        elif resourceType == 'style' and not re.search("CarUi", resourceName, re.IGNORECASE):
185            print(f"Please consider changing {resourceType}/{resourceName} to something like CarUi{resourceName}")
186            failed=True
187        elif resourceType != 'attr' and resourceType != 'style' and not re.match("^car_ui_", resourceName, re.IGNORECASE):
188            print(f"Please consider changing {resourceType}/{resourceName} to something like car_ui_{resourceName}")
189            failed=True
190    if failed:
191        sys.exit(1)
192
193def compare_resources(old_mapping, new_mapping, res_public_file):
194    removed = old_mapping.difference(new_mapping)
195    added = new_mapping.difference(old_mapping)
196    if len(removed) > 0:
197        print('Resources removed:\n' + '\n'.join(map(lambda x: str(x), removed)))
198    if len(added) > 0:
199        print('Resources added:\n' + '\n'.join(map(lambda x: str(x), added)))
200
201    if len(added) + len(removed) > 0:
202        print("Some resource have been modified. If this is intentional please " +
203              "run 'python3 $ANDROID_BUILD_TOP/packages/apps/Car/libs/car-ui-lib/tests/apitest/" +
204              "auto-generate-resources.py' again and submit the new %s" % res_public_file)
205        sys.exit(1)
206
207def check_removed_resources(mapping, removed_resources):
208    intersection = removed_resources.intersection(mapping)
209    if len(intersection) > 0:
210        print('Usage of removed resources detected\n'+ '\n'.join(map(lambda x: str(x), intersection)))
211
212if __name__ == '__main__':
213    main()
214