1#!/usr/bin/env python3
2#  Copyright (C) 2021 The Android Open Source Project
3#
4#  Licensed under the Apache License, Version 2.0 (the "License");
5#  you may not use this file except in compliance with the License.
6#  You may obtain a copy of the License at
7#
8#       http://www.apache.org/licenses/LICENSE-2.0
9#
10#  Unless required by applicable law or agreed to in writing, software
11#  distributed under the License is distributed on an "AS IS" BASIS,
12#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13#  See the License for the specific language governing permissions and
14#  limitations under the License.
15
16import os
17import re
18import sys
19try:
20    import lxml.etree as etree
21except ImportError:
22    print("Please install 'lxml' python package and retry. \n"
23        + "E.g., you can use 'sudo apt-get install python3-lxml'.")
24    sys.exit(1)
25
26class ResourceLocation:
27    def __init__(self, file, line=None):
28        self.file = file
29        self.line = line
30    def __str__(self):
31        if self.line is not None:
32            return self.file + ':' + str(self.line)
33        else:
34            return self.file
35
36class Resource:
37    def __init__(self, name, type, location=None):
38        self.name = name
39        self.type = type
40        self.locations = []
41        if location is not None:
42            self.locations.append(location)
43    def __eq__(self, other):
44        if isinstance(other, _Grab):
45            return other == self
46        return self.name == other.name and self.type == other.type
47    def __ne__(self, other):
48        if isinstance(other, _Grab):
49            return other != self
50        return self.name != other.name or self.type != other.type
51    def __hash__(self):
52        return hash((self.name, self.type))
53    def __str__(self):
54        result = ''
55        for location in self.locations:
56            result += str(location) + ': '
57        result += '<'+self.type+' name="'+self.name+'"'
58        return result + '>'
59    def __repr__(self):
60        return str(self)
61
62def get_all_resources(resDir, excluded_resource_files=[]):
63    excluded_resource_files = [os.path.abspath(file) for file in excluded_resource_files]
64    allResDirs = [f for f in os.listdir(resDir) if os.path.isdir(os.path.join(resDir, f))]
65    valuesDirs = [f for f in allResDirs if f.startswith('values')]
66    fileDirs = [f for f in allResDirs if not f.startswith('values')]
67    resources = set()
68    # Get the filenames of the all the files in all the fileDirs
69    for dir in fileDirs:
70        type = dir.split('-')[0]
71        for file in os.listdir(os.path.join(resDir, dir)):
72            filePath = os.path.abspath(os.path.join(resDir, dir, file))
73            if file.endswith('.xml') and filePath not in excluded_resource_files:
74                add_resource_to_set(resources,
75                                    Resource(file[:-4], type,
76                                             ResourceLocation(os.path.join(resDir, dir, file))))
77                if dir.startswith("layout"):
78                    for resource in get_ids_from_layout_file(os.path.join(resDir, dir, file)):
79                        add_resource_to_set(resources, resource)
80    for dir in valuesDirs:
81        for file in os.listdir(os.path.join(resDir, dir)):
82            filePath = os.path.abspath(os.path.join(resDir, dir, file))
83            if file.endswith('.xml') and filePath not in excluded_resource_files:
84                for resource in get_resources_from_single_file(os.path.join(resDir, dir, file),
85                                                               dir != "values"):
86                    add_resource_to_set(resources, resource)
87    return resources
88
89def get_ids_from_layout_file(filename):
90    result = set()
91    with open(filename, 'r') as file:
92        r = re.compile("@\+id/([a-zA-Z0-9_]+)")
93        for i in r.findall(file.read()):
94            add_resource_to_set(result, Resource(i, 'id', ResourceLocation(filename)))
95    return result
96
97def get_resources_from_single_file(filename, ignore_strings=False):
98    doc = etree.parse(filename)
99    root = doc.getroot()
100    result = set()
101    for resource in root:
102        if resource.tag is etree.Comment:
103            continue
104        if resource.tag == 'declare-styleable':
105            for attr in resource:
106                resName = attr.get('name')
107                # Skip resources beginning with 'android:' as they are part of the framework
108                # resources. This script finds only the app's resources.
109                if resName is None or resName.startswith('android:'):
110                    continue
111                resType = "attr"
112                add_resource_to_set(result, Resource(resName, resType, ResourceLocation(filename, attr.sourceline)))
113            continue
114        resName = resource.get('name')
115        resType = resource.tag
116        if resType == "string-array" or resType == "integer-array":
117            resType = "array"
118        if resource.tag == 'item' or resource.tag == 'public':
119            resType = resource.get('type')
120        if (resType == 'string' or resType == 'plurals') and ignore_strings:
121            continue
122        if resType == 'overlayable':
123            for policy in resource:
124                for overlayable in policy:
125                    resName = overlayable.get('name')
126                    resType = overlayable.get('type')
127                    add_resource_to_set(result, Resource(resName, resType,
128                                                         ResourceLocation(filename, resource.sourceline)))
129        else:
130            add_resource_to_set(result, Resource(resName, resType,
131                                                 ResourceLocation(filename, resource.sourceline)))
132    return result
133
134# Used to get objects out of sets
135class _Grab:
136    def __init__(self, value):
137        self.search_value = value
138    def __hash__(self):
139        return hash(self.search_value)
140    def __eq__(self, other):
141        if self.search_value == other:
142            self.actual_value = other
143            return True
144        return False
145
146def add_resource_to_set(resourceset, resource):
147    if (resource.name == None):
148        return
149    grabber = _Grab(resource)
150    if grabber in resourceset:
151        grabber.actual_value.locations.extend(resource.locations)
152    else:
153        resourceset.update([resource])
154
155def merge_resources(set1, set2):
156    for resource in set2:
157        add_resource_to_set(set1, resource)
158
159def get_androidx_resources():
160    # source: https://android.googlesource.com/platform/frameworks/opt/sherpa/+/studio-3.0/constraintlayout/src/main/res/values/attrs.xml
161    resources = set()
162    add_resource_to_set(resources, Resource('layout_optimizationLevel', 'attr'))
163    add_resource_to_set(resources, Resource('constraintSet', 'attr'))
164    add_resource_to_set(resources, Resource('barrierDirection', 'attr'))
165    add_resource_to_set(resources, Resource('constraint_referenced_ids', 'attr'))
166    add_resource_to_set(resources, Resource('chainUseRtl', 'attr'))
167    add_resource_to_set(resources, Resource('title', 'attr'))
168    add_resource_to_set(resources, Resource('layout_constraintGuide_begin', 'attr'))
169    add_resource_to_set(resources, Resource('layout_constraintGuide_end', 'attr'))
170    add_resource_to_set(resources, Resource('layout_constraintGuide_percent', 'attr'))
171    add_resource_to_set(resources, Resource('layout_constraintLeft_toLeftOf', 'attr'))
172    add_resource_to_set(resources, Resource('layout_constraintLeft_toRightOf', 'attr'))
173    add_resource_to_set(resources, Resource('layout_constraintRight_toLeftOf', 'attr'))
174    add_resource_to_set(resources, Resource('layout_constraintRight_toRightOf', 'attr'))
175    add_resource_to_set(resources, Resource('layout_constraintTop_toTopOf', 'attr'))
176    add_resource_to_set(resources, Resource('layout_constraintTop_toBottomOf', 'attr'))
177    add_resource_to_set(resources, Resource('layout_constraintBottom_toTopOf', 'attr'))
178    add_resource_to_set(resources, Resource('layout_constraintBottom_toBottomOf', 'attr'))
179    add_resource_to_set(resources, Resource('layout_constraintBaseline_toBaselineOf', 'attr'))
180    add_resource_to_set(resources, Resource('layout_constraintStart_toEndOf', 'attr'))
181    add_resource_to_set(resources, Resource('layout_constraintStart_toStartOf', 'attr'))
182    add_resource_to_set(resources, Resource('layout_constraintEnd_toStartOf', 'attr'))
183    add_resource_to_set(resources, Resource('layout_constraintEnd_toEndOf', 'attr'))
184    add_resource_to_set(resources, Resource('layout_goneMarginLeft', 'attr'))
185    add_resource_to_set(resources, Resource('layout_goneMarginTop', 'attr'))
186    add_resource_to_set(resources, Resource('layout_goneMarginRight', 'attr'))
187    add_resource_to_set(resources, Resource('layout_goneMarginBottom', 'attr'))
188    add_resource_to_set(resources, Resource('layout_goneMarginStart', 'attr'))
189    add_resource_to_set(resources, Resource('layout_goneMarginEnd', 'attr'))
190    add_resource_to_set(resources, Resource('layout_constraintHorizontal_bias', 'attr'))
191    add_resource_to_set(resources, Resource('layout_constraintVertical_bias', 'attr'))
192    add_resource_to_set(resources, Resource('layout_constraintWidth_default', 'attr'))
193    add_resource_to_set(resources, Resource('layout_constraintHeight_default', 'attr'))
194    add_resource_to_set(resources, Resource('layout_constraintWidth_min', 'attr'))
195    add_resource_to_set(resources, Resource('layout_constraintWidth_max', 'attr'))
196    add_resource_to_set(resources, Resource('layout_constraintWidth_percent', 'attr'))
197    add_resource_to_set(resources, Resource('layout_constraintHeight_min', 'attr'))
198    add_resource_to_set(resources, Resource('layout_constraintHeight_max', 'attr'))
199    add_resource_to_set(resources, Resource('layout_constraintHeight_percent', 'attr'))
200    add_resource_to_set(resources, Resource('layout_constraintLeft_creator', 'attr'))
201    add_resource_to_set(resources, Resource('layout_constraintTop_creator', 'attr'))
202    add_resource_to_set(resources, Resource('layout_constraintRight_creator', 'attr'))
203    add_resource_to_set(resources, Resource('layout_constraintBottom_creator', 'attr'))
204    add_resource_to_set(resources, Resource('layout_constraintBaseline_creator', 'attr'))
205    add_resource_to_set(resources, Resource('layout_constraintDimensionRatio', 'attr'))
206    add_resource_to_set(resources, Resource('layout_constraintHorizontal_weight', 'attr'))
207    add_resource_to_set(resources, Resource('layout_constraintVertical_weight', 'attr'))
208    add_resource_to_set(resources, Resource('layout_constraintHorizontal_chainStyle', 'attr'))
209    add_resource_to_set(resources, Resource('layout_constraintVertical_chainStyle', 'attr'))
210    add_resource_to_set(resources, Resource('layout_editor_absoluteX', 'attr'))
211    add_resource_to_set(resources, Resource('layout_editor_absoluteY', 'attr'))
212    return resources
213