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