commonroutesServer/www/platforms/android/cordova/lib/prepare.js
www/platforms/android/cordova/lib/prepare.jsRaw
1/**
2 Licensed to the Apache Software Foundation (ASF) under one
3 or more contributor license agreements. See the NOTICE file
4 distributed with this work for additional information
5 regarding copyright ownership. The ASF licenses this file
6 to you under the Apache License, Version 2.0 (the
7 "License"); you may not use this file except in compliance
8 with the License. You may obtain a copy of the License at
9
10 http://www.apache.org/licenses/LICENSE-2.0
11
12 Unless required by applicable law or agreed to in writing,
13 software distributed under the License is distributed on an
14 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 KIND, either express or implied. See the License for the
16 specific language governing permissions and limitations
17 under the License.
18*/
19
20var Q = require('q');
21var fs = require('fs');
22var path = require('path');
23var shell = require('shelljs');
24var events = require('cordova-common').events;
25var AndroidManifest = require('./AndroidManifest');
26var xmlHelpers = require('cordova-common').xmlHelpers;
27var CordovaError = require('cordova-common').CordovaError;
28var ConfigParser = require('cordova-common').ConfigParser;
29var FileUpdater = require('cordova-common').FileUpdater;
30var PlatformJson = require('cordova-common').PlatformJson;
31var PlatformMunger = require('cordova-common').ConfigChanges.PlatformMunger;
32var PluginInfoProvider = require('cordova-common').PluginInfoProvider;
33
34module.exports.prepare = function (cordovaProject, options) {
35 var self = this;
36 var platformResourcesDir = path.relative(cordovaProject.root, path.join(this.locations.root, 'res'));
37
38 var platformJson = PlatformJson.load(this.locations.root, this.platform);
39 var munger = new PlatformMunger(this.platform, this.locations.root, platformJson, new PluginInfoProvider());
40
41 this._config = updateConfigFilesFrom(cordovaProject.projectConfig, munger, this.locations);
42
43 // Update own www dir with project's www assets and plugins' assets and js-files
44 return Q.when(updateWww(cordovaProject, this.locations))
45 .then(function () {
46 // update project according to config.xml changes.
47 return updateProjectAccordingTo(self._config, self.locations);
48 })
49 .then(function () {
50 updateIcons(cordovaProject, platformResourcesDir);
51 updateSplashes(cordovaProject, platformResourcesDir);
52 })
53 .then(function () {
54 events.emit('verbose', 'Prepared android project successfully');
55 });
56};
57
58module.exports.clean = function (options) {
59 // A cordovaProject isn't passed into the clean() function, because it might have
60 // been called from the platform shell script rather than the CLI. Check for the
61 // noPrepare option passed in by the non-CLI clean script. If that's present, or if
62 // there's no config.xml found at the project root, then don't clean prepared files.
63 var projectRoot = path.resolve(this.root, '../..');
64 var projectConfigFile = path.join(projectRoot, 'config.xml');
65 if ((options && options.noPrepare) || !fs.existsSync(projectConfigFile) ||
66 !fs.existsSync(this.locations.configXml)) {
67 return Q();
68 }
69
70 var projectConfig = new ConfigParser(this.locations.configXml);
71 var platformResourcesDir = path.relative(projectRoot, path.join(this.locations.root, 'res'));
72
73 var self = this;
74 return Q().then(function () {
75 cleanWww(projectRoot, self.locations);
76 cleanIcons(projectRoot, projectConfig, platformResourcesDir);
77 cleanSplashes(projectRoot, projectConfig, platformResourcesDir);
78 });
79};
80
81/**
82 * Updates config files in project based on app's config.xml and config munge,
83 * generated by plugins.
84 *
85 * @param {ConfigParser} sourceConfig A project's configuration that will
86 * be merged into platform's config.xml
87 * @param {ConfigChanges} configMunger An initialized ConfigChanges instance
88 * for this platform.
89 * @param {Object} locations A map of locations for this platform
90 *
91 * @return {ConfigParser} An instance of ConfigParser, that
92 * represents current project's configuration. When returned, the
93 * configuration is already dumped to appropriate config.xml file.
94 */
95function updateConfigFilesFrom(sourceConfig, configMunger, locations) {
96 events.emit('verbose', 'Generating platform-specific config.xml from defaults for android at ' + locations.configXml);
97
98 // First cleanup current config and merge project's one into own
99 // Overwrite platform config.xml with defaults.xml.
100 shell.cp('-f', locations.defaultConfigXml, locations.configXml);
101
102 // Then apply config changes from global munge to all config files
103 // in project (including project's config)
104 configMunger.reapply_global_munge().save_all();
105
106 events.emit('verbose', 'Merging project\'s config.xml into platform-specific android config.xml');
107 // Merge changes from app's config.xml into platform's one
108 var config = new ConfigParser(locations.configXml);
109 xmlHelpers.mergeXml(sourceConfig.doc.getroot(),
110 config.doc.getroot(), 'android', /*clobber=*/true);
111
112 config.write();
113 return config;
114}
115
116/**
117 * Logs all file operations via the verbose event stream, indented.
118 */
119function logFileOp(message) {
120 events.emit('verbose', ' ' + message);
121}
122
123/**
124 * Updates platform 'www' directory by replacing it with contents of
125 * 'platform_www' and app www. Also copies project's overrides' folder into
126 * the platform 'www' folder
127 *
128 * @param {Object} cordovaProject An object which describes cordova project.
129 * @param {Object} destinations An object that contains destination
130 * paths for www files.
131 */
132function updateWww(cordovaProject, destinations) {
133 var sourceDirs = [
134 path.relative(cordovaProject.root, cordovaProject.locations.www),
135 path.relative(cordovaProject.root, destinations.platformWww)
136 ];
137
138 // If project contains 'merges' for our platform, use them as another overrides
139 var merges_path = path.join(cordovaProject.root, 'merges', 'android');
140 if (fs.existsSync(merges_path)) {
141 events.emit('verbose', 'Found "merges/android" folder. Copying its contents into the android project.');
142 sourceDirs.push(path.join('merges', 'android'));
143 }
144
145 var targetDir = path.relative(cordovaProject.root, destinations.www);
146 events.emit(
147 'verbose', 'Merging and updating files from [' + sourceDirs.join(', ') + '] to ' + targetDir);
148 FileUpdater.mergeAndUpdateDir(
149 sourceDirs, targetDir, { rootDir: cordovaProject.root }, logFileOp);
150}
151
152/**
153 * Cleans all files from the platform 'www' directory.
154 */
155function cleanWww(projectRoot, locations) {
156 var targetDir = path.relative(projectRoot, locations.www);
157 events.emit('verbose', 'Cleaning ' + targetDir);
158
159 // No source paths are specified, so mergeAndUpdateDir() will clear the target directory.
160 FileUpdater.mergeAndUpdateDir(
161 [], targetDir, { rootDir: projectRoot, all: true }, logFileOp);
162}
163
164/**
165 * Updates project structure and AndroidManifest according to project's configuration.
166 *
167 * @param {ConfigParser} platformConfig A project's configuration that will
168 * be used to update project
169 * @param {Object} locations A map of locations for this platform
170 */
171function updateProjectAccordingTo(platformConfig, locations) {
172 // Update app name by editing res/values/strings.xml
173 var name = platformConfig.name();
174 var strings = xmlHelpers.parseElementtreeSync(locations.strings);
175 strings.find('string[@name="app_name"]').text = name;
176 fs.writeFileSync(locations.strings, strings.write({indent: 4}), 'utf-8');
177 events.emit('verbose', 'Wrote out android application name "' + name + '" to ' + locations.strings);
178
179 // Java packages cannot support dashes
180 var pkg = (platformConfig.android_packageName() || platformConfig.packageName()).replace(/-/g, '_');
181
182 var manifest = new AndroidManifest(locations.manifest);
183 var orig_pkg = manifest.getPackageId();
184
185 manifest.getActivity()
186 .setOrientation(platformConfig.getPreference('orientation'))
187 .setLaunchMode(findAndroidLaunchModePreference(platformConfig));
188
189 manifest.setVersionName(platformConfig.version())
190 .setVersionCode(platformConfig.android_versionCode() || default_versionCode(platformConfig.version()))
191 .setPackageId(pkg)
192 .setMinSdkVersion(platformConfig.getPreference('android-minSdkVersion', 'android'))
193 .setMaxSdkVersion(platformConfig.getPreference('android-maxSdkVersion', 'android'))
194 .setTargetSdkVersion(platformConfig.getPreference('android-targetSdkVersion', 'android'))
195 .write();
196
197 var javaPattern = path.join(locations.root, 'src', orig_pkg.replace(/\./g, '/'), '*.java');
198 var java_files = shell.ls(javaPattern).filter(function(f) {
199 return shell.grep(/extends\s+CordovaActivity/g, f);
200 });
201
202 if (java_files.length === 0) {
203 throw new CordovaError('No Java files found that extend CordovaActivity.');
204 } else if(java_files.length > 1) {
205 events.emit('log', 'Multiple candidate Java files that extend CordovaActivity found. Guessing at the first one, ' + java_files[0]);
206 }
207
208 var destFile = path.join(locations.root, 'src', pkg.replace(/\./g, '/'), path.basename(java_files[0]));
209 shell.mkdir('-p', path.dirname(destFile));
210 shell.sed(/package [\w\.]*;/, 'package ' + pkg + ';', java_files[0]).to(destFile);
211 events.emit('verbose', 'Wrote out Android package name "' + pkg + '" to ' + destFile);
212
213 if (orig_pkg !== pkg) {
214 // If package was name changed we need to remove old java with main activity
215 shell.rm('-Rf',java_files[0]);
216 // remove any empty directories
217 var currentDir = path.dirname(java_files[0]);
218 var sourcesRoot = path.resolve(locations.root, 'src');
219 while(currentDir !== sourcesRoot) {
220 if(fs.existsSync(currentDir) && fs.readdirSync(currentDir).length === 0) {
221 fs.rmdirSync(currentDir);
222 currentDir = path.resolve(currentDir, '..');
223 } else {
224 break;
225 }
226 }
227 }
228}
229
230// Consturct the default value for versionCode as
231// PATCH + MINOR * 100 + MAJOR * 10000
232// see http://developer.android.com/tools/publishing/versioning.html
233function default_versionCode(version) {
234 var nums = version.split('-')[0].split('.');
235 var versionCode = 0;
236 if (+nums[0]) {
237 versionCode += +nums[0] * 10000;
238 }
239 if (+nums[1]) {
240 versionCode += +nums[1] * 100;
241 }
242 if (+nums[2]) {
243 versionCode += +nums[2];
244 }
245
246 events.emit('verbose', 'android-versionCode not found in config.xml. Generating a code based on version in config.xml (' + version + '): ' + versionCode);
247 return versionCode;
248}
249
250function getImageResourcePath(resourcesDir, density, name, sourceName) {
251 if (/\.9\.png$/.test(sourceName)) {
252 name = name.replace(/\.png$/, '.9.png');
253 }
254 var resourcePath = path.join(resourcesDir, (density ? 'drawable-' + density : 'drawable'), name);
255 return resourcePath;
256}
257
258function updateSplashes(cordovaProject, platformResourcesDir) {
259 var resources = cordovaProject.projectConfig.getSplashScreens('android');
260
261 // if there are "splash" elements in config.xml
262 if (resources.length === 0) {
263 events.emit('verbose', 'This app does not have splash screens defined');
264 return;
265 }
266
267 var resourceMap = mapImageResources(cordovaProject.root, platformResourcesDir, 'screen.png');
268
269 var hadMdpi = false;
270 resources.forEach(function (resource) {
271 if (!resource.density) {
272 return;
273 }
274 if (resource.density == 'mdpi') {
275 hadMdpi = true;
276 }
277 var targetPath = getImageResourcePath(
278 platformResourcesDir, resource.density, 'screen.png', path.basename(resource.src));
279 resourceMap[targetPath] = resource.src;
280 });
281
282 // There's no "default" drawable, so assume default == mdpi.
283 if (!hadMdpi && resources.defaultResource) {
284 var targetPath = getImageResourcePath(
285 platformResourcesDir, 'mdpi', 'screen.png', path.basename(resources.defaultResource.src));
286 resourceMap[targetPath] = resources.defaultResource.src;
287 }
288
289 events.emit('verbose', 'Updating splash screens at ' + platformResourcesDir);
290 FileUpdater.updatePaths(
291 resourceMap, { rootDir: cordovaProject.root }, logFileOp);
292}
293
294function cleanSplashes(projectRoot, projectConfig, platformResourcesDir) {
295 var resources = projectConfig.getSplashScreens('android');
296 if (resources.length > 0) {
297 var resourceMap = mapImageResources(projectRoot, platformResourcesDir, 'screen.png');
298 events.emit('verbose', 'Cleaning splash screens at ' + platformResourcesDir);
299
300 // No source paths are specified in the map, so updatePaths() will delete the target files.
301 FileUpdater.updatePaths(
302 resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
303 }
304}
305
306function updateIcons(cordovaProject, platformResourcesDir) {
307 var icons = cordovaProject.projectConfig.getIcons('android');
308
309 // if there are icon elements in config.xml
310 if (icons.length === 0) {
311 events.emit('verbose', 'This app does not have launcher icons defined');
312 return;
313 }
314
315 var resourceMap = mapImageResources(cordovaProject.root, platformResourcesDir, 'icon.png');
316
317 var android_icons = {};
318 var default_icon;
319 // http://developer.android.com/design/style/iconography.html
320 var sizeToDensityMap = {
321 36: 'ldpi',
322 48: 'mdpi',
323 72: 'hdpi',
324 96: 'xhdpi',
325 144: 'xxhdpi',
326 192: 'xxxhdpi'
327 };
328 // find the best matching icon for a given density or size
329 // @output android_icons
330 var parseIcon = function(icon, icon_size) {
331 // do I have a platform icon for that density already
332 var density = icon.density || sizeToDensityMap[icon_size];
333 if (!density) {
334 // invalid icon defition ( or unsupported size)
335 return;
336 }
337 var previous = android_icons[density];
338 if (previous && previous.platform) {
339 return;
340 }
341 android_icons[density] = icon;
342 };
343
344 // iterate over all icon elements to find the default icon and call parseIcon
345 for (var i=0; i<icons.length; i++) {
346 var icon = icons[i];
347 var size = icon.width;
348 if (!size) {
349 size = icon.height;
350 }
351 if (!size && !icon.density) {
352 if (default_icon) {
353 events.emit('verbose', 'Found extra default icon: ' + icon.src + ' (ignoring in favor of ' + default_icon.src + ')');
354 } else {
355 default_icon = icon;
356 }
357 } else {
358 parseIcon(icon, size);
359 }
360 }
361
362 // The source paths for icons and splashes are relative to
363 // project's config.xml location, so we use it as base path.
364 for (var density in android_icons) {
365 var targetPath = getImageResourcePath(
366 platformResourcesDir, density, 'icon.png', path.basename(android_icons[density].src));
367 resourceMap[targetPath] = android_icons[density].src;
368 }
369
370 // There's no "default" drawable, so assume default == mdpi.
371 if (default_icon && !android_icons.mdpi) {
372 var defaultTargetPath = getImageResourcePath(
373 platformResourcesDir, 'mdpi', 'icon.png', path.basename(default_icon.src));
374 resourceMap[defaultTargetPath] = default_icon.src;
375 }
376
377 events.emit('verbose', 'Updating icons at ' + platformResourcesDir);
378 FileUpdater.updatePaths(
379 resourceMap, { rootDir: cordovaProject.root }, logFileOp);
380}
381
382function cleanIcons(projectRoot, projectConfig, platformResourcesDir) {
383 var icons = projectConfig.getIcons('android');
384 if (icons.length > 0) {
385 var resourceMap = mapImageResources(projectRoot, platformResourcesDir, 'icon.png');
386 events.emit('verbose', 'Cleaning icons at ' + platformResourcesDir);
387
388 // No source paths are specified in the map, so updatePaths() will delete the target files.
389 FileUpdater.updatePaths(
390 resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
391 }
392}
393
394/**
395 * Gets a map containing resources of a specified name from all drawable folders in a directory.
396 */
397function mapImageResources(rootDir, subDir, resourceName) {
398 var pathMap = {};
399 shell.ls(path.join(rootDir, subDir, 'drawable-*'))
400 .forEach(function (drawableFolder) {
401 var imagePath = path.join(subDir, path.basename(drawableFolder), resourceName);
402 pathMap[imagePath] = null;
403 });
404 return pathMap;
405}
406
407/**
408 * Gets and validates 'AndroidLaunchMode' prepference from config.xml. Returns
409 * preference value and warns if it doesn't seems to be valid
410 *
411 * @param {ConfigParser} platformConfig A configParser instance for
412 * platform.
413 *
414 * @return {String} Preference's value from config.xml or
415 * default value, if there is no such preference. The default value is
416 * 'singleTop'
417 */
418function findAndroidLaunchModePreference(platformConfig) {
419 var launchMode = platformConfig.getPreference('AndroidLaunchMode');
420 if (!launchMode) {
421 // Return a default value
422 return 'singleTop';
423 }
424
425 var expectedValues = ['standard', 'singleTop', 'singleTask', 'singleInstance'];
426 var valid = expectedValues.indexOf(launchMode) >= 0;
427 if (!valid) {
428 // Note: warn, but leave the launch mode as developer wanted, in case the list of options changes in the future
429 events.emit('warn', 'Unrecognized value for AndroidLaunchMode preference: ' +
430 launchMode + '. Expected values are: ' + expectedValues.join(', '));
431 }
432
433 return launchMode;
434}