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