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.
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
21 def ReplaceInvalidChars(value, mode='default'):
22 """ Replace the invalid chars with '_' for input string.
24 value: the original string.
25 mode: the target usage mode of original string.
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,'_')
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.')
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))
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)
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.')
71 xmldoc = minidom.parse(strings_path)
72 AddElementAttributeAndText(xmldoc, 'string', 'name', 'description',
74 strings_file = open(strings_path, 'w')
75 xmldoc.writexml(strings_file, encoding='utf-8')
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.')
85 xmldoc = minidom.parse(theme_path)
87 EditElementValueByNodeName(xmldoc, 'item',
88 'android:windowFullscreen', 'true')
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.
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)
109 print('Error: Please make sure \"' + default_image + '\" exists!')
111 theme_file = open(theme_path, 'w')
112 xmldoc.writexml(theme_file, encoding='utf-8')
116 def CustomizeXML(sanitized_name, package, app_versionCode, app_version,
117 description, name, orientation, icon_dict, fullscreen,
118 icon, launch_screen_img, permissions, app_root):
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.')
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)
130 EditElementAttribute(xmldoc, 'manifest', 'android:versionCode',
131 str(app_versionCode))
133 EditElementAttribute(xmldoc, 'manifest', 'android:versionName',
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)
144 EditElementAttribute(xmldoc, 'activity', 'android:screenOrientation',
146 icon_name = CustomizeIcon(sanitized_name, app_root, icon, icon_dict)
148 EditElementAttribute(xmldoc, 'application', 'android:icon',
149 '@drawable/%s' % icon_name)
151 file_handle = open(os.path.join(sanitized_name, 'AndroidManifest.xml'), 'w')
152 xmldoc.writexml(file_handle, encoding='utf-8')
156 def ReplaceString(file_path, src, dest):
157 file_handle = open(file_path, 'r')
158 src_content = file_handle.read()
160 file_handle = open(file_path, 'w')
161 dest_content = src_content.replace(src, dest)
162 file_handle.write(dest_content)
166 def SetVariable(file_path, string_line, variable, value):
167 function_string = ('%sset%s(%s);\n' %
168 (' ', variable, value))
169 temp_file_path = file_path + '.backup'
170 file_handle = open(temp_file_path, 'w+')
171 for line in open(file_path):
172 file_handle.write(line)
173 if (line.find(string_line) >= 0):
174 file_handle.write(function_string)
176 shutil.move(temp_file_path, file_path)
179 def CustomizeJava(sanitized_name, package, app_url, app_local_path,
180 enable_remote_debugging, display_as_fullscreen):
181 root_path = os.path.join(sanitized_name, 'src',
182 package.replace('.', os.path.sep))
183 dest_activity = os.path.join(root_path, sanitized_name + 'Activity.java')
184 ReplaceString(dest_activity, 'org.xwalk.app.template', package)
185 ReplaceString(dest_activity, 'AppTemplate', sanitized_name)
186 manifest_file = os.path.join(sanitized_name, 'assets/www', 'manifest.json')
187 if os.path.isfile(manifest_file):
190 'loadAppFromUrl("file:///android_asset/www/index.html")',
191 'loadAppFromManifest("file:///android_asset/www/manifest.json")')
194 if re.search(r'^http(|s)', app_url):
195 ReplaceString(dest_activity, 'file:///android_asset/www/index.html',
198 if os.path.isfile(os.path.join(sanitized_name, 'assets/www',
200 ReplaceString(dest_activity, 'file:///android_asset/www/index.html',
201 'app://' + package + '/' + app_local_path)
203 print ('Please make sure that the relative path of entry file'
207 if enable_remote_debugging:
208 SetVariable(dest_activity,
209 'public void onCreate(Bundle savedInstanceState)',
210 'RemoteDebugging', 'true')
211 if display_as_fullscreen:
212 SetVariable(dest_activity,
213 'super.onCreate(savedInstanceState)',
214 'IsFullscreen', 'true')
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.'
226 os.mkdir(dest_extension_path)
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):
233 print('Error: %s is not found in %s.' % (file_name, src_path))
235 shutil.copyfile(src_file, dest_file)
238 def CustomizeExtensions(sanitized_name, name, extensions):
239 """Copy the files from external extensions and merge them into APK.
241 The directory of one external extension should be like:
246 That means the name of the internal files should be the same as the
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/.
257 apk_assets_path = os.path.join(apk_path, 'assets')
258 extensions_string = 'xwalk-extensions'
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')
268 # Split the paths into a list.
269 extension_paths = 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)
275 # Remove redundant separators to avoid empty basename.
276 source_path = os.path.normpath(source_path)
277 extension_name = os.path.basename(source_path)
279 # Copy .jar file into xwalk-extensions.
280 CopyExtensionFile(extension_name, '.jar', source_path, dest_jar_path)
282 # Copy .js file into assets/xwalk-extensions.
283 CopyExtensionFile(extension_name, '.js', source_path, dest_js_path)
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))
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.')
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.
313 usedPermissions = xmldoc.getElementsByTagName("uses-permission")
314 for used in usedPermissions:
315 existingList.append(used.getAttribute("android:name"))
317 # Add the permissions to manifest file if not used yet.
318 for p in json_output['permissions']:
319 if p in existingList:
321 AddElementAttribute(xmldoc, 'uses-permission', 'android:name', p)
322 existingList.append(p)
324 # Write to the manifest file to save the update.
325 file_handle = open(manifest_path, 'w')
326 xmldoc.writexml(file_handle)
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()
337 def GenerateCommandLineFile(sanitized_name, xwalk_command_line):
338 if xwalk_command_line == '':
340 assets_path = os.path.join(sanitized_name, 'assets')
341 file_path = os.path.join(assets_path, 'xwalk-command-line')
342 command_line_file = open(file_path, 'w')
343 command_line_file.write('xwalk ' + xwalk_command_line)
346 def CustomizeIconByDict(sanitized_name, app_root, icon_dict):
348 drawable_dict = {'ldpi':[1, 37], 'mdpi':[37, 72], 'hdpi':[72, 96],
349 'xhdpi':[96, 120], 'xxhdpi':[120, 144], 'xxxhdpi':[144, 168]}
354 icon_dict = dict((int(k), v) for k, v in icon_dict.items())
356 print('The key of icon in the manifest file should be a number.')
358 if len(icon_dict) > 0:
359 icon_list = sorted(icon_dict.iteritems(), key = lambda d:d[0])
360 for kd, vd in drawable_dict.iteritems():
361 for item in icon_list:
362 if item[0] >= vd[0] and item[0] < vd[1]:
363 drawable_path = os.path.join(sanitized_name, 'res', 'drawable-' + kd)
364 if not os.path.exists(drawable_path):
365 os.makedirs(drawable_path)
366 icon = os.path.join(app_root, item[1])
367 if icon and os.path.isfile(icon):
368 icon_name = os.path.basename(icon)
369 icon_suffix = icon_name.split('.')[-1]
370 shutil.copyfile(icon, os.path.join(drawable_path,
371 'icon.' + icon_suffix))
373 elif icon and (not os.path.isfile(icon)):
374 print ('Error: Please make sure \"' + icon + '\" does exist!')
380 def CustomizeIconByOption(sanitized_name, icon):
381 if os.path.isfile(icon):
382 drawable_path = os.path.join(sanitized_name, 'res', 'drawable')
383 if not os.path.exists(drawable_path):
384 os.makedirs(drawable_path)
385 icon_file = os.path.basename(icon)
386 icon_file = ReplaceInvalidChars(icon_file)
387 shutil.copyfile(icon, os.path.join(drawable_path, icon_file))
388 icon_name = os.path.splitext(icon_file)[0]
391 print ('Error: Please make sure \"' + icon + '\" is a file!')
395 def CustomizeIcon(sanitized_name, app_root, icon, icon_dict):
398 icon_name = CustomizeIconByOption(sanitized_name, icon)
400 icon_name = CustomizeIconByDict(sanitized_name, app_root, icon_dict)
404 def CustomizeAll(app_versionCode, description, icon_dict, permissions, app_url,
405 app_root, app_local_path, enable_remote_debugging,
406 display_as_fullscreen, extensions, launch_screen_img,
407 icon, package='org.xwalk.app.template', name='AppTemplate',
408 app_version='1.0.0', orientation='unspecified',
409 xwalk_command_line=''):
410 sanitized_name = ReplaceInvalidChars(name, 'apkname')
412 Prepare(sanitized_name, package, app_root)
413 CustomizeXML(sanitized_name, package, app_versionCode, app_version,
414 description, name, orientation, icon_dict,
415 display_as_fullscreen, icon, launch_screen_img, permissions,
417 CustomizeJava(sanitized_name, package, app_url, app_local_path,
418 enable_remote_debugging, display_as_fullscreen)
419 CustomizeExtensions(sanitized_name, name, extensions)
420 GenerateCommandLineFile(sanitized_name, xwalk_command_line)
421 except SystemExit as ec:
422 print('Exiting with error code: %d' % ec.code)
427 parser = optparse.OptionParser()
428 info = ('The package name. Such as: '
429 '--package=com.example.YourPackage')
430 parser.add_option('--package', help=info)
431 info = ('The apk name. Such as: --name="Your Application Name"')
432 parser.add_option('--name', help=info)
433 info = ('The version of the app. Such as: --app-version=TheVersionNumber')
434 parser.add_option('--app-version', help=info)
435 info = ('The versionCode of the app. Such as: --app-versionCode=24')
436 parser.add_option('--app-versionCode', type='int', help=info)
437 info = ('The application description. Such as:'
438 '--description=YourApplicationdDescription')
439 parser.add_option('--description', help=info)
440 info = ('The permission list. Such as: --permissions="geolocation"'
441 'For more permissions, such as:'
442 '--permissions="geolocation:permission2"')
443 parser.add_option('--permissions', help=info)
444 info = ('The url of application. '
445 'This flag allows to package website as apk. Such as: '
446 '--app-url=http://www.intel.com')
447 parser.add_option('--app-url', help=info)
448 info = ('The root path of the web app. '
449 'This flag allows to package local web app as apk. Such as: '
450 '--app-root=/root/path/of/the/web/app')
451 parser.add_option('--app-root', help=info)
452 info = ('The reletive path of entry file based on |app_root|. '
453 'This flag should work with "--app-root" together. '
454 'Such as: --app-local-path=/reletive/path/of/entry/file')
455 parser.add_option('--app-local-path', help=info)
456 parser.add_option('--enable-remote-debugging', action='store_true',
457 dest='enable_remote_debugging', default=False,
458 help = 'Enable remote debugging.')
459 parser.add_option('-f', '--fullscreen', action='store_true',
460 dest='fullscreen', default=False,
461 help='Make application fullscreen.')
462 info = ('The path list for external extensions separated by os separator.'
463 'On Linux and Mac, the separator is ":". On Windows, it is ";".'
464 'Such as: --extensions="/path/to/extension1:/path/to/extension2"')
465 parser.add_option('--extensions', help=info)
466 info = ('The orientation of the web app\'s display on the device. '
467 'Such as: --orientation=landscape. The default value is "unspecified"'
468 'The value options are the same as those on the Android: '
469 'http://developer.android.com/guide/topics/manifest/'
470 'activity-element.html#screen')
471 parser.add_option('--orientation', help=info)
472 parser.add_option('--launch-screen-img',
473 help='The fallback image for launch_screen')
474 info = ('Use command lines.'
475 'Crosswalk is powered by Chromium and supports Chromium command line.'
477 '--xwalk-command-line=\'--chromium-command-1 --xwalk-command-2\'')
478 parser.add_option('--xwalk-command-line', default='', help=info)
479 options, _ = parser.parse_args()
481 icon_dict = {144: 'icons/icon_144.png',
482 72: 'icons/icon_72.png',
483 96: 'icons/icon_96.png',
484 48: 'icons/icon_48.png'}
485 if options.name == None:
486 options.name = 'Example'
487 if options.app_root == None:
488 options.app_root = os.path.join('test_data', 'manifest')
489 if options.package == None:
490 options.package = 'org.xwalk.app.template'
491 if options.orientation == None:
492 options.orientation = 'unspecified'
493 if options.app_version == None:
494 options.app_version = '1.0.0'
495 icon = os.path.join('test_data', 'manifest', 'icons', 'icon_96.png')
496 CustomizeAll(options.app_versionCode, options.description, icon_dict,
497 options.permissions, options.app_url, options.app_root,
498 options.app_local_path, options.enable_remote_debugging,
499 options.fullscreen, options.extensions,
500 options.launch_screen_img, icon, options.package, options.name,
501 options.app_version, options.orientation,
502 options.xwalk_command_line)
503 except SystemExit as ec:
504 print('Exiting with error code: %d' % ec.code)
509 if __name__ == '__main__':