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.
*/

var Q = require('q');
var os = require('os');
var events = require('cordova-common').events;
var spawn = require('cordova-common').superspawn.spawn;
var CordovaError = require('cordova-common').CordovaError;

var Adb = {};

function isDevice (line) {
    return line.match(/\w+\tdevice/) && !line.match(/emulator/);
}

function isEmulator (line) {
    return line.match(/device/) && line.match(/emulator/);
}

/**
 * Lists available/connected devices and emulators
 *
 * @param   {Object}   opts            Various options
 * @param   {Boolean}  opts.emulators  Specifies whether this method returns
 *   emulators only
 *
 * @return  {Promise<String[]>}        list of available/connected
 *   devices/emulators
 */
Adb.devices = function (opts) {
    return spawn('adb', ['devices'], { cwd: os.tmpdir() }).then(function (output) {
        return output.split('\n').filter(function (line) {
            // Filter out either real devices or emulators, depending on options
            return (line && opts && opts.emulators) ? isEmulator(line) : isDevice(line);
        }).map(function (line) {
            return line.replace(/\tdevice/, '').replace('\r', '');
        });
    });
};

Adb.install = function (target, packagePath, opts) {
    events.emit('verbose', 'Installing apk ' + packagePath + ' on target ' + target + '...');
    var args = ['-s', target, 'install'];
    if (opts && opts.replace) args.push('-r');
    return spawn('adb', args.concat(packagePath), { cwd: os.tmpdir() }).then(function (output) {
        // 'adb install' seems to always returns no error, even if installation fails
        // so we catching output to detect installation failure
        if (output.match(/Failure/)) {
            if (output.match(/INSTALL_PARSE_FAILED_NO_CERTIFICATES/)) {
                output += '\n\n' + 'Sign the build using \'-- --keystore\' or \'--buildConfig\'' +
                    ' or sign and deploy the unsigned apk manually using Android tools.';
            } else if (output.match(/INSTALL_FAILED_VERSION_DOWNGRADE/)) {
                output += '\n\n' + 'You\'re trying to install apk with a lower versionCode that is already installed.' +
                    '\nEither uninstall an app or increment the versionCode.';
            }

            return Q.reject(new CordovaError('Failed to install apk to device: ' + output));
        }
    });
};

Adb.uninstall = function (target, packageId) {
    events.emit('verbose', 'Uninstalling package ' + packageId + ' from target ' + target + '...');
    return spawn('adb', ['-s', target, 'uninstall', packageId], { cwd: os.tmpdir() });
};

Adb.shell = function (target, shellCommand) {
    events.emit('verbose', 'Running adb shell command "' + shellCommand + '" on target ' + target + '...');
    var args = ['-s', target, 'shell'];
    shellCommand = shellCommand.split(/\s+/);
    return spawn('adb', args.concat(shellCommand), { cwd: os.tmpdir() }).catch(function (output) {
        return Q.reject(new CordovaError('Failed to execute shell command "' +
            shellCommand + '"" on device: ' + output));
    });
};

Adb.start = function (target, activityName) {
    events.emit('verbose', 'Starting application "' + activityName + '" on target ' + target + '...');
    return Adb.shell(target, 'am start -W -a android.intent.action.MAIN -n' + activityName).catch(function (output) {
        return Q.reject(new CordovaError('Failed to start application "' +
            activityName + '"" on device: ' + output));
    });
};

module.exports = Adb;


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.
*/

var fs = require('fs');
var xml = require('cordova-common').xmlHelpers;

var DEFAULT_ORIENTATION = 'default';

/** Wraps an AndroidManifest file */
function AndroidManifest (path) {
    this.path = path;
    this.doc = xml.parseElementtreeSync(path);
    if (this.doc.getroot().tag !== 'manifest') {
        throw new Error('AndroidManifest at ' + path + ' has incorrect root node name (expected "manifest")');
    }
}

AndroidManifest.prototype.getVersionName = function () {
    return this.doc.getroot().attrib['android:versionName'];
};

AndroidManifest.prototype.setVersionName = function (versionName) {
    this.doc.getroot().attrib['android:versionName'] = versionName;
    return this;
};

AndroidManifest.prototype.getVersionCode = function () {
    return this.doc.getroot().attrib['android:versionCode'];
};

AndroidManifest.prototype.setVersionCode = function (versionCode) {
    this.doc.getroot().attrib['android:versionCode'] = versionCode;
    return this;
};

AndroidManifest.prototype.getPackageId = function () {
    return this.doc.getroot().attrib['package'];
};

AndroidManifest.prototype.setPackageId = function (pkgId) {
    this.doc.getroot().attrib['package'] = pkgId;
    return this;
};

AndroidManifest.prototype.getActivity = function () {
    var activity = this.doc.getroot().find('./application/activity');
    return {
        getName: function () {
            return activity.attrib['android:name'];
        },
        setName: function (name) {
            if (!name) {
                delete activity.attrib['android:name'];
            } else {
                activity.attrib['android:name'] = name;
            }
            return this;
        },
        getOrientation: function () {
            return activity.attrib['android:screenOrientation'];
        },
        setOrientation: function (orientation) {
            if (!orientation || orientation.toLowerCase() === DEFAULT_ORIENTATION) {
                delete activity.attrib['android:screenOrientation'];
            } else {
                activity.attrib['android:screenOrientation'] = orientation;
            }
            return this;
        },
        getLaunchMode: function () {
            return activity.attrib['android:launchMode'];
        },
        setLaunchMode: function (launchMode) {
            if (!launchMode) {
                delete activity.attrib['android:launchMode'];
            } else {
                activity.attrib['android:launchMode'] = launchMode;
            }
            return this;
        }
    };
};

AndroidManifest.prototype.getDebuggable = function () {
    return this.doc.getroot().find('./application').attrib['android:debuggable'] === 'true';
};

AndroidManifest.prototype.setDebuggable = function (value) {
    var application = this.doc.getroot().find('./application');
    if (value) {
        application.attrib['android:debuggable'] = 'true';
    } else {
        // The default value is "false", so we can remove attribute at all.
        delete application.attrib['android:debuggable'];
    }
    return this;
};

/**
 * Writes manifest to disk syncronously. If filename is specified, then manifest
 *   will be written to that file
 *
 * @param   {String}  [destPath]  File to write manifest to. If omitted,
 *   manifest will be written to file it has been read from.
 */
AndroidManifest.prototype.write = function (destPath) {
    fs.writeFileSync(destPath || this.path, this.doc.write({ indent: 4 }), 'utf-8');
};

module.exports = AndroidManifest;

repo_name: Area/degrees

license: mit


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.
*/

var fs = require('fs');
var path = require('path');
var properties_parser = require('properties-parser');
var AndroidManifest = require('./AndroidManifest');
var pluginHandlers = require('./pluginHandlers');

var projectFileCache = {};

function addToPropertyList (projectProperties, key, value) {
    var i = 1;
    while (projectProperties.get(key + '.' + i)) { i++; }

    projectProperties.set(key + '.' + i, value);
    projectProperties.dirty = true;
}

function removeFromPropertyList (projectProperties, key, value) {
    var i = 1;
    var currentValue;
    while ((currentValue = projectProperties.get(key + '.' + i))) {
        if (currentValue === value) {
            while ((currentValue = projectProperties.get(key + '.' + (i + 1)))) {
                projectProperties.set(key + '.' + i, currentValue);
                i++;
            }
            projectProperties.set(key + '.' + i);
            break;
        }
        i++;
    }
    projectProperties.dirty = true;
}

function getRelativeLibraryPath (parentDir, subDir) {
    var libraryPath = path.relative(parentDir, subDir);
    return (path.sep === '\\') ? libraryPath.replace(/\\/g, '/') : libraryPath;
}

function AndroidProject (projectDir) {
    this._propertiesEditors = {};
    this._subProjectDirs = {};
    this._dirty = false;
    this.projectDir = projectDir;
    this.platformWww = path.join(this.projectDir, 'platform_www');
    this.www = path.join(this.projectDir, 'app/src/main/assets/www');
}

AndroidProject.getProjectFile = function (projectDir) {
    if (!projectFileCache[projectDir]) {
        projectFileCache[projectDir] = new AndroidProject(projectDir);
    }

    return projectFileCache[projectDir];
};

AndroidProject.purgeCache = function (projectDir) {
    if (projectDir) {
        delete projectFileCache[projectDir];
    } else {
        projectFileCache = {};
    }
};

/**
 * Reads the package name out of the Android Manifest file
 *
 * @param   {String}  projectDir  The absolute path to the directory containing the project
 *
 * @return  {String}              The name of the package
 */
AndroidProject.prototype.getPackageName = function () {
    var manifestPath = path.join(this.projectDir, 'app/src/main/AndroidManifest.xml');
    return new AndroidManifest(manifestPath).getPackageId();
};

AndroidProject.prototype.getCustomSubprojectRelativeDir = function (plugin_id, src) {
    // All custom subprojects are prefixed with the last portion of the package id.
    // This is to avoid collisions when opening multiple projects in Eclipse that have subprojects with the same name.
    var packageName = this.getPackageName();
    var lastDotIndex = packageName.lastIndexOf('.');
    var prefix = packageName.substring(lastDotIndex + 1);
    var subRelativeDir = path.join(plugin_id, prefix + '-' + path.basename(src));
    return subRelativeDir;
};

AndroidProject.prototype.addSubProject = function (parentDir, subDir) {
    var parentProjectFile = path.resolve(parentDir, 'project.properties');
    var subProjectFile = path.resolve(subDir, 'project.properties');
    var parentProperties = this._getPropertiesFile(parentProjectFile);
    // TODO: Setting the target needs to happen only for pre-3.7.0 projects
    if (fs.existsSync(subProjectFile)) {
        var subProperties = this._getPropertiesFile(subProjectFile);
        subProperties.set('target', parentProperties.get('target'));
        subProperties.dirty = true;
        this._subProjectDirs[subDir] = true;
    }
    addToPropertyList(parentProperties, 'android.library.reference', getRelativeLibraryPath(parentDir, subDir));

    this._dirty = true;
};

AndroidProject.prototype.removeSubProject = function (parentDir, subDir) {
    var parentProjectFile = path.resolve(parentDir, 'project.properties');
    var parentProperties = this._getPropertiesFile(parentProjectFile);
    removeFromPropertyList(parentProperties, 'android.library.reference', getRelativeLibraryPath(parentDir, subDir));
    delete this._subProjectDirs[subDir];
    this._dirty = true;
};

AndroidProject.prototype.addGradleReference = function (parentDir, subDir) {
    var parentProjectFile = path.resolve(parentDir, 'project.properties');
    var parentProperties = this._getPropertiesFile(parentProjectFile);
    addToPropertyList(parentProperties, 'cordova.gradle.include', getRelativeLibraryPath(parentDir, subDir));
    this._dirty = true;
};

AndroidProject.prototype.removeGradleReference = function (parentDir, subDir) {
    var parentProjectFile = path.resolve(parentDir, 'project.properties');
    var parentProperties = this._getPropertiesFile(parentProjectFile);
    removeFromPropertyList(parentProperties, 'cordova.gradle.include', getRelativeLibraryPath(parentDir, subDir));
    this._dirty = true;
};

AndroidProject.prototype.addSystemLibrary = function (parentDir, value) {
    var parentProjectFile = path.resolve(parentDir, 'project.properties');
    var parentProperties = this._getPropertiesFile(parentProjectFile);
    addToPropertyList(parentProperties, 'cordova.system.library', value);
    this._dirty = true;
};

AndroidProject.prototype.removeSystemLibrary = function (parentDir, value) {
    var parentProjectFile = path.resolve(parentDir, 'project.properties');
    var parentProperties = this._getPropertiesFile(parentProjectFile);
    removeFromPropertyList(parentProperties, 'cordova.system.library', value);
    this._dirty = true;
};

AndroidProject.prototype.write = function () {
    if (!this._dirty) {
        return;
    }
    this._dirty = false;

    for (var filename in this._propertiesEditors) {
        var editor = this._propertiesEditors[filename];
        if (editor.dirty) {
            fs.writeFileSync(filename, editor.toString());
            editor.dirty = false;
        }
    }
};

AndroidProject.prototype._getPropertiesFile = function (filename) {
    if (!this._propertiesEditors[filename]) {
        if (fs.existsSync(filename)) {
            this._propertiesEditors[filename] = properties_parser.createEditor(filename);
        } else {
            this._propertiesEditors[filename] = properties_parser.createEditor();
        }
    }

    return this._propertiesEditors[filename];
};

AndroidProject.prototype.getInstaller = function (type) {
    return pluginHandlers.getInstaller(type);
};

AndroidProject.prototype.getUninstaller = function (type) {
    return pluginHandlers.getUninstaller(type);
};

/*
 * This checks if an Android project is clean or has old build artifacts
 */

AndroidProject.prototype.isClean = function () {
    var build_path = path.join(this.projectDir, 'build');
    // If the build directory doesn't exist, it's clean
    return !(fs.existsSync(build_path));
};

module.exports = AndroidProject;


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.
*/

const PackageType = {
    APK: 'apk',
    BUNDLE: 'bundle'
};

module.exports = PackageType;


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.
*/

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

var suffix_number_regex = /(\d+)$/;
// Used for sorting Android targets, example strings to sort:
//   android-19
//   android-L
//   Google Inc.:Google APIs:20
//   Google Inc.:Glass Development Kit Preview:20
// The idea is to sort based on largest "suffix" number - meaning the bigger
// the number at the end, the more recent the target, the closer to the
// start of the array.
function sort_by_largest_numerical_suffix (a, b) {
    var suffix_a = a.match(suffix_number_regex);
    var suffix_b = b.match(suffix_number_regex);
    if (suffix_a && suffix_b) {
        // If the two targets being compared have suffixes, return less than
        // zero, or greater than zero, based on which suffix is larger.
        return (parseInt(suffix_a[1]) > parseInt(suffix_b[1]) ? -1 : 1);
    } else {
        // If no suffix numbers were detected, leave the order as-is between
        // elements a and b.
        return 0;
    }
}

module.exports.print_newest_available_sdk_target = function () {
    return module.exports.list_targets().then(function (targets) {
        targets.sort(sort_by_largest_numerical_suffix);
        console.log(targets[0]);
    });
};

module.exports.version_string_to_api_level = {
    '4.0': 14,
    '4.0.3': 15,
    '4.1': 16,
    '4.2': 17,
    '4.3': 18,
    '4.4': 19,
    '4.4W': 20,
    '5.0': 21,
    '5.1': 22,
    '6.0': 23,
    '7.0': 24,
    '7.1.1': 25,
    '8.0': 26
};

function parse_targets (output) {
    var target_out = output.split('\n');
    var targets = [];
    for (var i = target_out.length - 1; i >= 0; i--) {
        if (target_out[i].match(/id:/)) { // if "id:" is in the line...
            targets.push(target_out[i].match(/"(.+)"/)[1]); // .. match whatever is in quotes.
        }
    }
    return targets;
}

module.exports.list_targets_with_android = function () {
    return superspawn.spawn('android', ['list', 'target']).then(parse_targets);
};

module.exports.list_targets_with_avdmanager = function () {
    return superspawn.spawn('avdmanager', ['list', 'target']).then(parse_targets);
};

module.exports.list_targets = function () {
    return module.exports.list_targets_with_avdmanager().catch(function (err) {
        // If there's an error, like avdmanager could not be found, we can try
        // as a last resort, to run `android`, in case this is a super old
        // SDK installation.
        if (err && (err.code === 'ENOENT' || (err.stderr && err.stderr.match(/not recognized/)))) {
            return module.exports.list_targets_with_android();
        } else throw err;
    }).then(function (targets) {
        if (targets.length === 0) {
            return Promise.reject(new Error('No android targets (SDKs) installed!'));
        }
        return targets;
    });
};


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 Q = require('q');
var path = require('path');
var fs = require('fs');
var nopt = require('nopt');

var Adb = require('./Adb');

var builders = require('./builders/builders');
var events = require('cordova-common').events;
var spawn = require('cordova-common').superspawn.spawn;
var CordovaError = require('cordova-common').CordovaError;
var PackageType = require('./PackageType');

module.exports.parseBuildOptions = parseOpts;
function parseOpts (options, resolvedTarget, projectRoot) {
    options = options || {};
    options.argv = nopt({
        prepenv: Boolean,
        versionCode: String,
        minSdkVersion: String,
        maxSdkVersion: String,
        targetSdkVersion: String,
        gradleArg: [String, Array],
        keystore: path,
        alias: String,
        storePassword: String,
        password: String,
        keystoreType: String,
        packageType: String
    }, {}, options.argv, 0);

    // Android Studio Build method is the default
    var ret = {
        buildType: options.release ? 'release' : 'debug',
        prepEnv: options.argv.prepenv,
        arch: resolvedTarget && resolvedTarget.arch,
        extraArgs: []
    };

    if (options.argv.versionCode) { ret.extraArgs.push('-PcdvVersionCode=' + options.argv.versionCode); }
    if (options.argv.minSdkVersion) { ret.extraArgs.push('-PcdvMinSdkVersion=' + options.argv.minSdkVersion); }
    if (options.argv.maxSdkVersion) { ret.extraArgs.push('-PcdvMaxSdkVersion=' + options.argv.maxSdkVersion); }
    if (options.argv.targetSdkVersion) { ret.extraArgs.push('-PcdvTargetSdkVersion=' + options.argv.targetSdkVersion); }
    if (options.argv.gradleArg) {
        ret.extraArgs = ret.extraArgs.concat(options.argv.gradleArg);
    }

    var packageArgs = {};

    if (options.argv.keystore) { packageArgs.keystore = path.relative(projectRoot, path.resolve(options.argv.keystore)); }

    ['alias', 'storePassword', 'password', 'keystoreType', 'packageType'].forEach(function (flagName) {
        if (options.argv[flagName]) { packageArgs[flagName] = options.argv[flagName]; }
    });

    var buildConfig = options.buildConfig;

    // If some values are not specified as command line arguments - use build config to supplement them.
    // Command line arguments have precedence over build config.
    if (buildConfig) {
        if (!fs.existsSync(buildConfig)) {
            throw new Error('Specified build config file does not exist: ' + buildConfig);
        }
        events.emit('log', 'Reading build config file: ' + path.resolve(buildConfig));
        var buildjson = fs.readFileSync(buildConfig, 'utf8');
        var config = JSON.parse(buildjson.replace(/^\ufeff/, '')); // Remove BOM
        if (config.android && config.android[ret.buildType]) {
            var androidInfo = config.android[ret.buildType];
            if (androidInfo.keystore && !packageArgs.keystore) {
                if (androidInfo.keystore.substr(0, 1) === '~') {
                    androidInfo.keystore = process.env.HOME + androidInfo.keystore.substr(1);
                }
                packageArgs.keystore = path.resolve(path.dirname(buildConfig), androidInfo.keystore);
                events.emit('log', 'Reading the keystore from: ' + packageArgs.keystore);
            }

            ['alias', 'storePassword', 'password', 'keystoreType', 'packageType'].forEach(function (key) {
                packageArgs[key] = packageArgs[key] || androidInfo[key];
            });
        }
    }

    if (packageArgs.keystore && packageArgs.alias) {
        ret.packageInfo = new PackageInfo(packageArgs.keystore, packageArgs.alias, packageArgs.storePassword,
            packageArgs.password, packageArgs.keystoreType);
    }

    if (!ret.packageInfo) {
        // The following loop is to decide whether to print a warning about generating a signed archive
        // We only want to produce a warning if they are using a config property that is related to signing, but
        // missing the required properties for signing. We don't want to produce a warning if they are simply
        // using a build property that isn't related to signing, such as --packageType
        let shouldWarn = false;
        const signingKeys = ['keystore', 'alias', 'storePassword', 'password', 'keystoreType'];

        for (let key in packageArgs) {
            if (!shouldWarn && signingKeys.indexOf(key) > -1) {
                // If we enter this condition, we have a key used for signing a build,
                // but we are missing some required signing properties
                shouldWarn = true;
            }
        }

        if (shouldWarn) {
            events.emit('warn', '\'keystore\' and \'alias\' need to be specified to generate a signed archive.');
        }
    }

    if (packageArgs.packageType) {
        const VALID_PACKAGE_TYPES = [PackageType.APK, PackageType.BUNDLE];
        if (VALID_PACKAGE_TYPES.indexOf(packageArgs.packageType) === -1) {
            events.emit('warn', '"' + packageArgs.packageType + '" is an invalid packageType. Valid values are: ' + VALID_PACKAGE_TYPES.join(', ') + '\nDefaulting packageType to ' + PackageType.APK);
            ret.packageType = PackageType.APK;
        } else {
            ret.packageType = packageArgs.packageType;
        }
    } else {
        ret.packageType = PackageType.APK;
    }

    return ret;
}

/*
 * Builds the project with the specifed options
 * Returns a promise.
 */
module.exports.runClean = function (options) {
    var opts = parseOpts(options, null, this.root);
    var builder = builders.getBuilder();

    return builder.prepEnv(opts).then(function () {
        return builder.clean(opts);
    });
};

/**
 * Builds the project with the specifed options.
 *
 * @param   {BuildOptions}  options      A set of options. See PlatformApi.build
 *   method documentation for reference.
 * @param   {Object}  optResolvedTarget  A deployment target. Used to pass
 *   target architecture from upstream 'run' call. TODO: remove this option in
 *   favor of setting buildOptions.archs field.
 *
 * @return  {Promise<Object>}            Promise, resolved with built packages
 *   information.
 */
module.exports.run = function (options, optResolvedTarget) {
    var opts = parseOpts(options, optResolvedTarget, this.root);
    var builder = builders.getBuilder();

    return builder.prepEnv(opts).then(function () {
        if (opts.prepEnv) {
            events.emit('verbose', 'Build file successfully prepared.');
            return;
        }
        return builder.build(opts).then(function () {
            var paths;
            if (opts.packageType === PackageType.BUNDLE) {
                paths = builder.findOutputBundles(opts.buildType);
                events.emit('log', 'Built the following bundle(s): \n\t' + paths.join('\n\t'));
            } else {
                paths = builder.findOutputApks(opts.buildType, opts.arch);
                events.emit('log', 'Built the following apk(s): \n\t' + paths.join('\n\t'));
            }

            return {
                paths: paths,
                buildType: opts.buildType
            };
        });
    });
};

/*
 * Detects the architecture of a device/emulator
 * Returns "arm" or "x86".
 */
module.exports.detectArchitecture = function (target) {
    function helper () {
        return Adb.shell(target, 'cat /proc/cpuinfo').then(function (output) {
            return /intel/i.exec(output) ? 'x86' : 'arm';
        });
    }
    // It sometimes happens (at least on OS X), that this command will hang forever.
    // To fix it, either unplug & replug device, or restart adb server.
    return helper().timeout(1000, new CordovaError('Device communication timed out. Try unplugging & replugging the device.')).then(null, function (err) {
        if (/timed out/.exec('' + err)) {
            // adb kill-server doesn't seem to do the trick.
            // Could probably find a x-platform version of killall, but I'm not actually
            // sure that this scenario even happens on non-OSX machines.
            events.emit('verbose', 'adb timed out while detecting device/emulator architecture. Killing adb and trying again.');
            return spawn('killall', ['adb']).then(function () {
                return helper().then(null, function () {
                    // The double kill is sadly often necessary, at least on mac.
                    events.emit('warn', 'adb timed out a second time while detecting device/emulator architecture. Killing adb and trying again.');
                    return spawn('killall', ['adb']).then(function () {
                        return helper().then(null, function () {
                            return Q.reject(new CordovaError('adb timed out a third time while detecting device/emulator architecture. Try unplugging & replugging the device.'));
                        });
                    });
                });
            }, function () {
                // For non-killall OS's.
                return Q.reject(err);
            });
        }
        throw err;
    });
};

module.exports.findBestApkForArchitecture = function (buildResults, arch) {
    var paths = buildResults.apkPaths.filter(function (p) {
        var apkName = path.basename(p);
        if (buildResults.buildType === 'debug') {
            return /-debug/.exec(apkName);
        }
        return !/-debug/.exec(apkName);
    });
    var archPattern = new RegExp('-' + arch);
    var hasArchPattern = /-x86|-arm/;
    for (var i = 0; i < paths.length; ++i) {
        var apkName = path.basename(paths[i]);
        if (hasArchPattern.exec(apkName)) {
            if (archPattern.exec(apkName)) {
                return paths[i];
            }
        } else {
            return paths[i];
        }
    }
    throw new Error('Could not find apk architecture: ' + arch + ' build-type: ' + buildResults.buildType);
};

function PackageInfo (keystore, alias, storePassword, password, keystoreType) {
    this.keystore = {
        'name': 'key.store',
        'value': keystore
    };
    this.alias = {
        'name': 'key.alias',
        'value': alias
    };
    if (storePassword) {
        this.storePassword = {
            'name': 'key.store.password',
            'value': storePassword
        };
    }
    if (password) {
        this.password = {
            'name': 'key.alias.password',
            'value': password
        };
    }
    if (keystoreType) {
        this.keystoreType = {
            'name': 'key.store.type',
            'value': keystoreType
        };
    }
}

PackageInfo.prototype = {
    toProperties: function () {
        var self = this;
        var result = '';
        Object.keys(self).forEach(function (key) {
            result += self[key].name;
            result += '=';
            result += self[key].value.replace(/\\/g, '\\\\');
            result += '\n';
        });
        return result;
    }
};

module.exports.help = function () {
    console.log('Usage: ' + path.relative(process.cwd(), path.join('../build')) + ' [flags] [Signed APK flags]');
    console.log('Flags:');
    console.log('    \'--debug\': will build project in debug mode (default)');
    console.log('    \'--release\': will build project for release');
    console.log('    \'--nobuild\': will skip build process (useful when using run command)');
    console.log('    \'--prepenv\': don\'t build, but copy in build scripts where necessary');
    console.log('    \'--versionCode=#\': Override versionCode for this build. Useful for uploading multiple APKs.');
    console.log('    \'--minSdkVersion=#\': Override minSdkVersion for this build.');
    console.log('    \'--maxSdkVersion=#\': Override maxSdkVersion for this build. (Not Recommended)');
    console.log('    \'--targetSdkVersion=#\': Override targetSdkVersion for this build.');
    console.log('    \'--gradleArg=<gradle command line arg>\': Extra args to pass to the gradle command. Use one flag per arg. Ex. --gradleArg=-PcdvBuildMultipleApks=true');
    console.log('    \'--packageType=<apk|bundle>\': Builds an APK or a bundle');
    console.log('');
    console.log('Signed APK flags (overwrites debug/release-signing.proprties) :');
    console.log('    \'--keystore=<path to keystore>\': Key store used to build a signed archive. (Required)');
    console.log('    \'--alias=\': Alias for the key store. (Required)');
    console.log('    \'--storePassword=\': Password for the key store. (Optional - prompted)');
    console.log('    \'--password=\': Password for the key. (Optional - prompted)');
    console.log('    \'--keystoreType\': Type of the keystore. (Optional)');
    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.
*/
/* eslint no-self-assign: 0 */
/* eslint no-unused-vars: 0 */

var Q = require('q');
var fs = require('fs');
var path = require('path');
var shell = require('shelljs');
var spawn = require('cordova-common').superspawn.spawn;
var events = require('cordova-common').events;
var CordovaError = require('cordova-common').CordovaError;
var check_reqs = require('../check_reqs');
var PackageType = require('../PackageType');
const compareFunc = require('compare-func');

const MARKER = 'YOUR CHANGES WILL BE ERASED!';
const SIGNING_PROPERTIES = '-signing.properties';
const TEMPLATE =
    '# This file is automatically generated.\n' +
    '# Do not modify this file -- ' + MARKER + '\n';

class ProjectBuilder {
    constructor (rootDirectory) {
        this.root = rootDirectory || path.resolve(__dirname, '../../..');
        this.apkDir = path.join(this.root, 'app', 'build', 'outputs', 'apk');
        this.aabDir = path.join(this.root, 'app', 'build', 'outputs', 'bundle');
    }

    getArgs (cmd, opts) {
        let args;
        let buildCmd = cmd;
        if (opts.packageType === PackageType.BUNDLE) {
            if (cmd === 'release') {
                buildCmd = ':app:bundleRelease';
            } else if (cmd === 'debug') {
                buildCmd = ':app:bundleDebug';
            }

            args = [buildCmd, '-b', path.join(this.root, 'build.gradle')];
        } else {
            if (cmd === 'release') {
                buildCmd = 'cdvBuildRelease';
            } else if (cmd === 'debug') {
                buildCmd = 'cdvBuildDebug';
            }

            args = [buildCmd, '-b', path.join(this.root, 'build.gradle')];

            if (opts.arch) {
                args.push('-PcdvBuildArch=' + opts.arch);
            }

            args.push.apply(args, opts.extraArgs);
        }

        return args;
    }

    /*
    * This returns a promise
    */
    runGradleWrapper (gradle_cmd) {
        var gradlePath = path.join(this.root, 'gradlew');
        var wrapperGradle = path.join(this.root, 'wrapper.gradle');
        if (fs.existsSync(gradlePath)) {
            // Literally do nothing, for some reason this works, while !fs.existsSync didn't on Windows
        } else {
            return spawn(gradle_cmd, ['-p', this.root, 'wrapper', '-b', wrapperGradle], { stdio: 'inherit' });
        }
    }

    readProjectProperties () {
        function findAllUniq (data, r) {
            var s = {};
            var m;
            while ((m = r.exec(data))) {
                s[m[1]] = 1;
            }
            return Object.keys(s);
        }

        var data = fs.readFileSync(path.join(this.root, 'project.properties'), 'utf8');
        return {
            libs: findAllUniq(data, /^\s*android\.library\.reference\.\d+=(.*)(?:\s|$)/mg),
            gradleIncludes: findAllUniq(data, /^\s*cordova\.gradle\.include\.\d+=(.*)(?:\s|$)/mg),
            systemLibs: findAllUniq(data, /^\s*cordova\.system\.library\.\d+=(.*)(?:\s|$)/mg)
        };
    }

    extractRealProjectNameFromManifest () {
        var manifestPath = path.join(this.root, 'app', 'src', 'main', 'AndroidManifest.xml');
        var manifestData = fs.readFileSync(manifestPath, 'utf8');
        var m = /<manifest[\s\S]*?package\s*=\s*"(.*?)"/i.exec(manifestData);
        if (!m) {
            throw new CordovaError('Could not find package name in ' + manifestPath);
        }

        var packageName = m[1];
        var lastDotIndex = packageName.lastIndexOf('.');
        return packageName.substring(lastDotIndex + 1);
    }

    // Makes the project buildable, minus the gradle wrapper.
    prepBuildFiles () {
        // Update the version of build.gradle in each dependent library.
        var pluginBuildGradle = path.join(this.root, 'cordova', 'lib', 'plugin-build.gradle');
        var propertiesObj = this.readProjectProperties();
        var subProjects = propertiesObj.libs;

        // Check and copy the gradle file into the subproject
        // Called by the loop before this function def

        var checkAndCopy = function (subProject, root) {
            var subProjectGradle = path.join(root, subProject, 'build.gradle');
            // This is the future-proof way of checking if a file exists
            // This must be synchronous to satisfy a Travis test
            try {
                fs.accessSync(subProjectGradle, fs.F_OK);
            } catch (e) {
                shell.cp('-f', pluginBuildGradle, subProjectGradle);
            }
        };

        for (var i = 0; i < subProjects.length; ++i) {
            if (subProjects[i] !== 'CordovaLib') {
                checkAndCopy(subProjects[i], this.root);
            }
        }
        var name = this.extractRealProjectNameFromManifest();
        // Remove the proj.id/name- prefix from projects: https://issues.apache.org/jira/browse/CB-9149
        var settingsGradlePaths = subProjects.map(function (p) {
            var realDir = p.replace(/[/\\]/g, ':');
            var libName = realDir.replace(name + '-', '');
            var str = 'include ":' + libName + '"\n';
            if (realDir.indexOf(name + '-') !== -1) {
                str += 'project(":' + libName + '").projectDir = new File("' + p + '")\n';
            }
            return str;
        });

        fs.writeFileSync(path.join(this.root, 'settings.gradle'),
            '// GENERATED FILE - DO NOT EDIT\n' +
            'include ":"\n' + settingsGradlePaths.join(''));

        // Update dependencies within build.gradle.
        var buildGradle = fs.readFileSync(path.join(this.root, 'app', 'build.gradle'), 'utf8');
        var depsList = '';
        var root = this.root;
        var insertExclude = function (p) {
            var gradlePath = path.join(root, p, 'build.gradle');
            var projectGradleFile = fs.readFileSync(gradlePath, 'utf-8');
            if (projectGradleFile.indexOf('CordovaLib') !== -1) {
                depsList += '{\n        exclude module:("CordovaLib")\n    }\n';
            } else {
                depsList += '\n';
            }
        };
        subProjects.forEach(function (p) {
            events.emit('log', 'Subproject Path: ' + p);
            var libName = p.replace(/[/\\]/g, ':').replace(name + '-', '');
            if (libName !== 'app') {
                depsList += '    implementation(project(path: ":' + libName + '"))';
                insertExclude(p);
            }
        });
        // For why we do this mapping: https://issues.apache.org/jira/browse/CB-8390
        var SYSTEM_LIBRARY_MAPPINGS = [
            [/^\/?extras\/android\/support\/(.*)$/, 'com.android.support:support-$1:+'],
            [/^\/?google\/google_play_services\/libproject\/google-play-services_lib\/?$/, 'com.google.android.gms:play-services:+']
        ];

        propertiesObj.systemLibs.forEach(function (p) {
            var mavenRef;
            // It's already in gradle form if it has two ':'s
            if (/:.*:/.exec(p)) {
                mavenRef = p;
            } else {
                for (var i = 0; i < SYSTEM_LIBRARY_MAPPINGS.length; ++i) {
                    var pair = SYSTEM_LIBRARY_MAPPINGS[i];
                    if (pair[0].exec(p)) {
                        mavenRef = p.replace(pair[0], pair[1]);
                        break;
                    }
                }
                if (!mavenRef) {
                    throw new CordovaError('Unsupported system library (does not work with gradle): ' + p);
                }
            }
            depsList += '    implementation "' + mavenRef + '"\n';
        });

        buildGradle = buildGradle.replace(/(SUB-PROJECT DEPENDENCIES START)[\s\S]*(\/\/ SUB-PROJECT DEPENDENCIES END)/, '$1\n' + depsList + '    $2');
        var includeList = '';

        propertiesObj.gradleIncludes.forEach(function (includePath) {
            includeList += 'apply from: "../' + includePath + '"\n';
        });
        buildGradle = buildGradle.replace(/(PLUGIN GRADLE EXTENSIONS START)[\s\S]*(\/\/ PLUGIN GRADLE EXTENSIONS END)/, '$1\n' + includeList + '$2');
        // This needs to be stored in the app gradle, not the root grade
        fs.writeFileSync(path.join(this.root, 'app', 'build.gradle'), buildGradle);
    }

    prepEnv (opts) {
        var self = this;
        return check_reqs.check_gradle()
            .then(function (gradlePath) {
                return self.runGradleWrapper(gradlePath);
            }).then(function () {
                return self.prepBuildFiles();
            }).then(function () {
                // If the gradle distribution URL is set, make sure it points to version we want.
                // If it's not set, do nothing, assuming that we're using a future version of gradle that we don't want to mess with.
                // For some reason, using ^ and $ don't work.  This does the job, though.
                var distributionUrlRegex = /distributionUrl.*zip/;
                var distributionUrl = process.env['CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL'] || 'https\\://services.gradle.org/distributions/gradle-4.10.3-all.zip';
                var gradleWrapperPropertiesPath = path.join(self.root, 'gradle', 'wrapper', 'gradle-wrapper.properties');
                shell.chmod('u+w', gradleWrapperPropertiesPath);
                shell.sed('-i', distributionUrlRegex, 'distributionUrl=' + distributionUrl, gradleWrapperPropertiesPath);

                var propertiesFile = opts.buildType + SIGNING_PROPERTIES;
                var propertiesFilePath = path.join(self.root, propertiesFile);
                if (opts.packageInfo) {
                    fs.writeFileSync(propertiesFilePath, TEMPLATE + opts.packageInfo.toProperties());
                } else if (isAutoGenerated(propertiesFilePath)) {
                    shell.rm('-f', propertiesFilePath);
                }
            });
    }

    /*
    * Builds the project with gradle.
    * Returns a promise.
    */
    build (opts) {
        var wrapper = path.join(this.root, 'gradlew');
        var args = this.getArgs(opts.buildType === 'debug' ? 'debug' : 'release', opts);

        return spawn(wrapper, args, { stdio: 'pipe' })
            .progress(function (stdio) {
                if (stdio.stderr) {
                    /*
                    * Workaround for the issue with Java printing some unwanted information to
                    * stderr instead of stdout.
                    * This function suppresses 'Picked up _JAVA_OPTIONS' message from being
                    * printed to stderr. See https://issues.apache.org/jira/browse/CB-9971 for
                    * explanation.
                    */
                    var suppressThisLine = /^Picked up _JAVA_OPTIONS: /i.test(stdio.stderr.toString());
                    if (suppressThisLine) {
                        return;
                    }
                    process.stderr.write(stdio.stderr);
                } else {
                    process.stdout.write(stdio.stdout);
                }
            }).catch(function (error) {
                if (error.toString().indexOf('failed to find target with hash string') >= 0) {
                    return check_reqs.check_android_target(error).then(function () {
                        // If due to some odd reason - check_android_target succeeds
                        // we should still fail here.
                        return Q.reject(error);
                    });
                }
                return Q.reject(error);
            });
    }

    clean (opts) {
        var builder = this;
        var wrapper = path.join(this.root, 'gradlew');
        var args = builder.getArgs('clean', opts);
        return Q().then(function () {
            return spawn(wrapper, args, { stdio: 'inherit' });
        })
            .then(function () {
                shell.rm('-rf', path.join(builder.root, 'out'));

                ['debug', 'release'].forEach(function (config) {
                    var propertiesFilePath = path.join(builder.root, config + SIGNING_PROPERTIES);
                    if (isAutoGenerated(propertiesFilePath)) {
                        shell.rm('-f', propertiesFilePath);
                    }
                });
            });
    }

    findOutputApks (build_type, arch) {
        return findOutputApksHelper(this.apkDir, build_type, arch).sort(apkSorter);
    }

    findOutputBundles (build_type) {
        return findOutputBundlesHelper(this.aabDir, build_type);
    }

    fetchBuildResults (build_type, arch) {
        return {
            apkPaths: this.findOutputApks(build_type, arch),
            buildType: build_type
        };
    }
}

module.exports = ProjectBuilder;

const apkSorter = compareFunc([
    // Sort arch specific builds after generic ones
    apkPath => /-x86|-arm/.test(apkPath),

    // Sort unsigned builds after signed ones
    apkPath => /-unsigned/.test(apkPath),

    // Sort by file modification time, latest first
    apkPath => -fs.statSync(apkPath).mtime.getTime(),

    // Sort by file name length, ascending
    'length'
]);

function findOutputApksHelper (dir, build_type, arch) {
    var shellSilent = shell.config.silent;
    shell.config.silent = true;

    // list directory recursively
    var ret = shell.ls('-R', dir).map(function (file) {
        // ls does not include base directory
        return path.join(dir, file);
    }).filter(function (file) {
        // find all APKs
        return file.match(/\.apk?$/i);
    }).filter(function (candidate) {
        var apkName = path.basename(candidate);
        // Need to choose between release and debug .apk.
        if (build_type === 'debug') {
            return /-debug/.exec(apkName) && !/-unaligned|-unsigned/.exec(apkName);
        }
        if (build_type === 'release') {
            return /-release/.exec(apkName) && !/-unaligned/.exec(apkName);
        }
        return true;
    }).sort(apkSorter);

    shellSilent = shellSilent;

    if (ret.length === 0) {
        return ret;
    }
    // Assume arch-specific build if newest apk has -x86 or -arm.
    var archSpecific = !!/-x86|-arm/.exec(path.basename(ret[0]));
    // And show only arch-specific ones (or non-arch-specific)
    ret = ret.filter(function (p) {
        return !!/-x86|-arm/.exec(path.basename(p)) === archSpecific;
    });

    if (archSpecific && ret.length > 1 && arch) {
        ret = ret.filter(function (p) {
            return path.basename(p).indexOf('-' + arch) !== -1;
        });
    }

    return ret;
}

// This method was a copy of findOutputApksHelper and modified to look for bundles
// While replacing shell with fs-extra, it might be a good idea to see if we can
// generalise these findOutput methods.
function findOutputBundlesHelper (dir, build_type) {
    // This is an unused variable that was copied from findOutputApksHelper
    // we are pretty sure it was meant to reset shell.config.silent back to
    // the original value. However shell is planned to be replaced,
    // it was left as is to avoid unintended consequences.
    const shellSilent = shell.config.silent;
    shell.config.silent = true;

    // list directory recursively
    const ret = shell.ls('-R', dir).map(function (file) {
        return path.join(dir, file); // ls does not include base directory
    }).filter(function (file) {
        return file.match(/\.aab?$/i); // find all bundles
    }).filter(function (candidate) {
        // Need to choose between release and debug bundle.
        if (build_type === 'debug') {
            return /debug/.exec(candidate);
        }
        if (build_type === 'release') {
            return /release/.exec(candidate);
        }
        return true;
    });

    return ret;
}

function isAutoGenerated (file) {
    return fs.existsSync(file) && fs.readFileSync(file, 'utf8').indexOf(MARKER) > 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.
*/

const CordovaError = require('cordova-common').CordovaError;

/**
 * Helper method that instantiates and returns a builder for specified build type.
 *
 * @return {Builder} A builder instance for specified build type.
 */
module.exports.getBuilder = function (projectPath) {
    try {
        const Builder = require('./ProjectBuilder');
        return new Builder(projectPath);
    } catch (err) {
        throw new CordovaError('Failed to instantiate ProjectBuilder builder: ' + err);
    }
};


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 shelljs = require('shelljs');
var child_process = require('child_process');
var Q = require('q');
var path = require('path');
var fs = require('fs');
var os = require('os');
var REPO_ROOT = path.join(__dirname, '..', '..', '..', '..');
var PROJECT_ROOT = path.join(__dirname, '..', '..');
var CordovaError = require('cordova-common').CordovaError;
var superspawn = require('cordova-common').superspawn;
var android_sdk = require('./android_sdk');

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

module.exports.isWindows = function () {
    return (os.platform() === 'win32');
};

module.exports.isDarwin = function () {
    return (os.platform() === 'darwin');
};

// Get valid target from framework/project.properties if run from this repo
// Otherwise get target from project.properties file within a generated cordova-android project
module.exports.get_target = function () {
    function extractFromFile (filePath) {
        var target = shelljs.grep(/\btarget=/, filePath);
        if (!target) {
            throw new Error('Could not find android target within: ' + filePath);
        }
        return target.split('=')[1].trim();
    }
    var repo_file = path.join(REPO_ROOT, 'framework', 'project.properties');
    if (fs.existsSync(repo_file)) {
        return extractFromFile(repo_file);
    }
    var project_file = path.join(PROJECT_ROOT, 'project.properties');
    if (fs.existsSync(project_file)) {
        // if no target found, we're probably in a project and project.properties is in PROJECT_ROOT.
        return extractFromFile(project_file);
    }
    throw new Error('Could not find android target in either ' + repo_file + ' nor ' + project_file);
};

// Returns a promise. Called only by build and clean commands.
module.exports.check_ant = function () {
    return superspawn.spawn('ant', ['-version']).then(function (output) {
        // Parse Ant version from command output
        return /version ((?:\d+\.)+(?:\d+))/i.exec(output)[1];
    }).catch(function (err) {
        if (err) {
            throw new CordovaError('Failed to run `ant -version`. Make sure you have `ant` on your $PATH.');
        }
    });
};

module.exports.get_gradle_wrapper = function () {
    var androidStudioPath;
    var i = 0;
    var foundStudio = false;
    var program_dir;
    // OK, This hack only works on Windows, not on Mac OS or Linux.  We will be deleting this eventually!
    if (module.exports.isWindows()) {

        var result = child_process.spawnSync(path.join(__dirname, 'getASPath.bat'));
        // console.log('result.stdout =' + result.stdout.toString());
        // console.log('result.stderr =' + result.stderr.toString());

        if (result.stderr.toString().length > 0) {
            var androidPath = path.join(process.env['ProgramFiles'], 'Android') + '/';
            if (fs.existsSync(androidPath)) {
                program_dir = fs.readdirSync(androidPath);
                while (i < program_dir.length && !foundStudio) {
                    if (program_dir[i].startsWith('Android Studio')) {
                        foundStudio = true;
                        androidStudioPath = path.join(process.env['ProgramFiles'], 'Android', program_dir[i], 'gradle');
                    } else { ++i; }
                }
            }
        } else {
            // console.log('got android studio path from registry');
            // remove the (os independent) new line char at the end of stdout
            // add gradle to match the above.
            androidStudioPath = path.join(result.stdout.toString().split('\r\n')[0], 'gradle');
        }
    }

    if (androidStudioPath !== null && fs.existsSync(androidStudioPath)) {
        var dirs = fs.readdirSync(androidStudioPath);
        if (dirs[0].split('-')[0] === 'gradle') {
            return path.join(androidStudioPath, dirs[0], 'bin', 'gradle');
        }
    } else {
        // OK, let's try to check for Gradle!
        return forgivingWhichSync('gradle');
    }
};

// Returns a promise. Called only by build and clean commands.
module.exports.check_gradle = function () {
    var sdkDir = process.env['ANDROID_HOME'];
    var d = Q.defer();
    if (!sdkDir) {
        return Q.reject(new CordovaError('Could not find gradle wrapper within Android SDK. Could not find Android SDK directory.\n' +
            'Might need to install Android SDK or set up \'ANDROID_HOME\' env variable.'));
    }

    var gradlePath = module.exports.get_gradle_wrapper();
    if (gradlePath.length !== 0) { d.resolve(gradlePath); } else {
        d.reject(new CordovaError('Could not find an installed version of Gradle either in Android Studio,\n' +
                                'or on your system to install the gradle wrapper. Please include gradle \n' +
                                'in your path, or install Android Studio'));
    }
    return d.promise;
};

// Returns a promise.
module.exports.check_java = function () {
    var javacPath = forgivingWhichSync('javac');
    var hasJavaHome = !!process.env['JAVA_HOME'];
    return Q().then(function () {
        if (hasJavaHome) {
            // Windows java installer doesn't add javac to PATH, nor set JAVA_HOME (ugh).
            if (!javacPath) {
                process.env['PATH'] += path.delimiter + path.join(process.env['JAVA_HOME'], 'bin');
            }
        } else {
            if (javacPath) {
                // OS X has a command for finding JAVA_HOME.
                var find_java = '/usr/libexec/java_home';
                var default_java_error_msg = 'Failed to find \'JAVA_HOME\' environment variable. Try setting it manually.';
                if (fs.existsSync(find_java)) {
                    return superspawn.spawn(find_java).then(function (stdout) {
                        process.env['JAVA_HOME'] = stdout.trim();
                    }).catch(function (err) {
                        if (err) {
                            throw new CordovaError(default_java_error_msg);
                        }
                    });
                } else {
                    // See if we can derive it from javac's location.
                    // fs.realpathSync is require on Ubuntu, which symplinks from /usr/bin -> JDK
                    var maybeJavaHome = path.dirname(path.dirname(javacPath));
                    if (fs.existsSync(path.join(maybeJavaHome, 'lib', 'tools.jar'))) {
                        process.env['JAVA_HOME'] = maybeJavaHome;
                    } else {
                        throw new CordovaError(default_java_error_msg);
                    }
                }
            } else if (module.exports.isWindows()) {
                // Try to auto-detect java in the default install paths.
                var oldSilent = shelljs.config.silent;
                shelljs.config.silent = true;
                var firstJdkDir =
                    shelljs.ls(process.env['ProgramFiles'] + '\\java\\jdk*')[0] ||
                    shelljs.ls('C:\\Program Files\\java\\jdk*')[0] ||
                    shelljs.ls('C:\\Program Files (x86)\\java\\jdk*')[0];
                shelljs.config.silent = oldSilent;
                if (firstJdkDir) {
                    // shelljs always uses / in paths.
                    firstJdkDir = firstJdkDir.replace(/\//g, path.sep);
                    if (!javacPath) {
                        process.env['PATH'] += path.delimiter + path.join(firstJdkDir, 'bin');
                    }
                    process.env['JAVA_HOME'] = firstJdkDir;
                }
            }
        }
    }).then(function () {
        return Q.denodeify(child_process.exec)('javac -version')
            .then(outputs => {
                // outputs contains two entries: stdout and stderr
                // Java <= 8 writes version info to stderr, Java >= 9 to stdout
                const output = outputs.join('').trim();
                const match = /javac\s+([\d.]+)/i.exec(output);
                return match && match[1];
            }, () => {
                var msg =
                'Failed to run "javac -version", make sure that you have a JDK version 8 installed.\n' +
                'You can get it from the following location:\n' +
                'https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html';
                if (process.env['JAVA_HOME']) {
                    msg += '\n\n';
                    msg += 'Your JAVA_HOME is invalid: ' + process.env['JAVA_HOME'];
                }
                throw new CordovaError(msg);
            });
    });
};

// Returns a promise.
module.exports.check_android = function () {
    return Q().then(function () {
        var androidCmdPath = forgivingWhichSync('android');
        var adbInPath = forgivingWhichSync('adb');
        var avdmanagerInPath = forgivingWhichSync('avdmanager');
        var hasAndroidHome = !!process.env['ANDROID_HOME'] && fs.existsSync(process.env['ANDROID_HOME']);
        function maybeSetAndroidHome (value) {
            if (!hasAndroidHome && fs.existsSync(value)) {
                hasAndroidHome = true;
                process.env['ANDROID_HOME'] = value;
            }
        }
        // First ensure ANDROID_HOME is set
        // If we have no hints (nothing in PATH), try a few default locations
        if (!hasAndroidHome && !androidCmdPath && !adbInPath && !avdmanagerInPath) {
            if (process.env['ANDROID_SDK_ROOT']) {
                // Quick fix to set ANDROID_HOME according to ANDROID_SDK_ROOT
                // if ANDROID_HOME is **not** defined and
                // ANDROID_SDK_ROOT **is** defined
                // according to environment variables as documented in:
                // https://developer.android.com/studio/command-line/variables
                maybeSetAndroidHome(path.join(process.env['ANDROID_SDK_ROOT']));
            }
            if (module.exports.isWindows()) {
                // Android Studio 1.0 installer
                maybeSetAndroidHome(path.join(process.env['LOCALAPPDATA'], 'Android', 'sdk'));
                maybeSetAndroidHome(path.join(process.env['ProgramFiles'], 'Android', 'sdk'));
                // Android Studio pre-1.0 installer
                maybeSetAndroidHome(path.join(process.env['LOCALAPPDATA'], 'Android', 'android-studio', 'sdk'));
                maybeSetAndroidHome(path.join(process.env['ProgramFiles'], 'Android', 'android-studio', 'sdk'));
                // Stand-alone installer
                maybeSetAndroidHome(path.join(process.env['LOCALAPPDATA'], 'Android', 'android-sdk'));
                maybeSetAndroidHome(path.join(process.env['ProgramFiles'], 'Android', 'android-sdk'));
            } else if (module.exports.isDarwin()) {
                // Android Studio 1.0 installer
                maybeSetAndroidHome(path.join(process.env['HOME'], 'Library', 'Android', 'sdk'));
                // Android Studio pre-1.0 installer
                maybeSetAndroidHome('/Applications/Android Studio.app/sdk');
                // Stand-alone zip file that user might think to put under /Applications
                maybeSetAndroidHome('/Applications/android-sdk-macosx');
                maybeSetAndroidHome('/Applications/android-sdk');
            }
            if (process.env['HOME']) {
                // Stand-alone zip file that user might think to put under their home directory
                maybeSetAndroidHome(path.join(process.env['HOME'], 'android-sdk-macosx'));
                maybeSetAndroidHome(path.join(process.env['HOME'], 'android-sdk'));
            }
        }
        if (!hasAndroidHome) {
            // If we dont have ANDROID_HOME, but we do have some tools on the PATH, try to infer from the tooling PATH.
            var parentDir, grandParentDir;
            if (androidCmdPath) {
                parentDir = path.dirname(androidCmdPath);
                grandParentDir = path.dirname(parentDir);
                if (path.basename(parentDir) === 'tools' || fs.existsSync(path.join(grandParentDir, 'tools', 'android'))) {
                    maybeSetAndroidHome(grandParentDir);
                } else {
                    throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting it manually.\n' +
                        'Detected \'android\' command at ' + parentDir + ' but no \'tools\' directory found near.\n' +
                        'Try reinstall Android SDK or update your PATH to include valid path to SDK' + path.sep + 'tools directory.');
                }
            }
            if (adbInPath) {
                parentDir = path.dirname(adbInPath);
                grandParentDir = path.dirname(parentDir);
                if (path.basename(parentDir) === 'platform-tools') {
                    maybeSetAndroidHome(grandParentDir);
                } else {
                    throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting it manually.\n' +
                        'Detected \'adb\' command at ' + parentDir + ' but no \'platform-tools\' directory found near.\n' +
                        'Try reinstall Android SDK or update your PATH to include valid path to SDK' + path.sep + 'platform-tools directory.');
                }
            }
            if (avdmanagerInPath) {
                parentDir = path.dirname(avdmanagerInPath);
                grandParentDir = path.dirname(parentDir);
                if (path.basename(parentDir) === 'bin' && path.basename(grandParentDir) === 'tools') {
                    maybeSetAndroidHome(path.dirname(grandParentDir));
                } else {
                    throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting it manually.\n' +
                        'Detected \'avdmanager\' command at ' + parentDir + ' but no \'tools' + path.sep + 'bin\' directory found near.\n' +
                        'Try reinstall Android SDK or update your PATH to include valid path to SDK' + path.sep + 'tools' + path.sep + 'bin directory.');
                }
            }
        }
        if (!process.env['ANDROID_HOME']) {
            throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting it manually.\n' +
                'Failed to find \'android\' command in your \'PATH\'. Try update your \'PATH\' to include path to valid SDK directory.');
        }
        if (!fs.existsSync(process.env['ANDROID_HOME'])) {
            throw new CordovaError('\'ANDROID_HOME\' environment variable is set to non-existent path: ' + process.env['ANDROID_HOME'] +
                '\nTry update it manually to point to valid SDK directory.');
        }
        // Next let's make sure relevant parts of the SDK tooling is in our PATH
        if (hasAndroidHome && !androidCmdPath) {
            process.env['PATH'] += path.delimiter + path.join(process.env['ANDROID_HOME'], 'tools');
        }
        if (hasAndroidHome && !adbInPath) {
            process.env['PATH'] += path.delimiter + path.join(process.env['ANDROID_HOME'], 'platform-tools');
        }
        if (hasAndroidHome && !avdmanagerInPath) {
            process.env['PATH'] += path.delimiter + path.join(process.env['ANDROID_HOME'], 'tools', 'bin');
        }
        return hasAndroidHome;
    });
};

// TODO: is this actually needed?
module.exports.getAbsoluteAndroidCmd = function () {
    var cmd = forgivingWhichSync('android');
    if (cmd.length === 0) {
        cmd = forgivingWhichSync('sdkmanager');
    }
    if (module.exports.isWindows()) {
        return '"' + cmd + '"';
    }
    return cmd.replace(/(\s)/g, '\\$1');
};

module.exports.check_android_target = function (originalError) {
    // valid_target can look like:
    //   android-19
    //   android-L
    //   Google Inc.:Google APIs:20
    //   Google Inc.:Glass Development Kit Preview:20
    var desired_api_level = module.exports.get_target();
    return android_sdk.list_targets().then(function (targets) {
        if (targets.indexOf(desired_api_level) >= 0) {
            return targets;
        }
        var androidCmd = module.exports.getAbsoluteAndroidCmd();
        var msg = 'Please install Android target / API level: "' + desired_api_level + '".\n\n' +
            'Hint: Open the SDK manager by running: ' + androidCmd + '\n' +
            'You will require:\n' +
            '1. "SDK Platform" for API level ' + desired_api_level + '\n' +
            '2. "Android SDK Platform-tools (latest)\n' +
            '3. "Android SDK Build-tools" (latest)';
        if (originalError) {
            msg = originalError + '\n' + msg;
        }
        throw new CordovaError(msg);
    });
};

// Returns a promise.
module.exports.run = function () {
    return Q.all([this.check_java(), this.check_android()]).then(function (values) {
        console.log('Checking Java JDK and Android SDK versions');
        console.log('ANDROID_SDK_ROOT=' + process.env['ANDROID_SDK_ROOT'] + ' (recommended setting)');
        console.log('ANDROID_HOME=' + process.env['ANDROID_HOME'] + ' (DEPRECATED)');

        if (!String(values[0]).startsWith('1.8.')) {
            throw new CordovaError(
                'Requirements check failed for JDK 8 (\'1.8.*\')! Detected version: ' + values[0] + '\n' +
                'Check your ANDROID_SDK_ROOT / JAVA_HOME / PATH environment variables.'
            );
        }

        if (!values[1]) {
            throw new CordovaError('Requirements check failed for Android SDK! Android SDK was not detected.');
        }
    });
};

/**
 * Object thar represents one of requirements for current platform.
 * @param {String} id         The unique identifier for this requirements.
 * @param {String} name       The name of requirements. Human-readable field.
 * @param {String} version    The version of requirement installed. In some cases could be an array of strings
 *                            (for example, check_android_target returns an array of android targets installed)
 * @param {Boolean} installed Indicates whether the requirement is installed or not
 */
var Requirement = function (id, name, version, installed) {
    this.id = id;
    this.name = name;
    this.installed = installed || false;
    this.metadata = {
        version: version
    };
};

/**
 * Methods that runs all checks one by one and returns a result of checks
 * as an array of Requirement objects. This method intended to be used by cordova-lib check_reqs method
 *
 * @return Promise<Requirement[]> Array of requirements. Due to implementation, promise is always fulfilled.
 */
module.exports.check_all = function () {

    var requirements = [
        new Requirement('java', 'Java JDK'),
        new Requirement('androidSdk', 'Android SDK'),
        new Requirement('androidTarget', 'Android target'),
        new Requirement('gradle', 'Gradle')
    ];

    var checkFns = [
        this.check_java,
        this.check_android,
        this.check_android_target,
        this.check_gradle
    ];

    // Then execute requirement checks one-by-one
    return checkFns.reduce(function (promise, checkFn, idx) {
        // Update each requirement with results
        var requirement = requirements[idx];
        return promise.then(checkFn).then(function (version) {
            requirement.installed = true;
            requirement.metadata.version = version;
        }, function (err) {
            requirement.metadata.reason = err instanceof Error ? err.message : err;
        });
    }, Q()).then(function () {
        // When chain is completed, return requirements array to upstream API
        return requirements;
    });
};


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.
*/

let fs = require('fs');
let path = require('path');
let propertiesParser = require('properties-parser');
let events = require('cordova-common').events;

class GradlePropertiesParser {
    /**
    * Loads and Edits Gradle Properties File.
    *
    * @param {String} platformDir is the path of the Android platform directory
    */
    constructor (platformDir) {
        this._defaults = {
            // 10 seconds -> 6 seconds
            'org.gradle.daemon': 'true',

            // to allow dex in process
            'org.gradle.jvmargs': '-Xmx2048m',

            // allow NDK to be used - required by Gradle 1.5 plugin
            'android.useDeprecatedNdk': 'true'

            // Shaves another 100ms, but produces a "try at own risk" warning. Not worth it (yet):
            // 'org.gradle.parallel': 'true'
        };

        this.gradleFilePath = path.join(platformDir, 'gradle.properties');
    }

    configure (userConfigs) {
        events.emit('verbose', '[Gradle Properties] Preparing Configuration');

        this._initializeEditor();

        events.emit('verbose', '[Gradle Properties] Appending default configuration properties');
        this._configureProperties(this._defaults);

        events.emit('verbose', '[Gradle Properties] Appending custom configuration properties');
        this._configureProperties(userConfigs);

        this._save();
    }

    /**
     * Initialize the properties editor for parsing, setting, etc.
     */
    _initializeEditor () {
        // Touch empty gradle.properties file if missing.
        if (!fs.existsSync(this.gradleFilePath)) {
            events.emit('verbose', '[Gradle Properties] File missing, creating file with Cordova defaults.');
            fs.writeFileSync(this.gradleFilePath, '', 'utf-8');
        }

        // Create an editor for parsing, getting, and setting configurations.
        this.gradleFile = propertiesParser.createEditor(this.gradleFilePath);
    }

    /**
     * Validate that defaults or user configuration properties are set and
     * set the missing items.
     */
    _configureProperties (properties) {
        // Iterate though the properties and set only if missing.
        Object.keys(properties).forEach(key => {
            let value = this.gradleFile.get(key);

            if (!value) {
                // Handles the case of adding missing defaults or new properties that are missing.
                events.emit('verbose', `[Gradle Properties] Appending configuration item: ${key}=${properties[key]}`);
                this.gradleFile.set(key, properties[key]);
            } else if (value !== properties[key]) {
                if (this._defaults[key] && this._defaults[key] !== properties[key]) {
                    // Since the value does not match default, we will notify the discrepancy with Cordova's recommended value.
                    events.emit('info', `[Gradle Properties] Detected Gradle property "${key}" with the value of "${properties[key]}", Cordova's recommended value is "${this._defaults[key]}"`);
                } else {
                    // When the current value exists but does not match the new value or does matches the default key value, the new value it set.
                    events.emit('verbose', `[Gradle Properties] Updating Gradle property "${key}" with the value of "${properties[key]}"`);
                }

                // We will set the new value in either case.
                this.gradleFile.set(key, properties[key]);
            }
        });
    }

    /**
     * Saves any changes that has been made to the properties file.
     */
    _save () {
        events.emit('verbose', '[Gradle Properties] Updating and Saving File');
        this.gradleFile.save();
    }
}

module.exports = GradlePropertiesParser;


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 build = require('./build');
var path = require('path');
var Adb = require('./Adb');
var AndroidManifest = require('./AndroidManifest');
var spawn = require('cordova-common').superspawn.spawn;
var CordovaError = require('cordova-common').CordovaError;
var events = require('cordova-common').events;

/**
 * Returns a promise for the list of the device ID's found
 * @param lookHarder When true, try restarting adb if no devices are found.
 */
module.exports.list = function (lookHarder) {
    return Adb.devices().then(function (list) {
        if (list.length === 0 && lookHarder) {
            // adb kill-server doesn't seem to do the trick.
            // Could probably find a x-platform version of killall, but I'm not actually
            // sure that this scenario even happens on non-OSX machines.
            return spawn('killall', ['adb']).then(function () {
                events.emit('verbose', 'Restarting adb to see if more devices are detected.');
                return Adb.devices();
            }, function () {
                // For non-killall OS's.
                return list;
            });
        }
        return list;
    });
};

module.exports.resolveTarget = function (target) {
    return this.list(true).then(function (device_list) {
        if (!device_list || !device_list.length) {
            return Promise.reject(new CordovaError('Failed to deploy to device, no devices found.'));
        }
        // default device
        target = target || device_list[0];

        if (device_list.indexOf(target) < 0) {
            return Promise.reject(new CordovaError('ERROR: Unable to find target \'' + target + '\'.'));
        }

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

/*
 * Installs a previously built application on the device
 * and launches it.
 * Returns a promise.
 */
module.exports.install = function (target, buildResults) {
    return Promise.resolve().then(function () {
        if (target && typeof target === 'object') {
            return target;
        }
        return module.exports.resolveTarget(target);
    }).then(function (resolvedTarget) {
        var apk_path = build.findBestApkForArchitecture(buildResults, resolvedTarget.arch);
        var manifest = new AndroidManifest(path.join(__dirname, '../../app/src/main/AndroidManifest.xml'));
        var pkgName = manifest.getPackageId();
        var launchName = pkgName + '/.' + manifest.getActivity().getName();
        events.emit('log', 'Using apk: ' + apk_path);
        events.emit('log', 'Package name: ' + pkgName);

        return Adb.install(resolvedTarget.target, apk_path, { replace: true }).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 again because the ' +
                'installed app already 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(resolvedTarget.target, pkgName).then(function () {
                return Adb.install(resolvedTarget.target, apk_path, { replace: true });
            });
        }).then(function () {
            // unlock screen
            return Adb.shell(resolvedTarget.target, 'input keyevent 82');
        }).then(function () {
            return Adb.start(resolvedTarget.target, launchName);
        }).then(function () {
            events.emit('log', 'LAUNCH SUCCESS');
        });
    });
};