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