diff options
Diffstat (limited to 'StoneIsland/platforms/android/cordova/lib')
30 files changed, 2329 insertions, 965 deletions
diff --git a/StoneIsland/platforms/android/cordova/lib/Adb.js b/StoneIsland/platforms/android/cordova/lib/Adb.js new file mode 100644 index 00000000..84ae707e --- /dev/null +++ b/StoneIsland/platforms/android/cordova/lib/Adb.js @@ -0,0 +1,105 @@ +/** + 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; diff --git a/StoneIsland/platforms/android/cordova/lib/AndroidManifest.js b/StoneIsland/platforms/android/cordova/lib/AndroidManifest.js new file mode 100644 index 00000000..8248f593 --- /dev/null +++ b/StoneIsland/platforms/android/cordova/lib/AndroidManifest.js @@ -0,0 +1,161 @@ +/** + 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 et = require('elementtree'); +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() { + /*jshint -W069 */ + return this.doc.getroot().attrib['package']; + /*jshint +W069 */ +}; + +AndroidManifest.prototype.setPackageId = function(pkgId) { + /*jshint -W069 */ + this.doc.getroot().attrib['package'] = pkgId; + /*jshint +W069 */ + 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; + } + }; +}; + +['minSdkVersion', 'maxSdkVersion', 'targetSdkVersion'] +.forEach(function(sdkPrefName) { + // Copy variable reference to avoid closure issues + var prefName = sdkPrefName; + + AndroidManifest.prototype['get' + capitalize(prefName)] = function() { + var usesSdk = this.doc.getroot().find('./uses-sdk'); + return usesSdk && usesSdk.attrib['android:' + prefName]; + }; + + AndroidManifest.prototype['set' + capitalize(prefName)] = function(prefValue) { + var usesSdk = this.doc.getroot().find('./uses-sdk'); + + if (!usesSdk && prefValue) { // if there is no required uses-sdk element, we should create it first + usesSdk = new et.Element('uses-sdk'); + this.doc.getroot().append(usesSdk); + } + + if (prefValue) { + usesSdk.attrib['android:' + prefName] = prefValue; + } + + 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; + +function capitalize (str) { + return str.charAt(0).toUpperCase() + str.slice(1); +} diff --git a/StoneIsland/platforms/android/cordova/lib/AndroidProject.js b/StoneIsland/platforms/android/cordova/lib/AndroidProject.js new file mode 100644 index 00000000..fa1c6129 --- /dev/null +++ b/StoneIsland/platforms/android/cordova/lib/AndroidProject.js @@ -0,0 +1,210 @@ +/** + 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 AndroidStudio = require('./AndroidStudio'); +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, 'assets/www'); + if(AndroidStudio.isAndroidStudioProject(projectDir) === true) { + 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, 'AndroidManifest.xml'); + if(AndroidStudio.isAndroidStudioProject(this.projectDir) === true) { + 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; diff --git a/StoneIsland/platforms/android/cordova/lib/AndroidStudio.js b/StoneIsland/platforms/android/cordova/lib/AndroidStudio.js new file mode 100644 index 00000000..335b334b --- /dev/null +++ b/StoneIsland/platforms/android/cordova/lib/AndroidStudio.js @@ -0,0 +1,42 @@ +/* + * This is a simple routine that checks if project is an Android Studio Project + * + * @param {String} root Root folder of the project + */ + +/*jshint esnext: false */ + +var path = require('path'); +var fs = require('fs'); +var CordovaError = require('cordova-common').CordovaError; + +module.exports.isAndroidStudioProject = function isAndroidStudioProject(root) { + var eclipseFiles = ['AndroidManifest.xml', 'libs', 'res', 'project.properties', 'platform_www']; + var androidStudioFiles = ['app', 'gradle', 'app/src/main/res']; + + // assume it is an AS project and not an Eclipse project + var isEclipse = false; + var isAS = true; + + if(!fs.existsSync(root)) { + throw new CordovaError('AndroidStudio.js:inAndroidStudioProject root does not exist: ' + root); + } + + // if any of the following exists, then we are not an ASProj + eclipseFiles.forEach(function(file) { + if(fs.existsSync(path.join(root, file))) { + isEclipse = true; + } + }); + + // if it is NOT an eclipse project, check that all required files exist + if(!isEclipse) { + androidStudioFiles.forEach(function(file){ + if(!fs.existsSync(path.join(root, file))) { + console.log('missing file :: ' + file); + isAS = false; + } + }); + } + return (!isEclipse && isAS); +}; diff --git a/StoneIsland/platforms/android/cordova/lib/appinfo.js b/StoneIsland/platforms/android/cordova/lib/appinfo.js deleted file mode 100755 index 080c2ba8..00000000 --- a/StoneIsland/platforms/android/cordova/lib/appinfo.js +++ /dev/null @@ -1,41 +0,0 @@ -#!/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 fs = require('fs'); -var cachedAppInfo = null; - -function readAppInfoFromManifest() { - var manifestPath = path.join(__dirname, '..', '..', 'AndroidManifest.xml'); - var manifestData = fs.readFileSync(manifestPath, {encoding:'utf8'}); - var packageName = /\bpackage\s*=\s*"(.+?)"/.exec(manifestData); - if (!packageName) throw new Error('Could not find package name within ' + manifestPath); - var activityTag = /<activity\b[\s\S]*<\/activity>/.exec(manifestData); - if (!activityTag) throw new Error('Could not find <activity> within ' + manifestPath); - var activityName = /\bandroid:name\s*=\s*"(.+?)"/.exec(activityTag); - if (!activityName) throw new Error('Could not find android:name within ' + manifestPath); - - return packageName[1] + '/.' + activityName[1]; -} - -exports.getActivityName = function() { - return (cachedAppInfo = cachedAppInfo || readAppInfoFromManifest()); -}; diff --git a/StoneIsland/platforms/android/cordova/lib/build.js b/StoneIsland/platforms/android/cordova/lib/build.js index aa9f3d01..bd613da2 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/build.js +++ b/StoneIsland/platforms/android/cordova/lib/build.js @@ -19,477 +19,68 @@ under the License. */ -/* jshint sub:true */ - -var shell = require('shelljs'), - spawn = require('./spawn'), - Q = require('q'), +var Q = require('q'), path = require('path'), fs = require('fs'), - os = require('os'), - ROOT = path.join(__dirname, '..', '..'); -var check_reqs = require('./check_reqs'); -var exec = require('./exec'); - + nopt = require('nopt'); -var SIGNING_PROPERTIES = '-signing.properties'; -var MARKER = 'YOUR CHANGES WILL BE ERASED!'; -var TEMPLATE = - '# This file is automatically generated.\n' + - '# Do not modify this file -- ' + MARKER + '\n'; - -function findApks(directory) { - var ret = []; - if (fs.existsSync(directory)) { - fs.readdirSync(directory).forEach(function(p) { - if (path.extname(p) == '.apk') { - ret.push(path.join(directory, p)); - } - }); - } - return ret; -} +var Adb = require('./Adb'); -function sortFilesByDate(files) { - return files.map(function(p) { - return { p: p, t: fs.statSync(p).mtime }; - }).sort(function(a, b) { - var timeDiff = b.t - a.t; - return timeDiff === 0 ? a.p.length - b.p.length : timeDiff; - }).map(function(p) { return p.p; }); -} +var builders = require('./builders/builders'); +var events = require('cordova-common').events; +var spawn = require('cordova-common').superspawn.spawn; +var CordovaError = require('cordova-common').CordovaError; -function isAutoGenerated(file) { - if(fs.existsSync(file)) { - var fileContents = fs.readFileSync(file, 'utf8'); - return fileContents.indexOf(MARKER) > 0; - } - return false; -} +function parseOpts(options, resolvedTarget, projectRoot) { + options = options || {}; + options.argv = nopt({ + gradle: Boolean, + ant: Boolean, + prepenv: Boolean, + versionCode: String, + minSdkVersion: String, + gradleArg: [String, Array], + keystore: path, + alias: String, + storePassword: String, + password: String, + keystoreType: String + }, {}, options.argv, 0); -function findOutputApksHelper(dir, build_type, arch) { - var ret = findApks(dir).filter(function(candidate) { - // Need to choose between release and debug .apk. - if (build_type === 'debug') { - return /-debug/.exec(candidate) && !/-unaligned|-unsigned/.exec(candidate); - } - if (build_type === 'release') { - return /-release/.exec(candidate) && !/-unaligned/.exec(candidate); - } - return true; - }); - ret = sortFilesByDate(ret); - if (ret.length === 0) { - return ret; - } - // Assume arch-specific build if newest apk has -x86 or -arm. - var archSpecific = !!/-x86|-arm/.exec(ret[0]); - // And show only arch-specific ones (or non-arch-specific) - ret = ret.filter(function(p) { - /*jshint -W018 */ - return !!/-x86|-arm/.exec(p) == archSpecific; - /*jshint +W018 */ - }); - if (archSpecific && ret.length > 1) { - ret = ret.filter(function(p) { - return p.indexOf('-' + arch) != -1; - }); - } - - return ret; -} - -function hasCustomRules() { - return fs.existsSync(path.join(ROOT, 'custom_rules.xml')); -} - -function extractRealProjectNameFromManifest(projectPath) { - var manifestPath = path.join(projectPath, 'AndroidManifest.xml'); - var manifestData = fs.readFileSync(manifestPath, 'utf8'); - var m = /<manifest[\s\S]*?package\s*=\s*"(.*?)"/i.exec(manifestData); - if (!m) { - throw new Error('Could not find package name in ' + manifestPath); - } - - var packageName=m[1]; - var lastDotIndex = packageName.lastIndexOf('.'); - return packageName.substring(lastDotIndex + 1); -} - -function extractProjectNameFromManifest(projectPath) { - var manifestPath = path.join(projectPath, 'AndroidManifest.xml'); - var manifestData = fs.readFileSync(manifestPath, 'utf8'); - var m = /<activity[\s\S]*?android:name\s*=\s*"(.*?)"/i.exec(manifestData); - if (!m) { - throw new Error('Could not find activity name in ' + manifestPath); - } - return m[1]; -} - -function findAllUniq(data, r) { - var s = {}; - var m; - while ((m = r.exec(data))) { - s[m[1]] = 1; - } - return Object.keys(s); -} - -function readProjectProperties() { - var data = fs.readFileSync(path.join(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) + var ret = { + buildType: options.release ? 'release' : 'debug', + buildMethod: process.env.ANDROID_BUILD || 'gradle', + prepEnv: options.argv.prepenv, + arch: resolvedTarget && resolvedTarget.arch, + extraArgs: [] }; -} - -var builders = { - ant: { - getArgs: function(cmd, opts) { - var args = [cmd, '-f', path.join(ROOT, 'build.xml')]; - // custom_rules.xml is required for incremental builds. - if (hasCustomRules()) { - args.push('-Dout.dir=ant-build', '-Dgen.absolute.dir=ant-gen'); - } - if(opts.packageInfo) { - args.push('-propertyfile=' + path.join(ROOT, opts.buildType + SIGNING_PROPERTIES)); - } - return args; - }, - prepEnv: function(opts) { - return check_reqs.check_ant() - .then(function() { - // Copy in build.xml on each build so that: - // A) we don't require the Android SDK at project creation time, and - // B) we always use the SDK's latest version of it. - var sdkDir = process.env['ANDROID_HOME']; - var buildTemplate = fs.readFileSync(path.join(sdkDir, 'tools', 'lib', 'build.template'), 'utf8'); - function writeBuildXml(projectPath) { - var newData = buildTemplate.replace('PROJECT_NAME', extractProjectNameFromManifest(ROOT)); - fs.writeFileSync(path.join(projectPath, 'build.xml'), newData); - if (!fs.existsSync(path.join(projectPath, 'local.properties'))) { - fs.writeFileSync(path.join(projectPath, 'local.properties'), TEMPLATE); - } - } - writeBuildXml(ROOT); - var propertiesObj = readProjectProperties(); - var subProjects = propertiesObj.libs; - for (var i = 0; i < subProjects.length; ++i) { - writeBuildXml(path.join(ROOT, subProjects[i])); - } - if (propertiesObj.systemLibs.length > 0) { - throw new Error('Project contains at least one plugin that requires a system library. This is not supported with ANT. Please build using gradle.'); - } + if (options.argv.ant || options.argv.gradle) + ret.buildMethod = options.argv.ant ? 'ant' : 'gradle'; - var propertiesFile = opts.buildType + SIGNING_PROPERTIES; - var propertiesFilePath = path.join(ROOT, propertiesFile); - if (opts.packageInfo) { - fs.writeFileSync(propertiesFilePath, TEMPLATE + opts.packageInfo.toProperties()); - } else if(isAutoGenerated(propertiesFilePath)) { - shell.rm('-f', propertiesFilePath); - } - }); - }, + if (options.nobuild) ret.buildMethod = 'none'; - /* - * Builds the project with ant. - * Returns a promise. - */ - build: function(opts) { - // Without our custom_rules.xml, we need to clean before building. - var ret = Q(); - if (!hasCustomRules()) { - // clean will call check_ant() for us. - ret = this.clean(opts); - } + if (options.argv.versionCode) + ret.extraArgs.push('-PcdvVersionCode=' + options.argv.versionCode); - var args = this.getArgs(opts.buildType == 'debug' ? 'debug' : 'release', opts); - return check_reqs.check_ant() - .then(function() { - console.log('Executing: ant ' + args.join(' ')); - return spawn('ant', args); - }); - }, - - clean: function(opts) { - var args = this.getArgs('clean', opts); - return check_reqs.check_ant() - .then(function() { - return spawn('ant', args); - }); - }, + if (options.argv.minSdkVersion) + ret.extraArgs.push('-PcdvMinSdkVersion=' + options.argv.minSdkVersion); - findOutputApks: function(build_type) { - var binDir = path.join(ROOT, hasCustomRules() ? 'ant-build' : 'bin'); - return findOutputApksHelper(binDir, build_type, null); - } - }, - gradle: { - getArgs: function(cmd, opts) { - if (cmd == 'release') { - cmd = 'cdvBuildRelease'; - } else if (cmd == 'debug') { - cmd = 'cdvBuildDebug'; - } - var args = [cmd, '-b', path.join(ROOT, 'build.gradle')]; - if (opts.arch) { - args.push('-PcdvBuildArch=' + opts.arch); - } - - // 10 seconds -> 6 seconds - args.push('-Dorg.gradle.daemon=true'); - args.push.apply(args, opts.extraArgs); - // Shaves another 100ms, but produces a "try at own risk" warning. Not worth it (yet): - // args.push('-Dorg.gradle.parallel=true'); - return args; - }, - - // Makes the project buildable, minus the gradle wrapper. - prepBuildFiles: function() { - var projectPath = ROOT; - // Update the version of build.gradle in each dependent library. - var pluginBuildGradle = path.join(projectPath, 'cordova', 'lib', 'plugin-build.gradle'); - var propertiesObj = readProjectProperties(); - var subProjects = propertiesObj.libs; - for (var i = 0; i < subProjects.length; ++i) { - if (subProjects[i] !== 'CordovaLib') { - shell.cp('-f', pluginBuildGradle, path.join(ROOT, subProjects[i], 'build.gradle')); - } - } - - var name = extractRealProjectNameFromManifest(ROOT); - //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; - }); - - // Write the settings.gradle file. - fs.writeFileSync(path.join(projectPath, 'settings.gradle'), - '// GENERATED FILE - DO NOT EDIT\n' + - 'include ":"\n' + settingsGradlePaths.join('')); - // Update dependencies within build.gradle. - var buildGradle = fs.readFileSync(path.join(projectPath, 'build.gradle'), 'utf8'); - var depsList = ''; - subProjects.forEach(function(p) { - var libName=p.replace(/[/\\]/g, ':').replace(name+'-',''); - depsList += ' debugCompile project(path: "' + libName + '", configuration: "debug")\n'; - depsList += ' releaseCompile project(path: "' + libName + '", configuration: "release")\n'; - }); - // 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 Error('Unsupported system library (does not work with gradle): ' + p); - } - } - depsList += ' compile "' + 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'); - fs.writeFileSync(path.join(projectPath, 'build.gradle'), buildGradle); - }, - - prepEnv: function(opts) { - var self = this; - return check_reqs.check_gradle() - .then(function() { - return self.prepBuildFiles(); - }).then(function() { - // Copy the gradle wrapper on each build so that: - // A) we don't require the Android SDK at project creation time, and - // B) we always use the SDK's latest version of it. - var projectPath = ROOT; - // check_reqs ensures that this is set. - var sdkDir = process.env['ANDROID_HOME']; - var wrapperDir = path.join(sdkDir, 'tools', 'templates', 'gradle', 'wrapper'); - if (process.platform == 'win32') { - shell.rm('-f', path.join(projectPath, 'gradlew.bat')); - shell.cp(path.join(wrapperDir, 'gradlew.bat'), projectPath); - } else { - shell.rm('-f', path.join(projectPath, 'gradlew')); - shell.cp(path.join(wrapperDir, 'gradlew'), projectPath); - } - shell.rm('-rf', path.join(projectPath, 'gradle', 'wrapper')); - shell.mkdir('-p', path.join(projectPath, 'gradle')); - shell.cp('-r', path.join(wrapperDir, 'gradle', 'wrapper'), path.join(projectPath, 'gradle')); - - // 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 = 'distributionUrl=http\\://services.gradle.org/distributions/gradle-2.2.1-all.zip'; - var gradleWrapperPropertiesPath = path.join(projectPath, 'gradle', 'wrapper', 'gradle-wrapper.properties'); - shell.chmod('u+w', gradleWrapperPropertiesPath); - shell.sed('-i', distributionUrlRegex, distributionUrl, gradleWrapperPropertiesPath); - - var propertiesFile = opts.buildType + SIGNING_PROPERTIES; - var propertiesFilePath = path.join(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: function(opts) { - var wrapper = path.join(ROOT, 'gradlew'); - var args = this.getArgs(opts.buildType == 'debug' ? 'debug' : 'release', opts); - return Q().then(function() { - console.log('Running: ' + wrapper + ' ' + args.join(' ')); - return spawn(wrapper, args); - }); - }, - - clean: function(opts) { - var builder = this; - var wrapper = path.join(ROOT, 'gradlew'); - var args = builder.getArgs('clean', opts); - return Q().then(function() { - console.log('Running: ' + wrapper + ' ' + args.join(' ')); - return spawn(wrapper, args); - }); - }, - - findOutputApks: function(build_type, arch) { - var binDir = path.join(ROOT, 'build', 'outputs', 'apk'); - return findOutputApksHelper(binDir, build_type, arch); - } - }, - - none: { - prepEnv: function() { - return Q(); - }, - build: function() { - console.log('Skipping build...'); - return Q(null); - }, - clean: function() { - return Q(); - }, - findOutputApks: function(build_type, arch) { - return sortFilesByDate(builders.ant.findOutputApks(build_type, arch).concat(builders.gradle.findOutputApks(build_type, arch))); - } + if (options.argv.gradleArg) { + ret.extraArgs = ret.extraArgs.concat(options.argv.gradleArg); } -}; -module.exports.isBuildFlag = function(flag) { - return /^--(debug|release|ant|gradle|nobuild|versionCode=|minSdkVersion=|gradleArg=|keystore=|alias=|password=|storePassword=|keystoreType=|buildConfig=)/.exec(flag); -}; + var packageArgs = {}; -function parseOpts(options, resolvedTarget) { - // Backwards-compatibility: Allow a single string argument - if (typeof options == 'string') options = [options]; + if (options.argv.keystore) + packageArgs.keystore = path.relative(projectRoot, path.resolve(options.argv.keystore)); - var ret = { - buildType: 'debug', - buildMethod: process.env['ANDROID_BUILD'] || 'gradle', - arch: null, - extraArgs: [] - }; + ['alias','storePassword','password','keystoreType'].forEach(function (flagName) { + if (options.argv[flagName]) + packageArgs[flagName] = options.argv[flagName]; + }); - var multiValueArgs = { - 'versionCode': true, - 'minSdkVersion': true, - 'gradleArg': true, - 'keystore' : true, - 'alias' : true, - 'password' : true, - 'storePassword' : true, - 'keystoreType' : true, - 'buildConfig' : true - }; - var packageArgs = {}; - var buildConfig; - // Iterate through command line options - for (var i=0; options && (i < options.length); ++i) { - if (/^--/.exec(options[i])) { - var keyValue = options[i].substring(2).split('='); - var flagName = keyValue.shift(); - var flagValue = keyValue.join('='); - if (multiValueArgs[flagName] && !flagValue) { - flagValue = options[i + 1]; - ++i; - } - switch(flagName) { - case 'debug': - case 'release': - ret.buildType = flagName; - break; - case 'ant': - case 'gradle': - ret.buildMethod = flagName; - break; - case 'device': - case 'emulator': - // Don't need to do anything special to when building for device vs emulator. - // iOS uses this flag to switch on architecture. - break; - case 'prepenv' : - ret.prepEnv = true; - break; - case 'nobuild' : - ret.buildMethod = 'none'; - break; - case 'versionCode': - ret.extraArgs.push('-PcdvVersionCode=' + flagValue); - break; - case 'minSdkVersion': - ret.extraArgs.push('-PcdvMinSdkVersion=' + flagValue); - break; - case 'gradleArg': - ret.extraArgs.push(flagValue); - break; - case 'keystore': - packageArgs.keystore = path.relative(ROOT, path.resolve(flagValue)); - break; - case 'alias': - case 'storePassword': - case 'password': - case 'keystoreType': - packageArgs[flagName] = flagValue; - break; - case 'buildConfig': - buildConfig = flagValue; - break; - default : - console.warn('Build option --\'' + flagName + '\' not recognized (ignoring).'); - } - } else { - console.warn('Build option \'' + options[i] + '\' not recognized (ignoring).'); - } - } + var buildConfig = options.buildConfig; // If some values are not specified as command line arguments - use build config to supplement them. // Command line arguemnts have precedence over build config. @@ -497,16 +88,17 @@ function parseOpts(options, resolvedTarget) { if (!fs.existsSync(buildConfig)) { throw new Error('Specified build config file does not exist: ' + buildConfig); } - console.log('Reading build config file: '+ path.resolve(buildConfig)); - var config = JSON.parse(fs.readFileSync(buildConfig, 'utf8')); + 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(path.isAbsolute(androidInfo.keystore)) { - packageArgs.keystore = androidInfo.keystore; - } else { - packageArgs.keystore = path.relative(ROOT, path.join(path.dirname(buildConfig), androidInfo.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'].forEach(function (key){ @@ -514,6 +106,7 @@ function parseOpts(options, resolvedTarget) { }); } } + if (packageArgs.keystore && packageArgs.alias) { ret.packageInfo = new PackageInfo(packageArgs.keystore, packageArgs.alias, packageArgs.storePassword, packageArgs.password, packageArgs.keystoreType); @@ -521,10 +114,9 @@ function parseOpts(options, resolvedTarget) { if(!ret.packageInfo) { if(Object.keys(packageArgs).length > 0) { - console.warn('\'keystore\' and \'alias\' need to be specified to generate a signed archive.'); + events.emit('warn', '\'keystore\' and \'alias\' need to be specified to generate a signed archive.'); } } - ret.arch = resolvedTarget && resolvedTarget.arch; return ret; } @@ -534,41 +126,39 @@ function parseOpts(options, resolvedTarget) { * Returns a promise. */ module.exports.runClean = function(options) { - var opts = parseOpts(options); - var builder = builders[opts.buildMethod]; + var opts = parseOpts(options, null, this.root); + var builder = builders.getBuilder(opts.buildMethod); return builder.prepEnv(opts) .then(function() { return builder.clean(opts); - }).then(function() { - shell.rm('-rf', path.join(ROOT, 'out')); - - ['debug', 'release'].forEach(function(config) { - var propertiesFilePath = path.join(ROOT, config + SIGNING_PROPERTIES); - if(isAutoGenerated(propertiesFilePath)){ - shell.rm('-f', propertiesFilePath); - } - }); }); }; -/* - * Builds the project with the specifed options - * Returns a promise. +/** + * 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); - var builder = builders[opts.buildMethod]; + var opts = parseOpts(options, optResolvedTarget, this.root); + var builder = builders.getBuilder(opts.buildMethod); return builder.prepEnv(opts) .then(function() { if (opts.prepEnv) { - console.log('Build file successfully prepared.'); + events.emit('verbose', 'Build file successfully prepared.'); return; } return builder.build(opts) .then(function() { var apkPaths = builder.findOutputApks(opts.buildType, opts.arch); - console.log('Built the following apk(s):'); - console.log(' ' + apkPaths.join('\n ')); + events.emit('log', 'Built the following apk(s): \n\t' + apkPaths.join('\n\t')); return { apkPaths: apkPaths, buildType: opts.buildType, @@ -578,46 +168,38 @@ module.exports.run = function(options, optResolvedTarget) { }); }; -// Called by plugman after installing plugins, and by create script after creating project. -module.exports.prepBuildFiles = function() { - var builder = builders['gradle']; - return builder.prepBuildFiles(); -}; - /* * Detects the architecture of a device/emulator * Returns "arm" or "x86". */ module.exports.detectArchitecture = function(target) { function helper() { - return exec('adb -s ' + target + ' shell cat /proc/cpuinfo', os.tmpdir()) + return Adb.shell(target, 'cat /proc/cpuinfo') .then(function(output) { - if (/intel/i.exec(output)) { - return 'x86'; - } - return 'arm'; + 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, 'Device communication timed out. Try unplugging & replugging the device.') + 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. - return exec('killall adb') + events.emit('verbose', 'adb timed out while detecting device/emulator architecture. Killing adb and trying again.'); + return spawn('killall', ['adb']) .then(function() { - console.log('adb seems hung. retrying.'); return helper() .then(null, function() { // The double kill is sadly often necessary, at least on mac. - console.log('Now device not found... restarting adb again.'); - return exec('killall adb') + 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('USB is flakey. Try unplugging & replugging the device.'); + return Q.reject(new CordovaError('adb timed out a third time while detecting device/emulator architecture. Try unplugging & replugging the device.')); }); }); }); @@ -632,16 +214,18 @@ module.exports.detectArchitecture = function(target) { 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(p); + return /-debug/.exec(apkName); } - return !/-debug/.exec(p); + return !/-debug/.exec(apkName); }); var archPattern = new RegExp('-' + arch); var hasArchPattern = /-x86|-arm/; for (var i = 0; i < paths.length; ++i) { - if (hasArchPattern.exec(paths[i])) { - if (archPattern.exec(paths[i])) { + var apkName = path.basename(paths[i]); + if (hasArchPattern.exec(apkName)) { + if (archPattern.exec(apkName)) { return paths[i]; } } else { @@ -695,7 +279,7 @@ PackageInfo.prototype = { }; module.exports.help = function() { - console.log('Usage: ' + path.relative(process.cwd(), path.join(ROOT, 'cordova', 'build')) + ' [flags] [Signed APK flags]'); + 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'); diff --git a/StoneIsland/platforms/android/cordova/lib/builders/AntBuilder.js b/StoneIsland/platforms/android/cordova/lib/builders/AntBuilder.js new file mode 100644 index 00000000..4e0f71ab --- /dev/null +++ b/StoneIsland/platforms/android/cordova/lib/builders/AntBuilder.js @@ -0,0 +1,156 @@ +/* + 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 fs = require('fs'); +var path = require('path'); +var util = require('util'); +var shell = require('shelljs'); +var spawn = require('cordova-common').superspawn.spawn; +var CordovaError = require('cordova-common').CordovaError; +var check_reqs = require('../check_reqs'); + +var SIGNING_PROPERTIES = '-signing.properties'; +var MARKER = 'YOUR CHANGES WILL BE ERASED!'; +var TEMPLATE = + '# This file is automatically generated.\n' + + '# Do not modify this file -- ' + MARKER + '\n'; + +var GenericBuilder = require('./GenericBuilder'); + +function AntBuilder (projectRoot) { + GenericBuilder.call(this, projectRoot); + + this.binDirs = {ant: this.binDirs.ant}; +} + +util.inherits(AntBuilder, GenericBuilder); + +AntBuilder.prototype.getArgs = function(cmd, opts) { + var args = [cmd, '-f', path.join(this.root, 'build.xml')]; + // custom_rules.xml is required for incremental builds. + if (hasCustomRules(this.root)) { + args.push('-Dout.dir=ant-build', '-Dgen.absolute.dir=ant-gen'); + } + if(opts.packageInfo) { + args.push('-propertyfile=' + path.join(this.root, opts.buildType + SIGNING_PROPERTIES)); + } + return args; +}; + +AntBuilder.prototype.prepEnv = function(opts) { + var self = this; + return check_reqs.check_ant() + .then(function() { + // Copy in build.xml on each build so that: + // A) we don't require the Android SDK at project creation time, and + // B) we always use the SDK's latest version of it. + /*jshint -W069 */ + var sdkDir = process.env['ANDROID_HOME']; + /*jshint +W069 */ + var buildTemplate = fs.readFileSync(path.join(sdkDir, 'tools', 'lib', 'build.template'), 'utf8'); + function writeBuildXml(projectPath) { + var newData = buildTemplate.replace('PROJECT_NAME', self.extractRealProjectNameFromManifest()); + fs.writeFileSync(path.join(projectPath, 'build.xml'), newData); + if (!fs.existsSync(path.join(projectPath, 'local.properties'))) { + fs.writeFileSync(path.join(projectPath, 'local.properties'), TEMPLATE); + } + } + writeBuildXml(self.root); + var propertiesObj = self.readProjectProperties(); + var subProjects = propertiesObj.libs; + for (var i = 0; i < subProjects.length; ++i) { + writeBuildXml(path.join(self.root, subProjects[i])); + } + if (propertiesObj.systemLibs.length > 0) { + throw new CordovaError('Project contains at least one plugin that requires a system library. This is not supported with ANT. Use gradle instead.'); + } + + 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 ant. + * Returns a promise. + */ +AntBuilder.prototype.build = function(opts) { + // Without our custom_rules.xml, we need to clean before building. + var ret = Q(); + if (!hasCustomRules(this.root)) { + // clean will call check_ant() for us. + ret = this.clean(opts); + } + + var args = this.getArgs(opts.buildType == 'debug' ? 'debug' : 'release', opts); + return check_reqs.check_ant() + .then(function() { + return spawn('ant', args, {stdio: 'pipe'}); + }).progress(function (stdio){ + if (stdio.stderr) { + process.stderr.write(stdio.stderr); + } else { + process.stdout.write(stdio.stdout); + } + }).catch(function (error) { + if (error.toString().indexOf('Unable to resolve project target') >= 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); + }); +}; + +AntBuilder.prototype.clean = function(opts) { + var args = this.getArgs('clean', opts); + var self = this; + return check_reqs.check_ant() + .then(function() { + return spawn('ant', args, {stdio: 'inherit'}); + }) + .then(function () { + shell.rm('-rf', path.join(self.root, 'out')); + + ['debug', 'release'].forEach(function(config) { + var propertiesFilePath = path.join(self.root, config + SIGNING_PROPERTIES); + if(isAutoGenerated(propertiesFilePath)){ + shell.rm('-f', propertiesFilePath); + } + }); + }); +}; + +module.exports = AntBuilder; + +function hasCustomRules(projectRoot) { + return fs.existsSync(path.join(projectRoot, 'custom_rules.xml')); +} + +function isAutoGenerated(file) { + return fs.existsSync(file) && fs.readFileSync(file, 'utf8').indexOf(MARKER) > 0; +} diff --git a/StoneIsland/platforms/android/cordova/lib/builders/GenericBuilder.js b/StoneIsland/platforms/android/cordova/lib/builders/GenericBuilder.js new file mode 100644 index 00000000..362da431 --- /dev/null +++ b/StoneIsland/platforms/android/cordova/lib/builders/GenericBuilder.js @@ -0,0 +1,147 @@ +/* + 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 fs = require('fs'); +var path = require('path'); +var shell = require('shelljs'); +var events = require('cordova-common').events; +var CordovaError = require('cordova-common').CordovaError; + +function GenericBuilder (projectDir) { + this.root = projectDir || path.resolve(__dirname, '../../..'); + this.binDirs = { + ant: path.join(this.root, hasCustomRules(this.root) ? 'ant-build' : 'bin'), + gradle: path.join(this.root, 'build', 'outputs', 'apk') + }; +} + +function hasCustomRules(projectRoot) { + return fs.existsSync(path.join(projectRoot, 'custom_rules.xml')); +} + +GenericBuilder.prototype.prepEnv = function() { + return Q(); +}; + +GenericBuilder.prototype.build = function() { + events.emit('log', 'Skipping build...'); + return Q(null); +}; + +GenericBuilder.prototype.clean = function() { + return Q(); +}; + +GenericBuilder.prototype.findOutputApks = function(build_type, arch) { + var self = this; + return Object.keys(this.binDirs) + .reduce(function (result, builderName) { + var binDir = self.binDirs[builderName]; + return result.concat(findOutputApksHelper(binDir, build_type, builderName === 'ant' ? null : arch)); + }, []) + .sort(apkSorter); +}; + +GenericBuilder.prototype.readProjectProperties = function () { + 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) + }; +}; + +GenericBuilder.prototype.extractRealProjectNameFromManifest = function () { + var manifestPath = path.join(this.root, '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); +}; + +module.exports = GenericBuilder; + +function apkSorter(fileA, fileB) { + // De-prioritize unsigned builds + var unsignedRE = /-unsigned/; + if (unsignedRE.exec(fileA)) { + return 1; + } else if (unsignedRE.exec(fileB)) { + return -1; + } + + var timeDiff = fs.statSync(fileA).mtime - fs.statSync(fileB).mtime; + return timeDiff === 0 ? fileA.length - fileB.length : timeDiff; +} + +function findOutputApksHelper(dir, build_type, arch) { + var shellSilent = shell.config.silent; + shell.config.silent = true; + + var ret = shell.ls(path.join(dir, '*.apk')) + .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) { + /*jshint -W018 */ + return !!/-x86|-arm/.exec(path.basename(p)) == archSpecific; + /*jshint +W018 */ + }); + + if (archSpecific && ret.length > 1 && arch) { + ret = ret.filter(function(p) { + return path.basename(p).indexOf('-' + arch) != -1; + }); + } + + return ret; +} diff --git a/StoneIsland/platforms/android/cordova/lib/builders/GradleBuilder.js b/StoneIsland/platforms/android/cordova/lib/builders/GradleBuilder.js new file mode 100644 index 00000000..f415646e --- /dev/null +++ b/StoneIsland/platforms/android/cordova/lib/builders/GradleBuilder.js @@ -0,0 +1,266 @@ +/* + 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 fs = require('fs'); +var util = require('util'); +var path = require('path'); +var shell = require('shelljs'); +var spawn = require('cordova-common').superspawn.spawn; +var CordovaError = require('cordova-common').CordovaError; +var check_reqs = require('../check_reqs'); + +var GenericBuilder = require('./GenericBuilder'); + +var MARKER = 'YOUR CHANGES WILL BE ERASED!'; +var SIGNING_PROPERTIES = '-signing.properties'; +var TEMPLATE = + '# This file is automatically generated.\n' + + '# Do not modify this file -- ' + MARKER + '\n'; + +function GradleBuilder (projectRoot) { + GenericBuilder.call(this, projectRoot); + + this.binDirs = {gradle: this.binDirs.gradle}; +} + +util.inherits(GradleBuilder, GenericBuilder); + +GradleBuilder.prototype.getArgs = function(cmd, opts) { + if (cmd == 'release') { + cmd = 'cdvBuildRelease'; + } else if (cmd == 'debug') { + cmd = 'cdvBuildDebug'; + } + var args = [cmd, '-b', path.join(this.root, 'build.gradle')]; + if (opts.arch) { + args.push('-PcdvBuildArch=' + opts.arch); + } + + // 10 seconds -> 6 seconds + args.push('-Dorg.gradle.daemon=true'); + // to allow dex in process + args.push('-Dorg.gradle.jvmargs=-Xmx2048m'); + // allow NDK to be used - required by Gradle 1.5 plugin + args.push('-Pandroid.useDeprecatedNdk=true'); + args.push.apply(args, opts.extraArgs); + // Shaves another 100ms, but produces a "try at own risk" warning. Not worth it (yet): + // args.push('-Dorg.gradle.parallel=true'); + return args; +}; + +// Makes the project buildable, minus the gradle wrapper. +GradleBuilder.prototype.prepBuildFiles = function() { + // 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; + 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; + }); + + // Write the settings.gradle file. + 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, '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) { + console.log('Subproject Path: ' + p); + var libName=p.replace(/[/\\]/g, ':').replace(name+'-',''); + depsList += ' debugCompile(project(path: "' + libName + '", configuration: "debug"))'; + insertExclude(p); + depsList += ' releaseCompile(project(path: "' + libName + '", configuration: "release"))'; + 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 += ' compile "' + 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'); + fs.writeFileSync(path.join(this.root, 'build.gradle'), buildGradle); +}; + +GradleBuilder.prototype.prepEnv = function(opts) { + var self = this; + return check_reqs.check_gradle() + .then(function() { + return self.prepBuildFiles(); + }).then(function() { + // Copy the gradle wrapper on each build so that: + // A) we don't require the Android SDK at project creation time, and + // B) we always use the SDK's latest version of it. + // check_reqs ensures that this is set. + /*jshint -W069 */ + var sdkDir = process.env['ANDROID_HOME']; + /*jshint +W069 */ + var wrapperDir = path.join(sdkDir, 'tools', 'templates', 'gradle', 'wrapper'); + if (process.platform == 'win32') { + shell.rm('-f', path.join(self.root, 'gradlew.bat')); + shell.cp(path.join(wrapperDir, 'gradlew.bat'), self.root); + } else { + shell.rm('-f', path.join(self.root, 'gradlew')); + shell.cp(path.join(wrapperDir, 'gradlew'), self.root); + } + shell.rm('-rf', path.join(self.root, 'gradle', 'wrapper')); + shell.mkdir('-p', path.join(self.root, 'gradle')); + shell.cp('-r', path.join(wrapperDir, 'gradle', 'wrapper'), path.join(self.root, 'gradle')); + + // 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/; + /*jshint -W069 */ + var distributionUrl = process.env['CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL'] || 'https\\://services.gradle.org/distributions/gradle-2.14.1-all.zip'; + /*jshint +W069 */ + 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. + */ +GradleBuilder.prototype.build = function(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); + }); +}; + +GradleBuilder.prototype.clean = function(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); + } + }); + }); +}; + +module.exports = GradleBuilder; + +function isAutoGenerated(file) { + return fs.existsSync(file) && fs.readFileSync(file, 'utf8').indexOf(MARKER) > 0; +} diff --git a/StoneIsland/platforms/android/cordova/lib/builders/builders.js b/StoneIsland/platforms/android/cordova/lib/builders/builders.js new file mode 100644 index 00000000..4921c49a --- /dev/null +++ b/StoneIsland/platforms/android/cordova/lib/builders/builders.js @@ -0,0 +1,47 @@ +/* + 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 CordovaError = require('cordova-common').CordovaError; + +var knownBuilders = { + ant: 'AntBuilder', + gradle: 'GradleBuilder', + none: 'GenericBuilder' +}; + +/** + * Helper method that instantiates and returns a builder for specified build + * type. + * + * @param {String} builderType Builder name to construct and return. Must + * be one of 'ant', 'gradle' or 'none' + * + * @return {Builder} A builder instance for specified build type. + */ +module.exports.getBuilder = function (builderType, projectRoot) { + if (!knownBuilders[builderType]) + throw new CordovaError('Builder ' + builderType + ' is not supported.'); + + try { + var Builder = require('./' + knownBuilders[builderType]); + return new Builder(projectRoot); + } catch (err) { + throw new CordovaError('Failed to instantiate ' + knownBuilders[builderType] + ' builder: ' + err); + } +}; diff --git a/StoneIsland/platforms/android/cordova/lib/check_reqs.js b/StoneIsland/platforms/android/cordova/lib/check_reqs.js index 9d251596..ac6fa4c1 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/check_reqs.js +++ b/StoneIsland/platforms/android/cordova/lib/check_reqs.js @@ -26,15 +26,14 @@ var shelljs = require('shelljs'), Q = require('q'), path = require('path'), fs = require('fs'), - which = require('which'), ROOT = path.join(__dirname, '..', '..'); +var CordovaError = require('cordova-common').CordovaError; var isWindows = process.platform == 'win32'; function forgivingWhichSync(cmd) { try { - // TODO: Should use shelljs.which() here to have one less dependency. - return fs.realpathSync(which.sync(cmd)); + return fs.realpathSync(shelljs.which(cmd)); } catch (e) { return ''; } @@ -43,7 +42,7 @@ function forgivingWhichSync(cmd) { function tryCommand(cmd, errMsg, catchStderr) { var d = Q.defer(); child_process.exec(cmd, function(err, stdout, stderr) { - if (err) d.reject(new Error(errMsg)); + if (err) d.reject(new CordovaError(errMsg)); // Sometimes it is necessary to return an stderr instead of stdout in case of success, since // some commands prints theirs output to stderr instead of stdout. 'javac' is the example else d.resolve((catchStderr ? stderr : stdout).trim()); @@ -83,12 +82,12 @@ module.exports.check_ant = function() { module.exports.check_gradle = function() { var sdkDir = process.env['ANDROID_HOME']; if (!sdkDir) - return Q.reject('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.'); + 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 wrapperDir = path.join(sdkDir, 'tools', 'templates', 'gradle', 'wrapper'); if (!fs.existsSync(wrapperDir)) { - return Q.reject(new Error('Could not find gradle wrapper within Android SDK. Might need to update your Android SDK.\n' + + return Q.reject(new CordovaError('Could not find gradle wrapper within Android SDK. Might need to update your Android SDK.\n' + 'Looked here: ' + wrapperDir)); } return Q.when(); @@ -120,7 +119,7 @@ module.exports.check_java = function() { if (fs.existsSync(path.join(maybeJavaHome, 'lib', 'tools.jar'))) { process.env['JAVA_HOME'] = maybeJavaHome; } else { - throw new Error(msg); + throw new CordovaError(msg); } } } else if (isWindows) { @@ -143,22 +142,21 @@ module.exports.check_java = function() { } } }).then(function() { - var msg = - 'Failed to run "java -version", make sure that you have a JDK installed.\n' + - 'You can get it from: http://www.oracle.com/technetwork/java/javase/downloads.\n'; - if (process.env['JAVA_HOME']) { - msg += 'Your JAVA_HOME is invalid: ' + process.env['JAVA_HOME'] + '\n'; - } - return tryCommand('java -version', msg) - .then(function() { + var msg = + 'Failed to run "javac -version", make sure that you have a JDK installed.\n' + + 'You can get it from: http://www.oracle.com/technetwork/java/javase/downloads.\n'; + if (process.env['JAVA_HOME']) { + msg += 'Your JAVA_HOME is invalid: ' + process.env['JAVA_HOME'] + '\n'; + } // We use tryCommand with catchStderr = true, because // javac writes version info to stderr instead of stdout - return tryCommand('javac -version', msg, true); - }).then(function (output) { - var match = /javac ((?:\d+\.)+(?:\d+))/i.exec(output)[1]; - return match && match[1]; + return tryCommand('javac -version', msg, true) + .then(function (output) { + //Let's check for at least Java 8, and keep it future proof so we can support Java 10 + var match = /javac ((?:1\.)(?:[8-9]\.)(?:\d+))|((?:1\.)(?:[1-9]\d+\.)(?:\d+))/i.exec(output); + return match && match[1]; + }); }); - }); }; // Returns a promise. @@ -212,7 +210,7 @@ module.exports.check_android = function() { process.env['ANDROID_HOME'] = grandParentDir; hasAndroidHome = true; } else { - throw new Error('Failed to find \'ANDROID_HOME\' environment variable. Try setting setting it manually.\n' + + throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting 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 path to valid SDK directory.'); } @@ -221,27 +219,32 @@ module.exports.check_android = function() { process.env['PATH'] += path.delimiter + path.join(process.env['ANDROID_HOME'], 'platform-tools'); } if (!process.env['ANDROID_HOME']) { - throw new Error('Failed to find \'ANDROID_HOME\' environment variable. Try setting setting it manually.\n' + + throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting 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 Error('\'ANDROID_HOME\' environment variable is set to non-existent path: ' + 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.'); } + return hasAndroidHome; }); }; -module.exports.getAbsoluteAndroidCmd = function() { - return forgivingWhichSync('android').replace(/(\s)/g, '\\$1'); +module.exports.getAbsoluteAndroidCmd = function () { + var cmd = forgivingWhichSync('android'); + if (process.platform === 'win32') { + return '"' + cmd + '"'; + } + return cmd.replace(/(\s)/g, '\\$1'); }; -module.exports.check_android_target = function(valid_target) { +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 - if (!valid_target) valid_target = module.exports.get_target(); + var valid_target = module.exports.get_target(); var msg = 'Android SDK not found. Make sure that it is installed. If it is not at the default location, set the ANDROID_HOME environment variable.'; return tryCommand('android list targets --compact', msg) .then(function(output) { @@ -251,24 +254,38 @@ module.exports.check_android_target = function(valid_target) { } var androidCmd = module.exports.getAbsoluteAndroidCmd(); - throw new Error('Please install Android target: "' + valid_target + '".\n\n' + + var msg = 'Please install Android target: "' + valid_target + '".\n\n' + 'Hint: Open the SDK manager by running: ' + androidCmd + '\n' + 'You will require:\n' + '1. "SDK Platform" for ' + valid_target + '\n' + '2. "Android SDK Platform-tools (latest)\n' + - '3. "Android SDK Build-tools" (latest)'); + '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(this.check_android_target)]) - .then(function() { - console.log('ANDROID_HOME=' + process.env['ANDROID_HOME']); - console.log('JAVA_HOME=' + process.env['JAVA_HOME']); - }); + return Q.all([this.check_java(), this.check_android()]) + .then(function(values) { + console.log('ANDROID_HOME=' + process.env['ANDROID_HOME']); + console.log('JAVA_HOME=' + process.env['JAVA_HOME']); + + if (!values[0]) { + throw new CordovaError('Requirements check failed for JDK 1.8 or greater'); + } + + + if (!values[1]) { + throw new CordovaError('Requirements check failed for Android SDK'); + } + }); }; + /** * Object thar represents one of requirements for current platform. * @param {String} id The unique identifier for this requirements. diff --git a/StoneIsland/platforms/android/cordova/lib/device.js b/StoneIsland/platforms/android/cordova/lib/device.js index c13fdc40..4b171db6 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/device.js +++ b/StoneIsland/platforms/android/cordova/lib/device.js @@ -19,40 +19,30 @@ under the License. */ -var exec = require('./exec'), - Q = require('q'), - os = require('os'), - build = require('./build'), - appinfo = require('./appinfo'); +var Q = require('q'), + 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) { - function helper() { - return exec('adb devices', os.tmpdir()) - .then(function(output) { - var response = output.split('\n'); - var device_list = []; - for (var i = 1; i < response.length; i++) { - if (response[i].match(/\w+\tdevice/) && !response[i].match(/emulator/)) { - device_list.push(response[i].replace(/\tdevice/, '').replace('\r', '')); - } - } - return device_list; - }); - } - return helper() + 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 exec('killall adb') + return spawn('killall', ['adb']) .then(function() { - console.log('Restarting adb to see if more devices are detected.'); - return helper(); + events.emit('verbose', 'Restarting adb to see if more devices are detected.'); + return Adb.devices(); }, function() { // For non-killall OS's. return list; @@ -66,7 +56,7 @@ module.exports.resolveTarget = function(target) { return this.list(true) .then(function(device_list) { if (!device_list || !device_list.length) { - return Q.reject('ERROR: Failed to deploy to device, no devices found.'); + return Q.reject(new CordovaError('Failed to deploy to device, no devices found.')); } // default device target = target || device_list[0]; @@ -95,27 +85,36 @@ module.exports.install = function(target, buildResults) { return module.exports.resolveTarget(target); }).then(function(resolvedTarget) { var apk_path = build.findBestApkForArchitecture(buildResults, resolvedTarget.arch); - var launchName = appinfo.getActivityName(); - console.log('Using apk: ' + apk_path); - console.log('Installing app on device...'); - var cmd = 'adb -s ' + resolvedTarget.target + ' install -r "' + apk_path + '"'; - return exec(cmd, os.tmpdir()) - .then(function(output) { - if (output.match(/Failure/)) return Q.reject('ERROR: Failed to install apk to device: ' + output); + var manifest = new AndroidManifest(path.join(__dirname, '../../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); - //unlock screen - var cmd = 'adb -s ' + resolvedTarget.target + ' shell input keyevent 82'; - return exec(cmd, os.tmpdir()); - }, function(err) { return Q.reject('ERROR: Failed to install apk to device: ' + err); }) + 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() { - // launch the application - console.log('Launching application...'); - var cmd = 'adb -s ' + resolvedTarget.target + ' shell am start -W -a android.intent.action.MAIN -n ' + launchName; - return exec(cmd, os.tmpdir()); + //unlock screen + return Adb.shell(resolvedTarget.target, 'input keyevent 82'); + }).then(function() { + return Adb.start(resolvedTarget.target, launchName); }).then(function() { - console.log('LAUNCH SUCCESS'); - }, function(err) { - return Q.reject('ERROR: Failed to launch application on device: ' + err); + events.emit('log', 'LAUNCH SUCCESS'); }); }); }; diff --git a/StoneIsland/platforms/android/cordova/lib/emulator.js b/StoneIsland/platforms/android/cordova/lib/emulator.js index e81dd679..ff1e261c 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/emulator.js +++ b/StoneIsland/platforms/android/cordova/lib/emulator.js @@ -21,11 +21,14 @@ /* jshint sub:true */ -var exec = require('./exec'); -var appinfo = require('./appinfo'); var retry = require('./retry'); var build = require('./build'); -var check_reqs = require('./check_reqs'); +var path = require('path'); +var Adb = require('./Adb'); +var AndroidManifest = require('./AndroidManifest'); +var events = require('cordova-common').events; +var spawn = require('cordova-common').superspawn.spawn; +var CordovaError = require('cordova-common').CordovaError; var Q = require('q'); var os = require('os'); @@ -33,8 +36,10 @@ var child_process = require('child_process'); // constants var ONE_SECOND = 1000; // in milliseconds -var INSTALL_COMMAND_TIMEOUT = 120 * ONE_SECOND; // 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'; /** @@ -48,7 +53,7 @@ var EXEC_KILL_SIGNAL = 'SIGKILL'; } */ module.exports.list_images = function() { - return exec('android list avds') + return spawn('android', ['list', 'avds']) .then(function(output) { var response = output.split('\n'); var emulator_list = []; @@ -57,13 +62,18 @@ module.exports.list_images = function() { 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/)) { + if (response[i + 1].match(/\(API\slevel\s/) || (response[i + 2] && response[i + 2].match(/\(API\slevel\s/))) { i++; - img_obj['target'] = response[i].replace('\r', ''); + 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++; @@ -92,11 +102,15 @@ module.exports.list_images = function() { * Returns a promise. */ module.exports.best_image = function() { - var project_target = check_reqs.get_target().replace('android-', ''); 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]; + // Loading check_reqs at run-time to avoid test-time vs run-time directory structure difference issue + var project_target = require('./check_reqs').get_target().replace('android-', ''); for (var i in images) { var target = images[i].target; if(target) { @@ -115,22 +129,12 @@ module.exports.best_image = function() { // Returns a promise. module.exports.list_started = function() { - return exec('adb devices', os.tmpdir()) - .then(function(output) { - var response = output.split('\n'); - var started_emulator_list = []; - for (var i = 1; i < response.length; i++) { - if (response[i].match(/device/) && response[i].match(/emulator/)) { - started_emulator_list.push(response[i].replace(/\tdevice/, '').replace('\r', '')); - } - } - return started_emulator_list; - }); + return Adb.devices({emulators: true}); }; // Returns a promise. module.exports.list_targets = function() { - return exec('android list targets', os.tmpdir()) + return spawn('android', ['list', 'targets'], {cwd: os.tmpdir()}) .then(function(output) { var target_out = output.split('\n'); var targets = []; @@ -144,108 +148,139 @@ module.exports.list_targets = function() { }; /* + * 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 used the first image available, + * 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) { +module.exports.start = function(emulator_ID, boot_timeout) { var self = this; - var emulator_id, num_started, started_emulators; - return self.list_started() - .then(function(list) { - started_emulators = list; - num_started = started_emulators.length; - if (!emulator_ID) { - return self.list_images() - .then(function(emulator_list) { - if (emulator_list.length > 0) { - return self.best_image() - .then(function(best) { - emulator_ID = best.name; - console.log('WARNING : no emulator specified, defaulting to ' + emulator_ID); - return emulator_ID; - }); - } else { - var androidCmd = check_reqs.getAbsoluteAndroidCmd(); - return Q.reject('ERROR : 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'); - } - }); - } else { - return Q(emulator_ID); - } - }).then(function() { - var cmd = 'emulator'; - var args = ['-avd', emulator_ID]; - var proc = child_process.spawn(cmd, args, { stdio: 'inherit', detached: true }); - proc.unref(); // Don't wait for it to finish, since the emulator will probably keep running for a long time. - }).then(function() { - // wait for emulator to start - console.log('Waiting for emulator...'); - return self.wait_for_emulator(num_started); - }).then(function(new_started) { - if (new_started.length > 1) { - for (var i in new_started) { - if (started_emulators.indexOf(new_started[i]) < 0) { - emulator_id = new_started[i]; - } + return Q().then(function() { + if (emulator_ID) return Q(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; } - } else { - emulator_id = new_started[0]; - } - if (!emulator_id) return Q.reject('ERROR : Failed to start emulator, could not find new emulator'); - //wait for emulator to boot up - process.stdout.write('Booting up emulator (this may take a while)...'); - return self.wait_for_boot(emulator_id); - }).then(function() { - console.log('BOOT COMPLETE'); + // Loading check_reqs at run-time to avoid test-time vs run-time directory structure difference issue + var androidCmd = require('./check_reqs').getAbsoluteAndroidCmd(); + return Q.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) { + 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 }) + .unref(); - //unlock screen - return exec('adb -s ' + emulator_id + ' shell input keyevent 82', os.tmpdir()); - }).then(function() { - //return the new emulator id for the started emulators - return emulator_id; + // 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 Q.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 the new emulator to apear on the started-emulator list. - * Returns a promise with a list of newly started emulators' IDs. + * Waits for an emulator to boot on a given port. + * Returns this emulator's ID in a promise. */ -module.exports.wait_for_emulator = function(num_running) { +module.exports.wait_for_emulator = function(port) { var self = this; - return self.list_started() - .then(function(new_started) { - if (new_started.length > num_running) { - return new_started; - } else { - return Q.delay(1000).then(function() { - return self.wait_for_emulator(num_running); - }); - } - }); + return Q().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) { + // emulator not yet started, continue waiting + return self.wait_for_emulator(port); + } else { + // something unexpected has happened + throw error; + } + }); + }); }; /* - * Waits for the boot animation property of the emulator to switch to 'stopped' + * 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) { +module.exports.wait_for_boot = function(emulator_id, time_remaining) { var self = this; - return exec('adb -s ' + emulator_id + ' shell getprop init.svc.bootanim', os.tmpdir()) + return Adb.shell(emulator_id, 'ps') .then(function(output) { - if (output.match(/stopped/)) { - return; + if (output.match(/android\.process\.acore/)) { + return true; + } else if (time_remaining === 0) { + return false; } else { process.stdout.write('.'); - return Q.delay(3000).then(function() { - return self.wait_for_boot(emulator_id); + + // Check at regular intervals + return Q.delay(time_remaining < CHECK_BOOTED_INTERVAL ? time_remaining : CHECK_BOOTED_INTERVAL).then(function() { + var updated_time = time_remaining >= 0 ? Math.max(time_remaining - CHECK_BOOTED_INTERVAL, 0) : time_remaining; + return self.wait_for_boot(emulator_id, updated_time); }); } }); @@ -257,9 +292,9 @@ module.exports.wait_for_boot = function(emulator_id) { * Returns a promise. */ module.exports.create_image = function(name, target) { - console.log('Creating avd named ' + name); + console.log('Creating new avd named ' + name); if (target) { - return exec('android create avd --name ' + name + ' --target ' + target) + return 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 + '?'); @@ -267,11 +302,11 @@ module.exports.create_image = function(name, target) { }); } else { console.log('WARNING : Project target not found, creating avd with a different target but the project may fail to install.'); - return exec('android create avd --name ' + name + ' --target ' + this.list_targets()[0]) + return 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('Please insure you have targets available by running the "android" command'); + console.error('Ensure you have targets available by running the "android" command'); return Q.reject(); }, function(error) { console.error('ERROR : Failed to create emulator image : '); @@ -284,7 +319,7 @@ module.exports.resolveTarget = function(target) { return this.list_started() .then(function(emulator_list) { if (emulator_list.length < 1) { - return Q.reject('No started emulators found, please start an emultor before deploying your project.'); + return Q.reject('No running Android emulators found, please start an emulator before deploying your project.'); } // default emulator @@ -309,6 +344,8 @@ module.exports.resolveTarget = function(target) { module.exports.install = function(givenTarget, buildResults) { var target; + var manifest = new AndroidManifest(path.join(__dirname, '../../AndroidManifest.xml')); + var pkgName = manifest.getPackageId(); // resolve the target emulator return Q().then(function () { @@ -324,49 +361,83 @@ module.exports.install = function(givenTarget, buildResults) { // 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 Q.when() + .then(function() { - var apk_path = build.findBestApkForArchitecture(buildResults, target.arch); - var execOptions = { - timeout: INSTALL_COMMAND_TIMEOUT, // in milliseconds - killSignal: EXEC_KILL_SIGNAL - }; + var apk_path = build.findBestApkForArchitecture(buildResults, target.arch); + var execOptions = { + cwd: os.tmpdir(), + timeout: INSTALL_COMMAND_TIMEOUT, // in milliseconds + killSignal: EXEC_KILL_SIGNAL + }; - console.log('Installing app on emulator...'); - console.log('Using apk: ' + apk_path); + events.emit('log', 'Using apk: ' + apk_path); + events.emit('log', 'Package name: ' + pkgName); + events.emit('verbose', 'Installing app on emulator...'); - var retriedInstall = retry.retryPromise( - NUM_INSTALL_RETRIES, - exec, 'adb -s ' + target.target + ' install -r -d "' + apk_path + '"', os.tmpdir(), execOptions - ); + // 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 + '...'); - return retriedInstall.then(function (output) { - if (output.match(/Failure/)) { - return Q.reject('Failed to install apk to emulator: ' + output); - } else { - console.log('INSTALL SUCCESS'); + var command = 'adb -s ' + target + ' install -r "' + apk + '"'; + return Q.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 (err) { - return Q.reject('Failed to install apk to emulator: ' + err); - }); - // unlock screen - }).then(function () { + 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; - console.log('Unlocking screen...'); - return exec('adb -s ' + target.target + ' shell input keyevent 82', os.tmpdir()); + events.emit('warn', 'Uninstalling app from device and reinstalling it because the ' + + 'currently installed app was signed with different key'); - // launch the application - }).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 Adb.uninstall(target.target, pkgName) + .then(function() { + return adbInstallWithOptions(target.target, apk_path, execOptions); + }); + }); + } - console.log('Launching application...'); - var launchName = appinfo.getActivityName(); - var cmd = 'adb -s ' + target.target + ' shell am start -W -a android.intent.action.MAIN -n ' + launchName; - return exec(cmd, os.tmpdir()); + 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) { - console.log('LAUNCH SUCCESS'); - }, function (err) { - return Q.reject('Failed to launch app on emulator: ' + err); + events.emit('log', 'LAUNCH SUCCESS'); }); }; diff --git a/StoneIsland/platforms/android/cordova/lib/exec.js b/StoneIsland/platforms/android/cordova/lib/exec.js deleted file mode 100755 index 798a93ba..00000000 --- a/StoneIsland/platforms/android/cordova/lib/exec.js +++ /dev/null @@ -1,68 +0,0 @@ -#!/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 child_process = require("child_process"); -var Q = require("q"); - -// constants -var DEFAULT_MAX_BUFFER = 1024000; - -// Takes a command and optional current working directory. -// Returns a promise that either resolves with the stdout, or -// rejects with an error message and the stderr. -// -// WARNING: -// opt_cwd is an artifact of an old design, and must -// be removed in the future; the correct solution is -// to pass the options object the same way that -// child_process.exec expects -// -// NOTE: -// exec documented here - https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback -module.exports = function(cmd, opt_cwd, options) { - - var d = Q.defer(); - - if (typeof options === "undefined") { - options = {}; - } - - // override cwd to preserve old opt_cwd behavior - options.cwd = opt_cwd; - - // set maxBuffer - if (typeof options.maxBuffer === "undefined") { - options.maxBuffer = DEFAULT_MAX_BUFFER; - } - - try { - child_process.exec(cmd, options, function(err, stdout, stderr) { - if (err) d.reject("Error executing \"" + cmd + "\": " + stderr); - else d.resolve(stdout); - }); - } catch(e) { - console.error("error caught: " + e); - d.reject(e); - } - - return d.promise; -}; - diff --git a/StoneIsland/platforms/android/cordova/lib/install-device.bat b/StoneIsland/platforms/android/cordova/lib/install-device.bat index ac7214ac..ac7214ac 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/install-device.bat +++ b/StoneIsland/platforms/android/cordova/lib/install-device.bat diff --git a/StoneIsland/platforms/android/cordova/lib/install-emulator.bat b/StoneIsland/platforms/android/cordova/lib/install-emulator.bat index 1ec67790..1ec67790 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/install-emulator.bat +++ b/StoneIsland/platforms/android/cordova/lib/install-emulator.bat diff --git a/StoneIsland/platforms/android/cordova/lib/list-devices b/StoneIsland/platforms/android/cordova/lib/list-devices index e390bff6..fa84d7f6 100755 --- a/StoneIsland/platforms/android/cordova/lib/list-devices +++ b/StoneIsland/platforms/android/cordova/lib/list-devices @@ -22,12 +22,13 @@ var devices = require('./device'); // Usage support for when args are given -devices.list().done(function(device_list) { - device_list && device_list.forEach(function(dev) { - console.log(dev); +require('../lib/check_reqs').check_android().then(function() { + devices.list().done(function(device_list) { + device_list && device_list.forEach(function(dev) { + console.log(dev); + }); + }, function(err) { + console.error('ERROR: ' + err); + process.exit(2); }); -}, function(err) { - console.error('ERROR: ' + err); - process.exit(2); }); - diff --git a/StoneIsland/platforms/android/cordova/lib/list-devices.bat b/StoneIsland/platforms/android/cordova/lib/list-devices.bat index c0bcdd9a..c0bcdd9a 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/list-devices.bat +++ b/StoneIsland/platforms/android/cordova/lib/list-devices.bat diff --git a/StoneIsland/platforms/android/cordova/lib/list-emulator-images b/StoneIsland/platforms/android/cordova/lib/list-emulator-images index 996cf555..03c827fe 100755 --- a/StoneIsland/platforms/android/cordova/lib/list-emulator-images +++ b/StoneIsland/platforms/android/cordova/lib/list-emulator-images @@ -22,11 +22,13 @@ var emulators = require('./emulator'); // Usage support for when args are given -emulators.list_images().done(function(emulator_list) { - emulator_list && emulator_list.forEach(function(emu) { - console.log(emu.name); +require('../lib/check_reqs').check_android().then(function() { + emulators.list_images().done(function(emulator_list) { + emulator_list && emulator_list.forEach(function(emu) { + console.log(emu.name); + }); + }, function(err) { + console.error('ERROR: ' + err); + process.exit(2); }); -}, function(err) { - console.error('ERROR: ' + err); - process.exit(2); }); diff --git a/StoneIsland/platforms/android/cordova/lib/list-emulator-images.bat b/StoneIsland/platforms/android/cordova/lib/list-emulator-images.bat index 661cbf95..661cbf95 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/list-emulator-images.bat +++ b/StoneIsland/platforms/android/cordova/lib/list-emulator-images.bat diff --git a/StoneIsland/platforms/android/cordova/lib/list-started-emulators b/StoneIsland/platforms/android/cordova/lib/list-started-emulators index 2ae8c5a8..a890dec6 100755 --- a/StoneIsland/platforms/android/cordova/lib/list-started-emulators +++ b/StoneIsland/platforms/android/cordova/lib/list-started-emulators @@ -22,11 +22,13 @@ var emulators = require('./emulator'); // Usage support for when args are given -emulators.list_started().done(function(emulator_list) { - emulator_list && emulator_list.forEach(function(emu) { - console.log(emu); +require('../lib/check_reqs').check_android().then(function() { + emulators.list_started().done(function(emulator_list) { + emulator_list && emulator_list.forEach(function(emu) { + console.log(emu); + }); + }, function(err) { + console.error('ERROR: ' + err); + process.exit(2); }); -}, function(err) { - console.error('ERROR: ' + err); - process.exit(2); }); diff --git a/StoneIsland/platforms/android/cordova/lib/list-started-emulators.bat b/StoneIsland/platforms/android/cordova/lib/list-started-emulators.bat index a4e88f7d..a4e88f7d 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/list-started-emulators.bat +++ b/StoneIsland/platforms/android/cordova/lib/list-started-emulators.bat diff --git a/StoneIsland/platforms/android/cordova/lib/log.js b/StoneIsland/platforms/android/cordova/lib/log.js index ebf836d5..ebf836d5 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/log.js +++ b/StoneIsland/platforms/android/cordova/lib/log.js diff --git a/StoneIsland/platforms/android/cordova/lib/plugin-build.gradle b/StoneIsland/platforms/android/cordova/lib/plugin-build.gradle index b345b90a..d1c63365 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/plugin-build.gradle +++ b/StoneIsland/platforms/android/cordova/lib/plugin-build.gradle @@ -21,28 +21,19 @@ buildscript { repositories { mavenCentral() + 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 - if (gradle.gradleVersion >= "2.2") { - dependencies { - classpath 'com.android.tools.build:gradle:1.0.0+' - } - } else if (gradle.gradleVersion >= "2.1") { - dependencies { - classpath 'com.android.tools.build:gradle:0.14.0+' - } - } else { - dependencies { - classpath 'com.android.tools.build:gradle:0.12.0+' - } + dependencies { + classpath 'com.android.tools.build:gradle:1.0.0+' } } -apply plugin: 'android-library' +apply plugin: 'com.android.library' dependencies { compile fileTree(dir: 'libs', include: '*.jar') diff --git a/StoneIsland/platforms/android/cordova/lib/pluginHandlers.js b/StoneIsland/platforms/android/cordova/lib/pluginHandlers.js new file mode 100644 index 00000000..5e745fd5 --- /dev/null +++ b/StoneIsland/platforms/android/cordova/lib/pluginHandlers.js @@ -0,0 +1,308 @@ +/* + * + * Copyright 2013 Anis Kadri + * + * 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. + * +*/ + +/* jshint unused: vars */ + +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 = path.join(obj.targetDir, path.basename(obj.src)); + + if(options && options.android_studio === true) { + dest = path.join('app/src/main/java', obj.targetDir.substring(4), path.basename(obj.src)); + } + + 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 = path.join(obj.targetDir, path.basename(obj.src)); + + if(options && options.android_studio === true) { + dest = path.join('app/src/main/java', obj.targetDir.substring(4), path.basename(obj.src)); + } + + deleteJava(project.projectDir, dest); + } + }, + 'lib-file':{ + install:function(obj, plugin, project, options) { + var dest = path.join('libs', path.basename(obj.src)); + if(options && options.android_studio === true) { + 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('libs', path.basename(obj.src)); + if(options && options.android_studio === true) { + dest = path.join('app/libs', path.basename(obj.src)); + } + removeFile(project.projectDir, dest); + } + }, + 'resource-file':{ + install:function(obj, plugin, project, options) { + copyFile(plugin.dir, obj.src, project.projectDir, path.normalize(obj.target), !!(options && options.link)); + }, + uninstall:function(obj, plugin, project, options) { + removeFile(project.projectDir, path.normalize(obj.target)); + } + }, + '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; +} diff --git a/StoneIsland/platforms/android/cordova/lib/prepare.js b/StoneIsland/platforms/android/cordova/lib/prepare.js new file mode 100644 index 00000000..10a69ea3 --- /dev/null +++ b/StoneIsland/platforms/android/cordova/lib/prepare.js @@ -0,0 +1,431 @@ +/** + 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 fs = require('fs'); +var path = require('path'); +var shell = require('shelljs'); +var events = require('cordova-common').events; +var AndroidManifest = require('./AndroidManifest'); +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; + +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); + + // 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)); + }) + .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)); + }); +}; + +/** + * 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 name = platformConfig.name(); + var strings = xmlHelpers.parseElementtreeSync(locations.strings); + strings.find('string[@name="app_name"]').text = name.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 pkg = (platformConfig.android_packageName() || platformConfig.packageName()).replace(/-/g, '_'); + + var manifest = new AndroidManifest(locations.manifest); + var orig_pkg = manifest.getPackageId(); + + manifest.getActivity() + .setOrientation(platformConfig.getPreference('orientation')) + .setLaunchMode(findAndroidLaunchModePreference(platformConfig)); + + manifest.setVersionName(platformConfig.version()) + .setVersionCode(platformConfig.android_versionCode() || default_versionCode(platformConfig.version())) + .setPackageId(pkg) + .setMinSdkVersion(platformConfig.getPreference('android-minSdkVersion', 'android')) + .setMaxSdkVersion(platformConfig.getPreference('android-maxSdkVersion', 'android')) + .setTargetSdkVersion(platformConfig.getPreference('android-targetSdkVersion', 'android')) + .write(); + + var javaPattern = path.join(locations.root, 'src', orig_pkg.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, 'src', pkg.replace(/\./g, '/'), path.basename(java_files[0])); + shell.mkdir('-p', path.dirname(destFile)); + shell.sed(/package [\w\.]*;/, 'package ' + pkg + ';', java_files[0]).to(destFile); + events.emit('verbose', 'Wrote out Android package name "' + pkg + '" to ' + destFile); + + if (orig_pkg !== pkg) { + // 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 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) { + var icons = cordovaProject.projectConfig.getIcons('android'); + + // if there are icon elements in config.xml + if (icons.length === 0) { + events.emit('verbose', 'This app does not have launcher icons defined'); + return; + } + + var resourceMap = mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'icon.png'); + + var android_icons = {}; + var default_icon; + // http://developer.android.com/design/style/iconography.html + var sizeToDensityMap = { + 36: 'ldpi', + 48: 'mdpi', + 72: 'hdpi', + 96: 'xhdpi', + 144: 'xxhdpi', + 192: 'xxxhdpi' + }; + // 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 || sizeToDensityMap[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) { + events.emit('verbose', 'Found extra default icon: ' + icon.src + ' (ignoring in favor of ' + default_icon.src + ')'); + } else { + default_icon = icon; + } + } else { + parseIcon(icon, size); + } + } + + // 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, 'icon.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', 'icon.png', path.basename(default_icon.src)); + resourceMap[defaultTargetPath] = default_icon.src; + } + + events.emit('verbose', 'Updating icons at ' + platformResourcesDir); + FileUpdater.updatePaths( + resourceMap, { rootDir: cordovaProject.root }, logFileOp); +} + +function cleanIcons(projectRoot, projectConfig, platformResourcesDir) { + var icons = projectConfig.getIcons('android'); + if (icons.length > 0) { + var resourceMap = mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'icon.png'); + 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; +} + +/** + * 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; +} diff --git a/StoneIsland/platforms/android/cordova/lib/retry.js b/StoneIsland/platforms/android/cordova/lib/retry.js index dc52a7d2..3cb49274 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/retry.js +++ b/StoneIsland/platforms/android/cordova/lib/retry.js @@ -21,7 +21,9 @@ /* jshint node: true */ -"use strict"; +'use strict'; + +var events = require('cordova-common').events; /* * Retry a promise-returning function a number of times, propagating its @@ -56,7 +58,7 @@ module.exports.retryPromise = function (attemts_left, promiseFunction) { throw error; } - console.log("A retried call failed. Retrying " + attemts_left + " more time(s)."); + events.emit('verbose', 'A retried call failed. Retrying ' + attemts_left + ' more time(s).'); // retry call self again with the same arguments, except attemts_left is now lower var fullArguments = [attemts_left, promiseFunction].concat(promiseFunctionArguments); diff --git a/StoneIsland/platforms/android/cordova/lib/run.js b/StoneIsland/platforms/android/cordova/lib/run.js index 7f15448c..214a1e19 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/run.js +++ b/StoneIsland/platforms/android/cordova/lib/run.js @@ -25,63 +25,36 @@ var path = require('path'), build = require('./build'), emulator = require('./emulator'), device = require('./device'), - shell = require('shelljs'), - Q = require('q'); + Q = require('q'), + events = require('cordova-common').events; -/* - * 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. - * Returns a promise. - */ - module.exports.run = function(args) { - var buildFlags = []; +function getInstallTarget(runOptions) { var install_target; - var list = false; - - for (var i=2; i<args.length; i++) { - if (build.isBuildFlag(args[i])) { - buildFlags.push(args[i]); - } else if (args[i] == '--device') { - install_target = '--device'; - } else if (args[i] == '--emulator') { - install_target = '--emulator'; - } else if (/^--target=/.exec(args[i])) { - install_target = args[i].substring(9, args[i].length); - } else if (args[i] == '--list') { - list = true; - } else { - console.warn('Option \'' + args[i] + '\' not recognized (ignoring).'); - } + if (runOptions.target) { + install_target = runOptions.target; + } else if (runOptions.device) { + install_target = '--device'; + } else if (runOptions.emulator) { + install_target = '--emulator'; } - if (list) { - var output = ''; - var temp = ''; - if (!install_target) { - output += 'Available Android Devices:\n'; - temp = shell.exec(path.join(__dirname, 'list-devices'), {silent:true}).output; - temp = temp.replace(/^(?=[^\s])/gm, '\t'); - output += temp; - output += 'Available Android Virtual Devices:\n'; - temp = shell.exec(path.join(__dirname, 'list-emulator-images'), {silent:true}).output; - temp = temp.replace(/^(?=[^\s])/gm, '\t'); - output += temp; - } else if (install_target == '--emulator') { - output += 'Available Android Virtual Devices:\n'; - temp = shell.exec(path.join(__dirname, 'list-emulator-images'), {silent:true}).output; - temp = temp.replace(/^(?=[^\s])/gm, '\t'); - output += temp; - } else if (install_target == '--device') { - output += 'Available Android Devices:\n'; - temp = shell.exec(path.join(__dirname, 'list-devices'), {silent:true}).output; - temp = temp.replace(/^(?=[^\s])/gm, '\t'); - output += temp; - } - console.log(output); - return; - } + 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) { + + var self = this; + var install_target = getInstallTarget(runOptions); return Q() .then(function() { @@ -90,10 +63,10 @@ var path = require('path'), return device.list() .then(function(device_list) { if (device_list.length > 0) { - console.log('WARNING : No target specified, deploying to device \'' + device_list[0] + '\'.'); + events.emit('warn', 'No target specified, deploying to device \'' + device_list[0] + '\'.'); install_target = device_list[0]; } else { - console.log('WARNING : No target specified, deploying to emulator'); + events.emit('warn', 'No target specified and no devices found, deploying to emulator'); install_target = '--emulator'; } }); @@ -137,17 +110,25 @@ var path = require('path'), }); }); }).then(function(resolvedTarget) { - return build.run(buildFlags, resolvedTarget).then(function(buildResults) { + // Better just call self.build, but we're doing some processing of + // build results (according to platformApi spec) so they are in different + // format than emulator.install expects. + // TODO: Update emulator/device.install to handle this change + return build.run.call(self, runOptions, resolvedTarget) + .then(function(buildResults) { if (resolvedTarget.isEmulator) { - return emulator.install(resolvedTarget, buildResults); + return emulator.wait_for_boot(resolvedTarget.target) + .then(function () { + return emulator.install(resolvedTarget, buildResults); + }); } return device.install(resolvedTarget, buildResults); }); }); }; -module.exports.help = function(args) { - console.log('Usage: ' + path.relative(process.cwd(), args[1]) + ' [options]'); +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'); diff --git a/StoneIsland/platforms/android/cordova/lib/spawn.js b/StoneIsland/platforms/android/cordova/lib/spawn.js deleted file mode 100755 index 3e500a09..00000000 --- a/StoneIsland/platforms/android/cordova/lib/spawn.js +++ /dev/null @@ -1,50 +0,0 @@ -#!/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 child_process = require('child_process'), - Q = require('q'); -var isWindows = process.platform.slice(0, 3) == 'win'; - -// Takes a command and optional current working directory. -module.exports = function(cmd, args, opt_cwd) { - var d = Q.defer(); - var opts = { cwd: opt_cwd, stdio: 'inherit' }; - try { - // Work around spawn not being able to find .bat files. - if (isWindows) { - args = [['/s', '/c', '"' + [cmd].concat(args).map(function(a){if (/^[^"].* .*[^"]/.test(a)) return '"' + a + '"'; return a;}).join(' ')+'"'].join(' ')]; - cmd = 'cmd'; - opts.windowsVerbatimArguments = true; - } - var child = child_process.spawn(cmd, args, opts); - child.on('exit', function(code) { - if (code) { - d.reject('Error code ' + code + ' for command: ' + cmd + ' with args: ' + args); - } else { - d.resolve(); - } - }); - } catch(e) { - console.error('error caught: ' + e); - d.reject(e); - } - return d.promise; -}; diff --git a/StoneIsland/platforms/android/cordova/lib/start-emulator.bat b/StoneIsland/platforms/android/cordova/lib/start-emulator.bat index 9329d951..9329d951 100755..100644 --- a/StoneIsland/platforms/android/cordova/lib/start-emulator.bat +++ b/StoneIsland/platforms/android/cordova/lib/start-emulator.bat |
