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, 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.')
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 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!')
160 file_handle = open(os.path.join(sanitized_name, 'AndroidManifest.xml'), 'w')
161 xmldoc.writexml(file_handle, encoding='utf-8')
165 def ReplaceString(file_path, src, dest):
166 file_handle = open(file_path, 'r')
167 src_content = file_handle.read()
169 file_handle = open(file_path, 'w')
170 dest_content = src_content.replace(src, dest)
171 file_handle.write(dest_content)
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)
185 shutil.move(temp_file_path, file_path)
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):
199 'loadAppFromUrl("file:///android_asset/www/index.html")',
200 'loadAppFromManifest("file:///android_asset/www/manifest.json")')
203 if re.search(r'^http(|s)', app_url):
204 ReplaceString(dest_activity, 'file:///android_asset/www/index.html',
207 if os.path.isfile(os.path.join(sanitized_name, 'assets/www',
209 ReplaceString(dest_activity, 'file:///android_asset/www/index.html',
210 'app://' + package + '/' + app_local_path)
212 print ('Please make sure that the relative path of entry file'
216 if enable_remote_debugging:
217 SetVariable(dest_activity, 'RemoteDebugging', 'true')
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.'
229 os.mkdir(dest_extension_path)
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):
236 print('Error: %s is not found in %s.' % (file_name, src_path))
238 shutil.copyfile(src_file, dest_file)
241 def CustomizeExtensions(sanitized_name, name, extensions):
242 """Copy the files from external extensions and merge them into APK.
244 The directory of one external extension should be like:
249 That means the name of the internal files should be the same as the
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/.
260 apk_assets_path = os.path.join(apk_path, 'assets')
261 extensions_string = 'xwalk-extensions'
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')
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)
278 # Remove redundant separators to avoid empty basename.
279 source_path = os.path.normpath(source_path)
280 extension_name = os.path.basename(source_path)
282 # Copy .jar file into xwalk-extensions.
283 CopyExtensionFile(extension_name, '.jar', source_path, dest_jar_path)
285 # Copy .js file into assets/xwalk-extensions.
286 CopyExtensionFile(extension_name, '.js', source_path, dest_js_path)
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))
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.')
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.
316 usedPermissions = xmldoc.getElementsByTagName("uses-permission")
317 for used in usedPermissions:
318 existingList.append(used.getAttribute("android:name"))
320 # Add the permissions to manifest file if not used yet.
321 for p in json_output['permissions']:
322 if p in existingList:
324 AddElementAttribute(xmldoc, 'uses-permission', 'android:name', p)
325 existingList.append(p)
327 # Write to the manifest file to save the update.
328 file_handle = open(manifest_path, 'w')
329 xmldoc.writexml(file_handle)
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()
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')
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)
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()
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)
423 if __name__ == '__main__':