# Copyright 2020 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import collections
import os
import re
from xml.etree import ElementTree
from util import build_utils
from util import resource_utils
_TextSymbolEntry = collections.namedtuple(
'RTextEntry', ('java_type', 'resource_type', 'name', 'value'))
_DUMMY_RTXT_ID = '0x7f010001'
_DUMMY_RTXT_INDEX = '1'
def _ResourceNameToJavaSymbol(resource_name):
return re.sub('[\.:]', '_', resource_name)
class RTxtGenerator(object):
def __init__(self,
res_dirs,
ignore_pattern=resource_utils.AAPT_IGNORE_PATTERN):
self.res_dirs = res_dirs
self.ignore_pattern = ignore_pattern
def _ParseDeclareStyleable(self, node):
ret = set()
stylable_name = _ResourceNameToJavaSymbol(node.attrib['name'])
ret.add(
_TextSymbolEntry('int[]', 'styleable', stylable_name,
'{{{}}}'.format(_DUMMY_RTXT_ID)))
for child in node:
if child.tag == 'eat-comment':
continue
if child.tag != 'attr':
# This parser expects everything inside to be either
# an attr or an eat-comment. If new resource xml files are added that do
# not conform to this, this parser needs updating.
raise Exception('Unexpected tag {} inside '.format(
child.tag))
entry_name = '{}_{}'.format(
stylable_name, _ResourceNameToJavaSymbol(child.attrib['name']))
ret.add(
_TextSymbolEntry('int', 'styleable', entry_name, _DUMMY_RTXT_INDEX))
if not child.attrib['name'].startswith('android:'):
resource_name = _ResourceNameToJavaSymbol(child.attrib['name'])
ret.add(_TextSymbolEntry('int', 'attr', resource_name, _DUMMY_RTXT_ID))
for entry in child:
if entry.tag not in ('enum', 'flag'):
# This parser expects everything inside to be either an
# or an . If new resource xml files are added that do
# not conform to this, this parser needs updating.
raise Exception('Unexpected tag {} inside '.format(entry.tag))
resource_name = _ResourceNameToJavaSymbol(entry.attrib['name'])
ret.add(_TextSymbolEntry('int', 'id', resource_name, _DUMMY_RTXT_ID))
return ret
def _ExtractNewIdsFromNode(self, node):
ret = set()
# Sometimes there are @+id/ in random attributes (not just in android:id)
# and apparently that is valid. See:
# https://developer.android.com/reference/android/widget/RelativeLayout.LayoutParams.html
for value in node.attrib.values():
if value.startswith('@+id/'):
resource_name = value[5:]
ret.add(_TextSymbolEntry('int', 'id', resource_name, _DUMMY_RTXT_ID))
for child in node:
ret.update(self._ExtractNewIdsFromNode(child))
return ret
def _ExtractNewIdsFromXml(self, xml_path):
root = ElementTree.parse(xml_path).getroot()
return self._ExtractNewIdsFromNode(root)
def _ParseValuesXml(self, xml_path):
ret = set()
root = ElementTree.parse(xml_path).getroot()
assert root.tag == 'resources'
for child in root:
if child.tag == 'eat-comment':
# eat-comment is just a dummy documentation element.
continue
if child.tag == 'skip':
# skip is just a dummy element.
continue
if child.tag == 'declare-styleable':
ret.update(self._ParseDeclareStyleable(child))
else:
if child.tag == 'item':
resource_type = child.attrib['type']
elif child.tag in ('array', 'integer-array', 'string-array'):
resource_type = 'array'
else:
resource_type = child.tag
name = _ResourceNameToJavaSymbol(child.attrib['name'])
ret.add(_TextSymbolEntry('int', resource_type, name, _DUMMY_RTXT_ID))
return ret
def _CollectResourcesListFromDirectory(self, res_dir):
ret = set()
globs = resource_utils._GenerateGlobs(self.ignore_pattern)
for root, _, files in os.walk(res_dir):
resource_type = os.path.basename(root)
if '-' in resource_type:
resource_type = resource_type[:resource_type.index('-')]
for f in files:
if build_utils.MatchesGlob(f, globs):
continue
if resource_type == 'values':
ret.update(self._ParseValuesXml(os.path.join(root, f)))
else:
if '.' in f:
resource_name = f[:f.index('.')]
else:
resource_name = f
ret.add(
_TextSymbolEntry('int', resource_type, resource_name,
_DUMMY_RTXT_ID))
# Other types not just layouts can contain new ids (eg: Menus and
# Drawables). Just in case, look for new ids in all files.
if f.endswith('.xml'):
ret.update(self._ExtractNewIdsFromXml(os.path.join(root, f)))
return ret
def _CollectResourcesListFromDirectories(self):
ret = set()
for res_dir in self.res_dirs:
ret.update(self._CollectResourcesListFromDirectory(res_dir))
return ret
def WriteRTxtFile(self, rtxt_path):
resources = self._CollectResourcesListFromDirectories()
with build_utils.AtomicOutput(rtxt_path, mode='w') as f:
for resource in resources:
line = '{0.java_type} {0.resource_type} {0.name} {0.value}\n'.format(
resource)
f.write(line)