Compare commits

..

2 Commits
9.1.0 ... 9.0.x

Author SHA1 Message Date
Erisu
cdcb4ddcd9 Set VERSION to 9.0.0 (via coho) 2020-06-23 18:33:21 +09:00
Erisu
8e53e2aa56 Update JS snapshot to version 9.0.0 (via coho) 2020-06-23 18:33:21 +09:00
57 changed files with 3079 additions and 3142 deletions

View File

@@ -15,30 +15,6 @@
# specific language governing permissions and limitations
# under the License.
github:
description: Apache Cordova Android
homepage: https://cordova.apache.org/
labels:
- cordova
- cordova-platform
- android
- java
- mobile
- javascript
- nodejs
- hacktoberfest
features:
wiki: false
issues: true
projects: true
enabled_merge_buttons:
squash: true
merge: false
rebase: false
notifications:
commits: commits@cordova.apache.org
issues: issues@cordova.apache.org

View File

@@ -27,7 +27,7 @@ jobs:
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
node-version: [10.x, 12.x]
os: [ubuntu-latest, windows-latest, macos-latest]
steps:

65
LICENSE
View File

@@ -200,3 +200,68 @@
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.
ADDITIONAL LICENSES:
================================================================================
bin/node_modules/q
================================================================================
Copyright 20092017 Kristopher Michael Kowal. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
================================================================================
bin/node_modules/nopt
================================================================================
The ISC License
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
================================================================================
bin/node_modules/which
================================================================================
The ISC License
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@@ -20,69 +20,6 @@
-->
## Release Notes for Cordova (Android)
### 9.1.0 (Apr 09, 2021)
**Features:**
* [GH-1104](https://github.com/apache/cordova-android/pull/1104) feat: support `gzip` encoding requests & use `GZIPInputStream`
* [GH-1167](https://github.com/apache/cordova-android/pull/1167) feat: handle `intent://` scheme links with `browser_fallback_url` param
* [GH-1179](https://github.com/apache/cordova-android/pull/1179) feat: add `repositories` support
* [GH-1173](https://github.com/apache/cordova-android/pull/1173) feat(android-studio): display app name as project name
* [GH-1113](https://github.com/apache/cordova-android/pull/1113) feat: `webp` support for splashscreen
* [GH-1125](https://github.com/apache/cordova-android/pull/1125) feat(Adb): list `devices` _and_ `emulators` in one go
**Fixes:**
* [GH-1186](https://github.com/apache/cordova-android/pull/1186) fix: copy `repositories.gradle` to project on create
* [GH-1184](https://github.com/apache/cordova-android/pull/1184) fix: unit-test failure
* [GH-733](https://github.com/apache/cordova-android/pull/733) fix(splashscreen): nav & title bar showing in fullscreen mode
* [GH-1157](https://github.com/apache/cordova-android/pull/1157) fix: restore key event handlers when DOM element is fullscreen
* [GH-1073](https://github.com/apache/cordova-android/pull/1073) fix(android): Avoid Crash Report: ConcurrentModificationException
* [GH-1148](https://github.com/apache/cordova-android/pull/1148) fix: add not null checks to prevent running on destroyed activity
* [GH-1091](https://github.com/apache/cordova-android/pull/1091) fix: concurrent modification exception (#924)
* [GH-1153](https://github.com/apache/cordova-android/pull/1153) fix: optional arch parameter
* [GH-1136](https://github.com/apache/cordova-android/pull/1136) fix(prepare): `mapImageResources` always returning `[]`
* [GH-1111](https://github.com/apache/cordova-android/pull/1111) fix(android): allow file access for existing behavior
* [GH-1045](https://github.com/apache/cordova-android/pull/1045) fix: Reflect minimum required NodeJS
* [GH-1084](https://github.com/apache/cordova-android/pull/1084) fix(prepare): fix pattern used to collect image resources
* [GH-1014](https://github.com/apache/cordova-android/pull/1014) fix(`pluginHandlers`): properly check if path is inside another
* [GH-1018](https://github.com/apache/cordova-android/pull/1018) fix: gradle ignore properties
* [GH-1185](https://github.com/apache/cordova-android/pull/1185) fix(regression): Cannot read version of undefined caused by Java refactor
* [GH-1117](https://github.com/apache/cordova-android/pull/1117) fix: allow changing min sdk version
**Refactors:**
* [GH-1101](https://github.com/apache/cordova-android/pull/1101) refactor: unify target resolution for devices & emulators
* [GH-1130](https://github.com/apache/cordova-android/pull/1130) refactor: java checks
* [GH-1099](https://github.com/apache/cordova-android/pull/1099) refactor(`ProjectBuilder`): clean up output file collection code
* [GH-1123](https://github.com/apache/cordova-android/pull/1123) refactor: unify installation on devices & emulators
* [GH-1102](https://github.com/apache/cordova-android/pull/1102) refactor(`check_reqs`): cleanup default Java location detection on **Windows**
* [GH-1103](https://github.com/apache/cordova-android/pull/1103) refactor: do not kill adb on UNIX-like systems
* [GH-1086](https://github.com/apache/cordova-android/pull/1086) refactor(retry): simplify retryPromise using modern JS
* [GH-1085](https://github.com/apache/cordova-android/pull/1085) refactor(utils): reduce number of utils
* [GH-1046](https://github.com/apache/cordova-android/pull/1046) refactor: Stop suppressing un-needed TruelyRandom lints
* [GH-1016](https://github.com/apache/cordova-android/pull/1016) refactor: save `ProjectBuilder` instance in Api instance
* [GH-1108](https://github.com/apache/cordova-android/pull/1108) refactor: remove copied Adb.install from `emulator.install`
**Chores:**
* [GH-1196](https://github.com/apache/cordova-android/pull/1196) chore: add missing header license
* chore(asf): Update GitHub repo metadata
* [GH-1183](https://github.com/apache/cordova-android/pull/1183) chore: rebuilt package-lock
* [GH-1015](https://github.com/apache/cordova-android/pull/1015) chore: remove unnecessary stuff
* [GH-1081](https://github.com/apache/cordova-android/pull/1081) chore(pkg): remove deprecated `no-op` field `"engineStrict"`
* [GH-1019](https://github.com/apache/cordova-android/pull/1019) chore: remove unused `emulator.create_image` and its dependencies
**Tests & CI:**
* [GH-1017](https://github.com/apache/cordova-android/pull/1017) test(java): fix, improve and move clean script
* [GH-1012](https://github.com/apache/cordova-android/pull/1012) test: fix missing stack traces in jasmine output
* [GH-1013](https://github.com/apache/cordova-android/pull/1013) test(`pluginHandlers/common`): better setup & teardown
* [GH-1094](https://github.com/apache/cordova-android/pull/1094) test: fix unit test failures for certain random orders
* [GH-1094](https://github.com/apache/cordova-android/pull/1094) test: ensure single top-level describe block in test file
* [GH-1129](https://github.com/apache/cordova-android/pull/1129) test(java): remove duplicate code in `BackButtonMultipageTest`
* [GH-975](https://github.com/apache/cordova-android/pull/975) ci: Added Node 14.x
### 9.0.0 (Jun 23, 2020)
* [GH-1005](https://github.com/apache/cordova-android/pull/1005) chore: set AndroidX off by default

View File

@@ -1 +1 @@
9.1.0
9.0.0

View File

@@ -17,16 +17,12 @@
under the License.
*/
const fs = require('fs-extra');
const path = require('path');
const fs = require('fs');
/**
* This script is to be run manually (e.g. by npm run clean:java-unit-tests) if
* you want to upgrade gradlew or test its proper generation.
*/
for (const variant of ['android', 'androidx']) {
for (const file of ['gradlew', 'gradlew.bat']) {
fs.removeSync(path.join(__dirname, variant, file));
}
if (fs.existsSync('./test/gradlew')) {
fs.unlinkSync('./test/gradlew');
}
if (fs.existsSync('./test/gradlew.bat')) {
fs.unlinkSync('./test/gradlew.bat');
}

View File

@@ -19,7 +19,7 @@
var path = require('path');
var fs = require('fs-extra');
var utils = require('../templates/cordova/lib/utils');
var utils = require('../../bin/lib/utils');
var check_reqs = require('./../templates/cordova/lib/check_reqs');
var ROOT = path.join(__dirname, '..', '..');
@@ -36,7 +36,6 @@ exports.copyScripts = copyScripts;
exports.copyBuildRules = copyBuildRules;
exports.writeProjectProperties = writeProjectProperties;
exports.prepBuildFiles = prepBuildFiles;
exports.writeNameForAndroidStudio = writeNameForAndroidStudio;
function getFrameworkDir (projectPath, shared) {
return shared ? path.join(ROOT, 'framework') : path.join(projectPath, 'CordovaLib');
@@ -72,7 +71,6 @@ function copyJsAndLibrary (projectPath, shared, projectName, isLegacy) {
fs.copySync(path.join(ROOT, 'framework', 'project.properties'), path.join(nestedCordovaLibPath, 'project.properties'));
fs.copySync(path.join(ROOT, 'framework', 'build.gradle'), path.join(nestedCordovaLibPath, 'build.gradle'));
fs.copySync(path.join(ROOT, 'framework', 'cordova.gradle'), path.join(nestedCordovaLibPath, 'cordova.gradle'));
fs.copySync(path.join(ROOT, 'framework', 'repositories.gradle'), path.join(nestedCordovaLibPath, 'repositories.gradle'));
fs.copySync(path.join(ROOT, 'framework', 'src'), path.join(nestedCordovaLibPath, 'src'));
}
}
@@ -127,8 +125,6 @@ function copyBuildRules (projectPath, isLegacy) {
} else {
fs.copySync(path.join(srcDir, 'build.gradle'), path.join(projectPath, 'build.gradle'));
fs.copySync(path.join(srcDir, 'app', 'build.gradle'), path.join(projectPath, 'app', 'build.gradle'));
fs.copySync(path.join(srcDir, 'app', 'repositories.gradle'), path.join(projectPath, 'app', 'repositories.gradle'));
fs.copySync(path.join(srcDir, 'repositories.gradle'), path.join(projectPath, 'repositories.gradle'));
fs.copySync(path.join(srcDir, 'wrapper.gradle'), path.join(projectPath, 'wrapper.gradle'));
}
}
@@ -201,19 +197,6 @@ function validateProjectName (project_name) {
return Promise.resolve();
}
/**
* Write the name of the app in "platforms/android/.idea/.name" so that Android Studio can show that name in the
* project listing. This is helpful to quickly look in the Android Studio listing if there are so many projects in
* Android Studio.
*
* https://github.com/apache/cordova-android/issues/1172
*/
function writeNameForAndroidStudio (project_path, project_name) {
const ideaPath = path.join(project_path, '.idea');
fs.ensureDirSync(ideaPath);
fs.writeFileSync(path.join(ideaPath, '.name'), project_name);
}
/**
* Creates an android application with the given options.
*
@@ -311,7 +294,6 @@ exports.create = function (project_path, config, options, events) {
// Link it to local android install.
exports.writeProjectProperties(project_path, target_api);
exports.prepBuildFiles(project_path);
exports.writeNameForAndroidStudio(project_path, project_name);
events.emit('log', generateDoneMessage('create', options.link));
}).then(() => project_path);
};

47
bin/lib/utils.js Normal file
View File

@@ -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.
*/
/*
Provides a set of utility methods, which can also be spied on during unit tests.
*/
// TODO: Perhaps this should live in cordova-common?
const fs = require('fs-extra');
/**
* Reads, searches, and replaces the found occurences with replacementString and then writes the file back out.
* A backup is not made.
*
* @param {string} file A file path to a readable & writable file
* @param {RegExp} searchRegex The search regex
* @param {string} replacementString The string to replace the found occurences
* @returns {void}
*/
exports.replaceFileContents = function (file, searchRegex, replacementString) {
// let contents;
try {
var contents = fs.readFileSync(file).toString();
} catch (ex) {
console.log('TRYING TO READ: ', file);
throw ex;
}
contents = contents.replace(searchRegex, replacementString);
fs.writeFileSync(file, contents);
};

View File

@@ -24,7 +24,7 @@
* This workflow would not have the `package.json` file.
*/
// Coho updates this line
const VERSION = '9.1.0';
const VERSION = '9.0.0';
var path = require('path');
@@ -85,8 +85,6 @@ class Api {
build: path.join(this.root, 'build'),
javaSrc: path.join(appMain, 'java')
};
this._builder = require('./lib/builders/builders').getBuilder(this.root);
}
/**
@@ -163,7 +161,7 @@ class Api {
if (plugin.getFrameworks(this.platform).length === 0) return;
selfEvents.emit('verbose', 'Updating build files since android plugin contained <framework>');
// This should pick the correct builder, not just get gradle
this._builder.prepBuildFiles();
require('./lib/builders/builders').getBuilder().prepBuildFiles();
}.bind(this))
// CB-11022 Return truthy value to prevent running prepare after
.then(() => true);
@@ -195,7 +193,7 @@ class Api {
if (plugin.getFrameworks(this.platform).length === 0) return;
selfEvents.emit('verbose', 'Updating build files since android plugin contained <framework>');
this._builder.prepBuildFiles();
require('./lib/builders/builders').getBuilder().prepBuildFiles();
}.bind(this))
// CB-11022 Return truthy value to prevent running prepare after
.then(() => true);

View File

@@ -24,40 +24,42 @@ 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 = async function () {
const { stdout } = await execa('adb', ['devices'], { cwd: os.tmpdir() });
// Split into lines & drop first one (header)
const rawDeviceLines = stdout.trim().split(/\r?\n/).slice(1);
return rawDeviceLines
.map(line => line.split('\t'))
// We are only interested in fully booted devices & emulators. These
// have a state of `device`. For a list of all the other possible states
// see https://github.com/aosp-mirror/platform_system_core/blob/2abdb1eb5b83c8f39874644af576c869815f5c5b/adb/transport.cpp#L1129
.filter(([, state]) => state === 'device')
.map(([id]) => id);
Adb.devices = function (opts) {
return execa('adb', ['devices'], { cwd: os.tmpdir() }).then(({ stdout: 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, { replace = false, execOptions = {} } = {}) {
Adb.install = function (target, packagePath, opts) {
events.emit('verbose', 'Installing apk ' + packagePath + ' on target ' + target + '...');
var args = ['-s', target, 'install'];
if (replace) args.push('-r');
const opts = { cwd: os.tmpdir(), ...execOptions };
return execa('adb', args.concat(packagePath), opts).then(({ stdout: output }) => {
// 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.
if (opts && opts.replace) args.push('-r');
return execa('adb', args.concat(packagePath), { cwd: os.tmpdir() }).then(({ stdout: 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\'' +
@@ -67,7 +69,7 @@ Adb.install = function (target, packagePath, { replace = false, execOptions = {}
'\nEither uninstall an app or increment the versionCode.';
}
throw new CordovaError('Failed to install apk to target: ' + output);
return Promise.reject(new CordovaError('Failed to install apk to device: ' + output));
}
});
};

View File

@@ -23,7 +23,10 @@ var nopt = require('nopt');
var Adb = require('./Adb');
var builders = require('./builders/builders');
var events = require('cordova-common').events;
const execa = require('execa');
var CordovaError = require('cordova-common').CordovaError;
var PackageType = require('./PackageType');
module.exports.parseBuildOptions = parseOpts;
@@ -142,7 +145,7 @@ function parseOpts (options, resolvedTarget, projectRoot) {
*/
module.exports.runClean = function (options) {
var opts = parseOpts(options, null, this.root);
var builder = this._builder;
var builder = builders.getBuilder();
return builder.prepEnv(opts).then(function () {
return builder.clean(opts);
@@ -163,7 +166,7 @@ module.exports.runClean = function (options) {
*/
module.exports.run = function (options, optResolvedTarget) {
var opts = parseOpts(options, optResolvedTarget, this.root);
var builder = this._builder;
var builder = builders.getBuilder();
return builder.prepEnv(opts).then(function () {
if (opts.prepEnv) {
@@ -193,8 +196,45 @@ module.exports.run = function (options, optResolvedTarget) {
* Returns "arm" or "x86".
*/
module.exports.detectArchitecture = function (target) {
return Adb.shell(target, 'cat /proc/cpuinfo').then(function (output) {
return /intel/i.exec(output) ? 'x86' : 'arm';
function helper () {
return Adb.shell(target, 'cat /proc/cpuinfo').then(function (output) {
return /intel/i.exec(output) ? 'x86' : 'arm';
});
}
function timeout (ms, err) {
return new Promise((resolve, reject) => {
setTimeout(() => reject(err), ms);
});
}
// 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 Promise.race([
helper(),
timeout(5000, new CordovaError(
'Device communication timed out. Try unplugging & replugging the device.'
))
]).catch(err => {
if (/timed out/.exec('' + err)) {
// adb kill-server doesn't seem to do the trick.
// Could probably find a x-platform version of killall, but I'm not actually
// sure that this scenario even happens on non-OSX machines.
events.emit('verbose', 'adb timed out while detecting device/emulator architecture. Killing adb and trying again.');
return execa('killall', ['adb']).then(function () {
return helper().then(null, function () {
// The double kill is sadly often necessary, at least on mac.
events.emit('warn', 'adb timed out a second time while detecting device/emulator architecture. Killing adb and trying again.');
return execa('killall', ['adb']).then(function () {
return helper().then(null, function () {
return Promise.reject(new CordovaError('adb timed out a third time while detecting device/emulator architecture. Try unplugging & replugging the device.'));
});
});
});
}, function () {
// For non-killall OS's.
return Promise.reject(err);
});
}
throw err;
});
};

View File

@@ -20,12 +20,10 @@
var fs = require('fs-extra');
var path = require('path');
const execa = require('execa');
const glob = require('fast-glob');
var events = require('cordova-common').events;
var CordovaError = require('cordova-common').CordovaError;
var check_reqs = require('../check_reqs');
var PackageType = require('../PackageType');
const { compareByAll } = require('../utils');
const { createEditor } = require('properties-parser');
const MARKER = 'YOUR CHANGES WILL BE ERASED!';
@@ -34,46 +32,83 @@ const TEMPLATE =
'# This file is automatically generated.\n' +
'# Do not modify this file -- ' + MARKER + '\n';
const isPathArchSpecific = p => /-x86|-arm/.test(path.basename(p));
const archSpecificRegex = /-x86|-arm/;
const unsignedBuildRegex = /-unsigned/;
const outputFileComparator = compareByAll([
// Sort arch specific builds after generic ones
isPathArchSpecific,
const fileSorter = (filePathA, filePathB) => {
const archSpecificA = archSpecificRegex.test(filePathA);
const archSpecificB = archSpecificRegex.test(filePathB);
// Sort unsigned builds after signed ones
filePath => /-unsigned/.test(path.basename(filePath)),
// If they are not equal, then sort by specific archs after generic ones
if (archSpecificA !== archSpecificB) {
return archSpecificA < archSpecificB ? -1 : 1;
}
// Sort by file modification time, latest first
filePath => -fs.statSync(filePath).mtime.getTime(),
// Otherwise, move onto the next sort item, which is by sorting unsigned bulds after signed ones
const unsignedA = unsignedBuildRegex.test(filePathA);
const unsignedB = unsignedBuildRegex.test(filePathB);
// Sort by file name length, ascending
filePath => filePath.length
]);
if (unsignedA !== unsignedB) {
return unsignedA < unsignedB ? -1 : 1;
}
// Then, sort by modification time, latest first
const modTimeA = fs.statSync(filePathA).mtime.getTime();
const modTimeB = fs.statSync(filePathB).mtime.getTime();
if (modTimeA !== modTimeB) {
return modTimeA < modTimeB ? 1 : -1;
}
// Finally, if all above is the same, sort by file name length, ascending
return filePathB.length < filePathA.length ? -1 : 1;
};
/**
* @param {'apk' | 'aab'} bundleType
* @param {'debug' | 'release'} buildType
* @param {{arch?: string}} options
* If the provided directory does not exist or extension is missing, return an empty array.
* If the director exists, loop the directories and collect list of files matching the extension.
*
* @param {String} dir Directory to scan
* @param {String} extension
*/
function findOutputFiles (bundleType, buildType, { arch } = {}) {
let files = glob.sync(`**/*.${bundleType}`, {
absolute: true,
cwd: path.resolve(this[`${bundleType}Dir`], buildType)
}).map(path.normalize);
function recursivelyFindFiles (dir, extension) {
if (!fs.existsSync(dir) || !extension) return [];
const files = fs.readdirSync(dir, { withFileTypes: true })
.map(entry => {
const item = path.resolve(dir, entry.name);
if (entry.isDirectory()) return recursivelyFindFiles(item, extension);
if (path.extname(entry.name) === `.${extension}`) return item;
return false;
});
return Array.prototype.concat(...files)
.filter(file => file !== false);
}
/**
* @param {String} dir
* @param {String} build_type
* @param {String} arch
* @param {String} extension
*/
function findOutputFilesHelper (dir, build_type, arch, extension) {
let files = recursivelyFindFiles(path.resolve(dir, build_type), extension);
if (files.length === 0) return files;
// Assume arch-specific build if newest apk has -x86 or -arm.
const archSpecific = isPathArchSpecific(files[0]);
const archSpecific = !!/-x86|-arm/.exec(path.basename(files[0]));
// And show only arch-specific ones (or non-arch-specific)
files = files.filter(p => isPathArchSpecific(p) === archSpecific);
files = files.filter(p => !!/-x86|-arm/.exec(path.basename(p)) === archSpecific);
if (archSpecific && files.length > 1 && arch) {
files = files.filter(p => path.basename(p).includes('-' + arch));
files = files.filter(p => path.basename(p).indexOf('-' + arch) !== -1);
}
return files.sort(outputFileComparator);
return files;
}
class ProjectBuilder {
@@ -295,7 +330,7 @@ class ProjectBuilder {
var wrapper = path.join(this.root, 'gradlew');
var args = this.getArgs(opts.buildType === 'debug' ? 'debug' : 'release', opts);
return execa(wrapper, args, { stdio: 'inherit', cwd: path.resolve(this.root) })
return execa(wrapper, args, { stdio: 'inherit' })
.catch(function (error) {
if (error.toString().indexOf('failed to find target with hash string') >= 0) {
return check_reqs.check_android_target(error).then(function () {
@@ -311,7 +346,7 @@ class ProjectBuilder {
clean (opts) {
const wrapper = path.join(this.root, 'gradlew');
const args = this.getArgs('clean', opts);
return execa(wrapper, args, { stdio: 'inherit', cwd: path.resolve(this.root) })
return execa(wrapper, args, { stdio: 'inherit' })
.then(() => {
fs.removeSync(path.join(this.root, 'out'));
@@ -327,11 +362,11 @@ class ProjectBuilder {
}
findOutputApks (build_type, arch) {
return findOutputFiles.call(this, 'apk', build_type, { arch });
return findOutputFilesHelper(this.apkDir, build_type, arch, 'apk').sort(fileSorter);
}
findOutputBundles (build_type) {
return findOutputFiles.call(this, 'aab', build_type);
return findOutputFilesHelper(this.aabDir, build_type, false, 'aab').sort(fileSorter);
}
fetchBuildResults (build_type, arch) {

View File

@@ -20,20 +20,43 @@
const execa = require('execa');
var path = require('path');
var fs = require('fs-extra');
const { forgivingWhichSync, isWindows, isDarwin } = require('./utils');
const java = require('./env/java');
var os = require('os');
var which = require('which');
var REPO_ROOT = path.join(__dirname, '..', '..', '..', '..');
var PROJECT_ROOT = path.join(__dirname, '..', '..');
const { CordovaError, ConfigParser, events } = require('cordova-common');
var android_sdk = require('./android_sdk');
const { createEditor } = require('properties-parser');
const semver = require('semver');
const EXPECTED_JAVA_VERSION = '1.8.x';
function forgivingWhichSync (cmd) {
const whichResult = which.sync(cmd, { nothrow: true });
// Re-exporting these for backwards compatibility and for unit testing.
// TODO: Remove uses and use the ./utils module directly.
Object.assign(module.exports, { isWindows, isDarwin });
// On null, returns empty string to maintain backwards compatibility
// realpathSync follows symlinks
return whichResult === null ? '' : fs.realpathSync(whichResult);
}
function getJDKDirectory (directory) {
const p = path.resolve(directory, 'java');
if (fs.existsSync(p)) {
const directories = fs.readdirSync(p);
for (let i = 0; i < directories.length; i++) {
const dir = directories[i];
if (/^(jdk)+./.test(dir)) {
return path.resolve(directory, 'java', dir);
}
}
}
return null;
}
module.exports.isWindows = function () {
return (os.platform() === 'win32');
};
module.exports.isDarwin = function () {
return (os.platform() === 'darwin');
};
/**
* @description Get valid target from framework/project.properties if run from this repo
@@ -75,6 +98,18 @@ module.exports.get_target = function () {
return target;
};
// Returns a promise. Called only by build and clean commands.
module.exports.check_ant = function () {
return execa('ant', ['-version']).then(({ stdout: output }) => {
// Parse Ant version from command output
return /version ((?:\d+\.)+(?:\d+))/i.exec(output)[1];
}).catch(function (err) {
if (err) {
throw new CordovaError('Failed to run `ant -version`. Make sure you have `ant` on your $PATH.');
}
});
};
module.exports.get_gradle_wrapper = function () {
var androidStudioPath;
var i = 0;
@@ -133,22 +168,75 @@ module.exports.check_gradle = function () {
'in your path, or install Android Studio'));
};
/**
* Checks for the java installation and correct version
*
* Despite the name, it should return the Java version value, it's used by the Cordova CLI.
*/
module.exports.check_java = async function () {
const javaVersion = await java.getVersion();
// Returns a promise.
module.exports.check_java = function () {
var javacPath = forgivingWhichSync('javac');
var hasJavaHome = !!process.env.JAVA_HOME;
return Promise.resolve().then(function () {
if (hasJavaHome) {
// Windows java installer doesn't add javac to PATH, nor set JAVA_HOME (ugh).
if (!javacPath) {
process.env.PATH += path.delimiter + path.join(process.env.JAVA_HOME, 'bin');
}
} else {
if (javacPath) {
// OS X has a command for finding JAVA_HOME.
var find_java = '/usr/libexec/java_home';
var default_java_error_msg = 'Failed to find \'JAVA_HOME\' environment variable. Try setting it manually.';
if (fs.existsSync(find_java)) {
return execa(find_java).then(({ stdout }) => {
process.env.JAVA_HOME = stdout;
}).catch(function (err) {
if (err) {
throw new CordovaError(default_java_error_msg);
}
});
} else {
// See if we can derive it from javac's location.
var maybeJavaHome = path.dirname(path.dirname(javacPath));
if (fs.existsSync(path.join(maybeJavaHome, 'lib', 'tools.jar'))) {
process.env.JAVA_HOME = maybeJavaHome;
} else {
throw new CordovaError(default_java_error_msg);
}
}
} else if (module.exports.isWindows()) {
const programFilesEnv = path.resolve(process.env.ProgramFiles);
const programFiles = 'C:\\Program Files\\';
const programFilesx86 = 'C:\\Program Files (x86)\\';
if (!semver.satisfies(javaVersion.version, EXPECTED_JAVA_VERSION)) {
throw new CordovaError(
`Requirements check failed for JDK ${EXPECTED_JAVA_VERSION}! Detected version: ${javaVersion.version}\n` +
'Check your ANDROID_SDK_ROOT / JAVA_HOME / PATH environment variables.'
);
}
let firstJdkDir =
getJDKDirectory(programFilesEnv) ||
getJDKDirectory(programFiles) ||
getJDKDirectory(programFilesx86);
return javaVersion;
if (firstJdkDir) {
firstJdkDir = firstJdkDir.replace(/\//g, path.sep);
if (!javacPath) {
process.env.PATH += path.delimiter + path.join(firstJdkDir, 'bin');
}
process.env.JAVA_HOME = firstJdkDir;
}
}
}
}).then(function () {
return execa('javac', ['-version'], { all: true })
.then(({ all: output }) => {
// Java <= 8 writes version info to stderr, Java >= 9 to stdout
const match = /javac\s+([\d.]+)/i.exec(output);
return match && match[1];
}, () => {
var msg =
'Failed to run "javac -version", make sure that you have a JDK version 8 installed.\n' +
'You can get it from the following location:\n' +
'https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html';
if (process.env.JAVA_HOME) {
msg += '\n\n';
msg += 'Your JAVA_HOME is invalid: ' + process.env.JAVA_HOME;
}
throw new CordovaError(msg);
});
});
};
// Returns a promise.
@@ -324,6 +412,13 @@ module.exports.run = function () {
return Promise.all([this.check_java(), this.check_android()]).then(function (values) {
console.log('Using Android SDK: ' + process.env.ANDROID_SDK_ROOT);
if (!String(values[0]).startsWith('1.8.')) {
throw new CordovaError(
'Requirements check failed for JDK 8 (\'1.8.*\')! Detected version: ' + values[0] + '\n' +
'Check your ANDROID_SDK_ROOT / JAVA_HOME / PATH environment variables.'
);
}
if (!values[1]) {
throw new CordovaError('Requirements check failed for Android SDK! Android SDK was not detected.');
}

109
bin/templates/cordova/lib/device.js vendored Normal file
View File

@@ -0,0 +1,109 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
const execa = require('execa');
var build = require('./build');
var path = require('path');
var Adb = require('./Adb');
var AndroidManifest = require('./AndroidManifest');
var CordovaError = require('cordova-common').CordovaError;
var events = require('cordova-common').events;
/**
* Returns a promise for the list of the device ID's found
* @param lookHarder When true, try restarting adb if no devices are found.
*/
module.exports.list = function (lookHarder) {
return Adb.devices().then(function (list) {
if (list.length === 0 && lookHarder) {
// adb kill-server doesn't seem to do the trick.
// Could probably find a x-platform version of killall, but I'm not actually
// sure that this scenario even happens on non-OSX machines.
return execa('killall', ['adb']).then(function () {
events.emit('verbose', 'Restarting adb to see if more devices are detected.');
return Adb.devices();
}, function () {
// For non-killall OS's.
return list;
});
}
return list;
});
};
module.exports.resolveTarget = function (target) {
return this.list(true).then(function (device_list) {
if (!device_list || !device_list.length) {
return Promise.reject(new CordovaError('Failed to deploy to device, no devices found.'));
}
// default device
target = target || device_list[0];
if (device_list.indexOf(target) < 0) {
return Promise.reject(new CordovaError('ERROR: Unable to find target \'' + target + '\'.'));
}
return build.detectArchitecture(target).then(function (arch) {
return { target: target, arch: arch, isEmulator: false };
});
});
};
/*
* Installs a previously built application on the device
* and launches it.
* Returns a promise.
*/
module.exports.install = function (target, buildResults) {
return Promise.resolve().then(function () {
if (target && typeof target === 'object') {
return target;
}
return module.exports.resolveTarget(target);
}).then(function (resolvedTarget) {
var apk_path = build.findBestApkForArchitecture(buildResults, resolvedTarget.arch);
var manifest = new AndroidManifest(path.join(__dirname, '../../app/src/main/AndroidManifest.xml'));
var pkgName = manifest.getPackageId();
var launchName = pkgName + '/.' + manifest.getActivity().getName();
events.emit('log', 'Using apk: ' + apk_path);
events.emit('log', 'Package name: ' + pkgName);
return Adb.install(resolvedTarget.target, apk_path, { replace: true }).catch(function (error) {
// CB-9557 CB-10157 only uninstall and reinstall app if the one that
// is already installed on device was signed w/different certificate
if (!/INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES/.test(error.toString())) { throw error; }
events.emit('warn', 'Uninstalling app from device and reinstalling it again because the ' +
'installed app already signed with different key');
// This promise is always resolved, even if 'adb uninstall' fails to uninstall app
// or the app doesn't installed at all, so no error catching needed.
return Adb.uninstall(resolvedTarget.target, pkgName).then(function () {
return Adb.install(resolvedTarget.target, apk_path, { replace: true });
});
}).then(function () {
// unlock screen
return Adb.shell(resolvedTarget.target, 'input keyevent 82');
}).then(function () {
return Adb.start(resolvedTarget.target, launchName);
}).then(function () {
events.emit('log', 'LAUNCH SUCCESS');
});
});
};

View File

@@ -20,17 +20,25 @@
const execa = require('execa');
const fs = require('fs-extra');
var android_versions = require('android-versions');
var retry = require('./retry');
var build = require('./build');
var path = require('path');
var Adb = require('./Adb');
var AndroidManifest = require('./AndroidManifest');
var events = require('cordova-common').events;
var CordovaError = require('cordova-common').CordovaError;
var android_sdk = require('./android_sdk');
var check_reqs = require('./check_reqs');
var which = require('which');
var os = require('os');
// constants
const ONE_SECOND = 1000; // in milliseconds
const ONE_MINUTE = 60 * ONE_SECOND; // in milliseconds
const INSTALL_COMMAND_TIMEOUT = 5 * ONE_MINUTE; // in milliseconds
const NUM_INSTALL_RETRIES = 3;
const CHECK_BOOTED_INTERVAL = 3 * ONE_SECOND; // in milliseconds
const EXEC_KILL_SIGNAL = 'SIGKILL';
function forgivingWhichSync (cmd) {
const whichResult = which.sync(cmd, { nothrow: true });
@@ -207,9 +215,24 @@ module.exports.best_image = function () {
});
};
exports.list_started = async () => {
return (await Adb.devices())
.filter(id => id.startsWith('emulator-'));
// Returns a promise.
module.exports.list_started = function () {
return Adb.devices({ emulators: true });
};
// Returns a promise.
// TODO: we should remove this, there's a more robust method under android_sdk.js
module.exports.list_targets = function () {
return execa('android', ['list', 'targets'], { cwd: os.tmpdir() }).then(({ stdout: output }) => {
var target_out = output.split('\n');
var targets = [];
for (var i = target_out.length; i >= 0; i--) {
if (target_out[i].match(/id:/)) {
targets.push(targets[i].split(' ')[1]);
}
}
return targets;
});
};
/*
@@ -348,3 +371,147 @@ module.exports.wait_for_boot = function (emulator_id, time_remaining) {
}
});
};
/*
* Create avd
* TODO : Enter the stdin input required to complete the creation of an avd.
* Returns a promise.
*/
module.exports.create_image = function (name, target) {
console.log('Creating new avd named ' + name);
if (target) {
return execa('android', ['create', 'avd', '--name', name, '--target', target]).then(null, function (error) {
console.error('ERROR : Failed to create emulator image : ');
console.error(' Do you have the latest android targets including ' + target + '?');
console.error(error.message);
});
} else {
console.log('WARNING : Project target not found, creating avd with a different target but the project may fail to install.');
// TODO: there's a more robust method for finding targets in android_sdk.js
return execa('android', ['create', 'avd', '--name', name, '--target', this.list_targets()[0]]).then(function () {
// TODO: This seems like another error case, even though it always happens.
console.error('ERROR : Unable to create an avd emulator, no targets found.');
console.error('Ensure you have targets available by running the "android" command');
return Promise.reject(new CordovaError());
}, function (error) {
console.error('ERROR : Failed to create emulator image : ');
console.error(error.message);
});
}
};
module.exports.resolveTarget = function (target) {
return this.list_started().then(function (emulator_list) {
if (emulator_list.length < 1) {
return Promise.reject(new CordovaError('No running Android emulators found, please start an emulator before deploying your project.'));
}
// default emulator
target = target || emulator_list[0];
if (emulator_list.indexOf(target) < 0) {
return Promise.reject(new CordovaError('Unable to find target \'' + target + '\'. Failed to deploy to emulator.'));
}
return build.detectArchitecture(target).then(function (arch) {
return { target: target, arch: arch, isEmulator: true };
});
});
};
/*
* Installs a previously built application on the emulator and launches it.
* If no target is specified, then it picks one.
* If no started emulators are found, error out.
* Returns a promise.
*/
module.exports.install = function (givenTarget, buildResults) {
var target;
// We need to find the proper path to the Android Manifest
const manifestPath = path.join(__dirname, '..', '..', 'app', 'src', 'main', 'AndroidManifest.xml');
const manifest = new AndroidManifest(manifestPath);
const pkgName = manifest.getPackageId();
// resolve the target emulator
return Promise.resolve().then(function () {
if (givenTarget && typeof givenTarget === 'object') {
return givenTarget;
} else {
return module.exports.resolveTarget(givenTarget);
}
// set the resolved target
}).then(function (resolvedTarget) {
target = resolvedTarget;
// install the app
}).then(function () {
// This promise is always resolved, even if 'adb uninstall' fails to uninstall app
// or the app doesn't installed at all, so no error catching needed.
return Promise.resolve().then(function () {
var apk_path = build.findBestApkForArchitecture(buildResults, target.arch);
var execOptions = {
cwd: os.tmpdir(),
timeout: INSTALL_COMMAND_TIMEOUT, // in milliseconds
killSignal: EXEC_KILL_SIGNAL
};
events.emit('log', 'Using apk: ' + apk_path);
events.emit('log', 'Package name: ' + pkgName);
events.emit('verbose', 'Installing app on emulator...');
// A special function to call adb install in specific environment w/ specific options.
// Introduced as a part of fix for http://issues.apache.org/jira/browse/CB-9119
// to workaround sporadic emulator hangs
function adbInstallWithOptions (target, apk, opts) {
events.emit('verbose', 'Installing apk ' + apk + ' on ' + target + '...');
const args = ['-s', target, 'install', '-r', apk];
return execa('adb', args, opts).then(({ stdout }) => {
// 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.
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.';
}
throw new CordovaError('Failed to install apk to emulator: ' + stdout);
}
});
}
function installPromise () {
return adbInstallWithOptions(target.target, apk_path, execOptions).catch(function (error) {
// CB-9557 CB-10157 only uninstall and reinstall app if the one that
// is already installed on device was signed w/different certificate
if (!/INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES/.test(error.toString())) { throw error; }
events.emit('warn', 'Uninstalling app from device and reinstalling it because the ' +
'currently installed app was signed with different key');
// This promise is always resolved, even if 'adb uninstall' fails to uninstall app
// or the app doesn't installed at all, so no error catching needed.
return Adb.uninstall(target.target, pkgName).then(function () {
return adbInstallWithOptions(target.target, apk_path, execOptions);
});
});
}
return retry.retryPromise(NUM_INSTALL_RETRIES, installPromise).then(function (output) {
events.emit('log', 'INSTALL SUCCESS');
});
});
// unlock screen
}).then(function () {
events.emit('verbose', 'Unlocking screen...');
return Adb.shell(target.target, 'input keyevent 82');
}).then(function () {
Adb.start(target.target, pkgName + '/.' + manifest.getActivity().getName());
// report success or failure
}).then(function (output) {
events.emit('log', 'LAUNCH SUCCESS');
});
};

View File

@@ -1,123 +0,0 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
const execa = require('execa');
const fs = require('fs-extra');
const path = require('path');
const glob = require('fast-glob');
const { CordovaError, events } = require('cordova-common');
const utils = require('../utils');
const semver = require('semver');
/**
* Will be set to true on successful ensureness.
* If true, skips the expensive java checks.
*/
let javaIsEnsured = false;
const java = {
/**
* Gets the version from the javac executable.
*
* @returns {semver.SemVer}
*/
getVersion: async () => {
await java._ensure(process.env);
// Java <= 8 writes version info to stderr, Java >= 9 to stdout
let version = null;
try {
version = (await execa('javac', ['-version'], { all: true })).all;
} catch (ex) {
events.emit('verbose', ex.shortMessage);
let msg =
'Failed to run "javac -version", make sure that you have a JDK version 8 installed.\n' +
'You can get it from the following location:\n' +
'https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html';
if (process.env.JAVA_HOME) {
msg += '\n\n';
msg += 'Your JAVA_HOME is invalid: ' + process.env.JAVA_HOME;
}
throw new CordovaError(msg);
}
return semver.coerce(version);
},
/**
* Ensures that Java is installed. Will throw exception if not.
* Will set JAVA_HOME and PATH environment variables.
*
* This function becomes a no-op if already ran previously.
*/
_ensure: async (environment) => {
if (javaIsEnsured) {
return;
}
const javacPath = utils.forgivingWhichSync('javac');
const hasJavaHome = !!environment.JAVA_HOME;
if (hasJavaHome) {
// Windows java installer doesn't add javac to PATH, nor set JAVA_HOME (ugh).
if (!javacPath) {
environment.PATH += path.delimiter + path.join(environment.JAVA_HOME, 'bin');
}
} else {
if (javacPath) {
// OS X has a command for finding JAVA_HOME.
const find_java = '/usr/libexec/java_home';
const default_java_error_msg = 'Failed to find \'JAVA_HOME\' environment variable. Try setting it manually.';
if (fs.existsSync(find_java)) {
try {
environment.JAVA_HOME = (await execa(find_java)).stdout;
} catch (ex) {
events.emit('verbose', ex.shortMessage);
throw new CordovaError(default_java_error_msg);
}
} else {
// See if we can derive it from javac's location.
var maybeJavaHome = path.dirname(path.dirname(javacPath));
if (fs.existsSync(path.join(maybeJavaHome, 'lib', 'tools.jar'))) {
environment.JAVA_HOME = maybeJavaHome;
} else {
throw new CordovaError(default_java_error_msg);
}
}
} else if (utils.isWindows()) {
const baseDirs = [environment.ProgramFiles, environment['ProgramFiles(x86)']];
const globOpts = { absolute: true, onlyDirectories: true };
const flatMap = (arr, f) => [].concat(...arr.map(f));
const jdkDir = flatMap(baseDirs, cwd => {
return glob.sync('java/jdk*', { cwd, ...globOpts });
}
)[0];
if (jdkDir) {
environment.PATH += path.delimiter + path.join(jdkDir, 'bin');
environment.JAVA_HOME = path.normalize(jdkDir);
}
}
}
javaIsEnsured = true;
}
};
module.exports = java;

View File

@@ -19,21 +19,24 @@
under the License.
*/
const { resolve, install } = require('./target');
var device = require('./device');
var args = process.argv;
const targetSpec = { type: 'device' };
if (args.length > 2) {
var install_target;
if (args[2].substring(0, 9) === '--target=') {
targetSpec.id = args[2].substring(9, args[2].length);
install_target = args[2].substring(9, args[2].length);
device.install(install_target).catch(function (err) {
console.error('ERROR: ' + err);
process.exit(2);
});
} else {
console.error('ERROR : argument \'' + args[2] + '\' not recognized.');
process.exit(2);
}
} else {
device.install().catch(function (err) {
console.error('ERROR: ' + err);
process.exit(2);
});
}
resolve(targetSpec).then(install).catch(err => {
console.error('ERROR: ' + err);
process.exit(2);
});

View File

@@ -19,21 +19,20 @@
under the License.
*/
const { resolve, install } = require('./target');
var emulator = require('./emulator');
var args = process.argv;
const targetSpec = { type: 'emulator' };
var install_target;
if (args.length > 2) {
if (args[2].substring(0, 9) === '--target=') {
targetSpec.id = args[2].substring(9, args[2].length);
install_target = args[2].substring(9, args[2].length);
} else {
console.error('ERROR : argument \'' + args[2] + '\' not recognized.');
process.exit(2);
}
}
resolve(targetSpec).then(install).catch(err => {
emulator.install(install_target).catch(function (err) {
console.error('ERROR: ' + err);
process.exit(2);
});

View File

@@ -19,16 +19,14 @@
under the License.
*/
const { list } = require('./target');
var devices = require('./device');
// Usage support for when args are given
require('./check_reqs').check_android().then(function () {
list().then(targets => {
const deviceIds = targets
.filter(({ type }) => type === 'device')
.map(({ id }) => id);
console.log(deviceIds.join('\n'));
devices.list().then(function (device_list) {
device_list && device_list.forEach(function (dev) {
console.log(dev);
});
}, function (err) {
console.error('ERROR: ' + err);
process.exit(2);

View File

@@ -16,7 +16,6 @@
var fs = require('fs-extra');
var path = require('path');
var isPathInside = require('is-path-inside');
var events = require('cordova-common').events;
var CordovaError = require('cordova-common').CordovaError;
@@ -210,12 +209,12 @@ function copyFile (plugin_dir, src, project_dir, dest, link) {
// check that src path is inside plugin directory
var real_path = fs.realpathSync(src);
var real_plugin_path = fs.realpathSync(plugin_dir);
if (!isPathInside(real_path, real_plugin_path)) { throw new CordovaError('File "' + src + '" is located outside the plugin directory "' + 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 (!isPathInside(dest, project_dir)) { throw new CordovaError('Destination "' + dest + '" for source file "' + src + '" is located outside the project'); }
if (dest.indexOf(project_dir) !== 0) { throw new CordovaError('Destination "' + dest + '" for source file "' + src + '" is located outside the project'); }
fs.ensureDirSync(path.dirname(dest));
if (link) {

View File

@@ -20,7 +20,6 @@
var fs = require('fs-extra');
var path = require('path');
const nopt = require('nopt');
const glob = require('fast-glob');
var events = require('cordova-common').events;
var AndroidManifest = require('./AndroidManifest');
var checkReqs = require('./check_reqs');
@@ -238,9 +237,9 @@ function updateProjectAccordingTo (platformConfig, locations) {
// Java file paths shouldn't be hard coded
const javaDirectory = path.join(locations.javaSrc, manifestId.replace(/\./g, '/'));
const java_files = glob.sync('**/*.java', { cwd: javaDirectory, absolute: true }).filter(f => {
const contents = fs.readFileSync(f, 'utf-8');
return /extends\s+CordovaActivity/.test(contents);
const javaPattern = /\.java$/;
const java_files = utils.scanDirectory(javaDirectory, javaPattern, true).filter(function (f) {
return utils.grep(f, /extends\s+CordovaActivity/g) !== null;
});
if (java_files.length === 0) {
@@ -302,12 +301,11 @@ function default_versionCode (version) {
}
function getImageResourcePath (resourcesDir, type, density, name, sourceName) {
// Use same extension as source with special case for 9-Patch files
const ext = sourceName.endsWith('.9.png')
? '.9.png' : path.extname(sourceName).toLowerCase();
const subDir = density ? `${type}-${density}` : type;
return path.join(resourcesDir, subDir, name + ext);
if (/\.9\.png$/.test(sourceName)) {
name = name.replace(/\.png$/, '.9.png');
}
var resourcePath = path.join(resourcesDir, (density ? type + '-' + density : type), name);
return resourcePath;
}
function getAdaptiveImageResourcePath (resourcesDir, type, density, name, sourceName) {
@@ -318,15 +316,6 @@ function getAdaptiveImageResourcePath (resourcesDir, type, density, name, source
return resourcePath;
}
function makeSplashCleanupMap (projectRoot, resourcesDir) {
// Build an initial resource map that deletes all existing splash screens
const existingSplashPaths = glob.sync(
`${resourcesDir.replace(/\\/g, '/')}/drawable-*/screen.{png,9.png,webp,jpg,jpeg}`,
{ cwd: projectRoot }
);
return makeCleanResourceMap(existingSplashPaths);
}
function updateSplashes (cordovaProject, platformResourcesDir) {
var resources = cordovaProject.projectConfig.getSplashScreens('android');
@@ -336,8 +325,7 @@ function updateSplashes (cordovaProject, platformResourcesDir) {
return;
}
// Build an initial resource map that deletes all existing splash screens
const resourceMap = makeSplashCleanupMap(cordovaProject.root, platformResourcesDir);
var resourceMap = mapImageResources(cordovaProject.root, platformResourcesDir, 'drawable', 'screen.png');
var hadMdpi = false;
resources.forEach(function (resource) {
@@ -348,14 +336,14 @@ function updateSplashes (cordovaProject, platformResourcesDir) {
hadMdpi = true;
}
var targetPath = getImageResourcePath(
platformResourcesDir, 'drawable', resource.density, 'screen', path.basename(resource.src));
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', path.basename(resources.defaultResource.src));
platformResourcesDir, 'drawable', 'mdpi', 'screen.png', path.basename(resources.defaultResource.src));
resourceMap[targetPath] = resources.defaultResource.src;
}
@@ -367,8 +355,7 @@ function updateSplashes (cordovaProject, platformResourcesDir) {
function cleanSplashes (projectRoot, projectConfig, platformResourcesDir) {
var resources = projectConfig.getSplashScreens('android');
if (resources.length > 0) {
const resourceMap = makeSplashCleanupMap(projectRoot, platformResourcesDir);
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.
@@ -558,13 +545,13 @@ function updateIconResourceForLegacy (preparedIcons, resourceMap, platformResour
// The source paths for icons and splashes are relative to
// project's config.xml location, so we use it as base path.
for (var density in android_icons) {
var targetPath = getImageResourcePath(platformResourcesDir, 'mipmap', density, 'ic_launcher', path.basename(android_icons[density].src));
var targetPath = getImageResourcePath(platformResourcesDir, 'mipmap', density, 'ic_launcher.png', path.basename(android_icons[density].src));
resourceMap[targetPath] = android_icons[density].src;
}
// There's no "default" drawable, so assume default == mdpi.
if (default_icon && !android_icons.mdpi) {
var defaultTargetPath = getImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher', path.basename(default_icon.src));
var defaultTargetPath = getImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher.png', path.basename(default_icon.src));
resourceMap[defaultTargetPath] = default_icon.src;
}
@@ -677,24 +664,14 @@ function cleanIcons (projectRoot, projectConfig, platformResourcesDir) {
*/
function mapImageResources (rootDir, subDir, type, resourceName) {
const pathMap = {};
const globOptions = { cwd: path.join(rootDir, subDir), onlyDirectories: true };
glob.sync(type + '-*', globOptions).forEach(drawableFolder => {
const imagePath = path.join(subDir, drawableFolder, resourceName);
const pattern = new RegExp(type + '+-.+');
utils.scanDirectory(path.join(rootDir, subDir), pattern).forEach(function (drawableFolder) {
const imagePath = path.join(subDir, path.basename(drawableFolder), resourceName);
pathMap[imagePath] = null;
});
return pathMap;
}
/** Returns resource map that deletes all given paths */
function makeCleanResourceMap (resourcePaths) {
const pathMap = {};
resourcePaths.map(path.normalize)
.forEach(resourcePath => {
pathMap[resourcePath] = null;
});
return pathMap;
}
function updateFileResources (cordovaProject, platformDir) {
var files = cordovaProject.projectConfig.getFileResources('android');

View File

@@ -21,7 +21,7 @@
var events = require('cordova-common').events;
/**
/*
* Retry a promise-returning function a number of times, propagating its
* results on success or throwing its error on a failed final attempt.
*
@@ -31,13 +31,31 @@ var events = require('cordova-common').events;
*
* @returns {Promise}
*/
module.exports.retryPromise = async function (attemptsLeft, promiseFunction, ...args) {
while (true) {
try {
return await promiseFunction(...args);
} catch (error) {
if (--attemptsLeft < 1) throw error;
module.exports.retryPromise = function (attemptsLeft, promiseFunction) {
// NOTE:
// get all trailing arguments, by skipping the first two (attemptsLeft and
// promiseFunction) because they shouldn't get passed to promiseFunction
var promiseFunctionArguments = Array.prototype.slice.call(arguments, 2);
return promiseFunction.apply(undefined, promiseFunctionArguments).then(
// on success pass results through
function onFulfilled (value) {
return value;
},
// on rejection either retry, or throw the error
function onRejected (error) {
attemptsLeft -= 1;
if (attemptsLeft < 1) {
throw error;
}
events.emit('verbose', 'A retried call failed. Retrying ' + attemptsLeft + ' more time(s).');
// retry call self again with the same arguments, except attemptsLeft is now lower
var fullArguments = [attemptsLeft, promiseFunction].concat(promiseFunctionArguments);
return module.exports.retryPromise.apply(undefined, fullArguments);
}
}
);
};

View File

@@ -19,30 +19,21 @@
var path = require('path');
var emulator = require('./emulator');
const target = require('./target');
var device = require('./device');
var PackageType = require('./PackageType');
const { events } = require('cordova-common');
const { CordovaError, events } = require('cordova-common');
/**
* Builds a target spec from a runOptions object
*
* @param {{target?: string, device?: boolean, emulator?: boolean}} runOptions
* @return {target.TargetSpec}
*/
function buildTargetSpec (runOptions) {
const spec = {};
function getInstallTarget (runOptions) {
var install_target;
if (runOptions.target) {
spec.id = runOptions.target;
install_target = runOptions.target;
} else if (runOptions.device) {
spec.type = 'device';
install_target = '--device';
} else if (runOptions.emulator) {
spec.type = 'emulator';
install_target = '--emulator';
}
return spec;
}
function formatResolvedTarget ({ id, type }) {
return `${type} ${id}`;
return install_target;
}
/**
@@ -59,12 +50,57 @@ module.exports.run = function (runOptions) {
runOptions = runOptions || {};
var self = this;
const spec = buildTargetSpec(runOptions);
return target.resolve(spec).then(function (resolvedTarget) {
events.emit('log', `Deploying to ${formatResolvedTarget(resolvedTarget)}`);
var install_target = getInstallTarget(runOptions);
return Promise.resolve().then(function () {
if (!install_target) {
// no target given, deploy to device if available, otherwise use the emulator.
return device.list().then(function (device_list) {
if (device_list.length > 0) {
events.emit('warn', 'No target specified, deploying to device \'' + device_list[0] + '\'.');
install_target = device_list[0];
} else {
events.emit('warn', 'No target specified and no devices found, deploying to emulator');
install_target = '--emulator';
}
});
}
}).then(function () {
if (install_target === '--device') {
return device.resolveTarget(null);
} else if (install_target === '--emulator') {
// Give preference to any already started emulators. Else, start one.
return emulator.list_started().then(function (started) {
return started && started.length > 0 ? started[0] : emulator.start();
}).then(function (emulatorId) {
return emulator.resolveTarget(emulatorId);
});
}
// They specified a specific device/emulator ID.
return device.list().then(function (devices) {
if (devices.indexOf(install_target) > -1) {
return device.resolveTarget(install_target);
}
return emulator.list_started().then(function (started_emulators) {
if (started_emulators.indexOf(install_target) > -1) {
return emulator.resolveTarget(install_target);
}
return emulator.list_images().then(function (avds) {
// if target emulator isn't started, then start it.
for (var avd in avds) {
if (avds[avd].name === install_target) {
return emulator.start(install_target).then(function (emulatorId) {
return emulator.resolveTarget(emulatorId);
});
}
}
return Promise.reject(new CordovaError(`Target '${install_target}' not found, unable to run project`));
});
});
});
}).then(function (resolvedTarget) {
return new Promise((resolve) => {
const builder = require('./builders/builders').getBuilder();
const buildOptions = require('./build').parseBuildOptions(runOptions, null, self.root);
// Android app bundles cannot be deployed directly to the device
@@ -74,13 +110,15 @@ module.exports.run = function (runOptions) {
throw packageTypeErrorMessage;
}
resolve(self._builder.fetchBuildResults(buildOptions.buildType, buildOptions.arch));
}).then(async function (buildResults) {
if (resolvedTarget.type === 'emulator') {
await emulator.wait_for_boot(resolvedTarget.id);
resolve(builder.fetchBuildResults(buildOptions.buildType, buildOptions.arch));
}).then(function (buildResults) {
if (resolvedTarget && resolvedTarget.isEmulator) {
return emulator.wait_for_boot(resolvedTarget.target).then(function () {
return emulator.install(resolvedTarget, buildResults);
});
}
return target.install(resolvedTarget, buildResults);
return device.install(resolvedTarget, buildResults);
});
});
};

View File

@@ -1,155 +0,0 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
const path = require('path');
const { inspect } = require('util');
const Adb = require('./Adb');
const build = require('./build');
const emulator = require('./emulator');
const AndroidManifest = require('./AndroidManifest');
const { compareBy } = require('./utils');
const { retryPromise } = require('./retry');
const { events, CordovaError } = require('cordova-common');
const INSTALL_COMMAND_TIMEOUT = 5 * 60 * 1000;
const NUM_INSTALL_RETRIES = 3;
const EXEC_KILL_SIGNAL = 'SIGKILL';
/**
* @typedef { 'device' | 'emulator' } TargetType
* @typedef { { id: string, type: TargetType } } Target
* @typedef { { id?: string, type?: TargetType } } TargetSpec
*/
/**
* Returns a list of available targets (connected devices & started emulators)
*
* @return {Promise<Target[]>}
*/
exports.list = async () => {
return (await Adb.devices())
.map(id => ({
id,
type: id.startsWith('emulator-') ? 'emulator' : 'device'
}));
};
/**
* @param {TargetSpec?} spec
* @return {Promise<Target>}
*/
async function resolveToOnlineTarget (spec = {}) {
const targetList = await exports.list();
if (targetList.length === 0) return null;
// Sort by type: devices first, then emulators.
targetList.sort(compareBy(t => t.type));
// Find first matching target for spec. {} matches any target.
return targetList.find(target =>
Object.keys(spec).every(k => spec[k] === target[k])
) || null;
}
async function isEmulatorName (name) {
const emus = await emulator.list_images();
return emus.some(avd => avd.name === name);
}
/**
* @param {TargetSpec?} spec
* @return {Promise<Target>}
*/
async function resolveToOfflineEmulator (spec = {}) {
if (spec.type === 'device') return null;
if (spec.id && !(await isEmulatorName(spec.id))) return null;
// try to start an emulator with name spec.id
// if spec.id is undefined, picks best match regarding target API
const emulatorId = await emulator.start(spec.id);
return { id: emulatorId, type: 'emulator' };
}
/**
* @param {TargetSpec?} spec
* @return {Promise<Target & {arch: string}>}
*/
exports.resolve = async (spec = {}) => {
events.emit('verbose', `Trying to find target matching ${inspect(spec)}`);
const resolvedTarget =
(await resolveToOnlineTarget(spec)) ||
(await resolveToOfflineEmulator(spec));
if (!resolvedTarget) {
throw new CordovaError(`Could not find target matching ${inspect(spec)}`);
}
return {
...resolvedTarget,
arch: await build.detectArchitecture(resolvedTarget.id)
};
};
exports.install = async function ({ id: target, arch, type }, buildResults) {
const apk_path = build.findBestApkForArchitecture(buildResults, arch);
const manifest = new AndroidManifest(path.join(__dirname, '../../app/src/main/AndroidManifest.xml'));
const pkgName = manifest.getPackageId();
const launchName = pkgName + '/.' + manifest.getActivity().getName();
events.emit('log', 'Using apk: ' + apk_path);
events.emit('log', 'Package name: ' + pkgName);
events.emit('verbose', `Installing app on target ${target}`);
async function doInstall (execOptions = {}) {
try {
await Adb.install(target, apk_path, { replace: true, execOptions });
} catch (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.
await Adb.uninstall(target, pkgName);
await Adb.install(target, apk_path, { replace: true });
}
}
if (type === 'emulator') {
// Work around sporadic emulator hangs: http://issues.apache.org/jira/browse/CB-9119
await retryPromise(NUM_INSTALL_RETRIES, () => doInstall({
timeout: INSTALL_COMMAND_TIMEOUT,
killSignal: EXEC_KILL_SIGNAL
}));
} else {
await doInstall();
}
events.emit('log', 'INSTALL SUCCESS');
events.emit('verbose', 'Unlocking screen...');
await Adb.shell(target, 'input keyevent 82');
await Adb.start(target, launchName);
events.emit('log', 'LAUNCH SUCCESS');
};

View File

@@ -24,8 +24,7 @@
// TODO: Perhaps this should live in cordova-common?
const fs = require('fs-extra');
const which = require('which');
const os = require('os');
const path = require('path');
/**
* Reads, searches, and replaces the found occurences with replacementString and then writes the file back out.
@@ -42,27 +41,57 @@ exports.replaceFileContents = function (file, searchRegex, replacementString) {
fs.writeFileSync(file, contents);
};
// Some helpers for easier sorting
exports.compare = (a, b) => (a < b && -1) || +(a > b);
exports.compareBy = f => (a, b) => exports.compare(f(a), f(b));
exports.compareByAll = fns => {
const comparators = fns.map(exports.compareBy);
return (a, b) => {
for (const cmp of comparators) {
const result = cmp(a, b);
if (result) return result;
/**
* Reads a file and scans for regex. Returns the line of the first occurence or null if no occurences are found.
*
* @param {string} file A file path
* @param {RegExp} regex A search regex
* @returns string|null
*/
exports.grep = function (file, regex) {
const contents = fs.readFileSync(file).toString().replace(/\\r/g, '').split('\n');
for (let i = 0; i < contents.length; i++) {
const line = contents[i];
if (regex.test(line)) {
return line;
}
return 0;
};
}
return null;
};
exports.forgivingWhichSync = (cmd) => {
const whichResult = which.sync(cmd, { nothrow: true });
/**
* Scans directories and outputs a list of found paths that matches the regex
*
* @param {string} directory The starting directory
* @param {RegExp} regex The search regex
* @param {boolean} recursive Enables recursion
* @returns Array<string>
*/
exports.scanDirectory = function (directory, regex, recursive) {
let output = [];
// On null, returns empty string to maintain backwards compatibility
// realpathSync follows symlinks
return whichResult === null ? '' : fs.realpathSync(whichResult);
if (fs.existsSync(directory)) {
const items = fs.readdirSync(directory);
for (let i = 0; i < items.length; i++) {
const item = items[i];
const itemPath = path.join(directory, item);
const stats = fs.statSync(itemPath);
if (regex.test(itemPath)) {
output.push(itemPath);
}
if (stats.isDirectory()) {
if (recursive) {
output = output.concat(exports.scanDirectory(itemPath, regex, recursive));
} else {
// Move onto the next item
continue;
}
}
}
}
return output;
};
exports.isWindows = () => os.platform() === 'win32';
exports.isDarwin = () => os.platform() === 'darwin';

View File

@@ -44,8 +44,11 @@ buildscript {
ext.kotlin_version = gradlePluginKotlinVersion
}
apply from: 'repositories.gradle'
repositories repos
repositories {
mavenCentral()
google()
jcenter()
}
dependencies {
apply from: '../CordovaLib/cordova.gradle'
@@ -79,8 +82,10 @@ buildscript {
// Allow plugins to declare Maven dependencies via build-extras.gradle.
allprojects {
apply from: 'repositories.gradle'
repositories repos
repositories {
mavenCentral()
jcenter()
}
}
task wrapper(type: Wrapper) {

View File

@@ -1,23 +0,0 @@
/* 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.
*/
ext.repos = {
mavenCentral()
google()
jcenter()
}

View File

@@ -1,5 +1,5 @@
// Platform: android
// cordova-js rel/6.0.0-10-g07379820
// 538a985db128858c0a0eb4dd40fb9c8e5433fc94
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
@@ -19,7 +19,7 @@
under the License.
*/
;(function() {
var PLATFORM_VERSION_BUILD_LABEL = '9.1.0';
var PLATFORM_VERSION_BUILD_LABEL = '9.0.0';
// file: src/scripts/require.js
var require;
var define;

View File

@@ -20,9 +20,10 @@
buildscript {
ext.kotlin_version = '1.3.50'
apply from: 'repositories.gradle'
repositories repos
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
@@ -32,8 +33,10 @@ buildscript {
}
allprojects {
apply from: 'repositories.gradle'
repositories repos
repositories {
google()
jcenter()
}
//This replaces project.properties w.r.t. build settings
project.ext {

View File

@@ -1,22 +0,0 @@
/* 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.
*/
ext.repos = {
google()
jcenter()
}

View File

@@ -20,17 +20,13 @@ ext {
apply from: 'cordova.gradle'
cdvCompileSdkVersion = privateHelpers.getProjectTarget()
cdvBuildToolsVersion = privateHelpers.findLatestInstalledBuildTools()
if (project.hasProperty('cdvMinSdkVersion') && cdvMinSdkVersion.isInteger()) {
cdvMinSdkVersion = cdvMinSdkVersion as int
println '[Cordova] cdvMinSdkVersion is overridden, try it at your own risk.'
} else {
cdvMinSdkVersion = 22; // current Cordova's default
}
}
buildscript {
apply from: 'repositories.gradle'
repositories repos
repositories {
google()
jcenter()
}
dependencies {
// The gradle plugin and the maven plugin have to be updated after each version of Android
@@ -42,8 +38,10 @@ buildscript {
}
allprojects {
apply from: 'repositories.gradle'
repositories repos
repositories {
google()
jcenter()
}
}
apply plugin: 'com.android.library'
@@ -51,7 +49,7 @@ apply plugin: 'com.github.dcendents.android-maven'
apply plugin: 'com.jfrog.bintray'
group = 'org.apache.cordova'
version = '9.1.0'
version = '9.0.0'
android {
compileSdkVersion cdvCompileSdkVersion
@@ -62,9 +60,9 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
// For the Android Cordova Lib, we allow changing the minSdkVersion, but it is at the users own risk
// For the Android Cordova Lib, we will hardcode the minSdkVersion and not allow changes.
defaultConfig {
minSdkVersion cdvMinSdkVersion
minSdkVersion 22
}
sourceSets {
@@ -142,9 +140,9 @@ bintray {
licenses = ['Apache-2.0']
labels = ['android', 'cordova', 'phonegap']
version {
name = '9.1.0'
released = new Date()
vcsTag = '9.1.0'
name = '9.0.0'
released = new Date()
vcsTag = '9.0.0'
}
}
}

View File

@@ -1,22 +0,0 @@
/* 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.
*/
ext.repos = {
google()
jcenter()
}

View File

@@ -125,9 +125,6 @@ public class CordovaActivity extends Activity {
// (as was the case in previous cordova versions)
if (!preferences.getBoolean("FullscreenNotImmersive", false)) {
immersiveMode = true;
// The splashscreen plugin needs the flags set before we're focused to prevent
// the nav and title bars from flashing in.
setImmersiveUiVisibility();
} else {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
@@ -324,26 +321,22 @@ public class CordovaActivity extends Activity {
/**
* Called when view focus is changed
*/
@SuppressLint("InlinedApi")
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus && immersiveMode) {
setImmersiveUiVisibility();
final int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
getWindow().getDecorView().setSystemUiVisibility(uiOptions);
}
}
@SuppressLint("InlinedApi")
protected void setImmersiveUiVisibility() {
final int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
getWindow().getDecorView().setSystemUiVisibility(uiOptions);
}
@SuppressLint("NewApi")
@Override
public void startActivityForResult(Intent intent, int requestCode, Bundle options) {

View File

@@ -114,6 +114,7 @@ public class CordovaBridge {
/** Called by cordova.js to initialize the bridge. */
//On old Androids SecureRandom isn't really secure, this is the least of your problems if
//you're running Android 4.3 and below in 2017
@SuppressLint("TrulyRandom")
int generateBridgeSecret() {
SecureRandom randGen = new SecureRandom();
expectedBridgeSecret = randGen.nextInt(Integer.MAX_VALUE);

View File

@@ -41,7 +41,6 @@ import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.channels.FileChannel;
import java.util.Locale;
import java.util.zip.GZIPInputStream;
/**
* What this class provides:
@@ -287,19 +286,13 @@ public class CordovaResourceApi {
case URI_TYPE_HTTP:
case URI_TYPE_HTTPS: {
HttpURLConnection conn = (HttpURLConnection)new URL(uri.toString()).openConnection();
conn.setRequestProperty("Accept-Encoding", "gzip");
conn.setDoInput(true);
String mimeType = conn.getHeaderField("Content-Type");
if (mimeType != null) {
mimeType = mimeType.split(";")[0];
}
int length = conn.getContentLength();
InputStream inputStream;
if ("gzip".equals(conn.getContentEncoding())) {
inputStream = new GZIPInputStream(conn.getInputStream());
} else {
inputStream = conn.getInputStream();
}
InputStream inputStream = conn.getInputStream();
return new OpenForReadResult(uri, inputStream, mimeType, length, null);
}
case URI_TYPE_PLUGIN: {

View File

@@ -31,7 +31,7 @@ import android.webkit.WebChromeClient.CustomViewCallback;
* are not expected to implement it.
*/
public interface CordovaWebView {
public static final String CORDOVA_VERSION = "9.1.0";
public static final String CORDOVA_VERSION = "9.0.0";
void init(CordovaInterface cordova, List<PluginEntry> pluginEntries, CordovaPreferences preferences);

View File

@@ -19,7 +19,6 @@
package org.apache.cordova;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
@@ -35,7 +34,6 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.lang.reflect.Constructor;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -178,28 +176,22 @@ public class CordovaWebViewImpl implements CordovaWebView {
e.printStackTrace();
}
// If timeout, then stop loading and handle error (if activity still exists)
if (loadUrlTimeout == currentLoadUrlTimeout && cordova.getActivity() != null) {
// If timeout, then stop loading and handle error
if (loadUrlTimeout == currentLoadUrlTimeout) {
cordova.getActivity().runOnUiThread(loadError);
} else if (cordova.getActivity() == null) {
LOG.d(TAG, "Cordova activity does not exist.");
}
}
};
if (cordova.getActivity() != null) {
final boolean _recreatePlugins = recreatePlugins;
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
if (loadUrlTimeoutValue > 0) {
cordova.getThreadPool().execute(timeoutCheck);
}
engine.loadUrl(url, _recreatePlugins);
final boolean _recreatePlugins = recreatePlugins;
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
if (loadUrlTimeoutValue > 0) {
cordova.getThreadPool().execute(timeoutCheck);
}
});
} else {
LOG.d(TAG, "Cordova activity does not exist.");
}
engine.loadUrl(url, _recreatePlugins);
}
});
}
@@ -234,37 +226,21 @@ public class CordovaWebViewImpl implements CordovaWebView {
LOG.w(TAG, "showWebPage: Refusing to send intent for URL since it is not in the <allow-intent> whitelist. URL=" + url);
return;
}
Intent intent = null;
try {
if (url.startsWith("intent://")) {
intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
Intent intent = new Intent(Intent.ACTION_VIEW);
// To send an intent without CATEGORY_BROWSER, a custom plugin should be used.
intent.addCategory(Intent.CATEGORY_BROWSABLE);
Uri uri = Uri.parse(url);
// Omitting the MIME type for file: URLs causes "No Activity found to handle Intent".
// Adding the MIME type to http: URLs causes them to not be handled by the downloader.
if ("file".equals(uri.getScheme())) {
intent.setDataAndType(uri, resourceApi.getMimeType(uri));
} else {
intent = new Intent(Intent.ACTION_VIEW);
// To send an intent without CATEGORY_BROWSER, a custom plugin should be used.
intent.addCategory(Intent.CATEGORY_BROWSABLE);
Uri uri = Uri.parse(url);
// Omitting the MIME type for file: URLs causes "No Activity found to handle Intent".
// Adding the MIME type to http: URLs causes them to not be handled by the downloader.
if ("file".equals(uri.getScheme())) {
intent.setDataAndType(uri, resourceApi.getMimeType(uri));
} else {
intent.setData(uri);
}
}
if (cordova.getActivity() != null) {
cordova.getActivity().startActivity(intent);
} else {
LOG.d(TAG, "Cordova activity does not exist.");
}
} catch (URISyntaxException e) {
LOG.e(TAG, "Error parsing url " + url, e);
} catch (ActivityNotFoundException e) {
if (url.startsWith("intent://") && intent != null && intent.getStringExtra("browser_fallback_url") != null) {
showWebPage(intent.getStringExtra("browser_fallback_url"), openExternal, clearHistory, params);
} else {
LOG.e(TAG, "Error loading url " + url, e);
intent.setData(uri);
}
cordova.getActivity().startActivity(intent);
} catch (android.content.ActivityNotFoundException e) {
LOG.e(TAG, "Error loading url " + url, e);
}
}
@@ -279,12 +255,7 @@ public class CordovaWebViewImpl implements CordovaWebView {
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
boolean ret = engine.getView().dispatchKeyEvent(event);
if (!ret) {
// If the engine didn't handle the event, handle it normally.
ret = super.dispatchKeyEvent(event);
}
return ret;
return engine.getView().dispatchKeyEvent(event);
}
}
@@ -582,15 +553,11 @@ public class CordovaWebViewImpl implements CordovaWebView {
public void run() {
try {
Thread.sleep(2000);
if (cordova.getActivity() != null) {
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
pluginManager.postMessage("spinner", "stop");
}
});
} else {
LOG.d(TAG, "Cordova activity does not exist.");
}
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
pluginManager.postMessage("spinner", "stop");
}
});
} catch (InterruptedException e) {
}
}

View File

@@ -19,9 +19,7 @@
package org.apache.cordova;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import org.json.JSONException;
@@ -30,7 +28,6 @@ import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.os.Debug;
import android.os.Build;
/**
* PluginManager is exposed to JavaScript in the Cordova WebView.
@@ -43,8 +40,8 @@ public class PluginManager {
private static final int SLOW_EXEC_WARNING_THRESHOLD = Debug.isDebuggerConnected() ? 60 : 16;
// List of service entries
private final Map<String, CordovaPlugin> pluginMap = Collections.synchronizedMap(new LinkedHashMap<String, CordovaPlugin>());
private final Map<String, PluginEntry> entryMap = Collections.synchronizedMap(new LinkedHashMap<String, PluginEntry>());
private final LinkedHashMap<String, CordovaPlugin> pluginMap = new LinkedHashMap<String, CordovaPlugin>();
private final LinkedHashMap<String, PluginEntry> entryMap = new LinkedHashMap<String, PluginEntry>();
private final CordovaInterface ctx;
private final CordovaWebView app;
@@ -93,17 +90,13 @@ public class PluginManager {
* Create plugins objects that have onload set.
*/
private void startupPlugins() {
synchronized (entryMap) {
for (PluginEntry entry : entryMap.values()) {
// Add a null entry to for each non-startup plugin to avoid ConcurrentModificationException
// When iterating plugins.
if (entry.onload) {
getPlugin(entry.service);
}
else {
LOG.d(TAG, "startupPlugins: put - " + entry.service);
pluginMap.put(entry.service, null);
}
for (PluginEntry entry : entryMap.values()) {
// Add a null entry to for each non-startup plugin to avoid ConcurrentModificationException
// When iterating plugins.
if (entry.onload) {
getPlugin(entry.service);
} else {
pluginMap.put(entry.service, null);
}
}
}
@@ -176,7 +169,6 @@ public class PluginManager {
ret = instantiatePlugin(pe.pluginClass);
}
ret.privateInitialize(service, ctx, app, app.getPreferences());
LOG.d(TAG, "getPlugin - put: " + service);
pluginMap.put(service, ret);
}
return ret;
@@ -204,7 +196,6 @@ public class PluginManager {
this.entryMap.put(entry.service, entry);
if (entry.plugin != null) {
entry.plugin.privateInitialize(entry.service, ctx, app, app.getPreferences());
LOG.d(TAG, "addService: put - " + entry.service);
pluginMap.put(entry.service, entry.plugin);
}
}
@@ -215,11 +206,9 @@ public class PluginManager {
* @param multitasking Flag indicating if multitasking is turned on for app
*/
public void onPause(boolean multitasking) {
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onPause(multitasking);
}
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onPause(multitasking);
}
}
}
@@ -237,11 +226,9 @@ public class PluginManager {
*
*/
public boolean onReceivedHttpAuthRequest(CordovaWebView view, ICordovaHttpAuthHandler handler, String host, String realm) {
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null && plugin.onReceivedHttpAuthRequest(app, handler, host, realm)) {
return true;
}
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null && plugin.onReceivedHttpAuthRequest(app, handler, host, realm)) {
return true;
}
}
return false;
@@ -258,11 +245,9 @@ public class PluginManager {
*
*/
public boolean onReceivedClientCertRequest(CordovaWebView view, ICordovaClientCertRequest request) {
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null && plugin.onReceivedClientCertRequest(app, request)) {
return true;
}
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null && plugin.onReceivedClientCertRequest(app, request)) {
return true;
}
}
return false;
@@ -274,11 +259,9 @@ public class PluginManager {
* @param multitasking Flag indicating if multitasking is turned on for app
*/
public void onResume(boolean multitasking) {
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onResume(multitasking);
}
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onResume(multitasking);
}
}
}
@@ -287,11 +270,9 @@ public class PluginManager {
* Called when the activity is becoming visible to the user.
*/
public void onStart() {
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onStart();
}
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onStart();
}
}
}
@@ -300,11 +281,9 @@ public class PluginManager {
* Called when the activity is no longer visible to the user.
*/
public void onStop() {
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onStop();
}
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onStop();
}
}
}
@@ -313,11 +292,9 @@ public class PluginManager {
* The final call you receive before your activity is destroyed.
*/
public void onDestroy() {
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onDestroy();
}
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onDestroy();
}
}
}
@@ -330,22 +307,11 @@ public class PluginManager {
* @return Object to stop propagation or null
*/
public Object postMessage(String id, Object data) {
LOG.d(TAG, "postMessage: " + id);
synchronized (this.pluginMap) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
this.pluginMap.forEach((s, plugin) -> {
if (plugin != null) {
plugin.onMessage(id, data);
}
});
} else {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
Object obj = plugin.onMessage(id, data);
if (obj != null) {
return obj;
}
}
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
Object obj = plugin.onMessage(id, data);
if (obj != null) {
return obj;
}
}
}
@@ -356,11 +322,9 @@ public class PluginManager {
* Called when the activity receives a new intent.
*/
public void onNewIntent(Intent intent) {
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onNewIntent(intent);
}
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onNewIntent(intent);
}
}
}
@@ -377,14 +341,12 @@ public class PluginManager {
* false to block the resource.
*/
public boolean shouldAllowRequest(String url) {
synchronized (this.entryMap) {
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldAllowRequest(url);
if (result != null) {
return result;
}
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldAllowRequest(url);
if (result != null) {
return result;
}
}
}
@@ -417,14 +379,12 @@ public class PluginManager {
* false to block the navigation.
*/
public boolean shouldAllowNavigation(String url) {
synchronized (this.entryMap) {
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldAllowNavigation(url);
if (result != null) {
return result;
}
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldAllowNavigation(url);
if (result != null) {
return result;
}
}
}
@@ -438,14 +398,12 @@ public class PluginManager {
* Called when the webview is requesting the exec() bridge be enabled.
*/
public boolean shouldAllowBridgeAccess(String url) {
synchronized (this.entryMap) {
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldAllowBridgeAccess(url);
if (result != null) {
return result;
}
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldAllowBridgeAccess(url);
if (result != null) {
return result;
}
}
}
@@ -467,14 +425,12 @@ public class PluginManager {
* false to block the intent.
*/
public Boolean shouldOpenExternalUrl(String url) {
synchronized (this.entryMap) {
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldOpenExternalUrl(url);
if (result != null) {
return result;
}
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldOpenExternalUrl(url);
if (result != null) {
return result;
}
}
}
@@ -490,38 +446,32 @@ public class PluginManager {
* @return Return false to allow the URL to load, return true to prevent the URL from loading.
*/
public boolean onOverrideUrlLoading(String url) {
synchronized (this.entryMap) {
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null && plugin.onOverrideUrlLoading(url)) {
return true;
}
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null && plugin.onOverrideUrlLoading(url)) {
return true;
}
return false;
}
return false;
}
/**
* Called when the app navigates or refreshes.
*/
public void onReset() {
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onReset();
}
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onReset();
}
}
}
Uri remapUri(Uri uri) {
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
Uri ret = plugin.remapUri(uri);
if (ret != null) {
return ret;
}
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
Uri ret = plugin.remapUri(uri);
if (ret != null) {
return ret;
}
}
}
@@ -554,24 +504,20 @@ public class PluginManager {
* @param newConfig The new device configuration
*/
public void onConfigurationChanged(Configuration newConfig) {
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onConfigurationChanged(newConfig);
}
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onConfigurationChanged(newConfig);
}
}
}
public Bundle onSaveInstanceState() {
Bundle state = new Bundle();
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
Bundle pluginState = plugin.onSaveInstanceState();
if (pluginState != null) {
state.putBundle(plugin.getServiceName(), pluginState);
}
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
Bundle pluginState = plugin.onSaveInstanceState();
if(pluginState != null) {
state.putBundle(plugin.getServiceName(), pluginState);
}
}
}

View File

@@ -152,15 +152,6 @@ public class SystemWebViewEngine implements CordovaWebViewEngine {
settings.setJavaScriptCanOpenWindowsAutomatically(true);
settings.setLayoutAlgorithm(LayoutAlgorithm.NORMAL);
/**
* https://developer.android.com/reference/android/webkit/WebSettings#setAllowFileAccess(boolean)
*
* SDK >= 30 has recently set this value to false by default.
* It is recommended to turn off this settings To prevent possible security issues targeting Build.VERSION_CODES.Q and earlier.
* For existing functionality, this setting is set to true. In a future release, this should be defaulted to false.
*/
settings.setAllowFileAccess(true);
String manufacturer = android.os.Build.MANUFACTURER;
LOG.d(TAG, "CordovaWebView is running on device made by: " + manufacturer);

1197
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "cordova-android",
"version": "9.1.0",
"version": "9.0.0",
"description": "cordova-android release",
"bin": {
"create": "bin/create"
@@ -15,12 +15,12 @@
],
"scripts": {
"test": "npm run lint && npm run cover && npm run java-unit-tests",
"lint": "eslint . \"bin/**/!(*.*|gitignore)\"",
"unit-tests": "jasmine --config=spec/unit/jasmine.json",
"cover": "nyc jasmine --config=spec/coverage.json",
"e2e-tests": "jasmine --config=spec/e2e/jasmine.json",
"java-unit-tests": "node test/run_java_unit_tests.js",
"clean:java-unit-tests": "node test/clean.js"
"lint": "eslint . \"bin/**/!(*.*|gitignore)\"",
"clean-tests": "node bin/clean_test.js"
},
"author": "Apache Software Foundation",
"license": "Apache-2.0",
@@ -28,12 +28,9 @@
"android-versions": "^1.5.0",
"cordova-common": "^4.0.1",
"execa": "^4.0.2",
"fast-glob": "^3.2.4",
"fs-extra": "^9.0.1",
"is-path-inside": "^3.0.2",
"nopt": "^4.0.3",
"properties-parser": "^0.3.1",
"semver": "^7.3.4",
"which": "^2.0.2"
},
"devDependencies": {
@@ -44,8 +41,9 @@
"rewire": "^5.0.0"
},
"engines": {
"node": ">=10.10.0"
"node": ">=10.0.0"
},
"engineStrict": true,
"nyc": {
"include": [
"bin/lib/**",

View File

@@ -0,0 +1,149 @@
/*
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 PluginInfoProvider = require('cordova-common').PluginInfoProvider;
const fs = require('fs-extra');
var cp = require('child_process');
var path = require('path');
var util = require('util');
var cordova_bin = path.join(__dirname, '../../../bin');
/**
* Creates a project using platform create script with given parameters
* @param {string} projectname - name of the project
* @param {string} projectid - id of the project
* @param {string} platformpath - path to the platform
* @param {function} callback - function which is called (without arguments) when the project is created or (with error object) when error occurs
*/
module.exports.createProject = function (projectname, projectid, platformpath, callback) {
// platformpath is optional
if (!callback && typeof platformpath === 'function') {
callback = platformpath;
platformpath = null;
}
var projectDirName = getDirName(projectid);
var createScriptPath = platformpath ? path.join(platformpath, 'bin/create') : path.join(cordova_bin, 'create');
// remove existing folder
module.exports.removeProject(projectid);
// create the project
var command = util.format('"%s" %s %s "%s"', createScriptPath, projectDirName, projectid, projectname);
cp.exec(command, function (error, stdout, stderr) {
if (error) {
console.log(stdout);
console.error(stderr);
}
callback(error);
});
};
/**
* Updates a project using platform update script with given parameters
* @param {string} projectid - id of the project
* @param {string} platformpath - path to the platform
* @param {function} callback - function which is called (without arguments) when the project is updated or (with error object) when error occurs
*/
module.exports.updateProject = function (projectid, platformpath, callback) {
// platformpath is optional
if (!callback && typeof platformpath === 'function') {
callback = platformpath;
platformpath = null;
}
var projectDirName = getDirName(projectid);
var updateScriptPath = platformpath ? path.join(platformpath, 'bin/update') : path.join(cordova_bin, 'update');
var command = util.format('"%s" %s', updateScriptPath, projectDirName);
cp.exec(command, function (error, stdout, stderr) {
if (error) {
console.log(stdout);
console.error(stderr);
}
callback(error);
});
};
/**
* Builds a project using platform build script with given parameters
* @param {string} projectid - id of the project
* @param {function} callback - function which is called (without arguments) when the project is built or (with error object) when error occurs
*/
module.exports.buildProject = function (projectid, callback) {
var projectDirName = getDirName(projectid);
var command = path.join(projectDirName, 'cordova/build');
cp.exec(command, function (error, stdout, stderr) {
if (error) {
console.log(stdout);
console.error(stderr);
}
callback(error);
});
};
/**
* Removes a project
* @param {string} projectid - id of the project
*/
module.exports.removeProject = function (projectid) {
var projectDirName = getDirName(projectid);
fs.removeSync(projectDirName);
};
/**
* Add a plugin to a project using platform api
* @param {string} projectid - id of the project
* @param {string} plugindir - path to a plugin
* @param {function} callback - function which is called (without arguments) when the plugin is added or (with error object) when error occurs
*/
module.exports.addPlugin = function (projectid, plugindir, callback) {
var projectDirName = getDirName(projectid);
var pip = new PluginInfoProvider();
var pluginInfo = pip.get(plugindir);
var Api = require(path.join(__dirname, '../../..', projectDirName, 'cordova', 'Api.js'));
var api = new Api('android', projectDirName);
api.addPlugin(pluginInfo).then(function () {
callback(null);
}, function (error) {
console.error(error);
callback(error);
});
};
/**
* Gets a version number from project using platform script
* @param {string} projectid - id of the project
* @param {function} callback - function which is called with platform version as an argument
*/
module.exports.getPlatformVersion = function (projectid, callback) {
var command = path.join(getDirName(projectid), 'cordova/version');
cp.exec(command, function (error, stdout, stderr) {
if (error) {
console.log(stdout);
console.error(stderr);
}
callback(stdout.trim());
});
};
function getDirName (projectid) {
return 'test-' + projectid;
}

View File

@@ -26,6 +26,6 @@ jasmine.getEnv().addReporter(new SpecReporter({
},
summary: {
displayDuration: true,
displayStacktrace: 'raw'
displayStacktrace: true
}
}));

View File

@@ -20,15 +20,13 @@
const CordovaError = require('cordova-common').CordovaError;
const rewire = require('rewire');
const adbOutput = `List of devices attached
emulator-5554\tdevice
emulator-5556\toffline
123a76565509e124\tdevice
123a76565509e123\tbootloader
`;
describe('Adb', () => {
const deviceId = '123a76565509e124';
const adbOutput = `List of devices attached
emulator-5554\tdevice
123a76565509e124\tdevice`;
const [, emulatorLine, deviceLine] = adbOutput.split('\n');
const emulatorId = emulatorLine.split('\t')[0];
const deviceId = deviceLine.split('\t')[0];
const alreadyExistsError = 'adb: failed to install app.apk: Failure[INSTALL_FAILED_ALREADY_EXISTS]';
const certificateError = 'adb: failed to install app.apk: Failure[INSTALL_PARSE_FAILED_NO_CERTIFICATES]';
@@ -43,15 +41,40 @@ describe('Adb', () => {
Adb.__set__('execa', execaSpy);
});
describe('devices', () => {
it('should return the IDs of all fully booted devices & emulators', () => {
execaSpy.and.resolveTo({ stdout: adbOutput });
describe('isDevice', () => {
it('should return true for a real device', () => {
const isDevice = Adb.__get__('isDevice');
expect(isDevice(deviceLine)).toBeTruthy();
expect(isDevice(emulatorLine)).toBeFalsy();
});
});
describe('isEmulator', () => {
it('should return true for an emulator', () => {
const isEmulator = Adb.__get__('isEmulator');
expect(isEmulator(emulatorLine)).toBeTruthy();
expect(isEmulator(deviceLine)).toBeFalsy();
});
});
describe('devices', () => {
beforeEach(() => {
execaSpy.and.returnValue(Promise.resolve({ stdout: adbOutput }));
});
it('should return only devices if no options are specified', () => {
return Adb.devices().then(devices => {
expect(devices).toEqual([
'emulator-5554',
'123a76565509e124'
]);
expect(devices.length).toBe(1);
expect(devices[0]).toBe(deviceId);
});
});
it('should return only emulators if opts.emulators is true', () => {
return Adb.devices({ emulators: true }).then(devices => {
expect(devices.length).toBe(1);
expect(devices[0]).toBe(emulatorId);
});
});
});

View File

@@ -20,44 +20,51 @@
var os = require('os');
var path = require('path');
var common = require('cordova-common');
const EventEmitter = require('events');
var rewire = require('rewire');
var Api = require('../../bin/templates/cordova/Api');
var AndroidProject = require('../../bin/templates/cordova/lib/AndroidProject');
var builders = require('../../bin/templates/cordova/lib/builders/builders');
var PluginInfo = common.PluginInfo;
var FIXTURES = path.join(__dirname, '../e2e/fixtures');
var FAKE_PROJECT_DIR = path.join(os.tmpdir(), 'plugin-test-project');
describe('Api', () => {
describe('addPlugin method', function () {
var api;
describe('addPlugin method', function () {
var api, Api, gradleBuilder;
beforeEach(function () {
var pluginManager = jasmine.createSpyObj('pluginManager', ['addPlugin']);
pluginManager.addPlugin.and.resolveTo();
spyOn(common.PluginManager, 'get').and.returnValue(pluginManager);
beforeEach(function () {
Api = rewire('../../bin/templates/cordova/Api');
var projectSpy = jasmine.createSpyObj('AndroidProject', ['getPackageName', 'write', 'isClean']);
spyOn(AndroidProject, 'getProjectFile').and.returnValue(projectSpy);
var pluginManager = jasmine.createSpyObj('pluginManager', ['addPlugin']);
pluginManager.addPlugin.and.resolveTo();
spyOn(common.PluginManager, 'get').and.returnValue(pluginManager);
api = new Api('android', FAKE_PROJECT_DIR, new EventEmitter());
spyOn(api._builder, 'prepBuildFiles');
var projectSpy = jasmine.createSpyObj('AndroidProject', ['getPackageName', 'write', 'isClean']);
spyOn(AndroidProject, 'getProjectFile').and.returnValue(projectSpy);
Api.__set__('Api.prototype.clean', async () => {});
// Prevent logging to avoid polluting the test reports
Api.__set__('selfEvents.emit', jasmine.createSpy());
api = new Api('android', FAKE_PROJECT_DIR);
gradleBuilder = jasmine.createSpyObj('gradleBuilder', ['prepBuildFiles']);
spyOn(builders, 'getBuilder').and.returnValue(gradleBuilder);
});
const getPluginFixture = name => new PluginInfo(path.join(FIXTURES, name));
it('Test#001 : should call gradleBuilder.prepBuildFiles for every plugin with frameworks', () => {
return api.addPlugin(getPluginFixture('cordova-plugin-fake')).then(() => {
expect(gradleBuilder.prepBuildFiles).toHaveBeenCalled();
});
});
const getPluginFixture = name => new PluginInfo(path.join(FIXTURES, name));
it('Test#001 : should call gradleBuilder.prepBuildFiles for every plugin with frameworks', () => {
return api.addPlugin(getPluginFixture('cordova-plugin-fake')).then(() => {
expect(api._builder.prepBuildFiles).toHaveBeenCalled();
});
});
it('Test#002 : shouldn\'t trigger gradleBuilder.prepBuildFiles for plugins without android frameworks', () => {
return api.addPlugin(getPluginFixture('cordova-plugin-fake-ios-frameworks')).then(() => {
expect(api._builder.prepBuildFiles).not.toHaveBeenCalled();
});
it('Test#002 : shouldn\'t trigger gradleBuilder.prepBuildFiles for plugins without android frameworks', () => {
return api.addPlugin(getPluginFixture('cordova-plugin-fake-ios-frameworks')).then(() => {
expect(gradleBuilder.prepBuildFiles).not.toHaveBeenCalled();
});
});
});

View File

@@ -289,7 +289,7 @@ describe('ProjectBuilder', () => {
});
});
describe('outputFileComparator', () => {
describe('fileSorter', () => {
it('should sort APKs from most recent to oldest, deprioritising unsigned arch-specific builds', () => {
const APKs = {
'app-debug.apk': new Date('2018-04-20'),
@@ -309,7 +309,7 @@ describe('ProjectBuilder', () => {
});
const apkArray = Object.keys(APKs);
const sortedApks = apkArray.sort(ProjectBuilder.__get__('outputFileComparator'));
const sortedApks = apkArray.sort(ProjectBuilder.__get__('fileSorter'));
expect(sortedApks).toEqual(expectedResult);
});

View File

@@ -18,54 +18,26 @@
*/
var rewire = require('rewire');
var check_reqs = rewire('../../bin/templates/cordova/lib/check_reqs');
var android_sdk = require('../../bin/templates/cordova/lib/android_sdk');
var fs = require('fs-extra');
var path = require('path');
var events = require('cordova-common').events;
var which = require('which');
const { CordovaError } = require('cordova-common');
// This should match /bin/templates/project/build.gradle
const DEFAULT_TARGET_API = 29;
describe('check_reqs', function () {
let check_reqs;
beforeEach(() => {
check_reqs = rewire('../../bin/templates/cordova/lib/check_reqs');
});
var original_env;
beforeAll(function () {
original_env = Object.assign({}, process.env);
original_env = Object.create(process.env);
});
afterEach(function () {
// process.env has some special behavior, so we do not
// replace it but only restore its original properties
Object.keys(process.env).forEach(k => {
delete process.env[k];
});
Object.assign(process.env, original_env);
});
describe('check_java', () => {
it('detects if unexpected JDK version is installed', async () => {
check_reqs.__set__({
EXPECTED_JAVA_VERSION: '9999.9999.9999',
java: { getVersion: async () => ({ version: '1.8.0' }) }
});
await expectAsync(check_reqs.check_java()).toBeRejectedWithError(CordovaError, /Requirements check failed for JDK 9999.9999.9999! Detected version: 1.8.0/);
});
it('should return the version', async () => {
check_reqs.__set__({
java: { getVersion: async () => ({ version: '1.8.0' }) }
});
await expectAsync(check_reqs.check_java()).toBeResolvedTo({ version: '1.8.0' });
Object.keys(original_env).forEach(function (k) {
process.env[k] = original_env[k];
});
});
describe('check_android', function () {
describe('find and set ANDROID_HOME when ANDROID_HOME and ANDROID_SDK_ROOT is not set', function () {
beforeEach(function () {
@@ -83,6 +55,9 @@ describe('check_reqs', function () {
process.env.ProgramFiles = 'windows-program-files';
return check_reqs.check_android().then(function () {
expect(process.env.ANDROID_SDK_ROOT).toContain('windows-local-app-data');
}).finally(function () {
delete process.env.LOCALAPPDATA;
delete process.env.ProgramFiles;
});
});
it('it should set ANDROID_SDK_ROOT on Darwin', () => {
@@ -91,6 +66,8 @@ describe('check_reqs', function () {
process.env.HOME = 'home is where the heart is';
return check_reqs.check_android().then(function () {
expect(process.env.ANDROID_SDK_ROOT).toContain('home is where the heart is');
}).finally(function () {
delete process.env.HOME;
});
});
});
@@ -243,6 +220,9 @@ describe('check_reqs', function () {
process.env.ANDROID_SDK_ROOT = 'let the children play';
spyOn(fs, 'existsSync').and.returnValue(true);
});
afterEach(function () {
delete process.env.ANDROID_SDK_ROOT;
});
it('should add tools/bin,tools,platform-tools to PATH if `avdmanager`,`android`,`adb` is not found', () => {
return check_reqs.check_android().then(function () {
expect(process.env.PATH).toContain('let the children play' + path.sep + 'tools');
@@ -263,20 +243,20 @@ describe('check_reqs', function () {
});
});
it('with ANDROID_SDK_ROOT / without ANDROID_HOME', async () => {
it('with ANDROID_SDK_ROOT / without ANDROID_HOME', () => {
process.env.ANDROID_SDK_ROOT = '/android/sdk/root';
await expectAsync(check_reqs.check_gradle()).toBeResolvedTo('/android/sdk/root/bin/gradle');
expectAsync(check_reqs.check_gradle()).toBeResolvedTo('/android/sdk/root/bin/gradle');
});
it('with ANDROID_SDK_ROOT / with ANDROID_HOME', async () => {
it('with ANDROID_SDK_ROOT / with ANDROID_HOME', () => {
process.env.ANDROID_SDK_ROOT = '/android/sdk/root';
process.env.ANDROID_HOME = '/android/sdk/home';
await expectAsync(check_reqs.check_gradle()).toBeResolvedTo('/android/sdk/root/bin/gradle');
expectAsync(check_reqs.check_gradle()).toBeResolvedTo('/android/sdk/root/bin/gradle');
});
it('without ANDROID_SDK_ROOT / with ANDROID_HOME', async () => {
it('without ANDROID_SDK_ROOT / with ANDROID_HOME', () => {
process.env.ANDROID_HOME = '/android/sdk/home';
await expectAsync(check_reqs.check_gradle()).toBeResolvedTo('/android/sdk/home/bin/gradle');
expectAsync(check_reqs.check_gradle()).toBeResolvedTo('/android/sdk/home/bin/gradle');
});
it('without ANDROID_SDK_ROOT / without ANDROID_HOME', () => {
@@ -368,8 +348,6 @@ describe('check_reqs', function () {
}
});
spyOn(events, 'emit');
getPreferenceSpy.and.returnValue(DEFAULT_TARGET_API - 1);
var target = check_reqs.get_target();

View File

@@ -18,7 +18,7 @@
*/
var rewire = require('rewire');
var utils = require('../../bin/templates/cordova/lib/utils');
var utils = require('../../bin/lib/utils');
var create = rewire('../../bin/lib/create');
var check_reqs = require('../../bin/templates/cordova/lib/check_reqs');
var fs = require('fs-extra');
@@ -132,7 +132,6 @@ describe('create', function () {
spyOn(create, 'copyBuildRules');
spyOn(create, 'writeProjectProperties');
spyOn(create, 'prepBuildFiles');
spyOn(create, 'writeNameForAndroidStudio');
revert_manifest_mock = create.__set__('AndroidManifest', Manifest_mock);
spyOn(fs, 'existsSync').and.returnValue(false);
spyOn(fs, 'copySync');
@@ -301,24 +300,4 @@ describe('create', function () {
});
});
});
describe('writeNameForAndroidStudio', () => {
const project_path = path.join('some', 'path');
const appName = 'Test Cordova';
beforeEach(function () {
spyOn(fs, 'ensureDirSync');
spyOn(fs, 'writeFileSync');
});
it('should call ensureDirSync with path', () => {
create.writeNameForAndroidStudio(project_path, appName);
expect(fs.ensureDirSync).toHaveBeenCalledWith(path.join(project_path, '.idea'));
});
it('should call writeFileSync with content', () => {
create.writeNameForAndroidStudio(project_path, appName);
expect(fs.writeFileSync).toHaveBeenCalledWith(path.join(project_path, '.idea', '.name'), appName);
});
});
});

233
spec/unit/device.spec.js Normal file
View File

@@ -0,0 +1,233 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
const rewire = require('rewire');
const CordovaError = require('cordova-common').CordovaError;
describe('device', () => {
const DEVICE_LIST = ['device1', 'device2', 'device3'];
let AdbSpy;
let device;
beforeEach(() => {
device = rewire('../../bin/templates/cordova/lib/device');
AdbSpy = jasmine.createSpyObj('Adb', ['devices', 'install', 'shell', 'start', 'uninstall']);
device.__set__('Adb', AdbSpy);
});
describe('list', () => {
it('should return the list from adb devices', () => {
AdbSpy.devices.and.returnValue(Promise.resolve(DEVICE_LIST));
return device.list().then(list => {
expect(list).toEqual(DEVICE_LIST);
});
});
it('should kill adb and try to get devices again if none are found the first time, and `lookHarder` is set', () => {
const execaSpy = jasmine.createSpy('execa').and.returnValue(Promise.resolve());
device.__set__('execa', execaSpy);
AdbSpy.devices.and.returnValues(Promise.resolve([]), Promise.resolve(DEVICE_LIST));
return device.list(true).then(list => {
expect(execaSpy).toHaveBeenCalledWith('killall', ['adb']);
expect(list).toBe(DEVICE_LIST);
expect(AdbSpy.devices).toHaveBeenCalledTimes(2);
});
});
it('should return the empty list if killing adb fails', () => {
const emptyDevices = [];
const execaSpy = jasmine.createSpy('execa').and.returnValue(Promise.reject());
device.__set__('execa', execaSpy);
AdbSpy.devices.and.returnValues(Promise.resolve(emptyDevices));
return device.list(true).then(list => {
expect(execaSpy).toHaveBeenCalledWith('killall', ['adb']);
expect(list).toBe(emptyDevices);
expect(AdbSpy.devices).toHaveBeenCalledTimes(1);
});
});
});
describe('resolveTarget', () => {
let buildSpy;
beforeEach(() => {
buildSpy = jasmine.createSpyObj('build', ['detectArchitecture']);
buildSpy.detectArchitecture.and.returnValue(Promise.resolve());
device.__set__('build', buildSpy);
spyOn(device, 'list').and.returnValue(Promise.resolve(DEVICE_LIST));
});
it('should select the first device to be the target if none is specified', () => {
return device.resolveTarget().then(deviceInfo => {
expect(deviceInfo.target).toBe(DEVICE_LIST[0]);
});
});
it('should use the given target instead of the default', () => {
return device.resolveTarget(DEVICE_LIST[2]).then(deviceInfo => {
expect(deviceInfo.target).toBe(DEVICE_LIST[2]);
});
});
it('should set emulator to false', () => {
return device.resolveTarget().then(deviceInfo => {
expect(deviceInfo.isEmulator).toBe(false);
});
});
it('should throw an error if there are no devices', () => {
device.list.and.returnValue(Promise.resolve([]));
return device.resolveTarget().then(
() => fail('Unexpectedly resolved'),
err => {
expect(err).toEqual(jasmine.any(CordovaError));
}
);
});
it('should throw an error if the specified target does not exist', () => {
return device.resolveTarget('nonexistent-target').then(
() => fail('Unexpectedly resolved'),
err => {
expect(err).toEqual(jasmine.any(CordovaError));
}
);
});
it('should detect the architecture and return it with the device info', () => {
const target = DEVICE_LIST[1];
const arch = 'unittestarch';
buildSpy.detectArchitecture.and.returnValue(Promise.resolve(arch));
return device.resolveTarget(target).then(deviceInfo => {
expect(buildSpy.detectArchitecture).toHaveBeenCalledWith(target);
expect(deviceInfo.arch).toBe(arch);
});
});
});
describe('install', () => {
let AndroidManifestSpy;
let AndroidManifestFns;
let AndroidManifestGetActivitySpy;
let buildSpy;
let target;
beforeEach(() => {
target = { target: DEVICE_LIST[0], arch: 'arm7', isEmulator: false };
buildSpy = jasmine.createSpyObj('build', ['findBestApkForArchitecture']);
device.__set__('build', buildSpy);
AndroidManifestFns = jasmine.createSpyObj('AndroidManifestFns', ['getPackageId', 'getActivity']);
AndroidManifestGetActivitySpy = jasmine.createSpyObj('getActivity', ['getName']);
AndroidManifestFns.getActivity.and.returnValue(AndroidManifestGetActivitySpy);
AndroidManifestSpy = jasmine.createSpy('AndroidManifest').and.returnValue(AndroidManifestFns);
device.__set__('AndroidManifest', AndroidManifestSpy);
AdbSpy.install.and.returnValue(Promise.resolve());
AdbSpy.shell.and.returnValue(Promise.resolve());
AdbSpy.start.and.returnValue(Promise.resolve());
});
it('should get the full target object if only id is specified', () => {
const targetId = DEVICE_LIST[0];
spyOn(device, 'resolveTarget').and.returnValue(Promise.resolve(target));
return device.install(targetId).then(() => {
expect(device.resolveTarget).toHaveBeenCalledWith(targetId);
});
});
it('should install to the passed target', () => {
return device.install(target).then(() => {
expect(AdbSpy.install).toHaveBeenCalledWith(target.target, undefined, jasmine.anything());
});
});
it('should install the correct apk based on the architecture and build results', () => {
const buildResults = {
apkPaths: 'path/to/apks',
buildType: 'debug',
buildMethod: 'foo'
};
const apkPath = 'my/apk/path/app.apk';
buildSpy.findBestApkForArchitecture.and.returnValue(apkPath);
return device.install(target, buildResults).then(() => {
expect(buildSpy.findBestApkForArchitecture).toHaveBeenCalledWith(buildResults, target.arch);
expect(AdbSpy.install).toHaveBeenCalledWith(jasmine.anything(), apkPath, jasmine.anything());
});
});
it('should uninstall and reinstall app if failure is due to different certificates', () => {
AdbSpy.install.and.returnValues(
Promise.reject('Failed to install: INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES'),
Promise.resolve()
);
AdbSpy.uninstall.and.callFake(() => {
expect(AdbSpy.install).toHaveBeenCalledTimes(1);
return Promise.resolve();
});
return device.install(target).then(() => {
expect(AdbSpy.install).toHaveBeenCalledTimes(2);
expect(AdbSpy.uninstall).toHaveBeenCalled();
});
});
it('should throw any error not caused by different certificates', () => {
const errorMsg = new CordovaError('Failed to install');
AdbSpy.install.and.returnValues(Promise.reject(errorMsg));
return device.install(target).then(
() => fail('Unexpectedly resolved'),
err => {
expect(err).toBe(errorMsg);
}
);
});
it('should unlock the screen on device', () => {
return device.install(target).then(() => {
expect(AdbSpy.shell).toHaveBeenCalledWith(target.target, 'input keyevent 82');
});
});
it('should start the newly installed app on the device', () => {
const packageId = 'unittestapp';
const activityName = 'TestActivity';
AndroidManifestFns.getPackageId.and.returnValue(packageId);
AndroidManifestGetActivitySpy.getName.and.returnValue(activityName);
return device.install(target).then(() => {
expect(AdbSpy.start).toHaveBeenCalledWith(target.target, `${packageId}/.${activityName}`);
});
});
});
});

View File

@@ -26,6 +26,7 @@ const CordovaError = require('cordova-common').CordovaError;
const check_reqs = require('../../bin/templates/cordova/lib/check_reqs');
describe('emulator', () => {
const EMULATOR_LIST = ['emulator-5555', 'emulator-5556', 'emulator-5557'];
let emu;
beforeEach(() => {
@@ -204,13 +205,13 @@ describe('emulator', () => {
});
describe('list_started', () => {
it('should return a list of all online emulators', () => {
it('should call adb devices with the emulators flag', () => {
const AdbSpy = jasmine.createSpyObj('Adb', ['devices']);
AdbSpy.devices.and.resolveTo(['emulator-5556', '123a76565509e124']);
AdbSpy.devices.and.returnValue(Promise.resolve());
emu.__set__('Adb', AdbSpy);
return emu.list_started().then(emus => {
expect(emus).toEqual(['emulator-5556']);
return emu.list_started().then(() => {
expect(AdbSpy.devices).toHaveBeenCalledWith({ emulators: true });
});
});
});
@@ -528,4 +529,158 @@ describe('emulator', () => {
});
});
});
describe('resolveTarget', () => {
const arch = 'arm7-test';
beforeEach(() => {
const buildSpy = jasmine.createSpyObj('build', ['detectArchitecture']);
buildSpy.detectArchitecture.and.returnValue(Promise.resolve(arch));
emu.__set__('build', buildSpy);
spyOn(emu, 'list_started').and.returnValue(Promise.resolve(EMULATOR_LIST));
});
it('should throw an error if there are no running emulators', () => {
emu.list_started.and.returnValue(Promise.resolve([]));
return emu.resolveTarget().then(
() => fail('Unexpectedly resolved'),
err => {
expect(err).toEqual(jasmine.any(CordovaError));
}
);
});
it('should throw an error if the requested emulator is not running', () => {
const targetEmulator = 'unstarted-emu';
return emu.resolveTarget(targetEmulator).then(
() => fail('Unexpectedly resolved'),
err => {
expect(err.message).toContain(targetEmulator);
}
);
});
it('should return info on the first running emulator if none is specified', () => {
return emu.resolveTarget().then(emulatorInfo => {
expect(emulatorInfo.target).toBe(EMULATOR_LIST[0]);
});
});
it('should return the emulator info', () => {
return emu.resolveTarget(EMULATOR_LIST[1]).then(emulatorInfo => {
expect(emulatorInfo).toEqual({ target: EMULATOR_LIST[1], arch, isEmulator: true });
});
});
});
describe('install', () => {
let AndroidManifestSpy;
let AndroidManifestFns;
let AndroidManifestGetActivitySpy;
let AdbSpy;
let buildSpy;
let execaSpy;
let target;
beforeEach(() => {
target = { target: EMULATOR_LIST[1], arch: 'arm7', isEmulator: true };
buildSpy = jasmine.createSpyObj('build', ['findBestApkForArchitecture']);
emu.__set__('build', buildSpy);
AndroidManifestFns = jasmine.createSpyObj('AndroidManifestFns', ['getPackageId', 'getActivity']);
AndroidManifestGetActivitySpy = jasmine.createSpyObj('getActivity', ['getName']);
AndroidManifestFns.getActivity.and.returnValue(AndroidManifestGetActivitySpy);
AndroidManifestSpy = jasmine.createSpy('AndroidManifest').and.returnValue(AndroidManifestFns);
emu.__set__('AndroidManifest', AndroidManifestSpy);
AdbSpy = jasmine.createSpyObj('Adb', ['shell', 'start', 'uninstall']);
AdbSpy.shell.and.returnValue(Promise.resolve());
AdbSpy.start.and.returnValue(Promise.resolve());
AdbSpy.uninstall.and.returnValue(Promise.resolve());
emu.__set__('Adb', AdbSpy);
execaSpy = jasmine.createSpy('execa').and.resolveTo({});
emu.__set__('execa', execaSpy);
});
it('should get the full target object if only id is specified', () => {
const targetId = target.target;
spyOn(emu, 'resolveTarget').and.returnValue(Promise.resolve(target));
return emu.install(targetId, {}).then(() => {
expect(emu.resolveTarget).toHaveBeenCalledWith(targetId);
});
});
it('should install to the passed target', () => {
return emu.install(target, {}).then(() => {
const execCmd = execaSpy.calls.argsFor(0)[1].join(' ');
expect(execCmd).toContain(`-s ${target.target} install`);
});
});
it('should install the correct apk based on the architecture and build results', () => {
const buildResults = {
apkPaths: 'path/to/apks',
buildType: 'debug',
buildMethod: 'foo'
};
const apkPath = 'my/apk/path/app.apk';
buildSpy.findBestApkForArchitecture.and.returnValue(apkPath);
return emu.install(target, buildResults).then(() => {
expect(buildSpy.findBestApkForArchitecture).toHaveBeenCalledWith(buildResults, target.arch);
const execCmd = execaSpy.calls.argsFor(0)[1].join(' ');
expect(execCmd).toContain(`install -r ${apkPath}`);
});
});
it('should uninstall and reinstall app if failure is due to different certificates', () => {
execaSpy.and.returnValues(
...['Failure: INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES', '']
.map(out => Promise.resolve({ stdout: out }))
);
return emu.install(target, {}).then(() => {
expect(execaSpy).toHaveBeenCalledTimes(2);
expect(AdbSpy.uninstall).toHaveBeenCalled();
});
});
it('should throw any error not caused by different certificates', () => {
const errorMsg = 'Failure: Failed to install';
execaSpy.and.resolveTo({ stdout: errorMsg });
return emu.install(target, {}).then(
() => fail('Unexpectedly resolved'),
err => {
expect(err).toEqual(jasmine.any(CordovaError));
expect(err.message).toContain(errorMsg);
}
);
});
it('should unlock the screen on device', () => {
return emu.install(target, {}).then(() => {
expect(AdbSpy.shell).toHaveBeenCalledWith(target.target, 'input keyevent 82');
});
});
it('should start the newly installed app on the device', () => {
const packageId = 'unittestapp';
const activityName = 'TestActivity';
AndroidManifestFns.getPackageId.and.returnValue(packageId);
AndroidManifestGetActivitySpy.getName.and.returnValue(activityName);
return emu.install(target, {}).then(() => {
expect(AdbSpy.start).toHaveBeenCalledWith(target.target, `${packageId}/.${activityName}`);
});
});
});
});

View File

@@ -1,177 +0,0 @@
/**
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
const path = require('path');
const rewire = require('rewire');
const { CordovaError } = require('cordova-common');
const utils = require('../../bin/templates/cordova/lib/utils');
const glob = require('fast-glob');
describe('Java', () => {
const Java = rewire('../../bin/templates/cordova/lib/env/java');
describe('getVersion', () => {
let originalEnsureFunc = null;
beforeEach(() => {
/*
This is to avoid changing/polluting the
process environment variables
as a result of running these java tests; which could produce
unexpected side effects to other tests.
*/
originalEnsureFunc = Java._ensure;
spyOn(Java, '_ensure').and.callFake(() => {
return originalEnsureFunc({});
});
});
it('runs', async () => {
Java.__set__('execa', () => Promise.resolve({
all: 'javac 1.8.0_275'
}));
console.log('BEFORE', process.env.JAVA_HOME);
const result = await Java.getVersion();
console.log('AFTER', process.env.JAVA_HOME);
expect(result.major).toBe(1);
expect(result.minor).toBe(8);
expect(result.patch).toBe(0);
expect(result.version).toBe('1.8.0');
});
it('produces a CordovaError on error', async () => {
Java.__set__('execa', () => Promise.reject({
shortMessage: 'test error'
}));
const emitSpy = jasmine.createSpy('events.emit');
Java.__set__('events', {
emit: emitSpy
});
await expectAsync(Java.getVersion())
.toBeRejectedWithError(CordovaError, /Failed to run "javac -version"/);
expect(emitSpy).toHaveBeenCalledWith('verbose', 'test error');
});
});
describe('_ensure', () => {
beforeEach(() => {
Java.__set__('javaIsEnsured', false);
});
it('with JAVA_HOME / without javac', async () => {
spyOn(utils, 'forgivingWhichSync').and.returnValue('');
const env = {
JAVA_HOME: '/tmp/jdk'
};
await Java._ensure(env);
expect(env.PATH.split(path.delimiter))
.toContain(path.join(env.JAVA_HOME, 'bin'));
});
it('detects JDK in default location on windows', async () => {
spyOn(utils, 'forgivingWhichSync').and.returnValue('');
spyOn(utils, 'isWindows').and.returnValue(true);
const root = 'C:\\Program Files';
const env = {
ProgramFiles: root
};
spyOn(glob, 'sync').and.returnValue(`${root}\\java\\jdk1.8.0_275`);
const jdkDir = `${root}\\java\\jdk1.8.0_275`;
await Java._ensure(env);
expect(env.JAVA_HOME).withContext('JAVA_HOME').toBe(jdkDir);
expect(env.PATH).toContain(jdkDir);
});
it('detects JDK in default location on windows (x86)', async () => {
spyOn(utils, 'forgivingWhichSync').and.returnValue('');
spyOn(utils, 'isWindows').and.returnValue(true);
const root = 'C:\\Program Files (x86)';
const env = {
'ProgramFiles(x86)': root
};
spyOn(glob, 'sync').and.returnValue(`${root}\\java\\jdk1.8.0_275`);
const jdkDir = `${root}\\java\\jdk1.8.0_275`;
await Java._ensure(env);
expect(env.JAVA_HOME).withContext('JAVA_HOME').toBe(jdkDir);
expect(env.PATH).toContain(jdkDir);
});
it('without JAVA_HOME / with javac - Mac OS X - success', async () => {
spyOn(utils, 'forgivingWhichSync').and.returnValue('/tmp/jdk/bin');
const fsSpy = jasmine.createSpy('fs').and.returnValue(true);
Java.__set__('fs', {
existsSync: fsSpy
});
Java.__set__('execa', async () => ({ stdout: '/tmp/jdk' }));
const env = {};
await Java._ensure(env);
expect(fsSpy).toHaveBeenCalledWith('/usr/libexec/java_home');
expect(env.JAVA_HOME).toBe('/tmp/jdk');
});
it('without JAVA_HOME / with javac - Mac OS X - error', async () => {
spyOn(utils, 'forgivingWhichSync').and.returnValue('/tmp/jdk/bin');
Java.__set__('fs', { existsSync: () => true });
Java.__set__('execa', jasmine.createSpy('execa').and.returnValue(Promise.reject({
shortMessage: 'test error'
})));
const emitSpy = jasmine.createSpy('events.emit');
Java.__set__('events', {
emit: emitSpy
});
await expectAsync(Java._ensure({}))
.toBeRejectedWithError(CordovaError, /Failed to find 'JAVA_HOME' environment variable/);
expect(emitSpy).toHaveBeenCalledWith('verbose', 'test error');
});
it('derive from javac location - success', async () => {
spyOn(utils, 'forgivingWhichSync').and.returnValue('/tmp/jdk/bin');
Java.__set__('fs', { existsSync: path => !/java_home$/.test(path) });
const env = {};
await Java._ensure(env);
expect(env.JAVA_HOME).toBe('/tmp');
});
it('derive from javac location - error', async () => {
spyOn(utils, 'forgivingWhichSync').and.returnValue('/tmp/jdk/bin');
Java.__set__('fs', { existsSync: () => false });
await expectAsync(Java._ensure({})).toBeRejectedWithError(CordovaError, /Failed to find 'JAVA_HOME' environment variable/);
});
});
});

View File

@@ -21,7 +21,6 @@ var common = rewire('../../../bin/templates/cordova/lib/pluginHandlers');
var path = require('path');
var fs = require('fs-extra');
var osenv = require('os');
var test_dir = path.join(osenv.tmpdir(), 'test_plugman');
var project_dir = path.join(test_dir, 'project');
var src = path.join(project_dir, 'src');
@@ -36,27 +35,25 @@ var deleteJava = common.__get__('deleteJava');
var copyNewFile = common.__get__('copyNewFile');
describe('common platform handler', function () {
afterEach(() => {
fs.removeSync(test_dir);
fs.removeSync(non_plugin_file);
});
describe('copyFile', function () {
it('Test#001 : should throw if source path not found', function () {
fs.removeSync(src);
expect(function () { copyFile(test_dir, src, project_dir, dest); })
.toThrow(new Error('"' + src + '" not found!'));
});
it('Test#002 : should throw if src not in plugin directory', function () {
fs.ensureDirSync(project_dir);
fs.outputFileSync(non_plugin_file, 'contents');
fs.writeFileSync(non_plugin_file, 'contents', 'utf-8');
var outside_file = '../non_plugin_file';
expect(function () { copyFile(test_dir, outside_file, project_dir, dest); })
.toThrow(new Error('File "' + path.resolve(test_dir, outside_file) + '" is located outside the plugin directory "' + test_dir + '"'));
fs.removeSync(test_dir);
});
it('Test#003 : should allow symlink src, if inside plugin', function () {
fs.outputFileSync(java_file, 'contents');
fs.ensureDirSync(java_dir);
fs.writeFileSync(java_file, 'contents', 'utf-8');
// This will fail on windows if not admin - ignore the error in that case.
if (ignoreEPERMonWin32(java_file, symlink_file)) {
@@ -64,11 +61,12 @@ describe('common platform handler', function () {
}
copyFile(test_dir, symlink_file, project_dir, dest);
fs.removeSync(project_dir);
});
it('Test#004 : should throw if symlink is linked to a file outside the plugin', function () {
fs.ensureDirSync(java_dir);
fs.outputFileSync(non_plugin_file, 'contents');
fs.writeFileSync(non_plugin_file, 'contents', 'utf-8');
// This will fail on windows if not admin - ignore the error in that case.
if (ignoreEPERMonWin32(non_plugin_file, symlink_file)) {
@@ -77,16 +75,20 @@ describe('common platform handler', function () {
expect(function () { copyFile(test_dir, symlink_file, project_dir, dest); })
.toThrow(new Error('File "' + path.resolve(test_dir, symlink_file) + '" is located outside the plugin directory "' + test_dir + '"'));
fs.removeSync(project_dir);
});
it('Test#005 : should throw if dest is outside the project directory', function () {
fs.outputFileSync(java_file, 'contents');
fs.ensureDirSync(java_dir);
fs.writeFileSync(java_file, 'contents', 'utf-8');
expect(function () { copyFile(test_dir, java_file, project_dir, non_plugin_file); })
.toThrow(new Error('Destination "' + path.resolve(project_dir, non_plugin_file) + '" for source file "' + path.resolve(test_dir, java_file) + '" is located outside the project'));
fs.removeSync(project_dir);
});
it('Test#006 : should call mkdir -p on target path', function () {
fs.outputFileSync(java_file, 'contents');
fs.ensureDirSync(java_dir);
fs.writeFileSync(java_file, 'contents', 'utf-8');
var s = spyOn(fs, 'ensureDirSync').and.callThrough();
var resolvedDest = path.resolve(project_dir, dest);
@@ -95,10 +97,12 @@ describe('common platform handler', function () {
expect(s).toHaveBeenCalled();
expect(s).toHaveBeenCalledWith(path.dirname(resolvedDest));
fs.removeSync(project_dir);
});
it('Test#007 : should call cp source/dest paths', function () {
fs.outputFileSync(java_file, 'contents');
fs.ensureDirSync(java_dir);
fs.writeFileSync(java_file, 'contents', 'utf-8');
var s = spyOn(fs, 'copySync').and.callThrough();
var resolvedDest = path.resolve(project_dir, dest);
@@ -107,15 +111,8 @@ describe('common platform handler', function () {
expect(s).toHaveBeenCalled();
expect(s).toHaveBeenCalledWith(java_file, resolvedDest);
});
it('should handle relative paths when checking for sub paths', () => {
fs.outputFileSync(java_file, 'contents');
const relativeProjectPath = path.relative(process.cwd(), project_dir);
expect(() => {
copyFile(test_dir, java_file, relativeProjectPath, dest);
}).not.toThrow();
fs.removeSync(project_dir);
});
});
@@ -124,12 +121,18 @@ describe('common platform handler', function () {
fs.ensureDirSync(dest);
expect(function () { copyNewFile(test_dir, src, project_dir, dest); })
.toThrow(new Error('"' + dest + '" already exists!'));
fs.removeSync(dest);
});
});
describe('deleteJava', function () {
beforeEach(function () {
fs.outputFileSync(java_file, 'contents');
fs.ensureDirSync(java_dir);
fs.writeFileSync(java_file, 'contents', 'utf-8');
});
afterEach(function () {
fs.removeSync(java_dir);
});
it('Test#009 : should call fs.unlinkSync on the provided paths', function () {

File diff suppressed because it is too large Load Diff

View File

@@ -18,66 +18,195 @@
*/
const rewire = require('rewire');
const builders = require('../../bin/templates/cordova/lib/builders/builders');
describe('run', () => {
let run;
beforeEach(() => {
run = rewire('../../bin/templates/cordova/lib/run');
run.__set__({
events: jasmine.createSpyObj('eventsSpy', ['emit'])
});
});
describe('buildTargetSpec', () => {
it('Test#001 : should select correct target based on the run opts', () => {
const buildTargetSpec = run.__get__('buildTargetSpec');
describe('getInstallTarget', () => {
const targetOpts = { target: 'emu' };
const deviceOpts = { device: true };
const emulatorOpts = { emulator: true };
const emptyOpts = {};
expect(buildTargetSpec({ target: 'emu' })).toEqual({ id: 'emu' });
expect(buildTargetSpec({ device: true })).toEqual({ type: 'device' });
expect(buildTargetSpec({ emulator: true })).toEqual({ type: 'emulator' });
expect(buildTargetSpec({})).toEqual({});
it('Test#001 : should select correct target based on the run opts', () => {
const getInstallTarget = run.__get__('getInstallTarget');
expect(getInstallTarget(targetOpts)).toBe('emu');
expect(getInstallTarget(deviceOpts)).toBe('--device');
expect(getInstallTarget(emulatorOpts)).toBe('--emulator');
expect(getInstallTarget(emptyOpts)).toBeUndefined();
});
});
describe('run method', () => {
let targetSpyObj, emulatorSpyObj, resolvedTarget;
let deviceSpyObj;
let emulatorSpyObj;
let eventsSpyObj;
let getInstallTargetSpy;
beforeEach(() => {
resolvedTarget = { id: 'dev1', type: 'device', arch: 'atari' };
targetSpyObj = jasmine.createSpyObj('deviceSpy', ['resolve', 'install']);
targetSpyObj.resolve.and.resolveTo(resolvedTarget);
targetSpyObj.install.and.resolveTo();
emulatorSpyObj = jasmine.createSpyObj('emulatorSpy', ['wait_for_boot']);
emulatorSpyObj.wait_for_boot.and.resolveTo();
deviceSpyObj = jasmine.createSpyObj('deviceSpy', ['install', 'list', 'resolveTarget']);
emulatorSpyObj = jasmine.createSpyObj('emulatorSpy', ['install', 'list_images', 'list_started', 'resolveTarget', 'start', 'wait_for_boot']);
eventsSpyObj = jasmine.createSpyObj('eventsSpy', ['emit']);
getInstallTargetSpy = jasmine.createSpy('getInstallTargetSpy');
run.__set__({
target: targetSpyObj,
emulator: emulatorSpyObj
});
// run needs `this` to behave like an Api instance
run.run = run.run.bind({
_builder: builders.getBuilder('FakeRootPath')
device: deviceSpyObj,
emulator: emulatorSpyObj,
events: eventsSpyObj,
getInstallTarget: getInstallTargetSpy
});
});
it('should install on target after build', () => {
it('should run on default device when no target arguments are specified', () => {
const deviceList = ['testDevice1', 'testDevice2'];
getInstallTargetSpy.and.returnValue(null);
deviceSpyObj.list.and.returnValue(Promise.resolve(deviceList));
return run.run().then(() => {
expect(targetSpyObj.install).toHaveBeenCalledWith(
resolvedTarget,
{ apkPaths: [], buildType: 'debug' }
);
expect(deviceSpyObj.resolveTarget).toHaveBeenCalledWith(deviceList[0]);
});
});
it('should run on emulator when no target arguments are specified, and no devices are found', () => {
const deviceList = [];
getInstallTargetSpy.and.returnValue(null);
deviceSpyObj.list.and.returnValue(Promise.resolve(deviceList));
emulatorSpyObj.list_started.and.returnValue(Promise.resolve([]));
return run.run().then(() => {
expect(emulatorSpyObj.list_started).toHaveBeenCalled();
});
});
it('should run on default device when device is requested, but none specified', () => {
getInstallTargetSpy.and.returnValue('--device');
return run.run().then(() => {
// Default device is selected by calling device.resolveTarget(null)
expect(deviceSpyObj.resolveTarget).toHaveBeenCalledWith(null);
});
});
it('should run on a running emulator if one exists', () => {
const emulatorList = ['emulator1', 'emulator2'];
getInstallTargetSpy.and.returnValue('--emulator');
emulatorSpyObj.list_started.and.returnValue(Promise.resolve(emulatorList));
return run.run().then(() => {
expect(emulatorSpyObj.resolveTarget).toHaveBeenCalledWith(emulatorList[0]);
});
});
it('should start an emulator and run on that if none is running', () => {
const emulatorList = [];
const defaultEmulator = 'default-emu';
getInstallTargetSpy.and.returnValue('--emulator');
emulatorSpyObj.list_started.and.returnValue(Promise.resolve(emulatorList));
emulatorSpyObj.start.and.returnValue(Promise.resolve(defaultEmulator));
return run.run().then(() => {
expect(emulatorSpyObj.resolveTarget).toHaveBeenCalledWith(defaultEmulator);
});
});
it('should run on a named device if it is specified', () => {
const deviceList = ['device1', 'device2', 'device3'];
getInstallTargetSpy.and.returnValue(deviceList[1]);
deviceSpyObj.list.and.returnValue(Promise.resolve(deviceList));
return run.run().then(() => {
expect(deviceSpyObj.resolveTarget).toHaveBeenCalledWith(deviceList[1]);
});
});
it('should run on a named emulator if it is specified', () => {
const startedEmulatorList = ['emu1', 'emu2', 'emu3'];
getInstallTargetSpy.and.returnValue(startedEmulatorList[2]);
deviceSpyObj.list.and.returnValue(Promise.resolve([]));
emulatorSpyObj.list_started.and.returnValue(Promise.resolve(startedEmulatorList));
return run.run().then(() => {
expect(emulatorSpyObj.resolveTarget).toHaveBeenCalledWith(startedEmulatorList[2]);
});
});
it('should start named emulator and then run on it if it is specified', () => {
const emulatorList = [
{ name: 'emu1', id: 1 },
{ name: 'emu2', id: 2 },
{ name: 'emu3', id: 3 }
];
getInstallTargetSpy.and.returnValue(emulatorList[2].name);
deviceSpyObj.list.and.returnValue(Promise.resolve([]));
emulatorSpyObj.list_started.and.returnValue(Promise.resolve([]));
emulatorSpyObj.list_images.and.returnValue(Promise.resolve(emulatorList));
emulatorSpyObj.start.and.returnValue(Promise.resolve(emulatorList[2].id));
return run.run().then(() => {
expect(emulatorSpyObj.start).toHaveBeenCalledWith(emulatorList[2].name);
expect(emulatorSpyObj.resolveTarget).toHaveBeenCalledWith(emulatorList[2].id);
});
});
it('should throw an error if target is specified but does not exist', () => {
const emulatorList = [{ name: 'emu1', id: 1 }];
const deviceList = ['device1'];
const target = 'nonexistentdevice';
getInstallTargetSpy.and.returnValue(target);
deviceSpyObj.list.and.returnValue(Promise.resolve(deviceList));
emulatorSpyObj.list_started.and.returnValue(Promise.resolve([]));
emulatorSpyObj.list_images.and.returnValue(Promise.resolve(emulatorList));
return run.run().then(
() => fail('Expected error to be thrown'),
err => expect(err.message).toContain(target)
);
});
it('should install on device after build', () => {
const deviceTarget = { target: 'device1', isEmulator: false };
getInstallTargetSpy.and.returnValue('--device');
deviceSpyObj.resolveTarget.and.returnValue(deviceTarget);
return run.run().then(() => {
expect(deviceSpyObj.install).toHaveBeenCalledWith(deviceTarget, { apkPaths: [], buildType: 'debug' });
});
});
it('should install on emulator after build', () => {
const emulatorTarget = { target: 'emu1', isEmulator: true };
getInstallTargetSpy.and.returnValue('--emulator');
emulatorSpyObj.list_started.and.returnValue(Promise.resolve([emulatorTarget.target]));
emulatorSpyObj.resolveTarget.and.returnValue(emulatorTarget);
emulatorSpyObj.wait_for_boot.and.returnValue(Promise.resolve());
return run.run().then(() => {
expect(emulatorSpyObj.install).toHaveBeenCalledWith(emulatorTarget, { apkPaths: [], buildType: 'debug' });
});
});
it('should fail with the error message if --packageType=bundle setting is used', () => {
targetSpyObj.resolve.and.resolveTo(resolvedTarget);
return expectAsync(run.run({ argv: ['--packageType=bundle'] }))
.toBeRejectedWith(jasmine.stringMatching(/Package type "bundle" is not supported/));
const deviceList = ['testDevice1', 'testDevice2'];
getInstallTargetSpy.and.returnValue(null);
deviceSpyObj.list.and.returnValue(Promise.resolve(deviceList));
return run.run({ argv: ['--packageType=bundle'] }).then(
() => fail('Expected error to be thrown'),
err => expect(err).toContain('Package type "bundle" is not supported during cordova run.')
);
});
});

View File

@@ -1,313 +0,0 @@
/**
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
const rewire = require('rewire');
const { CordovaError } = require('cordova-common');
describe('target', () => {
let target;
beforeEach(() => {
target = rewire('../../bin/templates/cordova/lib/target');
});
describe('list', () => {
it('should return available targets from Adb.devices', () => {
const AdbSpy = jasmine.createSpyObj('Adb', ['devices']);
AdbSpy.devices.and.resolveTo(['emulator-5556', '123a76565509e124']);
target.__set__('Adb', AdbSpy);
return target.list().then(emus => {
expect(emus).toEqual([
{ id: 'emulator-5556', type: 'emulator' },
{ id: '123a76565509e124', type: 'device' }
]);
});
});
});
describe('resolveToOnlineTarget', () => {
let resolveToOnlineTarget, emus, devs;
beforeEach(() => {
resolveToOnlineTarget = target.__get__('resolveToOnlineTarget');
emus = [
{ id: 'emu1', type: 'emulator' },
{ id: 'emu2', type: 'emulator' }
];
devs = [
{ id: 'dev1', type: 'device' },
{ id: 'dev2', type: 'device' }
];
spyOn(target, 'list').and.returnValue([...emus, ...devs]);
});
it('should return first device when no target arguments are specified', async () => {
return resolveToOnlineTarget().then(result => {
expect(result.id).toBe('dev1');
});
});
it('should return first emulator when no target arguments are specified and no devices are found', async () => {
target.list.and.resolveTo(emus);
return resolveToOnlineTarget().then(result => {
expect(result.id).toBe('emu1');
});
});
it('should return first device when type device is specified', async () => {
return resolveToOnlineTarget({ type: 'device' }).then(result => {
expect(result.id).toBe('dev1');
});
});
it('should return first running emulator when type emulator is specified', async () => {
return resolveToOnlineTarget({ type: 'emulator' }).then(result => {
expect(result.id).toBe('emu1');
});
});
it('should return a device that matches given ID', async () => {
return resolveToOnlineTarget({ id: 'dev2' }).then(result => {
expect(result.id).toBe('dev2');
});
});
it('should return a running emulator that matches given ID', async () => {
return resolveToOnlineTarget({ id: 'emu2' }).then(result => {
expect(result.id).toBe('emu2');
});
});
it('should return null if there are no online targets', async () => {
target.list.and.resolveTo([]);
return expectAsync(resolveToOnlineTarget())
.toBeResolvedTo(null);
});
it('should return null if no target matches given ID', async () => {
return expectAsync(resolveToOnlineTarget({ id: 'foo' }))
.toBeResolvedTo(null);
});
it('should return null if no target matches given type', async () => {
target.list.and.resolveTo(devs);
return expectAsync(resolveToOnlineTarget({ type: 'emulator' }))
.toBeResolvedTo(null);
});
});
describe('resolveToOfflineEmulator', () => {
const emuId = 'emulator-5554';
let resolveToOfflineEmulator, emulatorSpyObj;
beforeEach(() => {
resolveToOfflineEmulator = target.__get__('resolveToOfflineEmulator');
emulatorSpyObj = jasmine.createSpyObj('emulatorSpy', ['start']);
emulatorSpyObj.start.and.resolveTo(emuId);
target.__set__({
emulator: emulatorSpyObj,
isEmulatorName: name => name.startsWith('emu')
});
});
it('should start an emulator and run on that if none is running', () => {
return resolveToOfflineEmulator().then(result => {
expect(result).toEqual({ id: emuId, type: 'emulator' });
expect(emulatorSpyObj.start).toHaveBeenCalled();
});
});
it('should start named emulator and then run on it if it is specified', () => {
return resolveToOfflineEmulator({ id: 'emu3' }).then(result => {
expect(result).toEqual({ id: emuId, type: 'emulator' });
expect(emulatorSpyObj.start).toHaveBeenCalledWith('emu3');
});
});
it('should return null if given ID is not an avd name', () => {
return resolveToOfflineEmulator({ id: 'dev1' }).then(result => {
expect(result).toBe(null);
expect(emulatorSpyObj.start).not.toHaveBeenCalled();
});
});
it('should return null if given type is not emulator', () => {
return resolveToOfflineEmulator({ type: 'device' }).then(result => {
expect(result).toBe(null);
expect(emulatorSpyObj.start).not.toHaveBeenCalled();
});
});
});
describe('resolve', () => {
let resolveToOnlineTarget, resolveToOfflineEmulator;
beforeEach(() => {
resolveToOnlineTarget = jasmine.createSpy('resolveToOnlineTarget')
.and.resolveTo(null);
resolveToOfflineEmulator = jasmine.createSpy('resolveToOfflineEmulator')
.and.resolveTo(null);
target.__set__({
resolveToOnlineTarget,
resolveToOfflineEmulator,
build: { detectArchitecture: id => id + '-arch' }
});
});
it('should delegate to resolveToOnlineTarget', () => {
const spec = { type: 'device' };
resolveToOnlineTarget.and.resolveTo({ id: 'dev1', type: 'device' });
return target.resolve(spec).then(result => {
expect(result.id).toBe('dev1');
expect(resolveToOnlineTarget).toHaveBeenCalledWith(spec);
expect(resolveToOfflineEmulator).not.toHaveBeenCalled();
});
});
it('should delegate to resolveToOfflineEmulator if resolveToOnlineTarget fails', () => {
const spec = { type: 'emulator' };
resolveToOfflineEmulator.and.resolveTo({ id: 'emu1', type: 'emulator' });
return target.resolve(spec).then(result => {
expect(result.id).toBe('emu1');
expect(resolveToOnlineTarget).toHaveBeenCalledWith(spec);
expect(resolveToOfflineEmulator).toHaveBeenCalledWith(spec);
});
});
it('should add the target arch', () => {
const spec = { type: 'device' };
resolveToOnlineTarget.and.resolveTo({ id: 'dev1', type: 'device' });
return target.resolve(spec).then(result => {
expect(result.arch).toBe('dev1-arch');
});
});
it('should throw an error if target cannot be resolved', () => {
return expectAsync(target.resolve())
.toBeRejectedWithError(/Could not find target matching/);
});
});
describe('install', () => {
let AndroidManifestSpy;
let AndroidManifestFns;
let AndroidManifestGetActivitySpy;
let AdbSpy;
let buildSpy;
let installTarget;
beforeEach(() => {
installTarget = { id: 'emulator-5556', type: 'emulator', arch: 'atari' };
buildSpy = jasmine.createSpyObj('build', ['findBestApkForArchitecture']);
target.__set__('build', buildSpy);
AndroidManifestFns = jasmine.createSpyObj('AndroidManifestFns', ['getPackageId', 'getActivity']);
AndroidManifestGetActivitySpy = jasmine.createSpyObj('getActivity', ['getName']);
AndroidManifestFns.getActivity.and.returnValue(AndroidManifestGetActivitySpy);
AndroidManifestSpy = jasmine.createSpy('AndroidManifest').and.returnValue(AndroidManifestFns);
target.__set__('AndroidManifest', AndroidManifestSpy);
AdbSpy = jasmine.createSpyObj('Adb', ['shell', 'start', 'install', 'uninstall']);
AdbSpy.shell.and.returnValue(Promise.resolve());
AdbSpy.start.and.returnValue(Promise.resolve());
AdbSpy.install.and.returnValue(Promise.resolve());
AdbSpy.uninstall.and.returnValue(Promise.resolve());
target.__set__('Adb', AdbSpy);
// Silence output during test
spyOn(target.__get__('events'), 'emit');
});
it('should install to the passed target', () => {
return target.install(installTarget, {}).then(() => {
expect(AdbSpy.install.calls.argsFor(0)[0]).toBe(installTarget.id);
});
});
it('should install the correct apk based on the architecture and build results', () => {
const buildResults = {
apkPaths: 'path/to/apks',
buildType: 'debug',
buildMethod: 'foo'
};
const apkPath = 'my/apk/path/app.apk';
buildSpy.findBestApkForArchitecture.and.returnValue(apkPath);
return target.install(installTarget, buildResults).then(() => {
expect(buildSpy.findBestApkForArchitecture).toHaveBeenCalledWith(buildResults, installTarget.arch);
expect(AdbSpy.install.calls.argsFor(0)[1]).toBe(apkPath);
});
});
it('should uninstall and reinstall app if failure is due to different certificates', () => {
AdbSpy.install.and.returnValues(
Promise.reject('Failure: INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES'),
Promise.resolve()
);
return target.install(installTarget, {}).then(() => {
expect(AdbSpy.install).toHaveBeenCalledTimes(2);
expect(AdbSpy.uninstall).toHaveBeenCalled();
});
});
it('should throw any error not caused by different certificates', () => {
const errorMsg = 'Failure: Failed to install';
AdbSpy.install.and.rejectWith(new CordovaError(errorMsg));
return target.install(installTarget, {}).then(
() => fail('Unexpectedly resolved'),
err => {
expect(err).toEqual(jasmine.any(CordovaError));
expect(err.message).toContain(errorMsg);
}
);
});
it('should unlock the screen on device', () => {
return target.install(installTarget, {}).then(() => {
expect(AdbSpy.shell).toHaveBeenCalledWith(installTarget.id, 'input keyevent 82');
});
});
it('should start the newly installed app on the device', () => {
const packageId = 'unittestapp';
const activityName = 'TestActivity';
AndroidManifestFns.getPackageId.and.returnValue(packageId);
AndroidManifestGetActivitySpy.getName.and.returnValue(activityName);
return target.install(installTarget, {}).then(() => {
expect(AdbSpy.start).toHaveBeenCalledWith(installTarget.id, `${packageId}/.${activityName}`);
});
});
});
});

View File

@@ -45,8 +45,6 @@ import static org.apache.cordova.unittests.R.id.cordovaWebView;
public class BackButtonMultipageTest {
private static final String START_URL = "file:///android_asset/www/backbuttonmultipage/index.html";
private static final String SAMPLE3_URL = "file:///android_asset/www/backbuttonmultipage/sample3.html";
private static final String SAMPLE2_URL = "file:///android_asset/www/backbuttonmultipage/sample2.html";
//I have no idea why we picked 100, but we did.
private static final int WEBVIEW_ID = 100;
private TestActivity mActivity;
@@ -73,19 +71,19 @@ public class BackButtonMultipageTest {
webInterface.sendJavascript("window.location = 'sample2.html';");
}
});
assertPageSample(SAMPLE2_URL);
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
webInterface.sendJavascript("window.location = 'sample3.html';");
}
});
assertPageSample(SAMPLE3_URL);
assertEquals("file:///android_asset/www/backbuttonmultipage/sample3.html", mActivity.onPageFinishedUrl.take());
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
assertTrue(webInterface.backHistory());
}
});
assertPageSample(SAMPLE2_URL);
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
assertTrue(webInterface.backHistory());
@@ -106,22 +104,22 @@ public class BackButtonMultipageTest {
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
webInterface.loadUrl(SAMPLE2_URL);
webInterface.loadUrl("file:///android_asset/www/backbuttonmultipage/sample2.html");
}
});
assertPageSample(SAMPLE2_URL);
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
webInterface.loadUrl(SAMPLE3_URL);
webInterface.loadUrl("file:///android_asset/www/backbuttonmultipage/sample3.html");
}
});
assertPageSample(SAMPLE3_URL);
assertEquals("file:///android_asset/www/backbuttonmultipage/sample3.html", mActivity.onPageFinishedUrl.take());
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
assertTrue(webInterface.backHistory());
}
});
assertPageSample(SAMPLE2_URL);
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
assertTrue(webInterface.backHistory());
@@ -142,24 +140,19 @@ public class BackButtonMultipageTest {
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
webInterface.loadUrl(SAMPLE2_URL);
webInterface.loadUrl("file:///android_asset/www/backbuttonmultipage/sample2.html");
}
});
assertPageSample(SAMPLE2_URL);
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
webInterface.loadUrl(SAMPLE3_URL);
webInterface.loadUrl("file:///android_asset/www/backbuttonmultipage/sample3.html");
}
});
assertPageSample(SAMPLE3_URL);
assertEquals("file:///android_asset/www/backbuttonmultipage/sample3.html", mActivity.onPageFinishedUrl.take());
onView(withId(WEBVIEW_ID)).perform(pressBack());
assertPageSample(SAMPLE2_URL);
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
onView(withId(WEBVIEW_ID)).perform(pressBack());
assertEquals(START_URL, mActivity.onPageFinishedUrl.take());
}
private void assertPageSample(String url) {
assertEquals(url, mActivity.onPageFinishedUrl.take());
}
}

View File

@@ -45,8 +45,6 @@ import static org.apache.cordova.unittests.R.id.cordovaWebView;
public class BackButtonMultipageTest {
private static final String START_URL = "file:///android_asset/www/backbuttonmultipage/index.html";
private static final String SAMPLE3_URL = "file:///android_asset/www/backbuttonmultipage/sample3.html";
private static final String SAMPLE2_URL = "file:///android_asset/www/backbuttonmultipage/sample2.html";
//I have no idea why we picked 100, but we did.
private static final int WEBVIEW_ID = 100;
private TestActivity mActivity;
@@ -73,19 +71,19 @@ public class BackButtonMultipageTest {
webInterface.sendJavascript("window.location = 'sample2.html';");
}
});
assertPageSample(SAMPLE2_URL);
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
webInterface.sendJavascript("window.location = 'sample3.html';");
}
});
assertPageSample(SAMPLE3_URL);
assertEquals("file:///android_asset/www/backbuttonmultipage/sample3.html", mActivity.onPageFinishedUrl.take());
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
assertTrue(webInterface.backHistory());
}
});
assertPageSample(SAMPLE2_URL);
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
assertTrue(webInterface.backHistory());
@@ -106,22 +104,22 @@ public class BackButtonMultipageTest {
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
webInterface.loadUrl(SAMPLE2_URL);
webInterface.loadUrl("file:///android_asset/www/backbuttonmultipage/sample2.html");
}
});
assertPageSample(SAMPLE2_URL);
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
webInterface.loadUrl(SAMPLE3_URL);
webInterface.loadUrl("file:///android_asset/www/backbuttonmultipage/sample3.html");
}
});
assertPageSample(SAMPLE3_URL);
assertEquals("file:///android_asset/www/backbuttonmultipage/sample3.html", mActivity.onPageFinishedUrl.take());
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
assertTrue(webInterface.backHistory());
}
});
assertPageSample(SAMPLE2_URL);
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
assertTrue(webInterface.backHistory());
@@ -142,23 +140,19 @@ public class BackButtonMultipageTest {
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
webInterface.loadUrl(SAMPLE2_URL);
webInterface.loadUrl("file:///android_asset/www/backbuttonmultipage/sample2.html");
}
});
assertPageSample(SAMPLE2_URL);
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
webInterface.loadUrl(SAMPLE3_URL);
webInterface.loadUrl("file:///android_asset/www/backbuttonmultipage/sample3.html");
}
});
assertPageSample(SAMPLE3_URL);
assertEquals("file:///android_asset/www/backbuttonmultipage/sample3.html", mActivity.onPageFinishedUrl.take());
onView(withId(WEBVIEW_ID)).perform(pressBack());
assertPageSample(SAMPLE2_URL);
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
onView(withId(WEBVIEW_ID)).perform(pressBack());
assertEquals(START_URL, mActivity.onPageFinishedUrl.take());
}
private void assertPageSample(String url) {
assertEquals(url, mActivity.onPageFinishedUrl.take());
}
}