Home | History | Annotate | Download | only in gyp
      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