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.
7 """Convert Android xml resources to API 14 compatible.
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.
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. b/8654490
16 Therefore, this resource generation script can be removed when
17 we drop the support for JB-MR1.
19 Please refer to http://crbug.com/235118 for the details.
28 import xml
.dom
.minidom
as minidom
30 from util
import build_utils
32 # Note that we are assuming 'android:' is an alias of
33 # the namespace 'http://schemas.android.com/apk/res/android'.
35 GRAVITY_ATTRIBUTES
= ('android:gravity', 'android:layout_gravity')
37 # Almost all the attributes that has "Start" or "End" in
38 # its name should be mapped.
39 ATTRIBUTES_TO_MAP
= {'paddingStart' : 'paddingLeft',
40 'drawableStart' : 'drawableLeft',
41 'layout_alignStart' : 'layout_alignLeft',
42 'layout_marginStart' : 'layout_marginLeft',
43 'layout_alignParentStart' : 'layout_alignParentLeft',
44 'layout_toStartOf' : 'layout_toLeftOf',
45 'paddingEnd' : 'paddingRight',
46 'drawableEnd' : 'drawableRight',
47 'layout_alignEnd' : 'layout_alignRight',
48 'layout_marginEnd' : 'layout_marginRight',
49 'layout_alignParentEnd' : 'layout_alignParentRight',
50 'layout_toEndOf' : 'layout_toRightOf'}
52 ATTRIBUTES_TO_MAP
= dict(['android:' + k
, 'android:' + v
] for k
, v
53 in ATTRIBUTES_TO_MAP
.iteritems())
55 ATTRIBUTES_TO_MAP_REVERSED
= dict([v
, k
] for k
, v
56 in ATTRIBUTES_TO_MAP
.iteritems())
59 def IterateXmlElements(node
):
60 """minidom helper function that iterates all the element nodes.
61 Iteration order is pre-order depth-first."""
62 if node
.nodeType
== node
.ELEMENT_NODE
:
64 for child_node
in node
.childNodes
:
65 for child_node_element
in IterateXmlElements(child_node
):
66 yield child_node_element
69 def ParseAndReportErrors(filename
):
71 return minidom
.parse(filename
)
72 except Exception: # pylint: disable=broad-except
75 sys
.stderr
.write('Failed to parse XML file: %s\n' % filename
)
79 def AssertNotDeprecatedAttribute(name
, value
, filename
):
80 """Raises an exception if the given attribute is deprecated."""
82 if name
in ATTRIBUTES_TO_MAP_REVERSED
:
83 msg
= '{0} should use {1} instead of {2}'.format(filename
,
84 ATTRIBUTES_TO_MAP_REVERSED
[name
], name
)
85 elif name
in GRAVITY_ATTRIBUTES
and ('left' in value
or 'right' in value
):
86 msg
= '{0} should use start/end instead of left/right for {1}'.format(
90 msg
+= ('\nFor background, see: http://android-developers.blogspot.com/'
91 '2013/03/native-rtl-support-in-android-42.html\n'
92 'If you have a legitimate need for this attribute, discuss with '
93 'kkimlabs@chromium.org or newt@chromium.org')
97 def WriteDomToFile(dom
, filename
):
98 """Write the given dom to filename."""
99 build_utils
.MakeDirectory(os
.path
.dirname(filename
))
100 with codecs
.open(filename
, 'w', 'utf-8') as f
:
101 dom
.writexml(f
, '', ' ', '\n', encoding
='utf-8')
104 def HasStyleResource(dom
):
105 """Return True if the dom is a style resource, False otherwise."""
106 root_node
= IterateXmlElements(dom
).next()
107 return bool(root_node
.nodeName
== 'resources' and
108 list(root_node
.getElementsByTagName('style')))
111 def ErrorIfStyleResourceExistsInDir(input_dir
):
112 """If a style resource is in input_dir, raises an exception."""
113 for input_filename
in build_utils
.FindInDirectory(input_dir
, '*.xml'):
114 dom
= ParseAndReportErrors(input_filename
)
115 if HasStyleResource(dom
):
116 # Allow style file in third_party to exist in non-v17 directories so long
117 # as they do not contain deprecated attributes.
118 if not 'third_party' in input_dir
or (
119 GenerateV14StyleResourceDom(dom
, input_filename
)):
120 raise Exception('error: style file ' + input_filename
+
121 ' should be under ' + input_dir
+
122 '-v17 directory. Please refer to '
123 'http://crbug.com/243952 for the details.')
126 def GenerateV14LayoutResourceDom(dom
, filename
, assert_not_deprecated
=True):
127 """Convert layout resource to API 14 compatible layout resource.
130 dom: Parsed minidom object to be modified.
131 filename: Filename that the DOM was parsed from.
132 assert_not_deprecated: Whether deprecated attributes (e.g. paddingLeft) will
133 cause an exception to be thrown.
136 True if dom is modified, False otherwise.
140 # Iterate all the elements' attributes to find attributes to convert.
141 for element
in IterateXmlElements(dom
):
142 for name
, value
in list(element
.attributes
.items()):
143 # Convert any API 17 Start/End attributes to Left/Right attributes.
144 # For example, from paddingStart="10dp" to paddingLeft="10dp"
145 # Note: gravity attributes are not necessary to convert because
146 # start/end values are backward-compatible. Explained at
147 # https://plus.sandbox.google.com/+RomanNurik/posts/huuJd8iVVXY?e=Showroom
148 if name
in ATTRIBUTES_TO_MAP
:
149 element
.setAttribute(ATTRIBUTES_TO_MAP
[name
], value
)
150 del element
.attributes
[name
]
152 elif assert_not_deprecated
:
153 AssertNotDeprecatedAttribute(name
, value
, filename
)
158 def GenerateV14StyleResourceDom(dom
, filename
, assert_not_deprecated
=True):
159 """Convert style resource to API 14 compatible style resource.
162 dom: Parsed minidom object to be modified.
163 filename: Filename that the DOM was parsed from.
164 assert_not_deprecated: Whether deprecated attributes (e.g. paddingLeft) will
165 cause an exception to be thrown.
168 True if dom is modified, False otherwise.
172 for style_element
in dom
.getElementsByTagName('style'):
173 for item_element
in style_element
.getElementsByTagName('item'):
174 name
= item_element
.attributes
['name'].value
175 value
= item_element
.childNodes
[0].nodeValue
176 if name
in ATTRIBUTES_TO_MAP
:
177 item_element
.attributes
['name'].value
= ATTRIBUTES_TO_MAP
[name
]
179 elif assert_not_deprecated
:
180 AssertNotDeprecatedAttribute(name
, value
, filename
)
185 def GenerateV14LayoutResource(input_filename
, output_v14_filename
,
186 output_v17_filename
):
187 """Convert API 17 layout resource to API 14 compatible layout resource.
189 It's mostly a simple replacement, s/Start/Left s/End/Right,
190 on the attribute names.
191 If the generated resource is identical to the original resource,
192 don't do anything. If not, write the generated resource to
193 output_v14_filename, and copy the original resource to output_v17_filename.
195 dom
= ParseAndReportErrors(input_filename
)
196 is_modified
= GenerateV14LayoutResourceDom(dom
, input_filename
)
199 # Write the generated resource.
200 WriteDomToFile(dom
, output_v14_filename
)
202 # Copy the original resource.
203 build_utils
.MakeDirectory(os
.path
.dirname(output_v17_filename
))
204 shutil
.copy2(input_filename
, output_v17_filename
)
207 def GenerateV14StyleResource(input_filename
, output_v14_filename
):
208 """Convert API 17 style resources to API 14 compatible style resource.
210 Write the generated style resource to output_v14_filename.
211 It's mostly a simple replacement, s/Start/Left s/End/Right,
212 on the attribute names.
214 dom
= ParseAndReportErrors(input_filename
)
215 GenerateV14StyleResourceDom(dom
, input_filename
)
217 # Write the generated resource.
218 WriteDomToFile(dom
, output_v14_filename
)
221 def GenerateV14LayoutResourcesInDir(input_dir
, output_v14_dir
, output_v17_dir
):
222 """Convert layout resources to API 14 compatible resources in input_dir."""
223 for input_filename
in build_utils
.FindInDirectory(input_dir
, '*.xml'):
224 rel_filename
= os
.path
.relpath(input_filename
, input_dir
)
225 output_v14_filename
= os
.path
.join(output_v14_dir
, rel_filename
)
226 output_v17_filename
= os
.path
.join(output_v17_dir
, rel_filename
)
227 GenerateV14LayoutResource(input_filename
, output_v14_filename
,
231 def GenerateV14StyleResourcesInDir(input_dir
, output_v14_dir
):
232 """Convert style resources to API 14 compatible resources in input_dir."""
233 for input_filename
in build_utils
.FindInDirectory(input_dir
, '*.xml'):
234 rel_filename
= os
.path
.relpath(input_filename
, input_dir
)
235 output_v14_filename
= os
.path
.join(output_v14_dir
, rel_filename
)
236 GenerateV14StyleResource(input_filename
, output_v14_filename
)
240 """Parses command line options.
243 An options object as from optparse.OptionsParser.parse_args()
245 parser
= optparse
.OptionParser()
246 parser
.add_option('--res-dir',
247 help='directory containing resources '
248 'used to generate v14 compatible resources')
249 parser
.add_option('--res-v14-compatibility-dir',
250 help='output directory into which '
251 'v14 compatible resources will be generated')
252 parser
.add_option('--stamp', help='File to touch on success')
254 options
, args
= parser
.parse_args()
257 parser
.error('No positional arguments should be given.')
259 # Check that required options have been provided.
260 required_options
= ('res_dir', 'res_v14_compatibility_dir')
261 build_utils
.CheckOptions(options
, parser
, required
=required_options
)
264 def GenerateV14Resources(res_dir
, res_v14_dir
):
265 for name
in os
.listdir(res_dir
):
266 if not os
.path
.isdir(os
.path
.join(res_dir
, name
)):
269 dir_pieces
= name
.split('-')
270 resource_type
= dir_pieces
[0]
271 qualifiers
= dir_pieces
[1:]
273 api_level_qualifier_index
= -1
274 api_level_qualifier
= ''
275 for index
, qualifier
in enumerate(qualifiers
):
276 if re
.match('v[0-9]+$', qualifier
):
277 api_level_qualifier_index
= index
278 api_level_qualifier
= qualifier
281 # Android pre-v17 API doesn't support RTL. Skip.
282 if 'ldrtl' in qualifiers
:
285 input_dir
= os
.path
.abspath(os
.path
.join(res_dir
, name
))
287 # We also need to copy the original v17 resource to *-v17 directory
288 # because the generated v14 resource will hide the original resource.
289 output_v14_dir
= os
.path
.join(res_v14_dir
, name
)
290 output_v17_dir
= os
.path
.join(res_v14_dir
, name
+ '-v17')
292 # We only convert layout resources under layout*/, xml*/,
293 # and style resources under values*/.
294 if resource_type
in ('layout', 'xml'):
295 if not api_level_qualifier
:
296 GenerateV14LayoutResourcesInDir(input_dir
, output_v14_dir
,
298 elif resource_type
== 'values':
299 if api_level_qualifier
== 'v17':
300 output_qualifiers
= qualifiers
[:]
301 del output_qualifiers
[api_level_qualifier_index
]
302 output_v14_dir
= os
.path
.join(res_v14_dir
,
303 '-'.join([resource_type
] +
305 GenerateV14StyleResourcesInDir(input_dir
, output_v14_dir
)
306 elif not api_level_qualifier
:
307 ErrorIfStyleResourceExistsInDir(input_dir
)
310 options
= ParseArgs()
312 res_v14_dir
= options
.res_v14_compatibility_dir
314 build_utils
.DeleteDirectory(res_v14_dir
)
315 build_utils
.MakeDirectory(res_v14_dir
)
317 GenerateV14Resources(options
.res_dir
, res_v14_dir
)
320 build_utils
.Touch(options
.stamp
)
322 if __name__
== '__main__':