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 AssertNotDeprecatedAttribute(name, value, filename): 69 """Raises an exception if the given attribute is deprecated.""" 70 msg = None 71 if name in ATTRIBUTES_TO_MAP_REVERSED: 72 msg = '{0} should use {1} instead of {2}'.format(filename, 73 ATTRIBUTES_TO_MAP_REVERSED[name], name) 74 elif name in GRAVITY_ATTRIBUTES and ('left' in value or 'right' in value): 75 msg = '{0} should use start/end instead of left/right for {1}'.format( 76 filename, name) 77 78 if msg: 79 msg += ('\nFor background, see: http://android-developers.blogspot.com/' 80 '2013/03/native-rtl-support-in-android-42.html\n' 81 'If you have a legitimate need for this attribute, discuss with ' 82 'kkimlabs (at] chromium.org or newt (at] chromium.org') 83 raise Exception(msg) 84 85 86 def WriteDomToFile(dom, filename): 87 """Write the given dom to filename.""" 88 build_utils.MakeDirectory(os.path.dirname(filename)) 89 with open(filename, 'w') as f: 90 dom.writexml(f, '', ' ', '\n', encoding='utf-8') 91 92 93 def HasStyleResource(dom): 94 """Return True if the dom is a style resource, False otherwise.""" 95 root_node = IterateXmlElements(dom).next() 96 return bool(root_node.nodeName == 'resources' and 97 list(root_node.getElementsByTagName('style'))) 98 99 100 def ErrorIfStyleResourceExistsInDir(input_dir): 101 """If a style resource is in input_dir, raises an exception.""" 102 for input_filename in build_utils.FindInDirectory(input_dir, '*.xml'): 103 dom = minidom.parse(input_filename) 104 if HasStyleResource(dom): 105 raise Exception('error: style file ' + input_filename + 106 ' should be under ' + input_dir + 107 '-v17 directory. Please refer to ' 108 'http://crbug.com/243952 for the details.') 109 110 111 def GenerateV14LayoutResourceDom(dom, filename, assert_not_deprecated=True): 112 """Convert layout resource to API 14 compatible layout resource. 113 114 Args: 115 dom: Parsed minidom object to be modified. 116 filename: Filename that the DOM was parsed from. 117 assert_not_deprecated: Whether deprecated attributes (e.g. paddingLeft) will 118 cause an exception to be thrown. 119 120 Returns: 121 True if dom is modified, False otherwise. 122 """ 123 is_modified = False 124 125 # Iterate all the elements' attributes to find attributes to convert. 126 for element in IterateXmlElements(dom): 127 for name, value in list(element.attributes.items()): 128 # Convert any API 17 Start/End attributes to Left/Right attributes. 129 # For example, from paddingStart="10dp" to paddingLeft="10dp" 130 # Note: gravity attributes are not necessary to convert because 131 # start/end values are backward-compatible. Explained at 132 # https://plus.sandbox.google.com/+RomanNurik/posts/huuJd8iVVXY?e=Showroom 133 if name in ATTRIBUTES_TO_MAP: 134 element.setAttribute(ATTRIBUTES_TO_MAP[name], value) 135 del element.attributes[name] 136 is_modified = True 137 elif assert_not_deprecated: 138 AssertNotDeprecatedAttribute(name, value, filename) 139 140 return is_modified 141 142 143 def GenerateV14StyleResourceDom(dom, filename, assert_not_deprecated=True): 144 """Convert style resource to API 14 compatible style resource. 145 146 Args: 147 dom: Parsed minidom object to be modified. 148 filename: Filename that the DOM was parsed from. 149 assert_not_deprecated: Whether deprecated attributes (e.g. paddingLeft) will 150 cause an exception to be thrown. 151 152 Returns: 153 True if dom is modified, False otherwise. 154 """ 155 is_modified = False 156 157 for style_element in dom.getElementsByTagName('style'): 158 for item_element in style_element.getElementsByTagName('item'): 159 name = item_element.attributes['name'].value 160 value = item_element.childNodes[0].nodeValue 161 if name in ATTRIBUTES_TO_MAP: 162 item_element.attributes['name'].value = ATTRIBUTES_TO_MAP[name] 163 is_modified = True 164 elif assert_not_deprecated: 165 AssertNotDeprecatedAttribute(name, value, filename) 166 167 return is_modified 168 169 170 def GenerateV14LayoutResource(input_filename, output_v14_filename, 171 output_v17_filename): 172 """Convert API 17 layout resource to API 14 compatible layout resource. 173 174 It's mostly a simple replacement, s/Start/Left s/End/Right, 175 on the attribute names. 176 If the generated resource is identical to the original resource, 177 don't do anything. If not, write the generated resource to 178 output_v14_filename, and copy the original resource to output_v17_filename. 179 """ 180 dom = minidom.parse(input_filename) 181 is_modified = GenerateV14LayoutResourceDom(dom, input_filename) 182 183 if is_modified: 184 # Write the generated resource. 185 WriteDomToFile(dom, output_v14_filename) 186 187 # Copy the original resource. 188 build_utils.MakeDirectory(os.path.dirname(output_v17_filename)) 189 shutil.copy2(input_filename, output_v17_filename) 190 191 192 def GenerateV14StyleResource(input_filename, output_v14_filename): 193 """Convert API 17 style resources to API 14 compatible style resource. 194 195 Write the generated style resource to output_v14_filename. 196 It's mostly a simple replacement, s/Start/Left s/End/Right, 197 on the attribute names. 198 """ 199 dom = minidom.parse(input_filename) 200 GenerateV14StyleResourceDom(dom, input_filename) 201 202 # Write the generated resource. 203 WriteDomToFile(dom, output_v14_filename) 204 205 206 def GenerateV14LayoutResourcesInDir(input_dir, output_v14_dir, output_v17_dir): 207 """Convert layout 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 output_v17_filename = os.path.join(output_v17_dir, rel_filename) 212 GenerateV14LayoutResource(input_filename, output_v14_filename, 213 output_v17_filename) 214 215 216 def GenerateV14StyleResourcesInDir(input_dir, output_v14_dir): 217 """Convert style resources to API 14 compatible resources in input_dir.""" 218 for input_filename in build_utils.FindInDirectory(input_dir, '*.xml'): 219 rel_filename = os.path.relpath(input_filename, input_dir) 220 output_v14_filename = os.path.join(output_v14_dir, rel_filename) 221 GenerateV14StyleResource(input_filename, output_v14_filename) 222 223 224 def VerifyV14ResourcesInDir(input_dir, resource_type): 225 """Verify that the resources in input_dir is compatible with v14, i.e., they 226 don't use attributes that cause crashes on certain devices. Print an error if 227 they have.""" 228 for input_filename in build_utils.FindInDirectory(input_dir, '*.xml'): 229 exception_message = ('error : ' + input_filename + ' has an RTL attribute, ' 230 'i.e., attribute that has "start" or "end" in its name.' 231 ' Pre-v17 resources should not include it because it ' 232 'can cause crashes on certain devices. Please refer to ' 233 'http://crbug.com/243952 for the details.') 234 dom = minidom.parse(input_filename) 235 if resource_type in ('layout', 'xml'): 236 if GenerateV14LayoutResourceDom(dom, input_filename, False): 237 raise Exception(exception_message) 238 elif resource_type == 'values': 239 if GenerateV14StyleResourceDom(dom, input_filename, False): 240 raise Exception(exception_message) 241 242 243 def AssertNoDeprecatedAttributesInDir(input_dir, resource_type): 244 """Raises an exception if resources in input_dir have deprecated attributes, 245 e.g., paddingLeft, paddingRight""" 246 for input_filename in build_utils.FindInDirectory(input_dir, '*.xml'): 247 dom = minidom.parse(input_filename) 248 if resource_type in ('layout', 'xml'): 249 GenerateV14LayoutResourceDom(dom, input_filename) 250 elif resource_type == 'values': 251 GenerateV14StyleResourceDom(dom, input_filename) 252 253 254 def ParseArgs(): 255 """Parses command line options. 256 257 Returns: 258 An options object as from optparse.OptionsParser.parse_args() 259 """ 260 parser = optparse.OptionParser() 261 parser.add_option('--res-dir', 262 help='directory containing resources ' 263 'used to generate v14 compatible resources') 264 parser.add_option('--res-v14-compatibility-dir', 265 help='output directory into which ' 266 'v14 compatible resources will be generated') 267 parser.add_option('--stamp', help='File to touch on success') 268 parser.add_option('--verify-only', action="store_true", help='Do not generate' 269 ' v14 resources. Instead, just verify that the resources are already ' 270 "compatible with v14, i.e. they don't use attributes that cause crashes " 271 'on certain devices.') 272 273 options, args = parser.parse_args() 274 275 if args: 276 parser.error('No positional arguments should be given.') 277 278 # Check that required options have been provided. 279 required_options = ('res_dir', 'res_v14_compatibility_dir') 280 build_utils.CheckOptions(options, parser, required=required_options) 281 return options 282 283 284 def main(argv): 285 options = ParseArgs() 286 287 build_utils.DeleteDirectory(options.res_v14_compatibility_dir) 288 build_utils.MakeDirectory(options.res_v14_compatibility_dir) 289 290 for name in os.listdir(options.res_dir): 291 if not os.path.isdir(os.path.join(options.res_dir, name)): 292 continue 293 294 dir_pieces = name.split('-') 295 resource_type = dir_pieces[0] 296 qualifiers = dir_pieces[1:] 297 298 api_level_qualifier_index = -1 299 api_level_qualifier = '' 300 for index, qualifier in enumerate(qualifiers): 301 if re.match('v[0-9]+$', qualifier): 302 api_level_qualifier_index = index 303 api_level_qualifier = qualifier 304 break 305 306 # Android pre-v17 API doesn't support RTL. Skip. 307 if 'ldrtl' in qualifiers: 308 continue 309 310 input_dir = os.path.abspath(os.path.join(options.res_dir, name)) 311 312 if options.verify_only: 313 if not api_level_qualifier or int(api_level_qualifier[1:]) < 17: 314 VerifyV14ResourcesInDir(input_dir, resource_type) 315 else: 316 AssertNoDeprecatedAttributesInDir(input_dir, resource_type) 317 else: 318 # We also need to copy the original v17 resource to *-v17 directory 319 # because the generated v14 resource will hide the original resource. 320 output_v14_dir = os.path.join(options.res_v14_compatibility_dir, name) 321 output_v17_dir = os.path.join(options.res_v14_compatibility_dir, name + 322 '-v17') 323 324 # We only convert layout resources under layout*/, xml*/, 325 # and style resources under values*/. 326 if resource_type in ('layout', 'xml'): 327 if not api_level_qualifier: 328 GenerateV14LayoutResourcesInDir(input_dir, output_v14_dir, 329 output_v17_dir) 330 elif resource_type == 'values': 331 if api_level_qualifier == 'v17': 332 output_qualifiers = qualifiers[:] 333 del output_qualifiers[api_level_qualifier_index] 334 output_v14_dir = os.path.join(options.res_v14_compatibility_dir, 335 '-'.join([resource_type] + 336 output_qualifiers)) 337 GenerateV14StyleResourcesInDir(input_dir, output_v14_dir) 338 elif not api_level_qualifier: 339 ErrorIfStyleResourceExistsInDir(input_dir) 340 341 if options.stamp: 342 build_utils.Touch(options.stamp) 343 344 if __name__ == '__main__': 345 sys.exit(main(sys.argv)) 346 347