Compare commits

...

62 Commits

Author SHA1 Message Date
Erisu
c9108d8479 Set VERSION to 9.1.0 (via coho) 2021-04-10 00:26:26 +09:00
Erisu
98bb8c2a6a Update JS snapshot to version 9.1.0 (via coho) 2021-04-10 00:26:26 +09:00
Erisu
ac4d3d1d45 android-v9.1.0 Updated version and RELEASENOTES.md for release 9.1.0 2021-04-10 00:25:19 +09:00
エリス
be9abf5f91 chore: add missing header license (#1196) 2021-04-09 23:44:50 +09:00
Raphael von der Grün
c04ea9b1c0 refactor: unify target resolution for devices & emulators (#1101)
* refactor: unify target resolution for devices & emulators
* fix: use unified target methods in platform-centric bins
2021-04-09 15:37:56 +09:00
anasofiagribeiro
c774bf3311 feat: support gzip encoding requests & use GZIPInputStream (#1104) 2021-04-09 11:26:23 +09:00
エリス
9071d5131a Revert "[Frequent Updates] Gradle 6.6.1 & Android Gradle Plugin 4.0.1 & com.jfrog.bintray.gradle 1.8.5 (#1079)" (#1193)
This reverts commit 33476b4754.
2021-04-09 11:23:23 +09:00
エリス
c676ca98ff revert: feat: upgrade kotlin@1.4.31 #1176 (#1194) 2021-04-09 11:23:09 +09:00
エリス
690ff3f364 Revert "feat: upgrade Google Services Gradle Plugin google-services@4.3.5 (#1177)" (#1191)
This reverts commit e8ec3b1e37.
2021-04-08 14:04:52 +09:00
エリス
04b0106bca Revert "feat: upgrade gradle-plugin@4.1.3 (#1175)" (#1189)
This reverts commit 1430304d36.
2021-04-08 14:04:26 +09:00
エリス
31233089f1 Revert "feat: upgrade gradle@6.8.3 (#1174)" (#1190)
This reverts commit fe4d4aeff0.
2021-04-08 14:03:34 +09:00
エリス
1f5426f939 fix: copy repositories.gradle to project on create (#1186) 2021-03-30 22:57:14 +09:00
Norman Breau
1ec87634d4 fix(regression): Cannot read version of undefined caused by Java refactor (https://github.com/apache/cordova-android/pull/1130#discussion_r563597125) (#1185) 2021-03-30 22:40:34 +09:00
Alexander
d22af021ee feat: handle intent:// scheme links with browser_fallback_url param (#1167)
* fix showWebPage url intent:// now works
* android handle external url `intent://` scheme
* code refactoring

Co-authored-by: Андреянов Александр Николаевич <a.andreyanov@sevstar.net>
Co-authored-by: Tim Brust <github@timbrust.de>
2021-03-30 21:43:27 +09:00
エリス
c9ab33eded chore: rebuilt package-lock (#1183) 2021-03-30 19:41:21 +09:00
エリス
6dcd67a902 fix: unit-test failure (#1184) 2021-03-30 18:46:43 +09:00
Daniel Stone
cb1cf4dc8e fix(splashscreen): nav & title bar showing in fullscreen mode (#733)
Co-authored-by: Daniel Stone <daniel.stone@powerdms.com>
Co-authored-by: distinctdan <distinctdan@users.noreply.github.com>
2021-03-30 14:55:16 +09:00
goffioul
6cbf69d109 fix: restore key event handlers when DOM element is fullscreen (#1157)
* GH-1156: Restore key event handlers when a DOM element is fullscreen

Make sure to call dispatchKeyEvent from base class in WrapperView, if
the event hasn't been handled by the engine.

* Remove unwanted whitespace in condition

Co-authored-by: エリス <erisu@users.noreply.github.com>
Co-authored-by: Michael Goffioul <michael.goffioul@lincor.com>
2021-03-30 14:54:43 +09:00
Mosab A
e8ec3b1e37 feat: upgrade Google Services Gradle Plugin google-services@4.3.5 (#1177) 2021-03-30 14:44:08 +09:00
Guillem Perez
f927014d06 fix(android): Avoid Crash Report: ConcurrentModificationException (#1073)
Authored-by: lempere <lempere@lempere.com>
2021-03-28 09:49:39 -03:00
Rick Habets
19a5feb875 fix: add not null checks to prevent running on destroyed activity (#1148)
* (android) #1002: Add Null Pointer Checks to prevent Cordova from running on a destroyed activity

* (android) Add logging statements if Cordova Activity does not exist anymore (i.e. is destroyed)

Co-authored-by: Habets Rick <rick.habets@kbc.be>
2021-03-27 12:19:46 -03:00
ebhsgit
9dcf3eb68b Fix for #924 - Concurrent Modification Exception (#1091)
Co-authored-by: 8bhsolutions <48874658+8bhsolutions@users.noreply.github.com>
2021-03-27 12:17:39 -03:00
Norman Breau
11364918b2 add repositories support (#1179)
Authored-by: Engin Diri <engin.diri@lidl.com>
2021-03-27 12:11:56 -03:00
Mosab A
1430304d36 feat: upgrade gradle-plugin@4.1.3 (#1175)
* Update build.gradle

* Update build.gradle

* Update build.gradle

* Update build.gradle

* Update build.gradle

* Update build.gradle
2021-03-27 10:36:39 -03:00
Mosab A
2a92c2e595 feat: upgrade kotlin@1.4.31 (#1176)
* Update build.gradle

* Update build.gradle
2021-03-27 10:36:09 -03:00
Mosab A
fe4d4aeff0 feat: upgrade gradle@6.8.3 (#1174)
* Gradle Update to 6.8.3

* Update build.gradle

* Update wrapper.gradle

* Update wrapper.gradle
2021-03-27 10:35:27 -03:00
Shashank Agrawal
23a1710557 feat(android-studio): display app name as project name (#1173)
* (android) Feature: Write name of the Android app to .idea/.name for Android Studio #1172

* Missing space before function parentheses.

* Add test for writeNameForAndroidStudio #1172

* Use ES6 for new code. Code DRYness in test spec. #1172
2021-03-27 10:06:26 -03:00
Norman Breau
774de78691 refactor: java checks (#1130)
Co-authored-by: エリス <erisu@users.noreply.github.com>
Co-authored-by: Raphael von der Grün <raphinesse@gmail.com>

Update spec/unit/java.spec.js

Co-authored-by: Raphael von der Grün <raphinesse@gmail.com>

Update spec/unit/java.spec.js

Co-authored-by: Raphael von der Grün <raphinesse@gmail.com>

Update bin/templates/cordova/lib/utils.js

Co-authored-by: Raphael von der Grün <raphinesse@gmail.com>

Update bin/templates/cordova/lib/check_reqs.js

Co-authored-by: Raphael von der Grün <raphinesse@gmail.com>

Update spec/unit/check_reqs.spec.js

Co-authored-by: Raphael von der Grün <raphinesse@gmail.com>

Update spec/unit/check_reqs.spec.js

Co-authored-by: Raphael von der Grün <raphinesse@gmail.com>

Co-authored-by: Raphael von der Grün <raphinesse@gmail.com>
2021-03-27 10:05:50 -03:00
Norman Breau
3081e5e6e9 fix: optional arch parameter (#1153) 2021-01-19 21:33:06 -04:00
Pieter Van Poyer
7428bd3a7f Features/webp support for splashscreen (#1113)
* - linting
- platform independent paths in testing
- addes some unittest
- remove duplication + add comments
- delete webp's if png's added, delete png's if webp' added.
- Update bin/templates/cordova/lib/prepare.js Co-authored-by: エリス <erisu@users.noreply.github.com>
- fix https://github.com/apache/cordova-plugin-splashscreen/issues/257 webp support for android

* revert changes

* refactor: use source extension for target in getImageResourcePath

* fix(prepare): include more extensions in initial splash-screen resource map

* tests(prepare): quick-fix for tests

* backward slashes must be changed to forward slashes for fast-glob package.

Co-authored-by: Raphael von der Grün <raphinesse@gmail.com>
2020-12-16 17:21:35 -04:00
Raphael von der Grün
55feadff05 fix(prepare): mapImageResources always returning [] (#1136) 2020-12-04 09:14:19 +01:00
Norman Breau
97e2d15634 test(java): remove duplicate code in BackButtonMultipageTest (#1129)
* Removed duplicate code in test

* test: Remove duplicate code in BackButtonMultipageTest

Authored-by: leofernandesmo <leonardo.fernandes@ifal.edu.br>
Co-authored-by: breautek <norman@nbsolutions.ca>
2020-11-21 09:10:07 -04:00
Raphael von der Grün
b245337501 refactor(ProjectBuilder): clean up output file collection code (#1099)
* refactor(ProjectBuilder): less repetitive fileSorter

This reverts the fileSorter to the state from before #937, but using our
own simple re-implementation of `compare-func`.

* fix(ProjectBuilder): apply sort RegExp to basename only

* refactor(ProjectBuilder): use fast-glob instead of hand-rolled equivalent

* refactor(ProjectBuilder): factor out common isPathArchSpecific

* refactor(ProjectBuilder): use includes instead of indexOf

* refactor(ProjectBuilder): move sorting into findOutputFilesHelper

* refactor(ProjectBuilder): simplify findOutputFiles signature
2020-11-21 10:44:56 +01:00
Raphael von der Grün
bb7d733cde refactor: unify installation on devices & emulators (#1123)
* refactor: unify installation on devices & emulators

This change replaces the almost identical methods `device.install` and
`emulator.install` with the generic `target.install`.

* fix: use unified installation in platform-centric bins
2020-11-20 22:12:18 +01:00
Raphael von der Grün
aa679ea1d6 feat(Adb): list devices _and_ emulators in one go (#1125) 2020-11-19 21:30:56 +01:00
Raphael von der Grün
0e8234abfd refactor(check_reqs): cleanup default Java location detection on Windows (#1102)
* test(check_reqs): test default Java location detection on Windows

* refactor(check_reqs): use glob for default Java location detection on Windows

This changes the implementation to be closer to what it was before #842
with everything being in one place.

* fix: remove always-taken if statement

* feat: take both Program Files variants from env

* refactor(check_reqs): cosmetic changes
2020-11-19 21:08:43 +01:00
Pieter Van Poyer
d5b9029a23 Android - allow changing min sdk version (#1117)
* try to allow changing min sdk version

* undo testing with cdvMinSdkVersion=21

* Update framework/build.gradle

No printing of default behaviour

Co-authored-by: Norman Breau <norman@nbsolutions.ca>

* Update framework/build.gradle

typo + match case of cdvMinSdkVersion

Co-authored-by: Norman Breau <norman@nbsolutions.ca>

* resolve PR feedback timbru

Co-authored-by: Norman Breau <norman@nbsolutions.ca>
2020-11-18 13:48:46 -04:00
Raphael von der Grün
671e1fd1c6 refactor: remove copied Adb.install from emulator.install (#1108)
`emulator.install` contains a copy of the code of `Adb.install` just to
be able to pass custom options to `execa`.

This change removes that duplicated code in favor of a new option in
`Adb.install` that allows to pass options through to `execa`.
2020-11-17 09:06:44 +01:00
エリス
c144c08112 fix(android): allow file access for existing behavior (#1111) 2020-11-04 14:15:26 +09:00
Raphael von der Grün
aada3e813d refactor: do not kill adb on UNIX-like systems (#1103) 2020-10-22 18:03:28 +02:00
Raphael von der Grün
335b0f2575 test: fix unit test failures for certain random orders (#1094)
* test(Api): do not clobber global events.emit w/ spy

* test(Api): remove unnecessary rewiring

* test(check_reqs): add missing spyOn call

* test(check_reqs): fix process.env restoration

* test(check_reqs): restore module under test before each test
2020-10-19 10:38:37 +02:00
Raphael von der Grün
b1f01d7a65 test: ensure single top-level describe block in test file (#1094)
This makes it easier to map test output to files and allows for common
setup & teardown of all tests in a file.

* test(prepare): wrap in top-level describe block
* test(Api): wrap in top-level describe block
2020-10-19 10:01:09 +02:00
Darryl Pogue
3b56160d38 chore(asf): Update GitHub repo metadata 2020-10-06 20:24:23 -07:00
Norman Breau
39e315628e fix: Reflect minimum required NodeJS (#1045)
Project uses APIs only added in 10.10, namely ProjectBuilder.js readdirSync(), with `withFileTypes` option.
https://nodejs.org/api/fs.html#fs_fs_readdirsync_path_options
2020-10-06 10:12:04 -03:00
Raphael von der Grün
e125ab1b9a refactor(retry): simplify retryPromise using modern JS (#1086) 2020-10-06 10:56:21 +02:00
Raphael von der Grün
5d3591b853 refactor(utils): reduce number of utils (#1085)
* refactor(utils): remove utils.grep

* refactor(utils): replace utils.scanDirectory w/ fast-glob

Note that fast-glob is already in our dependency graph anyway.
2020-10-06 09:04:48 +02:00
Raphael von der Grün
206238893b fix(prepare): fix pattern used to collect image resources (#1084)
The pattern contained an additional plus that slipped in during the
refactoring done in #842. See [the diff][1] for details.

[1]: 09e8248d1f (diff-26c51bfaa44eff1e46fd61ec3225ec13L640-R650)
2020-10-06 08:38:09 +02:00
Mosab A
33476b4754 [Frequent Updates] Gradle 6.6.1 & Android Gradle Plugin 4.0.1 & com.jfrog.bintray.gradle 1.8.5 (#1079)
* Update ProjectBuilder.js

* Update build.gradle

* Update build.gradle

* Update build.gradle

* Update build.gradle

* Update build.gradle

* Update wrapper.gradle

* Update build.gradle

* Update wrapper.gradle

* Update wrapper.gradle

* Update wrapper.gradle

* Update build.gradle

* Update build.gradle

* Update ProjectBuilder.js

* Update ProjectBuilder.js
2020-10-05 21:26:59 -03:00
Raphael von der Grün
2c888f418b chore(pkg): remove deprecated no-op field "engineStrict" (#1081)
See https://docs.npmjs.com/files/package.json#enginestrict

Related to https://github.com/apache/cordova/issues/55
2020-10-04 13:32:28 +02:00
Norman Breau
565106fc1f refactor: Stop suppressing un-needed TruelyRandom lints (#1046) 2020-08-11 10:13:33 -03:00
Norman Breau
a45217e6b9 ci: Added Node 14.x (#975) 2020-07-24 14:02:13 -03:00
Raphael von der Grün
d7790ca8bc chore: remove unused emulator.create_image and its dependencies (#1019) 2020-07-09 00:07:33 +02:00
Raphael von der Grün
8ef8d994df fix(pluginHandlers): properly check if path is inside another (#1014) 2020-07-08 23:31:16 +02:00
Raphael von der Grün
80ad635348 test(pluginHandlers/common): better setup & teardown (#1013) 2020-07-08 15:15:53 +02:00
Norman Breau
ec944cf068 fix: gradle ignore properties (#1018) 2020-07-05 11:19:56 -03:00
Raphael von der Grün
ba5781c3bf refactor: save ProjectBuilder instance in Api instance (#1016)
This reduces dependence on the `builders` module and reduces repitition.

This also facilitates another WIP refactoring I am working on.
2020-07-03 18:54:24 +02:00
Raphael von der Grün
d86cb99dd5 Remove unnecessary stuff (#1015)
* Remove licenses for previously bundled packages

* Remove utils module with duplicate function

* Remove unused function check_reqs.check_ant

* Remove unused test helper
2020-07-03 18:53:10 +02:00
Raphael von der Grün
3204b9804b test(java): fix, improve and move clean script (#1017)
This includes the following changes:

- move this developer-only script to test/ where it conceptually belongs
  - this also prevents it from being distributed with this package
- fix paths for `android` and `androidx` variants
- make paths relative to the script, not to CWD
- use `removeSync` instead of `existsSync` and `existsSync`
- rename npm script to `clean:java-unit-tests` to clarify scope
2020-07-03 18:50:53 +02:00
Raphael von der Grün
ce735256d3 test: fix missing stack traces in jasmine output (#1012)
`true` is not a valid value for the `displayStacktrace` option of
jasmine-spec-reporter.
2020-07-02 22:39:03 +02:00
Erisu
cccf812454 Set package-lock.json to 9.1.0-dev 2020-06-24 12:28:24 +09:00
Erisu
f7e1979665 Update JS snapshot to version 9.1.0-dev (via coho) 2020-06-23 18:33:24 +09:00
Erisu
49fc5da207 Set VERSION to 9.1.0-dev (via coho) 2020-06-23 18:33:21 +09:00
57 changed files with 3289 additions and 3257 deletions

View File

@@ -15,6 +15,30 @@
# 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]
node-version: [10.x, 12.x, 14.x]
os: [ubuntu-latest, windows-latest, macos-latest]
steps:

65
LICENSE
View File

@@ -200,68 +200,3 @@
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,6 +20,69 @@
-->
## 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.0.0-dev
9.1.0

View File

@@ -19,7 +19,7 @@
var path = require('path');
var fs = require('fs-extra');
var utils = require('../../bin/lib/utils');
var utils = require('../templates/cordova/lib/utils');
var check_reqs = require('./../templates/cordova/lib/check_reqs');
var ROOT = path.join(__dirname, '..', '..');
@@ -36,6 +36,7 @@ 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');
@@ -71,6 +72,7 @@ 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'));
}
}
@@ -125,6 +127,8 @@ 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'));
}
}
@@ -197,6 +201,19 @@ 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.
*
@@ -294,6 +311,7 @@ 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);
};

View File

@@ -1,47 +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.
*/
/*
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.0.0-dev';
const VERSION = '9.1.0';
var path = require('path');
@@ -85,6 +85,8 @@ class Api {
build: path.join(this.root, 'build'),
javaSrc: path.join(appMain, 'java')
};
this._builder = require('./lib/builders/builders').getBuilder(this.root);
}
/**
@@ -161,7 +163,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
require('./lib/builders/builders').getBuilder().prepBuildFiles();
this._builder.prepBuildFiles();
}.bind(this))
// CB-11022 Return truthy value to prevent running prepare after
.then(() => true);
@@ -193,7 +195,7 @@ class Api {
if (plugin.getFrameworks(this.platform).length === 0) return;
selfEvents.emit('verbose', 'Updating build files since android plugin contained <framework>');
require('./lib/builders/builders').getBuilder().prepBuildFiles();
this._builder.prepBuildFiles();
}.bind(this))
// CB-11022 Return truthy value to prevent running prepare after
.then(() => true);

View File

@@ -24,42 +24,40 @@ var CordovaError = require('cordova-common').CordovaError;
var Adb = {};
function isDevice (line) {
return line.match(/\w+\tdevice/) && !line.match(/emulator/);
}
function isEmulator (line) {
return line.match(/device/) && line.match(/emulator/);
}
/**
* Lists available/connected devices and emulators
*
* @param {Object} opts Various options
* @param {Boolean} opts.emulators Specifies whether this method returns
* emulators only
*
* @return {Promise<String[]>} list of available/connected
* devices/emulators
*/
Adb.devices = function (opts) {
return 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.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.install = function (target, packagePath, opts) {
Adb.install = function (target, packagePath, { replace = false, execOptions = {} } = {}) {
events.emit('verbose', 'Installing apk ' + packagePath + ' on target ' + target + '...');
var args = ['-s', target, 'install'];
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 (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 (output.match(/Failure/)) {
if (output.match(/INSTALL_PARSE_FAILED_NO_CERTIFICATES/)) {
output += '\n\n' + 'Sign the build using \'-- --keystore\' or \'--buildConfig\'' +
@@ -69,7 +67,7 @@ Adb.install = function (target, packagePath, opts) {
'\nEither uninstall an app or increment the versionCode.';
}
return Promise.reject(new CordovaError('Failed to install apk to device: ' + output));
throw new CordovaError('Failed to install apk to target: ' + output);
}
});
};

View File

@@ -23,10 +23,7 @@ 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;
@@ -145,7 +142,7 @@ function parseOpts (options, resolvedTarget, projectRoot) {
*/
module.exports.runClean = function (options) {
var opts = parseOpts(options, null, this.root);
var builder = builders.getBuilder();
var builder = this._builder;
return builder.prepEnv(opts).then(function () {
return builder.clean(opts);
@@ -166,7 +163,7 @@ module.exports.runClean = function (options) {
*/
module.exports.run = function (options, optResolvedTarget) {
var opts = parseOpts(options, optResolvedTarget, this.root);
var builder = builders.getBuilder();
var builder = this._builder;
return builder.prepEnv(opts).then(function () {
if (opts.prepEnv) {
@@ -196,45 +193,8 @@ module.exports.run = function (options, optResolvedTarget) {
* Returns "arm" or "x86".
*/
module.exports.detectArchitecture = function (target) {
function helper () {
return Adb.shell(target, 'cat /proc/cpuinfo').then(function (output) {
return /intel/i.exec(output) ? 'x86' : 'arm';
});
}
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;
return Adb.shell(target, 'cat /proc/cpuinfo').then(function (output) {
return /intel/i.exec(output) ? 'x86' : 'arm';
});
};

View File

@@ -20,10 +20,12 @@
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!';
@@ -32,83 +34,46 @@ const TEMPLATE =
'# This file is automatically generated.\n' +
'# Do not modify this file -- ' + MARKER + '\n';
const archSpecificRegex = /-x86|-arm/;
const unsignedBuildRegex = /-unsigned/;
const isPathArchSpecific = p => /-x86|-arm/.test(path.basename(p));
const fileSorter = (filePathA, filePathB) => {
const archSpecificA = archSpecificRegex.test(filePathA);
const archSpecificB = archSpecificRegex.test(filePathB);
const outputFileComparator = compareByAll([
// Sort arch specific builds after generic ones
isPathArchSpecific,
// If they are not equal, then sort by specific archs after generic ones
if (archSpecificA !== archSpecificB) {
return archSpecificA < archSpecificB ? -1 : 1;
}
// Sort unsigned builds after signed ones
filePath => /-unsigned/.test(path.basename(filePath)),
// 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 modification time, latest first
filePath => -fs.statSync(filePath).mtime.getTime(),
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;
};
// Sort by file name length, ascending
filePath => filePath.length
]);
/**
* 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
* @param {'apk' | 'aab'} bundleType
* @param {'debug' | 'release'} buildType
* @param {{arch?: string}} options
*/
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);
function findOutputFiles (bundleType, buildType, { arch } = {}) {
let files = glob.sync(`**/*.${bundleType}`, {
absolute: true,
cwd: path.resolve(this[`${bundleType}Dir`], buildType)
}).map(path.normalize);
if (files.length === 0) return files;
// Assume arch-specific build if newest apk has -x86 or -arm.
const archSpecific = !!/-x86|-arm/.exec(path.basename(files[0]));
const archSpecific = isPathArchSpecific(files[0]);
// And show only arch-specific ones (or non-arch-specific)
files = files.filter(p => !!/-x86|-arm/.exec(path.basename(p)) === archSpecific);
files = files.filter(p => isPathArchSpecific(p) === archSpecific);
if (archSpecific && files.length > 1 && arch) {
files = files.filter(p => path.basename(p).indexOf('-' + arch) !== -1);
files = files.filter(p => path.basename(p).includes('-' + arch));
}
return files;
return files.sort(outputFileComparator);
}
class ProjectBuilder {
@@ -330,7 +295,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' })
return execa(wrapper, args, { stdio: 'inherit', cwd: path.resolve(this.root) })
.catch(function (error) {
if (error.toString().indexOf('failed to find target with hash string') >= 0) {
return check_reqs.check_android_target(error).then(function () {
@@ -346,7 +311,7 @@ class ProjectBuilder {
clean (opts) {
const wrapper = path.join(this.root, 'gradlew');
const args = this.getArgs('clean', opts);
return execa(wrapper, args, { stdio: 'inherit' })
return execa(wrapper, args, { stdio: 'inherit', cwd: path.resolve(this.root) })
.then(() => {
fs.removeSync(path.join(this.root, 'out'));
@@ -362,11 +327,11 @@ class ProjectBuilder {
}
findOutputApks (build_type, arch) {
return findOutputFilesHelper(this.apkDir, build_type, arch, 'apk').sort(fileSorter);
return findOutputFiles.call(this, 'apk', build_type, { arch });
}
findOutputBundles (build_type) {
return findOutputFilesHelper(this.aabDir, build_type, false, 'aab').sort(fileSorter);
return findOutputFiles.call(this, 'aab', build_type);
}
fetchBuildResults (build_type, arch) {

View File

@@ -20,43 +20,20 @@
const execa = require('execa');
var path = require('path');
var fs = require('fs-extra');
var os = require('os');
var which = require('which');
const { forgivingWhichSync, isWindows, isDarwin } = require('./utils');
const java = require('./env/java');
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');
function forgivingWhichSync (cmd) {
const whichResult = which.sync(cmd, { nothrow: true });
const EXPECTED_JAVA_VERSION = '1.8.x';
// 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');
};
// 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 });
/**
* @description Get valid target from framework/project.properties if run from this repo
@@ -98,18 +75,6 @@ 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;
@@ -168,75 +133,22 @@ module.exports.check_gradle = function () {
'in your path, or install Android Studio'));
};
// 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)\\';
/**
* 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();
let firstJdkDir =
getJDKDirectory(programFilesEnv) ||
getJDKDirectory(programFiles) ||
getJDKDirectory(programFilesx86);
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.'
);
}
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);
});
});
return javaVersion;
};
// Returns a promise.
@@ -412,13 +324,6 @@ 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.');
}

View File

@@ -1,109 +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');
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,25 +20,17 @@
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 });
@@ -215,24 +207,9 @@ module.exports.best_image = function () {
});
};
// 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;
});
exports.list_started = async () => {
return (await Adb.devices())
.filter(id => id.startsWith('emulator-'));
};
/*
@@ -371,147 +348,3 @@ 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');
});
};

123
bin/templates/cordova/lib/env/java.js vendored Normal file
View File

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

View File

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

View File

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

View File

@@ -20,6 +20,7 @@
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');
@@ -237,9 +238,9 @@ function updateProjectAccordingTo (platformConfig, locations) {
// Java file paths shouldn't be hard coded
const javaDirectory = path.join(locations.javaSrc, manifestId.replace(/\./g, '/'));
const javaPattern = /\.java$/;
const java_files = utils.scanDirectory(javaDirectory, javaPattern, true).filter(function (f) {
return utils.grep(f, /extends\s+CordovaActivity/g) !== null;
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);
});
if (java_files.length === 0) {
@@ -301,11 +302,12 @@ function default_versionCode (version) {
}
function getImageResourcePath (resourcesDir, type, density, name, sourceName) {
if (/\.9\.png$/.test(sourceName)) {
name = name.replace(/\.png$/, '.9.png');
}
var resourcePath = path.join(resourcesDir, (density ? type + '-' + density : type), name);
return resourcePath;
// 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);
}
function getAdaptiveImageResourcePath (resourcesDir, type, density, name, sourceName) {
@@ -316,6 +318,15 @@ 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');
@@ -325,7 +336,8 @@ function updateSplashes (cordovaProject, platformResourcesDir) {
return;
}
var resourceMap = mapImageResources(cordovaProject.root, platformResourcesDir, 'drawable', 'screen.png');
// Build an initial resource map that deletes all existing splash screens
const resourceMap = makeSplashCleanupMap(cordovaProject.root, platformResourcesDir);
var hadMdpi = false;
resources.forEach(function (resource) {
@@ -336,14 +348,14 @@ function updateSplashes (cordovaProject, platformResourcesDir) {
hadMdpi = true;
}
var targetPath = getImageResourcePath(
platformResourcesDir, 'drawable', resource.density, 'screen.png', path.basename(resource.src));
platformResourcesDir, 'drawable', resource.density, 'screen', path.basename(resource.src));
resourceMap[targetPath] = resource.src;
});
// There's no "default" drawable, so assume default == mdpi.
if (!hadMdpi && resources.defaultResource) {
var targetPath = getImageResourcePath(
platformResourcesDir, 'drawable', 'mdpi', 'screen.png', path.basename(resources.defaultResource.src));
platformResourcesDir, 'drawable', 'mdpi', 'screen', path.basename(resources.defaultResource.src));
resourceMap[targetPath] = resources.defaultResource.src;
}
@@ -355,7 +367,8 @@ function updateSplashes (cordovaProject, platformResourcesDir) {
function cleanSplashes (projectRoot, projectConfig, platformResourcesDir) {
var resources = projectConfig.getSplashScreens('android');
if (resources.length > 0) {
var resourceMap = mapImageResources(projectRoot, platformResourcesDir, 'drawable', 'screen.png');
const resourceMap = makeSplashCleanupMap(projectRoot, platformResourcesDir);
events.emit('verbose', 'Cleaning splash screens at ' + platformResourcesDir);
// No source paths are specified in the map, so updatePaths() will delete the target files.
@@ -545,13 +558,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.png', path.basename(android_icons[density].src));
var targetPath = getImageResourcePath(platformResourcesDir, 'mipmap', density, 'ic_launcher', path.basename(android_icons[density].src));
resourceMap[targetPath] = android_icons[density].src;
}
// There's no "default" drawable, so assume default == mdpi.
if (default_icon && !android_icons.mdpi) {
var defaultTargetPath = getImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher.png', path.basename(default_icon.src));
var defaultTargetPath = getImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher', path.basename(default_icon.src));
resourceMap[defaultTargetPath] = default_icon.src;
}
@@ -664,14 +677,24 @@ function cleanIcons (projectRoot, projectConfig, platformResourcesDir) {
*/
function mapImageResources (rootDir, subDir, type, resourceName) {
const pathMap = {};
const pattern = new RegExp(type + '+-.+');
utils.scanDirectory(path.join(rootDir, subDir), pattern).forEach(function (drawableFolder) {
const imagePath = path.join(subDir, path.basename(drawableFolder), resourceName);
const globOptions = { cwd: path.join(rootDir, subDir), onlyDirectories: true };
glob.sync(type + '-*', globOptions).forEach(drawableFolder => {
const imagePath = path.join(subDir, 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,31 +31,13 @@ var events = require('cordova-common').events;
*
* @returns {Promise}
*/
module.exports.retryPromise = function (attemptsLeft, promiseFunction) {
// NOTE:
// get all trailing arguments, by skipping the first two (attemptsLeft and
// promiseFunction) because they shouldn't get passed to promiseFunction
var promiseFunctionArguments = Array.prototype.slice.call(arguments, 2);
return promiseFunction.apply(undefined, promiseFunctionArguments).then(
// on success pass results through
function onFulfilled (value) {
return value;
},
// on rejection either retry, or throw the error
function onRejected (error) {
attemptsLeft -= 1;
if (attemptsLeft < 1) {
throw error;
}
module.exports.retryPromise = async function (attemptsLeft, promiseFunction, ...args) {
while (true) {
try {
return await promiseFunction(...args);
} catch (error) {
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,21 +19,30 @@
var path = require('path');
var emulator = require('./emulator');
var device = require('./device');
const target = require('./target');
var PackageType = require('./PackageType');
const { CordovaError, events } = require('cordova-common');
const { events } = require('cordova-common');
function getInstallTarget (runOptions) {
var install_target;
/**
* Builds a target spec from a runOptions object
*
* @param {{target?: string, device?: boolean, emulator?: boolean}} runOptions
* @return {target.TargetSpec}
*/
function buildTargetSpec (runOptions) {
const spec = {};
if (runOptions.target) {
install_target = runOptions.target;
spec.id = runOptions.target;
} else if (runOptions.device) {
install_target = '--device';
spec.type = 'device';
} else if (runOptions.emulator) {
install_target = '--emulator';
spec.type = 'emulator';
}
return spec;
}
return install_target;
function formatResolvedTarget ({ id, type }) {
return `${type} ${id}`;
}
/**
@@ -50,57 +59,12 @@ module.exports.run = function (runOptions) {
runOptions = runOptions || {};
var self = this;
var install_target = getInstallTarget(runOptions);
const spec = buildTargetSpec(runOptions);
return target.resolve(spec).then(function (resolvedTarget) {
events.emit('log', `Deploying to ${formatResolvedTarget(resolvedTarget)}`);
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
@@ -110,15 +74,13 @@ module.exports.run = function (runOptions) {
throw packageTypeErrorMessage;
}
resolve(builder.fetchBuildResults(buildOptions.buildType, buildOptions.arch));
}).then(function (buildResults) {
if (resolvedTarget && resolvedTarget.isEmulator) {
return emulator.wait_for_boot(resolvedTarget.target).then(function () {
return emulator.install(resolvedTarget, buildResults);
});
resolve(self._builder.fetchBuildResults(buildOptions.buildType, buildOptions.arch));
}).then(async function (buildResults) {
if (resolvedTarget.type === 'emulator') {
await emulator.wait_for_boot(resolvedTarget.id);
}
return device.install(resolvedTarget, buildResults);
return target.install(resolvedTarget, buildResults);
});
});
};

155
bin/templates/cordova/lib/target.js vendored Normal file
View File

@@ -0,0 +1,155 @@
/*
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,7 +24,8 @@
// TODO: Perhaps this should live in cordova-common?
const fs = require('fs-extra');
const path = require('path');
const which = require('which');
const os = require('os');
/**
* Reads, searches, and replaces the found occurences with replacementString and then writes the file back out.
@@ -41,57 +42,27 @@ exports.replaceFileContents = function (file, searchRegex, replacementString) {
fs.writeFileSync(file, contents);
};
/**
* 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;
// 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;
}
}
return null;
return 0;
};
};
/**
* 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 = [];
exports.forgivingWhichSync = (cmd) => {
const whichResult = which.sync(cmd, { nothrow: true });
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;
// On null, returns empty string to maintain backwards compatibility
// realpathSync follows symlinks
return whichResult === null ? '' : fs.realpathSync(whichResult);
};
exports.isWindows = () => os.platform() === 'win32';
exports.isDarwin = () => os.platform() === 'darwin';

View File

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

View File

@@ -0,0 +1,23 @@
/* 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
// 74fdba8b327b2a13b4366dd141b52def96d4cb56
// cordova-js rel/6.0.0-10-g07379820
/*
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.0.0-dev';
var PLATFORM_VERSION_BUILD_LABEL = '9.1.0';
// file: src/scripts/require.js
var require;
var define;
@@ -50,10 +50,10 @@ var define;
require = function (id) {
if (!modules[id]) {
throw 'module ' + id + ' not found';
throw new Error('module ' + id + ' not found');
} else if (id in inProgressModules) {
var cycle = requireStack.slice(inProgressModules[id]).join('->') + '->' + id;
throw 'Cycle in require graph: ' + cycle;
throw new Error('Cycle in require graph: ' + cycle);
}
if (modules[id].factory) {
try {
@@ -70,7 +70,7 @@ var define;
define = function (id, factory) {
if (Object.prototype.hasOwnProperty.call(modules, id)) {
throw 'module ' + id + ' already defined';
throw new Error('module ' + id + ' already defined');
}
modules[id] = {
@@ -97,7 +97,7 @@ define("cordova", function(require, exports, module) {
// Workaround for Windows 10 in hosted environment case
// http://www.w3.org/html/wg/drafts/html/master/browsers.html#named-access-on-the-window-object
if (window.cordova && !(window.cordova instanceof HTMLElement)) { // eslint-disable-line no-undef
if (window.cordova && !(window.cordova instanceof HTMLElement)) {
throw new Error('cordova already defined');
}
@@ -162,7 +162,7 @@ function createEvent (type, data) {
event.initEvent(type, false, false);
if (data) {
for (var i in data) {
if (data.hasOwnProperty(i)) {
if (Object.prototype.hasOwnProperty.call(data, i)) {
event[i] = data[i];
}
}
@@ -170,7 +170,6 @@ function createEvent (type, data) {
return event;
}
/* eslint-disable no-undef */
var cordova = {
define: define,
require: require,
@@ -178,8 +177,6 @@ var cordova = {
platformVersion: PLATFORM_VERSION_BUILD_LABEL,
platformId: platform.id,
/* eslint-enable no-undef */
/**
* Methods to add/remove your own addEventListener hijacking on document + window.
*/
@@ -198,15 +195,25 @@ var cordova = {
removeDocumentEventHandler: function (event) {
delete documentEventHandlers[event];
},
/**
* Retrieve original event handlers that were replaced by Cordova
*
* @return object
*/
getOriginalHandlers: function () {
return { 'document': { 'addEventListener': m_document_addEventListener, 'removeEventListener': m_document_removeEventListener },
'window': { 'addEventListener': m_window_addEventListener, 'removeEventListener': m_window_removeEventListener } };
return {
document: {
addEventListener: m_document_addEventListener,
removeEventListener: m_document_removeEventListener
},
window: {
addEventListener: m_window_addEventListener,
removeEventListener: m_window_removeEventListener
}
};
},
/**
* Method to fire event from native code
* bNoDetach is required for events which cause an exception which needs to be caught in native code
@@ -229,6 +236,7 @@ var cordova = {
document.dispatchEvent(evt);
}
},
fireWindowEvent: function (type, data) {
var evt = createEvent(type, data);
if (typeof windowEventHandlers[type] !== 'undefined') {
@@ -302,12 +310,11 @@ var cordova = {
}
} catch (err) {
var msg = 'Error in ' + (isSuccess ? 'Success' : 'Error') + ' callbackId: ' + callbackId + ' : ' + err;
console && console.log && console.log(msg);
console && console.log && err.stack && console.log(err.stack);
cordova.fireWindowEvent('cordovacallbackerror', { 'message': msg });
cordova.fireWindowEvent('cordovacallbackerror', { message: msg, error: err });
throw err;
}
},
addConstructor: function (func) {
channel.onCordovaReady.subscribe(function () {
try {
@@ -334,12 +341,12 @@ var nativeApi = this._cordovaNative || require('cordova/android/promptbasednativ
var currentApi = nativeApi;
module.exports = {
get: function() { return currentApi; },
setPreferPrompt: function(value) {
get: function () { return currentApi; },
setPreferPrompt: function (value) {
currentApi = value ? require('cordova/android/promptbasednativeapi') : nativeApi;
},
// Used only by tests.
set: function(value) {
set: function (value) {
currentApi = value;
}
};
@@ -355,13 +362,13 @@ define("cordova/android/promptbasednativeapi", function(require, exports, module
*/
module.exports = {
exec: function(bridgeSecret, service, action, callbackId, argsJson) {
return prompt(argsJson, 'gap:'+JSON.stringify([bridgeSecret, service, action, callbackId]));
exec: function (bridgeSecret, service, action, callbackId, argsJson) {
return prompt(argsJson, 'gap:' + JSON.stringify([bridgeSecret, service, action, callbackId]));
},
setNativeToJsBridgeMode: function(bridgeSecret, value) {
setNativeToJsBridgeMode: function (bridgeSecret, value) {
prompt(value, 'gap_bridge_mode:' + bridgeSecret);
},
retrieveJsMessages: function(bridgeSecret, fromOnlineEvent) {
retrieveJsMessages: function (bridgeSecret, fromOnlineEvent) {
return prompt(+fromOnlineEvent, 'gap_poll:' + bridgeSecret);
}
};
@@ -376,12 +383,12 @@ var utils = require('cordova/utils');
var moduleExports = module.exports;
var typeMap = {
'A': 'Array',
'D': 'Date',
'N': 'Number',
'S': 'String',
'F': 'Function',
'O': 'Object'
A: 'Array',
D: 'Date',
N: 'Number',
S: 'String',
F: 'Function',
O: 'Object'
};
function extractParamName (callee, argIndex) {
@@ -472,7 +479,7 @@ base64.fromArrayBuffer = function (arrayBuffer) {
};
base64.toArrayBuffer = function (str) {
var decodedStr = typeof atob !== 'undefined' ? atob(str) : Buffer.from(str, 'base64').toString('binary'); // eslint-disable-line no-undef
var decodedStr = atob(str);
var arrayBuffer = new ArrayBuffer(decodedStr.length);
var array = new Uint8Array(arrayBuffer);
for (var i = 0, len = decodedStr.length; i < len; i++) {
@@ -534,14 +541,13 @@ var utils = require('cordova/utils');
function each (objects, func, context) {
for (var prop in objects) {
if (objects.hasOwnProperty(prop)) {
if (Object.prototype.hasOwnProperty.call(objects, prop)) {
func.apply(context, [objects[prop], prop]);
}
}
}
function clobber (obj, key, value) {
exports.replaceHookForTesting(obj, key);
var needsProperty = false;
try {
obj[key] = value;
@@ -615,7 +621,7 @@ function include (parent, objects, clobber, merge) {
*/
function recursiveMerge (target, src) {
for (var prop in src) {
if (src.hasOwnProperty(prop)) {
if (Object.prototype.hasOwnProperty.call(src, prop)) {
if (target.prototype && target.prototype.constructor === target) {
// If the target object is a constructor override off prototype.
clobber(target.prototype, prop, src[prop]);
@@ -641,7 +647,6 @@ exports.buildIntoAndMerge = function (objects, target) {
};
exports.recursiveMerge = recursiveMerge;
exports.assignOrWrapInDeprecateGetter = assignOrWrapInDeprecateGetter;
exports.replaceHookForTesting = function () {};
});
@@ -722,14 +727,14 @@ var channel = {
}
if (!len) h();
},
/* eslint-disable no-return-assign */
create: function (type) {
return channel[type] = new Channel(type, false);
return (channel[type] = new Channel(type, false));
},
createSticky: function (type) {
return channel[type] = new Channel(type, true);
return (channel[type] = new Channel(type, true));
},
/* eslint-enable no-return-assign */
/**
* cordova Channels that must fire before "deviceready" is fired.
*/
@@ -850,7 +855,6 @@ Channel.prototype.unsubscribe = function (eventListenerOrFunction) {
* Calls all functions subscribed to this channel.
*/
Channel.prototype.fire = function (e) {
var fail = false; // eslint-disable-line no-unused-vars
var fireArgs = Array.prototype.slice.call(arguments);
// Apply stickiness.
if (this.state === 1) {
@@ -924,38 +928,38 @@ define("cordova/exec", function(require, exports, module) {
* @param {String} action Action to be run in cordova
* @param {String[]} [args] Zero or more arguments to pass to the method
*/
var cordova = require('cordova'),
nativeApiProvider = require('cordova/android/nativeapiprovider'),
utils = require('cordova/utils'),
base64 = require('cordova/base64'),
channel = require('cordova/channel'),
jsToNativeModes = {
PROMPT: 0,
JS_OBJECT: 1
},
nativeToJsModes = {
// Polls for messages using the JS->Native bridge.
POLLING: 0,
// For LOAD_URL to be viable, it would need to have a work-around for
// the bug where the soft-keyboard gets dismissed when a message is sent.
LOAD_URL: 1,
// For the ONLINE_EVENT to be viable, it would need to intercept all event
// listeners (both through addEventListener and window.ononline) as well
// as set the navigator property itself.
ONLINE_EVENT: 2,
EVAL_BRIDGE: 3
},
jsToNativeBridgeMode, // Set lazily.
nativeToJsBridgeMode = nativeToJsModes.EVAL_BRIDGE,
pollEnabled = false,
bridgeSecret = -1;
var cordova = require('cordova');
var nativeApiProvider = require('cordova/android/nativeapiprovider');
var utils = require('cordova/utils');
var base64 = require('cordova/base64');
var channel = require('cordova/channel');
var jsToNativeModes = {
PROMPT: 0,
JS_OBJECT: 1
};
var nativeToJsModes = {
// Polls for messages using the JS->Native bridge.
POLLING: 0,
// For LOAD_URL to be viable, it would need to have a work-around for
// the bug where the soft-keyboard gets dismissed when a message is sent.
LOAD_URL: 1,
// For the ONLINE_EVENT to be viable, it would need to intercept all event
// listeners (both through addEventListener and window.ononline) as well
// as set the navigator property itself.
ONLINE_EVENT: 2,
EVAL_BRIDGE: 3
};
var jsToNativeBridgeMode; // Set lazily.
var nativeToJsBridgeMode = nativeToJsModes.EVAL_BRIDGE;
var pollEnabled = false;
var bridgeSecret = -1;
var messagesFromNative = [];
var isProcessing = false;
var resolvedPromise = typeof Promise == 'undefined' ? null : Promise.resolve();
var nextTick = resolvedPromise ? function(fn) { resolvedPromise.then(fn); } : function(fn) { setTimeout(fn); };
var resolvedPromise = typeof Promise === 'undefined' ? null : Promise.resolve();
var nextTick = resolvedPromise ? function (fn) { resolvedPromise.then(fn); } : function (fn) { setTimeout(fn); };
function androidExec(success, fail, service, action, args) {
function androidExec (success, fail, service, action, args) {
if (bridgeSecret < 0) {
// If we ever catch this firing, we'll need to queue up exec()s
// and fire them once we get a secret. For now, I don't think
@@ -974,21 +978,21 @@ function androidExec(success, fail, service, action, args) {
// Process any ArrayBuffers in the args into a string.
for (var i = 0; i < args.length; i++) {
if (utils.typeName(args[i]) == 'ArrayBuffer') {
if (utils.typeName(args[i]) === 'ArrayBuffer') {
args[i] = base64.fromArrayBuffer(args[i]);
}
}
var callbackId = service + cordova.callbackId++,
argsJson = JSON.stringify(args);
var callbackId = service + cordova.callbackId++;
var argsJson = JSON.stringify(args);
if (success || fail) {
cordova.callbacks[callbackId] = {success:success, fail:fail};
cordova.callbacks[callbackId] = { success: success, fail: fail };
}
var msgs = nativeApiProvider.get().exec(bridgeSecret, service, action, callbackId, argsJson);
// If argsJson was received by Java as null, try again with the PROMPT bridge mode.
// This happens in rare circumstances, such as when certain Unicode characters are passed over the bridge on a Galaxy S2. See CB-2666.
if (jsToNativeBridgeMode == jsToNativeModes.JS_OBJECT && msgs === "@Null arguments.") {
if (jsToNativeBridgeMode === jsToNativeModes.JS_OBJECT && msgs === '@Null arguments.') {
androidExec.setJsToNativeBridgeMode(jsToNativeModes.PROMPT);
androidExec(success, fail, service, action, args);
androidExec.setJsToNativeBridgeMode(jsToNativeModes.JS_OBJECT);
@@ -999,16 +1003,16 @@ function androidExec(success, fail, service, action, args) {
}
}
androidExec.init = function() {
androidExec.init = function () {
bridgeSecret = +prompt('', 'gap_init:' + nativeToJsBridgeMode);
channel.onNativeReady.fire();
};
function pollOnceFromOnlineEvent() {
function pollOnceFromOnlineEvent () {
pollOnce(true);
}
function pollOnce(opt_fromOnlineEvent) {
function pollOnce (opt_fromOnlineEvent) {
if (bridgeSecret < 0) {
// This can happen when the NativeToJsMessageQueue resets the online state on page transitions.
// We know there's nothing to retrieve, so no need to poll.
@@ -1022,15 +1026,15 @@ function pollOnce(opt_fromOnlineEvent) {
}
}
function pollingTimerFunc() {
function pollingTimerFunc () {
if (pollEnabled) {
pollOnce();
setTimeout(pollingTimerFunc, 50);
}
}
function hookOnlineApis() {
function proxyEvent(e) {
function hookOnlineApis () {
function proxyEvent (e) {
cordova.fireWindowEvent(e.type);
}
// The network module takes care of firing online and offline events.
@@ -1050,19 +1054,19 @@ hookOnlineApis();
androidExec.jsToNativeModes = jsToNativeModes;
androidExec.nativeToJsModes = nativeToJsModes;
androidExec.setJsToNativeBridgeMode = function(mode) {
if (mode == jsToNativeModes.JS_OBJECT && !window._cordovaNative) {
androidExec.setJsToNativeBridgeMode = function (mode) {
if (mode === jsToNativeModes.JS_OBJECT && !window._cordovaNative) {
mode = jsToNativeModes.PROMPT;
}
nativeApiProvider.setPreferPrompt(mode == jsToNativeModes.PROMPT);
nativeApiProvider.setPreferPrompt(mode === jsToNativeModes.PROMPT);
jsToNativeBridgeMode = mode;
};
androidExec.setNativeToJsBridgeMode = function(mode) {
if (mode == nativeToJsBridgeMode) {
androidExec.setNativeToJsBridgeMode = function (mode) {
if (mode === nativeToJsBridgeMode) {
return;
}
if (nativeToJsBridgeMode == nativeToJsModes.POLLING) {
if (nativeToJsBridgeMode === nativeToJsModes.POLLING) {
pollEnabled = false;
}
@@ -1073,32 +1077,32 @@ androidExec.setNativeToJsBridgeMode = function(mode) {
nativeApiProvider.get().setNativeToJsBridgeMode(bridgeSecret, mode);
}
if (mode == nativeToJsModes.POLLING) {
if (mode === nativeToJsModes.POLLING) {
pollEnabled = true;
setTimeout(pollingTimerFunc, 1);
}
};
function buildPayload(payload, message) {
function buildPayload (payload, message) {
var payloadKind = message.charAt(0);
if (payloadKind == 's') {
if (payloadKind === 's') {
payload.push(message.slice(1));
} else if (payloadKind == 't') {
} else if (payloadKind === 't') {
payload.push(true);
} else if (payloadKind == 'f') {
} else if (payloadKind === 'f') {
payload.push(false);
} else if (payloadKind == 'N') {
} else if (payloadKind === 'N') {
payload.push(null);
} else if (payloadKind == 'n') {
} else if (payloadKind === 'n') {
payload.push(+message.slice(1));
} else if (payloadKind == 'A') {
} else if (payloadKind === 'A') {
var data = message.slice(1);
payload.push(base64.toArrayBuffer(data));
} else if (payloadKind == 'S') {
} else if (payloadKind === 'S') {
payload.push(window.atob(message.slice(1)));
} else if (payloadKind == 'M') {
} else if (payloadKind === 'M') {
var multipartMessages = message.slice(1);
while (multipartMessages !== "") {
while (multipartMessages !== '') {
var spaceIdx = multipartMessages.indexOf(' ');
var msgLen = +multipartMessages.slice(0, spaceIdx);
var multipartMessage = multipartMessages.substr(spaceIdx + 1, msgLen);
@@ -1111,14 +1115,15 @@ function buildPayload(payload, message) {
}
// Processes a single message, as encoded by NativeToJsMessageQueue.java.
function processMessage(message) {
function processMessage (message) {
var firstChar = message.charAt(0);
if (firstChar == 'J') {
if (firstChar === 'J') {
// This is deprecated on the .java side. It doesn't work with CSP enabled.
// eslint-disable-next-line no-eval
eval(message.slice(1));
} else if (firstChar == 'S' || firstChar == 'F') {
var success = firstChar == 'S';
var keepCallback = message.charAt(1) == '1';
} else if (firstChar === 'S' || firstChar === 'F') {
var success = firstChar === 'S';
var keepCallback = message.charAt(1) === '1';
var spaceIdx = message.indexOf(' ', 2);
var status = +message.slice(2, spaceIdx);
var nextSpaceIdx = message.indexOf(' ', spaceIdx + 1);
@@ -1128,11 +1133,11 @@ function processMessage(message) {
buildPayload(payload, payloadMessage);
cordova.callbackFromNative(callbackId, success, status, payload, keepCallback);
} else {
console.log("processMessage failed: invalid message: " + JSON.stringify(message));
console.log('processMessage failed: invalid message: ' + JSON.stringify(message));
}
}
function processMessages() {
function processMessages () {
// Check for the reentrant case.
if (isProcessing) {
return;
@@ -1145,7 +1150,7 @@ function processMessages() {
var msg = popMessageFromQueue();
// The Java side can send a * message to indicate that it
// still has messages waiting to be retrieved.
if (msg == '*' && messagesFromNative.length === 0) {
if (msg === '*' && messagesFromNative.length === 0) {
nextTick(pollOnce);
return;
}
@@ -1158,9 +1163,9 @@ function processMessages() {
}
}
function popMessageFromQueue() {
function popMessageFromQueue () {
var messageBatch = messagesFromNative.shift();
if (messageBatch == '*') {
if (messageBatch === '*') {
return '*';
}
@@ -1216,7 +1221,6 @@ var cordova = require('cordova');
var modulemapper = require('cordova/modulemapper');
var platform = require('cordova/platform');
var pluginloader = require('cordova/pluginloader');
var utils = require('cordova/utils');
var platformInitChannelsArray = [channel.onNativeReady, channel.onPluginsReady];
@@ -1236,34 +1240,6 @@ window.setTimeout(function () {
}
}, 5000);
// Replace navigator before any modules are required(), to ensure it happens as soon as possible.
// We replace it so that properties that can't be clobbered can instead be overridden.
function replaceNavigator (origNavigator) {
var CordovaNavigator = function () {};
CordovaNavigator.prototype = origNavigator;
var newNavigator = new CordovaNavigator();
// This work-around really only applies to new APIs that are newer than Function.bind.
// Without it, APIs such as getGamepads() break.
if (CordovaNavigator.bind) {
for (var key in origNavigator) {
if (typeof origNavigator[key] === 'function') {
newNavigator[key] = origNavigator[key].bind(origNavigator);
} else {
(function (k) {
utils.defineGetterSetter(newNavigator, key, function () {
return origNavigator[k];
});
})(key);
}
}
}
return newNavigator;
}
if (window.navigator) {
window.navigator = replaceNavigator(window.navigator);
}
if (!window.console) {
window.console = {
log: function () {}
@@ -1329,7 +1305,6 @@ channel.join(function () {
channel.join(function () {
require('cordova').fireDocumentEvent('deviceready');
}, channel.deviceReadyChannelsArray);
}, platformInitChannelsArray);
});
@@ -1338,7 +1313,7 @@ channel.join(function () {
define("cordova/modulemapper", function(require, exports, module) {
var builder = require('cordova/builder');
var moduleMap = define.moduleMap; // eslint-disable-line no-undef
var moduleMap = define.moduleMap;
var symbolList;
var deprecationMap;
@@ -1378,12 +1353,9 @@ function prepareNamespace (symbolPath, context) {
if (!symbolPath) {
return context;
}
var parts = symbolPath.split('.');
var cur = context;
for (var i = 0, part; part = parts[i]; ++i) { // eslint-disable-line no-cond-assign
cur = cur[part] = cur[part] || {};
}
return cur;
return symbolPath.split('.').reduce(function (cur, part) {
return (cur[part] = cur[part] || {});
}, context);
}
exports.mapModules = function (context) {
@@ -1442,11 +1414,11 @@ var lastResumeEvent = null;
module.exports = {
id: 'android',
bootstrap: function() {
var channel = require('cordova/channel'),
cordova = require('cordova'),
exec = require('cordova/exec'),
modulemapper = require('cordova/modulemapper');
bootstrap: function () {
var channel = require('cordova/channel');
var cordova = require('cordova');
var exec = require('cordova/exec');
var modulemapper = require('cordova/modulemapper');
// Get the shared secret needed to use the bridge.
exec.init();
@@ -1458,21 +1430,21 @@ module.exports = {
// Inject a listener for the backbutton on the document.
var backButtonChannel = cordova.addDocumentEventHandler('backbutton');
backButtonChannel.onHasSubscribersChange = function() {
backButtonChannel.onHasSubscribersChange = function () {
// If we just attached the first handler or detached the last handler,
// let native know we need to override the back button.
exec(null, null, APP_PLUGIN_NAME, "overrideBackbutton", [this.numHandlers == 1]);
exec(null, null, APP_PLUGIN_NAME, 'overrideBackbutton', [this.numHandlers === 1]);
};
// Add hardware MENU and SEARCH button handlers
cordova.addDocumentEventHandler('menubutton');
cordova.addDocumentEventHandler('searchbutton');
function bindButtonChannel(buttonName) {
function bindButtonChannel (buttonName) {
// generic button bind used for volumeup/volumedown buttons
var volumeButtonChannel = cordova.addDocumentEventHandler(buttonName + 'button');
volumeButtonChannel.onHasSubscribersChange = function() {
exec(null, null, APP_PLUGIN_NAME, "overrideButton", [buttonName, this.numHandlers == 1]);
volumeButtonChannel.onHasSubscribersChange = function () {
exec(null, null, APP_PLUGIN_NAME, 'overrideButton', [buttonName, this.numHandlers === 1]);
};
}
// Inject a listener for the volume buttons on the document.
@@ -1484,7 +1456,7 @@ module.exports = {
// plugin result is delivered even after the event is fired (CB-10498)
var cordovaAddEventListener = document.addEventListener;
document.addEventListener = function(evt, handler, capture) {
document.addEventListener = function (evt, handler, capture) {
cordovaAddEventListener(evt, handler, capture);
if (evt === 'resume' && lastResumeEvent) {
@@ -1494,51 +1466,48 @@ module.exports = {
// Let native code know we are all done on the JS side.
// Native code will then un-hide the WebView.
channel.onCordovaReady.subscribe(function() {
channel.onCordovaReady.subscribe(function () {
exec(onMessageFromNative, null, APP_PLUGIN_NAME, 'messageChannel', []);
exec(null, null, APP_PLUGIN_NAME, "show", []);
exec(null, null, APP_PLUGIN_NAME, 'show', []);
});
}
};
function onMessageFromNative(msg) {
function onMessageFromNative (msg) {
var cordova = require('cordova');
var action = msg.action;
switch (action)
{
// Button events
case 'backbutton':
case 'menubutton':
case 'searchbutton':
// App life cycle events
case 'pause':
// Volume events
case 'volumedownbutton':
case 'volumeupbutton':
cordova.fireDocumentEvent(action);
break;
case 'resume':
if(arguments.length > 1 && msg.pendingResult) {
if(arguments.length === 2) {
msg.pendingResult.result = arguments[1];
} else {
// The plugin returned a multipart message
var res = [];
for(var i = 1; i < arguments.length; i++) {
res.push(arguments[i]);
}
msg.pendingResult.result = res;
switch (action) {
// pause and resume are Android app life cycle events
case 'backbutton':
case 'menubutton':
case 'searchbutton':
case 'pause':
case 'volumedownbutton':
case 'volumeupbutton':
cordova.fireDocumentEvent(action);
break;
case 'resume':
if (arguments.length > 1 && msg.pendingResult) {
if (arguments.length === 2) {
msg.pendingResult.result = arguments[1];
} else {
// The plugin returned a multipart message
var res = [];
for (var i = 1; i < arguments.length; i++) {
res.push(arguments[i]);
}
// Save the plugin result so that it can be delivered to the js
// even if they miss the initial firing of the event
lastResumeEvent = msg;
msg.pendingResult.result = res;
}
cordova.fireDocumentEvent(action, msg);
break;
default:
throw new Error('Unknown event action ' + action);
// Save the plugin result so that it can be delivered to the js
// even if they miss the initial firing of the event
lastResumeEvent = msg;
}
cordova.fireDocumentEvent(action, msg);
break;
default:
throw new Error('Unknown event action ' + action);
}
}
@@ -1554,8 +1523,8 @@ module.exports = {
/**
* Clear the resource cache.
*/
clearCache:function() {
exec(null, null, APP_PLUGIN_NAME, "clearCache", []);
clearCache: function () {
exec(null, null, APP_PLUGIN_NAME, 'clearCache', []);
},
/**
@@ -1572,31 +1541,31 @@ module.exports = {
* Example:
* navigator.app.loadUrl("http://server/myapp/index.html", {wait:2000, loadingDialog:"Wait,Loading App", loadUrlTimeoutValue: 60000});
*/
loadUrl:function(url, props) {
exec(null, null, APP_PLUGIN_NAME, "loadUrl", [url, props]);
loadUrl: function (url, props) {
exec(null, null, APP_PLUGIN_NAME, 'loadUrl', [url, props]);
},
/**
* Cancel loadUrl that is waiting to be loaded.
*/
cancelLoadUrl:function() {
exec(null, null, APP_PLUGIN_NAME, "cancelLoadUrl", []);
cancelLoadUrl: function () {
exec(null, null, APP_PLUGIN_NAME, 'cancelLoadUrl', []);
},
/**
* Clear web history in this web view.
* Instead of BACK button loading the previous web page, it will exit the app.
*/
clearHistory:function() {
exec(null, null, APP_PLUGIN_NAME, "clearHistory", []);
clearHistory: function () {
exec(null, null, APP_PLUGIN_NAME, 'clearHistory', []);
},
/**
* Go to previous page displayed.
* This is the same as pressing the backbutton on Android device.
*/
backHistory:function() {
exec(null, null, APP_PLUGIN_NAME, "backHistory", []);
backHistory: function () {
exec(null, null, APP_PLUGIN_NAME, 'backHistory', []);
},
/**
@@ -1608,8 +1577,8 @@ module.exports = {
*
* @param override T=override, F=cancel override
*/
overrideBackbutton:function(override) {
exec(null, null, APP_PLUGIN_NAME, "overrideBackbutton", [override]);
overrideBackbutton: function (override) {
exec(null, null, APP_PLUGIN_NAME, 'overrideBackbutton', [override]);
},
/**
@@ -1623,15 +1592,15 @@ module.exports = {
* @param button volumeup, volumedown
* @param override T=override, F=cancel override
*/
overrideButton:function(button, override) {
exec(null, null, APP_PLUGIN_NAME, "overrideButton", [button, override]);
overrideButton: function (button, override) {
exec(null, null, APP_PLUGIN_NAME, 'overrideButton', [button, override]);
},
/**
* Exit and terminate the application.
*/
exitApp:function() {
return exec(null, null, APP_PLUGIN_NAME, "exitApp", []);
exitApp: function () {
return exec(null, null, APP_PLUGIN_NAME, 'exitApp', []);
}
};
@@ -1656,11 +1625,11 @@ exports.injectScript = function (url, onload, onerror) {
function injectIfNecessary (id, url, onload, onerror) {
onerror = onerror || onload;
if (id in define.moduleMap) { // eslint-disable-line no-undef
if (id in define.moduleMap) {
onload();
} else {
exports.injectScript(url, function () {
if (id in define.moduleMap) { // eslint-disable-line no-undef
if (id in define.moduleMap) {
onload();
} else {
onerror();
@@ -1671,7 +1640,7 @@ function injectIfNecessary (id, url, onload, onerror) {
function onScriptLoadingComplete (moduleList, finishPluginLoading) {
// Loop through all the plugins and then through their clobbers and merges.
for (var i = 0, module; module = moduleList[i]; i++) { // eslint-disable-line no-cond-assign
for (var i = 0, module; (module = moduleList[i]); i++) {
if (module.clobbers && module.clobbers.length) {
for (var j = 0; j < module.clobbers.length; j++) {
modulemapper.clobbers(module.id, module.clobbers[j]);
@@ -1854,10 +1823,11 @@ utils.clone = function (obj) {
retVal = {};
for (i in obj) {
// https://issues.apache.org/jira/browse/CB-11522 'unknown' type may be returned in
// custom protocol activation case on Windows Phone 8.1 causing "No such interface supported" exception
// on cloning.
if ((!(i in retVal) || retVal[i] !== obj[i]) && typeof obj[i] !== 'undefined' && typeof obj[i] !== 'unknown') { // eslint-disable-line valid-typeof
// 'unknown' type may be returned in custom protocol activation case on
// Windows Phone 8.1 causing "No such interface supported" exception on
// cloning (https://issues.apache.org/jira/browse/CB-11522)
// eslint-disable-next-line valid-typeof
if ((!(i in retVal) || retVal[i] !== obj[i]) && typeof obj[i] !== 'undefined' && typeof obj[i] !== 'unknown') {
retVal[i] = utils.clone(obj[i]);
}
}
@@ -1907,7 +1877,6 @@ utils.extend = (function () {
var F = function () {};
// extend Child from Parent
return function (Child, Parent) {
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.__super__ = Parent.prototype;

View File

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

View File

@@ -0,0 +1,22 @@
/* 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,13 +20,17 @@ 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 {
repositories {
google()
jcenter()
}
apply from: 'repositories.gradle'
repositories repos
dependencies {
// The gradle plugin and the maven plugin have to be updated after each version of Android
@@ -38,10 +42,8 @@ buildscript {
}
allprojects {
repositories {
google()
jcenter()
}
apply from: 'repositories.gradle'
repositories repos
}
apply plugin: 'com.android.library'
@@ -49,7 +51,7 @@ apply plugin: 'com.github.dcendents.android-maven'
apply plugin: 'com.jfrog.bintray'
group = 'org.apache.cordova'
version = '9.0.0-dev'
version = '9.1.0'
android {
compileSdkVersion cdvCompileSdkVersion
@@ -60,9 +62,9 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
// For the Android Cordova Lib, we will hardcode the minSdkVersion and not allow changes.
// For the Android Cordova Lib, we allow changing the minSdkVersion, but it is at the users own risk
defaultConfig {
minSdkVersion 22
minSdkVersion cdvMinSdkVersion
}
sourceSets {
@@ -140,9 +142,9 @@ bintray {
licenses = ['Apache-2.0']
labels = ['android', 'cordova', 'phonegap']
version {
name = '9.0.0-dev'
released = new Date()
vcsTag = '9.0.0-dev'
name = '9.1.0'
released = new Date()
vcsTag = '9.1.0'
}
}
}

View File

@@ -0,0 +1,22 @@
/* 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,6 +125,9 @@ 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);
@@ -321,22 +324,26 @@ 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) {
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);
setImmersiveUiVisibility();
}
}
@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,7 +114,6 @@ 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,6 +41,7 @@ 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:
@@ -286,13 +287,19 @@ 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 = conn.getInputStream();
InputStream inputStream;
if ("gzip".equals(conn.getContentEncoding())) {
inputStream = new GZIPInputStream(conn.getInputStream());
} else {
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.0.0-dev";
public static final String CORDOVA_VERSION = "9.1.0";
void init(CordovaInterface cordova, List<PluginEntry> pluginEntries, CordovaPreferences preferences);

View File

@@ -19,6 +19,7 @@
package org.apache.cordova;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
@@ -34,6 +35,7 @@ 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;
@@ -176,22 +178,28 @@ public class CordovaWebViewImpl implements CordovaWebView {
e.printStackTrace();
}
// If timeout, then stop loading and handle error
if (loadUrlTimeout == currentLoadUrlTimeout) {
// If timeout, then stop loading and handle error (if activity still exists)
if (loadUrlTimeout == currentLoadUrlTimeout && cordova.getActivity() != null) {
cordova.getActivity().runOnUiThread(loadError);
} else if (cordova.getActivity() == null) {
LOG.d(TAG, "Cordova activity does not exist.");
}
}
};
final boolean _recreatePlugins = recreatePlugins;
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
if (loadUrlTimeoutValue > 0) {
cordova.getThreadPool().execute(timeoutCheck);
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);
}
engine.loadUrl(url, _recreatePlugins);
}
});
});
} else {
LOG.d(TAG, "Cordova activity does not exist.");
}
}
@@ -226,21 +234,37 @@ 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 {
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));
if (url.startsWith("intent://")) {
intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
} else {
intent.setData(uri);
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);
}
cordova.getActivity().startActivity(intent);
} catch (android.content.ActivityNotFoundException e) {
LOG.e(TAG, "Error loading url " + url, e);
}
}
@@ -255,7 +279,12 @@ public class CordovaWebViewImpl implements CordovaWebView {
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
return engine.getView().dispatchKeyEvent(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;
}
}
@@ -553,11 +582,15 @@ public class CordovaWebViewImpl implements CordovaWebView {
public void run() {
try {
Thread.sleep(2000);
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
pluginManager.postMessage("spinner", "stop");
}
});
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.");
}
} catch (InterruptedException e) {
}
}

View File

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

View File

@@ -152,6 +152,15 @@ 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);

1201
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.0.0",
"version": "9.1.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",
"lint": "eslint . \"bin/**/!(*.*|gitignore)\"",
"clean-tests": "node bin/clean_test.js"
"clean:java-unit-tests": "node test/clean.js"
},
"author": "Apache Software Foundation",
"license": "Apache-2.0",
@@ -28,9 +28,12 @@
"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": {
@@ -41,9 +44,8 @@
"rewire": "^5.0.0"
},
"engines": {
"node": ">=10.0.0"
"node": ">=10.10.0"
},
"engineStrict": true,
"nyc": {
"include": [
"bin/lib/**",

View File

@@ -1,149 +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.
*/
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: true
displayStacktrace: 'raw'
}
}));

View File

@@ -20,13 +20,15 @@
const CordovaError = require('cordova-common').CordovaError;
const rewire = require('rewire');
describe('Adb', () => {
const adbOutput = `List of devices attached
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];
emulator-5556\toffline
123a76565509e124\tdevice
123a76565509e123\tbootloader
`;
describe('Adb', () => {
const deviceId = '123a76565509e124';
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]';
@@ -41,40 +43,15 @@ emulator-5554\tdevice
Adb.__set__('execa', execaSpy);
});
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 the IDs of all fully booted devices & emulators', () => {
execaSpy.and.resolveTo({ stdout: adbOutput });
it('should return only devices if no options are specified', () => {
return Adb.devices().then(devices => {
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);
expect(devices).toEqual([
'emulator-5554',
'123a76565509e124'
]);
});
});
});

View File

@@ -20,51 +20,44 @@
var os = require('os');
var path = require('path');
var common = require('cordova-common');
var rewire = require('rewire');
const EventEmitter = require('events');
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('addPlugin method', function () {
var api, Api, gradleBuilder;
describe('Api', () => {
describe('addPlugin method', function () {
var api;
beforeEach(function () {
Api = rewire('../../bin/templates/cordova/Api');
beforeEach(function () {
var pluginManager = jasmine.createSpyObj('pluginManager', ['addPlugin']);
pluginManager.addPlugin.and.resolveTo();
spyOn(common.PluginManager, 'get').and.returnValue(pluginManager);
var pluginManager = jasmine.createSpyObj('pluginManager', ['addPlugin']);
pluginManager.addPlugin.and.resolveTo();
spyOn(common.PluginManager, 'get').and.returnValue(pluginManager);
var projectSpy = jasmine.createSpyObj('AndroidProject', ['getPackageName', 'write', 'isClean']);
spyOn(AndroidProject, 'getProjectFile').and.returnValue(projectSpy);
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();
api = new Api('android', FAKE_PROJECT_DIR, new EventEmitter());
spyOn(api._builder, 'prepBuildFiles');
});
});
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();
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();
});
});
});
});

View File

@@ -289,7 +289,7 @@ describe('ProjectBuilder', () => {
});
});
describe('fileSorter', () => {
describe('outputFileComparator', () => {
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__('fileSorter'));
const sortedApks = apkArray.sort(ProjectBuilder.__get__('outputFileComparator'));
expect(sortedApks).toEqual(expectedResult);
});

View File

@@ -18,26 +18,54 @@
*/
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.create(process.env);
original_env = Object.assign({}, process.env);
});
afterEach(function () {
Object.keys(original_env).forEach(function (k) {
process.env[k] = original_env[k];
// 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' });
});
});
describe('check_android', function () {
describe('find and set ANDROID_HOME when ANDROID_HOME and ANDROID_SDK_ROOT is not set', function () {
beforeEach(function () {
@@ -55,9 +83,6 @@ 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', () => {
@@ -66,8 +91,6 @@ 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;
});
});
});
@@ -220,9 +243,6 @@ 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');
@@ -243,20 +263,20 @@ describe('check_reqs', function () {
});
});
it('with ANDROID_SDK_ROOT / without ANDROID_HOME', () => {
it('with ANDROID_SDK_ROOT / without ANDROID_HOME', async () => {
process.env.ANDROID_SDK_ROOT = '/android/sdk/root';
expectAsync(check_reqs.check_gradle()).toBeResolvedTo('/android/sdk/root/bin/gradle');
await expectAsync(check_reqs.check_gradle()).toBeResolvedTo('/android/sdk/root/bin/gradle');
});
it('with ANDROID_SDK_ROOT / with ANDROID_HOME', () => {
it('with ANDROID_SDK_ROOT / with ANDROID_HOME', async () => {
process.env.ANDROID_SDK_ROOT = '/android/sdk/root';
process.env.ANDROID_HOME = '/android/sdk/home';
expectAsync(check_reqs.check_gradle()).toBeResolvedTo('/android/sdk/root/bin/gradle');
await expectAsync(check_reqs.check_gradle()).toBeResolvedTo('/android/sdk/root/bin/gradle');
});
it('without ANDROID_SDK_ROOT / with ANDROID_HOME', () => {
it('without ANDROID_SDK_ROOT / with ANDROID_HOME', async () => {
process.env.ANDROID_HOME = '/android/sdk/home';
expectAsync(check_reqs.check_gradle()).toBeResolvedTo('/android/sdk/home/bin/gradle');
await expectAsync(check_reqs.check_gradle()).toBeResolvedTo('/android/sdk/home/bin/gradle');
});
it('without ANDROID_SDK_ROOT / without ANDROID_HOME', () => {
@@ -348,6 +368,8 @@ 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/lib/utils');
var utils = require('../../bin/templates/cordova/lib/utils');
var create = rewire('../../bin/lib/create');
var check_reqs = require('../../bin/templates/cordova/lib/check_reqs');
var fs = require('fs-extra');
@@ -132,6 +132,7 @@ 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');
@@ -300,4 +301,24 @@ 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);
});
});
});

View File

@@ -1,233 +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').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,7 +26,6 @@ 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(() => {
@@ -205,13 +204,13 @@ describe('emulator', () => {
});
describe('list_started', () => {
it('should call adb devices with the emulators flag', () => {
it('should return a list of all online emulators', () => {
const AdbSpy = jasmine.createSpyObj('Adb', ['devices']);
AdbSpy.devices.and.returnValue(Promise.resolve());
AdbSpy.devices.and.resolveTo(['emulator-5556', '123a76565509e124']);
emu.__set__('Adb', AdbSpy);
return emu.list_started().then(() => {
expect(AdbSpy.devices).toHaveBeenCalledWith({ emulators: true });
return emu.list_started().then(emus => {
expect(emus).toEqual(['emulator-5556']);
});
});
});
@@ -529,158 +528,4 @@ 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}`);
});
});
});
});

177
spec/unit/java.spec.js Normal file
View File

@@ -0,0 +1,177 @@
/**
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,6 +21,7 @@ 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');
@@ -35,25 +36,27 @@ 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.writeFileSync(non_plugin_file, 'contents', 'utf-8');
fs.outputFileSync(non_plugin_file, 'contents');
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.ensureDirSync(java_dir);
fs.writeFileSync(java_file, 'contents', 'utf-8');
fs.outputFileSync(java_file, 'contents');
// This will fail on windows if not admin - ignore the error in that case.
if (ignoreEPERMonWin32(java_file, symlink_file)) {
@@ -61,12 +64,11 @@ 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.writeFileSync(non_plugin_file, 'contents', 'utf-8');
fs.outputFileSync(non_plugin_file, 'contents');
// This will fail on windows if not admin - ignore the error in that case.
if (ignoreEPERMonWin32(non_plugin_file, symlink_file)) {
@@ -75,20 +77,16 @@ 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.ensureDirSync(java_dir);
fs.writeFileSync(java_file, 'contents', 'utf-8');
fs.outputFileSync(java_file, 'contents');
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.ensureDirSync(java_dir);
fs.writeFileSync(java_file, 'contents', 'utf-8');
fs.outputFileSync(java_file, 'contents');
var s = spyOn(fs, 'ensureDirSync').and.callThrough();
var resolvedDest = path.resolve(project_dir, dest);
@@ -97,12 +95,10 @@ 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.ensureDirSync(java_dir);
fs.writeFileSync(java_file, 'contents', 'utf-8');
fs.outputFileSync(java_file, 'contents');
var s = spyOn(fs, 'copySync').and.callThrough();
var resolvedDest = path.resolve(project_dir, dest);
@@ -111,8 +107,15 @@ describe('common platform handler', function () {
expect(s).toHaveBeenCalled();
expect(s).toHaveBeenCalledWith(java_file, resolvedDest);
});
fs.removeSync(project_dir);
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();
});
});
@@ -121,18 +124,12 @@ 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.ensureDirSync(java_dir);
fs.writeFileSync(java_file, 'contents', 'utf-8');
});
afterEach(function () {
fs.removeSync(java_dir);
fs.outputFileSync(java_file, 'contents');
});
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,195 +18,66 @@
*/
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('getInstallTarget', () => {
const targetOpts = { target: 'emu' };
const deviceOpts = { device: true };
const emulatorOpts = { emulator: true };
const emptyOpts = {};
describe('buildTargetSpec', () => {
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();
const buildTargetSpec = run.__get__('buildTargetSpec');
expect(buildTargetSpec({ target: 'emu' })).toEqual({ id: 'emu' });
expect(buildTargetSpec({ device: true })).toEqual({ type: 'device' });
expect(buildTargetSpec({ emulator: true })).toEqual({ type: 'emulator' });
expect(buildTargetSpec({})).toEqual({});
});
});
describe('run method', () => {
let deviceSpyObj;
let emulatorSpyObj;
let eventsSpyObj;
let getInstallTargetSpy;
let targetSpyObj, emulatorSpyObj, resolvedTarget;
beforeEach(() => {
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');
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();
run.__set__({
device: deviceSpyObj,
emulator: emulatorSpyObj,
events: eventsSpyObj,
getInstallTarget: getInstallTargetSpy
target: targetSpyObj,
emulator: emulatorSpyObj
});
// run needs `this` to behave like an Api instance
run.run = run.run.bind({
_builder: builders.getBuilder('FakeRootPath')
});
});
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));
it('should install on target after build', () => {
return run.run().then(() => {
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' });
expect(targetSpyObj.install).toHaveBeenCalledWith(
resolvedTarget,
{ apkPaths: [], buildType: 'debug' }
);
});
});
it('should fail with the error message if --packageType=bundle setting is used', () => {
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.')
);
targetSpyObj.resolve.and.resolveTo(resolvedTarget);
return expectAsync(run.run({ argv: ['--packageType=bundle'] }))
.toBeRejectedWith(jasmine.stringMatching(/Package type "bundle" is not supported/));
});
});

313
spec/unit/target.spec.js Normal file
View File

@@ -0,0 +1,313 @@
/**
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,6 +45,8 @@ 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;
@@ -71,19 +73,19 @@ public class BackButtonMultipageTest {
webInterface.sendJavascript("window.location = 'sample2.html';");
}
});
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
assertPageSample(SAMPLE2_URL);
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
webInterface.sendJavascript("window.location = 'sample3.html';");
}
});
assertEquals("file:///android_asset/www/backbuttonmultipage/sample3.html", mActivity.onPageFinishedUrl.take());
assertPageSample(SAMPLE3_URL);
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
assertTrue(webInterface.backHistory());
}
});
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
assertPageSample(SAMPLE2_URL);
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
assertTrue(webInterface.backHistory());
@@ -104,22 +106,22 @@ public class BackButtonMultipageTest {
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
webInterface.loadUrl("file:///android_asset/www/backbuttonmultipage/sample2.html");
webInterface.loadUrl(SAMPLE2_URL);
}
});
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
assertPageSample(SAMPLE2_URL);
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
webInterface.loadUrl("file:///android_asset/www/backbuttonmultipage/sample3.html");
webInterface.loadUrl(SAMPLE3_URL);
}
});
assertEquals("file:///android_asset/www/backbuttonmultipage/sample3.html", mActivity.onPageFinishedUrl.take());
assertPageSample(SAMPLE3_URL);
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
assertTrue(webInterface.backHistory());
}
});
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
assertPageSample(SAMPLE2_URL);
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
assertTrue(webInterface.backHistory());
@@ -140,19 +142,24 @@ public class BackButtonMultipageTest {
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
webInterface.loadUrl("file:///android_asset/www/backbuttonmultipage/sample2.html");
webInterface.loadUrl(SAMPLE2_URL);
}
});
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
assertPageSample(SAMPLE2_URL);
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
webInterface.loadUrl("file:///android_asset/www/backbuttonmultipage/sample3.html");
webInterface.loadUrl(SAMPLE3_URL);
}
});
assertEquals("file:///android_asset/www/backbuttonmultipage/sample3.html", mActivity.onPageFinishedUrl.take());
assertPageSample(SAMPLE3_URL);
onView(withId(WEBVIEW_ID)).perform(pressBack());
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
assertPageSample(SAMPLE2_URL);
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,6 +45,8 @@ 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;
@@ -71,19 +73,19 @@ public class BackButtonMultipageTest {
webInterface.sendJavascript("window.location = 'sample2.html';");
}
});
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
assertPageSample(SAMPLE2_URL);
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
webInterface.sendJavascript("window.location = 'sample3.html';");
}
});
assertEquals("file:///android_asset/www/backbuttonmultipage/sample3.html", mActivity.onPageFinishedUrl.take());
assertPageSample(SAMPLE3_URL);
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
assertTrue(webInterface.backHistory());
}
});
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
assertPageSample(SAMPLE2_URL);
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
assertTrue(webInterface.backHistory());
@@ -104,22 +106,22 @@ public class BackButtonMultipageTest {
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
webInterface.loadUrl("file:///android_asset/www/backbuttonmultipage/sample2.html");
webInterface.loadUrl(SAMPLE2_URL);
}
});
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
assertPageSample(SAMPLE2_URL);
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
webInterface.loadUrl("file:///android_asset/www/backbuttonmultipage/sample3.html");
webInterface.loadUrl(SAMPLE3_URL);
}
});
assertEquals("file:///android_asset/www/backbuttonmultipage/sample3.html", mActivity.onPageFinishedUrl.take());
assertPageSample(SAMPLE3_URL);
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
assertTrue(webInterface.backHistory());
}
});
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
assertPageSample(SAMPLE2_URL);
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
assertTrue(webInterface.backHistory());
@@ -140,19 +142,23 @@ public class BackButtonMultipageTest {
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
webInterface.loadUrl("file:///android_asset/www/backbuttonmultipage/sample2.html");
webInterface.loadUrl(SAMPLE2_URL);
}
});
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
assertPageSample(SAMPLE2_URL);
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
webInterface.loadUrl("file:///android_asset/www/backbuttonmultipage/sample3.html");
webInterface.loadUrl(SAMPLE3_URL);
}
});
assertEquals("file:///android_asset/www/backbuttonmultipage/sample3.html", mActivity.onPageFinishedUrl.take());
assertPageSample(SAMPLE3_URL);
onView(withId(WEBVIEW_ID)).perform(pressBack());
assertEquals("file:///android_asset/www/backbuttonmultipage/sample2.html", mActivity.onPageFinishedUrl.take());
assertPageSample(SAMPLE2_URL);
onView(withId(WEBVIEW_ID)).perform(pressBack());
assertEquals(START_URL, mActivity.onPageFinishedUrl.take());
}
private void assertPageSample(String url) {
assertEquals(url, mActivity.onPageFinishedUrl.take());
}
}

View File

@@ -17,12 +17,16 @@
under the License.
*/
const fs = require('fs');
const fs = require('fs-extra');
const path = require('path');
if (fs.existsSync('./test/gradlew')) {
fs.unlinkSync('./test/gradlew');
}
/**
* 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.
*/
if (fs.existsSync('./test/gradlew.bat')) {
fs.unlinkSync('./test/gradlew.bat');
for (const variant of ['android', 'androidx']) {
for (const file of ['gradlew', 'gradlew.bat']) {
fs.removeSync(path.join(__dirname, variant, file));
}
}