1280cf47404e416214f665a790d2938d10910fbd
[platform/framework/web/crosswalk.git] / src / xwalk / app / tools / android / customize.py
1 #!/usr/bin/env python
2
3 # Copyright (c) 2013 Intel Corporation. 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 import json
8 import optparse
9 import os
10 import re
11 import shutil
12 import sys
13
14 from handle_xml import AddElementAttribute
15 from handle_xml import AddElementAttributeAndText
16 from handle_xml import EditElementAttribute
17 from handle_xml import EditElementValueByNodeName
18 from handle_permissions import HandlePermissions
19 from xml.dom import minidom
20
21 def ReplaceInvalidChars(value, mode='default'):
22   """ Replace the invalid chars with '_' for input string.
23   Args:
24     value: the original string.
25     mode: the target usage mode of original string.
26   """
27   if mode == 'default':
28     invalid_chars = '\/:*?"<>|- '
29   elif mode == 'apkname':
30     invalid_chars = '\/:.*?"<>|-'
31   for c in invalid_chars:
32     if mode == 'apkname' and c in value:
33       print("Illegal character: '%s' is replaced with '_'" % c)
34     value = value.replace(c,'_')
35   return value
36
37
38 def Prepare(sanitized_name, package, app_root):
39   if os.path.exists(sanitized_name):
40     shutil.rmtree(sanitized_name)
41   shutil.copytree('app_src', sanitized_name)
42   shutil.rmtree(os.path.join(sanitized_name, 'src'))
43   src_root = os.path.join('app_src', 'src', 'org', 'xwalk', 'app', 'template')
44   src_activity = os.path.join(src_root, 'AppTemplateActivity.java')
45   if not os.path.isfile(src_activity):
46     print ('Please make sure that the java file'
47            ' of activity does exist.')
48     sys.exit(7)
49   root_path =  os.path.join(sanitized_name, 'src',
50                             package.replace('.', os.path.sep))
51   if not os.path.exists(root_path):
52     os.makedirs(root_path)
53   dest_activity = sanitized_name + 'Activity.java'
54   shutil.copyfile(src_activity, os.path.join(root_path, dest_activity))
55   if app_root:
56     assets_path = os.path.join(sanitized_name, 'assets')
57     shutil.rmtree(assets_path)
58     os.makedirs(assets_path)
59     app_src_path = os.path.join(assets_path, 'www')
60     shutil.copytree(app_root, app_src_path)
61
62
63 def CustomizeStringXML(sanitized_name, description):
64   strings_path = os.path.join(sanitized_name, 'res', 'values', 'strings.xml')
65   if not os.path.isfile(strings_path):
66     print ('Please make sure strings_xml'
67            ' exists under app_src folder.')
68     sys.exit(6)
69
70   if description:
71     xmldoc = minidom.parse(strings_path)
72     AddElementAttributeAndText(xmldoc, 'string', 'name', 'description',
73                                description)
74     strings_file = open(strings_path, 'w')
75     xmldoc.writexml(strings_file, encoding='utf-8')
76     strings_file.close()
77
78
79 def CustomizeThemeXML(sanitized_name, fullscreen, launch_screen_img):
80   theme_path = os.path.join(sanitized_name, 'res', 'values', 'theme.xml')
81   if not os.path.isfile(theme_path):
82     print('Error: theme.xml is missing in the build tool.')
83     sys.exit(6)
84
85   xmldoc = minidom.parse(theme_path)
86   if fullscreen:
87     EditElementValueByNodeName(xmldoc, 'item',
88                                'android:windowFullscreen', 'true')
89   if launch_screen_img:
90     EditElementValueByNodeName(xmldoc, 'item',
91                                'android:windowBackground',
92                                '@drawable/launchscreen')
93     default_image = launch_screen_img
94     if os.path.isfile(default_image):
95       drawable_path = os.path.join(sanitized_name, 'res', 'drawable')
96       if not os.path.exists(drawable_path):
97         os.makedirs(drawable_path)
98       # Get the extension of default_image.
99       # Need to take care of special case, such as 'img.9.png'
100       name = os.path.basename(default_image)
101       extlist = name.split('.')
102       # Remove the file name from the list.
103       extlist.pop(0)
104       ext = '.' + '.'.join(extlist)
105       final_launch_screen_path = os.path.join(drawable_path,
106                                               'launchscreen' + ext)
107       shutil.copyfile(default_image, final_launch_screen_path)
108     else:
109       print('Error: Please make sure \"' + default_image + '\" exists!')
110       sys.exit(6)
111   theme_file = open(theme_path, 'w')
112   xmldoc.writexml(theme_file, encoding='utf-8')
113   theme_file.close()
114
115
116 def CustomizeXML(sanitized_name, package, app_versionCode, app_version,
117                  description, name, orientation, icon_dict, fullscreen,
118                  icon, launch_screen_img, permissions, app_root):
119   manifest_path = os.path.join(sanitized_name, 'AndroidManifest.xml')
120   if not os.path.isfile(manifest_path):
121     print ('Please make sure AndroidManifest.xml'
122            ' exists under app_src folder.')
123     sys.exit(6)
124
125   CustomizeStringXML(sanitized_name, description)
126   CustomizeThemeXML(sanitized_name, fullscreen, launch_screen_img)
127   xmldoc = minidom.parse(manifest_path)
128   EditElementAttribute(xmldoc, 'manifest', 'package', package)
129   if app_versionCode:
130     EditElementAttribute(xmldoc, 'manifest', 'android:versionCode',
131                          str(app_versionCode))
132   if app_version:
133     EditElementAttribute(xmldoc, 'manifest', 'android:versionName',
134                          app_version)
135   if description:
136     EditElementAttribute(xmldoc, 'manifest', 'android:description',
137                          "@string/description")
138   HandlePermissions(permissions, xmldoc)
139   EditElementAttribute(xmldoc, 'application', 'android:label', name)
140   activity_name = package + '.' + sanitized_name + 'Activity'
141   EditElementAttribute(xmldoc, 'activity', 'android:name', activity_name)
142   EditElementAttribute(xmldoc, 'activity', 'android:label', name)
143   if orientation:
144     EditElementAttribute(xmldoc, 'activity', 'android:screenOrientation',
145                          orientation)
146   icon_name = CustomizeIcon(sanitized_name, app_root, icon, icon_dict)
147   if icon_name:
148     EditElementAttribute(xmldoc, 'application', 'android:icon',
149                          '@drawable/%s' % icon_name)
150
151   file_handle = open(os.path.join(sanitized_name, 'AndroidManifest.xml'), 'w')
152   xmldoc.writexml(file_handle, encoding='utf-8')
153   file_handle.close()
154
155
156 def ReplaceString(file_path, src, dest):
157   file_handle = open(file_path, 'r')
158   src_content = file_handle.read()
159   file_handle.close()
160   file_handle = open(file_path, 'w')
161   dest_content = src_content.replace(src, dest)
162   file_handle.write(dest_content)
163   file_handle.close()
164
165
166 def SetVariable(file_path, string_line, variable, value):
167   function_string = ('%sset%s(%s);\n' %
168                     ('        ', variable, value))
169   temp_file_path = file_path + '.backup'
170   file_handle = open(temp_file_path, 'w+')
171   for line in open(file_path):
172     file_handle.write(line)
173     if (line.find(string_line) >= 0):
174       file_handle.write(function_string)
175   file_handle.close()
176   shutil.move(temp_file_path, file_path)
177
178
179 def CustomizeJava(sanitized_name, package, app_url, app_local_path,
180                   enable_remote_debugging, display_as_fullscreen):
181   root_path =  os.path.join(sanitized_name, 'src',
182                             package.replace('.', os.path.sep))
183   dest_activity = os.path.join(root_path, sanitized_name + 'Activity.java')
184   ReplaceString(dest_activity, 'org.xwalk.app.template', package)
185   ReplaceString(dest_activity, 'AppTemplate', sanitized_name)
186   manifest_file = os.path.join(sanitized_name, 'assets/www', 'manifest.json')
187   if os.path.isfile(manifest_file):
188     ReplaceString(
189         dest_activity,
190         'loadAppFromUrl("file:///android_asset/www/index.html")',
191         'loadAppFromManifest("file:///android_asset/www/manifest.json")')
192   else:
193     if app_url:
194       if re.search(r'^http(|s)', app_url):
195         ReplaceString(dest_activity, 'file:///android_asset/www/index.html',
196                       app_url)
197     elif app_local_path:
198       if os.path.isfile(os.path.join(sanitized_name, 'assets/www',
199                                      app_local_path)):
200         ReplaceString(dest_activity, 'file:///android_asset/www/index.html',
201                       'app://' + package + '/' + app_local_path)
202       else:
203         print ('Please make sure that the relative path of entry file'
204                ' is correct.')
205         sys.exit(8)
206
207   if enable_remote_debugging:
208     SetVariable(dest_activity,
209                 'public void onCreate(Bundle savedInstanceState)',
210                 'RemoteDebugging', 'true')
211   if display_as_fullscreen:
212     SetVariable(dest_activity,
213                 'super.onCreate(savedInstanceState)',
214                 'IsFullscreen', 'true')
215
216
217 def CopyExtensionFile(extension_name, suffix, src_path, dest_path):
218   # Copy the file from src_path into dest_path.
219   dest_extension_path = os.path.join(dest_path, extension_name)
220   if os.path.exists(dest_extension_path):
221     # TODO: Refine it by renaming it internally.
222     print('Error: duplicated extension names "%s" are found. Please rename it.'
223           % extension_name)
224     sys.exit(9)
225   else:
226     os.mkdir(dest_extension_path)
227
228   file_name = extension_name + suffix
229   src_file = os.path.join(src_path, file_name)
230   dest_file = os.path.join(dest_extension_path, file_name)
231   if not os.path.isfile(src_file):
232     sys.exit(9)
233     print('Error: %s is not found in %s.' % (file_name, src_path))
234   else:
235     shutil.copyfile(src_file, dest_file)
236
237
238 def CustomizeExtensions(sanitized_name, name, extensions):
239   """Copy the files from external extensions and merge them into APK.
240
241   The directory of one external extension should be like:
242     myextension/
243       myextension.jar
244       myextension.js
245       myextension.json
246   That means the name of the internal files should be the same as the
247   directory name.
248   For .jar files, they'll be copied to xwalk-extensions/ and then
249   built into classes.dex in make_apk.py.
250   For .js files, they'll be copied into assets/xwalk-extensions/.
251   For .json files, the'll be merged into one file called
252   extensions-config.json and copied into assets/.
253   """
254   if not extensions:
255     return
256   apk_path = name
257   apk_assets_path = os.path.join(apk_path, 'assets')
258   extensions_string = 'xwalk-extensions'
259
260   # Set up the target directories and files.
261   dest_jar_path = os.path.join(apk_path, extensions_string)
262   os.mkdir(dest_jar_path)
263   dest_js_path = os.path.join(apk_assets_path, extensions_string)
264   os.mkdir(dest_js_path)
265   apk_extensions_json_path = os.path.join(apk_assets_path,
266                                           'extensions-config.json')
267
268   # Split the paths into a list.
269   extension_paths = extensions.split(os.pathsep)
270   extension_json_list = []
271   for source_path in extension_paths:
272     if not os.path.exists(source_path):
273       print('Error: can\'t find the extension directory \'%s\'.' % source_path)
274       sys.exit(9)
275     # Remove redundant separators to avoid empty basename.
276     source_path = os.path.normpath(source_path)
277     extension_name = os.path.basename(source_path)
278
279     # Copy .jar file into xwalk-extensions.
280     CopyExtensionFile(extension_name, '.jar', source_path, dest_jar_path)
281
282     # Copy .js file into assets/xwalk-extensions.
283     CopyExtensionFile(extension_name, '.js', source_path, dest_js_path)
284
285     # Merge .json file into assets/xwalk-extensions.
286     file_name = extension_name + '.json'
287     src_file = os.path.join(source_path, file_name)
288     if not os.path.isfile(src_file):
289       print('Error: %s is not found in %s.' % (file_name, source_path))
290       sys.exit(9)
291     else:
292       src_file_handle = file(src_file)
293       src_file_content = src_file_handle.read()
294       json_output = json.JSONDecoder().decode(src_file_content)
295       # Below 3 properties are used by runtime. See extension manager.
296       # And 'permissions' will be merged.
297       if ((not 'name' in json_output) or (not 'class' in json_output)
298           or (not 'jsapi' in json_output)):
299         print ('Error: properties \'name\', \'class\' and \'jsapi\' in a json '
300                'file are mandatory.')
301         sys.exit(9)
302       # Reset the path for JavaScript.
303       js_path_prefix = extensions_string + '/' + extension_name + '/'
304       json_output['jsapi'] = js_path_prefix + json_output['jsapi']
305       extension_json_list.append(json_output)
306       # Merge the permissions of extensions into AndroidManifest.xml.
307       manifest_path = os.path.join(sanitized_name, 'AndroidManifest.xml')
308       xmldoc = minidom.parse(manifest_path)
309       if ('permissions' in json_output):
310         # Get used permission list to avoid repetition as "--permissions"
311         # option can also be used to declare used permissions.
312         existingList = []
313         usedPermissions = xmldoc.getElementsByTagName("uses-permission")
314         for used in usedPermissions:
315           existingList.append(used.getAttribute("android:name"))
316
317         # Add the permissions to manifest file if not used yet.
318         for p in json_output['permissions']:
319           if p in existingList:
320             continue
321           AddElementAttribute(xmldoc, 'uses-permission', 'android:name', p)
322           existingList.append(p)
323
324         # Write to the manifest file to save the update.
325         file_handle = open(manifest_path, 'w')
326         xmldoc.writexml(file_handle)
327         file_handle.close()
328
329   # Write configuration of extensions into the target extensions-config.json.
330   if extension_json_list:
331     extensions_string = json.JSONEncoder().encode(extension_json_list)
332     extension_json_file = open(apk_extensions_json_path, 'w')
333     extension_json_file.write(extensions_string)
334     extension_json_file.close()
335
336
337 def GenerateCommandLineFile(sanitized_name, xwalk_command_line):
338   if xwalk_command_line == '':
339     return
340   assets_path = os.path.join(sanitized_name, 'assets')
341   file_path = os.path.join(assets_path, 'xwalk-command-line')
342   command_line_file = open(file_path, 'w')
343   command_line_file.write('xwalk ' + xwalk_command_line)
344
345
346 def CustomizeIconByDict(sanitized_name, app_root, icon_dict):
347   icon_name = None
348   drawable_dict = {'ldpi':[1, 37], 'mdpi':[37, 72], 'hdpi':[72, 96],
349                    'xhdpi':[96, 120], 'xxhdpi':[120, 144], 'xxxhdpi':[144, 168]}
350   if not icon_dict:
351     return icon_name
352
353   try:
354     icon_dict = dict((int(k), v) for k, v in icon_dict.items())
355   except ValueError:
356     print('The key of icon in the manifest file should be a number.')
357
358   if len(icon_dict) > 0:
359     icon_list = sorted(icon_dict.iteritems(), key = lambda d:d[0])
360     for kd, vd in drawable_dict.iteritems():
361       for item in icon_list:
362         if item[0] >= vd[0] and item[0] < vd[1]:
363           drawable_path = os.path.join(sanitized_name, 'res', 'drawable-' + kd)
364           if not os.path.exists(drawable_path):
365             os.makedirs(drawable_path)
366           icon = os.path.join(app_root, item[1])
367           if icon and os.path.isfile(icon):
368             icon_name = os.path.basename(icon)
369             icon_suffix = icon_name.split('.')[-1]
370             shutil.copyfile(icon, os.path.join(drawable_path,
371                                                'icon.' + icon_suffix))
372             icon_name = 'icon'
373           elif icon and (not os.path.isfile(icon)):
374             print ('Error: Please make sure \"' + icon + '\" does exist!')
375             sys.exit(6)
376           break
377   return icon_name
378
379
380 def CustomizeIconByOption(sanitized_name, icon):
381   if os.path.isfile(icon):
382     drawable_path = os.path.join(sanitized_name, 'res', 'drawable')
383     if not os.path.exists(drawable_path):
384       os.makedirs(drawable_path)
385     icon_file = os.path.basename(icon)
386     icon_file = ReplaceInvalidChars(icon_file)
387     shutil.copyfile(icon, os.path.join(drawable_path, icon_file))
388     icon_name = os.path.splitext(icon_file)[0]
389     return icon_name
390   else:
391     print ('Error: Please make sure \"' + icon + '\" is a file!')
392     sys.exit(6)
393
394
395 def CustomizeIcon(sanitized_name, app_root, icon, icon_dict):
396   icon_name = None
397   if icon:
398     icon_name = CustomizeIconByOption(sanitized_name, icon)
399   else:
400     icon_name = CustomizeIconByDict(sanitized_name, app_root, icon_dict)
401   return icon_name
402
403
404 def CustomizeAll(app_versionCode, description, icon_dict, permissions, app_url,
405                  app_root, app_local_path, enable_remote_debugging,
406                  display_as_fullscreen, extensions, launch_screen_img,
407                  icon, package='org.xwalk.app.template', name='AppTemplate',
408                  app_version='1.0.0', orientation='unspecified',
409                  xwalk_command_line=''):
410   sanitized_name = ReplaceInvalidChars(name, 'apkname')
411   try:
412     Prepare(sanitized_name, package, app_root)
413     CustomizeXML(sanitized_name, package, app_versionCode, app_version,
414                  description, name, orientation, icon_dict,
415                  display_as_fullscreen, icon, launch_screen_img, permissions,
416                  app_root)
417     CustomizeJava(sanitized_name, package, app_url, app_local_path,
418                   enable_remote_debugging, display_as_fullscreen)
419     CustomizeExtensions(sanitized_name, name, extensions)
420     GenerateCommandLineFile(sanitized_name, xwalk_command_line)
421   except SystemExit as ec:
422     print('Exiting with error code: %d' % ec.code)
423     sys.exit(ec.code)
424
425
426 def main():
427   parser = optparse.OptionParser()
428   info = ('The package name. Such as: '
429           '--package=com.example.YourPackage')
430   parser.add_option('--package', help=info)
431   info = ('The apk name. Such as: --name="Your Application Name"')
432   parser.add_option('--name', help=info)
433   info = ('The version of the app. Such as: --app-version=TheVersionNumber')
434   parser.add_option('--app-version', help=info)
435   info = ('The versionCode of the app. Such as: --app-versionCode=24')
436   parser.add_option('--app-versionCode', type='int', help=info)
437   info = ('The application description. Such as:'
438           '--description=YourApplicationdDescription')
439   parser.add_option('--description', help=info)
440   info = ('The permission list. Such as: --permissions="geolocation"'
441           'For more permissions, such as:'
442           '--permissions="geolocation:permission2"')
443   parser.add_option('--permissions', help=info)
444   info = ('The url of application. '
445           'This flag allows to package website as apk. Such as: '
446           '--app-url=http://www.intel.com')
447   parser.add_option('--app-url', help=info)
448   info = ('The root path of the web app. '
449           'This flag allows to package local web app as apk. Such as: '
450           '--app-root=/root/path/of/the/web/app')
451   parser.add_option('--app-root', help=info)
452   info = ('The reletive path of entry file based on |app_root|. '
453           'This flag should work with "--app-root" together. '
454           'Such as: --app-local-path=/reletive/path/of/entry/file')
455   parser.add_option('--app-local-path', help=info)
456   parser.add_option('--enable-remote-debugging', action='store_true',
457                     dest='enable_remote_debugging', default=False,
458                     help = 'Enable remote debugging.')
459   parser.add_option('-f', '--fullscreen', action='store_true',
460                     dest='fullscreen', default=False,
461                     help='Make application fullscreen.')
462   info = ('The path list for external extensions separated by os separator.'
463           'On Linux and Mac, the separator is ":". On Windows, it is ";".'
464           'Such as: --extensions="/path/to/extension1:/path/to/extension2"')
465   parser.add_option('--extensions', help=info)
466   info = ('The orientation of the web app\'s display on the device. '
467           'Such as: --orientation=landscape. The default value is "unspecified"'
468           'The value options are the same as those on the Android: '
469           'http://developer.android.com/guide/topics/manifest/'
470           'activity-element.html#screen')
471   parser.add_option('--orientation', help=info)
472   parser.add_option('--launch-screen-img',
473                     help='The fallback image for launch_screen')
474   info = ('Use command lines.'
475           'Crosswalk is powered by Chromium and supports Chromium command line.'
476           'For example, '
477           '--xwalk-command-line=\'--chromium-command-1 --xwalk-command-2\'')
478   parser.add_option('--xwalk-command-line', default='', help=info)
479   options, _ = parser.parse_args()
480   try:
481     icon_dict = {144: 'icons/icon_144.png',
482                  72: 'icons/icon_72.png',
483                  96: 'icons/icon_96.png',
484                  48: 'icons/icon_48.png'}
485     if options.name == None:
486       options.name = 'Example'
487     if options.app_root == None:
488       options.app_root = os.path.join('test_data', 'manifest')
489     if options.package == None:
490       options.package = 'org.xwalk.app.template'
491     if options.orientation == None:
492       options.orientation = 'unspecified'
493     if options.app_version == None:
494       options.app_version = '1.0.0'
495     icon = os.path.join('test_data', 'manifest', 'icons', 'icon_96.png')
496     CustomizeAll(options.app_versionCode, options.description, icon_dict,
497                  options.permissions, options.app_url, options.app_root,
498                  options.app_local_path, options.enable_remote_debugging,
499                  options.fullscreen, options.extensions,
500                  options.launch_screen_img, icon, options.package, options.name,
501                  options.app_version, options.orientation,
502                  options.xwalk_command_line)
503   except SystemExit as ec:
504     print('Exiting with error code: %d' % ec.code)
505     return ec.code
506   return 0
507
508
509 if __name__ == '__main__':
510   sys.exit(main())