1#  Copyright (C) 2023 The Android Open Source Project
2#
3#  Licensed under the Apache License, Version 2.0 (the "License");
4#  you may not use this file except in compliance with the License.
5#  You may obtain a copy of the License at
6#
7#       http://www.apache.org/licenses/LICENSE-2.0
8#
9#  Unless required by applicable law or agreed to in writing, software
10#  distributed under the License is distributed on an "AS IS" BASIS,
11#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12#  See the License for the specific language governing permissions and
13#  limitations under the License.
14
15import xml.etree.ElementTree as ET
16import datetime
17import os
18from os import listdir
19from os.path import isfile, join
20from enum import Enum
21import argparse
22import re
23
24
25class ResourceCategory(Enum):
26    Value = 1
27    File = 2
28    Id = 3
29
30
31class AdServicesUiUtil:
32    PUBLIC_XML_DIR = '../apk/publicres/values/public.xml'
33    STRINGS_XML_DIR = '../apk/res/values/strings.xml'
34    COLORS_XML_DIR = '../apk/res/values/colors.xml'
35    DIMENS_XML_DIR = '../apk/res/values/dimens.xml'
36    INTEGERS_XML_DIR = '../apk/res/values/integers.xml'
37    STYLES_XML_DIR = '../apk/res/values/styles.xml'
38    DRAWABLE_XML_DIR = '../apk/res/drawable/'
39    LAYOUT_XML_DIR = '../apk/res/layout/'
40    COPYRIGHT_TEXT = f'''<?xml version="1.0" encoding="utf-8"?>
41<!-- Copyright (C) {datetime.date.today().year} The Android Open Source Project
42
43    Licensed under the Apache License, Version 2.0 (the "License");
44    you may not use this file except in compliance with the License.
45    You may obtain a copy of the License at
46
47    http://www.apache.org/licenses/LICENSE-2.0
48
49    Unless required by applicable law or agreed to in writing, software
50    distributed under the License is distributed on an "AS IS" BASIS,
51    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
52    See the License for the specific language governing permissions and
53    limitations under the License.
54-->
55<!-- DO NOT MODIFY. This file is automatically generated by generate_adservices_public_xml.py -->\n'''
56
57    def __init__(self):
58        pass
59
60    def _get_next_id(self, existing_mapping, map_start):
61        if existing_mapping:
62            return int(min(existing_mapping.values()), 0) - 1
63        else:
64            return int(map_start, 0)
65
66    def _get_existing_tree(self, dir, node_type):
67        if not os.path.exists(dir):
68            return None, {}
69        else:
70            root = ET.parse(dir).getroot()
71            return root, {
72                node.attrib['name']: node.attrib['id']
73                for node in root if node.attrib['type'] == node_type
74            }
75
76    def _get_updated_resources(self, res_category, res_xml_dir):
77        match res_category:
78            case ResourceCategory.Value:
79                return [node.attrib['name'] for node in ET.parse(res_xml_dir).getroot()]
80            case ResourceCategory.File:
81                return [file.split('.')[0] for file in listdir(res_xml_dir) if
82                        isfile(join(res_xml_dir, file))]
83            case ResourceCategory.Id:
84                new_strings = set()
85                for file in os.scandir(res_xml_dir):
86                    for child in ET.parse(file.path).getroot().iter():
87                        id_key = [key for key in child.attrib if re.match(r'\{.*\}id', key)]
88                        if len(id_key) > 0:
89                            new_strings.add(child.attrib[id_key[0]].split('/')[1])
90                return new_strings
91
92    def _overwrite_public_xml(self, root, public_xml_dir):
93        if os.path.exists(public_xml_dir):
94            os.remove(public_xml_dir)
95
96        ET.indent(root, space='    ')
97        with open(public_xml_dir, "w+") as file:
98            file.write(self.COPYRIGHT_TEXT)
99            file.write(ET.tostring(root, encoding="unicode"))
100
101    def update_public_xml(self, res_xml_dir, res_type, res_category, map_start,
102                          public_xml_dir=PUBLIC_XML_DIR):
103        if not os.path.exists(res_xml_dir):
104            return
105
106        res_all = self._get_updated_resources(res_category, res_xml_dir)
107        root, mapping = self._get_existing_tree(public_xml_dir, res_type)
108
109        # resources of this type already exist in public.xml
110        if mapping:
111            res_new = [res for res in res_all if res not in mapping]
112            # TO-DO: add code to remove exsting elements when needed.
113            res_all = set(res_all)
114            deleted_res = set(res for res in mapping if res not in res_all)
115        # new type of resources are being added in public.xml
116        else:
117            res_new = res_all
118
119        if not res_new:
120            return
121
122        i_min = self._get_next_id(mapping, map_start)
123        for res in res_new:
124            added_element = ET.SubElement(root, 'public')
125            added_element.set('type', res_type)
126            added_element.set('name', res)
127            added_element.set('id', hex(i_min))
128            i_min -= 1
129
130        self._overwrite_public_xml(root, public_xml_dir)
131
132    def update_adservices_public_xml(self):
133        parser = argparse.ArgumentParser()
134        parser.add_argument("--enable_ota_layout",
135                            help="Enable adding all resource types to public.xml for OTA layout updates",
136                            action='store_true', default=False)
137        args = parser.parse_args()
138        util.update_public_xml(res_xml_dir=self.STRINGS_XML_DIR,
139                               res_type='string', res_category=ResourceCategory.Value,
140                               map_start=hex(0x7f017fff))
141        if args.enable_ota_layout:
142            util.update_public_xml(res_xml_dir=self.DIMENS_XML_DIR,
143                                   res_type='dimen', res_category=ResourceCategory.Value,
144                                   map_start=hex(0x7f027fff))
145            util.update_public_xml(res_xml_dir=self.COLORS_XML_DIR,
146                                   res_type='color', res_category=ResourceCategory.Value,
147                                   map_start=hex(0x7f037fff))
148            util.update_public_xml(res_xml_dir=self.INTEGERS_XML_DIR,
149                                   res_type='integer', res_category=ResourceCategory.Value,
150                                   map_start=hex(0x7f047fff))
151            util.update_public_xml(res_xml_dir=self.STYLES_XML_DIR,
152                                   res_type='style', res_category=ResourceCategory.Value,
153                                   map_start=hex(0x7f057fff))
154            util.update_public_xml(res_xml_dir=self.DRAWABLE_XML_DIR,
155                                   res_type='drawable', res_category=ResourceCategory.File,
156                                   map_start=hex(0x7f067fff))
157            util.update_public_xml(res_xml_dir=self.LAYOUT_XML_DIR,
158                                   res_type='layout', res_category=ResourceCategory.File,
159                                   map_start=hex(0x7f077fff))
160            util.update_public_xml(res_xml_dir=self.LAYOUT_XML_DIR, res_type='id',
161                                   res_category=ResourceCategory.Id,
162                                   map_start=hex(0x7f087fff))
163
164
165if __name__ == '__main__':
166    util = AdServicesUiUtil()
167    util.update_adservices_public_xml()
168