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(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.')
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))
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)
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.')
70 if options.description:
71 xmldoc = minidom.parse(strings_path)
72 AddElementAttributeAndText(xmldoc, 'string', 'name', 'description',
74 strings_file = open(strings_path, 'w')
75 xmldoc.writexml(strings_file)
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.')
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.
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, 'wb')
112 xmldoc.writexml(theme_file)
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.')
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',
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',
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!')
158 file_handle = open(os.path.join(sanitized_name, 'AndroidManifest.xml'), 'w')
159 xmldoc.writexml(file_handle)
163 def ReplaceString(file_path, src, dest):
164 file_handle = open(file_path, 'r')
165 src_content = file_handle.read()
167 file_handle = open(file_path, 'w')
168 dest_content = src_content.replace(src, dest)
169 file_handle.write(dest_content)
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)
183 shutil.move(temp_file_path, file_path)
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):
196 'loadAppFromUrl("file:///android_asset/www/index.html")',
197 'loadAppFromManifest("file:///android_asset/www/manifest.json")')
200 if re.search(r'^http(|s)', options.app_url):
201 ReplaceString(dest_activity, 'file:///android_asset/www/index.html',
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)
209 print ('Please make sure that the relative path of entry file'
213 if options.enable_remote_debugging:
214 SetVariable(dest_activity, 'RemoteDebugging', '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(options, sanitized_name):
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/.
254 if not options.extensions:
256 apk_path = options.name
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 = 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)
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()
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')
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)
400 if __name__ == '__main__':