arnaucube
arnaucube/commonroutesServerPublic Self-hosted repository
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}