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 == 'declare-styleable':
89        ret.update(self._ParseDeclareStyleable(child))
90      else:
91        if child.tag == 'item':
92          resource_type = child.attrib['type']
93        elif child.tag in ('array', 'integer-array', 'string-array'):
94          resource_type = 'array'
95        else:
96          resource_type = child.tag
97        name = _ResourceNameToJavaSymbol(child.attrib['name'])
98        ret.add(_TextSymbolEntry('int', resource_type, name, _DUMMY_RTXT_ID))
99    return ret
100
101  def _CollectResourcesListFromDirectory(self, res_dir):
102    ret = set()
103    globs = resource_utils._GenerateGlobs(self.ignore_pattern)
104    for root, _, files in os.walk(res_dir):
105      resource_type = os.path.basename(root)
106      if '-' in resource_type:
107        resource_type = resource_type[:resource_type.index('-')]
108      for f in files:
109        if build_utils.MatchesGlob(f, globs):
110          continue
111        if resource_type == 'values':
112          ret.update(self._ParseValuesXml(os.path.join(root, f)))
113        else:
114          if '.' in f:
115            resource_name = f[:f.index('.')]
116          else:
117            resource_name = f
118          ret.add(
119              _TextSymbolEntry('int', resource_type, resource_name,
120                               _DUMMY_RTXT_ID))
121          # Other types not just layouts can contain new ids (eg: Menus and
122          # Drawables). Just in case, look for new ids in all files.
123          if f.endswith('.xml'):
124            ret.update(self._ExtractNewIdsFromXml(os.path.join(root, f)))
125    return ret
126
127  def _CollectResourcesListFromDirectories(self):
128    ret = set()
129    for res_dir in self.res_dirs:
130      ret.update(self._CollectResourcesListFromDirectory(res_dir))
131    return ret
132
133  def WriteRTxtFile(self, rtxt_path):
134    resources = self._CollectResourcesListFromDirectories()
135    with build_utils.AtomicOutput(rtxt_path) as f:
136      for resource in resources:
137        line = '{0.java_type} {0.resource_type} {0.name} {0.value}\n'.format(
138            resource)
139        f.write(line)
140