1# Copyright 2020 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import collections
6import os
7import re
8from xml.etree import ElementTree
9
10from util import build_utils
11from util import resource_utils
12
13_TextSymbolEntry = collections.namedtuple(
14    'RTextEntry', ('java_type', 'resource_type', 'name', 'value'))
15
16_DUMMY_RTXT_ID = '0x7f010001'
17_DUMMY_RTXT_INDEX = '1'
18
19
20def _ResourceNameToJavaSymbol(resource_name):
21  return re.sub('[\.:]', '_', resource_name)
22
23
24class RTxtGenerator(object):
25  def __init__(self,
26               res_dirs,
27               ignore_pattern=resource_utils.AAPT_IGNORE_PATTERN):
28    self.res_dirs = res_dirs
29    self.ignore_pattern = ignore_pattern
30
31  def _ParseDeclareStyleable(self, node):
32    ret = set()
33    stylable_name = _ResourceNameToJavaSymbol(node.attrib['name'])
34    ret.add(
35        _TextSymbolEntry('int[]', 'styleable', stylable_name,
36                         '{{{}}}'.format(_DUMMY_RTXT_ID)))
37    for child in node:
38      if child.tag == 'eat-comment':
39        continue
40      if child.tag != 'attr':
41        # This parser expects everything inside <declare-stylable/> to be either
42        # an attr or an eat-comment. If new resource xml files are added that do
43        # not conform to this, this parser needs updating.
44        raise Exception('Unexpected tag {} inside <delcare-stylable/>'.format(
45            child.tag))
46      entry_name = '{}_{}'.format(
47          stylable_name, _ResourceNameToJavaSymbol(child.attrib['name']))
48      ret.add(
49          _TextSymbolEntry('int', 'styleable', entry_name, _DUMMY_RTXT_INDEX))
50      if not child.attrib['name'].startswith('android:'):
51        resource_name = _ResourceNameToJavaSymbol(child.attrib['name'])
52        ret.add(_TextSymbolEntry('int', 'attr', resource_name, _DUMMY_RTXT_ID))
53      for entry in child:
54        if entry.tag not in ('enum', 'flag'):
55          # This parser expects everything inside <attr/> to be either an
56          # <enum/> or an <flag/>. If new resource xml files are added that do
57          # not conform to this, this parser needs updating.
58          raise Exception('Unexpected tag {} inside <attr/>'.format(entry.tag))
59        resource_name = _ResourceNameToJavaSymbol(entry.attrib['name'])
60        ret.add(_TextSymbolEntry('int', 'id', resource_name, _DUMMY_RTXT_ID))
61    return ret
62
63  def _ExtractNewIdsFromNode(self, node):
64    ret = set()
65    # Sometimes there are @+id/ in random attributes (not just in android:id)
66    # and apparently that is valid. See:
67    # https://developer.android.com/reference/android/widget/RelativeLayout.LayoutParams.html
68    for value in node.attrib.values():
69      if value.startswith('@+id/'):
70        resource_name = value[5:]
71        ret.add(_TextSymbolEntry('int', 'id', resource_name, _DUMMY_RTXT_ID))
72    for child in node:
73      ret.update(self._ExtractNewIdsFromNode(child))
74    return ret
75
76  def _ExtractNewIdsFromXml(self, xml_path):
77    root = ElementTree.parse(xml_path).getroot()
78    return self._ExtractNewIdsFromNode(root)
79
80  def _ParseValuesXml(self, xml_path):
81    ret = set()
82    root = ElementTree.parse(xml_path).getroot()
83    assert root.tag == 'resources'
84    for child in root:
85      if child.tag == 'eat-comment':
86        # eat-comment is just a dummy documentation element.
87        continue
88      if child.tag == 'skip':
89        # skip is just a dummy element.
90        continue
91      if child.tag == 'declare-styleable':
92        ret.update(self._ParseDeclareStyleable(child))
93      else:
94        if child.tag == 'item':
95          resource_type = child.attrib['type']
96        elif child.tag in ('array', 'integer-array', 'string-array'):
97          resource_type = 'array'
98        else:
99          resource_type = child.tag
100        name = _ResourceNameToJavaSymbol(child.attrib['name'])
101        ret.add(_TextSymbolEntry('int', resource_type, name, _DUMMY_RTXT_ID))
102    return ret
103
104  def _CollectResourcesListFromDirectory(self, res_dir):
105    ret = set()
106    globs = resource_utils._GenerateGlobs(self.ignore_pattern)
107    for root, _, files in os.walk(res_dir):
108      resource_type = os.path.basename(root)
109      if '-' in resource_type:
110        resource_type = resource_type[:resource_type.index('-')]
111      for f in files:
112        if build_utils.MatchesGlob(f, globs):
113          continue
114        if resource_type == 'values':
115          ret.update(self._ParseValuesXml(os.path.join(root, f)))
116        else:
117          if '.' in f:
118            resource_name = f[:f.index('.')]
119          else:
120            resource_name = f
121          ret.add(
122              _TextSymbolEntry('int', resource_type, resource_name,
123                               _DUMMY_RTXT_ID))
124          # Other types not just layouts can contain new ids (eg: Menus and
125          # Drawables). Just in case, look for new ids in all files.
126          if f.endswith('.xml'):
127            ret.update(self._ExtractNewIdsFromXml(os.path.join(root, f)))
128    return ret
129
130  def _CollectResourcesListFromDirectories(self):
131    ret = set()
132    for res_dir in self.res_dirs:
133      ret.update(self._CollectResourcesListFromDirectory(res_dir))
134    return ret
135
136  def WriteRTxtFile(self, rtxt_path):
137    resources = self._CollectResourcesListFromDirectories()
138    with build_utils.AtomicOutput(rtxt_path, mode='w') as f:
139      for resource in resources:
140        line = '{0.java_type} {0.resource_type} {0.name} {0.value}\n'.format(
141            resource)
142        f.write(line)
143