c8eac2e3b607e7c74b3e1c7926822cab2922c543
[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, fullscreen,
118                  launch_screen_img, permissions):
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   if icon and os.path.isfile(icon):
147     drawable_path = os.path.join(sanitized_name, 'res', 'drawable')
148     if not os.path.exists(drawable_path):
149       os.makedirs(drawable_path)
150     icon_file = os.path.basename(icon)
151     icon_file = ReplaceInvalidChars(icon_file)
152     shutil.copyfile(icon, os.path.join(drawable_path, icon_file))
153     icon_name = os.path.splitext(icon_file)[0]
154     EditElementAttribute(xmldoc, 'application',
155                          'android:icon', '@drawable/%s' % icon_name)
156   elif icon and (not os.path.isfile(icon)):
157     print ('Please make sure the icon file does exist!')
158     sys.exit(6)
159
160   file_handle = open(os.path.join(sanitized_name, 'AndroidManifest.xml'), 'w')
161   xmldoc.writexml(file_handle, encoding='utf-8')
162   file_handle.close()
163
164
165 def ReplaceString(file_path, src, dest):
166   file_handle = open(file_path, 'r')
167   src_content = file_handle.read()
168   file_handle.close()
169   file_handle = open(file_path, 'w')
170   dest_content = src_content.replace(src, dest)
171   file_handle.write(dest_content)
172   file_handle.close()
173
174
175 def SetVariable(file_path, variable, value):
176   function_string = ('%sset%s(%s);\n' %
177                     ('        ', variable, value))
178   temp_file_path = file_path + '.backup'
179   file_handle = open(temp_file_path, 'w+')
180   for line in open(file_path):
181     file_handle.write(line)
182     if (line.find('public void onCreate(Bundle savedInstanceState)') >= 0):
183       file_handle.write(function_string)
184   file_handle.close()
185   shutil.move(temp_file_path, file_path)
186
187
188 def CustomizeJava(sanitized_name, package, app_url, app_local_path,
189                   enable_remote_debugging):
190   root_path =  os.path.join(sanitized_name, 'src',
191                             package.replace('.', os.path.sep))
192   dest_activity = os.path.join(root_path, sanitized_name + 'Activity.java')
193   ReplaceString(dest_activity, 'org.xwalk.app.template', package)
194   ReplaceString(dest_activity, 'AppTemplate', sanitized_name)
195   manifest_file = os.path.join(sanitized_name, 'assets/www', 'manifest.json')
196   if os.path.isfile(manifest_file):
197     ReplaceString(
198         dest_activity,
199         'loadAppFromUrl("file:///android_asset/www/index.html")',
200         'loadAppFromManifest("file:///android_asset/www/manifest.json")')
201   else:
202     if app_url:
203       if re.search(r'^http(|s)', app_url):
204         ReplaceString(dest_activity, 'file:///android_asset/www/index.html',
205                       app_url)
206     elif app_local_path:
207       if os.path.isfile(os.path.join(sanitized_name, 'assets/www',
208                                      app_local_path)):
209         ReplaceString(dest_activity, 'file:///android_asset/www/index.html',
210                       'app://' + package + '/' + app_local_path)
211       else:
212         print ('Please make sure that the relative path of entry file'
213                ' is correct.')
214         sys.exit(8)
215
216   if enable_remote_debugging:
217     SetVariable(dest_activity, 'RemoteDebugging', 'true')
218
219
220 def CopyExtensionFile(extension_name, suffix, src_path, dest_path):
221   # Copy the file from src_path into dest_path.
222   dest_extension_path = os.path.join(dest_path, extension_name)
223   if os.path.exists(dest_extension_path):
224     # TODO: Refine it by renaming it internally.
225     print('Error: duplicated extension names "%s" are found. Please rename it.'
226           % extension_name)
227     sys.exit(9)
228   else:
229     os.mkdir(dest_extension_path)
230
231   file_name = extension_name + suffix
232   src_file = os.path.join(src_path, file_name)
233   dest_file = os.path.join(dest_extension_path, file_name)
234   if not os.path.isfile(src_file):
235     sys.exit(9)
236     print('Error: %s is not found in %s.' % (file_name, src_path))
237   else:
238     shutil.copyfile(src_file, dest_file)
239
240
241 def CustomizeExtensions(sanitized_name, name, extensions):
242   """Copy the files from external extensions and merge them into APK.
243
244   The directory of one external extension should be like:
245     myextension/
246       myextension.jar
247       myextension.js
248       myextension.json
249   That means the name of the internal files should be the same as the
250   directory name.
251   For .jar files, they'll be copied to xwalk-extensions/ and then
252   built into classes.dex in make_apk.py.
253   For .js files, they'll be copied into assets/xwalk-extensions/.
254   For .json files, the'll be merged into one file called
255   extensions-config.json and copied into assets/.
256   """
257   if not extensions:
258     return
259   apk_path = name
260   apk_assets_path = os.path.join(apk_path, 'assets')
261   extensions_string = 'xwalk-extensions'
262
263   # Set up the target directories and files.
264   dest_jar_path = os.path.join(apk_path, extensions_string)
265   os.mkdir(dest_jar_path)
266   dest_js_path = os.path.join(apk_assets_path, extensions_string)
267   os.mkdir(dest_js_path)
268   apk_extensions_json_path = os.path.join(apk_assets_path,
269                                           'extensions-config.json')
270
271   # Split the paths into a list.
272   extension_paths = extensions.split(os.pathsep)
273   extension_json_list = []
274   for source_path in extension_paths:
275     if not os.path.exists(source_path):
276       print('Error: can\'t find the extension directory \'%s\'.' % source_path)
277       sys.exit(9)
278     # Remove redundant separators to avoid empty basename.
279     source_path = os.path.normpath(source_path)
280     extension_name = os.path.basename(source_path)
281
282     # Copy .jar file into xwalk-extensions.
283     CopyExtensionFile(extension_name, '.jar', source_path, dest_jar_path)
284
285     # Copy .js file into assets/xwalk-extensions.
286     CopyExtensionFile(extension_name, '.js', source_path, dest_js_path)
287
288     # Merge .json file into assets/xwalk-extensions.
289     file_name = extension_name + '.json'
290     src_file = os.path.join(source_path, file_name)
291     if not os.path.isfile(src_file):
292       print('Error: %s is not found in %s.' % (file_name, source_path))
293       sys.exit(9)
294     else:
295       src_file_handle = file(src_file)
296       src_file_content = src_file_handle.read()
297       json_output = json.JSONDecoder().decode(src_file_content)
298       # Below 3 properties are used by runtime. See extension manager.
299       # And 'permissions' will be merged.
300       if ((not 'name' in json_output) or (not 'class' in json_output)
301           or (not 'jsapi' in json_output)):
302         print ('Error: properties \'name\', \'class\' and \'jsapi\' in a json '
303                'file are mandatory.')
304         sys.exit(9)
305       # Reset the path for JavaScript.
306       js_path_prefix = extensions_string + '/' + extension_name + '/'
307       json_output['jsapi'] = js_path_prefix + json_output['jsapi']
308       extension_json_list.append(json_output)
309       # Merge the permissions of extensions into AndroidManifest.xml.
310       manifest_path = os.path.join(sanitized_name, 'AndroidManifest.xml')
311       xmldoc = minidom.parse(manifest_path)
312       if ('permissions' in json_output):
313         # Get used permission list to avoid repetition as "--permissions"
314         # option can also be used to declare used permissions.
315         existingList = []
316         usedPermissions = xmldoc.getElementsByTagName("uses-permission")
317         for used in usedPermissions:
318           existingList.append(used.getAttribute("android:name"))
319
320         # Add the permissions to manifest file if not used yet.
321         for p in json_output['permissions']:
322           if p in existingList:
323             continue
324           AddElementAttribute(xmldoc, 'uses-permission', 'android:name', p)
325           existingList.append(p)
326
327         # Write to the manifest file to save the update.
328         file_handle = open(manifest_path, 'w')
329         xmldoc.writexml(file_handle)
330         file_handle.close()
331
332   # Write configuration of extensions into the target extensions-config.json.
333   if extension_json_list:
334     extensions_string = json.JSONEncoder().encode(extension_json_list)
335     extension_json_file = open(apk_extensions_json_path, 'w')
336     extension_json_file.write(extensions_string)
337     extension_json_file.close()
338
339
340 def CustomizeAll(app_versionCode, description, icon, permissions, app_url,
341                  app_root, app_local_path, enable_remote_debugging,
342                  fullscreen_flag, extensions, launch_screen_img,
343                  package='org.xwalk.app.template', name='AppTemplate',
344                  app_version='1.0.0', orientation='unspecified'):
345   sanitized_name = ReplaceInvalidChars(name, 'apkname')
346   try:
347     Prepare(sanitized_name, package, app_root)
348     CustomizeXML(sanitized_name, package, app_versionCode, app_version,
349                  description, name, orientation, icon, fullscreen_flag,
350                  launch_screen_img, permissions)
351     CustomizeJava(sanitized_name, package, app_url, app_local_path,
352                   enable_remote_debugging)
353     CustomizeExtensions(sanitized_name, name, extensions)
354   except SystemExit as ec:
355     print('Exiting with error code: %d' % ec.code)
356     sys.exit(ec.code)
357
358
359 def main():
360   parser = optparse.OptionParser()
361   info = ('The package name. Such as: '
362           '--package=com.example.YourPackage')
363   parser.add_option('--package', help=info)
364   info = ('The apk name. Such as: --name="Your Application Name"')
365   parser.add_option('--name', help=info)
366   info = ('The version of the app. Such as: --app-version=TheVersionNumber')
367   parser.add_option('--app-version', help=info)
368   info = ('The versionCode of the app. Such as: --app-versionCode=24')
369   parser.add_option('--app-versionCode', type='int', help=info)
370   info = ('The application description. Such as:'
371           '--description=YourApplicationdDescription')
372   parser.add_option('--description', help=info)
373   info = ('The path of icon. Such as: --icon=/path/to/your/customized/icon')
374   parser.add_option('--icon', help=info)
375   info = ('The permission list. Such as: --permissions="geolocation"'
376           'For more permissions, such as:'
377           '--permissions="geolocation:permission2"')
378   parser.add_option('--permissions', help=info)
379   info = ('The url of application. '
380           'This flag allows to package website as apk. Such as: '
381           '--app-url=http://www.intel.com')
382   parser.add_option('--app-url', help=info)
383   info = ('The root path of the web app. '
384           'This flag allows to package local web app as apk. Such as: '
385           '--app-root=/root/path/of/the/web/app')
386   parser.add_option('--app-root', help=info)
387   info = ('The reletive path of entry file based on |app_root|. '
388           'This flag should work with "--app-root" together. '
389           'Such as: --app-local-path=/reletive/path/of/entry/file')
390   parser.add_option('--app-local-path', help=info)
391   parser.add_option('--enable-remote-debugging', action='store_true',
392                     dest='enable_remote_debugging', default=False,
393                     help = 'Enable remote debugging.')
394   parser.add_option('-f', '--fullscreen', action='store_true',
395                     dest='fullscreen', default=False,
396                     help='Make application fullscreen.')
397   info = ('The path list for external extensions separated by os separator.'
398           'On Linux and Mac, the separator is ":". On Windows, it is ";".'
399           'Such as: --extensions="/path/to/extension1:/path/to/extension2"')
400   parser.add_option('--extensions', help=info)
401   info = ('The orientation of the web app\'s display on the device. '
402           'Such as: --orientation=landscape. The default value is "unspecified"'
403           'The value options are the same as those on the Android: '
404           'http://developer.android.com/guide/topics/manifest/'
405           'activity-element.html#screen')
406   parser.add_option('--orientation', help=info)
407   parser.add_option('--launch-screen-img',
408                     help='The fallback image for launch_screen')
409   options, _ = parser.parse_args()
410   try:
411     CustomizeAll(options.app_versionCode, options.description, options.icon,
412                  options.permissions, options.app_url, options.app_root,
413                  options.app_local_path, options.enable_remote_debugging,
414                  options.fullscreen, options.extensions,
415                  options.launch_screen_img, options.package, options.name,
416                  options.app_version, options.orientation)
417   except SystemExit as ec:
418     print('Exiting with error code: %d' % ec.code)
419     return ec.code
420   return 0
421
422
423 if __name__ == '__main__':
424   sys.exit(main())