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