# 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)