generate_v14_compatible_resources.py revision 23730a6e56a168d1879203e4b3819bb36e3d8f1f
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
9There are two reasons that we cannot just use API 17 attributes,
10so we are generating another set of resources by this script.
11
121. paddingStart attribute can cause a crash on Galaxy Tab 2.
132. There is a bug that paddingStart does not override paddingLeft on
14   JB-MR1. This is fixed on JB-MR2.
15
16Therefore, this resource generation script can be removed when
17we drop the support for JB-MR1.
18
19Please refer to http://crbug.com/235118 for the details.
20"""
21
22import optparse
23import os
24import re
25import shutil
26import sys
27import xml.dom.minidom as minidom
28
29from 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
34GRAVITY_ATTRIBUTES = ('android:gravity', 'android:layout_gravity')
35
36# Almost all the attributes that has "Start" or "End" in
37# its name should be mapped.
38ATTRIBUTES_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
51ATTRIBUTES_TO_MAP = dict(['android:' + k, 'android:' + v] for k, v
52                         in ATTRIBUTES_TO_MAP.iteritems())
53
54ATTRIBUTES_TO_MAP_REVERSED = dict([v, k] for k, v
55                                  in ATTRIBUTES_TO_MAP.iteritems())
56
57
58def 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
68def 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@chromium.org or newt@chromium.org')
83    raise Exception(msg)
84
85
86def 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
93def 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
100def 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
111def 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
143def 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
170def 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
192def 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
206def 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
216def 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
224def 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
243def 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
254def 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
284def main():
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
344if __name__ == '__main__':
345  sys.exit(main())
346
347