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