1 #!/usr/bin/env python 2 # 3 # Copyright 2013 The Chromium Authors. All rights reserved. 4 # Use of this source code is governed by a BSD-style license that can be 5 # found in the LICENSE file. 6 7 """Convert Android xml resources to API 14 compatible. 8 9 There are two reasons that we cannot just use API 17 attributes, 10 so we are generating another set of resources by this script. 11 12 1. paddingStart attribute can cause a crash on Galaxy Tab 2. 13 2. There is a bug that paddingStart does not override paddingLeft on 14 JB-MR1. This is fixed on JB-MR2. 15 16 Therefore, this resource generation script can be removed when 17 we drop the support for JB-MR1. 18 19 Please refer to http://crbug.com/235118 for the details. 20 """ 21 22 import optparse 23 import os 24 import re 25 import shutil 26 import sys 27 import xml.dom.minidom as minidom 28 29 from util import build_utils 30 31 # Note that we are assuming 'android:' is an alias of 32 # the namespace 'http://schemas.android.com/apk/res/android'. 33 34 GRAVITY_ATTRIBUTES = ('android:gravity', 'android:layout_gravity') 35 36 # Almost all the attributes that has "Start" or "End" in 37 # its name should be mapped. 38 ATTRIBUTES_TO_MAP = {'paddingStart' : 'paddingLeft', 39 'drawableStart' : 'drawableLeft', 40 'layout_alignStart' : 'layout_alignLeft', 41 'layout_marginStart' : 'layout_marginLeft', 42 'layout_alignParentStart' : 'layout_alignParentLeft', 43 'layout_toStartOf' : 'layout_toLeftOf', 44 'paddingEnd' : 'paddingRight', 45 'drawableEnd' : 'drawableRight', 46 'layout_alignEnd' : 'layout_alignRight', 47 'layout_marginEnd' : 'layout_marginRight', 48 'layout_alignParentEnd' : 'layout_alignParentRight', 49 'layout_toEndOf' : 'layout_toRightOf'} 50 51 ATTRIBUTES_TO_MAP = dict(['android:' + k, 'android:' + v] for k, v 52 in ATTRIBUTES_TO_MAP.iteritems()) 53 54 ATTRIBUTES_TO_MAP_REVERSED = dict([v,k] for k, v 55 in ATTRIBUTES_TO_MAP.iteritems()) 56 57 58 def IterateXmlElements(node): 59 """minidom helper function that iterates all the element nodes. 60 Iteration order is pre-order depth-first.""" 61 if node.nodeType == node.ELEMENT_NODE: 62 yield node 63 for child_node in node.childNodes: 64 for child_node_element in IterateXmlElements(child_node): 65 yield child_node_element 66 67 68 def WarnIfDeprecatedAttribute(name, value, filename): 69 """print a warning message if the given attribute is deprecated.""" 70 if name in ATTRIBUTES_TO_MAP_REVERSED: 71 print >> sys.stderr, ('warning: ' + filename + ' should use ' + 72 ATTRIBUTES_TO_MAP_REVERSED[name] + 73 ' instead of ' + name) 74 elif name in GRAVITY_ATTRIBUTES and ('left' in value or 'right' in value): 75 print >> sys.stderr, ('warning: ' + filename + 76 ' should use start/end instead of left/right for ' + 77 name) 78 79 80 def WriteDomToFile(dom, filename): 81 """Write the given dom to filename.""" 82 build_utils.MakeDirectory(os.path.dirname(filename)) 83 with open(filename, 'w') as f: 84 dom.writexml(f, '', ' ', '\n', encoding='utf-8') 85 86 87 def HasStyleResource(dom): 88 """Return True if the dom is a style resource, False otherwise.""" 89 root_node = IterateXmlElements(dom).next() 90 return bool(root_node.nodeName == 'resources' and 91 list(root_node.getElementsByTagName('style'))) 92 93 94 def ErrorIfStyleResourceExistsInDir(input_dir): 95 """If a style resource is in input_dir, exist with an error message.""" 96 for input_filename in build_utils.FindInDirectory(input_dir, '*.xml'): 97 dom = minidom.parse(input_filename) 98 if HasStyleResource(dom): 99 raise Exception('error: style file ' + input_filename + 100 ' should be under ' + input_dir + 101 '-v17 directory. Please refer to ' 102 'http://crbug.com/243952 for the details.') 103 104 105 def GenerateV14LayoutResourceDom(dom, filename_for_warning): 106 """Convert layout resource to API 14 compatible layout resource. 107 108 Args: 109 dom: parsed minidom object to be modified. 110 filename_for_warning: file name to display in case we print warnings. 111 If None, do not print warning. 112 Returns: 113 True if dom is modified, False otherwise. 114 """ 115 is_modified = False 116 117 # Iterate all the elements' attributes to find attributes to convert. 118 for element in IterateXmlElements(dom): 119 for name, value in list(element.attributes.items()): 120 # Convert any API 17 Start/End attributes to Left/Right attributes. 121 # For example, from paddingStart="10dp" to paddingLeft="10dp" 122 # Note: gravity attributes are not necessary to convert because 123 # start/end values are backward-compatible. Explained at 124 # https://plus.sandbox.google.com/+RomanNurik/posts/huuJd8iVVXY?e=Showroom 125 if name in ATTRIBUTES_TO_MAP: 126 element.setAttribute(ATTRIBUTES_TO_MAP[name], value) 127 del element.attributes[name] 128 is_modified = True 129 elif filename_for_warning: 130 WarnIfDeprecatedAttribute(name, value, filename_for_warning) 131 132 return is_modified 133 134 135 def GenerateV14StyleResourceDom(dom, filename_for_warning): 136 """Convert style resource to API 14 compatible style resource. 137 138 Args: 139 dom: parsed minidom object to be modified. 140 filename_for_warning: file name to display in case we print warnings. 141 If None, do not print warning. 142 Returns: 143 True if dom is modified, False otherwise. 144 """ 145 is_modified = False 146 147 for style_element in dom.getElementsByTagName('style'): 148 for item_element in style_element.getElementsByTagName('item'): 149 name = item_element.attributes['name'].value 150 value = item_element.childNodes[0].nodeValue 151 if name in ATTRIBUTES_TO_MAP: 152 item_element.attributes['name'].value = ATTRIBUTES_TO_MAP[name] 153 is_modified = True 154 elif filename_for_warning: 155 WarnIfDeprecatedAttribute(name, value, filename_for_warning) 156 157 return is_modified 158 159 160 def GenerateV14LayoutResource(input_filename, output_v14_filename, 161 output_v17_filename): 162 """Convert API 17 layout resource to API 14 compatible layout resource. 163 164 It's mostly a simple replacement, s/Start/Left s/End/Right, 165 on the attribute names. 166 If the generated resource is identical to the original resource, 167 don't do anything. If not, write the generated resource to 168 output_v14_filename, and copy the original resource to output_v17_filename. 169 """ 170 dom = minidom.parse(input_filename) 171 is_modified = GenerateV14LayoutResourceDom(dom, input_filename) 172 173 if is_modified: 174 # Write the generated resource. 175 WriteDomToFile(dom, output_v14_filename) 176 177 # Copy the original resource. 178 build_utils.MakeDirectory(os.path.dirname(output_v17_filename)) 179 shutil.copy2(input_filename, output_v17_filename) 180 181 182 def GenerateV14StyleResource(input_filename, output_v14_filename): 183 """Convert API 17 style resources to API 14 compatible style resource. 184 185 Write the generated style resource to output_v14_filename. 186 It's mostly a simple replacement, s/Start/Left s/End/Right, 187 on the attribute names. 188 """ 189 dom = minidom.parse(input_filename) 190 GenerateV14StyleResourceDom(dom, input_filename) 191 192 # Write the generated resource. 193 WriteDomToFile(dom, output_v14_filename) 194 195 196 def GenerateV14LayoutResourcesInDir(input_dir, output_v14_dir, output_v17_dir): 197 """Convert layout resources to API 14 compatible resources in input_dir.""" 198 for input_filename in build_utils.FindInDirectory(input_dir, '*.xml'): 199 rel_filename = os.path.relpath(input_filename, input_dir) 200 output_v14_filename = os.path.join(output_v14_dir, rel_filename) 201 output_v17_filename = os.path.join(output_v17_dir, rel_filename) 202 GenerateV14LayoutResource(input_filename, output_v14_filename, 203 output_v17_filename) 204 205 206 def GenerateV14StyleResourcesInDir(input_dir, output_v14_dir): 207 """Convert style resources to API 14 compatible resources in input_dir.""" 208 for input_filename in build_utils.FindInDirectory(input_dir, '*.xml'): 209 rel_filename = os.path.relpath(input_filename, input_dir) 210 output_v14_filename = os.path.join(output_v14_dir, rel_filename) 211 GenerateV14StyleResource(input_filename, output_v14_filename) 212 213 214 def VerifyV14ResourcesInDir(input_dir, resource_type): 215 """Verify that the resources in input_dir is compatible with v14, i.e., they 216 don't use attributes that cause crashes on certain devices. Print an error if 217 they have.""" 218 for input_filename in build_utils.FindInDirectory(input_dir, '*.xml'): 219 exception_message = ('error : ' + input_filename + ' has an RTL attribute, ' 220 'i.e., attribute that has "start" or "end" in its name.' 221 ' Pre-v17 resources should not include it because it ' 222 'can cause crashes on certain devices. Please refer to ' 223 'http://crbug.com/243952 for the details.') 224 dom = minidom.parse(input_filename) 225 if resource_type in ('layout', 'xml'): 226 if GenerateV14LayoutResourceDom(dom, None): 227 raise Exception(exception_message) 228 elif resource_type == 'values': 229 if GenerateV14StyleResourceDom(dom, None): 230 raise Exception(exception_message) 231 232 233 def WarnIfDeprecatedAttributeInDir(input_dir, resource_type): 234 """Print warning if resources in input_dir have deprecated attributes, e.g., 235 paddingLeft, PaddingRight""" 236 for input_filename in build_utils.FindInDirectory(input_dir, '*.xml'): 237 dom = minidom.parse(input_filename) 238 if resource_type in ('layout', 'xml'): 239 GenerateV14LayoutResourceDom(dom, input_filename) 240 elif resource_type == 'values': 241 GenerateV14StyleResourceDom(dom, input_filename) 242 243 244 def ParseArgs(): 245 """Parses command line options. 246 247 Returns: 248 An options object as from optparse.OptionsParser.parse_args() 249 """ 250 parser = optparse.OptionParser() 251 parser.add_option('--res-dir', 252 help='directory containing resources ' 253 'used to generate v14 compatible resources') 254 parser.add_option('--res-v14-compatibility-dir', 255 help='output directory into which ' 256 'v14 compatible resources will be generated') 257 parser.add_option('--stamp', help='File to touch on success') 258 parser.add_option('--verify-only', action="store_true", help='Do not generate' 259 ' v14 resources. Instead, just verify that the resources are already ' 260 "compatible with v14, i.e. they don't use attributes that cause crashes " 261 'on certain devices.') 262 263 options, args = parser.parse_args() 264 265 if args: 266 parser.error('No positional arguments should be given.') 267 268 # Check that required options have been provided. 269 required_options = ('res_dir', 'res_v14_compatibility_dir') 270 build_utils.CheckOptions(options, parser, required=required_options) 271 return options 272 273 274 def main(argv): 275 options = ParseArgs() 276 277 build_utils.DeleteDirectory(options.res_v14_compatibility_dir) 278 build_utils.MakeDirectory(options.res_v14_compatibility_dir) 279 280 for name in os.listdir(options.res_dir): 281 if not os.path.isdir(os.path.join(options.res_dir, name)): 282 continue 283 284 dir_pieces = name.split('-') 285 resource_type = dir_pieces[0] 286 qualifiers = dir_pieces[1:] 287 288 api_level_qualifier_index = -1 289 api_level_qualifier = '' 290 for index, qualifier in enumerate(qualifiers): 291 if re.match('v[0-9]+$', qualifier): 292 api_level_qualifier_index = index 293 api_level_qualifier = qualifier 294 break 295 296 # Android pre-v17 API doesn't support RTL. Skip. 297 if 'ldrtl' in qualifiers: 298 continue 299 300 input_dir = os.path.abspath(os.path.join(options.res_dir, name)) 301 302 if options.verify_only: 303 if not api_level_qualifier or int(api_level_qualifier[1:]) < 17: 304 VerifyV14ResourcesInDir(input_dir, resource_type) 305 else: 306 WarnIfDeprecatedAttributeInDir(input_dir, resource_type) 307 else: 308 # We also need to copy the original v17 resource to *-v17 directory 309 # because the generated v14 resource will hide the original resource. 310 output_v14_dir = os.path.join(options.res_v14_compatibility_dir, name) 311 output_v17_dir = os.path.join(options.res_v14_compatibility_dir, name + 312 '-v17') 313 314 # We only convert layout resources under layout*/, xml*/, 315 # and style resources under values*/. 316 if resource_type in ('layout', 'xml'): 317 if not api_level_qualifier: 318 GenerateV14LayoutResourcesInDir(input_dir, output_v14_dir, 319 output_v17_dir) 320 elif resource_type == 'values': 321 if api_level_qualifier == 'v17': 322 output_qualifiers = qualifiers[:] 323 del output_qualifiers[api_level_qualifier_index] 324 output_v14_dir = os.path.join(options.res_v14_compatibility_dir, 325 '-'.join([resource_type] + 326 output_qualifiers)) 327 GenerateV14StyleResourcesInDir(input_dir, output_v14_dir) 328 elif not api_level_qualifier: 329 ErrorIfStyleResourceExistsInDir(input_dir) 330 331 if options.stamp: 332 build_utils.Touch(options.stamp) 333 334 if __name__ == '__main__': 335 sys.exit(main(sys.argv)) 336 337