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