code:
#!/usr/bin/env node

/*
       Licensed to the Apache Software Foundation (ASF) under one
       or more contributor license agreements.  See the NOTICE file
       distributed with this work for additional information
       regarding copyright ownership.  The ASF licenses this file
       to you under the Apache License, Version 2.0 (the
       "License"); you may not use this file except in compliance
       with the License.  You may obtain a copy of the License at

         http://www.apache.org/licenses/LICENSE-2.0

       Unless required by applicable law or agreed to in writing,
       software distributed under the License is distributed on an
       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
       KIND, either express or implied.  See the License for the
       specific language governing permissions and limitations
       under the License.
*/

var android_versions = require('android-versions');
var retry = require('./retry');
var build = require('./build');
var path = require('path');
var Adb = require('./Adb');
var AndroidManifest = require('./AndroidManifest');
var events = require('cordova-common').events;
var superspawn = require('cordova-common').superspawn;
var CordovaError = require('cordova-common').CordovaError;
var shelljs = require('shelljs');
var android_sdk = require('./android_sdk');
var check_reqs = require('./check_reqs');

var os = require('os');
var fs = require('fs');
var child_process = require('child_process');

// constants
var ONE_SECOND = 1000; // in milliseconds
var ONE_MINUTE = 60 * ONE_SECOND; // in milliseconds
var INSTALL_COMMAND_TIMEOUT = 5 * ONE_MINUTE; // in milliseconds
var NUM_INSTALL_RETRIES = 3;
var CHECK_BOOTED_INTERVAL = 3 * ONE_SECOND; // in milliseconds
var EXEC_KILL_SIGNAL = 'SIGKILL';

function forgivingWhichSync (cmd) {
    try {
        return fs.realpathSync(shelljs.which(cmd));
    } catch (e) {
        return '';
    }
}

module.exports.list_images_using_avdmanager = function () {
    return superspawn.spawn('avdmanager', ['list', 'avd']).then(function (output) {
        var response = output.split('\n');
        var emulator_list = [];
        for (var i = 1; i < response.length; i++) {
            // To return more detailed information use img_obj
            var img_obj = {};
            if (response[i].match(/Name:\s/)) {
                img_obj['name'] = response[i].split('Name: ')[1].replace('\r', '');
                if (response[i + 1].match(/Device:\s/)) {
                    i++;
                    img_obj['device'] = response[i].split('Device: ')[1].replace('\r', '');
                }
                if (response[i + 1].match(/Path:\s/)) {
                    i++;
                    img_obj['path'] = response[i].split('Path: ')[1].replace('\r', '');
                }
                if (response[i + 1].match(/Target:\s/)) {
                    i++;
                    if (response[i + 1].match(/ABI:\s/)) {
                        img_obj['abi'] = response[i + 1].split('ABI: ')[1].replace('\r', '');
                    }
                    // This next conditional just aims to match the old output of `android list avd`
                    // We do so so that we don't have to change the logic when parsing for the
                    // best emulator target to spawn (see below in `best_image`)
                    // This allows us to transitionally support both `android` and `avdmanager` binaries,
                    // depending on what SDK version the user has
                    if (response[i + 1].match(/Based\son:\s/)) {
                        img_obj['target'] = response[i + 1].split('Based on:')[1];
                        if (img_obj['target'].match(/Tag\/ABI:\s/)) {
                            img_obj['target'] = img_obj['target'].split('Tag/ABI:')[0].replace('\r', '').trim();
                            if (img_obj['target'].indexOf('(') > -1) {
                                img_obj['target'] = img_obj['target'].substr(0, img_obj['target'].indexOf('(') - 1).trim();
                            }
                        }
                        var version_string = img_obj['target'].replace(/Android\s+/, '');

                        var api_level = android_sdk.version_string_to_api_level[version_string];
                        if (api_level) {
                            img_obj['target'] += ' (API level ' + api_level + ')';
                        }
                    }
                }
                if (response[i + 1].match(/Skin:\s/)) {
                    i++;
                    img_obj['skin'] = response[i].split('Skin: ')[1].replace('\r', '');
                }

                emulator_list.push(img_obj);
            }
            /* To just return a list of names use this
            if (response[i].match(/Name:\s/)) {
                emulator_list.push(response[i].split('Name: ')[1].replace('\r', '');
            } */

        }
        return emulator_list;
    });
};

module.exports.list_images_using_android = function () {
    return superspawn.spawn('android', ['list', 'avd']).then(function (output) {
        var response = output.split('\n');
        var emulator_list = [];
        for (var i = 1; i < response.length; i++) {
            // To return more detailed information use img_obj
            var img_obj = {};
            if (response[i].match(/Name:\s/)) {
                img_obj['name'] = response[i].split('Name: ')[1].replace('\r', '');
                if (response[i + 1].match(/Device:\s/)) {
                    i++;
                    img_obj['device'] = response[i].split('Device: ')[1].replace('\r', '');
                }
                if (response[i + 1].match(/Path:\s/)) {
                    i++;
                    img_obj['path'] = response[i].split('Path: ')[1].replace('\r', '');
                }
                if (response[i + 1].match(/\(API\slevel\s/) || (response[i + 2] && response[i + 2].match(/\(API\slevel\s/))) {
                    i++;
                    var secondLine = response[i + 1].match(/\(API\slevel\s/) ? response[i + 1] : '';
                    img_obj['target'] = (response[i] + secondLine).split('Target: ')[1].replace('\r', '');
                }
                if (response[i + 1].match(/ABI:\s/)) {
                    i++;
                    img_obj['abi'] = response[i].split('ABI: ')[1].replace('\r', '');
                }
                if (response[i + 1].match(/Skin:\s/)) {
                    i++;
                    img_obj['skin'] = response[i].split('Skin: ')[1].replace('\r', '');
                }

                emulator_list.push(img_obj);
            }
            /* To just return a list of names use this
            if (response[i].match(/Name:\s/)) {
                emulator_list.push(response[i].split('Name: ')[1].replace('\r', '');
            } */

        }
        return emulator_list;
    });
};

/**
 * Returns a Promise for a list of emulator images in the form of objects
 * {
       name   : <emulator_name>,
       device : <device>,
       path   : <path_to_emulator_image>,
       target : <api_target>,
       abi    : <cpu>,
       skin   : <skin>
   }
 */
module.exports.list_images = function () {
    return Promise.resolve().then(function () {
        if (forgivingWhichSync('avdmanager')) {
            return module.exports.list_images_using_avdmanager();
        } else if (forgivingWhichSync('android')) {
            return module.exports.list_images_using_android();
        } else {
            return Promise.reject(new CordovaError('Could not find either `android` or `avdmanager` on your $PATH! Are you sure the Android SDK is installed and available?'));
        }
    }).then(function (avds) {
        // In case we're missing the Android OS version string from the target description, add it.
        return avds.map(function (avd) {
            if (avd.target && avd.target.indexOf('Android API') > -1 && avd.target.indexOf('API level') < 0) {
                var api_level = avd.target.match(/\d+/);
                if (api_level) {
                    var level = android_versions.get(api_level);
                    if (level) {
                        avd.target = 'Android ' + level.semver + ' (API level ' + api_level + ')';
                    }
                }
            }
            return avd;
        });
    });
};

/**
 * Will return the closest avd to the projects target
 * or undefined if no avds exist.
 * Returns a promise.
 */
module.exports.best_image = function () {
    return this.list_images().then(function (images) {
        // Just return undefined if there is no images
        if (images.length === 0) return;

        var closest = 9999;
        var best = images[0];
        var project_target = parseInt(check_reqs.get_target().replace('android-', ''));
        for (var i in images) {
            var target = images[i].target;
            if (target && target.indexOf('API level') > -1) {
                var num = parseInt(target.split('(API level ')[1].replace(')', ''));
                if (num === project_target) {
                    return images[i];
                } else if (project_target - num < closest && project_target > num) {
                    closest = project_target - num;
                    best = images[i];
                }
            }
        }
        return best;
    });
};

// Returns a promise.
module.exports.list_started = function () {
    return Adb.devices({ emulators: true });
};

// Returns a promise.
// TODO: we should remove this, there's a more robust method under android_sdk.js
module.exports.list_targets = function () {
    return superspawn.spawn('android', ['list', 'targets'], { cwd: os.tmpdir() }).then(function (output) {
        var target_out = output.split('\n');
        var targets = [];
        for (var i = target_out.length; i >= 0; i--) {
            if (target_out[i].match(/id:/)) {
                targets.push(targets[i].split(' ')[1]);
            }
        }
        return targets;
    });
};

/*
 * Gets unused port for android emulator, between 5554 and 5584
 * Returns a promise.
 */
module.exports.get_available_port = function () {
    var self = this;

    return self.list_started().then(function (emulators) {
        for (var p = 5584; p >= 5554; p -= 2) {
            if (emulators.indexOf('emulator-' + p) === -1) {
                events.emit('verbose', 'Found available port: ' + p);
                return p;
            }
        }
        throw new CordovaError('Could not find an available avd port');
    });
};

/*
 * Starts an emulator with the given ID,
 * and returns the started ID of that emulator.
 * If no ID is given it will use the first image available,
 * if no image is available it will error out (maybe create one?).
 * If no boot timeout is given or the value is negative it will wait forever for
 * the emulator to boot
 *
 * Returns a promise.
 */
module.exports.start = function (emulator_ID, boot_timeout) {
    var self = this;

    return Promise.resolve().then(function () {
        if (emulator_ID) return Promise.resolve(emulator_ID);

        return self.best_image().then(function (best) {
            if (best && best.name) {
                events.emit('warn', 'No emulator specified, defaulting to ' + best.name);
                return best.name;
            }

            var androidCmd = check_reqs.getAbsoluteAndroidCmd();
            return Promise.reject(new CordovaError('No emulator images (avds) found.\n' +
                '1. Download desired System Image by running: ' + androidCmd + ' sdk\n' +
                '2. Create an AVD by running: ' + androidCmd + ' avd\n' +
                'HINT: For a faster emulator, use an Intel System Image and install the HAXM device driver\n'));
        });
    }).then(function (emulatorId) {
        return self.get_available_port().then(function (port) {
            // Figure out the directory the emulator binary runs in, and set the cwd to that directory.
            // Workaround for https://code.google.com/p/android/issues/detail?id=235461
            var emulator_dir = path.dirname(shelljs.which('emulator'));
            var args = ['-avd', emulatorId, '-port', port];
            // Don't wait for it to finish, since the emulator will probably keep running for a long time.
            child_process
                .spawn('emulator', args, { stdio: 'inherit', detached: true, cwd: emulator_dir })
                .unref();

            // wait for emulator to start
            events.emit('log', 'Waiting for emulator to start...');
            return self.wait_for_emulator(port);
        });
    }).then(function (emulatorId) {
        if (!emulatorId) { return Promise.reject(new CordovaError('Failed to start emulator')); }

        // wait for emulator to boot up
        process.stdout.write('Waiting for emulator to boot (this may take a while)...');
        return self.wait_for_boot(emulatorId, boot_timeout).then(function (success) {
            if (success) {
                events.emit('log', 'BOOT COMPLETE');
                // unlock screen
                return Adb.shell(emulatorId, 'input keyevent 82').then(function () {
                    // return the new emulator id for the started emulators
                    return emulatorId;
                });
            } else {
                // We timed out waiting for the boot to happen
                return null;
            }
        });
    });
};

/*
 * Waits for an emulator to boot on a given port.
 * Returns this emulator's ID in a promise.
 */
module.exports.wait_for_emulator = function (port) {
    var self = this;
    return Promise.resolve().then(function () {
        var emulator_id = 'emulator-' + port;
        return Adb.shell(emulator_id, 'getprop dev.bootcomplete').then(function (output) {
            if (output.indexOf('1') >= 0) {
                return emulator_id;
            }
            return self.wait_for_emulator(port);
        }, function (error) {
            if ((error && error.message &&
            (error.message.indexOf('not found') > -1)) ||
            (error.message.indexOf('device offline') > -1) ||
            (error.message.indexOf('device still connecting') > -1) ||
            (error.message.indexOf('device still authorizing') > -1)) {
                // emulator not yet started, continue waiting
                return self.wait_for_emulator(port);
            } else {
                // something unexpected has happened
                throw error;
            }
        });
    });
};

/*
 * Waits for the core android process of the emulator to start. Returns a
 * promise that resolves to a boolean indicating success. Not specifying a
 * time_remaining or passing a negative value will cause it to wait forever
 */
module.exports.wait_for_boot = function (emulator_id, time_remaining) {
    var self = this;
    return Adb.shell(emulator_id, 'ps').then(function (output) {
        if (output.match(/android\.process\.acore/)) {
            return true;
        } else if (time_remaining === 0) {
            return false;
        } else {
            process.stdout.write('.');

            return new Promise(resolve => {
                const delay = time_remaining < CHECK_BOOTED_INTERVAL ? time_remaining : CHECK_BOOTED_INTERVAL;

                setTimeout(() => {
                    const updated_time = time_remaining >= 0 ? Math.max(time_remaining - CHECK_BOOTED_INTERVAL, 0) : time_remaining;
                    resolve(self.wait_for_boot(emulator_id, updated_time));
                }, delay);
            });
        }
    });
};

/*
 * Create avd
 * TODO : Enter the stdin input required to complete the creation of an avd.
 * Returns a promise.
 */
module.exports.create_image = function (name, target) {
    console.log('Creating new avd named ' + name);
    if (target) {
        return superspawn.spawn('android', ['create', 'avd', '--name', name, '--target', target]).then(null, function (error) {
            console.error('ERROR : Failed to create emulator image : ');
            console.error(' Do you have the latest android targets including ' + target + '?');
            console.error(error);
        });
    } else {
        console.log('WARNING : Project target not found, creating avd with a different target but the project may fail to install.');
        // TODO: there's a more robust method for finding targets in android_sdk.js
        return superspawn.spawn('android', ['create', 'avd', '--name', name, '--target', this.list_targets()[0]]).then(function () {
            // TODO: This seems like another error case, even though it always happens.
            console.error('ERROR : Unable to create an avd emulator, no targets found.');
            console.error('Ensure you have targets available by running the "android" command');
            return Promise.reject(new CordovaError());
        }, function (error) {
            console.error('ERROR : Failed to create emulator image : ');
            console.error(error);
        });
    }
};

module.exports.resolveTarget = function (target) {
    return this.list_started().then(function (emulator_list) {
        if (emulator_list.length < 1) {
            return Promise.reject(new CordovaError('No running Android emulators found, please start an emulator before deploying your project.'));
        }

        // default emulator
        target = target || emulator_list[0];
        if (emulator_list.indexOf(target) < 0) {
            return Promise.reject(new CordovaError('Unable to find target \'' + target + '\'. Failed to deploy to emulator.'));
        }

        return build.detectArchitecture(target).then(function (arch) {
            return { target: target, arch: arch, isEmulator: true };
        });
    });
};

/*
 * Installs a previously built application on the emulator and launches it.
 * If no target is specified, then it picks one.
 * If no started emulators are found, error out.
 * Returns a promise.
 */
module.exports.install = function (givenTarget, buildResults) {

    var target;
    // We need to find the proper path to the Android Manifest
    const manifestPath = path.join(__dirname, '..', '..', 'app', 'src', 'main', 'AndroidManifest.xml');
    const manifest = new AndroidManifest(manifestPath);
    const pkgName = manifest.getPackageId();

    // resolve the target emulator
    return Promise.resolve().then(function () {
        if (givenTarget && typeof givenTarget === 'object') {
            return givenTarget;
        } else {
            return module.exports.resolveTarget(givenTarget);
        }

    // set the resolved target
    }).then(function (resolvedTarget) {
        target = resolvedTarget;

    // install the app
    }).then(function () {
        // This promise is always resolved, even if 'adb uninstall' fails to uninstall app
        // or the app doesn't installed at all, so no error catching needed.
        return Promise.resolve().then(function () {

            var apk_path = build.findBestApkForArchitecture(buildResults, target.arch);
            var execOptions = {
                cwd: os.tmpdir(),
                timeout: INSTALL_COMMAND_TIMEOUT, // in milliseconds
                killSignal: EXEC_KILL_SIGNAL
            };

            events.emit('log', 'Using apk: ' + apk_path);
            events.emit('log', 'Package name: ' + pkgName);
            events.emit('verbose', 'Installing app on emulator...');

            // A special function to call adb install in specific environment w/ specific options.
            // Introduced as a part of fix for http://issues.apache.org/jira/browse/CB-9119
            // to workaround sporadic emulator hangs
            function adbInstallWithOptions (target, apk, opts) {
                events.emit('verbose', 'Installing apk ' + apk + ' on ' + target + '...');

                var command = 'adb -s ' + target + ' install -r "' + apk + '"';
                return new Promise(function (resolve, reject) {
                    child_process.exec(command, opts, function (err, stdout, stderr) {
                        if (err) reject(new CordovaError('Error executing "' + command + '": ' + stderr));
                        // adb does not return an error code even if installation fails. Instead it puts a specific
                        // message to stdout, so we have to use RegExp matching to detect installation failure.
                        else if (/Failure/.test(stdout)) {
                            if (stdout.match(/INSTALL_PARSE_FAILED_NO_CERTIFICATES/)) {
                                stdout += 'Sign the build using \'-- --keystore\' or \'--buildConfig\'' +
                                    ' or sign and deploy the unsigned apk manually using Android tools.';
                            } else if (stdout.match(/INSTALL_FAILED_VERSION_DOWNGRADE/)) {
                                stdout += 'You\'re trying to install apk with a lower versionCode that is already installed.' +
                                    '\nEither uninstall an app or increment the versionCode.';
                            }

                            reject(new CordovaError('Failed to install apk to emulator: ' + stdout));
                        } else resolve(stdout);
                    });
                });
            }

            function installPromise () {
                return adbInstallWithOptions(target.target, apk_path, execOptions).catch(function (error) {
                    // CB-9557 CB-10157 only uninstall and reinstall app if the one that
                    // is already installed on device was signed w/different certificate
                    if (!/INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES/.test(error.toString())) { throw error; }

                    events.emit('warn', 'Uninstalling app from device and reinstalling it because the ' +
                        'currently installed app was signed with different key');

                    // This promise is always resolved, even if 'adb uninstall' fails to uninstall app
                    // or the app doesn't installed at all, so no error catching needed.
                    return Adb.uninstall(target.target, pkgName).then(function () {
                        return adbInstallWithOptions(target.target, apk_path, execOptions);
                    });
                });
            }

            return retry.retryPromise(NUM_INSTALL_RETRIES, installPromise).then(function (output) {
                events.emit('log', 'INSTALL SUCCESS');
            });
        });
    // unlock screen
    }).then(function () {

        events.emit('verbose', 'Unlocking screen...');
        return Adb.shell(target.target, 'input keyevent 82');
    }).then(function () {
        Adb.start(target.target, pkgName + '/.' + manifest.getActivity().getName());
    // report success or failure
    }).then(function (output) {
        events.emit('log', 'LAUNCH SUCCESS');
    });
};


code:
@ECHO OFF
for /f "tokens=2*" %%a in ('REG QUERY "HKEY_LOCAL_MACHINE\SOFTWARE\Android Studio" /v Path') do set "ASPath=%%~b"
ECHO %ASPath%

repo_name: Area/degrees

license: mit


code:
#!/usr/bin/env node

/*
       Licensed to the Apache Software Foundation (ASF) under one
       or more contributor license agreements.  See the NOTICE file
       distributed with this work for additional information
       regarding copyright ownership.  The ASF licenses this file
       to you under the Apache License, Version 2.0 (the
       "License"); you may not use this file except in compliance
       with the License.  You may obtain a copy of the License at

         http://www.apache.org/licenses/LICENSE-2.0

       Unless required by applicable law or agreed to in writing,
       software distributed under the License is distributed on an
       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
       KIND, either express or implied.  See the License for the
       specific language governing permissions and limitations
       under the License.
*/

var device = require('./device');
var args = process.argv;

if (args.length > 2) {
    var install_target;
    if (args[2].substring(0, 9) === '--target=') {
        install_target = args[2].substring(9, args[2].length);
        device.install(install_target).catch(function (err) {
            console.error('ERROR: ' + err);
            process.exit(2);
        });
    } else {
        console.error('ERROR : argument \'' + args[2] + '\' not recognized.');
        process.exit(2);
    }
} else {
    device.install().catch(function (err) {
        console.error('ERROR: ' + err);
        process.exit(2);
    });
}


code:
:: Licensed to the Apache Software Foundation (ASF) under one
:: or more contributor license agreements.  See the NOTICE file
:: distributed with this work for additional information
:: regarding copyright ownership.  The ASF licenses this file
:: to you under the Apache License, Version 2.0 (the
:: "License"); you may not use this file except in compliance
:: with the License.  You may obtain a copy of the License at
::
:: http://www.apache.org/licenses/LICENSE-2.0
::
:: Unless required by applicable law or agreed to in writing,
:: software distributed under the License is distributed on an
:: "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
:: KIND, either express or implied.  See the License for the
:: specific language governing permissions and limitations
:: under the License.

@ECHO OFF
SET script_path="%~dp0install-device"
IF EXIST %script_path% (
        node %script_path% %*
) ELSE (
    ECHO.
    ECHO ERROR: Could not find 'install-device' script in 'cordova\lib' folder, aborting...>&2
    EXIT /B 1
)


code:
#!/usr/bin/env node

/*
       Licensed to the Apache Software Foundation (ASF) under one
       or more contributor license agreements.  See the NOTICE file
       distributed with this work for additional information
       regarding copyright ownership.  The ASF licenses this file
       to you under the Apache License, Version 2.0 (the
       "License"); you may not use this file except in compliance
       with the License.  You may obtain a copy of the License at

         http://www.apache.org/licenses/LICENSE-2.0

       Unless required by applicable law or agreed to in writing,
       software distributed under the License is distributed on an
       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
       KIND, either express or implied.  See the License for the
       specific language governing permissions and limitations
       under the License.
*/

var emulator = require('./emulator');
var args = process.argv;

var install_target;
if (args.length > 2) {
    if (args[2].substring(0, 9) === '--target=') {
        install_target = args[2].substring(9, args[2].length);
    } else {
        console.error('ERROR : argument \'' + args[2] + '\' not recognized.');
        process.exit(2);
    }
}

emulator.install(install_target).catch(function (err) {
    console.error('ERROR: ' + err);
    process.exit(2);
});


code:
:: Licensed to the Apache Software Foundation (ASF) under one
:: or more contributor license agreements.  See the NOTICE file
:: distributed with this work for additional information
:: regarding copyright ownership.  The ASF licenses this file
:: to you under the Apache License, Version 2.0 (the
:: "License"); you may not use this file except in compliance
:: with the License.  You may obtain a copy of the License at
::
:: http://www.apache.org/licenses/LICENSE-2.0
::
:: Unless required by applicable law or agreed to in writing,
:: software distributed under the License is distributed on an
:: "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
:: KIND, either express or implied.  See the License for the
:: specific language governing permissions and limitations
:: under the License.

@ECHO OFF
SET script_path="%~dp0install-emulator"
IF EXIST %script_path% (
        node %script_path% %*
) ELSE (
    ECHO.
    ECHO ERROR: Could not find 'install-emulator' script in 'cordova\lib' folder, aborting...>&2
    EXIT /B 1
)


code:
#!/usr/bin/env node

/*
       Licensed to the Apache Software Foundation (ASF) under one
       or more contributor license agreements.  See the NOTICE file
       distributed with this work for additional information
       regarding copyright ownership.  The ASF licenses this file
       to you under the Apache License, Version 2.0 (the
       "License"); you may not use this file except in compliance
       with the License.  You may obtain a copy of the License at

         http://www.apache.org/licenses/LICENSE-2.0

       Unless required by applicable law or agreed to in writing,
       software distributed under the License is distributed on an
       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
       KIND, either express or implied.  See the License for the
       specific language governing permissions and limitations
       under the License.
*/

var devices = require('./device');

// Usage support for when args are given
require('./check_reqs').check_android().then(function () {
    devices.list().then(function (device_list) {
        device_list && device_list.forEach(function (dev) {
            console.log(dev);
        });
    }, function (err) {
        console.error('ERROR: ' + err);
        process.exit(2);
    });
});


code:
:: Licensed to the Apache Software Foundation (ASF) under one
:: or more contributor license agreements.  See the NOTICE file
:: distributed with this work for additional information
:: regarding copyright ownership.  The ASF licenses this file
:: to you under the Apache License, Version 2.0 (the
:: "License"); you may not use this file except in compliance
:: with the License.  You may obtain a copy of the License at
::
:: http://www.apache.org/licenses/LICENSE-2.0
::
:: Unless required by applicable law or agreed to in writing,
:: software distributed under the License is distributed on an
:: "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
:: KIND, either express or implied.  See the License for the
:: specific language governing permissions and limitations
:: under the License.

@ECHO OFF
SET script_path="%~dp0list-devices"
IF EXIST %script_path% (
        node %script_path% %*
) ELSE (
    ECHO.
    ECHO ERROR: Could not find 'list-devices' script in 'cordova\lib' folder, aborting...>&2
    EXIT /B 1
)


code:
#!/usr/bin/env node

/*
       Licensed to the Apache Software Foundation (ASF) under one
       or more contributor license agreements.  See the NOTICE file
       distributed with this work for additional information
       regarding copyright ownership.  The ASF licenses this file
       to you under the Apache License, Version 2.0 (the
       "License"); you may not use this file except in compliance
       with the License.  You may obtain a copy of the License at

         http://www.apache.org/licenses/LICENSE-2.0

       Unless required by applicable law or agreed to in writing,
       software distributed under the License is distributed on an
       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
       KIND, either express or implied.  See the License for the
       specific language governing permissions and limitations
       under the License.
*/

var emulators = require('./emulator');

// Usage support for when args are given
require('./check_reqs').check_android().then(function () {
    emulators.list_images().then(function (emulator_list) {
        emulator_list && emulator_list.forEach(function (emu) {
            console.log(emu.name);
        });
    }, function (err) {
        console.error('ERROR: ' + err);
        process.exit(2);
    });
});


code:
:: Licensed to the Apache Software Foundation (ASF) under one
:: or more contributor license agreements.  See the NOTICE file
:: distributed with this work for additional information
:: regarding copyright ownership.  The ASF licenses this file
:: to you under the Apache License, Version 2.0 (the
:: "License"); you may not use this file except in compliance
:: with the License.  You may obtain a copy of the License at
::
:: http://www.apache.org/licenses/LICENSE-2.0
::
:: Unless required by applicable law or agreed to in writing,
:: software distributed under the License is distributed on an
:: "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
:: KIND, either express or implied.  See the License for the
:: specific language governing permissions and limitations
:: under the License.

@ECHO OFF
SET script_path="%~dp0list-emulator-images"
IF EXIST %script_path% (
        node %script_path% %*
) ELSE (
    ECHO.
    ECHO ERROR: Could not find 'list-emulator-images' script in 'cordova\lib' folder, aborting...>&2
    EXIT /B 1
)


code:
#!/usr/bin/env node

/*
       Licensed to the Apache Software Foundation (ASF) under one
       or more contributor license agreements.  See the NOTICE file
       distributed with this work for additional information
       regarding copyright ownership.  The ASF licenses this file
       to you under the Apache License, Version 2.0 (the
       "License"); you may not use this file except in compliance
       with the License.  You may obtain a copy of the License at

         http://www.apache.org/licenses/LICENSE-2.0

       Unless required by applicable law or agreed to in writing,
       software distributed under the License is distributed on an
       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
       KIND, either express or implied.  See the License for the
       specific language governing permissions and limitations
       under the License.
*/

var emulators = require('./emulator');

// Usage support for when args are given
require('./check_reqs').check_android().then(function () {
    emulators.list_started().then(function (emulator_list) {
        emulator_list && emulator_list.forEach(function (emu) {
            console.log(emu);
        });
    }, function (err) {
        console.error('ERROR: ' + err);
        process.exit(2);
    });
});


code:
:: Licensed to the Apache Software Foundation (ASF) under one
:: or more contributor license agreements.  See the NOTICE file
:: distributed with this work for additional information
:: regarding copyright ownership.  The ASF licenses this file
:: to you under the Apache License, Version 2.0 (the
:: "License"); you may not use this file except in compliance
:: with the License.  You may obtain a copy of the License at
::
:: http://www.apache.org/licenses/LICENSE-2.0
::
:: Unless required by applicable law or agreed to in writing,
:: software distributed under the License is distributed on an
:: "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
:: KIND, either express or implied.  See the License for the
:: specific language governing permissions and limitations
:: under the License.

@ECHO OFF
SET script_path="%~dp0list-started-emulators"
IF EXIST %script_path% (
        node %script_path% %*
) ELSE (
    ECHO.
    ECHO ERROR: Could not find 'list-started-emulators' script in 'cordova\lib' folder, aborting...>&2
    EXIT /B 1
)


code:
#!/usr/bin/env node

/*
       Licensed to the Apache Software Foundation (ASF) under one
       or more contributor license agreements.  See the NOTICE file
       distributed with this work for additional information
       regarding copyright ownership.  The ASF licenses this file
       to you under the Apache License, Version 2.0 (the
       "License"); you may not use this file except in compliance
       with the License.  You may obtain a copy of the License at

         http://www.apache.org/licenses/LICENSE-2.0

       Unless required by applicable law or agreed to in writing,
       software distributed under the License is distributed on an
       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
       KIND, either express or implied.  See the License for the
       specific language governing permissions and limitations
       under the License.
*/

var path = require('path');
var os = require('os');
var Q = require('q');
var child_process = require('child_process');
var ROOT = path.join(__dirname, '..', '..');

/*
 * Starts running logcat in the shell.
 * Returns a promise.
 */
module.exports.run = function () {
    var d = Q.defer();
    var adb = child_process.spawn('adb', ['logcat'], { cwd: os.tmpdir() });

    adb.stdout.on('data', function (data) {
        var lines = data ? data.toString().split('\n') : [];
        var out = lines.filter(function (x) { return x.indexOf('nativeGetEnabledTags') < 0; });
        console.log(out.join('\n'));
    });

    adb.stderr.on('data', console.error);
    adb.on('close', function (code) {
        if (code > 0) {
            d.reject('Failed to run logcat command.');
        } else d.resolve();
    });

    return d.promise;
};

module.exports.help = function () {
    console.log('Usage: ' + path.relative(process.cwd(), path.join(ROOT, 'cordova', 'log')));
    console.log('Gives the logcat output on the command line.');
    process.exit(0);
};


code:
/* Licensed to the Apache Software Foundation (ASF) under one
   or more contributor license agreements.  See the NOTICE file
   distributed with this work for additional information
   regarding copyright ownership.  The ASF licenses this file
   to you under the Apache License, Version 2.0 (the
   "License"); you may not use this file except in compliance
   with the License.  You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing,
   software distributed under the License is distributed on an
   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   KIND, either express or implied.  See the License for the
   specific language governing permissions and limitations
   under the License.
*/

// GENERATED FILE! DO NOT EDIT!

buildscript {
    repositories {
        google()
        jcenter()
    }

    // Switch the Android Gradle plugin version requirement depending on the
    // installed version of Gradle. This dependency is documented at
    // http://tools.android.com/tech-docs/new-build-system/version-compatibility
    // and https://issues.apache.org/jira/browse/CB-8143
    dependencies {
        classpath 'com.android.tools.build:gradle:1.0.0+'
    }
}

apply plugin: 'com.android.library'

dependencies {
    implementation fileTree(dir: 'libs', include: '*.jar')
    debugCompile project(path: ":CordovaLib", configuration: "debug")
    releaseCompile project(path: ":CordovaLib", configuration: "release")
}

android {
    compileSdkVersion cdvCompileSdkVersion
    buildToolsVersion cdvBuildToolsVersion

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_6
        targetCompatibility JavaVersion.VERSION_1_6
    }

    sourceSets {
        main {
            manifest.srcFile 'AndroidManifest.xml'
            java.srcDirs = ['src']
            resources.srcDirs = ['src']
            aidl.srcDirs = ['src']
            renderscript.srcDirs = ['src']
            res.srcDirs = ['res']
            assets.srcDirs = ['assets']
            jniLibs.srcDirs = ['libs']
        }
    }
}

if (file('build-extras.gradle').exists()) {
    apply from: 'build-extras.gradle'
}


code:
/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
*/

var fs = require('fs');
var path = require('path');
var shell = require('shelljs');
var events = require('cordova-common').events;
var CordovaError = require('cordova-common').CordovaError;

var handlers = {
    'source-file': {
        install: function (obj, plugin, project, options) {
            if (!obj.src) throw new CordovaError(generateAttributeError('src', 'source-file', plugin.id));
            if (!obj.targetDir) throw new CordovaError(generateAttributeError('target-dir', 'source-file', plugin.id));

            var dest = getInstallDestination(obj);

            if (options && options.force) {
                copyFile(plugin.dir, obj.src, project.projectDir, dest, !!(options && options.link));
            } else {
                copyNewFile(plugin.dir, obj.src, project.projectDir, dest, !!(options && options.link));
            }
        },
        uninstall: function (obj, plugin, project, options) {
            var dest = getInstallDestination(obj);

            // TODO: Add Koltin extension to uninstall, since they are handled like Java files
            if (obj.src.endsWith('java')) {
                deleteJava(project.projectDir, dest);
            } else {
                // Just remove the file, not the whole parent directory
                removeFile(project.projectDir, dest);
            }
        }
    },
    'lib-file': {
        install: function (obj, plugin, project, options) {
            var dest = path.join('app/libs', path.basename(obj.src));
            copyFile(plugin.dir, obj.src, project.projectDir, dest, !!(options && options.link));
        },
        uninstall: function (obj, plugin, project, options) {
            var dest = path.join('app/libs', path.basename(obj.src));
            removeFile(project.projectDir, dest);
        }
    },
    'resource-file': {
        install: function (obj, plugin, project, options) {
            var dest = path.join('app', 'src', 'main', obj.target);
            copyFile(plugin.dir, obj.src, project.projectDir, dest, !!(options && options.link));
        },
        uninstall: function (obj, plugin, project, options) {
            var dest = path.join('app', 'src', 'main', obj.target);
            removeFile(project.projectDir, dest);
        }
    },
    'framework': {
        install: function (obj, plugin, project, options) {
            var src = obj.src;
            if (!src) throw new CordovaError(generateAttributeError('src', 'framework', plugin.id));

            events.emit('verbose', 'Installing Android library: ' + src);
            var parentDir = obj.parent ? path.resolve(project.projectDir, obj.parent) : project.projectDir;
            var subDir;

            if (obj.custom) {
                var subRelativeDir = project.getCustomSubprojectRelativeDir(plugin.id, src);
                copyNewFile(plugin.dir, src, project.projectDir, subRelativeDir, !!(options && options.link));
                subDir = path.resolve(project.projectDir, subRelativeDir);
            } else {
                obj.type = 'sys';
                subDir = src;
            }

            if (obj.type === 'gradleReference') {
                project.addGradleReference(parentDir, subDir);
            } else if (obj.type === 'sys') {
                project.addSystemLibrary(parentDir, subDir);
            } else {
                project.addSubProject(parentDir, subDir);
            }
        },
        uninstall: function (obj, plugin, project, options) {
            var src = obj.src;
            if (!src) throw new CordovaError(generateAttributeError('src', 'framework', plugin.id));

            events.emit('verbose', 'Uninstalling Android library: ' + src);
            var parentDir = obj.parent ? path.resolve(project.projectDir, obj.parent) : project.projectDir;
            var subDir;

            if (obj.custom) {
                var subRelativeDir = project.getCustomSubprojectRelativeDir(plugin.id, src);
                removeFile(project.projectDir, subRelativeDir);
                subDir = path.resolve(project.projectDir, subRelativeDir);
                // If it's the last framework in the plugin, remove the parent directory.
                var parDir = path.dirname(subDir);
                if (fs.existsSync(parDir) && fs.readdirSync(parDir).length === 0) {
                    fs.rmdirSync(parDir);
                }
            } else {
                obj.type = 'sys';
                subDir = src;
            }

            if (obj.type === 'gradleReference') {
                project.removeGradleReference(parentDir, subDir);
            } else if (obj.type === 'sys') {
                project.removeSystemLibrary(parentDir, subDir);
            } else {
                project.removeSubProject(parentDir, subDir);
            }
        }
    },
    asset: {
        install: function (obj, plugin, project, options) {
            if (!obj.src) {
                throw new CordovaError(generateAttributeError('src', 'asset', plugin.id));
            }
            if (!obj.target) {
                throw new CordovaError(generateAttributeError('target', 'asset', plugin.id));
            }

            copyFile(plugin.dir, obj.src, project.www, obj.target);
            if (options && options.usePlatformWww) {
                // CB-11022 copy file to both directories if usePlatformWww is specified
                copyFile(plugin.dir, obj.src, project.platformWww, obj.target);
            }
        },
        uninstall: function (obj, plugin, project, options) {
            var target = obj.target || obj.src;

            if (!target) throw new CordovaError(generateAttributeError('target', 'asset', plugin.id));

            removeFileF(path.resolve(project.www, target));
            removeFileF(path.resolve(project.www, 'plugins', plugin.id));
            if (options && options.usePlatformWww) {
                // CB-11022 remove file from both directories if usePlatformWww is specified
                removeFileF(path.resolve(project.platformWww, target));
                removeFileF(path.resolve(project.platformWww, 'plugins', plugin.id));
            }
        }
    },
    'js-module': {
        install: function (obj, plugin, project, options) {
            // Copy the plugin's files into the www directory.
            var moduleSource = path.resolve(plugin.dir, obj.src);
            var moduleName = plugin.id + '.' + (obj.name || path.basename(obj.src, path.extname(obj.src)));

            // Read in the file, prepend the cordova.define, and write it back out.
            var scriptContent = fs.readFileSync(moduleSource, 'utf-8').replace(/^\ufeff/, ''); // Window BOM
            if (moduleSource.match(/.*\.json$/)) {
                scriptContent = 'module.exports = ' + scriptContent;
            }
            scriptContent = 'cordova.define("' + moduleName + '", function(require, exports, module) {\n' + scriptContent + '\n});\n';

            var wwwDest = path.resolve(project.www, 'plugins', plugin.id, obj.src);
            shell.mkdir('-p', path.dirname(wwwDest));
            fs.writeFileSync(wwwDest, scriptContent, 'utf-8');

            if (options && options.usePlatformWww) {
                // CB-11022 copy file to both directories if usePlatformWww is specified
                var platformWwwDest = path.resolve(project.platformWww, 'plugins', plugin.id, obj.src);
                shell.mkdir('-p', path.dirname(platformWwwDest));
                fs.writeFileSync(platformWwwDest, scriptContent, 'utf-8');
            }
        },
        uninstall: function (obj, plugin, project, options) {
            var pluginRelativePath = path.join('plugins', plugin.id, obj.src);
            removeFileAndParents(project.www, pluginRelativePath);
            if (options && options.usePlatformWww) {
                // CB-11022 remove file from both directories if usePlatformWww is specified
                removeFileAndParents(project.platformWww, pluginRelativePath);
            }
        }
    }
};

module.exports.getInstaller = function (type) {
    if (handlers[type] && handlers[type].install) {
        return handlers[type].install;
    }

    events.emit('verbose', '<' + type + '> is not supported for android plugins');
};

module.exports.getUninstaller = function (type) {
    if (handlers[type] && handlers[type].uninstall) {
        return handlers[type].uninstall;
    }

    events.emit('verbose', '<' + type + '> is not supported for android plugins');
};

function copyFile (plugin_dir, src, project_dir, dest, link) {
    src = path.resolve(plugin_dir, src);
    if (!fs.existsSync(src)) throw new CordovaError('"' + src + '" not found!');

    // check that src path is inside plugin directory
    var real_path = fs.realpathSync(src);
    var real_plugin_path = fs.realpathSync(plugin_dir);
    if (real_path.indexOf(real_plugin_path) !== 0) { throw new CordovaError('File "' + src + '" is located outside the plugin directory "' + plugin_dir + '"'); }

    dest = path.resolve(project_dir, dest);

    // check that dest path is located in project directory
    if (dest.indexOf(project_dir) !== 0) { throw new CordovaError('Destination "' + dest + '" for source file "' + src + '" is located outside the project'); }

    shell.mkdir('-p', path.dirname(dest));
    if (link) {
        symlinkFileOrDirTree(src, dest);
    } else if (fs.statSync(src).isDirectory()) {
        // XXX shelljs decides to create a directory when -R|-r is used which sucks. http://goo.gl/nbsjq
        shell.cp('-Rf', src + '/*', dest);
    } else {
        shell.cp('-f', src, dest);
    }
}

// Same as copy file but throws error if target exists
function copyNewFile (plugin_dir, src, project_dir, dest, link) {
    var target_path = path.resolve(project_dir, dest);
    if (fs.existsSync(target_path)) { throw new CordovaError('"' + target_path + '" already exists!'); }

    copyFile(plugin_dir, src, project_dir, dest, !!link);
}

function symlinkFileOrDirTree (src, dest) {
    if (fs.existsSync(dest)) {
        shell.rm('-Rf', dest);
    }

    if (fs.statSync(src).isDirectory()) {
        shell.mkdir('-p', dest);
        fs.readdirSync(src).forEach(function (entry) {
            symlinkFileOrDirTree(path.join(src, entry), path.join(dest, entry));
        });
    } else {
        fs.symlinkSync(path.relative(fs.realpathSync(path.dirname(dest)), src), dest);
    }
}

// checks if file exists and then deletes. Error if doesn't exist
function removeFile (project_dir, src) {
    var file = path.resolve(project_dir, src);
    shell.rm('-Rf', file);
}

// deletes file/directory without checking
function removeFileF (file) {
    shell.rm('-Rf', file);
}

// Sometimes we want to remove some java, and prune any unnecessary empty directories
function deleteJava (project_dir, destFile) {
    removeFileAndParents(project_dir, destFile, 'src');
}

function removeFileAndParents (baseDir, destFile, stopper) {
    stopper = stopper || '.';
    var file = path.resolve(baseDir, destFile);
    if (!fs.existsSync(file)) return;

    removeFileF(file);

    // check if directory is empty
    var curDir = path.dirname(file);

    while (curDir !== path.resolve(baseDir, stopper)) {
        if (fs.existsSync(curDir) && fs.readdirSync(curDir).length === 0) {
            fs.rmdirSync(curDir);
            curDir = path.resolve(curDir, '..');
        } else {
            // directory not empty...do nothing
            break;
        }
    }
}

function generateAttributeError (attribute, element, id) {
    return 'Required attribute "' + attribute + '" not specified in <' + element + '> element from plugin: ' + id;
}

function getInstallDestination (obj) {
    var APP_MAIN_PREFIX = 'app/src/main';
    var PATH_SEPARATOR = '/';

    var PATH_SEP_MATCH = '\\' + PATH_SEPARATOR;
    var PATH_SEP_OR_EOL_MATCH = '(\\' + PATH_SEPARATOR + '|$)';

    var appReg = new RegExp('^app' + PATH_SEP_OR_EOL_MATCH);
    var libsReg = new RegExp('^libs' + PATH_SEP_OR_EOL_MATCH);
    var srcReg = new RegExp('^src' + PATH_SEP_OR_EOL_MATCH);
    var srcMainReg = new RegExp('^src' + PATH_SEP_MATCH + 'main' + PATH_SEP_OR_EOL_MATCH);

    if (appReg.test(obj.targetDir)) {
        // If any source file is using the new app directory structure,
        // don't penalize it
        return path.join(obj.targetDir, path.basename(obj.src));
    } else {
        // Plugin using deprecated target directory structure (GH-580)
        if (obj.src.endsWith('.java')) {
            return path.join(APP_MAIN_PREFIX, 'java', obj.targetDir.replace(srcReg, ''),
                path.basename(obj.src));
        } else if (obj.src.endsWith('.aidl')) {
            return path.join(APP_MAIN_PREFIX, 'aidl', obj.targetDir.replace(srcReg, ''),
                path.basename(obj.src));
        } else if (libsReg.test(obj.targetDir)) {
            if (obj.src.endsWith('.so')) {
                return path.join(APP_MAIN_PREFIX, 'jniLibs', obj.targetDir.replace(libsReg, ''),
                    path.basename(obj.src));
            } else {
                return path.join('app', obj.targetDir, path.basename(obj.src));
            }
        } else if (srcMainReg.test(obj.targetDir)) {
            return path.join('app', obj.targetDir, path.basename(obj.src));
        }

        // For all other source files not using the new app directory structure,
        // add 'app/src/main' to the targetDir
        return path.join(APP_MAIN_PREFIX, obj.targetDir, path.basename(obj.src));
    }
}


code:
/**
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
*/
/* eslint no-useless-escape: 0 */

var Q = require('q');
var fs = require('fs');
var path = require('path');
var shell = require('shelljs');
var events = require('cordova-common').events;
var AndroidManifest = require('./AndroidManifest');
var checkReqs = require('./check_reqs');
var xmlHelpers = require('cordova-common').xmlHelpers;
var CordovaError = require('cordova-common').CordovaError;
var ConfigParser = require('cordova-common').ConfigParser;
var FileUpdater = require('cordova-common').FileUpdater;
var PlatformJson = require('cordova-common').PlatformJson;
var PlatformMunger = require('cordova-common').ConfigChanges.PlatformMunger;
var PluginInfoProvider = require('cordova-common').PluginInfoProvider;

const GradlePropertiesParser = require('./config/GradlePropertiesParser');

module.exports.prepare = function (cordovaProject, options) {
    var self = this;

    var platformJson = PlatformJson.load(this.locations.root, this.platform);
    var munger = new PlatformMunger(this.platform, this.locations.root, platformJson, new PluginInfoProvider());

    this._config = updateConfigFilesFrom(cordovaProject.projectConfig, munger, this.locations);

    // Get the min SDK version from config.xml
    const minSdkVersion = this._config.getPreference('android-minSdkVersion', 'android');
    const maxSdkVersion = this._config.getPreference('android-maxSdkVersion', 'android');
    const targetSdkVersion = this._config.getPreference('android-targetSdkVersion', 'android');

    let gradlePropertiesUserConfig = {};
    if (minSdkVersion) gradlePropertiesUserConfig.cdvMinSdkVersion = minSdkVersion;
    if (maxSdkVersion) gradlePropertiesUserConfig.cdvMaxSdkVersion = maxSdkVersion;
    if (targetSdkVersion) gradlePropertiesUserConfig.cdvTargetSdkVersion = targetSdkVersion;

    let gradlePropertiesParser = new GradlePropertiesParser(this.locations.root);
    gradlePropertiesParser.configure(gradlePropertiesUserConfig);

    // Update own www dir with project's www assets and plugins' assets and js-files
    return Q.when(updateWww(cordovaProject, this.locations)).then(function () {
        // update project according to config.xml changes.
        return updateProjectAccordingTo(self._config, self.locations);
    }).then(function () {
        updateIcons(cordovaProject, path.relative(cordovaProject.root, self.locations.res));
        updateSplashes(cordovaProject, path.relative(cordovaProject.root, self.locations.res));
        updateFileResources(cordovaProject, path.relative(cordovaProject.root, self.locations.root));
    }).then(function () {
        events.emit('verbose', 'Prepared android project successfully');
    });
};

module.exports.clean = function (options) {
    // A cordovaProject isn't passed into the clean() function, because it might have
    // been called from the platform shell script rather than the CLI. Check for the
    // noPrepare option passed in by the non-CLI clean script. If that's present, or if
    // there's no config.xml found at the project root, then don't clean prepared files.
    var projectRoot = path.resolve(this.root, '../..');
    if ((options && options.noPrepare) || !fs.existsSync(this.locations.configXml) ||
            !fs.existsSync(this.locations.configXml)) {
        return Q();
    }

    var projectConfig = new ConfigParser(this.locations.configXml);

    var self = this;
    return Q().then(function () {
        cleanWww(projectRoot, self.locations);
        cleanIcons(projectRoot, projectConfig, path.relative(projectRoot, self.locations.res));
        cleanSplashes(projectRoot, projectConfig, path.relative(projectRoot, self.locations.res));
        cleanFileResources(projectRoot, projectConfig, path.relative(projectRoot, self.locations.root));
    });
};

/**
 * Updates config files in project based on app's config.xml and config munge,
 *   generated by plugins.
 *
 * @param   {ConfigParser}   sourceConfig  A project's configuration that will
 *   be merged into platform's config.xml
 * @param   {ConfigChanges}  configMunger  An initialized ConfigChanges instance
 *   for this platform.
 * @param   {Object}         locations     A map of locations for this platform
 *
 * @return  {ConfigParser}                 An instance of ConfigParser, that
 *   represents current project's configuration. When returned, the
 *   configuration is already dumped to appropriate config.xml file.
 */
function updateConfigFilesFrom (sourceConfig, configMunger, locations) {
    events.emit('verbose', 'Generating platform-specific config.xml from defaults for android at ' + locations.configXml);

    // First cleanup current config and merge project's one into own
    // Overwrite platform config.xml with defaults.xml.
    shell.cp('-f', locations.defaultConfigXml, locations.configXml);

    // Then apply config changes from global munge to all config files
    // in project (including project's config)
    configMunger.reapply_global_munge().save_all();

    events.emit('verbose', 'Merging project\'s config.xml into platform-specific android config.xml');
    // Merge changes from app's config.xml into platform's one
    var config = new ConfigParser(locations.configXml);
    xmlHelpers.mergeXml(sourceConfig.doc.getroot(),
        config.doc.getroot(), 'android', /* clobber= */true);

    config.write();
    return config;
}

/**
 * Logs all file operations via the verbose event stream, indented.
 */
function logFileOp (message) {
    events.emit('verbose', '  ' + message);
}

/**
 * Updates platform 'www' directory by replacing it with contents of
 *   'platform_www' and app www. Also copies project's overrides' folder into
 *   the platform 'www' folder
 *
 * @param   {Object}  cordovaProject    An object which describes cordova project.
 * @param   {Object}  destinations      An object that contains destination
 *   paths for www files.
 */
function updateWww (cordovaProject, destinations) {
    var sourceDirs = [
        path.relative(cordovaProject.root, cordovaProject.locations.www),
        path.relative(cordovaProject.root, destinations.platformWww)
    ];

    // If project contains 'merges' for our platform, use them as another overrides
    var merges_path = path.join(cordovaProject.root, 'merges', 'android');
    if (fs.existsSync(merges_path)) {
        events.emit('verbose', 'Found "merges/android" folder. Copying its contents into the android project.');
        sourceDirs.push(path.join('merges', 'android'));
    }

    var targetDir = path.relative(cordovaProject.root, destinations.www);
    events.emit(
        'verbose', 'Merging and updating files from [' + sourceDirs.join(', ') + '] to ' + targetDir);
    FileUpdater.mergeAndUpdateDir(
        sourceDirs, targetDir, { rootDir: cordovaProject.root }, logFileOp);
}

/**
 * Cleans all files from the platform 'www' directory.
 */
function cleanWww (projectRoot, locations) {
    var targetDir = path.relative(projectRoot, locations.www);
    events.emit('verbose', 'Cleaning ' + targetDir);

    // No source paths are specified, so mergeAndUpdateDir() will clear the target directory.
    FileUpdater.mergeAndUpdateDir(
        [], targetDir, { rootDir: projectRoot, all: true }, logFileOp);
}

/**
 * Updates project structure and AndroidManifest according to project's configuration.
 *
 * @param   {ConfigParser}  platformConfig  A project's configuration that will
 *   be used to update project
 * @param   {Object}  locations       A map of locations for this platform
 */
function updateProjectAccordingTo (platformConfig, locations) {
    // Update app name by editing res/values/strings.xml
    var strings = xmlHelpers.parseElementtreeSync(locations.strings);

    var name = platformConfig.name();
    strings.find('string[@name="app_name"]').text = name.replace(/\'/g, '\\\'');

    var shortName = platformConfig.shortName && platformConfig.shortName();
    if (shortName && shortName !== name) {
        strings.find('string[@name="launcher_name"]').text = shortName.replace(/\'/g, '\\\'');
    }

    fs.writeFileSync(locations.strings, strings.write({ indent: 4 }), 'utf-8');
    events.emit('verbose', 'Wrote out android application name "' + name + '" to ' + locations.strings);

    // Java packages cannot support dashes
    var androidPkgName = (platformConfig.android_packageName() || platformConfig.packageName()).replace(/-/g, '_');

    var manifest = new AndroidManifest(locations.manifest);
    var manifestId = manifest.getPackageId();

    manifest.getActivity()
        .setOrientation(platformConfig.getPreference('orientation'))
        .setLaunchMode(findAndroidLaunchModePreference(platformConfig));

    manifest.setVersionName(platformConfig.version())
        .setVersionCode(platformConfig.android_versionCode() || default_versionCode(platformConfig.version()))
        .setPackageId(androidPkgName)
        .write();

    // Java file paths shouldn't be hard coded
    var javaPattern = path.join(locations.javaSrc, manifestId.replace(/\./g, '/'), '*.java');
    var java_files = shell.ls(javaPattern).filter(function (f) {
        return shell.grep(/extends\s+CordovaActivity/g, f);
    });

    if (java_files.length === 0) {
        throw new CordovaError('No Java files found that extend CordovaActivity.');
    } else if (java_files.length > 1) {
        events.emit('log', 'Multiple candidate Java files that extend CordovaActivity found. Guessing at the first one, ' + java_files[0]);
    }

    var destFile = path.join(locations.root, 'app', 'src', 'main', 'java', androidPkgName.replace(/\./g, '/'), path.basename(java_files[0]));
    shell.mkdir('-p', path.dirname(destFile));
    shell.sed(/package [\w\.]*;/, 'package ' + androidPkgName + ';', java_files[0]).to(destFile);
    events.emit('verbose', 'Wrote out Android package name "' + androidPkgName + '" to ' + destFile);

    var removeOrigPkg = checkReqs.isWindows() || checkReqs.isDarwin() ?
        manifestId.toUpperCase() !== androidPkgName.toUpperCase() :
        manifestId !== androidPkgName;

    if (removeOrigPkg) {
        // If package was name changed we need to remove old java with main activity
        shell.rm('-Rf', java_files[0]);
        // remove any empty directories
        var currentDir = path.dirname(java_files[0]);
        var sourcesRoot = path.resolve(locations.root, 'src');
        while (currentDir !== sourcesRoot) {
            if (fs.existsSync(currentDir) && fs.readdirSync(currentDir).length === 0) {
                fs.rmdirSync(currentDir);
                currentDir = path.resolve(currentDir, '..');
            } else {
                break;
            }
        }
    }
}

// Consturct the default value for versionCode as
// PATCH + MINOR * 100 + MAJOR * 10000
// see http://developer.android.com/tools/publishing/versioning.html
function default_versionCode (version) {
    var nums = version.split('-')[0].split('.');
    var versionCode = 0;
    if (+nums[0]) {
        versionCode += +nums[0] * 10000;
    }
    if (+nums[1]) {
        versionCode += +nums[1] * 100;
    }
    if (+nums[2]) {
        versionCode += +nums[2];
    }

    events.emit('verbose', 'android-versionCode not found in config.xml. Generating a code based on version in config.xml (' + version + '): ' + versionCode);
    return versionCode;
}

function getImageResourcePath (resourcesDir, type, density, name, sourceName) {
    if (/\.9\.png$/.test(sourceName)) {
        name = name.replace(/\.png$/, '.9.png');
    }
    var resourcePath = path.join(resourcesDir, (density ? type + '-' + density : type), name);
    return resourcePath;
}

function getAdaptiveImageResourcePath (resourcesDir, type, density, name, sourceName) {
    if (/\.9\.png$/.test(sourceName)) {
        name = name.replace(/\.png$/, '.9.png');
    }
    var resourcePath = path.join(resourcesDir, (density ? type + '-' + density + '-v26' : type), name);
    return resourcePath;
}

function updateSplashes (cordovaProject, platformResourcesDir) {
    var resources = cordovaProject.projectConfig.getSplashScreens('android');

    // if there are "splash" elements in config.xml
    if (resources.length === 0) {
        events.emit('verbose', 'This app does not have splash screens defined');
        return;
    }

    var resourceMap = mapImageResources(cordovaProject.root, platformResourcesDir, 'drawable', 'screen.png');

    var hadMdpi = false;
    resources.forEach(function (resource) {
        if (!resource.density) {
            return;
        }
        if (resource.density === 'mdpi') {
            hadMdpi = true;
        }
        var targetPath = getImageResourcePath(
            platformResourcesDir, 'drawable', resource.density, 'screen.png', path.basename(resource.src));
        resourceMap[targetPath] = resource.src;
    });

    // There's no "default" drawable, so assume default == mdpi.
    if (!hadMdpi && resources.defaultResource) {
        var targetPath = getImageResourcePath(
            platformResourcesDir, 'drawable', 'mdpi', 'screen.png', path.basename(resources.defaultResource.src));
        resourceMap[targetPath] = resources.defaultResource.src;
    }

    events.emit('verbose', 'Updating splash screens at ' + platformResourcesDir);
    FileUpdater.updatePaths(
        resourceMap, { rootDir: cordovaProject.root }, logFileOp);
}

function cleanSplashes (projectRoot, projectConfig, platformResourcesDir) {
    var resources = projectConfig.getSplashScreens('android');
    if (resources.length > 0) {
        var resourceMap = mapImageResources(projectRoot, platformResourcesDir, 'drawable', 'screen.png');
        events.emit('verbose', 'Cleaning splash screens at ' + platformResourcesDir);

        // No source paths are specified in the map, so updatePaths() will delete the target files.
        FileUpdater.updatePaths(
            resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
    }
}

function updateIcons (cordovaProject, platformResourcesDir) {
    let icons = cordovaProject.projectConfig.getIcons('android');

    // Skip if there are no app defined icons in config.xml
    if (icons.length === 0) {
        events.emit('verbose', 'This app does not have launcher icons defined');
        return;
    }

    // 1. loop icons determin if there is an error in the setup.
    // 2. during initial loop, also setup for legacy support.
    let errorMissingAttributes = [];
    let errorLegacyIconNeeded = [];
    let hasAdaptive = false;
    icons.forEach((icon, key) => {
        if (
            (icon.background && !icon.foreground)
            || (!icon.background && icon.foreground)
            || (!icon.background && !icon.foreground && !icon.src)
        ) {
            errorMissingAttributes.push(icon.density ? icon.density : 'size=' + (icon.height || icon.width));
        }

        if (icon.foreground) {
            hasAdaptive = true;

            if (
                !icon.src
                && (
                    icon.foreground.startsWith('@color')
                    || path.extname(path.basename(icon.foreground)) === '.xml'
                )
            ) {
                errorLegacyIconNeeded.push(icon.density ? icon.density : 'size=' + (icon.height || icon.width));
            } else if (!icon.src) {
                icons[key].src = icon.foreground;
            }
        }
    });

    let errorMessage = [];
    if (errorMissingAttributes.length > 0) {
        errorMessage.push('One of the following attributes are set but missing the other for the density type: ' + errorMissingAttributes.join(', ') + '. Please ensure that all require attributes are defined.');
    }

    if (errorLegacyIconNeeded.length > 0) {
        errorMessage.push('For the following icons with the density of: ' + errorLegacyIconNeeded.join(', ') + ', adaptive foreground with a defined color or vector can not be used as a standard fallback icon for older Android devices. To support older Android environments, please provide a value for the src attribute.');
    }

    if (errorMessage.length > 0) {
        throw new CordovaError(errorMessage.join(' '));
    }

    let resourceMap = Object.assign(
        {},
        mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher.png'),
        mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher_foreground.png'),
        mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher_background.png'),
        mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher_foreground.xml'),
        mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher_background.xml'),
        mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher.xml')
    );

    let preparedIcons = prepareIcons(icons);

    if (hasAdaptive) {
        resourceMap = updateIconResourceForAdaptive(preparedIcons, resourceMap, platformResourcesDir);
    }

    resourceMap = updateIconResourceForLegacy(preparedIcons, resourceMap, platformResourcesDir);

    events.emit('verbose', 'Updating icons at ' + platformResourcesDir);
    FileUpdater.updatePaths(resourceMap, { rootDir: cordovaProject.root }, logFileOp);
}

function updateIconResourceForAdaptive (preparedIcons, resourceMap, platformResourcesDir) {
    let android_icons = preparedIcons.android_icons;
    let default_icon = preparedIcons.default_icon;

    // The source paths for icons and splashes are relative to
    // project's config.xml location, so we use it as base path.
    let background;
    let foreground;
    let targetPathBackground;
    let targetPathForeground;

    for (let density in android_icons) {
        let backgroundVal = '@mipmap/ic_launcher_background';
        let foregroundVal = '@mipmap/ic_launcher_foreground';

        background = android_icons[density].background;
        foreground = android_icons[density].foreground;

        if (background.startsWith('@color')) {
            // Colors Use Case
            backgroundVal = background; // Example: @color/background_foobar_1
        } else if (path.extname(path.basename(background)) === '.xml') {
            // Vector Use Case
            targetPathBackground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', density, 'ic_launcher_background.xml', path.basename(android_icons[density].background));
            resourceMap[targetPathBackground] = android_icons[density].background;
        } else if (path.extname(path.basename(background)) === '.png') {
            // Images Use Case
            targetPathBackground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', density, 'ic_launcher_background.png', path.basename(android_icons[density].background));
            resourceMap[targetPathBackground] = android_icons[density].background;
        }

        if (foreground.startsWith('@color')) {
            // Colors Use Case
            foregroundVal = foreground;
        } else if (path.extname(path.basename(foreground)) === '.xml') {
            // Vector Use Case
            targetPathForeground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', density, 'ic_launcher_foreground.xml', path.basename(android_icons[density].foreground));
            resourceMap[targetPathForeground] = android_icons[density].foreground;
        } else if (path.extname(path.basename(foreground)) === '.png') {
            // Images Use Case
            targetPathForeground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', density, 'ic_launcher_foreground.png', path.basename(android_icons[density].foreground));
            resourceMap[targetPathForeground] = android_icons[density].foreground;
        }

        // create an XML for DPI and set color
        const icLauncherTemplate = `<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="` + backgroundVal + `" />
    <foreground android:drawable="` + foregroundVal + `" />
</adaptive-icon>`;

        let launcherXmlPath = path.join(platformResourcesDir, 'mipmap-' + density + '-v26', 'ic_launcher.xml');

        // Remove the XML from the resourceMap so the file does not get removed.
        delete resourceMap[launcherXmlPath];

        fs.writeFileSync(path.resolve(launcherXmlPath), icLauncherTemplate);
    }

    // There's no "default" drawable, so assume default == mdpi.
    if (default_icon && !android_icons.mdpi) {
        let defaultTargetPathBackground;
        let defaultTargetPathForeground;

        if (background.startsWith('@color')) {
            // Colors Use Case
            targetPathBackground = default_icon.background;
        } else if (path.extname(path.basename(background)) === '.xml') {
            // Vector Use Case
            defaultTargetPathBackground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher_background.xml', path.basename(default_icon.background));
            resourceMap[defaultTargetPathBackground] = default_icon.background;
        } else if (path.extname(path.basename(background)) === '.png') {
            // Images Use Case
            defaultTargetPathBackground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher_background.png', path.basename(default_icon.background));
            resourceMap[defaultTargetPathBackground] = default_icon.background;
        }

        if (foreground.startsWith('@color')) {
            // Colors Use Case
            targetPathForeground = default_icon.foreground;
        } else if (path.extname(path.basename(foreground)) === '.xml') {
            // Vector Use Case
            defaultTargetPathForeground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher_foreground.xml', path.basename(default_icon.foreground));
            resourceMap[defaultTargetPathForeground] = default_icon.foreground;
        } else if (path.extname(path.basename(foreground)) === '.png') {
            // Images Use Case
            defaultTargetPathForeground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher_foreground.png', path.basename(default_icon.foreground));
            resourceMap[defaultTargetPathForeground] = default_icon.foreground;
        }
    }

    return resourceMap;
}

function updateIconResourceForLegacy (preparedIcons, resourceMap, platformResourcesDir) {
    let android_icons = preparedIcons.android_icons;
    let default_icon = preparedIcons.default_icon;

    // The source paths for icons and splashes are relative to
    // project's config.xml location, so we use it as base path.
    for (var density in android_icons) {
        var targetPath = getImageResourcePath(platformResourcesDir, 'mipmap', density, 'ic_launcher.png', path.basename(android_icons[density].src));
        resourceMap[targetPath] = android_icons[density].src;
    }

    // There's no "default" drawable, so assume default == mdpi.
    if (default_icon && !android_icons.mdpi) {
        var defaultTargetPath = getImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher.png', path.basename(default_icon.src));
        resourceMap[defaultTargetPath] = default_icon.src;
    }

    return resourceMap;
}

function prepareIcons (icons) {
    // http://developer.android.com/design/style/iconography.html
    const SIZE_TO_DENSITY_MAP = {
        36: 'ldpi',
        48: 'mdpi',
        72: 'hdpi',
        96: 'xhdpi',
        144: 'xxhdpi',
        192: 'xxxhdpi'
    };

    let android_icons = {};
    let default_icon;

    // find the best matching icon for a given density or size
    // @output android_icons
    var parseIcon = function (icon, icon_size) {
        // do I have a platform icon for that density already
        var density = icon.density || SIZE_TO_DENSITY_MAP[icon_size];
        if (!density) {
            // invalid icon defition ( or unsupported size)
            return;
        }
        var previous = android_icons[density];
        if (previous && previous.platform) {
            return;
        }
        android_icons[density] = icon;
    };

    // iterate over all icon elements to find the default icon and call parseIcon
    for (var i = 0; i < icons.length; i++) {
        var icon = icons[i];
        var size = icon.width;

        if (!size) {
            size = icon.height;
        }

        if (!size && !icon.density) {
            if (default_icon) {
                let found = {};
                let favor = {};

                // populating found icon.
                if (icon.background && icon.foreground) {
                    found.background = icon.background;
                    found.foreground = icon.foreground;
                }
                if (icon.src) {
                    found.src = icon.src;
                }

                if (default_icon.background && default_icon.foreground) {
                    favor.background = default_icon.background;
                    favor.foreground = default_icon.foreground;
                }
                if (default_icon.src) {
                    favor.src = default_icon.src;
                }

                events.emit('verbose', 'Found extra default icon: ' + JSON.stringify(found) + ' and ignoring in favor of ' + JSON.stringify(favor) + '.');
            } else {
                default_icon = icon;
            }
        } else {
            parseIcon(icon, size);
        }
    }

    return {
        android_icons: android_icons,
        default_icon: default_icon
    };
}

function cleanIcons (projectRoot, projectConfig, platformResourcesDir) {
    var icons = projectConfig.getIcons('android');

    // Skip if there are no app defined icons in config.xml
    if (icons.length === 0) {
        events.emit('verbose', 'This app does not have launcher icons defined');
        return;
    }

    let resourceMap = Object.assign(
        {},
        mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher.png'),
        mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher_foreground.png'),
        mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher_background.png'),
        mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher_foreground.xml'),
        mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher_background.xml'),
        mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher.xml')
    );

    events.emit('verbose', 'Cleaning icons at ' + platformResourcesDir);

    // No source paths are specified in the map, so updatePaths() will delete the target files.
    FileUpdater.updatePaths(resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
}

/**
 * Gets a map containing resources of a specified name from all drawable folders in a directory.
 */
function mapImageResources (rootDir, subDir, type, resourceName) {
    var pathMap = {};
    shell.ls(path.join(rootDir, subDir, type + '-*')).forEach(function (drawableFolder) {
        var imagePath = path.join(subDir, path.basename(drawableFolder), resourceName);
        pathMap[imagePath] = null;
    });
    return pathMap;
}

function updateFileResources (cordovaProject, platformDir) {
    var files = cordovaProject.projectConfig.getFileResources('android');

    // if there are resource-file elements in config.xml
    if (files.length === 0) {
        events.emit('verbose', 'This app does not have additional resource files defined');
        return;
    }

    var resourceMap = {};
    files.forEach(function (res) {
        var targetPath = path.join(platformDir, res.target);
        resourceMap[targetPath] = res.src;
    });

    events.emit('verbose', 'Updating resource files at ' + platformDir);
    FileUpdater.updatePaths(
        resourceMap, { rootDir: cordovaProject.root }, logFileOp);
}

function cleanFileResources (projectRoot, projectConfig, platformDir) {
    var files = projectConfig.getFileResources('android', true);
    if (files.length > 0) {
        events.emit('verbose', 'Cleaning resource files at ' + platformDir);

        var resourceMap = {};
        files.forEach(function (res) {
            var filePath = path.join(platformDir, res.target);
            resourceMap[filePath] = null;
        });

        FileUpdater.updatePaths(
            resourceMap, {
                rootDir: projectRoot, all: true }, logFileOp);
    }
}

/**
 * Gets and validates 'AndroidLaunchMode' prepference from config.xml. Returns
 *   preference value and warns if it doesn't seems to be valid
 *
 * @param   {ConfigParser}  platformConfig  A configParser instance for
 *   platform.
 *
 * @return  {String}                  Preference's value from config.xml or
 *   default value, if there is no such preference. The default value is
 *   'singleTop'
 */
function findAndroidLaunchModePreference (platformConfig) {
    var launchMode = platformConfig.getPreference('AndroidLaunchMode');
    if (!launchMode) {
        // Return a default value
        return 'singleTop';
    }

    var expectedValues = ['standard', 'singleTop', 'singleTask', 'singleInstance'];
    var valid = expectedValues.indexOf(launchMode) >= 0;
    if (!valid) {
        // Note: warn, but leave the launch mode as developer wanted, in case the list of options changes in the future
        events.emit('warn', 'Unrecognized value for AndroidLaunchMode preference: ' +
            launchMode + '. Expected values are: ' + expectedValues.join(', '));
    }

    return launchMode;
}


code:
#!/usr/bin/env node

/*
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
*/

'use strict';

var events = require('cordova-common').events;

/*
 * Retry a promise-returning function a number of times, propagating its
 * results on success or throwing its error on a failed final attempt.
 *
 * @arg {Number}   attemptsLeft    - The number of times to retry the passed call.
 * @arg {Function} promiseFunction - A function that returns a promise.
 * @arg {...}                      - Arguments to pass to promiseFunction.
 *
 * @returns {Promise}
 */
module.exports.retryPromise = function (attemptsLeft, promiseFunction) {

    // NOTE:
    //      get all trailing arguments, by skipping the first two (attemptsLeft and
    //      promiseFunction) because they shouldn't get passed to promiseFunction
    var promiseFunctionArguments = Array.prototype.slice.call(arguments, 2);

    return promiseFunction.apply(undefined, promiseFunctionArguments).then(
        // on success pass results through
        function onFulfilled (value) {
            return value;
        },

        // on rejection either retry, or throw the error
        function onRejected (error) {
            attemptsLeft -= 1;

            if (attemptsLeft < 1) {
                throw error;
            }

            events.emit('verbose', 'A retried call failed. Retrying ' + attemptsLeft + ' more time(s).');

            // retry call self again with the same arguments, except attemptsLeft is now lower
            var fullArguments = [attemptsLeft, promiseFunction].concat(promiseFunctionArguments);
            return module.exports.retryPromise.apply(undefined, fullArguments);
        }
    );
};


code:
#!/usr/bin/env node

/*
       Licensed to the Apache Software Foundation (ASF) under one
       or more contributor license agreements.  See the NOTICE file
       distributed with this work for additional information
       regarding copyright ownership.  The ASF licenses this file
       to you under the Apache License, Version 2.0 (the
       "License"); you may not use this file except in compliance
       with the License.  You may obtain a copy of the License at

         http://www.apache.org/licenses/LICENSE-2.0

       Unless required by applicable law or agreed to in writing,
       software distributed under the License is distributed on an
       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
       KIND, either express or implied.  See the License for the
       specific language governing permissions and limitations
       under the License.
*/

var path = require('path');
var emulator = require('./emulator');
var device = require('./device');
var Q = require('q');
var PackageType = require('./PackageType');
var events = require('cordova-common').events;

function getInstallTarget (runOptions) {
    var install_target;
    if (runOptions.target) {
        install_target = runOptions.target;
    } else if (runOptions.device) {
        install_target = '--device';
    } else if (runOptions.emulator) {
        install_target = '--emulator';
    }

    return install_target;
}

/**
 * Runs the application on a device if available. If no device is found, it will
 *   use a started emulator. If no started emulators are found it will attempt
 *   to start an avd. If no avds are found it will error out.
 *
 * @param   {Object}  runOptions  various run/build options. See Api.js build/run
 *   methods for reference.
 *
 * @return  {Promise}
 */
module.exports.run = function (runOptions) {
    runOptions = runOptions || {};

    var self = this;
    var install_target = getInstallTarget(runOptions);

    return Q().then(function () {
        if (!install_target) {
            // no target given, deploy to device if available, otherwise use the emulator.
            return device.list().then(function (device_list) {
                if (device_list.length > 0) {
                    events.emit('warn', 'No target specified, deploying to device \'' + device_list[0] + '\'.');
                    install_target = device_list[0];
                } else {
                    events.emit('warn', 'No target specified and no devices found, deploying to emulator');
                    install_target = '--emulator';
                }
            });
        }
    }).then(function () {
        if (install_target === '--device') {
            return device.resolveTarget(null);
        } else if (install_target === '--emulator') {
            // Give preference to any already started emulators. Else, start one.
            return emulator.list_started().then(function (started) {
                return started && started.length > 0 ? started[0] : emulator.start();
            }).then(function (emulatorId) {
                return emulator.resolveTarget(emulatorId);
            });
        }
        // They specified a specific device/emulator ID.
        return device.list().then(function (devices) {
            if (devices.indexOf(install_target) > -1) {
                return device.resolveTarget(install_target);
            }
            return emulator.list_started().then(function (started_emulators) {
                if (started_emulators.indexOf(install_target) > -1) {
                    return emulator.resolveTarget(install_target);
                }
                return emulator.list_images().then(function (avds) {
                    // if target emulator isn't started, then start it.
                    for (var avd in avds) {
                        if (avds[avd].name === install_target) {
                            return emulator.start(install_target).then(function (emulatorId) {
                                return emulator.resolveTarget(emulatorId);
                            });
                        }
                    }
                    return Q.reject('Target \'' + install_target + '\' not found, unable to run project');
                });
            });
        });
    }).then(function (resolvedTarget) {
        return new Promise((resolve) => {
            const builder = require('./builders/builders').getBuilder();
            const buildOptions = require('./build').parseBuildOptions(runOptions, null, self.root);

            // Android app bundles cannot be deployed directly to the device
            if (buildOptions.packageType === PackageType.BUNDLE) {
                const packageTypeErrorMessage = 'Package type "bundle" is not supported during cordova run.';
                events.emit('error', packageTypeErrorMessage);
                throw packageTypeErrorMessage;
            }

            resolve(builder.fetchBuildResults(buildOptions.buildType, buildOptions.arch));
        }).then(function (buildResults) {
            if (resolvedTarget && resolvedTarget.isEmulator) {
                return emulator.wait_for_boot(resolvedTarget.target).then(function () {
                    return emulator.install(resolvedTarget, buildResults);
                });
            }

            return device.install(resolvedTarget, buildResults);
        });
    });
};

module.exports.help = function () {
    console.log('Usage: ' + path.relative(process.cwd(), process.argv[1]) + ' [options]');
    console.log('Build options :');
    console.log('    --debug : Builds project in debug mode');
    console.log('    --release : Builds project in release mode');
    console.log('    --nobuild : Runs the currently built project without recompiling');
    console.log('Deploy options :');
    console.log('    --device : Will deploy the built project to a device');
    console.log('    --emulator : Will deploy the built project to an emulator if one exists');
    console.log('    --target=<target_id> : Installs to the target with the specified id.');
    process.exit(0);
};


code:
#!/usr/bin/env node

/*
       Licensed to the Apache Software Foundation (ASF) under one
       or more contributor license agreements.  See the NOTICE file
       distributed with this work for additional information
       regarding copyright ownership.  The ASF licenses this file
       to you under the Apache License, Version 2.0 (the
       "License"); you may not use this file except in compliance
       with the License.  You may obtain a copy of the License at

         http://www.apache.org/licenses/LICENSE-2.0

       Unless required by applicable law or agreed to in writing,
       software distributed under the License is distributed on an
       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
       KIND, either express or implied.  See the License for the
       specific language governing permissions and limitations
       under the License.
*/

var emulator = require('./emulator');
var args = process.argv;

var install_target;
if (args.length > 2) {
    if (args[2].substring(0, 9) === '--target=') {
        install_target = args[2].substring(9, args[2].length);
    } else {
        console.error('ERROR : argument \'' + args[2] + '\' not recognized.');
        process.exit(2);
    }
}

emulator.start(install_target).catch(function (err) {
    console.error('ERROR: ' + err);
    process.exit(2);
});


code:
:: Licensed to the Apache Software Foundation (ASF) under one
:: or more contributor license agreements.  See the NOTICE file
:: distributed with this work for additional information
:: regarding copyright ownership.  The ASF licenses this file
:: to you under the Apache License, Version 2.0 (the
:: "License"); you may not use this file except in compliance
:: with the License.  You may obtain a copy of the License at
::
:: http://www.apache.org/licenses/LICENSE-2.0
::
:: Unless required by applicable law or agreed to in writing,
:: software distributed under the License is distributed on an
:: "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
:: KIND, either express or implied.  See the License for the
:: specific language governing permissions and limitations
:: under the License.

@ECHO OFF
SET script_path="%~dp0start-emulator"
IF EXIST %script_path% (
        node %script_path% %*
) ELSE (
    ECHO.
    ECHO ERROR: Could not find 'start-emulator' script in 'cordova\lib' folder, aborting...>&2
    EXIT /B 1
)


code:
#!/usr/bin/env node

/*
       Licensed to the Apache Software Foundation (ASF) under one
       or more contributor license agreements.  See the NOTICE file
       distributed with this work for additional information
       regarding copyright ownership.  The ASF licenses this file
       to you under the Apache License, Version 2.0 (the
       "License"); you may not use this file except in compliance
       with the License.  You may obtain a copy of the License at

         http://www.apache.org/licenses/LICENSE-2.0

       Unless required by applicable law or agreed to in writing,
       software distributed under the License is distributed on an
       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
       KIND, either express or implied.  See the License for the
       specific language governing permissions and limitations
       under the License.
*/

var log = require('./lib/log');
var reqs = require('./lib/check_reqs');
var args = process.argv;

// Usage support for when args are given
if (args.length > 2) {
    log.help();
} else {
    reqs.run().done(function () {
        return log.run();
    }, function (err) {
        console.error('ERROR: ' + err);
        process.exit(2);
    });
}


code:
:: Licensed to the Apache Software Foundation (ASF) under one
:: or more contributor license agreements.  See the NOTICE file
:: distributed with this work for additional information
:: regarding copyright ownership.  The ASF licenses this file
:: to you under the Apache License, Version 2.0 (the
:: "License"); you may not use this file except in compliance
:: with the License.  You may obtain a copy of the License at
::
:: http://www.apache.org/licenses/LICENSE-2.0
::
:: Unless required by applicable law or agreed to in writing,
:: software distributed under the License is distributed on an
:: "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
:: KIND, either express or implied.  See the License for the
:: specific language governing permissions and limitations
:: under the License.

@ECHO OFF
SET script_path="%~dp0log"
IF EXIST %script_path% (
        node %script_path% %*
) ELSE (
    ECHO.
    ECHO ERROR: Could not find 'log' script in 'cordova' folder, aborting...>&2
    EXIT /B 1
)


code:
var CordovaLogger = require('cordova-common').CordovaLogger;

module.exports = {
    adjustLoggerLevel: function (opts) {
        if (opts instanceof Array) {
            opts.silent = opts.indexOf('--silent') !== -1;
            opts.verbose = opts.indexOf('--verbose') !== -1;
        }

        if (opts.silent) {
            CordovaLogger.get().setLevel('error');
        }

        if (opts.verbose) {
            CordovaLogger.get().setLevel('verbose');
        }
    }
};


code:
#!/usr/bin/env node

/*
       Licensed to the Apache Software Foundation (ASF) under one
       or more contributor license agreements.  See the NOTICE file
       distributed with this work for additional information
       regarding copyright ownership.  The ASF licenses this file
       to you under the Apache License, Version 2.0 (the
       "License"); you may not use this file except in compliance
       with the License.  You may obtain a copy of the License at

         http://www.apache.org/licenses/LICENSE-2.0

       Unless required by applicable law or agreed to in writing,
       software distributed under the License is distributed on an
       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
       KIND, either express or implied.  See the License for the
       specific language governing permissions and limitations
       under the License.
*/

var Api = require('./Api');
var nopt = require('nopt');
var path = require('path');

// Support basic help commands
if (['--help', '/?', '-h', 'help', '-help', '/help'].indexOf(process.argv[2]) >= 0) {
    require('./lib/run').help();
}

// Do some basic argument parsing
var runOpts = nopt({
    'verbose': Boolean,
    'silent': Boolean,
    'debug': Boolean,
    'release': Boolean,
    'nobuild': Boolean,
    'buildConfig': path,
    'archs': String,
    'device': Boolean,
    'emulator': Boolean,
    'target': String
}, { 'd': '--verbose' });

// Make runOptions compatible with PlatformApi run method spec
runOpts.argv = runOpts.argv.remain;

require('./loggingHelper').adjustLoggerLevel(runOpts);

new Api().run(runOpts)
    .catch(function (err) {
        console.error(err, err.stack);
        process.exit(2);
    });