Compare commits

...

189 Commits

Author SHA1 Message Date
João Gonçalves
eaca570cb6 fix: opaque navigation bar in edge to edge (#1867) 2025-12-19 20:58:02 +09:00
Norman Breau
76bac55fba fix: cordova requirements command and SDK lookup based on tools (#1877)
* fix: cordova requirements command and SDK lookup based on tools

* Update spec/unit/AndroidCommandLineTools.spec.js

Co-authored-by: エリス <erisu@users.noreply.github.com>

* Update lib/env/AndroidCommandLineTools.js

Co-authored-by: エリス <erisu@users.noreply.github.com>

* Update lib/env/AndroidCommandLineTools.js

Co-authored-by: エリス <erisu@users.noreply.github.com>

* Update lib/env/AndroidCommandLineTools.js

---------

Co-authored-by: エリス <erisu@users.noreply.github.com>
2025-12-18 09:23:01 -04:00
Manuel Beck
6b76757c80 CallbackContext: Add success method for boolean (#1864)
- Add helper method for success callbacks that returns `Status.OK` with a boolean
2025-12-17 10:31:26 +01:00
Norman Breau
fb562f4ed0 fix(windows): Escape back-slashes for gradle config jdk path (#1876) 2025-12-16 22:31:10 -04:00
Manuel Beck
eaf875b0d6 chore(readme): added nightly build section (#1873)
Added section on installing nightly builds of Cordova Android for testing purposes, with warnings about stability and production use.
2025-12-04 00:52:18 +01:00
エリス
d7afba0ad0 chore: update release audit workflow & license headers (#1870) 2025-11-23 13:04:10 +09:00
dependabot[bot]
ebe6890dee chore(deps-dev): bump js-yaml from 4.1.0 to 4.1.1 (#1869)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-18 18:41:35 -08:00
dependabot[bot]
172b8448af chore(deps): bump glob from 10.4.5 to 10.5.0 (#1868)
Bumps [glob](https://github.com/isaacs/node-glob) from 10.4.5 to 10.5.0.
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v10.4.5...v10.5.0)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 10.5.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-18 18:19:35 -08:00
エリス
655aa0a5fb fix: re-expose and support pollOnce (#1854) 2025-10-27 12:40:49 +09:00
エリス
52578ae770 fix(statusbar): inject script block to compute color & replace padStart w/ logic supported on SDK 24 (#1853) 2025-10-27 10:46:24 +09:00
João Gonçalves
360be21ea5 fix: edge to edge/fullscreen margins (#1847)
Ensures that when in edge to edge or fullscreen modes the values for
margin left and right are 0

Co-authored-by: Kepa Totorica <kepa.baum.totorica@outsystems.com>
2025-10-09 11:47:23 +09:00
André Sousa
5bca218e5b feat: Allows additional settings to be included on settings.gradle (#1819) 2025-09-27 02:24:26 +09:00
Manuel Beck
eb5fe4fbda fix: Deprecation warning in PluginManager for using Class.newInstance (#1823)
- Calling Class.newInstance is deprecated. Instead Class.getDeclaredConstructor().newInstance() should be called
2025-09-27 02:23:24 +09:00
Norman Breau
46af3114b8 fix: Back button override on API 36+ (#1831) 2025-09-27 02:22:22 +09:00
エリス
76aa938002 feat!: support previous non-E2E (#1817)
* feat: support previous non-e2e (add FrameLayout wrapper)
* feat: implement internal SystemBar plugin
* feat: implement StatusBar plugin JS API (SystemBarPlugin)
* feat!: force custom statusbarView for all SDKs
* chore: various cleanup, refactors, fixes, and docs from recent changes
* feat: use getComputedStyle for setBackgroundColor
* chore: suppress deprecation warnings for method using setNavigationBarColor
* chore: return null when rootView is null
* fix: setOnApplyWindowInsetsListener to return insets
* fix: setting appearance when e2e is enabled
* fix: set statusBarColor to transparent, use new statusBar UI
2025-09-27 02:21:17 +09:00
エリス
7d7f511023 feat: replace nyc with c8 (#1844) 2025-09-12 00:46:39 +09:00
エリス
488c498733 chore!: update dependencies (#1843)
* chore!: bump rewire@9.0.1
* chore: bump jasmine@5.10.0
* chore: bump dedent@1.7.0
* chore: tmp@0.2.5
* chore: rebuilt package-lock.json
* ci: update workflow dependencies
2025-09-11 17:42:38 +09:00
エリス
e4457f7fdb chore!: update template defaults (#1837)
* chore: update default package id
* chore: sync cordova-app-hello-world defaults & modified for Android differences
2025-08-19 13:55:18 +09:00
エリス
8742cfe4a6 chore(npm)!: bump cordova-common@6.0.0 w/ rebuilt package-lock.json (#1835)
* chore(npm)!: bump cordova-common@6.0.0
* chore(npm): rebuilt package-lock.json
2025-08-09 13:01:06 +09:00
dependabot[bot]
af1ae68a97 chore(deps-dev): bump tmp from 0.2.3 to 0.2.4 (#1834)
Bumps [tmp](https://github.com/raszi/node-tmp) from 0.2.3 to 0.2.4.
- [Changelog](https://github.com/raszi/node-tmp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/raszi/node-tmp/compare/v0.2.3...v0.2.4)

---
updated-dependencies:
- dependency-name: tmp
  dependency-version: 0.2.4
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-08 12:52:32 +09:00
エリス
56afb70894 dep(npm): bump @cordova/eslint-config@6.0.0 (#1830) 2025-07-25 23:14:25 +09:00
エリス
1204a79378 ci: use macos-15 (#1829) 2025-07-25 01:16:44 +09:00
エリス
bf0ba3dd5e ci(workflow): update release-audit & license config (#1828) 2025-07-20 12:29:07 +09:00
エリス
c2cf589d84 feat: allow disabling splash screen for embedded Cordova (#1824) 2025-07-16 14:34:03 +09:00
Norman Breau
0190d2e33a feat: AndroidShowDeprecations preference flag (#1822) 2025-07-09 12:21:28 +09:00
Manuel Beck
df36c7a2c4 fix: gradle deprecation warnings about property assignment (#1821)
When building a plain Android Cordova app without plugins, there will be Gradle depraction warnings: `Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.`. Compiling it with `--warning-mode all` shows, that some properties are assigned without an `=`, which will not be supported any longer with Gradle 10.0.
2025-07-09 12:18:02 +09:00
stephan-kutzner
b79232612b fix: replace deprecated fs.F_OK with fs.constants.F_OK (#1820)
This addresses the warning DEP0176 introduces in Node.js v20.8.0
2025-07-08 01:41:36 -03:00
エリス
60d2842024 feat!: add partial night & day theme support (#1818)
* feat!: partial night & day theme support
* feat!: prefix core resource values files with cdv_
2025-07-07 22:31:54 +09:00
エリス
cab5c5b7ec fix: apply repositories.gradle for cordova.gradle dependencies (#1816) 2025-07-05 13:41:58 +09:00
エリス
484c60e483 test(plugins): remove old deprecated android_studio option (#1815) 2025-06-24 12:06:00 +09:00
エリス
72e7148884 feat!: bump kotlin@2.1.21 (#1814) 2025-06-24 11:58:49 +09:00
エリス
36bee66493 feat: androidx.appcompat:appcompat@1.7.1 (#1813) 2025-06-24 11:43:45 +09:00
エリス
4dcfc361d2 feat: androidx.webkit:webkit@1.14.0 (#1812) 2025-06-24 11:22:47 +09:00
エリス
c25ed27828 feat!: bump Gradle@8.14.2 & AGP@8.10.1 (#1811) 2025-06-24 10:57:25 +09:00
エリス
d8f6f37737 feat!: bump sdk & build tools to 36 (#1810) 2025-06-24 10:42:10 +09:00
エリス
f0e8885693 fix(plugins): remove from platformWWW by default (#1807) 2025-06-16 21:29:33 +09:00
エリス
5dc9c72821 fix(plugins): rename & match removeFileF logic with other platforms (#1806) 2025-06-16 21:29:02 +09:00
エリス
7a47fe01dc feat!: remove unused getASPath.bat file (#1808) 2025-06-16 13:13:39 +09:00
エリス
08b8a95416 fix(plugins): handle uninstalling multiple plugin assets (#1805) 2025-06-16 13:11:04 +09:00
エリス
6024465814 ci: update workflow - added node 24, permission scoping & codecov pinning (#1804) 2025-06-16 12:35:09 +09:00
エリス
6b8e819f21 ci(release-audit): use latest apache-rat-action (#1809) 2025-06-16 12:34:39 +09:00
エリス
00744c4f71 chore: bump 15.0.0-dev (#1803) 2025-06-16 12:34:19 +09:00
Erisu
872d98876e chore: bump version 14.0.2-dev 2025-04-24 13:05:52 +09:00
Erisu
855fab238c release(android-v14.0.1): updated version and RELEASENOTES.md 2025-04-24 12:50:39 +09:00
エリス
2ffe68ab17 fix: configure gradle java.home (#1795) 2025-04-02 19:10:56 +09:00
エリス
f697ca7dec fix(windows): get gradle path with which command (#1793) 2025-03-28 20:30:40 +09:00
Erisu
1d82a3b52f chore: bump version 14.0.1-dev 2025-03-23 18:46:43 +09:00
Erisu
688d2cf5ad release(android-v14.0.0): updated version and RELEASENOTES.md 2025-03-23 18:37:46 +09:00
エリス
f6e384a9ea fix: replace fs-extra.ensureFileSync with fs.writeFileSync (#1790) 2025-03-22 00:45:00 +09:00
Erisu
839f9b878b Revert "release(android-v14.0.0): updated version and RELEASENOTES.md"
This reverts commit 2258d33a72.
2025-03-20 12:52:07 +09:00
Erisu
d4eca414e3 Revert "chore: bump version 14.0.1-dev"
This reverts commit 5da9bd6d9d.
2025-03-20 12:51:45 +09:00
Erisu
5da9bd6d9d chore: bump version 14.0.1-dev 2025-03-19 11:26:04 +09:00
Erisu
2258d33a72 release(android-v14.0.0): updated version and RELEASENOTES.md 2025-03-19 10:56:30 +09:00
エリス
ca4caf3fc1 dep!: bump npm packages (#1788)
* dep: bump nyc@17.1.0
* dep!: bump which@5.0.0
* dep: bump semver@7.7.1
* dep: bump jasmine@5.6.0
* dep: bump android-versions@2.1.0
* dep: bump cordova-common@5.0.1
* dep: bump fast-glob@3.3.3
* dep!: bump nopt@8.1.0
* chore: rebuilt package-lock
2025-03-19 10:16:55 +09:00
エリス
7ab18487cf feat!: bump node engine requirement >=20.5.0 (#1789) 2025-03-19 10:15:25 +09:00
エリス
ff11f659f0 fix: copy gradle wrapper from tools to platform root dir (#1781) 2025-03-18 10:55:43 +09:00
エリス
aad36fe565 feat: bump gradle to 8.13 (#1785) 2025-03-18 10:54:49 +09:00
エリス
7f9529408b chore: add androidx build test to gitignore (#1786) 2025-03-17 10:59:43 +09:00
エリス
bb4f86e7b9 feat: add AndroidEdgeToEdge preference & theme flag (#1779) 2025-03-14 12:22:05 +09:00
エリス
d0b59863ac feat!: bump java default targets to 11 (#1784)
* feat!: bump java source, target & kotlin jvm target to default 11
* chore!: remove java <= 8 logic
* refactor: setting of kotlin's jvmTarget
2025-03-14 11:55:43 +09:00
エリス
7544fdf1ed chore: bump CordovaWebView version to 14.0.0-dev (#1782) 2025-03-04 00:05:00 +09:00
Andrii Kurdiumov
8f458b042b feat: Account for Node security patch (#1778)
As of https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2#command-injection-via-args-parameter-of-child_processspawn-without-shell-option-enabled-on-windows-cve-2024-27980---high

Cordova produce unrecognized error on Windows.

Fixes: https://github.com/apache/cordova-cli/issues/456

---------

Co-authored-by: Norman Breau <norman@breautek.com>
2025-02-18 20:53:18 +09:00
エリス
eb0f002112 style: update & resolve doc block warnings (#1774)
* style: resolve throw symbols (except InvalidArgumentException)
* style: resolve unknown symbol & reduce indention for PluginEntry
* fix: define IllegalArgumentException not InvalidArgumentException
2025-01-30 18:17:22 +09:00
エリス
e012478537 refactor: replace fs-extra with node:fs (#1772)
* spec: add devDependencies "tmp"
2025-01-29 10:39:11 +09:00
エリス
b623311efa feat!: deprecate CordovaPlugin's method initialize (#1771) 2025-01-28 12:28:32 +09:00
エリス
1f349f2984 fix: creation of cdv-gradle-config.json w/ --link flag (#1770) 2025-01-28 12:15:19 +09:00
エリス
9f5518000f refactor: prefix node:* (#1769)
* refactor: prefix node:* to path
* refactor: prefix node:* to os
* refactor: prefix node:* to fs
* refactor: prefix node:* to util
2025-01-28 12:13:36 +09:00
エリス
92116dee48 feat!: use kotlin-stdlib instead of kotlin-stdlib-jdk* (#1767) 2025-01-28 11:58:23 +09:00
エリス
34220ae0e3 feat: androidx.core:core-splashscreen@1.0.1 (#1768) 2025-01-28 11:57:42 +09:00
エリス
1fe44d71c5 feat: com.google.gms:google-services@4.4.2 (#1766) 2025-01-28 11:14:07 +09:00
エリス
58c2e3ae15 feat: androidx.webkit:webkit@1.12.1 (#1765) 2025-01-28 10:54:19 +09:00
エリス
ea045dee63 feat: androidx.appcompat:appcompat@1.7.0 (#1764) 2025-01-28 10:33:25 +09:00
エリス
cee7b0b8ac feat!: SDK 35 Support (#1763)
* feat(gradle)!: bump to 8.9 w/ AGP@8.7.3
* feat!: bump android sdk@35 & minimum-build-tool@35.0.0
2025-01-28 10:32:49 +09:00
エリス
6f0efd3a0d chore: bump 14.0.0-dev (#1762) 2025-01-28 10:32:04 +09:00
dependabot[bot]
dff2fc6331 chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 (#1748)
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-19 21:25:05 -08:00
Darryl Pogue
1347e48d14 chore(ci): Fix dependabot PR failures (#1750) 2024-11-19 20:55:50 -08:00
dependabot[bot]
5a2c50d1ed chore(deps): bump micromatch from 4.0.5 to 4.0.8 (#1736)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.5 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

---
updated-dependencies:
- dependency-name: micromatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-22 23:16:21 -07:00
Norman Breau
5eddc460e4 fix(docs): Incorrect JDK requirement stated in README (#1739) 2024-09-17 09:33:52 -03:00
5r1m
3503bfa31b Update AndroidManifest.xml by extending android:configChanges with "navigation" as application is restarted when BT keyboard is connected in some devices (#1718)
In some devices (especially pixel) connecting with BT keyboard is resulting in application being restarted. The existing "keyboard" seems to be not sufficient as in this case it was triggering "navigation" change.

https://stackoverflow.com/questions/25735227/bluetooth-keyboard-will-cause-activity-destroy-and-recreate/27238892#27238892
2024-06-24 08:43:22 -03:00
dependabot[bot]
d281727113 chore(deps): bump braces from 3.0.2 to 3.0.3 (#1716)
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-17 09:38:52 -07:00
Erisu
172e947d18 chore: bump version 13.0.1-dev 2024-05-15 19:45:51 +09:00
Erisu
2143045d4e release(android-v13.0.0): updated version and RELEASENOTES.md 2024-05-15 19:35:44 +09:00
エリス
3c5df42df5 dep: bump npm dependencies 20240515 (#1713)
* dep: bump android-versions@2.0.0
* dep: bump properties-parser@0.6.0
* dep: bump which@4.0.0
* chore: rebuilt package-lock w/ lockfileVersion 3
* dep(dev): bump @cordova/eslint-config@5.1.0
* dep: bump dedent@1.5.3
* dep: bump fast-glob@3.3.2
* dep: bump fs-extra@11.2.0
* dep(dev): bump jasmine@5.1.0
* dep(dev): bump nopt@7.2.1
* dep(dev): bump rewire@7.0.0
* dep: bump semver@7.6.2
* chore: rebuilt package-lock after package updates
2024-05-15 18:01:22 +09:00
エリス
c2f315c0ff feat: bump kotlin 1.9.24 & drop kotlin-android-extensions when kotlin >= 1.8.0 (#1543)
* feat: bump kotlin 1.9.24 & don't apply kotlin-android-extensions when kotlin >= 1.8.0
2024-05-13 23:32:49 +09:00
Norman Breau
89a0a72da5 feat!: API 34 Support (#1678)
* feat!: Upgrade to Gradle and AGP 8

* java 17

* feat!: API 34 Support

API 34: Upgrade AGP from 8.2.0-rc01 to 8.2.0-rc02

API 34: Upgrade AGP from 8.2.0-rc02 to 8.2.0-rc03

API 34: Upgrade AGP from 8.2.0-rc03 to 8.2.0

feat: add AndroidKotlinJVMTarget preference to set the kotlin JVM target
This is in addition to the java source / target compatibility preferences.
AndroidKotlinJVMTarget is only affective if Kotlin is enabled.

chore: Upgrade Gradle from 8.4 to 8.5

AGP 8.2.0 -> 8.2.1

Gradle 8.5 -> 8.7

fix: Add --validate-url to gradle wrapper commands

AGP 8.4.0

* fix(test): ProjectBuilder using Gradle 8.3, no longer supported version

* API 34: Change Kotlin JVM Target default.

The new default value is null. When null, it will by default
to the Java Target compatibility. Updating AndroidJavaTargetCompatibility
will also influence the Kotlin JVM target, unless if AndroidKotlinJVMTarget
is also explicitly defined.

* removed leftover debug prints

* API 34: Gradle Wrapper

* API 34: ratignore generated gradle wrapper files

* fix gradle wrapper jar via git attributes

* fix(test): normalise gradle paths

* fix(windows): Gradle paths

* fix(windows): Keep CRLF endings for bat files

* chore: Updated license for Gradle Wrapper 8.7 pointer

* API 34 Support Gradle Tools project

* API 34: omit --validate-url on installing the wrapper

* revert: LICENSE notice on bundling the gradle wrapper jar

* Revert: AGP 8.4 -> 8.3

* test(ci): Added NodeJS 22 to the test matrix

---------

Co-authored-by: jcesarmobile <jcesarmobile@gmail.com>
2024-05-13 10:28:57 -03:00
エリス
ed8e5d2f0a ci: Set up CodeQL analysis w/ fixes (#1711)
* ci: Set up CodeQL analysis
* spec: disable allowBackup in testing
* ci: do not check cordova.js - convered in cordova-js repo
* chore: add missing @Override annotation
2024-05-13 10:24:04 +09:00
Darryl Pogue
7fa4a65d0a feat(splash): Support SplashScreenBackgroundColor preference (#1700) 2024-05-09 15:57:04 +09:00
Ken Corbett
b773ae48f4 feat: add camera intent with file input capture (#1609)
* Allow image file input to be from camera
* Reverting some irrellevant formatting changes
* Removing the openFileChooser functions as they are no longer necessary
* Update templates/project/res/xml/opener_paths.xml
* Code review feedback
* Adding license to provider paths xml file
* Adding a comment describing the proper use of the core cordova file provider
* Adding the ability to query the android.media.action.IMAGE_CAPTURE intent action
* Only including a cache path
* Applying code review feedback

---------

Co-authored-by: Ken Corbett <ken@truepic.com>
Co-authored-by: エリス <erisu@users.noreply.github.com>
2024-05-09 13:30:49 +09:00
jcesarmobile
ebf0b105a3 feat: Add ResolveServiceWorkerRequests preference (#1696)
* feat: Add ResolveServiceWorkerRequests preference
* Change default to true
2024-05-08 20:53:09 +09:00
エリス
9261b29cf2 ci(release-audit): add license header and dependency checker (#1687)
* ci(release-audit): add license header and dependency checker
* chore: add Apache license headers
* chore: add new line to .ratignore
2024-05-08 15:07:52 +09:00
エリス
90e74befc7 chore: bump version 13.0.0-dev (#1708) 2024-05-08 01:51:15 +09:00
エリス
97806db463 ci: update codecov@v4 w/ token (#1703) 2024-04-13 18:31:55 +09:00
dependabot[bot]
4742358601 chore(deps-dev): bump @babel/traverse from 7.22.10 to 7.23.2 (#1677)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.10 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-20 17:36:12 -03:00
Erisu
e61e271f5b chore: bump version 12.0.2-dev 2023-08-23 18:39:28 +09:00
Erisu
0c805a0a8e release(android-v12.0.1): updated version and RELEASENOTES.md 2023-08-23 18:34:34 +09:00
エリス
a7cd4227a4 chore: rebuild package-lock w/ lint corrections (#1649)
* chore: rebuild package-lock.json
* chore: remew file starting new-line
2023-08-23 18:24:41 +09:00
Norman Breau
c9e7c59986 fix(GH-1616): Fix monochrome checks (#1632) 2023-07-13 13:18:54 -03:00
Erisu
94234d988e chore: bump version 12.0.1-dev 2023-05-20 01:13:49 +09:00
Erisu
b104554877 release(android-v12.0.0): updated version and RELEASENOTES.md 2023-05-20 00:47:09 +09:00
エリス
7da13ccf77 feat: add listTarget api (#1602)
* feat: add listTarget api
* test: write Platform API target list specs
2023-05-08 22:27:17 +09:00
Philip Peitsch
cb48147398 feat: add plugin hooks for WebViewClient.onRenderProcessGone (#1574)
* feat: add plugin hooks for WebViewClient.onRenderProcessGone

* Update framework/src/org/apache/cordova/CordovaPlugin.java

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

---------

Co-authored-by: Norman Breau <norman@nbsolutions.ca>
2023-04-23 23:20:48 -03:00
Norman Breau
6f6717afbd ci: Added NodeJS 20.x to the workflow matrix (#1607) 2023-04-22 17:01:24 -03:00
Norman Breau
3343c3bb34 fix: Gradle Args parsing (#1606)
* fix: Gradle Args parsing

* refactor: Applied ARGVParser.parseArgsStringToArgv -> parseArgsStringToArgv suggestion

* test: Added deeper testing for gradle argument parsing
2023-04-22 17:00:51 -03:00
Norman Breau
a62f699380 fix!: Make CoreAndroid plugin instantiate on load (#1605)
I don't anticipate breaking changes from this change, however it is a difference
in behaviour since CoreAndroid won't be lazily loaded, therefore I've marked this
commit has a breaking change.
2023-04-21 16:00:11 -03:00
喻志强
7efe90faac fix(BuildHelper): get package name from ApplicationInfo (#1575) 2023-04-14 10:15:55 -03:00
Norman Breau
5b546a27e6 deprecate: CoreAndroid.getBuildConfigValue (#1597) 2023-04-14 10:15:09 -03:00
Norman Breau
2252c09a49 feat: bump default kotlin to version 1.7.21 (#1594)
The rationale is based on the Gradle Compatibility Matrix: https://docs.gradle.org/current/userguide/compatibility.html

1.7.21 is the current latest version of the 1.7 Kotlin release.
2023-04-14 07:17:15 -03:00
Norman Breau
3a9c87d3b8 fix(test): Native test namespace refactor (#1595) 2023-04-14 18:47:03 +09:00
エリス
a9d4d4ebd2 feat!: bump Gradle 7.6 & AGP 7.4.2 (#1539)
* feat: bump gradle 7.6
* feat: bump android gradle plugin 7.3.1
* feat: bump android gradle plugin 7.4.2
* fix!: move android package name to build.gradle namespace
* fix!: remove deprecated package name from AndroidManifest
* fix: package name
* fix: rename CordovaGradleConfigParser's _save to write
* test: fix CordovaGradleConfigParser related specs
* fix: test refactoring for gradle namespace
* fix: accidental variable naming mixing

---------

Co-authored-by: Norman Breau <norman@nbsolutions.ca>
2023-04-12 14:39:47 +09:00
Alexis THOMAS
841710edf7 fix: ANDROID_HOME is the new default, to check first and give advice (#1471) 2023-04-09 20:43:11 -03:00
Mahendra Liya
016018513e feat: add monochrome app icon support (#1550)
* Added the monochrome version for Cordova's icon

* android: modified the ic_launcher.xml to include the monochrome drawable

* android: replaced monochrome image with  rasterized images

* android: Added support for custom monochrome icons defined in config.xml

* android: Updated Tests

* android: wrapped inside if(monochrome)

* android: Update templates/project/res/mipmap-hdpi-v26/ic_launcher.xml

Co-authored-by: エリス <erisu@users.noreply.github.com>

* Update lib/prepare.js

Co-authored-by: エリス <erisu@users.noreply.github.com>

* android: Update templates/project/res/mipmap-ldpi-v26/ic_launcher.xml

Co-authored-by: エリス <erisu@users.noreply.github.com>

* android: Update templates/project/res/mipmap-mdpi-v26/ic_launcher.xml

Co-authored-by: エリス <erisu@users.noreply.github.com>

* android Update templates/project/res/mipmap-xhdpi-v26/ic_launcher.xml

Co-authored-by: エリス <erisu@users.noreply.github.com>

* android: Update templates/project/res/mipmap-xxhdpi-v26/ic_launcher.xml

Co-authored-by: エリス <erisu@users.noreply.github.com>

* android: Update templates/project/res/mipmap-xxxhdpi-v26/ic_launcher.xml

Co-authored-by: エリス <erisu@users.noreply.github.com>

* android: Update lib/prepare.js

Co-authored-by: エリス <erisu@users.noreply.github.com>

* Resolved lint errors

* fix: test failure

---------

Co-authored-by: エリス <erisu@users.noreply.github.com>
2023-04-09 20:41:38 -03:00
Norman Breau
a78fad1783 feat: InspectableWebview preference (#1589) 2023-04-09 20:41:00 -03:00
Norman Breau
b91639dbb5 refactor: Removed obsolete version code checks (#1588)
Now that our Min SDK is 24, testing for >= N (API 24) and >= M (API 22) is obsolete as it will always be true.
Simplify the codebase by removing the conditions and keeping only the API 24 or later codepath.
2023-04-08 15:32:19 -03:00
エリス
c2013439bc dep: bump npm dependencies (#1587)
* dep: bump npm dependencies
  - fs-extra@11.1.1
  - nopt@7.1.0
  - @cordova/eslint-config@5.0.0
  - jasmine@4.6.0
* chore: apply eslint fix
2023-04-06 12:35:00 +09:00
Norman Breau
d4bfd5079b fix(GH-1432): Default content src when content tag is missing (#1573) 2023-03-21 13:24:13 -03:00
Norman Breau
dbddbf253b feat!: Bump min SDK to 24 (#1571)
Rationale:
API 22 & API 23 both account for an insignificant part of the market share.
While API 24 - API 26 has similar market share, we felt that bumping to API 26
from API 22 is too large of a jump.

Legacy devices may be completely out of support by Google and may not be able
to receive the latest webview version. As of writing, Chromium's latest
tag shows they are using a Min SDK version of 24. (Ref: https://chromium.googlesource.com/chromium/src/+/refs/tags/113.0.5653.1/build/config/android/config.gni#46)

Based on AOSP emulators, API 24 (Android 7.0) will ship with
Chrome 52 webview, which has good support for ECMAscript 2015 (ES6) (Ref: https://caniuse.com/?search=es6)

While in most cases, app users will likely have a modern webview vesion installed, this means
you can be confident that the app user will have a chrome webview version with good ES6 support,
even if they happen to be running on a factory versioned device.

See the mailing thread for the full discussion:
https://lists.apache.org/thread/zcgof080hdzzo2j96mjz0qpj0gotmn57
2023-03-16 19:45:01 -03:00
エリス
04723eb8f3 feat: bump androidx.appcompat.appcompat 1.6.1 (#1568) 2023-03-11 16:41:38 +09:00
エリス
862d33694e feat: bump androidx.webkit.webkit 1.6.0 (#1567) 2023-03-11 12:19:16 +09:00
エリス
fe3940a73c feat: bump androidx.webkit.webkit 1.5.0 (#1545) 2023-03-11 03:07:19 +09:00
エリス
81c678c58d feat: bump com.google.gms.google-services 4.3.15 (#1547)
* feat: bump com.google.gms.google-services 4.3.14
* feat: bump com.google.gms.google-services 4.3.15
Co-authored-by: jcesarmobile <jcesarmobile@gmail.com>
2023-03-11 03:06:56 +09:00
エリス
bfe086a2d7 feat: bump androidx.core.core-splashscreen 1.0.0 (#1546) 2023-03-11 02:51:33 +09:00
エリス
8fb707567a feat: bump androidx.appcompat.appcompat 1.5.1 (#1544) 2023-03-11 02:45:42 +09:00
エリス
992a60a434 feat!: bump target sdk & build tools for SDK 33 support (#1538)
* feat!: bump target sdk version 33
* feat!: bump minimum build tools version 33.0.1
* feat!: bump minimum build tools version 33.0.2
2023-03-10 12:12:17 +09:00
エリス
2318ef58ad dep(npm)!: bump acceptable modules w/ rebuilt package-lock (#1541)
* dep(npm)!: bump acceptable modules
* chore: rebuilt package-lock
2023-03-09 18:57:19 +09:00
エリス
968bd85cc3 feat!: bump node engine requirement >=16.13.0 (#1540) 2023-03-09 18:40:16 +09:00
エリス
9ef3ee9539 dep(npm)!: bump cordova-common@5.0.0 (#1566) 2023-03-09 18:34:31 +09:00
エリス
5347054efb chore: prepare package version for next major 12.0.0-dev (#1537) 2023-03-09 18:33:28 +09:00
エリス
3340e98519 ci(workflow): update codecov/codecov-action@v3 (#1542) 2023-01-18 23:53:36 +09:00
jcesarmobile
ce19a3b445 chore: add lint:fix script for fixing lint errors (#1493)
* chore: Add fmt command for fixing lint errors
* rename to lint:fix
2023-01-11 10:06:16 +09:00
Jesse MacFadyen
d02f8eafe8 Update codecoverage reporting (#1532) 2022-12-30 12:18:54 -08:00
jcesarmobile
56d4b8312b fix: only do fadeout animation if FadeSplashScreen is true (#1506) 2022-10-27 23:38:30 +02:00
Darryl Pogue
80f232aa79 fix: correctly flag API dependency on AppCompat for Maven (#1505)
* Correctly flag API dependency on AppCompat for Maven

Currently when cordova-android is published to Maven, it lists no
dependencies. However, `CordovaActivity` extends `AppCompatActivity`
which requires that the AndroidX AppCompat library be available.

Marking this as an API dependency (rather than an implementation/compile
dependency) should cause the AndroidX AppCompat library to be installed
when the cordova-android framework is added to the build.gradle of an
existing Android application.

* Publish to Maven with proper metadata

This allows the Maven publish to pick up information from the android
library component and include things like dependencies in the pom.xml
file.
2022-10-15 13:39:47 +09:00
jcesarmobile
954d3e0e75 fix: Add android prefix to windowSplashScreenBrandingImage (#1487) 2022-09-16 09:34:20 +02:00
jcesarmobile
8a1ffeeafd chore: Use gradle 7.4.2 distribution url (#1491) 2022-09-16 09:33:15 +02:00
jcesarmobile
7793db97cc refactor: replace deprecated Handler constructor (#1492) 2022-09-16 09:32:38 +02:00
Philip Peitsch
248257bd37 fix: import type definitions from obsolete cordova-plugin-splashscreen (#1489) 2022-09-13 10:03:53 -03:00
Erisu
60e3803c67 chore: bump version 11.0.1-dev (2) 2022-07-04 23:12:43 +09:00
Erisu
d828785435 release(android-v11.0.0): updated version and RELEASENOTES.md (2) 2022-07-04 22:37:49 +09:00
エリス
e5b7e8ab26 fix(prepare): destFile path separator (#1455) 2022-07-04 22:27:11 +09:00
Erisu
f38e8eb3d0 chore: bump version 11.0.1-dev 2022-06-30 21:53:33 +09:00
Erisu
98895f7d78 release(android-v11.0.0): updated version and RELEASENOTES.md 2022-06-30 21:43:31 +09:00
エリス
e968cac0b9 fix: support installing platfrom from local git checkout (#1453) 2022-06-30 21:35:27 +09:00
エリス
861fec2cc7 dep: bump jasmine@4.2.1 w/ package-lock rebuild (#1452) 2022-06-30 20:25:06 +09:00
エリス
273d1bdecd chore: display warning on deprecated <splash> tag usage (#1451)
* chore: display warning on deprecated <splash> tag usage
* test: create spy on warnForDeprecatedSplashScreen
2022-06-30 20:00:25 +09:00
エリス
606e9c4826 feat!: android 12 splash screen (#1441)
* chore!: remove old splashscreen logic
* feat(splashscreen): add backwards compatibility
* chore: remove unused method
* chore: prefix splashscreen_background with cdv_
* feat: support android 12 splashscreen api configs
* feat: improve & refactor the logic for android splashscreen api 12
* feat: splashscreen copy image resources
* feat: splashscreen branding image & xml cleanup
* fix: splashscreen cleanup & branding conditions
* fix: splashscreen @color usage
* feat: update default Apache Cordova splash screen
* chore: add missing asf header
* fix: splashscreen image size
* chore: use Theme.SplashScreen.IconBackground as default parent to support windowSplashScreenIconBackgroundColor
* fix: center default test image by correct pivot
* fix: fs-extra copySync
* feat: re-add AutoHideSplashScreen and SplashScreenDelay preference support
* chore: move splashscreen into CordovaActivity
* feat: support splashscreen.hide & centralize to SplashScreenPlugin
* chore: cleanup SplashScreenPlugin
* feat: support fade, default auto hide on onPageFinished, support delays, refactor
* refactor: cleanup splash screen
* refactor: cleanup remove unused import
* chore: add show method as unsupported
* test: create a spy on updateProjectSplashScreen
* style: add ending new line
* chore: improve logging to warn when image path is missing
* chore: split windowSplashScreenAnimatedIcon and windowSplashScreenBrandingImage case and added branding warning
* chore: improve when to display warning
* fix: add splashscreen dependency to app as well
* chore: move the core-splashscreen dep lower

Co-authored-by: Niklas Merz <niklasmerz@linux.com>
2022-06-30 10:49:10 +09:00
jcesarmobile
2d2ad4cb81 fix: accept file cookies only if AndroidInsecureFileModeEnabled (#1449) 2022-06-29 11:36:55 +09:00
jcesarmobile
26b21219f7 feat: Update androidx.appcompat version (#1448) 2022-06-29 11:34:08 +09:00
jcesarmobile
8d6e41fd77 feat: Update gradle plugin version (#1446) 2022-06-28 12:23:42 +09:00
jcesarmobile
262a314c72 feat: Update google services pluging (#1447) 2022-06-28 12:23:10 +09:00
エリス
bf9e4d8aab fix!: set & use ANDROID_HOME as default (#1444)
* fix: remove ANDROID_HOME's DEPRECATED text
* fix: check_gradle to check ANDROID_HOME first ANDROID_SDK_ROOT last
* fix: set ANDROID_HOME
* chore: deprecate flag on ANDROID_SDK_ROOT
2022-06-27 22:07:32 +09:00
エリス
4916e1db51 fix: force hostname to lowercase (#1443) 2022-06-20 23:25:57 +09:00
エリス
68a9a3181a ci: update github action workflow (#1439)
* ci: bump actions/checkout dependency
* ci: bump actions/setup-node dependency
* ci: bump codecov/codecov-action dependency
* ci: migrate from adopt to temurin
2022-06-03 15:05:38 +09:00
jcesarmobile
ba032df665 chore: Remove unneeded deprecated annotation (#1430) 2022-06-02 01:13:25 +02:00
エリス
53d39fb135 fix: restore checkReqs in prepare.js (#1434) 2022-05-19 00:49:52 +09:00
エリス
4744bfe6bf feat: support custom compileSdk setting (#1431)
* feat: support custom compileSdk setting
* chore: apply suggestions from code review
* chore: apply cdv-gradle-config-defaults.json suggestion
* fix: set compile sdk when null
* fix: move compileSdk null check to gradle
* fix: compile sdk requirement warning & display in gradle per subproject

Co-authored-by: Norman Breau <norman@nbsolutions.ca>
2022-05-18 23:18:33 +09:00
wedgberto
cb494ff9b1 fix(android): move MainActivity.java to folder that tracks the app package name (widget id) (#1154)
* move mainactivity.java

* unit test for moving mainactivity.java

* fix new unit test

* eslint fixes

* add explicit elementtree npm dependency

Co-authored-by: David Wedgbury <david.wedgbury@telent.com>
2022-05-18 11:11:31 -03:00
ebhsgit
bd0c8ce639 [Android] Added support for BoM imports (#1311)
* Added support for BoM imports

https://docs.gradle.org/current/userguide/platforms.html#sub:bom_import

Changes

* propertiesObj.systemLibs regex - exclude the value contains (
* added propertiesObj.bomPlatforms - any value which matches platform("...")

* Fix eslint style issues

Co-authored-by: 8bhsolutions <48874658+8bhsolutions@users.noreply.github.com>
2022-05-18 01:10:15 -03:00
Norman Breau
e73000023b feat: API 32 support (#1427) 2022-05-03 20:13:17 -03:00
Norman Breau
087e9e6178 ci: Added Node 18 to test matrix (#1424) 2022-04-22 11:58:44 -03:00
エリス
a2bb7f1173 chore(npm): bump @cordova/eslint-config@^4.0.0 (#1421)
* chore(npm): bump @cordova/eslint-config@^4.0.0
* style(lint): apply auto corrections
* style(lint): convert hasAndroidHome var to let and hoisted
2022-04-18 10:39:54 +09:00
エリス
62ed71c539 chore(npm): bump dependencies (#1420)
* chore(npm): bump dev dependencies
* chore(npm): bump release dependencies
2022-04-17 22:02:40 +09:00
Norman Breau
5704ef9ea5 feat!: API 31 support (#1410)
* feat(breaking): API 31 support
* ci: bump actions/setup-java@3 w/ java 11
* ci: set setup-java distribution to adopt

Co-authored-by: Erisu <erisu@apache.org>
2022-04-17 19:43:45 +09:00
Norman Breau
adcd9d9ff8 chore(breaking): Drop Node 12 support (#1411) 2022-04-17 19:43:11 +09:00
エリス
f3c75a89b5 chore: bump for next major 11.0.0-dev (#1419) 2022-04-17 19:06:51 +09:00
Erisu
21e7c2f661 chore: bump version 10.1.3-dev 2022-04-11 22:10:31 +09:00
Erisu
f12080b7e2 android-v10.1.2 Updated version and RELEASENOTES.md for release 10.1.2 2022-04-11 22:01:02 +09:00
Norman Breau
51291f8985 chore: update package-lock to satisfy npm audit (#1413) 2022-04-06 11:59:21 -03:00
Lars Mikkelsen
112f0a61a8 fix: detect JAVA_HOME with Java 11 (#1406) 2022-03-17 20:38:04 -03:00
Norman Breau
6d3ce211dd fix(GH-1391): Reword minimum build tools version to make it more clear what is actually required. (#1401) 2022-02-22 10:14:22 -04:00
Tiago Pereira
f100809bf3 fix: escape strings.xml app name (#1384) 2022-02-08 11:29:36 +09:00
エリス
a1ed1c0af7 feat(AndroidManifest): explicitly define the activity attribute android:exported (#1372) 2021-10-28 17:29:55 +09:00
Norman Breau
05e3e3cf8d chore: npmrc (#1348) 2021-09-17 09:54:59 -03:00
Erisu
8a957fb9c9 chore: bump version 10.1.2-dev 2021-09-13 20:17:31 +09:00
Erisu
e188c61c86 android-v10.1.1 Updated version and RELEASENOTES.md for release 10.1.1 2021-09-13 15:56:27 +09:00
エリス
ca19084b1c fix(PluginManager): AllowNavigation default policy to handle scheme & hostname (#1349) 2021-09-13 15:48:02 +09:00
Joel Skrepnek
aea6b7f6f4 fix(AllowListPlugin): Safely handle default allow navigation policy in allow request (#1342) 2021-09-09 14:58:58 +09:00
エリス
7a67e00b9f fix(PluginManager): AllowBridgeAccess default policy to handle scheme & hostname (#1332) 2021-09-07 19:52:23 +09:00
Erisu
dc4e065f61 chore: bump version 10.1.1-dev 2021-08-13 13:42:59 +09:00
Erisu
c3fd6bca4a android-v10.1.0 Updated version and RELEASENOTES.md for release 10.1.0 2021-08-13 13:26:31 +09:00
Alexis THOMAS
13bd3f4a9f feat: unify create default values & stop project name transform (#1213) 2021-08-13 12:52:30 +09:00
Alexis THOMAS
09c75237d9 fix: display project name in Android Studio (#1214) 2021-08-13 12:08:18 +09:00
Alexis THOMAS
b5e79b5a4d doc: update README about development & testing (#1308)
Co-authored-by: エリス <erisu@users.noreply.github.com>
2021-08-13 11:28:47 +09:00
Mosab A
5db850890d feat: bump ANDROIDX_APP_COMPAT@1.3.1 (#1306) 2021-08-02 16:34:06 +09:00
Mosab A
b6c5db3e37 feat: bump Google Services Gradle Plugin@4.3.8 (#1303) 2021-08-02 14:21:11 +09:00
Mosab A
cba3410b17 feat: bump kotlin@1.5.21 (#1302) 2021-08-02 14:21:02 +09:00
エリス
565ac9c7b1 fix: fall back to project root repositories.gradle (#1300) 2021-08-02 14:19:40 +09:00
エリス
1636d70f25 feat: support http w/ content src fix (#1298) 2021-07-30 17:08:23 +09:00
エリス
4e5892c5ed chore: bump version 10.1.0-dev (#1301) 2021-07-30 17:08:04 +09:00
Erisu
e69ab6a687 chore: bump version 10.0.2-dev 2021-07-27 23:23:36 +09:00
160 changed files with 9230 additions and 4737 deletions

View File

@@ -1,3 +0,0 @@
templates/project/assets/www/cordova.js
test/android/app
test/androidx/app

20
.gitattributes vendored
View File

@@ -1,3 +1,20 @@
# 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.
* text eol=lf
# source code
@@ -23,7 +40,7 @@
*.scm text
*.sql text
*.sh text
*.bat text
*.bat text eol=crlf
# templates
*.ejs text
@@ -92,3 +109,4 @@ AUTHORS text
*.woff binary
*.pyc binary
*.pdf binary
*.jar binary

View File

@@ -6,18 +6,28 @@
# "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
# 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
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
name: Node CI
on: [push, pull_request]
on:
push:
branches-ignore:
- 'dependabot/**'
pull_request:
branches:
- '*'
permissions:
contents: read
security-events: write
jobs:
test:
@@ -27,21 +37,19 @@ jobs:
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [20.x, 22.x, 24.x]
os: [ubuntu-latest, windows-latest, macos-15]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: ${{ matrix.node-version }}
- name: set up JDK 1.8
uses: actions/setup-java@v1
- uses: actions/setup-java@v5
with:
java-version: 1.8
distribution: 'temurin'
java-version: '17'
- name: Environment Information
run: |
@@ -49,6 +57,21 @@ jobs:
npm --version
gradle --version
# "bin/templates/platform_www/cordova.js" is ignored because it is a generated file.
# It contains mixed content from the npm package "cordova-js" and "./cordova-js-src".
# The report might not be resolvable because of the external package.
# If the report is related to this repository, it would be detected when scanning "./cordova-js-src".
- uses: github/codeql-action/init@v3
with:
languages: javascript, java-kotlin
queries: security-and-quality
config: |
paths-ignore:
- coverage
- node_modules
- templates/project/assets/www/cordova.js
- test/androidx/app/src/main/assets/www/cordova.js
- name: npm install and test
run: |
npm i
@@ -56,6 +79,12 @@ jobs:
env:
CI: true
- uses: codecov/codecov-action@v1
- uses: github/codeql-action/analyze@v3
# v4.6.0
- uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238
if: success()
with:
fail_ci_if_error: true
name: ${{ runner.os }} node.js ${{ matrix.node-version }}
token: ${{ secrets.CORDOVA_CODECOV_TOKEN }}
fail_ci_if_error: false

55
.github/workflows/release-audit.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
# 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.
name: Release Auditing
on:
push:
branches-ignore:
- 'dependabot/**'
pull_request:
branches:
- '*'
permissions:
contents: read
jobs:
test:
name: Audit Licenses
runs-on: ubuntu-latest
steps:
# Checkout project
- uses: actions/checkout@v6
# Check license headers (v2.0.0)
- uses: erisu/apache-rat-action@46fb01ce7d8f76bdcd7ab10e7af46e1ea95ca01c
# Setup environment with node
- uses: actions/setup-node@v6
with:
node-version: 24
# Install node packages
- name: npm install packages
run: npm ci
# Check node package licenses (v2.0.1)
- uses: erisu/license-checker-action@99cffa11264fe545fd0baa6c13bca5a00ae608f2
with:
license-config: 'licence_checker.yml'
include-asf-category-a: true

27
.gitignore vendored
View File

@@ -1,3 +1,20 @@
# 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.
.DS_Store
.gradle
.metadata
@@ -29,12 +46,11 @@ example
**/assets/www/cordova.js
/test/.externalNativeBuild
/test/androidx/gradle
/test/androidx/gradlew
/test/androidx/gradlew.bat
/test/androidx/cdv-gradle-config.json
/test/androidx/repositories.gradle
/test/androidx/app/repositories.gradle
/test/androidx/tools
/test/androidx/build
/test/assets/www/.tmp*
/test/assets/www/cordova.js
/test/bin
@@ -50,7 +66,6 @@ tmp/**/*
npm-debug.log
node_modules/
coverage/
.nyc_output/
# Eclipse Buildship files
.project
.settings

View File

@@ -1,3 +1,20 @@
# 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.
.*
coverage
test

View File

@@ -15,13 +15,4 @@
# specific language governing permissions and limitations
# under the License.
extends: '@cordova/eslint-config/node'
overrides:
- files: [spec/**/*.js]
extends: '@cordova/eslint-config/node-tests'
rules:
prefer-promise-reject-errors: off
- files: [cordova-js-src/**/*.js]
extends: '@cordova/eslint-config/browser'
registry=https://registry.npmjs.org

View File

@@ -1,8 +1,27 @@
*.properties
templates
gen
proguard-project.txt
spec
framework/build
ic_launcher.png
build
# 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.
.git/
coverage/
framework/build/
node_modules/
spec/fixtures/
templates/project/gitignore
test/androidx/app/.gitignore
test/androidx/app/build/
test/androidx/build/
test/androidx/tools/

View File

@@ -1,3 +1,20 @@
# 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.
#
# Settings for post-review (used for uploading diffs to reviews.apache.org).
#

View File

@@ -187,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2015-2020 Apache Cordova
Copyright 2015-2024 Apache Cordova
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -200,3 +200,4 @@
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.

View File

@@ -28,22 +28,58 @@
Cordova Android is an Android application library that allows for Cordova-based projects to be built for the Android Platform. Cordova based applications are, at the core, applications written with web technology: HTML, CSS and JavaScript.
[Apache Cordova](https://cordova.apache.org) is a project of The Apache Software Foundation (ASF).
[Apache Cordova](https://cordova.apache.org/) is a project of [The Apache Software Foundation (ASF)](https://apache.org/).
## Requirements
- Java Development Kit (JDK) 11
- [Android SDK](https://developer.android.com/)
* Java Development Kit (JDK) 17
* [Android SDK](https://developer.android.com/)
* [Node.js](https://nodejs.org)
## Cordova Android Developer Tools
## Create a Cordova project
Use the [Cordova command-line tool](https://www.npmjs.com/package/cordova) to create projects and install plugins.
Follow the instructions in the [**Create your first Cordova app**](https://cordova.apache.org/docs/en/latest/guide/cli/index.html) section of [Apache Cordova Docs](https://cordova.apache.org/docs/en/latest/)
## Using Android Studio
To use a **shared framework**, for example in development, link the appropriate cordova-android platform folder path:
1. Create a project
2. Import it via "Non-Android Studio Project"
```bash
cordova platform add --link /path/to/cordova-android
```
## Running the Native Tests
## Updating a Cordova project
The `test/` directory in this project contains an Android test project that can be used to run different kinds of native tests. Check out the [README contained therein](test/README.md) for more details!
When you install a new version of the [`Cordova CLI`](https://www.npmjs.com/package/cordova) that pins a new version of the [`Cordova-Android`](https://www.npmjs.com/package/cordova-android) platform, you can follow these simple upgrade steps within your project:
```bash
cordova platform rm android
cordova platform add android
```
## Debugging in Android Studio
Import project in Android Studio through _File > Open_ and targeting `/path/to/your-cdv-project/platforms/android/`.
## How to Test Repo Development
```bash
npm install
npm test
```
## Install Nightly Build
```bash
cordova platform add android@nightly
```
### Notes
Nightly builds are **not recommended for production apps**. They are intended for testing purposes. This allows users either to check if recent changes in the main branch have fixed existing issues or to identify new bugs before an official release.
Nightly builds are generated daily and may be **unstable**.
See [Apache Cordova - Nightly Builds](https://cordova.apache.org/contribute/nightly_builds.html) for more details.
## Further reading
* [Apache Cordova](https://cordova.apache.org/)

View File

@@ -20,6 +20,230 @@
-->
## Release Notes for Cordova (Android)
### 14.0.1 (Apr 24, 2025)
**Fixes:**
* [GH-1795](https://github.com/apache/cordova-android/pull/1795) fix: configure gradle `java.home`
* [GH-1793](https://github.com/apache/cordova-android/pull/1793) fix(windows): get gradle path with `which` command
### 14.0.0 (Mar 23, 2025)
**Breaking Changes:**
* [GH-1788](https://github.com/apache/cordova-android/pull/1788) dep!: bump npm packages
* nyc@17.1.0
* which@5.0.0
* semver@7.7.1
* jasmine@5.6.0
* android-versions@2.1.0
* cordova-common@5.0.1
* fast-glob@3.3.3
* nopt@8.1.0
* [GH-1789](https://github.com/apache/cordova-android/pull/1789) feat!: bump node engine requirement `>=20.5.0`
* [GH-1784](https://github.com/apache/cordova-android/pull/1784) feat!: bump java default targets to 11
* [GH-1771](https://github.com/apache/cordova-android/pull/1771) feat!: deprecate CordovaPlugin's method initialize
* [GH-1767](https://github.com/apache/cordova-android/pull/1767) feat!: use kotlin-stdlib instead of kotlin-stdlib-jdk*
* [GH-1763](https://github.com/apache/cordova-android/pull/1763) feat!: SDK 35 Support
**Features:**
* [GH-1785](https://github.com/apache/cordova-android/pull/1785) feat: bump gradle to 8.13
* [GH-1779](https://github.com/apache/cordova-android/pull/1779) feat: add `AndroidEdgeToEdge` preference & theme flag
* [GH-1778](https://github.com/apache/cordova-android/pull/1778) feat: Account for Node security patch
* [GH-1768](https://github.com/apache/cordova-android/pull/1768) feat: `androidx.core:core-splashscreen@1.0.1`
* [GH-1766](https://github.com/apache/cordova-android/pull/1766) feat: `com.google.gms:google-services@4.4.2`
* [GH-1765](https://github.com/apache/cordova-android/pull/1765) feat: `androidx.webkit:webkit@1.12.1`
* [GH-1764](https://github.com/apache/cordova-android/pull/1764) feat: `androidx.appcompat:appcompat@1.7.0`
**Fixes:**
* [GH-1790](https://github.com/apache/cordova-android/pull/1790) fix: replace fs-extra.ensureFileSync with fs.writeFileSync
* [GH-1781](https://github.com/apache/cordova-android/pull/1781) fix: copy gradle wrapper from tools to platform root dir
* [GH-1770](https://github.com/apache/cordova-android/pull/1770) fix: creation of cdv-gradle-config.json w/ --link flag
* [GH-1739](https://github.com/apache/cordova-android/pull/1739) fix(docs): Incorrect JDK requirement stated in README
* [GH-1718](https://github.com/apache/cordova-android/pull/1718) fix: app restart when BT keyboard is connected in some devices
**Chores & Refactoring:**
* [GH-1786](https://github.com/apache/cordova-android/pull/1786) chore: add AndroidX build test to gitignore
* [GH-1774](https://github.com/apache/cordova-android/pull/1774) style: update & resolve doc block warnings
* [GH-1772](https://github.com/apache/cordova-android/pull/1772) refactor: replace fs-extra with node:fs
* [GH-1769](https://github.com/apache/cordova-android/pull/1769) refactor: prefix node:*
* [GH-1748](https://github.com/apache/cordova-android/pull/1748) chore(deps): bump cross-spawn from 7.0.3 to 7.0.6
* [GH-1750](https://github.com/apache/cordova-android/pull/1750) chore(ci): Fix dependabot PR failures
* [GH-1736](https://github.com/apache/cordova-android/pull/1736) chore(deps): bump micromatch from 4.0.5 to 4.0.8
* [GH-1716](https://github.com/apache/cordova-android/pull/1716) chore(deps): bump braces from 3.0.2 to 3.0.3
### 13.0.0 (May 15, 2024)
**Breaking Changes:**
* [GH-1678](https://github.com/apache/cordova-android/pull/1678) feat!: API 34 Support
* [GH-1543](https://github.com/apache/cordova-android/pull/1543) feat!: bump `kotlin@1.9.24` & drop `kotlin-android-extensions` when kotlin `>=1.8.0`
**Features:**
* [GH-1700](https://github.com/apache/cordova-android/pull/1700) feat(splash): Support `SplashScreenBackgroundColor` preference
* [GH-1609](https://github.com/apache/cordova-android/pull/1609) feat: add camera intent with file input capture
* [GH-1696](https://github.com/apache/cordova-android/pull/1696) feat: Add `ResolveServiceWorkerRequests` preference
**Chores, Dependencies & CI:**
* [GH-1677](https://github.com/apache/cordova-android/pull/1677) chore(deps-dev): bump `@babel/traverse` from `7.22.10` to `7.23.2`
* [GH-1713](https://github.com/apache/cordova-android/pull/1713) dep: bump npm dependencies 20240515
* `semver@7.6.2`
* `rewire@7.0.0`
* `nopt@7.2.1`
* `jasmine@5.1.0`
* `fs-extra@11.2.0`
* `fast-glob@3.3.2`
* `dedent@1.5.3`
* `@cordova/eslint-config@5.1.0`
* `which@4.0.0`
* `properties-parser@0.6.0`
* `android-versions@2.0.0`
* [GH-1711](https://github.com/apache/cordova-android/pull/1711) ci: Set up CodeQL analysis w/ fixes
* [GH-1687](https://github.com/apache/cordova-android/pull/1687) ci(release-audit): add license header and dependency checker
* [GH-1703](https://github.com/apache/cordova-android/pull/1703) ci: update `codecov@v4` w/ token
### 12.0.1 (Aug 23, 2023)
* [GH-1632](https://github.com/apache/cordova-android/pull/1632) fix(android): `monochrome` checks
* [GH-1649](https://github.com/apache/cordova-android/pull/1649) chore: rebuild `package-lock` w/ lint corrections
### 12.0.0 (May 20, 2023)
**Breaking:**
* [GH-1605](https://github.com/apache/cordova-android/pull/1605) fix!: Make `CoreAndroid` plugin instantiate on load
* [GH-1539](https://github.com/apache/cordova-android/pull/1539) feat!: bump Gradle 7.6 & AGP 7.4.2
* [GH-1571](https://github.com/apache/cordova-android/pull/1571) feat!: bump min SDK to 24
* [GH-1538](https://github.com/apache/cordova-android/pull/1538) feat!: bump target sdk & build tools for SDK 33 support
* [GH-1540](https://github.com/apache/cordova-android/pull/1540) feat!: bump node engine requirement `>=16.13.0`
* [GH-1597](https://github.com/apache/cordova-android/pull/1597) deprecate: `CoreAndroid.getBuildConfigValue`
* [GH-1541](https://github.com/apache/cordova-android/pull/1541) dep(npm)!: bump acceptable modules w/ rebuilt `package-lock`
* [GH-1566](https://github.com/apache/cordova-android/pull/1566) dep(npm)!: bump `cordova-common@5.0.0`
**Features:**
* [GH-1602](https://github.com/apache/cordova-android/pull/1602) feat: add `listTarget` api
* [GH-1574](https://github.com/apache/cordova-android/pull/1574) feat: add plugin hooks for `WebViewClient.onRenderProcessGone`
* [GH-1594](https://github.com/apache/cordova-android/pull/1594) feat: bump default `kotlin` to version 1.7.21
* [GH-1550](https://github.com/apache/cordova-android/pull/1550) feat: add `monochrome` app icon support
* [GH-1589](https://github.com/apache/cordova-android/pull/1589) feat: `InspectableWebview` preference
* [GH-1568](https://github.com/apache/cordova-android/pull/1568) feat: bump `androidx.appcompat.appcompat` 1.6.1
* [GH-1567](https://github.com/apache/cordova-android/pull/1567) feat: bump `androidx.webkit.webkit` 1.6.0
* [GH-1545](https://github.com/apache/cordova-android/pull/1545) feat: bump `androidx.webkit.webkit` 1.5.0
* [GH-1547](https://github.com/apache/cordova-android/pull/1547) feat: bump `com.google.gms.google-services` 4.3.15
* [GH-1546](https://github.com/apache/cordova-android/pull/1546) feat: bump `androidx.core.core-splashscreen` 1.0.0
* [GH-1544](https://github.com/apache/cordova-android/pull/1544) feat: bump `androidx.appcompat.appcompat` 1.5.1
**Fixes:**
* [GH-1606](https://github.com/apache/cordova-android/pull/1606) fix: Gradle Args parsing
* [GH-1575](https://github.com/apache/cordova-android/pull/1575) fix(`BuildHelper`): get package name from `ApplicationInfo`
* [GH-1595](https://github.com/apache/cordova-android/pull/1595) fix(test): Native test namespace refactor
* [GH-1471](https://github.com/apache/cordova-android/pull/1471) fix: `ANDROID_HOME` is the new default, to check first and give advice
* [GH-1573](https://github.com/apache/cordova-android/pull/1573) fix(GH-1432): Default `content` `src` when content tag is missing
* [GH-1506](https://github.com/apache/cordova-android/pull/1506) fix: only do fadeout animation if `FadeSplashScreen` is true
* [GH-1505](https://github.com/apache/cordova-android/pull/1505) fix: correctly flag API dependency on `AppCompat` for Maven
* [GH-1487](https://github.com/apache/cordova-android/pull/1487) fix: Add **Android** prefix to `WindowSplashScreenBrandingImage`
* [GH-1489](https://github.com/apache/cordova-android/pull/1489) fix: import type definitions from obsolete `cordova-plugin-splashscreen`
**Chores, Refactor, Dependencies & CI:**
* [GH-1493](https://github.com/apache/cordova-android/pull/1493) chore: add `lint:fix` script for fixing lint errors
* [GH-1491](https://github.com/apache/cordova-android/pull/1491) chore: Use gradle 7.4.2 distribution url
* [GH-1588](https://github.com/apache/cordova-android/pull/1588) refactor: Removed obsolete version code checks
* [GH-1492](https://github.com/apache/cordova-android/pull/1492) refactor: replace deprecated `Handler` constructor
* [GH-1587](https://github.com/apache/cordova-android/pull/1587) dep: bump npm dependencies
* `fs-extra@11.1.1`
* `nopt@7.1.0`
* `@cordova/eslint-config@5.0.0`
* `jasmine@4.6.0`
* [GH-1607](https://github.com/apache/cordova-android/pull/1607) ci: Added NodeJS 20.x to the workflow matrix
* [GH-1542](https://github.com/apache/cordova-android/pull/1542) ci(workflow): update `codecov/codecov-action@v3`
* [GH-1532](https://github.com/apache/cordova-android/pull/1532) ci: update `codecov/codecov-action` reporting format
### 11.0.0 (Jul 04, 2022)
**Breaking:**
* [GH-1441](https://github.com/apache/cordova-android/pull/1441) feat!: **Android** 12 splash screen
* [GH-1427](https://github.com/apache/cordova-android/pull/1427) feat!: API 32 support
* [GH-1410](https://github.com/apache/cordova-android/pull/1410) feat!: API 31 support
* [GH-1444](https://github.com/apache/cordova-android/pull/1444) fix!: set & use `ANDROID_HOME` as default
* [GH-1411](https://github.com/apache/cordova-android/pull/1411) chore!: Drop Node 12 support
**Features:**
* [GH-1448](https://github.com/apache/cordova-android/pull/1448) feat: Update `androidx.appcompat` version
* [GH-1446](https://github.com/apache/cordova-android/pull/1446) feat: Update gradle plugin version
* [GH-1447](https://github.com/apache/cordova-android/pull/1447) feat: Update google services pluging
* [GH-1431](https://github.com/apache/cordova-android/pull/1431) feat: support custom `compileSdk` setting
* [GH-1311](https://github.com/apache/cordova-android/pull/1311) feat: added support for BoM imports
**Fixes:**
* [GH-1455](https://github.com/apache/cordova-android/pull/1455) fix(`prepare`): `destFile` path separator
* [GH-1453](https://github.com/apache/cordova-android/pull/1453) fix: support installing platfrom from local git checkout
* [GH-1449](https://github.com/apache/cordova-android/pull/1449) fix: accept file cookies only if `AndroidInsecureFileModeEnabled`
* [GH-1443](https://github.com/apache/cordova-android/pull/1443) fix: force `hostname` to lowercase
* [GH-1434](https://github.com/apache/cordova-android/pull/1434) fix: restore `checkReqs` in `prepare.js`
* [GH-1154](https://github.com/apache/cordova-android/pull/1154) fix: move `MainActivity.java` to folder that tracks the app package name (widget id)
**Chores, Dependencies & CI:**
* [GH-1451](https://github.com/apache/cordova-android/pull/1451) chore: display warning on deprecated `<splash>` tag usage
* [GH-1430](https://github.com/apache/cordova-android/pull/1430) chore: remove unneeded deprecated annotation
* [GH-1421](https://github.com/apache/cordova-android/pull/1421) chore(npm): bump `@cordova/eslint-config@^4.0.0`
* [GH-1420](https://github.com/apache/cordova-android/pull/1420) chore(npm): bump dependencies
* [GH-1452](https://github.com/apache/cordova-android/pull/1452) dep: bump `jasmine@4.2.1` w/ `package-lock` rebuild
* [GH-1439](https://github.com/apache/cordova-android/pull/1439) ci: update github action workflow
* [GH-1424](https://github.com/apache/cordova-android/pull/1424) ci: Added Node 18 to test matrix
### 10.1.2 (Apr 11, 2022)
**Fixes:**
* [GH-1372](https://github.com/apache/cordova-android/pull/1372) fix(`AndroidManifest`): explicitly define the `activity` attribute `android:exported`
* [GH-1406](https://github.com/apache/cordova-android/pull/1406) fix: detect `JAVA_HOME` with Java 11
* [GH-1401](https://github.com/apache/cordova-android/pull/1401) fix(GH-1391): Reword minimum build tools version to make it more clear what is actually required.
* [GH-1384](https://github.com/apache/cordova-android/pull/1384) fix: escape `strings.xml` app name
**Chores:**
* [GH-1413](https://github.com/apache/cordova-android/pull/1413) chore: update `package-lock` to satisfy `npm audit`
* [GH-1348](https://github.com/apache/cordova-android/pull/1348) chore: `npmrc`
### 10.1.1 (Sep 13, 2021)
**Fixes:**
* [GH-1349](https://github.com/apache/cordova-android/pull/1349) fix(`PluginManager`): `AllowNavigation` default policy to handle scheme & hostname
* [GH-1342](https://github.com/apache/cordova-android/pull/1342) fix(`AllowListPlugin`): Safely handle default allow navigation policy in allow request
* [GH-1332](https://github.com/apache/cordova-android/pull/1332) fix(`PluginManager`): `AllowBridgeAccess` default policy to handle scheme & hostname
### 10.1.0 (Aug 13, 2021)
**Features:**
* [GH-1213](https://github.com/apache/cordova-android/pull/1213) feat: unify `create` default values & stop project name transform
* [GH-1306](https://github.com/apache/cordova-android/pull/1306) feat: bump `ANDROIDX_APP_COMPAT@1.3.1`
* [GH-1303](https://github.com/apache/cordova-android/pull/1303) feat: bump `Google Services Gradle Plugin@4.3.8`
* [GH-1302](https://github.com/apache/cordova-android/pull/1302) feat: bump `kotlin@1.5.21`
* [GH-1298](https://github.com/apache/cordova-android/pull/1298) feat: support `http` w/ `content` `src` fix
**Fixes:**
* [GH-1214](https://github.com/apache/cordova-android/pull/1214) fix: display project name in Android Studio
* [GH-1300](https://github.com/apache/cordova-android/pull/1300) fix: fall back to project root `repositories.gradle`
**Docs:**
* [GH-1308](https://github.com/apache/cordova-android/pull/1308) doc: update `README` about development & testing
### 10.0.1 (Jul 27, 2021)
**Fixes:**

View File

@@ -91,7 +91,7 @@ function androidExec (success, fail, service, action, 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, fail };
}
var msgs = nativeApiProvider.get().exec(bridgeSecret, service, action, callbackId, argsJson);
@@ -131,6 +131,8 @@ function pollOnce (opt_fromOnlineEvent) {
}
}
androidExec.pollOnce = pollOnce;
function pollingTimerFunc () {
if (pollEnabled) {
pollOnce();

View File

@@ -36,6 +36,13 @@ module.exports = {
// TODO: Extract this as a proper plugin.
modulemapper.clobbers('cordova/plugin/android/app', 'navigator.app');
// Core Splash Screen
modulemapper.clobbers('cordova/plugin/android/splashscreen', 'navigator.splashscreen');
// Attach the internal statusBar utility to window.statusbar
// see the file under plugin/android/statusbar.js
modulemapper.clobbers('cordova/plugin/android/statusbar', 'window.statusbar');
var APP_PLUGIN_NAME = Number(cordova.platformVersion.split('.')[0]) >= 4 ? 'CoreAndroid' : 'App';
// Inject a listener for the backbutton on the document.

View File

@@ -31,14 +31,14 @@ module.exports = {
},
/**
* Load the url into the webview or into new browser instance.
* Load the url into the WebView or into new browser instance.
*
* @param url The URL to load
* @param props Properties that can be passed in to the activity:
* wait: int => wait msec before loading URL
* loadingDialog: "Title,Message" => display a native loading dialog
* loadUrlTimeoutValue: int => time in msec to wait before triggering a timeout error
* clearHistory: boolean => clear webview history (default=false)
* clearHistory: boolean => clear WebView history (default=false)
* openExternal: boolean => open in a new browser (default=false)
*
* Example:

View File

@@ -0,0 +1,33 @@
/*
*
* 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 exec = require('cordova/exec');
var splashscreen = {
show: function () {
console.log('"navigator.splashscreen.show()" is unsupported on Android.');
},
hide: function () {
exec(null, null, 'CordovaSplashScreenPlugin', 'hide', []);
}
};
module.exports = splashscreen;

View File

@@ -0,0 +1,93 @@
/*
* 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 exec = require('cordova/exec');
var statusBarVisible = true;
var statusBar = {};
// This <script> element is explicitly used by Cordova's statusbar for computing color. (Do not use this element)
const statusBarScript = document.createElement('script');
document.head.appendChild(statusBarScript);
Object.defineProperty(statusBar, 'visible', {
configurable: false,
enumerable: true,
get: function () {
if (window.StatusBar) {
// try to let the StatusBar plugin handle it
return window.StatusBar.isVisible;
}
return statusBarVisible;
},
set: function (value) {
if (window.StatusBar) {
// try to let the StatusBar plugin handle it
if (value) {
window.StatusBar.show();
} else {
window.StatusBar.hide();
}
} else {
statusBarVisible = value;
exec(null, null, 'SystemBarPlugin', 'setStatusBarVisible', [!!value]);
}
}
});
Object.defineProperty(statusBar, 'setBackgroundColor', {
configurable: false,
enumerable: false,
writable: false,
value: function (value) {
statusBarScript.style.color = value;
var rgbStr = window.getComputedStyle(statusBarScript).getPropertyValue('color');
if (!rgbStr.match(/^rgb/)) { return; }
var rgbVals = rgbStr.match(/\d+/g).map(function (v) { return parseInt(v, 10); });
if (rgbVals.length < 3) {
return;
} else if (rgbVals.length === 3) {
rgbVals = [255].concat(rgbVals);
}
// TODO: Use `padStart(2, '0')` once SDK 24 is dropped.
const padRgb = (val) => {
const hex = val.toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
const a = padRgb(rgbVals[0]);
const r = padRgb(rgbVals[1]);
const g = padRgb(rgbVals[2]);
const b = padRgb(rgbVals[3]);
const hexStr = '#' + a + r + g + b;
if (window.StatusBar) {
window.StatusBar.backgroundColorByHexString(hexStr);
} else {
exec(null, null, 'SystemBarPlugin', 'setStatusBarBackgroundColor', rgbVals);
}
}
});
module.exports = statusBar;

66
eslint.config.js Normal file
View File

@@ -0,0 +1,66 @@
/**
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 { defineConfig, globalIgnores } = require('eslint/config');
const nodeConfig = require('@cordova/eslint-config/node');
const nodeTestConfig = require('@cordova/eslint-config/node-tests');
const browserConfig = require('@cordova/eslint-config/browser');
module.exports = defineConfig([
globalIgnores([
'**/coverage/',
'spec/fixtures/',
'templates/project/assets/www/cordova.js',
'test/android/app',
'test/androidx/app'
]),
{
// Include these JavaScript files that do not have file extensions.
files: [
'templates/cordova/version',
'templates/cordova/android_sdk_version',
'templates/cordova/lib/list-devices',
'templates/cordova/lib/list-emulator-images'
]
},
...nodeConfig,
...nodeTestConfig.map(config => ({
files: ['spec/**/*.js'],
...config,
rules: {
...(config.rules || {}),
'prefer-promise-reject-errors': 'off'
}
})),
...browserConfig.map(config => ({
files: [
'cordova-js-src/**/*.js',
'templates/project/assets/**/*.js'
],
...config,
languageOptions: {
...(config?.languageOptions || {}),
globals: {
...(config.languageOptions?.globals || {}),
require: 'readonly',
module: 'readonly'
}
}
}))
]);

View File

@@ -18,5 +18,6 @@
under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.apache.cordova" android:versionName="1.0" android:versionCode="1">
android:versionName="1.0"
android:versionCode="1">
</manifest>

View File

@@ -26,22 +26,32 @@ buildscript {
// Android Gradle Plugin (AGP) Build Tools
classpath "com.android.tools.build:gradle:${cordovaConfig.AGP_VERSION}"
}
cdvHelpers.verifyCordovaConfigForBuild()
}
allprojects {
apply from: 'repositories.gradle'
def hasRepositoriesGradle = file('repositories.gradle').exists()
if (hasRepositoriesGradle) {
apply from: 'repositories.gradle'
} else {
apply from: "${project.rootDir}/repositories.gradle"
}
repositories repos
}
apply plugin: 'com.android.library'
android {
compileSdkVersion cordovaConfig.SDK_VERSION
namespace = 'org.apache.cordova'
compileSdkVersion cordovaConfig.COMPILE_SDK_VERSION
buildToolsVersion cordovaConfig.BUILD_TOOLS_VERSION
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaLanguageVersion.of(cordovaConfig.JAVA_SOURCE_COMPATIBILITY)
targetCompatibility JavaLanguageVersion.of(cordovaConfig.JAVA_TARGET_COMPATIBILITY)
}
// For the Android Cordova Lib, we allow changing the minSdkVersion, but it is at the users own risk
@@ -67,11 +77,18 @@ android {
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/NOTICE'
}
publishing {
singleVariant('release') {
withSourcesJar()
}
}
}
dependencies {
implementation "androidx.appcompat:appcompat:${cordovaConfig.ANDROIDX_APP_COMPAT_VERSION}"
api "androidx.appcompat:appcompat:${cordovaConfig.ANDROIDX_APP_COMPAT_VERSION}"
implementation "androidx.webkit:webkit:${cordovaConfig.ANDROIDX_WEBKIT_VERSION}"
implementation "androidx.core:core-splashscreen:${cordovaConfig.ANDROIDX_CORE_SPLASHSCREEN_VERSION}"
}
/**

View File

@@ -1,13 +1,19 @@
{
"MIN_SDK_VERSION": 22,
"SDK_VERSION": 30,
"GRADLE_VERSION": "7.1.1",
"MIN_BUILD_TOOLS_VERSION": "30.0.3",
"AGP_VERSION": "4.2.2",
"KOTLIN_VERSION": "1.5.20",
"ANDROIDX_APP_COMPAT_VERSION": "1.3.0",
"ANDROIDX_WEBKIT_VERSION": "1.4.0",
"GRADLE_PLUGIN_GOOGLE_SERVICES_VERSION": "4.3.5",
"MIN_SDK_VERSION": 24,
"SDK_VERSION": 36,
"COMPILE_SDK_VERSION": null,
"GRADLE_VERSION": "8.14.2",
"MIN_BUILD_TOOLS_VERSION": "36.0.0",
"AGP_VERSION": "8.10.1",
"KOTLIN_VERSION": "2.1.21",
"ANDROIDX_APP_COMPAT_VERSION": "1.7.1",
"ANDROIDX_WEBKIT_VERSION": "1.14.0",
"ANDROIDX_CORE_SPLASHSCREEN_VERSION": "1.0.1",
"GRADLE_PLUGIN_GOOGLE_SERVICES_VERSION": "4.4.2",
"IS_GRADLE_PLUGIN_GOOGLE_SERVICES_ENABLED": false,
"IS_GRADLE_PLUGIN_KOTLIN_ENABLED": false
"IS_GRADLE_PLUGIN_KOTLIN_ENABLED": false,
"PACKAGE_NAMESPACE": "org.apache.cordova.hellocordova",
"JAVA_SOURCE_COMPATIBILITY": 11,
"JAVA_TARGET_COMPATIBILITY": 11,
"KOTLIN_JVM_TARGET": null
}

View File

@@ -46,73 +46,69 @@ if (project.hasProperty('signEnabled')) {
}
}
task sourcesJar(type: Jar) {
from android.sourceSets.main.java.srcDirs
classifier = 'sources'
}
afterEvaluate {
publishing {
publications {
mavenJava(MavenPublication) {
groupId = 'org.apache.cordova'
artifactId = 'framework'
version = getCordovaAndroidVersion()
publishing {
publications {
mavenJava(MavenPublication) {
groupId = 'org.apache.cordova'
artifactId = 'framework'
version = getCordovaAndroidVersion()
from components.release
artifact(sourcesJar)
artifact("$buildDir/outputs/aar/framework-release.aar")
pom {
name = 'Cordova'
description = 'A library to build Cordova-based projects for the Android platform.'
url = 'https://cordova.apache.org'
pom {
name = 'Cordova'
description = 'A library to build Cordova-based projects for the Android platform.'
url = 'https://cordova.apache.org'
licenses {
license {
name = 'Apache License, Version 2.0'
url = 'https://www.apache.org/licenses/LICENSE-2.0.txt'
licenses {
license {
name = 'Apache License, Version 2.0'
url = 'https://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
}
developers {
developer {
id = 'stevengill'
name = 'Steve Gill'
developers {
developer {
id = 'stevengill'
name = 'Steve Gill'
}
developer {
id = 'erisu'
name = 'Bryan Ellis'
email = 'erisu@apache.org'
}
}
developer {
id = 'erisu'
name = 'Bryan Ellis'
email = 'erisu@apache.org'
}
}
scm {
connection = 'scm:git:https://github.com/apache/cordova-android.git'
developerConnection = 'scm:git:git@github.com:apache/cordova-android.git'
url = 'https://github.com/apache/cordova-android'
scm {
connection = 'scm:git:https://github.com/apache/cordova-android.git'
developerConnection = 'scm:git:git@github.com:apache/cordova-android.git'
url = 'https://github.com/apache/cordova-android'
}
}
}
}
}
repositories {
maven {
def releasesRepoUrl = 'https://repository.apache.org/content/repositories/releases'
def snapshotsRepoUrl = 'https://repository.apache.org/content/repositories/snapshots'
repositories {
maven {
def releasesRepoUrl = 'https://repository.apache.org/content/repositories/releases'
def snapshotsRepoUrl = 'https://repository.apache.org/content/repositories/snapshots'
url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
credentials {
if (project.hasProperty('apacheUsername') && project.hasProperty('apachePassword')) {
username apacheUsername
password apachePassword
credentials {
if (project.hasProperty('apacheUsername') && project.hasProperty('apachePassword')) {
username apacheUsername
password apachePassword
}
}
}
}
}
signing {
if (Boolean.valueOf(cdvEnableSigning)) {
sign publishing.publications.mavenJava
signing {
if (Boolean.valueOf(cdvEnableSigning)) {
sign publishing.publications.mavenJava
}
}
}
}

View File

@@ -43,6 +43,10 @@ Boolean isVersionValid(version) {
return !(new Version(version)).isEqual('0.0.0')
}
Boolean isVersionGreaterThanEqual(versionX, versionY) {
return (new Version(versionX)) >= (new Version(versionY))
}
String doFindLatestInstalledBuildTools(String minBuildToolsVersionString) {
def buildToolsDirContents
try {
@@ -64,16 +68,16 @@ String doFindLatestInstalledBuildTools(String minBuildToolsVersionString) {
if (highestBuildToolsVersion == null) {
throw new RuntimeException("""
No installed build tools found. Install the Android build tools
version ${minBuildToolsVersionString} or higher.
No installed build tools found. Please install the Android build tools
version ${minBuildToolsVersionString}.
""".replaceAll(/\s+/, ' ').trim())
}
if (highestBuildToolsVersion.isLowerThan(minBuildToolsVersionString)) {
throw new RuntimeException("""
No usable Android build tools found. Highest ${minBuildToolsVersion.getMajor()}.x installed version is
${highestBuildToolsVersion.getOriginalString()}; minimum version
required is ${minBuildToolsVersionString}.
${highestBuildToolsVersion.getOriginalString()}; Recommended version
is ${minBuildToolsVersionString}.
""".replaceAll(/\s+/, ' ').trim())
}
@@ -83,9 +87,9 @@ String doFindLatestInstalledBuildTools(String minBuildToolsVersionString) {
String getAndroidSdkDir() {
def rootDir = project.rootDir
def androidSdkDir = null
String envVar = System.getenv("ANDROID_SDK_ROOT")
String envVar = System.getenv("ANDROID_HOME")
if (envVar == null) {
envVar = System.getenv("ANDROID_HOME")
envVar = System.getenv("ANDROID_SDK_ROOT")
}
def localProperties = new File(rootDir, 'local.properties')
@@ -125,14 +129,6 @@ def doExtractIntFromManifest(name) {
return new BigInteger(matcher.group(1))
}
def doExtractStringFromManifest(name) {
def manifestFile = file(android.sourceSets.main.manifest.srcFile)
def pattern = Pattern.compile(name + "=\"(\\S+)\"")
def matcher = pattern.matcher(manifestFile.getText())
matcher.find()
return matcher.group(1)
}
def doGetConfigXml() {
def xml = file("src/main/res/xml/config.xml").getText()
// Disable namespace awareness since Cordova doesn't use them properly
@@ -154,13 +150,16 @@ def doGetConfigPreference(name, defaultValue) {
}
def doApplyCordovaConfigCustomization() {
// Apply user overide properties that comes from the "--gradleArg=-P" parameters
// Apply user override properties that comes from the "--gradleArg=-P" parameters
if (project.hasProperty('cdvMinSdkVersion')) {
cordovaConfig.MIN_SDK_VERSION = Integer.parseInt('' + cdvMinSdkVersion)
}
if (project.hasProperty('cdvSdkVersion')) {
cordovaConfig.SDK_VERSION = Integer.parseInt('' + cdvSdkVersion)
}
if (project.hasProperty('cdvCompileSdkVersion')) {
cordovaConfig.COMPILE_SDK_VERSION = Integer.parseInt('' + cdvCompileSdkVersion)
}
if (project.hasProperty('cdvMaxSdkVersion')) {
cordovaConfig.MAX_SDK_VERSION = Integer.parseInt('' + cdvMaxSdkVersion)
}
@@ -190,6 +189,12 @@ def doApplyCordovaConfigCustomization() {
}
}
def doVerifyCordovaConfigForBuild() {
if (cordovaConfig.COMPILE_SDK_VERSION < cordovaConfig.SDK_VERSION) {
println "The \"compileSdkVersion\" (${cordovaConfig.COMPILE_SDK_VERSION}) should be greater than or equal to the the \"targetSdkVersion\" (${cordovaConfig.SDK_VERSION})."
}
}
// Properties exported here are visible to all plugins.
ext {
def defaultsFilePath = './cdv-gradle-config-defaults.json'
@@ -210,6 +215,10 @@ ext {
def jsonFile = new File(targetConfigFilePath)
cordovaConfig = new groovy.json.JsonSlurper().parseText(jsonFile.text)
if (cordovaConfig.COMPILE_SDK_VERSION == null) {
cordovaConfig.COMPILE_SDK_VERSION = cordovaConfig.SDK_VERSION
}
// Apply Gradle Properties
doApplyCordovaConfigCustomization()
@@ -218,7 +227,6 @@ ext {
privateHelpers.getProjectTarget = { doGetProjectTarget() }
privateHelpers.applyCordovaConfigCustomization = { doApplyCordovaConfigCustomization() }
privateHelpers.extractIntFromManifest = { name -> doExtractIntFromManifest(name) }
privateHelpers.extractStringFromManifest = { name -> doExtractStringFromManifest(name) }
privateHelpers.ensureValueExists = { filePath, props, key -> doEnsureValueExists(filePath, props, key) }
// These helpers can be used by plugins / projects and will not change.
@@ -227,13 +235,13 @@ ext {
cdvHelpers.getConfigXml = { doGetConfigXml() }
// Returns the value for the desired <preference>. Added in 4.1.0.
cdvHelpers.getConfigPreference = { name, defaultValue -> doGetConfigPreference(name, defaultValue) }
// Display warnings if any cordova config is not proper for build.
cdvHelpers.verifyCordovaConfigForBuild = { doVerifyCordovaConfigForBuild() }
}
buildscript {
repositories {
google()
mavenCentral()
}
apply from: 'repositories.gradle'
repositories repos
dependencies {
classpath 'io.github.g00fy2:versioncompare:1.4.1@jar'

View File

@@ -1,3 +1,20 @@
# 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.
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:

View File

@@ -1,3 +1,20 @@
# 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.
#Thu Nov 09 10:50:25 PST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists

View File

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

View File

@@ -39,7 +39,7 @@ public class AllowListPlugin extends CordovaPlugin {
// Used when instantiated via reflection by PluginManager
public AllowListPlugin() { }
// These can be used by embedders to allow Java-configuration of an allow list.
// These can be used by plugin developers to allow Java-configuration of an allow list.
public AllowListPlugin(Context context) {
this(new AllowList(), new AllowList(), null);
new CustomConfigXmlParser().parse(context);
@@ -82,11 +82,6 @@ public class AllowListPlugin extends CordovaPlugin {
if (strNode.equals("content")) {
String startPage = xml.getAttributeValue(null, "src");
allowedNavigations.addAllowListEntry(startPage, false);
// Allow origin for WebViewAssetLoader
if (!this.prefs.getBoolean("AndroidInsecureFileModeEnabled", false)) {
allowedNavigations.addAllowListEntry("https://" + this.prefs.getString("hostname", "localhost"), false);
}
} else if (strNode.equals("allow-navigation")) {
String origin = xml.getAttributeValue(null, "href");
if ("*".equals(origin)) {
@@ -127,7 +122,7 @@ public class AllowListPlugin extends CordovaPlugin {
@Override
public Boolean shouldAllowRequest(String url) {
return (this.shouldAllowNavigation(url) || this.allowedRequests.isUrlAllowListed(url))
return (Boolean.TRUE.equals(this.shouldAllowNavigation(url)) || this.allowedRequests.isUrlAllowListed(url))
? true
: null; // default policy
}

View File

@@ -37,8 +37,7 @@ public class AuthenticationToken {
/**
* Sets the user name.
*
* @param userName
* the new user name
* @param userName the new user name
*/
public void setUserName(String userName) {
this.userName = userName;
@@ -56,8 +55,7 @@ public class AuthenticationToken {
/**
* Sets the password.
*
* @param password
* the new password
* @param password the new password
*/
public void setPassword(String password) {
this.password = password;

View File

@@ -51,7 +51,8 @@ public class BuildHelper {
{
try
{
Class<?> clazz = Class.forName(ctx.getClass().getPackage().getName() + ".BuildConfig");
String packageName = ctx.getApplicationInfo().packageName;
Class<?> clazz = Class.forName(packageName + ".BuildConfig");
Field field = clazz.getField(key);
return field.get(null);
} catch (ClassNotFoundException e) {

View File

@@ -61,6 +61,15 @@ public class CallbackContext {
webView.sendPluginResult(pluginResult, callbackId);
}
/**
* Helper for success callbacks that just returns the Status.OK by default
*
* @param message The message to add to the success result.
*/
public void success(boolean message) {
sendPluginResult(new PluginResult(PluginResult.Status.OK, message));
}
/**
* Helper for success callbacks that just returns the Status.OK by default
*

View File

@@ -54,7 +54,7 @@ public class CallbackMap {
* obtained from registerCallback()
*
* @param mappedId The request code obtained from registerCallback()
* @return The CordovaPlugin and orignal request code that correspond to the
* @return The CordovaPlugin and original request code that correspond to the
* given mappedCode
*/
public synchronized Pair<CordovaPlugin, Integer> getAndRemoveCallback(int mappedId) {

View File

@@ -23,7 +23,7 @@ import java.util.List;
import android.app.Activity;
@Deprecated // Use AllowList, CordovaPrefences, etc. directly.
@Deprecated // Use AllowList, CordovaPreferences, etc. directly.
public class Config {
private static final String TAG = "Config";

View File

@@ -24,16 +24,20 @@ import java.util.ArrayList;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import android.content.Context;
public class ConfigXmlParser {
private static String TAG = "ConfigXmlParser";
private String launchUrl = null;
private static String SCHEME_HTTP = "http";
private static String SCHEME_HTTPS = "https";
private static String DEFAULT_HOSTNAME = "localhost";
private static final String DEFAULT_CONTENT_SRC = "index.html";
private String launchUrl;
private String contentSrc;
private CordovaPreferences prefs = new CordovaPreferences();
private ArrayList<PluginEntry> pluginEntries = new ArrayList<PluginEntry>(20);
@@ -47,11 +51,7 @@ public class ConfigXmlParser {
public String getLaunchUrl() {
if (launchUrl == null) {
launchUrl = "https://" + this.prefs.getString("hostname", "localhost");
}
if (this.prefs.getBoolean("AndroidInsecureFileModeEnabled", false)) {
launchUrl = "file:///android_asset/www/index.html";
setStartUrl(contentSrc);
}
return launchUrl;
@@ -77,6 +77,22 @@ public class ConfigXmlParser {
)
);
pluginEntries.add(
new PluginEntry(
SystemBarPlugin.PLUGIN_NAME,
"org.apache.cordova.SystemBarPlugin",
true
)
);
pluginEntries.add(
new PluginEntry(
SplashScreenPlugin.PLUGIN_NAME,
"org.apache.cordova.SplashScreenPlugin",
true
)
);
parse(action.getResources().getXml(id));
}
@@ -103,6 +119,18 @@ public class ConfigXmlParser {
e.printStackTrace();
}
}
onPostParse();
}
private void onPostParse() {
// After parsing, if contentSrc is still null, it signals
// that <content> tag was completely missing. In this case,
// default it.
// https://github.com/apache/cordova-android/issues/1432
if (contentSrc == null) {
contentSrc = DEFAULT_CONTENT_SRC;
}
}
public void handleStartTag(XmlPullParser xml) {
@@ -130,7 +158,10 @@ public class ConfigXmlParser {
else if (strNode.equals("content")) {
String src = xml.getAttributeValue(null, "src");
if (src != null) {
setStartUrl(src);
contentSrc = src;
} else {
// Default
contentSrc = DEFAULT_CONTENT_SRC;
}
}
}
@@ -147,20 +178,40 @@ public class ConfigXmlParser {
}
}
private String getLaunchUrlPrefix() {
if (prefs.getBoolean("AndroidInsecureFileModeEnabled", false)) {
return "file:///android_asset/www/";
} else {
String scheme = prefs.getString("scheme", SCHEME_HTTPS).toLowerCase();
String hostname = prefs.getString("hostname", DEFAULT_HOSTNAME).toLowerCase();
if (!scheme.contentEquals(SCHEME_HTTP) && !scheme.contentEquals(SCHEME_HTTPS)) {
LOG.d(TAG, "The provided scheme \"" + scheme + "\" is not valid. " +
"Defaulting to \"" + SCHEME_HTTPS + "\". " +
"(Valid Options=" + SCHEME_HTTP + "," + SCHEME_HTTPS + ")");
scheme = SCHEME_HTTPS;
}
return scheme + "://" + hostname + '/';
}
}
private void setStartUrl(String src) {
Pattern schemeRegex = Pattern.compile("^[a-z-]+://");
Matcher matcher = schemeRegex.matcher(src);
if (matcher.find()) {
launchUrl = src;
} else {
String launchUrlPrefix = getLaunchUrlPrefix();
// remove leading slash, "/", from content src if existing,
if (src.charAt(0) == '/') {
src = src.substring(1);
}
if (this.prefs.getBoolean("AndroidInsecureFileModeEnabled", false)) {
launchUrl = "file:///android_asset/www/" + src;
} else {
launchUrl = "https://" + this.prefs.getString("hostname", "localhost") + "/" + src;
}
launchUrl = launchUrlPrefix + src;
}
}
}

View File

@@ -29,9 +29,10 @@ import android.annotation.SuppressLint;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Color;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@@ -42,13 +43,18 @@ import android.webkit.WebViewClient;
import android.widget.FrameLayout;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.splashscreen.SplashScreen;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
/**
* This class is the main Android activity that represents the Cordova
* application. It should be extended by the user to load the specific
* html file that contains the application.
*
* As an example:
* <p>As an example:</p>
*
* <pre>
* package org.apache.cordova.examples;
@@ -67,17 +73,16 @@ import androidx.appcompat.app.AppCompatActivity;
* }
* </pre>
*
* Cordova xml configuration: Cordova uses a configuration file at
* res/xml/config.xml to specify its settings. See "The config.xml File"
* guide in cordova-docs at http://cordova.apache.org/docs for the documentation
* for the configuration. The use of the set*Property() methods is
* deprecated in favor of the config.xml file.
* <p>Cordova xml configuration: Cordova uses a configuration file at
* res/xml/config.xml to specify its settings. See the "Config.xml API" documentation for
* configuration details at <a href="https://cordova.apache.org/docs">Apache Cordova Docs</a>.</p>
*
* <p>The use of the set*Property() methods is deprecated in favor of the config.xml file.</p>
*/
public class CordovaActivity extends AppCompatActivity {
public static String TAG = "CordovaActivity";
// The webview for our app
// The WebView for our app
protected CordovaWebView appView;
private static int ACTIVITY_STARTING = 0;
@@ -98,14 +103,27 @@ public class CordovaActivity extends AppCompatActivity {
protected ArrayList<PluginEntry> pluginEntries;
protected CordovaInterfaceImpl cordovaInterface;
private SplashScreen splashScreen;
private boolean canEdgeToEdge = false;
private boolean isFullScreen = false;
/**
* Called when the activity is first created.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
// Handle the splash screen transition.
if (showInitialSplashScreen()) {
splashScreen = SplashScreen.installSplashScreen(this);
}
// need to activate preferences before super.onCreate to avoid "requestFeature() must be called before adding content" exception
loadConfig();
canEdgeToEdge = preferences.getBoolean("AndroidEdgeToEdge", false)
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM;
String logLevel = preferences.getString("loglevel", "ERROR");
LOG.setLogLevel(logLevel);
@@ -120,13 +138,14 @@ public class CordovaActivity extends AppCompatActivity {
LOG.d(TAG, "The SetFullscreen configuration is deprecated in favor of Fullscreen, and will be removed in a future version.");
preferences.set("Fullscreen", true);
}
if (preferences.getBoolean("Fullscreen", false)) {
isFullScreen = preferences.getBoolean("Fullscreen", false);
if (isFullScreen) {
// NOTE: use the FullscreenNotImmersive configuration key to set the activity in a REAL full screen
// (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,
@@ -153,6 +172,11 @@ public class CordovaActivity extends AppCompatActivity {
}
cordovaInterface.onCordovaInit(appView.getPluginManager());
// Setup the splash screen based on preference settings
if (showInitialSplashScreen()) {
cordovaInterface.pluginManager.postMessage("setupSplashScreen", splashScreen);
}
// Wire the hardware volume controls to control media if desired.
String volumePref = preferences.getString("DefaultVolumeStream", "");
if ("media".equals(volumePref.toLowerCase(Locale.ENGLISH))) {
@@ -174,32 +198,64 @@ public class CordovaActivity extends AppCompatActivity {
//Suppressing warnings in AndroidStudio
@SuppressWarnings({"deprecation", "ResourceType"})
protected void createViews() {
//Why are we setting a constant as the ID? This should be investigated
appView.getView().setId(100);
appView.getView().setLayoutParams(new FrameLayout.LayoutParams(
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
// Root FrameLayout
FrameLayout rootLayout = new FrameLayout(this);
rootLayout.setLayoutParams(new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
ViewGroup.LayoutParams.MATCH_PARENT
));
setContentView(appView.getView());
// WebView
View webView = appView.getView();
webView.setLayoutParams(new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
));
if (preferences.contains("BackgroundColor")) {
try {
int backgroundColor = preferences.getInteger("BackgroundColor", Color.BLACK);
// Background of activity:
appView.getView().setBackgroundColor(backgroundColor);
}
catch (NumberFormatException e){
e.printStackTrace();
}
}
// Create StatusBar view that will overlay the top inset
View statusBarView = new View(this);
statusBarView.setTag("statusBarView");
appView.getView().requestFocusFromTouch();
// Handle Window Insets
ViewCompat.setOnApplyWindowInsetsListener(rootLayout, (v, insets) -> {
Insets bars = insets.getInsets(
WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()
);
boolean isStatusBarVisible = statusBarView.getVisibility() != View.GONE;
int top = isStatusBarVisible && !canEdgeToEdge && !isFullScreen ? bars.top : 0;
int bottom = !canEdgeToEdge && !isFullScreen ? bars.bottom : 0;
int left = !canEdgeToEdge && !isFullScreen ? bars.left : 0;
int right = !canEdgeToEdge && !isFullScreen ? bars.right : 0;
FrameLayout.LayoutParams webViewParams = (FrameLayout.LayoutParams) webView.getLayoutParams();
webViewParams.setMargins(left, top, right, bottom);
webView.setLayoutParams(webViewParams);
FrameLayout.LayoutParams statusBarParams = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
top,
Gravity.TOP
);
statusBarView.setLayoutParams(statusBarParams);
return insets;
});
rootLayout.addView(webView);
rootLayout.addView(statusBarView);
setContentView(rootLayout);
rootLayout.post(() -> ViewCompat.requestApplyInsets(rootLayout));
webView.requestFocusFromTouch();
}
/**
* Construct the default web view object.
* <p/>
* Override this to customize the webview that is used.
* Override this to customize the WebView that is used.
*/
protected CordovaWebView makeWebView() {
return new CordovaWebViewImpl(makeWebViewEngine());
@@ -220,7 +276,7 @@ public class CordovaActivity extends AppCompatActivity {
}
/**
* Load the url into the webview.
* Load the url into the WebView.
*/
public void loadUrl(String url) {
if (appView == null) {
@@ -243,7 +299,7 @@ public class CordovaActivity extends AppCompatActivity {
if (this.appView != null) {
// CB-9382 If there is an activity that started for result and main activity is waiting for callback
// result, we shoudn't stop WebView Javascript timers, as activity for result might be using them
// result, we shouldn't stop WebView Javascript timers, as activity for result might be using them
boolean keepRunning = this.keepRunning || this.cordovaInterface.activityResultCallback != null;
this.appView.handlePause(keepRunning);
}
@@ -384,6 +440,7 @@ public class CordovaActivity extends AppCompatActivity {
if ((errorUrl != null) && (!failingUrl.equals(errorUrl)) && (appView != null)) {
// Load URL on UI thread
me.runOnUiThread(new Runnable() {
@Override
public void run() {
me.appView.showWebPage(errorUrl, false, true, null);
}
@@ -393,6 +450,7 @@ public class CordovaActivity extends AppCompatActivity {
else {
final boolean exit = !(errorCode == WebViewClient.ERROR_HOST_LOOKUP);
me.runOnUiThread(new Runnable() {
@Override
public void run() {
if (exit) {
me.appView.getView().setVisibility(View.GONE);
@@ -409,6 +467,7 @@ public class CordovaActivity extends AppCompatActivity {
public void displayError(final String title, final String message, final String button, final boolean exit) {
final CordovaActivity me = this;
me.runOnUiThread(new Runnable() {
@Override
public void run() {
try {
AlertDialog.Builder dlg = new AlertDialog.Builder(me);
@@ -417,6 +476,7 @@ public class CordovaActivity extends AppCompatActivity {
dlg.setCancelable(false);
dlg.setPositiveButton(button,
new AlertDialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
if (exit) {
@@ -481,6 +541,7 @@ public class CordovaActivity extends AppCompatActivity {
return null;
}
@Override
protected void onSaveInstanceState(Bundle outState) {
cordovaInterface.onSaveInstanceState(outState);
super.onSaveInstanceState(outState);
@@ -527,4 +588,19 @@ public class CordovaActivity extends AppCompatActivity {
}
/**
* Indicates whether to show the splash screen while the WebView is initially loading.
* <p>
* This method is available for native apps that embed a Cordova WebView.
* Native apps most likely already have their own splash screen setup.
* This option is not configurable for Cordova CLIcreated apps.
*
* @return {@code true}
* <p>
* To disable the initial splash screen, override this method and return {@code false}
* in your activity that extends {@link CordovaActivity}.
*/
protected boolean showInitialSplashScreen() {
return true;
}
}

View File

@@ -41,63 +41,70 @@ public class CordovaClientCertRequest implements ICordovaClientCertRequest {
* Cancel this request
*/
@SuppressLint("NewApi")
@Override
public void cancel()
{
request.cancel();
}
/*
* Returns the host name of the server requesting the certificate.
/**
* @return the host name of the server requesting the certificate.
*/
@SuppressLint("NewApi")
@Override
public String getHost()
{
return request.getHost();
}
/*
* Returns the acceptable types of asymmetric keys (can be null).
/**
* @return the acceptable types of asymmetric keys (can be null).
*/
@SuppressLint("NewApi")
@Override
public String[] getKeyTypes()
{
return request.getKeyTypes();
}
/*
* Returns the port number of the server requesting the certificate.
/**
* @return the port number of the server requesting the certificate.
*/
@SuppressLint("NewApi")
@Override
public int getPort()
{
return request.getPort();
}
/*
* Returns the acceptable certificate issuers for the certificate matching the private key (can be null).
/**
* @return the acceptable certificate issuers for the certificate matching the private key (can be null).
*/
@SuppressLint("NewApi")
@Override
public Principal[] getPrincipals()
{
return request.getPrincipals();
}
/*
/**
* Ignore the request for now. Do not remember user's choice.
*/
@SuppressLint("NewApi")
@Override
public void ignore()
{
request.ignore();
}
/*
/**
* Proceed with the specified private key and client certificate chain. Remember the user's positive choice and use it for future requests.
*
* @param privateKey The privateKey
* @param chain The certificate chain
*/
@SuppressLint("NewApi")
@Override
public void proceed(PrivateKey privateKey, X509Certificate[] chain)
{
request.proceed(privateKey, chain);

View File

@@ -43,18 +43,21 @@ public class CordovaDialogsHelper {
dlg.setCancelable(true);
dlg.setPositiveButton(android.R.string.ok,
new AlertDialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.gotResult(true, null);
}
});
dlg.setOnCancelListener(
new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
result.gotResult(false, null);
}
});
dlg.setOnKeyListener(new DialogInterface.OnKeyListener() {
//DO NOTHING
@Override
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK)
{
@@ -75,24 +78,28 @@ public class CordovaDialogsHelper {
dlg.setCancelable(true);
dlg.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.gotResult(true, null);
}
});
dlg.setNegativeButton(android.R.string.cancel,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.gotResult(false, null);
}
});
dlg.setOnCancelListener(
new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
result.gotResult(false, null);
}
});
dlg.setOnKeyListener(new DialogInterface.OnKeyListener() {
//DO NOTHING
@Override
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK)
{
@@ -111,8 +118,8 @@ public class CordovaDialogsHelper {
* If the client returns true, WebView will assume that the client will
* handle the prompt dialog and call the appropriate JsPromptResult method.
*
* Since we are hacking prompts for our own purposes, we should not be using them for
* this purpose, perhaps we should hack console.log to do this instead!
* <p>Since we are hacking prompts for our own purposes, we should not be using them for
* this purpose, perhaps we should hack console.log to do this instead!</p>
*/
public void showPrompt(String message, String defaultValue, final Result result) {
// Returning false would also show a dialog, but the default one shows the origin (ugly).
@@ -126,6 +133,7 @@ public class CordovaDialogsHelper {
dlg.setCancelable(false);
dlg.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String userText = input.getText().toString();
result.gotResult(true, userText);
@@ -133,6 +141,7 @@ public class CordovaDialogsHelper {
});
dlg.setNegativeButton(android.R.string.cancel,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.gotResult(false, null);
}

View File

@@ -35,6 +35,7 @@ public class CordovaHttpAuthHandler implements ICordovaHttpAuthHandler {
/**
* Instructs the WebView to cancel the authentication request.
*/
@Override
public void cancel () {
this.handler.cancel();
}
@@ -45,6 +46,7 @@ public class CordovaHttpAuthHandler implements ICordovaHttpAuthHandler {
* @param username
* @param password
*/
@Override
public void proceed (String username, String password) {
this.handler.proceed(username, password);
}

View File

@@ -51,7 +51,8 @@ public interface CordovaInterface {
/**
* Get the Android activity.
*
* If a custom engine lives outside of the Activity's lifecycle the return value may be null.
* <p>If a custom engine lives outside of the Activity's lifecycle the return value
* may be null.</p>
*
* @return the Activity
*/
@@ -74,7 +75,7 @@ public interface CordovaInterface {
public Object onMessage(String id, Object data);
/**
* Returns a shared thread pool that can be used for background tasks.
* @return a shared thread pool that can be used for background tasks.
*/
public ExecutorService getThreadPool();
@@ -89,7 +90,9 @@ public interface CordovaInterface {
public void requestPermissions(CordovaPlugin plugin, int requestCode, String [] permissions);
/**
* Check for a permission. Returns true if the permission is granted, false otherwise.
* Check for a permission.
*
* @return true if the permission is granted, false otherwise.
*/
public boolean hasPermission(String permission);

View File

@@ -135,7 +135,9 @@ public class CordovaInterfaceImpl implements CordovaInterface {
}
/**
* Routes the result to the awaiting plugin. Returns false if no plugin was waiting.
* Routes the result to the awaiting plugin.
*
* @return false if no plugin was waiting.
*/
public boolean onActivityResult(int requestCode, int resultCode, Intent intent) {
CordovaPlugin callback = activityResultCallback;
@@ -223,28 +225,23 @@ public class CordovaInterfaceImpl implements CordovaInterface {
}
}
@Override
public void requestPermission(CordovaPlugin plugin, int requestCode, String permission) {
String[] permissions = new String [1];
permissions[0] = permission;
requestPermissions(plugin, requestCode, permissions);
}
@SuppressLint("NewApi")
@SuppressLint("NewApi")
@Override
public void requestPermissions(CordovaPlugin plugin, int requestCode, String [] permissions) {
int mappedRequestCode = permissionResultCallbacks.registerCallback(plugin, requestCode);
getActivity().requestPermissions(permissions, mappedRequestCode);
}
@Override
public boolean hasPermission(String permission)
{
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
{
int result = activity.checkSelfPermission(permission);
return PackageManager.PERMISSION_GRANTED == result;
}
else
{
return true;
}
return PackageManager.PERMISSION_GRANTED == activity.checkSelfPermission(permission);
}
}

View File

@@ -31,6 +31,8 @@ import android.content.res.Configuration;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.webkit.RenderProcessGoneDetail;
import android.webkit.WebView;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -62,7 +64,11 @@ public class CordovaPlugin {
* Called after plugin construction and fields have been initialized.
* Prefer to use pluginInitialize instead since there is no value in
* having parameters on the initialize() function.
*
* @deprecated Use {@link #pluginInitialize()} instead. This method is no longer recommended
* and will be removed in future versions.
*/
@Deprecated
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
}
@@ -73,7 +79,7 @@ public class CordovaPlugin {
}
/**
* Returns the plugin's service name (what you'd use when calling pluginManger.getPlugin())
* @return the plugin's service name (what you'd use when calling pluginManger.getPlugin())
*/
public String getServiceName() {
return serviceName;
@@ -82,11 +88,14 @@ public class CordovaPlugin {
/**
* Executes the request.
*
* This method is called from the WebView thread. To do a non-trivial amount of work, use:
* cordova.getThreadPool().execute(runnable);
* <p>This method is called from the WebView thread. To do a non-trivial
* amount of work, use:</p>
*
* To run on the UI thread, use:
* cordova.getActivity().runOnUiThread(runnable);
* <pre>cordova.getThreadPool().execute(runnable);</pre>
*
* <p>To run on the UI thread, use:</p>
*
* <pre>cordova.getActivity().runOnUiThread(runnable);</pre>
*
* @param action The action to execute.
* @param rawArgs The exec() arguments in JSON form.
@@ -101,11 +110,13 @@ public class CordovaPlugin {
/**
* Executes the request.
*
* This method is called from the WebView thread. To do a non-trivial amount of work, use:
* cordova.getThreadPool().execute(runnable);
* <p>This method is called from the WebView thread. To do a non-trivial amount of work, use:</p>
*
* To run on the UI thread, use:
* cordova.getActivity().runOnUiThread(runnable);
* <pre>cordova.getThreadPool().execute(runnable);</pre>
*
* <p>To run on the UI thread, use:</p>
*
* <pre>cordova.getActivity().runOnUiThread(runnable);</pre>
*
* @param action The action to execute.
* @param args The exec() arguments.
@@ -120,10 +131,10 @@ public class CordovaPlugin {
/**
* Executes the request.
*
* This method is called from the WebView thread. To do a non-trivial amount of work, use:
* <p>This method is called from the WebView thread. To do a non-trivial amount of work, use:</p>
* cordova.getThreadPool().execute(runnable);
*
* To run on the UI thread, use:
* <p>To run on the UI thread, use:</p>
* cordova.getActivity().runOnUiThread(runnable);
*
* @param action The action to execute.
@@ -225,18 +236,18 @@ public class CordovaPlugin {
/**
* Hook for blocking the loading of external resources.
*
* This will be called when the WebView's shouldInterceptRequest wants to
* <p>This will be called when the WebView's shouldInterceptRequest wants to
* know whether to open a connection to an external resource. Return false
* to block the request: if any plugin returns false, Cordova will block
* the request. If all plugins return null, the default policy will be
* enforced. If at least one plugin returns true, and no plugins return
* false, then the request will proceed.
* false, then the request will proceed.</p>
*
* Note that this only affects resource requests which are routed through
* <p>Note that this only affects resource requests which are routed through
* WebViewClient.shouldInterceptRequest, such as XMLHttpRequest requests and
* img tag loads. WebSockets and media requests (such as <video> and <audio>
* tags) are not affected by this method. Use CSP headers to control access
* to such resources.
* to such resources.</p>
*/
public Boolean shouldAllowRequest(String url) {
return null;
@@ -244,13 +255,13 @@ public class CordovaPlugin {
/**
* Hook for blocking navigation by the Cordova WebView. This applies both to top-level and
* iframe navigations.
* iframe navigation.
*
* This will be called when the WebView's needs to know whether to navigate
* <p>This will be called when the WebView's needs to know whether to navigate
* to a new page. Return false to block the navigation: if any plugin
* returns false, Cordova will block the navigation. If all plugins return
* null, the default policy will be enforced. It at least one plugin returns
* true, and no plugins return false, then the navigation will proceed.
* true, and no plugins return false, then the navigation will proceed.</p>
*/
public Boolean shouldAllowNavigation(String url) {
return null;
@@ -268,12 +279,12 @@ public class CordovaPlugin {
/**
* Hook for blocking the launching of Intents by the Cordova application.
*
* This will be called when the WebView will not navigate to a page, but
* <p>This will be called when the WebView will not navigate to a page, but
* could launch an intent to handle the URL. Return false to block this: if
* any plugin returns false, Cordova will block the navigation. If all
* plugins return null, the default policy will be enforced. If at least one
* plugin returns true, and no plugins return false, then the URL will be
* opened.
* opened.</p>
*/
public Boolean shouldOpenExternalUrl(String url) {
return null;
@@ -282,8 +293,8 @@ public class CordovaPlugin {
/**
* Allows plugins to handle a link being clicked. Return true here to cancel the navigation.
*
* @param url The URL that is trying to be loaded in the Cordova webview.
* @return Return true to prevent the URL from loading. Default is false.
* @param url The URL that is trying to be loaded in the Cordova WebView.
* @return true to prevent the URL from loading. Default is false.
*/
public boolean onOverrideUrlLoading(String url) {
return false;
@@ -293,17 +304,20 @@ public class CordovaPlugin {
* Hook for redirecting requests. Applies to WebView requests as well as requests made by plugins.
* To handle the request directly, return a URI in the form:
*
* cdvplugin://pluginId/...
* <pre>cdvplugin://pluginId/...</pre>
*
* And implement handleOpenForRead().
* To make this easier, use the toPluginUri() and fromPluginUri() helpers:
* <p>And implement handleOpenForRead().</p>
*
* <p>To make this easier, use the toPluginUri() and fromPluginUri() helpers:</p>
*
* <pre>
* public Uri remapUri(Uri uri) { return toPluginUri(uri); }
*
* public CordovaResourceApi.OpenForReadResult handleOpenForRead(Uri uri) throws IOException {
* Uri origUri = fromPluginUri(uri);
* ...
* }
* </pre>
*/
public Uri remapUri(Uri uri) {
return null;
@@ -341,9 +355,9 @@ public class CordovaPlugin {
/**
* Called when the WebView does a top-level navigation or refreshes.
*
* Plugins should stop any long-running processes and clean up internal state.
* <p>Plugins should stop any long-running processes and clean up internal state.</p>
*
* Does nothing by default.
* <p>Does nothing by default.</p>
*/
public void onReset() {
}
@@ -356,9 +370,7 @@ public class CordovaPlugin {
* @param handler The HttpAuthHandler used to set the WebView's response
* @param host The host requiring authentication
* @param realm The realm for which authentication is required
*
* @return Returns True if plugin will resolve this auth challenge, otherwise False
*
* @return true if the plugin will resolve this auth challenge, else false
*/
public boolean onReceivedHttpAuthRequest(CordovaWebView view, ICordovaHttpAuthHandler handler, String host, String realm) {
return false;
@@ -370,9 +382,7 @@ public class CordovaPlugin {
*
* @param view The WebView that is initiating the callback
* @param request The client certificate request
*
* @return Returns True if plugin will resolve this auth challenge, otherwise False
*
* @return True if plugin will resolve this auth challenge, otherwise False
*/
public boolean onReceivedClientCertRequest(CordovaWebView view, ICordovaClientCertRequest request) {
return false;
@@ -390,20 +400,17 @@ public class CordovaPlugin {
* Called by the Plugin Manager when we need to actually request permissions
*
* @param requestCode Passed to the activity to track the request
*
* @return Returns the permission that was stored in the plugin
* @return The permission that was stored in the plugin
*/
public void requestPermissions(int requestCode) {
}
/*
/**
* Called by the WebView implementation to check for geolocation permissions, can be used
* by other Java methods in the event that a plugin is using this as a dependency.
*
* @return Returns true if the plugin has all the permissions it needs to operate.
* @return True if the plugin has all the permissions it needs to operate.
*/
public boolean hasPermisssion() {
return true;
}
@@ -414,7 +421,6 @@ public class CordovaPlugin {
* @param requestCode
* @param permissions
* @param grantResults
*
* @deprecated Use {@link #onRequestPermissionsResult} instead.
*/
@Deprecated
@@ -437,9 +443,27 @@ public class CordovaPlugin {
/**
* Allow plugins to supply a PathHandler for the WebViewAssetHandler
*
* @return a CordovaPluginPathHandler which listen for paths and returns a response
*/
public CordovaPluginPathHandler getPathHandler() {
return null;
}
/**
* Called when the WebView's render process has exited. Can be used to collect information
* regarding the crash for crashlytics or optionally attempt to gracefully handle/recover the
* crashed WebView by recreating it.
*
* <p>See <a href="https://developer.android.com/reference/android/webkit/WebViewClient#onRenderProcessGone(android.webkit.WebView,%20android.webkit.RenderProcessGoneDetail)">WebViewClient#onRenderProcessGone</a></p>
*
* <p>Note: A plugin must not attempt to recover a WebView that it does not own/manage.</p>
*
* @return true if the host application handled the situation that process has exited,
* otherwise, application will crash if render process crashed, or be killed
* if render process was killed by the system.
*/
public boolean onRenderProcessGone(final WebView view, RenderProcessGoneDetail detail) {
return false;
}
}

View File

@@ -45,21 +45,36 @@ import java.util.zip.GZIPInputStream;
/**
* What this class provides:
* 1. Helpers for reading & writing to URLs.
* - E.g. handles assets, resources, content providers, files, data URIs, http[s]
* - E.g. Can be used to query for mime-type & content length.
*
* 2. To allow plugins to redirect URLs (via remapUrl).
* - All plugins should call remapUrl() on URLs they receive from JS *before*
* passing the URL onto other utility functions in this class.
* - For an example usage of this, refer to the org.apache.cordova.file plugin.
* <ol>
* <li>
* Helpers for reading & writing to URLs.
* <ul>
* <li>E.g. handles assets, resources, content providers, files, data URIs, http[s]</li>
* <li>E.g. Can be used to query for mime-type & content length.</p></li>
* </ul>
* </li>
* <li>
* To allow plugins to redirect URLs (via remapUrl).
* <ul>
* <li>
* All plugins should call remapUrl() on URLs they receive from JS *before* passing the URL onto other utility functions in this class.
* </li>
* <li>For an example usage of this, refer to the org.apache.cordova.file plugin.</li>
* </ul>
* </li>
* </ol>
*
* Future Work:
* - Consider using a Cursor to query content URLs for their size (like the file plugin does).
* - Allow plugins to remapUri to "cdv-plugin://plugin-name/foo", which CordovaResourceApi
* would then delegate to pluginManager.getPlugin(plugin-name).openForRead(url)
* - Currently, plugins *can* do this by remapping to a data: URL, but it's inefficient
* for large payloads.
* <p>Future Work:</p>
* <ul>
* <li>Consider using a Cursor to query content URLs for their size (like the file plugin does).</li>
* <li>
* Allow plugins to remapUri to "cdv-plugin://plugin-name/foo", which CordovaResourceApi would then delegate to pluginManager.getPlugin(plugin-name).openForRead(url)
* <ul>
* <li>Currently, plugins *can* do this by remapping to a data: URL, but it's inefficient for large payloads.</li>
* </ul>
* </li>
* </ul>
*/
public class CordovaResourceApi {
@SuppressWarnings("unused")
@@ -143,8 +158,7 @@ public class CordovaResourceApi {
}
/**
* Returns a File that points to the resource, or null if the resource
* is not on the local filesystem.
* @return A file that points to the resource, or null if the resource is not on the local filesystem.
*/
public File mapUriToFile(Uri uri) {
assertBackgroundThread();
@@ -223,11 +237,12 @@ public class CordovaResourceApi {
/**
* Opens a stream to the given URI, also providing the MIME type & length.
*
* @return Never returns null.
* @throws Throws an InvalidArgumentException for relative URIs. Relative URIs should be
* resolved before being passed into this function.
* @throws Throws an IOException if the URI cannot be opened.
* @throws Throws an IllegalStateException if called on a foreground thread.
* @throws IllegalArgumentException For relative URIs. Relative URIs should be resolved before
* being passed into this function.
* @throws IOException If the URI cannot be opened.
* @throws IllegalStateException If called on a foreground thread.
*/
public OpenForReadResult openForRead(Uri uri) throws IOException {
return openForRead(uri, false);
@@ -235,11 +250,12 @@ public class CordovaResourceApi {
/**
* Opens a stream to the given URI, also providing the MIME type & length.
*
* @return Never returns null.
* @throws Throws an InvalidArgumentException for relative URIs. Relative URIs should be
* resolved before being passed into this function.
* @throws Throws an IOException if the URI cannot be opened.
* @throws Throws an IllegalStateException if called on a foreground thread and skipThreadCheck is false.
* @throws IllegalArgumentException For relative URIs. Relative URIs should be resolved before
* being passed into this function.
* @throws IOException If the URI cannot be opened.
* @throws IllegalStateException If called on a foreground thread and skipThreadCheck is false.
*/
public OpenForReadResult openForRead(Uri uri, boolean skipThreadCheck) throws IOException {
if (!skipThreadCheck) {
@@ -320,10 +336,11 @@ public class CordovaResourceApi {
/**
* Opens a stream to the given URI.
*
* @return Never returns null.
* @throws Throws an InvalidArgumentException for relative URIs. Relative URIs should be
* resolved before being passed into this function.
* @throws Throws an IOException if the URI cannot be opened.
* @throws IllegalArgumentException For relative URIs. Relative URIs should be resolved before
* being passed into this function.
* @throws IOException If the URI cannot be opened.
*/
public OutputStream openOutputStream(Uri uri, boolean append) throws IOException {
assertBackgroundThread();

View File

@@ -25,13 +25,13 @@ import android.view.View;
import android.webkit.WebChromeClient.CustomViewCallback;
/**
* Main interface for interacting with a Cordova webview - implemented by CordovaWebViewImpl.
* Main interface for interacting with a Cordova WebView - implemented by CordovaWebViewImpl.
* This is an interface so that it can be easily mocked in tests.
* Methods may be added to this interface without a major version bump, as plugins & embedders
* Methods may be added to this interface without a major version bump, as plugins/developer
* are not expected to implement it.
*/
public interface CordovaWebView {
public static final String CORDOVA_VERSION = "10.0.1";
public static final String CORDOVA_VERSION = "15.0.0-dev";
void init(CordovaInterface cordova, List<PluginEntry> pluginEntries, CordovaPreferences preferences);
@@ -70,34 +70,47 @@ public interface CordovaWebView {
/**
* Send JavaScript statement back to JavaScript.
*
* Deprecated (https://issues.apache.org/jira/browse/CB-6851)
* <p>Deprecated (<a href="https://issues.apache.org/jira/browse/CB-6851">CB-6851</a>)
* Instead of executing snippets of JS, you should use the exec bridge
* to create a Java->JS communication channel.
* To do this:
* 1. Within plugin.xml (to have your JS run before deviceready):
* <js-module><runs/></js-module>
* 2. Within your .js (call exec on start-up):
* to create a Java->JS communication channel.</p>
*
* <p>To do this:</p>
*
* <p>1. Within plugin.xml (to have your JS run before deviceready):</p>
*
* <pre>
* <js-module><runs/></js-module>
* </pre>
*
* <p>2. Within your .js (call exec on start-up):</p>
*
* <pre>
* require('cordova/channel').onCordovaReady.subscribe(function() {
* require('cordova/exec')(win, null, 'Plugin', 'method', []);
* function win(message) {
* ... process message from java here ...
* }
* });
* 3. Within your .java:
* </pre>
*
* <p>3. Within your .java:</p>
*
* <pre>
* PluginResult dataResult = new PluginResult(PluginResult.Status.OK, CODE);
* dataResult.setKeepCallback(true);
* savedCallbackContext.sendPluginResult(dataResult);
* </pre>
*/
@Deprecated
void sendJavascript(String statememt);
/**
* Load the specified URL in the Cordova webview or a new browser instance.
* Load the specified URL in the Cordova WebView or a new browser instance.
*
* NOTE: If openExternal is false, only allow listed URLs can be loaded.
* <p>NOTE: If openExternal is false, only allow listed URLs can be loaded.</p>
*
* @param url The url to load.
* @param openExternal Load url in browser instead of Cordova webview.
* @param openExternal Load url in browser instead of Cordova WebView.
* @param clearHistory Clear the history stack, so new page becomes top of history
* @param params Parameters for new app
*/

View File

@@ -59,7 +59,7 @@ public interface CordovaWebViewEngine {
/** Clean up all resources associated with the WebView. */
void destroy();
/** Add the evaulate Javascript method **/
/** Add the evaluate Javascript method **/
void evaluateJavascript(String js, ValueCallback<String> callback);
/**

View File

@@ -43,7 +43,7 @@ import java.util.Map;
import java.util.Set;
/**
* Main class for interacting with a Cordova webview. Manages plugins, events, and a CordovaWebViewEngine.
* Main class for interacting with a Cordova WebView. Manages plugins, events, and a CordovaWebViewEngine.
* Class uses two-phase initialization. You must call init() before calling any other methods.
*/
public class CordovaWebViewImpl implements CordovaWebView {
@@ -115,7 +115,7 @@ public class CordovaWebViewImpl implements CordovaWebView {
// This isn't enforced by the compiler, so assert here.
assert engine.getView() instanceof CordovaWebViewEngine.EngineView;
pluginManager.addService(CoreAndroid.PLUGIN_NAME, "org.apache.cordova.CoreAndroid");
pluginManager.addService(CoreAndroid.PLUGIN_NAME, "org.apache.cordova.CoreAndroid", true);
pluginManager.init();
}
@@ -149,11 +149,12 @@ public class CordovaWebViewImpl implements CordovaWebView {
// Timeout error method
final Runnable loadError = new Runnable() {
@Override
public void run() {
stopLoading();
LOG.e(TAG, "CordovaWebView: TIMEOUT ERROR!");
// Handle other errors by passing them to the webview in JS
// Handle other errors by passing them to the WebView in JS
JSONObject data = new JSONObject();
try {
data.put("errorCode", -6);
@@ -168,6 +169,7 @@ public class CordovaWebViewImpl implements CordovaWebView {
// Timeout timer method
final Runnable timeoutCheck = new Runnable() {
@Override
public void run() {
try {
synchronized (this) {
@@ -189,6 +191,7 @@ public class CordovaWebViewImpl implements CordovaWebView {
if (cordova.getActivity() != null) {
final boolean _recreatePlugins = recreatePlugins;
cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (loadUrlTimeoutValue > 0) {
cordova.getThreadPool().execute(timeoutCheck);
@@ -216,7 +219,7 @@ public class CordovaWebViewImpl implements CordovaWebView {
engine.clearHistory();
}
// If loading into our webview
// If loading into our WebView
if (!openExternal) {
// Make sure url is in allow list
if (pluginManager.shouldAllowNavigation(url)) {
@@ -484,7 +487,7 @@ public class CordovaWebViewImpl implements CordovaWebView {
// If app doesn't want to run in background
if (!keepRunning) {
// Pause JavaScript timers. This affects all webviews within the app!
// Pause JavaScript timers. This affects all WebViews within the app!
engine.setPaused(true);
}
}
@@ -494,7 +497,7 @@ public class CordovaWebViewImpl implements CordovaWebView {
return;
}
// Resume JavaScript timers. This affects all webviews within the app!
// Resume JavaScript timers. This affects all WebViews within the app!
engine.setPaused(false);
this.pluginManager.onResume(keepRunning);
@@ -534,7 +537,7 @@ public class CordovaWebViewImpl implements CordovaWebView {
// We should use a blank data: url instead so it's more obvious
this.loadUrl("about:blank");
// TODO: Should not destroy webview until after about:blank is done loading.
// TODO: Should not destroy WebView until after about:blank is done loading.
engine.destroy();
hideCustomView();
}
@@ -579,11 +582,13 @@ public class CordovaWebViewImpl implements CordovaWebView {
// Make app visible after 2 sec in case there was a JS error and Cordova JS never initialized correctly
if (engine.getView().getVisibility() != View.VISIBLE) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
if (cordova.getActivity() != null) {
cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
pluginManager.postMessage("spinner", "stop");
}

View File

@@ -19,6 +19,8 @@
package org.apache.cordova;
import org.apache.cordova.BuildHelper;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -26,11 +28,13 @@ import org.json.JSONObject;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.content.IntentFilter;
import android.telephony.TelephonyManager;
import android.view.KeyEvent;
import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedDispatcher;
import java.lang.reflect.Field;
import java.util.HashMap;
/**
@@ -44,7 +48,9 @@ public class CoreAndroid extends CordovaPlugin {
private CallbackContext messageChannel;
private PluginResult pendingResume;
private PluginResult pendingPause;
private OnBackInvokedCallback backCallback;
private final Object messageChannelLock = new Object();
private final Object backButtonHandlerLock = new Object();
/**
* Send an event to be fired on the Javascript side.
@@ -62,16 +68,19 @@ public class CoreAndroid extends CordovaPlugin {
@Override
public void pluginInitialize() {
this.initTelephonyReceiver();
backCallback = null;
}
/**
* Executes the request and returns PluginResult.
*
* @param action The action to execute.
* @param args JSONArry of arguments for the plugin.
* @param args JSONArray of arguments for the plugin.
* @param callbackContext The callback context from which we were invoked.
*
* @return A PluginResult object with a status and message.
*/
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
PluginResult.Status status = PluginResult.Status.OK;
String result = "";
@@ -81,10 +90,11 @@ public class CoreAndroid extends CordovaPlugin {
this.clearCache();
}
else if (action.equals("show")) {
// This gets called from JavaScript onCordovaReady to show the webview.
// This gets called from JavaScript onCordovaReady to show the WebView.
// I recommend we change the name of the Message as spinner/stop is not
// indicative of what this actually does (shows the webview).
// indicative of what this actually does (shows the WebView).
cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
webView.getPluginManager().postMessage("spinner", "stop");
}
@@ -143,6 +153,7 @@ public class CoreAndroid extends CordovaPlugin {
*/
public void clearCache() {
cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
webView.clearCache();
}
@@ -150,7 +161,7 @@ public class CoreAndroid extends CordovaPlugin {
}
/**
* Load the url into the webview.
* Load the url into the WebView.
*
* @param url
* @param props Properties that can be passed in to the Cordova activity (i.e. loadingDialog, wait, ...)
@@ -214,6 +225,7 @@ public class CoreAndroid extends CordovaPlugin {
*/
public void clearHistory() {
cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
webView.clearHistory();
}
@@ -226,6 +238,7 @@ public class CoreAndroid extends CordovaPlugin {
*/
public void backHistory() {
cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
webView.backHistory();
}
@@ -240,6 +253,29 @@ public class CoreAndroid extends CordovaPlugin {
*/
public void overrideBackbutton(boolean override) {
LOG.i("App", "WARNING: Back Button Default Behavior will be overridden. The backbutton event will be fired!");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
if (override) {
synchronized (backButtonHandlerLock) {
if (backCallback == null) {
// The callback is intentionally empty. Since API 36, intercepting the back button is ignored, which means
// the onDispatchKeyEvent boolean result won't actually stop native from consuming the back button and doing
// it's own logic, UNLESS if there is an OnBackInvokedCallback registered on the dispatcher.
// The key dispatch events will still fire, which still handles propagating back button events to the webview.
// See https://developer.android.com/about/versions/16/behavior-changes-16#predictive-back for more info on the caveat.
backCallback = () -> {};
this.cordova.getActivity().getOnBackInvokedDispatcher().registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, backCallback);
}
}
} else {
synchronized (backButtonHandlerLock) {
if (backCallback != null) {
this.cordova.getActivity().getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(backCallback);
backCallback = null;
}
}
}
}
webView.setButtonPlumbedToJs(KeyEvent.KEYCODE_BACK, override);
}
@@ -348,10 +384,10 @@ public class CoreAndroid extends CordovaPlugin {
}
}
/*
/**
* Unregister the receiver
*
*/
@Override
public void onDestroy()
{
webView.getContext().unregisterReceiver(this.telephonyReceiver);
@@ -376,35 +412,19 @@ public class CoreAndroid extends CordovaPlugin {
}
}
/*
/*
* This needs to be implemented if you wish to use the Camera Plugin or other plugins
* that read the Build Configuration.
*
* Thanks to Phil@Medtronic and Graham Borland for finding the answer and posting it to
* StackOverflow. This is annoying as hell!
*
* @deprecated Use {@link BuildHelper#getBuildConfigValue} instead.
*/
@Deprecated
public static Object getBuildConfigValue(Context ctx, String key)
{
try
{
Class<?> clazz = Class.forName(ctx.getClass().getPackage().getName() + ".BuildConfig");
Field field = clazz.getField(key);
return field.get(null);
} catch (ClassNotFoundException e) {
LOG.d(TAG, "Unable to get the BuildConfig, is this built with ANT?");
e.printStackTrace();
} catch (NoSuchFieldException e) {
LOG.d(TAG, key + " is not a valid field. Check your build.gradle");
} catch (IllegalAccessException e) {
LOG.d(TAG, "Illegal Access Exception: Let's print a stack trace.");
e.printStackTrace();
} catch (NullPointerException e) {
LOG.d(TAG, "Null Pointer Exception: Let's print a stack trace.");
e.printStackTrace();
}
return null;
LOG.w(TAG, "CoreAndroid.getBuildConfigValue is deprecated and will be removed in a future release. Use BuildHelper.getBuildConfigValue instead.");
return BuildHelper.getBuildConfigValue(ctx, key);
}
}

View File

@@ -31,32 +31,32 @@ public interface ICordovaClientCertRequest {
*/
public void cancel();
/*
* Returns the host name of the server requesting the certificate.
/**
* @return the host name of the server requesting the certificate.
*/
public String getHost();
/*
* Returns the acceptable types of asymmetric keys (can be null).
/**
* @return the acceptable types of asymmetric keys (can be null).
*/
public String[] getKeyTypes();
/*
* Returns the port number of the server requesting the certificate.
/**
* @return the port number of the server requesting the certificate.
*/
public int getPort();
/*
* Returns the acceptable certificate issuers for the certificate matching the private key (can be null).
/**
* @return the acceptable certificate issuers for the certificate matching the private key (can be null).
*/
public Principal[] getPrincipals();
/*
/**
* Ignore the request for now. Do not remember user's choice.
*/
public void ignore();
/*
/**
* Proceed with the specified private key and client certificate chain. Remember the user's positive choice and use it for future requests.
*
* @param privateKey The privateKey

View File

@@ -23,8 +23,8 @@ import android.util.Log;
/**
* Log to Android logging system.
*
* Log message can be a string or a printf formatted string with arguments.
* See http://developer.android.com/reference/java/util/Formatter.html
* <p>Log message can be a string or a printf formatted string with arguments.
* See <a href="http://developer.android.com/reference/java/util/Formatter.html">Formatter</a></p>
*/
public class LOG {

View File

@@ -124,12 +124,10 @@ public class NativeToJsMessageQueue {
}
/**
* Combines and returns queued messages combined into a single string.
*
* Combines as many messages as possible, without exceeding
* COMBINED_RESPONSE_CUTOFF in case of multiple response messages.
*
* Returns null if the queue is empty.
* @return a string of queued messages combined or null if the queue is empty.
*/
public String popAndEncode(boolean fromOnlineEvent) {
synchronized (this) {
@@ -206,7 +204,7 @@ public class NativeToJsMessageQueue {
}
}
if (!willSendAllMessages) {
sb.append("window.setTimeout(function(){cordova.require('cordova/plugin/android/polling').pollOnce();},0);");
sb.append("window.setTimeout(function(){cordova.require('cordova/exec').pollOnce();},0);");
}
for (int i = willSendAllMessages ? 1 : 0; i < numMessagesToSend; ++i) {
sb.append('}');
@@ -302,6 +300,7 @@ public class NativeToJsMessageQueue {
@Override
public void onNativeToJsMessageAvailable(final NativeToJsMessageQueue queue) {
cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
String js = queue.popAndEncodeAsJs();
if (js != null) {
@@ -330,6 +329,7 @@ public class NativeToJsMessageQueue {
@Override
public void reset() {
delegate.runOnUiThread(new Runnable() {
@Override
public void run() {
online = false;
// If the following call triggers a notifyOfFlush, then ignore it.
@@ -342,6 +342,7 @@ public class NativeToJsMessageQueue {
@Override
public void onNativeToJsMessageAvailable(final NativeToJsMessageQueue queue) {
delegate.runOnUiThread(new Runnable() {
@Override
public void run() {
if (!queue.isEmpty()) {
ignoreNextFlush = false;
@@ -372,6 +373,7 @@ public class NativeToJsMessageQueue {
@Override
public void onNativeToJsMessageAvailable(final NativeToJsMessageQueue queue) {
cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
String js = queue.popAndEncodeAsJs();
if (js != null) {

View File

@@ -66,7 +66,6 @@ public class PermissionHelper {
*
* @param plugin The plugin the permission is being checked against
* @param permission The permission to be checked
*
* @return True if the permission has already been granted and false otherwise
*/
public static boolean hasPermission(CordovaPlugin plugin, String permission) {

View File

@@ -48,36 +48,36 @@ public final class PluginEntry {
/**
* Constructs with a CordovaPlugin already instantiated.
*
* @param service The name of the service
* @param pluginClass The plugin class name
* @param service The name of the service
* @param plugin The plugin class name
*/
public PluginEntry(String service, CordovaPlugin plugin) {
this(service, plugin.getClass().getName(), true, plugin);
}
/**
* @param service The name of the service
* @param plugin The CordovaPlugin already instantiated
* @param onload Create plugin object when HTML page is loaded
* @param service The name of the service
* @param plugin The CordovaPlugin already instantiated
* @param onload Create plugin object when HTML page is loaded
*/
public PluginEntry(String service, CordovaPlugin plugin, boolean onload) {
this(service, plugin.getClass().getName(), onload, plugin);
}
/**
* @param service The name of the service
* @param pluginClass The plugin class name
* @param onload Create plugin object when HTML page is loaded
* @param service The name of the service
* @param pluginClass The plugin class name
* @param onload Create plugin object when HTML page is loaded
*/
public PluginEntry(String service, String pluginClass, boolean onload) {
this(service, pluginClass, onload, null);
}
/**
* @param service The name of the service
* @param pluginClass The plugin class name
* @param onload Create plugin object when HTML page is loaded
* @param plugin The CordovaPlugin already instantiated
* @param service The name of the service
* @param pluginClass The plugin class name
* @param onload Create plugin object when HTML page is loaded
* @param plugin The CordovaPlugin already instantiated
*/
private PluginEntry(String service, String pluginClass, boolean onload, CordovaPlugin plugin) {
this.service = service;

View File

@@ -32,15 +32,23 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.Debug;
import android.os.Build;
import android.webkit.RenderProcessGoneDetail;
import android.webkit.WebView;
/**
* PluginManager is exposed to JavaScript in the Cordova WebView.
*
* Calling native plugin code can be done by calling PluginManager.exec(...)
* from JavaScript.
* <p>Calling native plugin code can be done by calling PluginManager.exec(...)
* from JavaScript.</p>
*/
public class PluginManager {
private static String TAG = "PluginManager";
// @todo same as ConfigXmlParser. Research centralizing ideas, maybe create CordovaConstants
private static String SCHEME_HTTPS = "https";
// @todo same as ConfigXmlParser. Research centralizing ideas, maybe create CordovaConstants
private static String DEFAULT_HOSTNAME = "localhost";
private static final int SLOW_EXEC_WARNING_THRESHOLD = Debug.isDebuggerConnected() ? 60 : 16;
// List of service entries
@@ -79,7 +87,7 @@ public class PluginManager {
}
/**
* Init when loading a new HTML page into webview.
* Init when loading a new HTML page into WebView.
*/
public void init() {
LOG.d(TAG, "init()");
@@ -113,9 +121,9 @@ public class PluginManager {
* Receives a request for execution and fulfills it by finding the appropriate
* Java class and calling it's execute method.
*
* PluginManager.exec can be used either synchronously or async. In either case, a JSON encoded
* <p>PluginManager.exec can be used either synchronously or async. In either case, a JSON encoded
* string is returned that will indicate if any errors have occurred when trying to find
* or execute the class denoted by the clazz argument.
* or execute the class denoted by the clazz argument.</p>
*
* @param service String containing the service to run
* @param action String containing the action that the class is supposed to perform. This is
@@ -191,7 +199,18 @@ public class PluginManager {
* @param className The plugin class name
*/
public void addService(String service, String className) {
PluginEntry entry = new PluginEntry(service, className, false);
addService(service, className, false);
}
/**
* Add a plugin class that implements a service to the service entry table.
*
* @param service The service name
* @param className The plugin class name
* @param onload If true, the plugin will be instantiated immediately
*/
public void addService(String service, String className, boolean onload) {
PluginEntry entry = new PluginEntry(service, className, onload);
this.addService(entry);
}
@@ -233,9 +252,7 @@ public class PluginManager {
* @param handler The HttpAuthHandler used to set the WebView's response
* @param host The host requiring authentication
* @param realm The realm for which authentication is required
*
* @return Returns True if there is a plugin which will resolve this auth challenge, otherwise False
*
* @return True if there is a plugin which will resolve this auth challenge, otherwise False
*/
public boolean onReceivedHttpAuthRequest(CordovaWebView view, ICordovaHttpAuthHandler handler, String host, String realm) {
synchronized (this.pluginMap) {
@@ -254,9 +271,7 @@ public class PluginManager {
*
* @param view The WebView that is initiating the callback
* @param request The client certificate request
*
* @return Returns True if plugin will resolve this auth challenge, otherwise False
*
* @return True if plugin will resolve this auth challenge, otherwise False
*/
public boolean onReceivedClientCertRequest(CordovaWebView view, ICordovaClientCertRequest request) {
synchronized (this.pluginMap) {
@@ -333,22 +348,11 @@ public class PluginManager {
public Object postMessage(String id, Object data) {
LOG.d(TAG, "postMessage: " + id);
synchronized (this.pluginMap) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
this.pluginMap.forEach((s, plugin) -> {
if (plugin != null) {
plugin.onMessage(id, data);
}
});
} else {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
Object obj = plugin.onMessage(id, data);
if (obj != null) {
return obj;
}
}
this.pluginMap.forEach((s, plugin) -> {
if (plugin != null) {
plugin.onMessage(id, data);
}
}
});
}
return ctx.onMessage(id, data);
}
@@ -367,14 +371,34 @@ public class PluginManager {
}
/**
* Called when the webview is going to request an external resource.
* TODO: should we move this somewhere public and accessible by all plugins?
*
* This delegates to the installed plugins, and returns true/false for the
* <p>For now, it is placed where it is used and kept private so we can decide later and move without causing a breaking change.
* An ideal location might be in the "ConfigXmlParser" at the time it generates the "launchUrl".</p>
*
* TODO: should we be restrictive on the "file://" return? e.g. "file:///android_asset/www/"
*
* <p>Would be considered as a breaking change if we apply a more granular check.</p>
*/
private String getLaunchUrlPrefix() {
if (!app.getPreferences().getBoolean("AndroidInsecureFileModeEnabled", false)) {
String scheme = app.getPreferences().getString("scheme", SCHEME_HTTPS).toLowerCase();
String hostname = app.getPreferences().getString("hostname", DEFAULT_HOSTNAME).toLowerCase();
return scheme + "://" + hostname + '/';
}
return "file://";
}
/**
* Called when the WebView is going to request an external resource.
*
* <p>This delegates to the installed plugins, and returns true/false for the
* first plugin to provide a non-null result. If no plugins respond, then
* the default policy is applied.
* the default policy is applied.</p>
*
* @param url The URL that is being requested.
* @return Returns true to allow the resource to load,
* @return true to allow the resource to load,
* false to block the resource.
*/
public boolean shouldAllowRequest(String url) {
@@ -399,7 +423,7 @@ public class PluginManager {
return true;
}
if (url.startsWith("file://")) {
//This directory on WebKit/Blink based webviews contains SQLite databases!
//This directory on WebKit/Blink based WebViews contains SQLite databases!
//DON'T CHANGE THIS UNLESS YOU KNOW WHAT YOU'RE DOING!
return !url.contains("/app_webview/");
}
@@ -407,14 +431,14 @@ public class PluginManager {
}
/**
* Called when the webview is going to change the URL of the loaded content.
* Called when the WebView is going to change the URL of the loaded content.
*
* This delegates to the installed plugins, and returns true/false for the
* <p>This delegates to the installed plugins, and returns true/false for the
* first plugin to provide a non-null result. If no plugins respond, then
* the default policy is applied.
* the default policy is applied.</p>
*
* @param url The URL that is being requested.
* @return Returns true to allow the navigation,
* @return true to allow the navigation,
* false to block the navigation.
*/
public boolean shouldAllowNavigation(String url) {
@@ -431,12 +455,12 @@ public class PluginManager {
}
// Default policy:
return url.startsWith("file://") || url.startsWith("about:blank");
return url.startsWith(getLaunchUrlPrefix()) || url.startsWith("about:blank");
}
/**
* Called when the webview is requesting the exec() bridge be enabled.
* Called when the WebView is requesting the exec() bridge be enabled.
*/
public boolean shouldAllowBridgeAccess(String url) {
synchronized (this.entryMap) {
@@ -452,19 +476,19 @@ public class PluginManager {
}
// Default policy:
return url.startsWith("file://");
return url.startsWith(getLaunchUrlPrefix());
}
/**
* Called when the webview is going not going to navigate, but may launch
* Called when the WebView is going not going to navigate, but may launch
* an Intent for an URL.
*
* This delegates to the installed plugins, and returns true/false for the
* <p>This delegates to the installed plugins, and returns true/false for the
* first plugin to provide a non-null result. If no plugins respond, then
* the default policy is applied.
* the default policy is applied.</p>
*
* @param url The URL that is being requested.
* @return Returns true to allow the URL to launch an intent,
* @return true to allow the URL to launch an intent,
* false to block the intent.
*/
public Boolean shouldOpenExternalUrl(String url) {
@@ -485,7 +509,7 @@ public class PluginManager {
}
/**
* Called when the URL of the webview changes.
* Called when the URL of the WebView changes.
*
* @param url The URL that is being changed to.
* @return Return false to allow the URL to load, return true to prevent the URL from loading.
@@ -540,7 +564,7 @@ public class PluginManager {
c = Class.forName(className);
}
if (c != null & CordovaPlugin.class.isAssignableFrom(c)) {
ret = (CordovaPlugin) c.newInstance();
ret = (CordovaPlugin) c.getDeclaredConstructor().newInstance();
}
} catch (Exception e) {
e.printStackTrace();
@@ -593,4 +617,29 @@ public class PluginManager {
}
return handlers;
}
/**
* Called when the WebView's render process has exited.
*
* <p>See <a href="https://developer.android.com/reference/android/webkit/WebViewClient#onRenderProcessGone(android.webkit.WebView,%20android.webkit.RenderProcessGoneDetail)">WebViewClient#onRenderProcessGone</a></p>
*
* @return true if the host application handled the situation that process has exited,
* otherwise, application will crash if render process crashed, or be killed
* if render process was killed by the system.
*/
public boolean onRenderProcessGone(final WebView view, RenderProcessGoneDetail detail) {
boolean result = false;
synchronized (this.entryMap) {
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
if (plugin.onRenderProcessGone(view, detail)) {
result = true;
}
}
}
}
return result;
}
}

View File

@@ -118,8 +118,7 @@ public class PluginResult {
}
/**
* If messageType == MESSAGE_TYPE_STRING, then returns the message string.
* Otherwise, returns null.
* @return message string when messageType is MESSAGE_TYPE_STRING otherwise null.
*/
public String getStrMessage() {
return strMessage;

View File

@@ -0,0 +1,173 @@
/*
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.
*/
package org.apache.cordova;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.SuppressLint;
import android.os.Handler;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import androidx.annotation.NonNull;
import androidx.core.splashscreen.SplashScreen;
import androidx.core.splashscreen.SplashScreenViewProvider;
import org.json.JSONArray;
import org.json.JSONException;
@SuppressLint("LongLogTag")
public class SplashScreenPlugin extends CordovaPlugin {
static final String PLUGIN_NAME = "CordovaSplashScreenPlugin";
// Default config preference values
private static final boolean DEFAULT_AUTO_HIDE = true;
private static final int DEFAULT_DELAY_TIME = -1;
private static final boolean DEFAULT_FADE = true;
private static final int DEFAULT_FADE_TIME = 500;
// Config preference values
/**
* Boolean flag to auto hide splash screen (default=true)
*/
private boolean autoHide;
/**
* Integer value of how long to delay in milliseconds (default=-1)
*/
private int delayTime;
/**
* Boolean flag if to fade to fade out splash screen (default=true)
*/
private boolean isFadeEnabled;
/**
* Integer value of the fade duration in milliseconds (default=500)
*/
private int fadeDuration;
// Internal variables
/**
* Boolean flag to determine if the splash screen remains visible.
*/
private boolean keepOnScreen = true;
@Override
protected void pluginInitialize() {
// Auto Hide & Delay Settings
autoHide = preferences.getBoolean("AutoHideSplashScreen", DEFAULT_AUTO_HIDE);
delayTime = preferences.getInteger("SplashScreenDelay", DEFAULT_DELAY_TIME);
LOG.d(PLUGIN_NAME, "Auto Hide: " + autoHide);
if (delayTime != DEFAULT_DELAY_TIME) {
LOG.d(PLUGIN_NAME, "Delay: " + delayTime + "ms");
}
// Fade & Fade Duration
isFadeEnabled = preferences.getBoolean("FadeSplashScreen", DEFAULT_FADE);
fadeDuration = preferences.getInteger("FadeSplashScreenDuration", DEFAULT_FADE_TIME);
LOG.d(PLUGIN_NAME, "Fade: " + isFadeEnabled);
if (isFadeEnabled) {
LOG.d(PLUGIN_NAME, "Fade Duration: " + fadeDuration + "ms");
}
}
@Override
public boolean execute(
String action,
JSONArray args,
CallbackContext callbackContext
) throws JSONException {
if (action.equals("hide") && autoHide == false) {
/*
* The `.hide()` method can only be triggered if the `splashScreenAutoHide`
* is set to `false`.
*/
keepOnScreen = false;
} else {
return false;
}
callbackContext.success();
return true;
}
@Override
public Object onMessage(String id, Object data) {
switch (id) {
case "setupSplashScreen":
setupSplashScreen((SplashScreen) data);
break;
case "onPageFinished":
attemptCloseOnPageFinished();
break;
}
return null;
}
private void setupSplashScreen(SplashScreen splashScreen) {
// Setup Splash Screen Delay
splashScreen.setKeepOnScreenCondition(() -> keepOnScreen);
// auto hide splash screen when custom delay is defined.
if (autoHide && delayTime != DEFAULT_DELAY_TIME) {
Handler splashScreenDelayHandler = new Handler(cordova.getContext().getMainLooper());
splashScreenDelayHandler.postDelayed(() -> keepOnScreen = false, delayTime);
}
// auto hide splash screen with default delay (-1) delay is controlled by the
// `onPageFinished` message.
// If auto hide is disabled (false), the hiding of the splash screen must be determined &
// triggered by the front-end code with the `navigator.splashscreen.hide()` method.
if (isFadeEnabled) {
// Setup the fade
splashScreen.setOnExitAnimationListener(new SplashScreen.OnExitAnimationListener() {
@Override
public void onSplashScreenExit(@NonNull SplashScreenViewProvider splashScreenViewProvider) {
View splashScreenView = splashScreenViewProvider.getView();
splashScreenView
.animate()
.alpha(0.0f)
.setDuration(fadeDuration)
.setStartDelay(0)
.setInterpolator(new AccelerateInterpolator())
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
splashScreenViewProvider.remove();
webView.getPluginManager().postMessage("updateSystemBars", null);
}
}).start();
}
});
} else {
webView.getPluginManager().postMessage("updateSystemBars", null);
}
}
private void attemptCloseOnPageFinished() {
if (autoHide && delayTime == DEFAULT_DELAY_TIME) {
keepOnScreen = false;
}
}
}

View File

@@ -0,0 +1,374 @@
/*
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.
*/
package org.apache.cordova;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.os.Build;
import android.view.View;
import android.view.ViewParent;
import android.view.Window;
import android.view.WindowInsetsController;
import android.widget.FrameLayout;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import org.json.JSONArray;
import org.json.JSONException;
public class SystemBarPlugin extends CordovaPlugin {
static final String PLUGIN_NAME = "SystemBarPlugin";
static final int INVALID_COLOR = -1;
// Internal variables
private Context context;
private Resources resources;
private int overrideStatusBarBackgroundColor = INVALID_COLOR;
private boolean canEdgeToEdge = false;
@Override
protected void pluginInitialize() {
context = cordova.getContext();
resources = context.getResources();
canEdgeToEdge = preferences.getBoolean("AndroidEdgeToEdge", false)
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM;
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
cordova.getActivity().runOnUiThread(this::updateSystemBars);
}
@Override
public void onResume(boolean multitasking) {
super.onResume(multitasking);
cordova.getActivity().runOnUiThread(this::updateSystemBars);
}
@Override
public Object onMessage(String id, Object data) {
if (id.equals("updateSystemBars")) {
cordova.getActivity().runOnUiThread(this::updateSystemBars);
}
return null;
}
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
if(canEdgeToEdge) {
return false;
}
if ("setStatusBarVisible".equals(action)) {
boolean visible = args.getBoolean(0);
cordova.getActivity().runOnUiThread(() -> setStatusBarVisible(visible));
} else if ("setStatusBarBackgroundColor".equals(action)) {
cordova.getActivity().runOnUiThread(() -> setStatusBarBackgroundColor(args));
} else {
return false;
}
callbackContext.success();
return true;
}
/**
* Allow the app to override the status bar visibility from JS API.
* If for some reason the statusBarView could not be discovered, it will silently ignore
* the change request
*
* @param visible should the status bar be visible?
*/
private void setStatusBarVisible(final boolean visible) {
View statusBar = getStatusBarView(webView);
if (statusBar != null) {
statusBar.setVisibility(visible ? View.VISIBLE : View.GONE);
FrameLayout rootLayout = getRootLayout(webView);
if (rootLayout != null) {
ViewCompat.requestApplyInsets(rootLayout);
}
}
}
/**
* Allow the app to override the status bar background color from JS API.
* If the supplied ARGB is invalid or fails to parse, it will silently ignore
* the change request.
*
* @param argbVals {A, R, G, B}
*/
private void setStatusBarBackgroundColor(JSONArray argbVals) {
try {
int a = argbVals.getInt(0);
int r = argbVals.getInt(1);
int g = argbVals.getInt(2);
int b = argbVals.getInt(3);
String hexColor = String.format("#%02X%02X%02X%02X", a, r, g, b);
int parsedColor = parseColorFromString(hexColor);
if (parsedColor == INVALID_COLOR) return;
overrideStatusBarBackgroundColor = parsedColor;
updateStatusBar(overrideStatusBarBackgroundColor);
} catch (JSONException e) {
// Silently skip
}
}
/**
* Attempt to update all system bars (status, navigation and gesture bars) in various points
* of the apps life cycle.
* For example:
* 1. Device configurations between (E.g. between dark and light mode)
* 2. User resumes the app
* 3. App transitions from SplashScreen Theme to App's Theme
*/
private void updateSystemBars() {
// Update Root View Background Color
int rootViewBackgroundColor = getPreferenceBackgroundColor();
if (rootViewBackgroundColor == INVALID_COLOR) {
rootViewBackgroundColor = canEdgeToEdge ? Color.TRANSPARENT : getUiModeColor();
}
updateRootView(rootViewBackgroundColor);
// Update StatusBar Background Color
int statusBarBackgroundColor;
if (overrideStatusBarBackgroundColor != INVALID_COLOR) {
statusBarBackgroundColor = overrideStatusBarBackgroundColor;
} else if (preferences.contains("StatusBarBackgroundColor")) {
statusBarBackgroundColor = getPreferenceStatusBarBackgroundColor();
} else if(preferences.contains("BackgroundColor")){
statusBarBackgroundColor = rootViewBackgroundColor;
} else {
statusBarBackgroundColor = canEdgeToEdge ? Color.TRANSPARENT : getUiModeColor();
}
updateStatusBar(statusBarBackgroundColor);
}
/**
* Updates the root layout's background color with the supplied color int.
* It will also determine if the background color is light or dark to properly adjust the
* appearance of the navigation/gesture bar's icons so it will not clash with the background.
* <p>
* System bars (navigation & gesture) on SDK 25 or lower is forced to black as the appearance
* of the fonts can not be updated.
* System bars (navigation & gesture) on SDK 26 or greater allows custom background color.
* <p/>
*
* @param bgColor Background color
*/
@SuppressWarnings("deprecation")
private void updateRootView(int bgColor) {
Window window = cordova.getActivity().getWindow();
// Set the root view's background color. Works on SDK 36+
View root = cordova.getActivity().findViewById(android.R.id.content);
if (root != null) root.setBackgroundColor(bgColor);
// Automatically set the font and icon color of the system bars based on background color.
boolean isBackgroundColorLight;
if(bgColor == Color.TRANSPARENT) {
isBackgroundColorLight = isColorLight(getUiModeColor());
} else {
isBackgroundColorLight = isColorLight(bgColor);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
WindowInsetsController controller = window.getInsetsController();
if (controller != null) {
int appearance = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
if (isBackgroundColorLight) {
controller.setSystemBarsAppearance(0, appearance);
} else {
controller.setSystemBarsAppearance(appearance, appearance);
}
}
}
WindowInsetsControllerCompat controllerCompat = WindowCompat.getInsetsController(window, window.getDecorView());
controllerCompat.setAppearanceLightNavigationBars(isBackgroundColorLight);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
window.setNavigationBarColor(bgColor);
} else {
window.setNavigationBarColor(Color.BLACK);
}
}
/**
* Updates the statusBarView background color with the supplied color int.
* It will also determine if the background color is light or dark to properly adjust the
* appearance of the status bar so the font will not clash with the background.
*
* @param bgColor Background color
*/
private void updateStatusBar(int bgColor) {
Window window = cordova.getActivity().getWindow();
View statusBar = getStatusBarView(webView);
if (statusBar != null) {
statusBar.setBackgroundColor(bgColor);
}
// Automatically set the font and icon color of the system bars based on background color.
boolean isStatusBarBackgroundColorLight;
if(bgColor == Color.TRANSPARENT) {
isStatusBarBackgroundColorLight = isColorLight(getUiModeColor());
} else {
isStatusBarBackgroundColorLight = isColorLight(bgColor);
}
WindowInsetsControllerCompat controllerCompat = WindowCompat.getInsetsController(window, window.getDecorView());
controllerCompat.setAppearanceLightStatusBars(isStatusBarBackgroundColorLight);
}
/**
* Determines if the supplied color's appearance is light.
*
* @param color color
* @return boolean value true is returned when the color is light.
*/
private static boolean isColorLight(int color) {
double r = Color.red(color) / 255.0;
double g = Color.green(color) / 255.0;
double b = Color.blue(color) / 255.0;
double luminance = 0.299 * r + 0.587 * g + 0.114 * b;
return luminance > 0.5;
}
/**
* Returns the StatusBarBackgroundColor preference value.
* If the value is missing or fails to parse, it will attempt to try to guess the background
* color by extracting from the apps R.color.cdv_background_color or determine from the uiModes.
* If all fails, the color normally used in light mode is returned.
*
* @return int
*/
private int getPreferenceStatusBarBackgroundColor() {
String colorString = preferences.getString("StatusBarBackgroundColor", null);
int parsedColor = parseColorFromString(colorString);
if (parsedColor != INVALID_COLOR) return parsedColor;
return getUiModeColor(); // fallback
}
/**
* Returns the BackgroundColor preference value.
* If missing or fails to decode, it will return INVALID_COLOR (-1).
*
* @return int
*/
private int getPreferenceBackgroundColor() {
try {
return preferences.getInteger("BackgroundColor", INVALID_COLOR);
} catch (NumberFormatException e) {
LOG.e(PLUGIN_NAME, "Invalid background color argument. Example valid string: '0x00000000'");
return INVALID_COLOR;
}
}
/**
* Tries to find and return the rootLayout.
*
* @param webView CordovaWebView
* @return FrameLayout|null
*/
private FrameLayout getRootLayout(CordovaWebView webView) {
ViewParent parent = webView.getView().getParent();
if (parent instanceof FrameLayout) {
return (FrameLayout) parent;
}
return null;
}
/**
* Tries to find and return the statusBarView.
*
* @param webView CordovaWebView
* @return View|null
*/
private View getStatusBarView(CordovaWebView webView) {
FrameLayout rootView = getRootLayout(webView);
if (rootView == null) {
return null;
}
for (int i = 0; i < rootView.getChildCount(); i++) {
View child = rootView.getChildAt(i);
Object tag = child.getTag();
if ("statusBarView".equals(tag)) {
return child;
}
}
return null;
}
/**
* Determines the background color for status bar & root layer.
* The color will come from the app's R.color.cdv_background_color.
* If for some reason the resource is missing, it will try to fallback on the uiMode.
* <p>
* The uiMode as follows.
* If night mode: "#121318" (android.R.color.system_background_dark)
* If day mode: "#FAF8FF" (android.R.color.system_background_light)
* If all fails, light mode will be returned.
* </p>
* The hex values are supplied instead of "android.R.color" for backwards compatibility.
*
* @return int color
*/
@SuppressLint("DiscouragedApi")
private int getUiModeColor() {
boolean isNightMode = (resources.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
String fallbackColor = isNightMode ? "#121318" : "#FAF8FF";
int colorResId = resources.getIdentifier("cdv_background_color", "color", context.getPackageName());
return colorResId != 0
? ContextCompat.getColor(context, colorResId)
: Color.parseColor(fallbackColor);
}
/**
* Parse color string that would be provided by app developers.
* If the color string is empty or unable to parse, it will return INVALID_COLOR (-1).
*
* @param colorPref hex string value, #AARRGGBB or #RRGGBB
* @return int
*/
private int parseColorFromString(final String colorPref) {
if (colorPref.isEmpty()) return INVALID_COLOR;
try {
return Color.parseColor(colorPref);
} catch (IllegalArgumentException ignore) {
LOG.e(PLUGIN_NAME, "Invalid color hex code. Valid format: #RRGGBB or #AARRGGBB");
return INVALID_COLOR;
}
}
}

View File

@@ -19,8 +19,6 @@
package org.apache.cordova.engine;
import android.annotation.TargetApi;
import android.os.Build;
import android.webkit.CookieManager;
import android.webkit.WebView;
@@ -35,27 +33,35 @@ class SystemCookieManager implements ICordovaCookieManager {
webView = webview;
cookieManager = CookieManager.getInstance();
cookieManager.setAcceptFileSchemeCookies(true);
cookieManager.setAcceptThirdPartyCookies(webView, true);
}
@SuppressWarnings("deprecation")
public void setAcceptFileSchemeCookies() {
cookieManager.setAcceptFileSchemeCookies(true);
}
@Override
public void setCookiesEnabled(boolean accept) {
cookieManager.setAcceptCookie(accept);
}
@Override
public void setCookie(final String url, final String value) {
cookieManager.setCookie(url, value);
}
@Override
public String getCookie(final String url) {
return cookieManager.getCookie(url);
}
@SuppressWarnings("deprecation")
@Override
public void clearCookies() {
cookieManager.removeAllCookies(null);
}
@Override
public void flush() {
cookieManager.flush();
}

View File

@@ -37,16 +37,19 @@ class SystemExposedJsApi implements ExposedJsApi {
}
@JavascriptInterface
@Override
public String exec(int bridgeSecret, String service, String action, String callbackId, String arguments) throws JSONException, IllegalAccessException {
return bridge.jsExec(bridgeSecret, service, action, callbackId, arguments);
}
@JavascriptInterface
@Override
public void setNativeToJsBridgeMode(int bridgeSecret, int value) throws IllegalAccessException {
bridge.jsSetNativeToJsBridgeMode(bridgeSecret, value);
}
@JavascriptInterface
@Override
public String retrieveJsMessages(int bridgeSecret, boolean fromOnlineEvent) throws IllegalAccessException {
return bridge.jsRetrieveJsMessages(bridgeSecret, fromOnlineEvent);
}

View File

@@ -18,18 +18,22 @@
*/
package org.apache.cordova.engine;
import java.io.IOException;
import java.io.File;
import java.util.Arrays;
import android.annotation.TargetApi;
import java.util.ArrayList;
import java.util.List;
import android.app.Activity;
import android.content.ClipData;
import android.content.Context;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.webkit.ConsoleMessage;
import android.webkit.GeolocationPermissions.Callback;
import android.webkit.JsPromptResult;
import android.webkit.JsResult;
@@ -41,6 +45,7 @@ import android.webkit.PermissionRequest;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import androidx.core.content.FileProvider;
import org.apache.cordova.CordovaDialogsHelper;
import org.apache.cordova.CordovaPlugin;
@@ -114,8 +119,8 @@ public class SystemWebChromeClient extends WebChromeClient {
* If the client returns true, WebView will assume that the client will
* handle the prompt dialog and call the appropriate JsPromptResult method.
*
* Since we are hacking prompts for our own purposes, we should not be using them for
* this purpose, perhaps we should hack console.log to do this instead!
* <p>Since we are hacking prompts for our own purposes, we should not be using them for
* this purpose, perhaps we should hack console.log to do this instead!</p>
*/
@Override
public boolean onJsPrompt(WebView view, String origin, String message, String defaultValue, final JsPromptResult result) {
@@ -150,15 +155,15 @@ public class SystemWebChromeClient extends WebChromeClient {
quotaUpdater.updateQuota(MAX_QUOTA);
}
@Override
/**
* Instructs the client to show a prompt to ask the user to set the Geolocation permission state for the specified origin.
*
* This also checks for the Geolocation Plugin and requests permission from the application to use Geolocation.
* <p>This also checks for the Geolocation Plugin and requests permission from the application to use Geolocation.</p>
*
* @param origin
* @param callback
*/
@Override
public void onGeolocationPermissionsShowPrompt(String origin, Callback callback) {
super.onGeolocationPermissionsShowPrompt(origin, callback);
callback.invoke(origin, true, false);
@@ -183,12 +188,13 @@ public class SystemWebChromeClient extends WebChromeClient {
parentEngine.getCordovaWebView().hideCustomView();
}
@Override
/**
* Ask the host application for a custom progress view to show while
* a <video> is loading.
*
* @return View The progress view.
*/
@Override
public View getVideoLoadingProgressView() {
if (mVideoProgressView == null) {
// Create a new Loading view programmatically.
@@ -199,7 +205,7 @@ public class SystemWebChromeClient extends WebChromeClient {
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
layout.setLayoutParams(layoutParams);
// the proress bar
// the progress bar
ProgressBar bar = new ProgressBar(parentEngine.getView().getContext());
LinearLayout.LayoutParams barLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
barLayoutParams.gravity = Gravity.CENTER;
@@ -212,52 +218,110 @@ public class SystemWebChromeClient extends WebChromeClient {
}
@Override
public boolean onShowFileChooser(WebView webView, final ValueCallback<Uri[]> filePathsCallback, final WebChromeClient.FileChooserParams fileChooserParams) {
public boolean onShowFileChooser(WebView webView, final ValueCallback<Uri[]> filePathsCallback,
final WebChromeClient.FileChooserParams fileChooserParams) {
Intent fileIntent = fileChooserParams.createIntent();
// Check if multiple-select is specified
Boolean selectMultiple = false;
if (fileChooserParams.getMode() == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE) {
selectMultiple = true;
}
Intent intent = fileChooserParams.createIntent();
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, selectMultiple);
fileIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, selectMultiple);
// Uses Intent.EXTRA_MIME_TYPES to pass multiple mime types.
String[] acceptTypes = fileChooserParams.getAcceptTypes();
if (acceptTypes.length > 1) {
intent.setType("*/*"); // Accept all, filter mime types by Intent.EXTRA_MIME_TYPES.
intent.putExtra(Intent.EXTRA_MIME_TYPES, acceptTypes);
fileIntent.setType("*/*"); // Accept all, filter mime types by Intent.EXTRA_MIME_TYPES.
fileIntent.putExtra(Intent.EXTRA_MIME_TYPES, acceptTypes);
}
// Image from camera intent
Uri tempUri = null;
Intent captureIntent = null;
if (fileChooserParams.isCaptureEnabled()) {
captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
Context context = parentEngine.getView().getContext();
if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
&& captureIntent.resolveActivity(context.getPackageManager()) != null) {
try {
File tempFile = createTempFile(context);
LOG.d(LOG_TAG, "Temporary photo capture file: " + tempFile);
tempUri = createUriForFile(context, tempFile);
LOG.d(LOG_TAG, "Temporary photo capture URI: " + tempUri);
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, tempUri);
} catch (IOException e) {
LOG.e(LOG_TAG, "Unable to create temporary file for photo capture", e);
captureIntent = null;
}
} else {
LOG.w(LOG_TAG, "Device does not support photo capture");
captureIntent = null;
}
}
final Uri captureUri = tempUri;
// Chooser intent
Intent chooserIntent = Intent.createChooser(fileIntent, null);
if (captureIntent != null) {
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { captureIntent });
}
try {
LOG.i(LOG_TAG, "Starting intent for file chooser");
parentEngine.cordova.startActivityForResult(new CordovaPlugin() {
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
// Handle result
Uri[] result = null;
if (resultCode == Activity.RESULT_OK && intent != null) {
if (intent.getClipData() != null) {
// handle multiple-selected files
final int numSelectedFiles = intent.getClipData().getItemCount();
result = new Uri[numSelectedFiles];
for (int i = 0; i < numSelectedFiles; i++) {
result[i] = intent.getClipData().getItemAt(i).getUri();
LOG.d(LOG_TAG, "Receive file chooser URL: " + result[i]);
if (resultCode == Activity.RESULT_OK) {
List<Uri> uris = new ArrayList<Uri>();
if (intent != null && intent.getData() != null) { // single file
LOG.v(LOG_TAG, "Adding file (single): " + intent.getData());
uris.add(intent.getData());
} else if (captureUri != null) { // camera
LOG.v(LOG_TAG, "Adding camera capture: " + captureUri);
uris.add(captureUri);
} else if (intent != null && intent.getClipData() != null) { // multiple files
ClipData clipData = intent.getClipData();
int count = clipData.getItemCount();
for (int i = 0; i < count; i++) {
Uri uri = clipData.getItemAt(i).getUri();
LOG.v(LOG_TAG, "Adding file (multiple): " + uri);
if (uri != null) {
uris.add(uri);
}
}
}
else if (intent.getData() != null) {
// handle single-selected file
result = WebChromeClient.FileChooserParams.parseResult(resultCode, intent);
LOG.d(LOG_TAG, "Receive file chooser URL: " + result);
if (!uris.isEmpty()) {
LOG.d(LOG_TAG, "Receive file chooser URL: " + uris.toString());
result = uris.toArray(new Uri[uris.size()]);
}
}
filePathsCallback.onReceiveValue(result);
}
}, intent, FILECHOOSER_RESULTCODE);
}, chooserIntent, FILECHOOSER_RESULTCODE);
} catch (ActivityNotFoundException e) {
LOG.w("No activity found to handle file chooser intent.", e);
LOG.w(LOG_TAG, "No activity found to handle file chooser intent.", e);
filePathsCallback.onReceiveValue(null);
}
return true;
}
private File createTempFile(Context context) throws IOException {
// Create an image file name
File tempFile = File.createTempFile("temp", ".jpg", context.getCacheDir());
return tempFile;
}
private Uri createUriForFile(Context context, File tempFile) throws IOException {
String appId = context.getPackageName();
Uri uri = FileProvider.getUriForFile(context, appId + ".cdv.core.file.provider", tempFile);
return uri;
}
@Override
public void onPermissionRequest(final PermissionRequest request) {
LOG.d(LOG_TAG, "onPermissionRequest: " + Arrays.toString(request.getResources()));

View File

@@ -28,6 +28,9 @@ import android.net.http.SslError;
import android.webkit.ClientCertRequest;
import android.webkit.HttpAuthHandler;
import android.webkit.MimeTypeMap;
import android.webkit.RenderProcessGoneDetail;
import android.webkit.ServiceWorkerClient;
import android.webkit.ServiceWorkerController;
import android.webkit.SslErrorHandler;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
@@ -71,7 +74,7 @@ public class SystemWebViewClient extends WebViewClient {
this.parentEngine = parentEngine;
WebViewAssetLoader.Builder assetLoaderBuilder = new WebViewAssetLoader.Builder()
.setDomain(parentEngine.preferences.getString("hostname", "localhost"))
.setDomain(parentEngine.preferences.getString("hostname", "localhost").toLowerCase())
.setHttpAllowed(true);
assetLoaderBuilder.addPathHandler("/", path -> {
@@ -115,6 +118,18 @@ public class SystemWebViewClient extends WebViewClient {
});
this.assetLoader = assetLoaderBuilder.build();
boolean setAsServiceWorkerClient = parentEngine.preferences.getBoolean("ResolveServiceWorkerRequests", true);
ServiceWorkerController controller = null;
if (setAsServiceWorkerClient) {
controller = ServiceWorkerController.getInstance();
controller.setServiceWorkerClient(new ServiceWorkerClient(){
@Override
public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) {
return assetLoader.shouldInterceptRequest(request.getUrl());
}
});
}
}
/**
@@ -184,7 +199,7 @@ public class SystemWebViewClient extends WebViewClient {
* one time for the main frame. This also means that onPageStarted will not be called when the contents of an
* embedded frame changes, i.e. clicking a link whose target is an iframe.
*
* @param view The webview initiating the callback.
* @param view The WebView initiating the callback.
* @param url The url of the page.
*/
@Override
@@ -201,7 +216,7 @@ public class SystemWebViewClient extends WebViewClient {
* This method is called only for main frame. When onPageFinished() is called, the rendering picture may not be updated yet.
*
*
* @param view The webview initiating the callback.
* @param view The WebView initiating the callback.
* @param url The url of the page.
*/
@Override
@@ -213,7 +228,7 @@ public class SystemWebViewClient extends WebViewClient {
}
isCurrentlyLoading = false;
/**
/*
* Because of a timing issue we need to clear this history in onPageFinished as well as
* onPageStarted. However we only want to do this if the doClearHistory boolean is set to
* true. You see when you load a url with a # in it which is common in jQuery applications
@@ -317,7 +332,6 @@ public class SystemWebViewClient extends WebViewClient {
*
* @param host
* @param realm
*
* @return the authentication token or null if did not exist
*/
public AuthenticationToken removeAuthenticationToken(String host, String realm) {
@@ -327,15 +341,16 @@ public class SystemWebViewClient extends WebViewClient {
/**
* Gets the authentication token.
*
* In order it tries:
* 1- host + realm
* 2- host
* 3- realm
* 4- no host, no realm
* <p>In order it tries:</p>
* <ol>
* <li>host + realm</li>
* <li>host</li>
* <li>realm</li>
* <li>no host, no realm</li>
* </ol>
*
* @param host
* @param realm
*
* @return the authentication token
*/
public AuthenticationToken getAuthenticationToken(String host, String realm) {
@@ -422,4 +437,15 @@ public class SystemWebViewClient extends WebViewClient {
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
return this.assetLoader.shouldInterceptRequest(request.getUrl());
}
@Override
public boolean onRenderProcessGone(final WebView view, RenderProcessGoneDetail detail) {
// Check if there is some plugin which can handle this event
PluginManager pluginManager = this.parentEngine.pluginManager;
if (pluginManager != null && pluginManager.onRenderProcessGone(view, detail)) {
return true;
}
return super.onRenderProcessGone(view, detail);
}
}

View File

@@ -110,7 +110,7 @@ public class SystemWebViewEngine implements CordovaWebViewEngine {
nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.OnlineEventsBridgeMode(new NativeToJsMessageQueue.OnlineEventsBridgeMode.OnlineEventsBridgeModeDelegate() {
@Override
public void setNetworkAvailable(boolean value) {
//sometimes this can be called after calling webview.destroy() on destroy()
//sometimes this can be called after calling webView.destroy() on destroy()
//thus resulting in a NullPointerException
if(webView!=null) {
webView.setNetworkAvailable(value);
@@ -165,6 +165,7 @@ public class SystemWebViewEngine implements CordovaWebViewEngine {
LOG.d(TAG, "Enabled insecure file access");
settings.setAllowFileAccess(true);
settings.setAllowUniversalAccessFromFileURLs(true);
cookieManager.setAcceptFileSchemeCookies();
}
settings.setMediaPlaybackRequiresUserGesture(false);
@@ -174,9 +175,20 @@ public class SystemWebViewEngine implements CordovaWebViewEngine {
String databasePath = webView.getContext().getApplicationContext().getDir("database", Context.MODE_PRIVATE).getPath();
settings.setDatabaseEnabled(true);
//Determine whether we're in debug or release mode, and turn on Debugging!
ApplicationInfo appInfo = webView.getContext().getApplicationContext().getApplicationInfo();
if ((appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
// The default is to use the module's debuggable state to decide if the WebView inspector
// should be enabled. However, users can configure InspectableWebView preference to forcefully enable
// or disable the WebView inspector.
String inspectableWebview = preferences.getString("InspectableWebview", null);
boolean shouldEnableInspector = false;
if (inspectableWebview == null) {
ApplicationInfo appInfo = webView.getContext().getApplicationContext().getApplicationInfo();
shouldEnableInspector = (appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
}
else if ("true".equals(inspectableWebview)) {
shouldEnableInspector = true;
}
if (shouldEnableInspector) {
enableRemoteDebugging();
}
@@ -237,7 +249,7 @@ public class SystemWebViewEngine implements CordovaWebViewEngine {
/**
* Load the url into the webview.
* Load the url into the WebView.
*/
@Override
public void loadUrl(final String url, boolean clearNavigationStack) {
@@ -276,7 +288,7 @@ public class SystemWebViewEngine implements CordovaWebViewEngine {
*/
@Override
public boolean goBack() {
// Check webview first to see if there is a history
// Check WebView first to see if there is a history
// This is needed to support curPage#diffLink, since they are added to parentEngine's history, but not our history url array (JQMobile behavior)
if (webView.canGoBack()) {
webView.goBack();

View File

@@ -17,12 +17,12 @@
under the License.
*/
var os = require('os');
var execa = require('execa');
var events = require('cordova-common').events;
var CordovaError = require('cordova-common').CordovaError;
const os = require('node:os');
const execa = require('execa');
const events = require('cordova-common').events;
const CordovaError = require('cordova-common').CordovaError;
var Adb = {};
const Adb = {};
/**
* Lists available/connected devices and emulators
@@ -50,7 +50,7 @@ Adb.devices = async function () {
Adb.install = function (target, packagePath, { replace = false, execOptions = {} } = {}) {
events.emit('verbose', 'Installing apk ' + packagePath + ' on target ' + target + '...');
var args = ['-s', target, 'install'];
const args = ['-s', target, 'install'];
if (replace) args.push('-r');
const opts = { cwd: os.tmpdir(), ...execOptions };
@@ -79,7 +79,7 @@ Adb.uninstall = function (target, packageId) {
Adb.shell = function (target, shellCommand) {
events.emit('verbose', 'Running adb shell command "' + shellCommand + '" on target ' + target + '...');
var args = ['-s', target, 'shell'];
const args = ['-s', target, 'shell'];
shellCommand = shellCommand.split(/\s+/);
return execa('adb', args.concat(shellCommand), { cwd: os.tmpdir() })
.then(({ stdout }) => stdout)

View File

@@ -17,10 +17,10 @@
under the License.
*/
var fs = require('fs');
var xml = require('cordova-common').xmlHelpers;
const fs = require('node:fs');
const xml = require('cordova-common').xmlHelpers;
var DEFAULT_ORIENTATION = 'default';
const DEFAULT_ORIENTATION = 'default';
/** Wraps an AndroidManifest file */
class AndroidManifest {
@@ -50,17 +50,8 @@ class AndroidManifest {
return this;
}
getPackageId () {
return this.doc.getroot().attrib.package;
}
setPackageId (pkgId) {
this.doc.getroot().attrib.package = pkgId;
return this;
}
getActivity () {
var activity = this.doc.getroot().find('./application/activity');
const activity = this.doc.getroot().find('./application/activity');
return {
getName: function () {
return activity.attrib['android:name'];
@@ -103,7 +94,7 @@ class AndroidManifest {
}
setDebuggable (value) {
var application = this.doc.getroot().find('./application');
const application = this.doc.getroot().find('./application');
if (value) {
application.attrib['android:debuggable'] = 'true';
} else {

View File

@@ -17,16 +17,16 @@
under the License.
*/
var fs = require('fs');
var path = require('path');
var properties_parser = require('properties-parser');
var AndroidManifest = require('./AndroidManifest');
var pluginHandlers = require('./pluginHandlers');
const fs = require('node:fs');
const path = require('node:path');
const properties_parser = require('properties-parser');
const pluginHandlers = require('./pluginHandlers');
const CordovaGradleConfigParserFactory = require('./config/CordovaGradleConfigParserFactory');
var projectFileCache = {};
let projectFileCache = {};
function addToPropertyList (projectProperties, key, value) {
var i = 1;
let i = 1;
while (projectProperties.get(key + '.' + i)) { i++; }
projectProperties.set(key + '.' + i, value);
@@ -34,8 +34,8 @@ function addToPropertyList (projectProperties, key, value) {
}
function removeFromPropertyList (projectProperties, key, value) {
var i = 1;
var currentValue;
let i = 1;
let currentValue;
while ((currentValue = projectProperties.get(key + '.' + i))) {
if (currentValue === value) {
while ((currentValue = projectProperties.get(key + '.' + (i + 1)))) {
@@ -51,7 +51,7 @@ function removeFromPropertyList (projectProperties, key, value) {
}
function getRelativeLibraryPath (parentDir, subDir) {
var libraryPath = path.relative(parentDir, subDir);
const libraryPath = path.relative(parentDir, subDir);
return (path.sep === '\\') ? libraryPath.replace(/\\/g, '/') : libraryPath;
}
@@ -63,36 +63,36 @@ class AndroidProject {
this.projectDir = projectDir;
this.platformWww = path.join(this.projectDir, 'platform_www');
this.www = path.join(this.projectDir, 'app/src/main/assets/www');
this.cordovaGradleConfigParser = CordovaGradleConfigParserFactory.create(this.projectDir);
}
/**
* Reads the package name out of the Android Manifest file
* Reads the package name out of the Cordova's Gradle Config file
*
* @param {String} projectDir The absolute path to the directory containing the project
* @return {String} The name of the package
*/
getPackageName () {
var manifestPath = path.join(this.projectDir, 'app/src/main/AndroidManifest.xml');
return new AndroidManifest(manifestPath).getPackageId();
return this.cordovaGradleConfigParser.getPackageName();
}
getCustomSubprojectRelativeDir (plugin_id, src) {
// All custom subprojects are prefixed with the last portion of the package id.
// This is to avoid collisions when opening multiple projects in Eclipse that have subprojects with the same name.
var packageName = this.getPackageName();
var lastDotIndex = packageName.lastIndexOf('.');
var prefix = packageName.substring(lastDotIndex + 1);
var subRelativeDir = path.join(plugin_id, prefix + '-' + path.basename(src));
const packageName = this.getPackageName();
const lastDotIndex = packageName.lastIndexOf('.');
const prefix = packageName.substring(lastDotIndex + 1);
const subRelativeDir = path.join(plugin_id, prefix + '-' + path.basename(src));
return subRelativeDir;
}
addSubProject (parentDir, subDir) {
var parentProjectFile = path.resolve(parentDir, 'project.properties');
var subProjectFile = path.resolve(subDir, 'project.properties');
var parentProperties = this._getPropertiesFile(parentProjectFile);
const parentProjectFile = path.resolve(parentDir, 'project.properties');
const subProjectFile = path.resolve(subDir, 'project.properties');
const parentProperties = this._getPropertiesFile(parentProjectFile);
// TODO: Setting the target needs to happen only for pre-3.7.0 projects
if (fs.existsSync(subProjectFile)) {
var subProperties = this._getPropertiesFile(subProjectFile);
const subProperties = this._getPropertiesFile(subProjectFile);
subProperties.set('target', parentProperties.get('target'));
subProperties.dirty = true;
this._subProjectDirs[subDir] = true;
@@ -103,37 +103,37 @@ class AndroidProject {
}
removeSubProject (parentDir, subDir) {
var parentProjectFile = path.resolve(parentDir, 'project.properties');
var parentProperties = this._getPropertiesFile(parentProjectFile);
const parentProjectFile = path.resolve(parentDir, 'project.properties');
const parentProperties = this._getPropertiesFile(parentProjectFile);
removeFromPropertyList(parentProperties, 'android.library.reference', getRelativeLibraryPath(parentDir, subDir));
delete this._subProjectDirs[subDir];
this._dirty = true;
}
addGradleReference (parentDir, subDir) {
var parentProjectFile = path.resolve(parentDir, 'project.properties');
var parentProperties = this._getPropertiesFile(parentProjectFile);
const parentProjectFile = path.resolve(parentDir, 'project.properties');
const parentProperties = this._getPropertiesFile(parentProjectFile);
addToPropertyList(parentProperties, 'cordova.gradle.include', getRelativeLibraryPath(parentDir, subDir));
this._dirty = true;
}
removeGradleReference (parentDir, subDir) {
var parentProjectFile = path.resolve(parentDir, 'project.properties');
var parentProperties = this._getPropertiesFile(parentProjectFile);
const parentProjectFile = path.resolve(parentDir, 'project.properties');
const parentProperties = this._getPropertiesFile(parentProjectFile);
removeFromPropertyList(parentProperties, 'cordova.gradle.include', getRelativeLibraryPath(parentDir, subDir));
this._dirty = true;
}
addSystemLibrary (parentDir, value) {
var parentProjectFile = path.resolve(parentDir, 'project.properties');
var parentProperties = this._getPropertiesFile(parentProjectFile);
const parentProjectFile = path.resolve(parentDir, 'project.properties');
const parentProperties = this._getPropertiesFile(parentProjectFile);
addToPropertyList(parentProperties, 'cordova.system.library', value);
this._dirty = true;
}
removeSystemLibrary (parentDir, value) {
var parentProjectFile = path.resolve(parentDir, 'project.properties');
var parentProperties = this._getPropertiesFile(parentProjectFile);
const parentProjectFile = path.resolve(parentDir, 'project.properties');
const parentProperties = this._getPropertiesFile(parentProjectFile);
removeFromPropertyList(parentProperties, 'cordova.system.library', value);
this._dirty = true;
}
@@ -144,8 +144,8 @@ class AndroidProject {
}
this._dirty = false;
for (var filename in this._propertiesEditors) {
var editor = this._propertiesEditors[filename];
for (const filename in this._propertiesEditors) {
const editor = this._propertiesEditors[filename];
if (editor.dirty) {
fs.writeFileSync(filename, editor.toString());
editor.dirty = false;
@@ -165,7 +165,7 @@ class AndroidProject {
* This checks if an Android project is clean or has old build artifacts
*/
isClean () {
var build_path = path.join(this.projectDir, 'build');
const build_path = path.join(this.projectDir, 'build');
// If the build directory doesn't exist, it's clean
return !(fs.existsSync(build_path));
}

View File

@@ -17,17 +17,17 @@
under the License.
*/
var path = require('path');
const path = require('node:path');
var AndroidProject = require('./AndroidProject');
var PluginManager = require('cordova-common').PluginManager;
const AndroidProject = require('./AndroidProject');
const PluginManager = require('cordova-common').PluginManager;
var CordovaLogger = require('cordova-common').CordovaLogger;
var selfEvents = require('cordova-common').events;
var ConfigParser = require('cordova-common').ConfigParser;
const CordovaLogger = require('cordova-common').CordovaLogger;
const selfEvents = require('cordova-common').events;
const ConfigParser = require('cordova-common').ConfigParser;
const prepare = require('./prepare').prepare;
var PLATFORM = 'android';
const PLATFORM = 'android';
const VERSION = require('../package').version;
function setupEvents (externalEventEmitter) {
@@ -72,7 +72,9 @@ class Api {
platformWww: path.join(this.root, 'platform_www'),
configXml: path.join(appRes, 'xml', 'config.xml'),
defaultConfigXml: path.join(this.root, 'cordova', 'defaults.xml'),
strings: path.join(appRes, 'values', 'strings.xml'),
strings: path.join(appRes, 'values', 'cdv_strings.xml'),
themes: path.join(appRes, 'values', 'cdv_themes.xml'),
colors: path.join(appRes, 'values', 'cdv_colors.xml'),
manifest: path.join(appMain, 'AndroidManifest.xml'),
build: path.join(this.root, 'build'),
javaSrc: path.join(appMain, 'java')
@@ -88,7 +90,7 @@ class Api {
* platform's file structure and other properties of platform.
*/
getPlatformInfo () {
var result = {};
const result = {};
result.locations = this.locations;
result.root = this.root;
result.name = this.platform;
@@ -139,8 +141,8 @@ class Api {
* CordovaError instance.
*/
addPlugin (plugin, installOptions) {
var project = AndroidProject.getProjectFile(this.root);
var self = this;
const project = AndroidProject.getProjectFile(this.root);
const self = this;
installOptions = installOptions || {};
installOptions.variables = installOptions.variables || {};
@@ -175,11 +177,7 @@ class Api {
* CordovaError instance.
*/
removePlugin (plugin, uninstallOptions) {
var project = AndroidProject.getProjectFile(this.root);
if (uninstallOptions && uninstallOptions.usePlatformWww === true) {
uninstallOptions.usePlatformWww = false;
}
const project = AndroidProject.getProjectFile(this.root);
return PluginManager.get(this.platform, this.locations, project)
.removePlugin(plugin, uninstallOptions)
@@ -239,7 +237,7 @@ class Api {
* arhcitectures is specified.
*/
build (buildOptions) {
var self = this;
const self = this;
return require('./check_reqs').run().then(function () {
return require('./build').run.call(self, buildOptions);
@@ -269,12 +267,18 @@ class Api {
* successfully, or rejected with CordovaError.
*/
run (runOptions) {
var self = this;
const self = this;
return require('./check_reqs').run().then(function () {
return require('./run').run.call(self, runOptions);
});
}
listTargets (options) {
return require('./check_reqs').check_android().then(() => {
return require('./run').runListDevices.call(this, options);
});
}
/**
* Cleans out the build artifacts from platform's directory, and also
* cleans out the platform www directory if called without options specified.
@@ -283,7 +287,7 @@ class Api {
* CordovaError.
*/
clean (cleanOptions) {
var self = this;
const self = this;
// This will lint, checking for null won't
if (typeof cleanOptions === 'undefined') {
cleanOptions = {};
@@ -328,7 +332,7 @@ class Api {
*/
static createPlatform (destination, config, options, events) {
events = setupEvents(events);
var result;
let result;
try {
result = require('./create').create(destination, config, options, events).then(function (destination) {
return new Api(PLATFORM, destination, events);
@@ -358,7 +362,7 @@ class Api {
*/
static updatePlatform (destination, options, events) {
events = setupEvents(events);
var result;
let result;
try {
result = require('../../lib/create').update(destination, options, events).then(function (destination) {
return new Api(PLATFORM, destination, events);

View File

@@ -19,7 +19,7 @@
const execa = require('execa');
var suffix_number_regex = /(\d+)$/;
const suffix_number_regex = /(\d+)$/;
// Used for sorting Android targets, example strings to sort:
// android-19
// android-L
@@ -66,9 +66,9 @@ module.exports.version_string_to_api_level = {
/* eslint-enable quote-props */
function parse_targets (output) {
var target_out = output.split('\n');
var targets = [];
for (var i = target_out.length - 1; i >= 0; i--) {
const target_out = output.split('\n');
const targets = [];
for (let i = target_out.length - 1; i >= 0; i--) {
if (target_out[i].match(/id:/)) { // if "id:" is in the line...
targets.push(target_out[i].match(/"(.+)"/)[1]); // .. match whatever is in quotes.
}

View File

@@ -17,15 +17,16 @@
under the License.
*/
var path = require('path');
var fs = require('fs');
var nopt = require('nopt');
const path = require('node:path');
const fs = require('node:fs');
const nopt = require('nopt');
const untildify = require('untildify');
const { parseArgsStringToArgv } = require('string-argv');
var Adb = require('./Adb');
const Adb = require('./Adb');
var events = require('cordova-common').events;
var PackageType = require('./PackageType');
const events = require('cordova-common').events;
const PackageType = require('./PackageType');
module.exports.parseBuildOptions = parseOpts;
function parseOpts (options, resolvedTarget, projectRoot) {
@@ -36,7 +37,11 @@ function parseOpts (options, resolvedTarget, projectRoot) {
minSdkVersion: String,
maxSdkVersion: String,
targetSdkVersion: String,
// This needs to be an array since nopts will parse its entries as further options for this process
// It will be an array of 1 string: [ "string args" ]
gradleArg: [String, Array],
keystore: path,
alias: String,
storePassword: String,
@@ -46,7 +51,7 @@ function parseOpts (options, resolvedTarget, projectRoot) {
}, {}, options.argv, 0);
// Android Studio Build method is the default
var ret = {
const ret = {
buildType: options.release ? 'release' : 'debug',
prepEnv: options.argv.prepenv,
arch: resolvedTarget && resolvedTarget.arch,
@@ -58,10 +63,11 @@ function parseOpts (options, resolvedTarget, projectRoot) {
if (options.argv.maxSdkVersion) { ret.extraArgs.push('-PcdvMaxSdkVersion=' + options.argv.maxSdkVersion); }
if (options.argv.targetSdkVersion) { ret.extraArgs.push('-PcdvTargetSdkVersion=' + options.argv.targetSdkVersion); }
if (options.argv.gradleArg) {
ret.extraArgs = ret.extraArgs.concat(options.argv.gradleArg);
const gradleArgs = parseArgsStringToArgv(options.argv.gradleArg[0]);
ret.extraArgs = ret.extraArgs.concat(gradleArgs);
}
var packageArgs = {};
const packageArgs = {};
if (options.argv.keystore) { packageArgs.keystore = path.relative(projectRoot, path.resolve(options.argv.keystore)); }
@@ -69,7 +75,7 @@ function parseOpts (options, resolvedTarget, projectRoot) {
if (options.argv[flagName]) { packageArgs[flagName] = options.argv[flagName]; }
});
var buildConfig = options.buildConfig;
const buildConfig = options.buildConfig;
// If some values are not specified as command line arguments - use build config to supplement them.
// Command line arguments have precedence over build config.
@@ -78,10 +84,10 @@ function parseOpts (options, resolvedTarget, projectRoot) {
throw new Error('Specified build config file does not exist: ' + buildConfig);
}
events.emit('log', 'Reading build config file: ' + path.resolve(buildConfig));
var buildjson = fs.readFileSync(buildConfig, 'utf8');
var config = JSON.parse(buildjson.replace(/^\ufeff/, '')); // Remove BOM
const buildjson = fs.readFileSync(buildConfig, 'utf8');
const config = JSON.parse(buildjson.replace(/^\ufeff/, '')); // Remove BOM
if (config.android && config.android[ret.buildType]) {
var androidInfo = config.android[ret.buildType];
const androidInfo = config.android[ret.buildType];
if (androidInfo.keystore && !packageArgs.keystore) {
androidInfo.keystore = untildify(androidInfo.keystore);
packageArgs.keystore = path.resolve(path.dirname(buildConfig), androidInfo.keystore);
@@ -144,8 +150,8 @@ function parseOpts (options, resolvedTarget, projectRoot) {
* Returns a promise.
*/
module.exports.runClean = function (options) {
var opts = parseOpts(options, null, this.root);
var builder = this._builder;
const opts = parseOpts(options, null, this.root);
const builder = this._builder;
return builder.prepEnv(opts).then(function () {
return builder.clean(opts);
@@ -165,8 +171,8 @@ module.exports.runClean = function (options) {
* information.
*/
module.exports.run = function (options, optResolvedTarget) {
var opts = parseOpts(options, optResolvedTarget, this.root);
var builder = this._builder;
const opts = parseOpts(options, optResolvedTarget, this.root);
const builder = this._builder;
return builder.prepEnv(opts).then(function () {
if (opts.prepEnv) {
@@ -174,7 +180,7 @@ module.exports.run = function (options, optResolvedTarget) {
return;
}
return builder.build(opts).then(function () {
var paths;
let paths;
if (opts.packageType === PackageType.BUNDLE) {
paths = builder.findOutputBundles(opts.buildType);
events.emit('log', 'Built the following bundle(s): \n\t' + paths.join('\n\t'));
@@ -184,7 +190,7 @@ module.exports.run = function (options, optResolvedTarget) {
}
return {
paths: paths,
paths,
buildType: opts.buildType
};
});
@@ -202,17 +208,17 @@ module.exports.detectArchitecture = function (target) {
};
module.exports.findBestApkForArchitecture = function (buildResults, arch) {
var paths = buildResults.apkPaths.filter(function (p) {
var apkName = path.basename(p);
const paths = buildResults.apkPaths.filter(function (p) {
const apkName = path.basename(p);
if (buildResults.buildType === 'debug') {
return /-debug/.exec(apkName);
}
return !/-debug/.exec(apkName);
});
var archPattern = new RegExp('-' + arch);
var hasArchPattern = /-x86|-arm/;
for (var i = 0; i < paths.length; ++i) {
var apkName = path.basename(paths[i]);
const archPattern = new RegExp('-' + arch);
const hasArchPattern = /-x86|-arm/;
for (let i = 0; i < paths.length; ++i) {
const apkName = path.basename(paths[i]);
if (hasArchPattern.exec(apkName)) {
if (archPattern.exec(apkName)) {
return paths[i];

View File

@@ -17,16 +17,18 @@
under the License.
*/
var fs = require('fs-extra');
var path = require('path');
const fs = require('node:fs');
const fsp = require('node:fs/promises');
const path = require('node: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 events = require('cordova-common').events;
const CordovaError = require('cordova-common').CordovaError;
const check_reqs = require('../check_reqs');
const PackageType = require('../PackageType');
const { compareByAll, isWindows } = require('../utils');
const { createEditor } = require('properties-parser');
const CordovaGradleConfigParserFactory = require('../config/CordovaGradleConfigParserFactory');
const MARKER = 'YOUR CHANGES WILL BE ERASED!';
const SIGNING_PROPERTIES = '-signing.properties';
@@ -84,7 +86,11 @@ class ProjectBuilder {
}
getArgs (cmd, opts) {
let args;
let args = [];
if (opts.extraArgs) {
args = args.concat(opts.extraArgs);
}
let buildCmd = cmd;
if (opts.packageType === PackageType.BUNDLE) {
if (cmd === 'release') {
@@ -92,8 +98,6 @@ class ProjectBuilder {
} else if (cmd === 'debug') {
buildCmd = ':app:bundleDebug';
}
args = [buildCmd, '-b', path.join(this.root, 'build.gradle')];
} else {
if (cmd === 'release') {
buildCmd = 'cdvBuildRelease';
@@ -101,111 +105,121 @@ class ProjectBuilder {
buildCmd = 'cdvBuildDebug';
}
args = [buildCmd, '-b', path.join(this.root, 'build.gradle')];
if (opts.arch) {
args.push('-PcdvBuildArch=' + opts.arch);
}
}
args.push.apply(args, opts.extraArgs);
args.push(buildCmd);
return args;
}
/*
* This returns a promise
*/
runGradleWrapper (gradle_cmd) {
var gradlePath = path.join(this.root, 'gradlew');
var wrapperGradle = path.join(this.root, 'wrapper.gradle');
if (fs.existsSync(gradlePath)) {
// Literally do nothing, for some reason this works, while !fs.existsSync didn't on Windows
getGradleWrapperPath () {
let wrapper = path.join(this.root, 'tools', 'gradlew');
if (isWindows()) {
wrapper += '.bat';
}
return wrapper;
}
/**
* Installs/updates the gradle wrapper
* @param {string} gradleVersion The gradle version to install. Ignored if CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL environment variable is defined
* @returns {Promise<void>}
*/
async installGradleWrapper (gradleVersion) {
if (process.env.CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL) {
events.emit('verbose', `Overriding Gradle Version via CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL (${process.env.CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL})`);
await execa('gradle', ['-p', path.join(this.root, 'tools'), 'wrapper', '--gradle-distribution-url', process.env.CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL], { stdio: 'inherit' });
} else {
return execa(gradle_cmd, ['-p', this.root, 'wrapper', '-b', wrapperGradle], { stdio: 'inherit' });
await execa('gradle', ['-p', path.join(this.root, 'tools'), 'wrapper', '--gradle-version', gradleVersion], { stdio: 'inherit' });
}
}
readProjectProperties () {
function findAllUniq (data, r) {
var s = {};
var m;
const s = {};
let m;
while ((m = r.exec(data))) {
s[m[1]] = 1;
}
return Object.keys(s);
}
var data = fs.readFileSync(path.join(this.root, 'project.properties'), 'utf8');
const data = fs.readFileSync(path.join(this.root, 'project.properties'), 'utf8');
return {
libs: findAllUniq(data, /^\s*android\.library\.reference\.\d+=(.*)(?:\s|$)/mg),
gradleIncludes: findAllUniq(data, /^\s*cordova\.gradle\.include\.\d+=(.*)(?:\s|$)/mg),
systemLibs: findAllUniq(data, /^\s*cordova\.system\.library\.\d+=(.*)(?:\s|$)/mg)
systemLibs: findAllUniq(data, /^\s*cordova\.system\.library\.\d+=((?!.*\().*)(?:\s|$)/mg),
bomPlatforms: findAllUniq(data, /^\s*cordova\.system\.library\.\d+=platform\((?:'|")(.*)(?:'|")\)/mg)
};
}
extractRealProjectNameFromManifest () {
var manifestPath = path.join(this.root, 'app', 'src', 'main', 'AndroidManifest.xml');
var manifestData = fs.readFileSync(manifestPath, 'utf8');
var m = /<manifest[\s\S]*?package\s*=\s*"(.*?)"/i.exec(manifestData);
if (!m) {
throw new CordovaError('Could not find package name in ' + manifestPath);
}
var packageName = m[1];
var lastDotIndex = packageName.lastIndexOf('.');
return packageName.substring(lastDotIndex + 1);
}
// Makes the project buildable, minus the gradle wrapper.
prepBuildFiles () {
// Update the version of build.gradle in each dependent library.
var pluginBuildGradle = path.join(__dirname, 'plugin-build.gradle');
var propertiesObj = this.readProjectProperties();
var subProjects = propertiesObj.libs;
const pluginBuildGradle = path.join(__dirname, 'plugin-build.gradle');
const propertiesObj = this.readProjectProperties();
const subProjects = propertiesObj.libs;
// Check and copy the gradle file into the subproject
// Called by the loop before this function def
var checkAndCopy = function (subProject, root) {
var subProjectGradle = path.join(root, subProject, 'build.gradle');
const checkAndCopy = function (subProject, root) {
const subProjectGradle = path.join(root, subProject, 'build.gradle');
// This is the future-proof way of checking if a file exists
// This must be synchronous to satisfy a Travis test
try {
fs.accessSync(subProjectGradle, fs.F_OK);
fs.accessSync(subProjectGradle, fs.constants.F_OK);
} catch (e) {
fs.copySync(pluginBuildGradle, subProjectGradle);
fs.cpSync(pluginBuildGradle, subProjectGradle);
}
};
for (var i = 0; i < subProjects.length; ++i) {
for (let i = 0; i < subProjects.length; ++i) {
if (subProjects[i] !== 'CordovaLib') {
checkAndCopy(subProjects[i], this.root);
}
}
var name = this.extractRealProjectNameFromManifest();
// get project name cdv-gradle-config.
const cdvGradleConfig = CordovaGradleConfigParserFactory.create(this.root);
const projectName = cdvGradleConfig.getProjectNameFromPackageName();
// Remove the proj.id/name- prefix from projects: https://issues.apache.org/jira/browse/CB-9149
var settingsGradlePaths = subProjects.map(function (p) {
var realDir = p.replace(/[/\\]/g, ':');
var libName = realDir.replace(name + '-', '');
var str = 'include ":' + libName + '"\n';
if (realDir.indexOf(name + '-') !== -1) {
const settingsGradlePaths = subProjects.map(function (p) {
const realDir = p.replace(/[/\\]/g, ':');
const libName = realDir.replace(projectName + '-', '');
let str = 'include ":' + libName + '"\n';
if (realDir.indexOf(projectName + '-') !== -1) {
str += 'project(":' + libName + '").projectDir = new File("' + p + '")\n';
}
return str;
});
// Update subprojects within settings.gradle.
fs.writeFileSync(path.join(this.root, 'settings.gradle'),
'// GENERATED FILE - DO NOT EDIT\n' +
'include ":"\n' + settingsGradlePaths.join(''));
'apply from: "cdv-gradle-name.gradle"\n' +
'include ":"\n' +
settingsGradlePaths.join('') +
'\nif (file("settings-extras.gradle").exists()) {\n apply from: "settings-extras.gradle"\n}\n');
// Touch empty cdv-gradle-name.gradle file if missing.
if (!fs.existsSync(path.join(this.root, 'cdv-gradle-name.gradle'))) {
fs.writeFileSync(path.join(this.root, 'cdv-gradle-name.gradle'), '');
}
// Update dependencies within build.gradle.
var buildGradle = fs.readFileSync(path.join(this.root, 'app', 'build.gradle'), 'utf8');
var depsList = '';
var root = this.root;
var insertExclude = function (p) {
var gradlePath = path.join(root, p, 'build.gradle');
var projectGradleFile = fs.readFileSync(gradlePath, 'utf-8');
let buildGradle = fs.readFileSync(path.join(this.root, 'app', 'build.gradle'), 'utf8');
let depsList = '';
const root = this.root;
const insertExclude = function (p) {
const gradlePath = path.join(root, p, 'build.gradle');
const projectGradleFile = fs.readFileSync(gradlePath, 'utf-8');
if (projectGradleFile.indexOf('CordovaLib') !== -1) {
depsList += '{\n exclude module:("CordovaLib")\n }\n';
} else {
@@ -214,26 +228,39 @@ class ProjectBuilder {
};
subProjects.forEach(function (p) {
events.emit('log', 'Subproject Path: ' + p);
var libName = p.replace(/[/\\]/g, ':').replace(name + '-', '');
const libName = p.replace(/[/\\]/g, ':').replace(projectName + '-', '');
if (libName !== 'app') {
depsList += ' implementation(project(path: ":' + libName + '"))';
insertExclude(p);
}
});
// For why we do this mapping: https://issues.apache.org/jira/browse/CB-8390
var SYSTEM_LIBRARY_MAPPINGS = [
const SYSTEM_LIBRARY_MAPPINGS = [
[/^\/?extras\/android\/support\/(.*)$/, 'com.android.support:support-$1:+'],
[/^\/?google\/google_play_services\/libproject\/google-play-services_lib\/?$/, 'com.google.android.gms:play-services:+']
];
propertiesObj.bomPlatforms.forEach(function (p) {
if (!/:.*:/.exec(p)) {
throw new CordovaError('Malformed BoM platform: ' + p);
}
// Add bom platform
depsList += ' implementation platform("' + p + '")\n';
});
propertiesObj.systemLibs.forEach(function (p) {
var mavenRef;
let mavenRef;
// It's already in gradle form if it has two ':'s
if (/:.*:/.exec(p)) {
mavenRef = p;
} else if (/:.*/.exec(p)) {
// Support BoM imports
mavenRef = p;
events.emit('warn', 'Library expects a BoM package: ' + p);
} else {
for (var i = 0; i < SYSTEM_LIBRARY_MAPPINGS.length; ++i) {
var pair = SYSTEM_LIBRARY_MAPPINGS[i];
for (let i = 0; i < SYSTEM_LIBRARY_MAPPINGS.length; ++i) {
const pair = SYSTEM_LIBRARY_MAPPINGS[i];
if (pair[0].exec(p)) {
mavenRef = p.replace(pair[0], pair[1]);
break;
@@ -247,7 +274,7 @@ class ProjectBuilder {
});
buildGradle = buildGradle.replace(/(SUB-PROJECT DEPENDENCIES START)[\s\S]*(\/\/ SUB-PROJECT DEPENDENCIES END)/, '$1\n' + depsList + ' $2');
var includeList = '';
let includeList = '';
propertiesObj.gradleIncludes.forEach(function (includePath) {
includeList += 'apply from: "../' + includePath + '"\n';
@@ -258,32 +285,64 @@ class ProjectBuilder {
}
prepEnv (opts) {
var self = this;
const self = this;
const config = this._getCordovaConfig();
const configPropertiesPath = path.join(self.root, 'tools', '.gradle', 'config.properties');
return check_reqs.check_gradle()
.then(function (gradlePath) {
return self.runGradleWrapper(gradlePath);
.then(function () {
events.emit('verbose', `Using Gradle: ${config.GRADLE_VERSION}`);
return self.installGradleWrapper(config.GRADLE_VERSION);
}).then(async function () {
try {
// Try to create "config.properties" file if missing
const fd = fs.openSync(configPropertiesPath, 'wx+', 0o600);
fs.writeFileSync(fd, '', 'utf8');
fs.closeSync(fd);
} catch {
// File already existed, nothing to do.
}
// Loads "config.properties"for editing
const configProperties = createEditor(configPropertiesPath);
/*
* File is replaced on each build. "before_build" hook scripts can inject
* custom settings before this step runs. This step will still overwrite
* the "java.home" setting. Ensure environment variables are properly
* configured if want to use custom "java.home".
*
* Sets "java.home" using the "CORDOVA_JAVA_HOME" environment variable.
* If unavailable, fallback to "JAVA_HOME".
* If neither is set, "java.home" is removed (if previously set),
* allowing Android Studio to display a warning and auto-configure
* to use its internal (bundled) Java.
*/
const javaHome = process.env.CORDOVA_JAVA_HOME || process.env.JAVA_HOME || false;
if (javaHome) {
// Double escape back-slashes so that it is written as escaped back-slashes
// in the gradle config. Primary an issue in window environments.
configProperties.set('java.home', javaHome.replace(/\\/g, '\\\\'));
} else {
configProperties.unset('java.home');
}
// Saves changes
configProperties.save();
}).then(async function () {
await fsp.cp(path.join(self.root, 'tools', 'gradle'), path.join(self.root, 'gradle'), { recursive: true, force: true });
await fsp.cp(path.join(self.root, 'tools', 'gradlew'), path.join(self.root, 'gradlew'), { recursive: true, force: true });
await fsp.cp(path.join(self.root, 'tools', 'gradlew.bat'), path.join(self.root, 'gradlew.bat'), { recursive: true, force: true });
await fsp.cp(path.join(self.root, 'tools', '.gradle'), path.join(self.root, '.gradle'), { recursive: true, force: true });
}).then(function () {
return self.prepBuildFiles();
}).then(() => {
const config = this._getCordovaConfig();
// update/set the distributionUrl in the gradle-wrapper.properties
const gradleWrapperPropertiesPath = path.join(self.root, 'gradle/wrapper/gradle-wrapper.properties');
const gradleWrapperProperties = createEditor(gradleWrapperPropertiesPath);
const distributionUrl = process.env.CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL || `https://services.gradle.org/distributions/gradle-${config.GRADLE_VERSION}-all.zip`;
gradleWrapperProperties.set('distributionUrl', distributionUrl);
gradleWrapperProperties.save();
events.emit('verbose', `Gradle Distribution URL: ${distributionUrl}`);
})
.then(() => {
const signingPropertiesPath = path.join(self.root, `${opts.buildType}${SIGNING_PROPERTIES}`);
if (fs.existsSync(signingPropertiesPath)) fs.removeSync(signingPropertiesPath);
if (opts.packageInfo) {
fs.ensureFileSync(signingPropertiesPath);
fs.writeFileSync(signingPropertiesPath, '', 'utf8');
const signingProperties = createEditor(signingPropertiesPath);
signingProperties.addHeadComment(TEMPLATE);
opts.packageInfo.appendToProperties(signingProperties);
} else {
fs.rmSync(signingPropertiesPath, { force: true });
}
});
}
@@ -293,7 +352,7 @@ class ProjectBuilder {
* @returns The user defined configs
*/
_getCordovaConfig () {
return fs.readJSONSync(path.join(this.root, 'cdv-gradle-config.json'));
return JSON.parse(fs.readFileSync(path.join(this.root, 'cdv-gradle-config.json'), 'utf-8') || '{}');
}
/*
@@ -301,8 +360,10 @@ class ProjectBuilder {
* Returns a promise.
*/
async build (opts) {
var wrapper = path.join(this.root, 'gradlew');
var args = this.getArgs(opts.buildType === 'debug' ? 'debug' : 'release', opts);
const wrapper = this.getGradleWrapperPath();
const args = this.getArgs(opts.buildType === 'debug' ? 'debug' : 'release', opts);
events.emit('verbose', `Running Gradle Build: ${wrapper} ${args.join(' ')}`);
try {
return await execa(wrapper, args, { stdio: 'inherit', cwd: path.resolve(this.root) });
@@ -320,11 +381,11 @@ class ProjectBuilder {
}
clean (opts) {
const wrapper = path.join(this.root, 'gradlew');
const wrapper = this.getGradleWrapperPath();
const args = this.getArgs('clean', opts);
return execa(wrapper, args, { stdio: 'inherit', cwd: path.resolve(this.root) })
.then(() => {
fs.removeSync(path.join(this.root, 'out'));
fs.rmSync(path.join(this.root, 'out'), { recursive: true, force: true });
['debug', 'release'].map(config => path.join(this.root, `${config}${SIGNING_PROPERTIES}`))
.forEach(file => {
@@ -332,7 +393,7 @@ class ProjectBuilder {
const hasMarker = hasFile && fs.readFileSync(file, 'utf8')
.includes(MARKER);
if (hasFile && hasMarker) fs.removeSync(file);
if (hasFile && hasMarker) fs.rmSync(file);
});
});
}

View File

@@ -42,7 +42,7 @@ dependencies {
}
android {
compileSdkVersion cordovaConfig.SDK_VERSION
compileSdkVersion cordovaConfig.COMPILE_SDK_VERSION
buildToolsVersion cordovaConfig.BUILD_TOOLS_VERSION
compileOptions {

View File

@@ -17,14 +17,14 @@
under the License.
*/
const execa = require('execa');
var path = require('path');
var fs = require('fs-extra');
const path = require('node:path');
const fs = require('node:fs');
const { forgivingWhichSync, isWindows, isDarwin } = require('./utils');
const java = require('./env/java');
const { CordovaError, ConfigParser, events } = require('cordova-common');
var android_sdk = require('./android_sdk');
const android_sdk = require('./android_sdk');
const { SDK_VERSION } = require('./gradle-config-defaults');
const AndroidCommandLineTools = require('./env/AndroidCommandLineTools');
// Re-exporting these for backwards compatibility and for unit testing.
// TODO: Remove uses and use the ./utils module directly.
@@ -44,6 +44,31 @@ module.exports.get_target = function (projectRoot) {
return `android-${Math.max(userTargetSdkVersion, SDK_VERSION)}`;
};
/**
* @param {string} projectRoot
* @returns {string} The android target in format "android-${target}"
*/
module.exports.get_compile = function (projectRoot) {
const userTargetSdkVersion = getUserTargetSdkVersion(projectRoot) || SDK_VERSION;
const userCompileSdkVersion = getUserCompileSdkVersion(projectRoot) || userTargetSdkVersion;
module.exports.isCompileSdkValid(userCompileSdkVersion, userTargetSdkVersion);
return userCompileSdkVersion;
};
module.exports.isCompileSdkValid = (compileSdk, targetSdk) => {
targetSdk = (targetSdk || SDK_VERSION);
compileSdk = (compileSdk || targetSdk);
const isValid = compileSdk >= targetSdk;
if (!isValid) {
events.emit('warn', `The "android-compileSdkVersion" (${compileSdk}) should be greater than or equal to the "android-targetSdkVersion" (${targetSdk}).`);
}
return isValid;
};
/**
* @param {string} projectRoot
* @returns {number} target sdk or 0 if undefined
@@ -61,56 +86,36 @@ function getUserTargetSdkVersion (projectRoot) {
return isNaN(targetSdkVersion) ? 0 : targetSdkVersion;
}
/**
* @param {string} projectRoot
* @returns {number} target sdk or 0 if undefined
*/
function getUserCompileSdkVersion (projectRoot) {
// If the repo config.xml file exists, find the desired compileSdkVersion.
// We need to use the cordova project's config.xml here, since the platform
// project's config.xml does not yet have the user's preferences when this
// function is called during `Api.createPlatform`.
const configFile = path.join(projectRoot, '../../config.xml');
if (!fs.existsSync(configFile)) return 0;
const configParser = new ConfigParser(configFile);
const compileSdkVersion = parseInt(configParser.getPreference('android-compileSdkVersion', 'android'), 10);
return isNaN(compileSdkVersion) ? 0 : compileSdkVersion;
}
module.exports.get_gradle_wrapper = function () {
var androidStudioPath;
var i = 0;
var foundStudio = false;
var program_dir;
// OK, This hack only works on Windows, not on Mac OS or Linux. We will be deleting this eventually!
if (module.exports.isWindows()) {
var result = execa.sync(path.join(__dirname, 'getASPath.bat'));
// console.log('result.stdout =' + result.stdout.toString());
// console.log('result.stderr =' + result.stderr.toString());
if (result.stderr.toString().length > 0) {
var androidPath = path.join(process.env.ProgramFiles, 'Android') + '/';
if (fs.existsSync(androidPath)) {
program_dir = fs.readdirSync(androidPath);
while (i < program_dir.length && !foundStudio) {
if (program_dir[i].startsWith('Android Studio')) {
foundStudio = true;
androidStudioPath = path.join(process.env.ProgramFiles, 'Android', program_dir[i], 'gradle');
} else { ++i; }
}
}
} else {
// console.log('got android studio path from registry');
// remove the (os independent) new line char at the end of stdout
// add gradle to match the above.
androidStudioPath = path.join(result.stdout.toString().split('\r\n')[0], 'gradle');
}
}
if (androidStudioPath !== null && fs.existsSync(androidStudioPath)) {
var dirs = fs.readdirSync(androidStudioPath);
if (dirs[0].split('-')[0] === 'gradle') {
return path.join(androidStudioPath, dirs[0], 'bin', 'gradle');
}
} else {
// OK, let's try to check for Gradle!
return forgivingWhichSync('gradle');
}
return forgivingWhichSync('gradle');
};
// Returns a promise. Called only by build and clean commands.
module.exports.check_gradle = function () {
var sdkDir = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME;
const sdkDir = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
if (!sdkDir) {
return Promise.reject(new CordovaError('Could not find gradle wrapper within Android SDK. Could not find Android SDK directory.\n' +
'Might need to install Android SDK or set up \'ANDROID_SDK_ROOT\' env variable.'));
'Might need to install Android SDK or set up \'ANDROID_HOME\' env variable.'));
}
var gradlePath = module.exports.get_gradle_wrapper();
const gradlePath = module.exports.get_gradle_wrapper();
if (gradlePath.length !== 0) return Promise.resolve(gradlePath);
@@ -132,19 +137,20 @@ module.exports.check_java = async function () {
// Returns a promise.
module.exports.check_android = function () {
return Promise.resolve().then(function () {
let hasAndroidHome = false;
function maybeSetAndroidHome (value) {
if (!hasAndroidHome && fs.existsSync(value)) {
hasAndroidHome = true;
process.env.ANDROID_SDK_ROOT = value;
process.env.ANDROID_HOME = value;
}
}
var adbInPath = forgivingWhichSync('adb');
var avdmanagerInPath = forgivingWhichSync('avdmanager');
var hasAndroidHome = false;
const adbInPath = forgivingWhichSync('adb');
const avdmanagerInPath = forgivingWhichSync('avdmanager');
if (process.env.ANDROID_SDK_ROOT) {
maybeSetAndroidHome(path.resolve(process.env.ANDROID_SDK_ROOT));
if (process.env.ANDROID_HOME) {
maybeSetAndroidHome(path.resolve(process.env.ANDROID_HOME));
}
// First ensure ANDROID_HOME is set
@@ -197,45 +203,48 @@ module.exports.check_android = function () {
}
if (!hasAndroidHome) {
// If we dont have ANDROID_SDK_ROOT, but we do have some tools on the PATH, try to infer from the tooling PATH.
var parentDir, grandParentDir;
// If we dont have ANDROID_HOME, but we do have some tools on the PATH, try to infer from the tooling PATH.
let parentDir, grandParentDir;
if (adbInPath) {
parentDir = path.dirname(adbInPath);
grandParentDir = path.dirname(parentDir);
if (path.basename(parentDir) === 'platform-tools') {
maybeSetAndroidHome(grandParentDir);
} else {
throw new CordovaError('Failed to find \'ANDROID_SDK_ROOT\' environment variable. Try setting it manually.\n' +
throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting it manually.\n' +
'Detected \'adb\' command at ' + parentDir + ' but no \'platform-tools\' directory found near.\n' +
'Try reinstall Android SDK or update your PATH to include valid path to SDK' + path.sep + 'platform-tools directory.');
}
}
if (avdmanagerInPath) {
parentDir = path.dirname(avdmanagerInPath);
grandParentDir = path.dirname(parentDir);
if (path.basename(parentDir) === 'bin' && path.basename(grandParentDir) === 'tools') {
maybeSetAndroidHome(path.dirname(grandParentDir));
let sdkPath = null;
if (/cmdline-tools/.test(avdmanagerInPath)) {
sdkPath = path.resolve(avdmanagerInPath, '../../../..');
maybeSetAndroidHome(sdkPath);
} else {
throw new CordovaError('Failed to find \'ANDROID_SDK_ROOT\' environment variable. Try setting it manually.\n' +
'Detected \'avdmanager\' command at ' + parentDir + ' but no \'tools' + path.sep + 'bin\' directory found near.\n' +
'Try reinstall Android SDK or update your PATH to include valid path to SDK' + path.sep + 'tools' + path.sep + 'bin directory.');
throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting it manually.\n' +
'Detected \'avdmanager\' command at ' + parentDir + ' but does not appear to be within an Android SDK installation.\n' +
'Try reinstall Android SDK or update your PATH to include valid path to SDK');
}
}
}
if (!process.env.ANDROID_SDK_ROOT) {
throw new CordovaError('Failed to find \'ANDROID_SDK_ROOT\' environment variable. Try setting it manually.\n' +
if (!process.env.ANDROID_HOME) {
throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting it manually.\n' +
'Failed to find \'android\' command in your \'PATH\'. Try update your \'PATH\' to include path to valid SDK directory.');
}
if (!fs.existsSync(process.env.ANDROID_SDK_ROOT)) {
throw new CordovaError('\'ANDROID_SDK_ROOT\' environment variable is set to non-existent path: ' + process.env.ANDROID_SDK_ROOT +
if (!fs.existsSync(process.env.ANDROID_HOME)) {
throw new CordovaError('\'ANDROID_HOME\' environment variable is set to non-existent path: ' + process.env.ANDROID_HOME +
'\nTry update it manually to point to valid SDK directory.');
}
// Next let's make sure relevant parts of the SDK tooling is in our PATH
if (hasAndroidHome && !adbInPath) {
process.env.PATH += path.delimiter + path.join(process.env.ANDROID_SDK_ROOT, 'platform-tools');
process.env.PATH += path.delimiter + path.join(process.env.ANDROID_HOME, 'platform-tools');
}
if (hasAndroidHome && !avdmanagerInPath) {
process.env.PATH += path.delimiter + path.join(process.env.ANDROID_SDK_ROOT, 'tools', 'bin');
const cmdLineToolsBin = AndroidCommandLineTools.getBinPath();
if (cmdLineToolsBin) {
process.env.PATH += path.delimiter + cmdLineToolsBin;
}
}
return hasAndroidHome;
});
@@ -247,7 +256,7 @@ module.exports.check_android_target = function (projectRoot) {
// android-L
// Google Inc.:Google APIs:20
// Google Inc.:Glass Development Kit Preview:20
var desired_api_level = module.exports.get_target(projectRoot);
const desired_api_level = module.exports.get_target(projectRoot);
return android_sdk.list_targets().then(function (targets) {
if (targets.indexOf(desired_api_level) >= 0) {
return targets;
@@ -259,11 +268,11 @@ module.exports.check_android_target = function (projectRoot) {
// Returns a promise.
module.exports.run = function () {
console.log('Checking Java JDK and Android SDK versions');
console.log('ANDROID_SDK_ROOT=' + process.env.ANDROID_SDK_ROOT + ' (recommended setting)');
console.log('ANDROID_HOME=' + process.env.ANDROID_HOME + ' (DEPRECATED)');
console.log('ANDROID_HOME=' + process.env.ANDROID_HOME + ' (recommended setting)');
console.log('ANDROID_SDK_ROOT=' + process.env.ANDROID_SDK_ROOT + ' (DEPRECATED)');
return Promise.all([this.check_java(), this.check_android()]).then(function (values) {
console.log('Using Android SDK: ' + process.env.ANDROID_SDK_ROOT);
console.log('Using Android SDK: ' + (process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT));
if (!values[1]) {
throw new CordovaError('Requirements check failed for Android SDK! Android SDK was not detected.');
@@ -279,12 +288,12 @@ module.exports.run = function () {
* (for example, check_android_target returns an array of android targets installed)
* @param {Boolean} installed Indicates whether the requirement is installed or not
*/
var Requirement = function (id, name, version, installed) {
const Requirement = function (id, name, version, installed) {
this.id = id;
this.name = name;
this.installed = installed || false;
this.metadata = {
version: version
version
};
};
@@ -296,14 +305,14 @@ var Requirement = function (id, name, version, installed) {
* @return Promise<Requirement[]> Array of requirements. Due to implementation, promise is always fulfilled.
*/
module.exports.check_all = function (projectRoot) {
var requirements = [
const requirements = [
new Requirement('java', 'Java JDK'),
new Requirement('androidSdk', 'Android SDK'),
new Requirement('androidTarget', 'Android target'),
new Requirement('gradle', 'Gradle')
];
var checkFns = [
const checkFns = [
this.check_java,
this.check_android,
this.check_android_target.bind(this, projectRoot),
@@ -313,7 +322,7 @@ module.exports.check_all = function (projectRoot) {
// Then execute requirement checks one-by-one
return checkFns.reduce(function (promise, checkFn, idx) {
// Update each requirement with results
var requirement = requirements[idx];
const requirement = requirements[idx];
return promise.then(checkFn).then(function (version) {
requirement.installed = true;
requirement.metadata.version = version;

View File

@@ -0,0 +1,71 @@
/**
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 fs = require('node:fs');
const path = require('node:path');
const events = require('cordova-common').events;
class CordovaGradleConfigParser {
/**
* Loads and Edits Gradle Properties File.
*
* Do not construct this directly. Use CordovaGradleConfigParserFactory instead.
*
* @param {String} platformDir is the path of the Android platform directory
*/
constructor (platformDir) {
this._cdvGradleConfigFilePath = path.join(platformDir, 'cdv-gradle-config.json');
this._cdvGradleConfig = this._readConfig(this._cdvGradleConfigFilePath);
}
/**
* Reads and parses the configuration JSON file
*
* @param {String} configPath
* @returns {Record<any, any>} The parsed JSON object representing the gradle config.
*/
_readConfig (configPath) {
return JSON.parse(fs.readFileSync(configPath, 'utf-8') || '{}');
}
setPackageName (packageName) {
events.emit('verbose', '[Cordova Gradle Config] Setting "PACKAGE_NAMESPACE" to ' + packageName);
this._cdvGradleConfig.PACKAGE_NAMESPACE = packageName;
return this;
}
getPackageName () {
return this._cdvGradleConfig.PACKAGE_NAMESPACE;
}
getProjectNameFromPackageName () {
const packageName = this._cdvGradleConfig.PACKAGE_NAMESPACE;
return packageName.substring(packageName.lastIndexOf('.') + 1);
}
/**
* Saves any changes that has been made to the properties file.
*/
write () {
events.emit('verbose', '[Cordova Gradle Config] Saving File');
fs.writeFileSync(this._cdvGradleConfigFilePath, JSON.stringify(this._cdvGradleConfig, null, 2), 'utf-8');
}
}
module.exports = CordovaGradleConfigParser;

View File

@@ -0,0 +1,34 @@
/**
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 CordovaGradleConfigParser = require('./CordovaGradleConfigParser');
/**
* Builds new gradle config parsers
*/
module.exports = class CordovaGradleConfigParserFactory {
/**
* Loads and Edits Gradle Properties File.
*
* @param {String} platformDir is the path of the Android platform directory
*/
static create (platformDir) {
return new CordovaGradleConfigParser(platformDir);
}
};

View File

@@ -17,8 +17,8 @@
under the License.
*/
const fs = require('fs');
const path = require('path');
const fs = require('node:fs');
const path = require('node:path');
const propertiesParser = require('properties-parser');
const events = require('cordova-common').events;

View File

@@ -17,15 +17,16 @@
under the License.
*/
var path = require('path');
var fs = require('fs-extra');
var utils = require('./utils');
var check_reqs = require('./check_reqs');
var ROOT = path.join(__dirname, '..');
const path = require('node:path');
const fs = require('node:fs');
const utils = require('./utils');
const check_reqs = require('./check_reqs');
const ROOT = path.join(__dirname, '..');
const { createEditor } = require('properties-parser');
const CordovaGradleConfigParserFactory = require('./config/CordovaGradleConfigParserFactory');
var CordovaError = require('cordova-common').CordovaError;
var AndroidManifest = require('./AndroidManifest');
const CordovaError = require('cordova-common').CordovaError;
const AndroidManifest = require('./AndroidManifest');
// Export all helper functions, and make sure internally within this module, we
// reference these methods via the `exports` object - this helps with testing
@@ -37,46 +38,45 @@ 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');
}
function copyJsAndLibrary (projectPath, shared, projectName, targetAPI) {
var nestedCordovaLibPath = getFrameworkDir(projectPath, false);
var srcCordovaJsPath = path.join(ROOT, 'templates', 'project', 'assets', 'www', 'cordova.js');
var app_path = path.join(projectPath, 'app', 'src', 'main');
const nestedCordovaLibPath = getFrameworkDir(projectPath, false);
const srcCordovaJsPath = path.join(ROOT, 'templates', 'project', 'assets', 'www', 'cordova.js');
const app_path = path.join(projectPath, 'app', 'src', 'main');
const platform_www = path.join(projectPath, 'platform_www');
fs.copySync(srcCordovaJsPath, path.join(app_path, 'assets', 'www', 'cordova.js'));
fs.cpSync(srcCordovaJsPath, path.join(app_path, 'assets', 'www', 'cordova.js'));
// Copy the cordova.js file to platforms/<platform>/platform_www/
// The www dir is nuked on each prepare so we keep cordova.js in platform_www
fs.ensureDirSync(platform_www);
fs.copySync(srcCordovaJsPath, path.join(platform_www, 'cordova.js'));
fs.mkdirSync(platform_www, { recursive: true });
fs.cpSync(srcCordovaJsPath, path.join(platform_www, 'cordova.js'));
fs.cpSync(path.join(ROOT, 'framework', 'cdv-gradle-config-defaults.json'), path.join(projectPath, 'cdv-gradle-config.json'));
if (shared) {
var relativeFrameworkPath = path.relative(projectPath, getFrameworkDir(projectPath, true));
const relativeFrameworkPath = path.relative(projectPath, getFrameworkDir(projectPath, true));
fs.symlinkSync(relativeFrameworkPath, nestedCordovaLibPath, 'dir');
} else {
fs.ensureDirSync(nestedCordovaLibPath);
fs.copySync(path.join(ROOT, 'framework', 'AndroidManifest.xml'), path.join(nestedCordovaLibPath, 'AndroidManifest.xml'));
fs.mkdirSync(nestedCordovaLibPath, { recursive: true });
fs.cpSync(path.join(ROOT, 'framework', 'AndroidManifest.xml'), path.join(nestedCordovaLibPath, 'AndroidManifest.xml'));
const propertiesEditor = createEditor(path.join(ROOT, 'framework', 'project.properties'));
propertiesEditor.set('target', targetAPI);
propertiesEditor.save(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'));
fs.copySync(path.join(ROOT, 'framework', 'cdv-gradle-config-defaults.json'), path.join(projectPath, 'cdv-gradle-config.json'));
fs.cpSync(path.join(ROOT, 'framework', 'build.gradle'), path.join(nestedCordovaLibPath, 'build.gradle'));
fs.cpSync(path.join(ROOT, 'framework', 'cordova.gradle'), path.join(nestedCordovaLibPath, 'cordova.gradle'));
fs.cpSync(path.join(ROOT, 'framework', 'repositories.gradle'), path.join(nestedCordovaLibPath, 'repositories.gradle'));
fs.cpSync(path.join(ROOT, 'framework', 'src'), path.join(nestedCordovaLibPath, 'src'), { recursive: true });
}
}
function extractSubProjectPaths (data) {
var ret = {};
var r = /^\s*android\.library\.reference\.\d+=(.*)(?:\s|$)/mg;
var m;
const ret = {};
const r = /^\s*android\.library\.reference\.\d+=(.*)(?:\s|$)/mg;
let m;
while ((m = r.exec(data))) {
ret[m[1]] = 1;
}
@@ -84,13 +84,13 @@ function extractSubProjectPaths (data) {
}
function writeProjectProperties (projectPath, target_api) {
var dstPath = path.join(projectPath, 'project.properties');
var templatePath = path.join(ROOT, 'templates', 'project', 'project.properties');
var srcPath = fs.existsSync(dstPath) ? dstPath : templatePath;
const dstPath = path.join(projectPath, 'project.properties');
const templatePath = path.join(ROOT, 'templates', 'project', 'project.properties');
const srcPath = fs.existsSync(dstPath) ? dstPath : templatePath;
var data = fs.readFileSync(srcPath, 'utf8');
let data = fs.readFileSync(srcPath, 'utf8');
data = data.replace(/^target=.*/m, 'target=' + target_api);
var subProjects = extractSubProjectPaths(data);
let subProjects = extractSubProjectPaths(data);
subProjects = subProjects.filter(function (p) {
return !(/^CordovaLib$/m.exec(p) ||
/[\\/]cordova-android[\\/]framework$/m.exec(p) ||
@@ -101,7 +101,7 @@ function writeProjectProperties (projectPath, target_api) {
if (!/\n$/.exec(data)) {
data += '\n';
}
for (var i = 0; i < subProjects.length; ++i) {
for (let i = 0; i < subProjects.length; ++i) {
data += 'android.library.reference.' + (i + 1) + '=' + subProjects[i] + '\n';
}
fs.writeFileSync(dstPath, data);
@@ -109,33 +109,28 @@ function writeProjectProperties (projectPath, target_api) {
// This makes no sense, what if you're building with a different build system?
function prepBuildFiles (projectPath) {
var buildModule = require('./builders/builders');
const buildModule = require('./builders/builders');
buildModule.getBuilder(projectPath).prepBuildFiles();
}
function copyBuildRules (projectPath, isLegacy) {
var srcDir = path.join(ROOT, 'templates', 'project');
function copyBuildRules (projectPath) {
const srcDir = path.join(ROOT, 'templates', 'project');
if (isLegacy) {
// The project's build.gradle is identical to the earlier build.gradle, so it should still work
fs.copySync(path.join(srcDir, 'legacy', 'build.gradle'), path.join(projectPath, 'legacy', 'build.gradle'));
fs.copySync(path.join(srcDir, 'wrapper.gradle'), path.join(projectPath, 'wrapper.gradle'));
} 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'));
}
fs.cpSync(path.join(srcDir, 'build.gradle'), path.join(projectPath, 'build.gradle'));
fs.cpSync(path.join(srcDir, 'app', 'build.gradle'), path.join(projectPath, 'app', 'build.gradle'));
fs.cpSync(path.join(srcDir, 'app', 'repositories.gradle'), path.join(projectPath, 'app', 'repositories.gradle'));
fs.cpSync(path.join(srcDir, 'repositories.gradle'), path.join(projectPath, 'repositories.gradle'));
copyGradleTools(projectPath);
}
function copyScripts (projectPath) {
var srcScriptsDir = path.join(ROOT, 'templates', 'cordova');
var destScriptsDir = path.join(projectPath, 'cordova');
const srcScriptsDir = path.join(ROOT, 'templates', 'cordova');
const destScriptsDir = path.join(projectPath, 'cordova');
// Delete old scripts directory if this is an update.
fs.removeSync(destScriptsDir);
fs.rmSync(destScriptsDir, { recursive: true, force: true });
// Copy in the new ones.
fs.copySync(srcScriptsDir, destScriptsDir);
fs.cpSync(srcScriptsDir, destScriptsDir, { recursive: true });
}
/**
@@ -147,7 +142,7 @@ function validatePackageName (package_name) {
// Make the package conform to Java package types
// http://developer.android.com/guide/topics/manifest/manifest-element.html#package
// Enforce underscore limitation
var msg = 'Error validating package name. ';
const msg = 'Error validating package name. ';
if (!/^[a-zA-Z][a-zA-Z0-9_]+(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(package_name)) {
return Promise.reject(new CordovaError(msg + 'Must look like: `com.company.Name`. Currently is: `' + package_name + '`'));
@@ -167,7 +162,7 @@ function validatePackageName (package_name) {
* otherwise.
*/
function validateProjectName (project_name) {
var msg = 'Error validating project name. ';
const msg = 'Error validating project name. ';
// Make sure there's something there
if (project_name === '') {
return Promise.reject(new CordovaError(msg + 'Project name cannot be empty'));
@@ -176,17 +171,10 @@ 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);
function copyGradleTools (projectPath) {
const srcDir = path.join(ROOT, 'templates', 'project');
fs.cpSync(path.resolve(srcDir, 'tools'), path.resolve(projectPath, 'tools'), { recursive: true });
}
/**
@@ -211,73 +199,78 @@ exports.create = function (project_path, config, options, events) {
options = options || {};
// Set default values for path, package and name
project_path = path.relative(process.cwd(), (project_path || 'CordovaExample'));
project_path = path.relative(process.cwd(), project_path);
// Check if project already exists
if (fs.existsSync(project_path)) {
return Promise.reject(new CordovaError('Project already exists! Delete and recreate'));
}
var package_name = config.android_packageName() || config.packageName() || 'my.cordova.project';
var project_name = config.name()
? config.name().replace(/[^\w.]/g, '_') : 'CordovaExample';
const package_name = config.android_packageName() || config.packageName() || 'org.apache.cordova.hellocordova';
const project_name = config.name() || 'Hello Cordova';
var safe_activity_name = config.android_activityName() || options.activityName || 'MainActivity';
var target_api = check_reqs.get_target(project_path);
const safe_activity_name = config.android_activityName() || options.activityName || 'MainActivity';
const target_api = check_reqs.get_target(project_path);
const compile_api = check_reqs.get_compile(project_path);
// Make the package conform to Java package types
return exports.validatePackageName(package_name)
.then(function () {
return exports.validateProjectName(project_name);
}).then(function () {
// Log the given values for the project
// Log the given values for the project
events.emit('log', 'Creating Cordova project for the Android platform:');
events.emit('log', '\tPath: ' + project_path);
events.emit('log', '\tPackage: ' + package_name);
events.emit('log', '\tName: ' + project_name);
events.emit('log', '\tActivity: ' + safe_activity_name);
events.emit('log', '\tAndroid target: ' + target_api);
events.emit('log', '\tAndroid Target SDK: ' + target_api);
events.emit('log', '\tAndroid Compile SDK: ' + compile_api);
events.emit('verbose', 'Copying android template project to ' + project_path);
var project_template_dir = options.customTemplate || path.join(ROOT, 'templates', 'project');
var app_path = path.join(project_path, 'app', 'src', 'main');
const project_template_dir = options.customTemplate || path.join(ROOT, 'templates', 'project');
const app_path = path.join(project_path, 'app', 'src', 'main');
// copy project template
fs.ensureDirSync(app_path);
fs.copySync(path.join(project_template_dir, 'assets'), path.join(app_path, 'assets'));
fs.copySync(path.join(project_template_dir, 'res'), path.join(app_path, 'res'));
fs.copySync(path.join(project_template_dir, 'gitignore'), path.join(project_path, '.gitignore'));
fs.mkdirSync(app_path, { recursive: true });
fs.cpSync(path.join(project_template_dir, 'assets'), path.join(app_path, 'assets'), { recursive: true });
fs.cpSync(path.join(project_template_dir, 'res'), path.join(app_path, 'res'), { recursive: true });
fs.cpSync(path.join(project_template_dir, 'gitignore'), path.join(project_path, '.gitignore'));
// Manually create directories that would be empty within the template (since git doesn't track directories).
fs.ensureDirSync(path.join(app_path, 'libs'));
fs.mkdirSync(path.join(app_path, 'libs'), { recursive: true });
// copy cordova.js, cordova.jar
exports.copyJsAndLibrary(project_path, options.link, safe_activity_name, target_api);
// Set up ther Android Studio paths
var java_path = path.join(app_path, 'java');
var assets_path = path.join(app_path, 'assets');
var resource_path = path.join(app_path, 'res');
fs.ensureDirSync(java_path);
fs.ensureDirSync(assets_path);
fs.ensureDirSync(resource_path);
const java_path = path.join(app_path, 'java');
const assets_path = path.join(app_path, 'assets');
const resource_path = path.join(app_path, 'res');
fs.mkdirSync(java_path, { recursive: true });
fs.mkdirSync(assets_path, { recursive: true });
fs.mkdirSync(resource_path, { recursive: true });
// store package name in cdv-gradle-config
const cdvGradleConfig = CordovaGradleConfigParserFactory.create(project_path);
cdvGradleConfig.setPackageName(package_name)
.write();
// interpolate the activity name and package
var packagePath = package_name.replace(/\./g, path.sep);
var activity_dir = path.join(java_path, packagePath);
var activity_path = path.join(activity_dir, safe_activity_name + '.java');
const packagePath = package_name.replace(/\./g, path.sep);
const activity_dir = path.join(java_path, packagePath);
const activity_path = path.join(activity_dir, safe_activity_name + '.java');
fs.ensureDirSync(activity_dir);
fs.copySync(path.join(project_template_dir, 'Activity.java'), activity_path);
fs.mkdirSync(activity_dir, { recursive: true });
fs.cpSync(path.join(project_template_dir, 'Activity.java'), activity_path);
utils.replaceFileContents(activity_path, /__ACTIVITY__/, safe_activity_name);
utils.replaceFileContents(path.join(app_path, 'res', 'values', 'strings.xml'), /__NAME__/, project_name);
utils.replaceFileContents(path.join(app_path, 'res', 'values', 'cdv_strings.xml'), /__NAME__/, utils.escape(project_name));
utils.replaceFileContents(activity_path, /__ID__/, package_name);
var manifest = new AndroidManifest(path.join(project_template_dir, 'AndroidManifest.xml'));
manifest.setPackageId(package_name)
.getActivity().setName(safe_activity_name);
const manifest = new AndroidManifest(path.join(project_template_dir, 'AndroidManifest.xml'));
manifest.getActivity().setName(safe_activity_name);
var manifest_path = path.join(app_path, 'AndroidManifest.xml');
const manifest_path = path.join(app_path, 'AndroidManifest.xml');
manifest.write(manifest_path);
exports.copyScripts(project_path);
@@ -286,14 +279,13 @@ 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);
};
function generateDoneMessage (type, link) {
var pkg = require('../package');
var msg = 'Android project ' + (type === 'update' ? 'updated ' : 'created ') + 'with ' + pkg.name + '@' + pkg.version;
const pkg = require('../package');
let msg = 'Android project ' + (type === 'update' ? 'updated ' : 'created ') + 'with ' + pkg.name + '@' + pkg.version;
if (link) {
msg += ' and has a linked CordovaLib';
}
@@ -302,7 +294,7 @@ function generateDoneMessage (type, link) {
// Returns a promise.
exports.update = function (projectPath, options, events) {
var errorString =
const errorString =
'An in-place platform update is not supported. \n' +
'The `platforms` folder is always treated as a build artifact in the CLI workflow.\n' +
'To update your platform, you have to remove, then add your android platform again.\n' +

View File

@@ -18,14 +18,14 @@
*/
const execa = require('execa');
const fs = require('fs-extra');
var android_versions = require('android-versions');
var path = require('path');
var Adb = require('./Adb');
var events = require('cordova-common').events;
var CordovaError = require('cordova-common').CordovaError;
var android_sdk = require('./android_sdk');
var which = require('which');
const fs = require('node:fs');
const android_versions = require('android-versions');
const path = require('node:path');
const Adb = require('./Adb');
const events = require('cordova-common').events;
const CordovaError = require('cordova-common').CordovaError;
const android_sdk = require('./android_sdk');
const which = require('which');
// constants
const ONE_SECOND = 1000; // in milliseconds
@@ -41,11 +41,11 @@ function forgivingWhichSync (cmd) {
module.exports.list_images_using_avdmanager = function () {
return execa('avdmanager', ['list', 'avd']).then(({ stdout: output }) => {
var response = output.split('\n');
var emulator_list = [];
for (var i = 1; i < response.length; i++) {
const response = output.split('\n');
const emulator_list = [];
for (let i = 1; i < response.length; i++) {
// To return more detailed information use img_obj
var img_obj = {};
const img_obj = {};
if (response[i].match(/Name:\s/)) {
img_obj.name = response[i].split('Name: ')[1].replace('\r', '');
if (response[i + 1].match(/Device:\s/)) {
@@ -74,9 +74,9 @@ module.exports.list_images_using_avdmanager = function () {
img_obj.target = img_obj.target.substr(0, img_obj.target.indexOf('(') - 1).trim();
}
}
var version_string = img_obj.target.replace(/Android\s+/, '');
const version_string = img_obj.target.replace(/Android\s+/, '');
var api_level = android_sdk.version_string_to_api_level[version_string];
const api_level = android_sdk.version_string_to_api_level[version_string];
if (api_level) {
img_obj.target += ' (API level ' + api_level + ')';
}
@@ -120,9 +120,9 @@ module.exports.list_images = function () {
// In case we're missing the Android OS version string from the target description, add it.
return avds.map(function (avd) {
if (avd.target && avd.target.indexOf('Android API') > -1 && avd.target.indexOf('API level') < 0) {
var api_level = avd.target.match(/\d+/);
const api_level = avd.target.match(/\d+/);
if (api_level) {
var level = android_versions.get(api_level);
const level = android_versions.get(api_level);
if (level) {
avd.target = 'Android ' + level.semver + ' (API level ' + api_level + ')';
}
@@ -145,12 +145,12 @@ module.exports.best_image = function (project_target) {
// Just return undefined if there is no images
if (images.length === 0) return;
var closest = 9999;
var best = images[0];
for (var i in images) {
var target = images[i].target;
let closest = 9999;
let best = images[0];
for (const i in images) {
const target = images[i].target;
if (target && target.indexOf('API level') > -1) {
var num = parseInt(target.split('(API level ')[1].replace(')', ''));
const num = parseInt(target.split('(API level ')[1].replace(')', ''));
if (num === project_target) {
return images[i];
} else if (project_target - num < closest && project_target > num) {
@@ -173,10 +173,10 @@ exports.list_started = async () => {
* Returns a promise.
*/
module.exports.get_available_port = function () {
var self = this;
const self = this;
return self.list_started().then(function (emulators) {
for (var p = 5584; p >= 5554; p -= 2) {
for (let p = 5584; p >= 5554; p -= 2) {
if (emulators.indexOf('emulator-' + p) === -1) {
events.emit('verbose', 'Found available port: ' + p);
return p;
@@ -195,7 +195,7 @@ module.exports.get_available_port = function () {
* Returns a promise.
*/
module.exports.start = function (emulatorId, boot_timeout) {
var self = this;
const self = this;
return Promise.resolve().then(function () {
if (!emulatorId) {
@@ -205,8 +205,8 @@ module.exports.start = function (emulatorId, boot_timeout) {
return self.get_available_port().then(function (port) {
// Figure out the directory the emulator binary runs in, and set the cwd to that directory.
// Workaround for https://code.google.com/p/android/issues/detail?id=235461
var emulator_dir = path.dirname(which.sync('emulator'));
var args = ['-avd', emulatorId, '-port', port];
const emulator_dir = path.dirname(which.sync('emulator'));
const args = ['-avd', emulatorId, '-port', port];
// Don't wait for it to finish, since the emulator will probably keep running for a long time.
execa('emulator', args, { stdio: 'inherit', detached: true, cwd: emulator_dir })
.unref();
@@ -241,9 +241,9 @@ module.exports.start = function (emulatorId, boot_timeout) {
* Returns this emulator's ID in a promise.
*/
module.exports.wait_for_emulator = function (port) {
var self = this;
const self = this;
return Promise.resolve().then(function () {
var emulator_id = 'emulator-' + port;
const emulator_id = 'emulator-' + port;
return Adb.shell(emulator_id, 'getprop dev.bootcomplete').then(function (output) {
if (output.indexOf('1') >= 0) {
return emulator_id;
@@ -271,7 +271,7 @@ module.exports.wait_for_emulator = function (port) {
* time_remaining or passing a negative value will cause it to wait forever
*/
module.exports.wait_for_boot = function (emulator_id, time_remaining) {
var self = this;
const self = this;
return Adb.shell(emulator_id, 'getprop sys.boot_completed').then(function (output) {
if (output.match(/1/)) {
return true;

102
lib/env/AndroidCommandLineTools.js vendored Normal file
View File

@@ -0,0 +1,102 @@
/**
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 { events } = require('cordova-common');
const fs = require('node:fs');
const path = require('node:path');
const semver = require('semver');
/**
* Utility collection for resolving the Android SDK command line tools installed
* on the workstation.
*/
const AndroidCommandLineTools = {
/**
* Gets a sorted list of available versions found on the system.
*
* If the command line tools is not resolvable, then an empty array will be returned.
*
* This function depends on ANDROID_HOME environment variable.
*
* @returns {String[]}
*/
getAvailableVersions: () => {
const androidHome = path.resolve(AndroidCommandLineTools.__getAndroidHome());
if (!fs.existsSync(androidHome)) {
events.emit('warn', 'ANDROID_HOME is not resolvable.');
return [];
}
const cmdLineToolsContainer = path.join(androidHome, 'cmdline-tools');
if (!fs.existsSync(cmdLineToolsContainer)) {
events.emit('warn', 'Android SDK is missing cmdline-tools directory.');
return [];
}
const cmdLineVersions = fs.readdirSync(cmdLineToolsContainer)
.filter((value) => {
// expected directory paths are semver-like version strings or literally "latest"
return value === 'latest' || semver.coerce(value) !== null;
})
.sort((a, b) => {
// "latest" directory always comes first
if (a === 'latest') return -1;
if (b === 'latest') return 1;
const av = semver.coerce(a, {
includePrerelease: true
});
const bv = semver.coerce(b, {
includePrerelease: true
});
// Descending (highest version first)
return semver.rcompare(av, bv);
});
return cmdLineVersions;
},
/**
* Gets the bin path of the cmd line tools using the latest available that
* is installed on the workstation.
*
* Returns null if there are no versions fond
*
* @returns {String | null}
*/
getBinPath: () => {
const versions = AndroidCommandLineTools.getAvailableVersions();
if (versions.length === 0) {
return null;
}
const version = versions[0];
return path.resolve(AndroidCommandLineTools.__getAndroidHome(), 'cmdline-tools', version, 'bin');
},
/**
* @internal
*/
__getAndroidHome: () => process.env.ANDROID_HOME
};
module.exports = AndroidCommandLineTools;

9
lib/env/java.js vendored
View File

@@ -18,8 +18,8 @@
*/
const execa = require('execa');
const fs = require('fs-extra');
const path = require('path');
const fs = require('node:fs');
const path = require('node:path');
const glob = require('fast-glob');
const { CordovaError, events } = require('cordova-common');
const utils = require('../utils');
@@ -98,8 +98,9 @@ const java = {
}
} 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'))) {
const maybeJavaHome = path.dirname(path.dirname(javacPath));
if (fs.existsSync(path.join(maybeJavaHome, 'bin', 'java')) ||
fs.existsSync(path.join(maybeJavaHome, 'bin', 'java.exe'))) {
environment.JAVA_HOME = maybeJavaHome;
} else {
throw new CordovaError(default_java_error_msg);

View File

@@ -1,20 +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.
@ECHO OFF
for /f "tokens=2*" %%a in ('REG QUERY "HKEY_LOCAL_MACHINE\SOFTWARE\Android Studio" /v Path') do set "ASPath=%%~b"
ECHO %ASPath%

View File

@@ -14,19 +14,19 @@
*
*/
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;
const fs = require('node:fs');
const path = require('node:path');
const isPathInside = require('is-path-inside');
const events = require('cordova-common').events;
const CordovaError = require('cordova-common').CordovaError;
var handlers = {
const handlers = {
'source-file': {
install: function (obj, plugin, project, options) {
if (!obj.src) throw new CordovaError(generateAttributeError('src', 'source-file', plugin.id));
if (!obj.targetDir) throw new CordovaError(generateAttributeError('target-dir', 'source-file', plugin.id));
var dest = getInstallDestination(obj);
const dest = getInstallDestination(obj);
if (options && options.force) {
copyFile(plugin.dir, obj.src, project.projectDir, dest, !!(options && options.link));
@@ -35,48 +35,48 @@ var handlers = {
}
},
uninstall: function (obj, plugin, project, options) {
var dest = getInstallDestination(obj);
const dest = getInstallDestination(obj);
// TODO: Add Koltin extension to uninstall, since they are handled like Java files
if (obj.src.endsWith('java')) {
deleteJava(project.projectDir, dest);
} else {
// Just remove the file, not the whole parent directory
removeFile(path.resolve(project.projectDir, dest));
removeFileF(path.resolve(project.projectDir, dest));
}
}
},
'lib-file': {
install: function (obj, plugin, project, options) {
var dest = path.join('app/libs', path.basename(obj.src));
const dest = path.join('app/libs', path.basename(obj.src));
copyFile(plugin.dir, obj.src, project.projectDir, dest, !!(options && options.link));
},
uninstall: function (obj, plugin, project, options) {
var dest = path.join('app/libs', path.basename(obj.src));
removeFile(path.resolve(project.projectDir, dest));
const dest = path.join('app/libs', path.basename(obj.src));
removeFileF(path.resolve(project.projectDir, dest));
}
},
'resource-file': {
install: function (obj, plugin, project, options) {
var dest = path.join('app', 'src', 'main', obj.target);
const dest = path.join('app', 'src', 'main', obj.target);
copyFile(plugin.dir, obj.src, project.projectDir, dest, !!(options && options.link));
},
uninstall: function (obj, plugin, project, options) {
var dest = path.join('app', 'src', 'main', obj.target);
removeFile(path.resolve(project.projectDir, dest));
const dest = path.join('app', 'src', 'main', obj.target);
removeFileF(path.resolve(project.projectDir, dest));
}
},
framework: {
install: function (obj, plugin, project, options) {
var src = obj.src;
const src = obj.src;
if (!src) throw new CordovaError(generateAttributeError('src', 'framework', plugin.id));
events.emit('verbose', 'Installing Android library: ' + src);
var parentDir = obj.parent ? path.resolve(project.projectDir, obj.parent) : project.projectDir;
var subDir;
const parentDir = obj.parent ? path.resolve(project.projectDir, obj.parent) : project.projectDir;
let subDir;
if (obj.custom) {
var subRelativeDir = project.getCustomSubprojectRelativeDir(plugin.id, src);
const subRelativeDir = project.getCustomSubprojectRelativeDir(plugin.id, src);
copyNewFile(plugin.dir, src, project.projectDir, subRelativeDir, !!(options && options.link));
subDir = path.resolve(project.projectDir, subRelativeDir);
} else {
@@ -93,19 +93,19 @@ var handlers = {
}
},
uninstall: function (obj, plugin, project, options) {
var src = obj.src;
const src = obj.src;
if (!src) throw new CordovaError(generateAttributeError('src', 'framework', plugin.id));
events.emit('verbose', 'Uninstalling Android library: ' + src);
var parentDir = obj.parent ? path.resolve(project.projectDir, obj.parent) : project.projectDir;
var subDir;
const parentDir = obj.parent ? path.resolve(project.projectDir, obj.parent) : project.projectDir;
let subDir;
if (obj.custom) {
var subRelativeDir = project.getCustomSubprojectRelativeDir(plugin.id, src);
removeFile(path.resolve(project.projectDir, subRelativeDir));
const subRelativeDir = project.getCustomSubprojectRelativeDir(plugin.id, src);
removeFileF(path.resolve(project.projectDir, subRelativeDir));
subDir = path.resolve(project.projectDir, subRelativeDir);
// If it's the last framework in the plugin, remove the parent directory.
var parDir = path.dirname(subDir);
const parDir = path.dirname(subDir);
if (fs.existsSync(parDir) && fs.readdirSync(parDir).length === 0) {
fs.rmdirSync(parDir);
}
@@ -139,45 +139,44 @@ var handlers = {
}
},
uninstall: function (obj, plugin, project, options) {
var target = obj.target || obj.src;
const target = obj.target || obj.src;
if (!target) throw new CordovaError(generateAttributeError('target', 'asset', plugin.id));
if (!target) {
throw new CordovaError(generateAttributeError('target', 'asset', plugin.id));
}
removeFile(path.resolve(project.www, target));
removeFile(path.resolve(project.www, 'plugins', plugin.id));
removeFileAndParents(project.www, target);
if (options && options.usePlatformWww) {
// CB-11022 remove file from both directories if usePlatformWww is specified
removeFile(path.resolve(project.platformWww, target));
removeFile(path.resolve(project.platformWww, 'plugins', plugin.id));
removeFileAndParents(project.platformWww, target);
}
}
},
'js-module': {
install: function (obj, plugin, project, options) {
// Copy the plugin's files into the www directory.
var moduleSource = path.resolve(plugin.dir, obj.src);
var moduleName = plugin.id + '.' + (obj.name || path.basename(obj.src, path.extname(obj.src)));
const moduleSource = path.resolve(plugin.dir, obj.src);
const moduleName = plugin.id + '.' + (obj.name || path.basename(obj.src, path.extname(obj.src)));
// Read in the file, prepend the cordova.define, and write it back out.
var scriptContent = fs.readFileSync(moduleSource, 'utf-8').replace(/^\ufeff/, ''); // Window BOM
let scriptContent = fs.readFileSync(moduleSource, 'utf-8').replace(/^\ufeff/, ''); // Window BOM
if (moduleSource.match(/.*\.json$/)) {
scriptContent = 'module.exports = ' + scriptContent;
}
scriptContent = 'cordova.define("' + moduleName + '", function(require, exports, module) {\n' + scriptContent + '\n});\n';
var wwwDest = path.resolve(project.www, 'plugins', plugin.id, obj.src);
fs.ensureDirSync(path.dirname(wwwDest));
const wwwDest = path.resolve(project.www, 'plugins', plugin.id, obj.src);
fs.mkdirSync(path.dirname(wwwDest), { recursive: true });
fs.writeFileSync(wwwDest, scriptContent, 'utf-8');
if (options && options.usePlatformWww) {
// CB-11022 copy file to both directories if usePlatformWww is specified
var platformWwwDest = path.resolve(project.platformWww, 'plugins', plugin.id, obj.src);
fs.ensureDirSync(path.dirname(platformWwwDest));
const platformWwwDest = path.resolve(project.platformWww, 'plugins', plugin.id, obj.src);
fs.mkdirSync(path.dirname(platformWwwDest), { recursive: true });
fs.writeFileSync(platformWwwDest, scriptContent, 'utf-8');
}
},
uninstall: function (obj, plugin, project, options) {
var pluginRelativePath = path.join('plugins', plugin.id, obj.src);
const pluginRelativePath = path.join('plugins', plugin.id, obj.src);
removeFileAndParents(project.www, pluginRelativePath);
if (options && options.usePlatformWww) {
// CB-11022 remove file from both directories if usePlatformWww is specified
@@ -208,8 +207,8 @@ function copyFile (plugin_dir, src, project_dir, dest, link) {
if (!fs.existsSync(src)) throw new CordovaError('"' + src + '" not found!');
// check that src path is inside plugin directory
var real_path = fs.realpathSync(src);
var real_plugin_path = fs.realpathSync(plugin_dir);
const real_path = fs.realpathSync(src);
const real_plugin_path = fs.realpathSync(plugin_dir);
if (!isPathInside(real_path, real_plugin_path)) { throw new CordovaError('File "' + src + '" is located outside the plugin directory "' + plugin_dir + '"'); }
dest = path.resolve(project_dir, dest);
@@ -217,17 +216,17 @@ function copyFile (plugin_dir, src, project_dir, dest, link) {
// check that dest path is located in project directory
if (!isPathInside(dest, project_dir)) { throw new CordovaError('Destination "' + dest + '" for source file "' + src + '" is located outside the project'); }
fs.ensureDirSync(path.dirname(dest));
fs.mkdirSync(path.dirname(dest), { recursive: true });
if (link) {
symlinkFileOrDirTree(src, dest);
} else {
fs.copySync(src, dest);
fs.cpSync(src, dest, { recursive: true });
}
}
// Same as copy file but throws error if target exists
function copyNewFile (plugin_dir, src, project_dir, dest, link) {
var target_path = path.resolve(project_dir, dest);
const target_path = path.resolve(project_dir, dest);
if (fs.existsSync(target_path)) { throw new CordovaError('"' + target_path + '" already exists!'); }
copyFile(plugin_dir, src, project_dir, dest, !!link);
@@ -235,11 +234,11 @@ function copyNewFile (plugin_dir, src, project_dir, dest, link) {
function symlinkFileOrDirTree (src, dest) {
if (fs.existsSync(dest)) {
fs.removeSync(dest);
fs.rmSync(dest, { recursive: true, force: true });
}
if (fs.statSync(src).isDirectory()) {
fs.ensureDirSync(path.dirname(dest));
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.readdirSync(src).forEach(function (entry) {
symlinkFileOrDirTree(path.join(src, entry), path.join(dest, entry));
});
@@ -248,8 +247,8 @@ function symlinkFileOrDirTree (src, dest) {
}
}
function removeFile (file) {
fs.removeSync(file);
function removeFileF (file) {
fs.rmSync(file, { recursive: true, force: true });
}
// Sometimes we want to remove some java, and prune any unnecessary empty directories
@@ -259,13 +258,13 @@ function deleteJava (project_dir, destFile) {
function removeFileAndParents (baseDir, destFile, stopper) {
stopper = stopper || '.';
var file = path.resolve(baseDir, destFile);
const file = path.resolve(baseDir, destFile);
if (!fs.existsSync(file)) return;
removeFile(file);
removeFileF(file);
// check if directory is empty
var curDir = path.dirname(file);
let curDir = path.dirname(file);
while (curDir !== path.resolve(baseDir, stopper)) {
if (fs.existsSync(curDir) && fs.readdirSync(curDir).length === 0) {
@@ -283,16 +282,16 @@ function generateAttributeError (attribute, element, id) {
}
function getInstallDestination (obj) {
var APP_MAIN_PREFIX = 'app/src/main';
var PATH_SEPARATOR = '/';
const APP_MAIN_PREFIX = 'app/src/main';
const PATH_SEPARATOR = '/';
var PATH_SEP_MATCH = '\\' + PATH_SEPARATOR;
var PATH_SEP_OR_EOL_MATCH = '(\\' + PATH_SEPARATOR + '|$)';
const PATH_SEP_MATCH = '\\' + PATH_SEPARATOR;
const PATH_SEP_OR_EOL_MATCH = '(\\' + PATH_SEPARATOR + '|$)';
var appReg = new RegExp('^app' + PATH_SEP_OR_EOL_MATCH);
var libsReg = new RegExp('^libs' + PATH_SEP_OR_EOL_MATCH);
var srcReg = new RegExp('^src' + PATH_SEP_OR_EOL_MATCH);
var srcMainReg = new RegExp('^src' + PATH_SEP_MATCH + 'main' + PATH_SEP_OR_EOL_MATCH);
const appReg = new RegExp('^app' + PATH_SEP_OR_EOL_MATCH);
const libsReg = new RegExp('^libs' + PATH_SEP_OR_EOL_MATCH);
const srcReg = new RegExp('^src' + PATH_SEP_OR_EOL_MATCH);
const srcMainReg = new RegExp('^src' + PATH_SEP_MATCH + 'main' + PATH_SEP_OR_EOL_MATCH);
if (appReg.test(obj.targetDir)) {
// If any source file is using the new app directory structure,

View File

@@ -17,24 +17,25 @@
under the License.
*/
var fs = require('fs-extra');
var path = require('path');
const fs = require('node:fs');
const path = require('node: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');
var xmlHelpers = require('cordova-common').xmlHelpers;
var CordovaError = require('cordova-common').CordovaError;
var ConfigParser = require('cordova-common').ConfigParser;
var FileUpdater = require('cordova-common').FileUpdater;
var PlatformJson = require('cordova-common').PlatformJson;
var PlatformMunger = require('cordova-common').ConfigChanges.PlatformMunger;
var PluginInfoProvider = require('cordova-common').PluginInfoProvider;
const dedent = require('dedent');
const events = require('cordova-common').events;
const AndroidManifest = require('./AndroidManifest');
const xmlHelpers = require('cordova-common').xmlHelpers;
const CordovaError = require('cordova-common').CordovaError;
const ConfigParser = require('cordova-common').ConfigParser;
const FileUpdater = require('cordova-common').FileUpdater;
const PlatformJson = require('cordova-common').PlatformJson;
const PlatformMunger = require('cordova-common').ConfigChanges.PlatformMunger;
const PluginInfoProvider = require('cordova-common').PluginInfoProvider;
const utils = require('./utils');
const gradleConfigDefaults = require('./gradle-config-defaults');
const checkReqs = require('./check_reqs');
const GradlePropertiesParser = require('./config/GradlePropertiesParser');
const CordovaGradleConfigParserFactory = require('./config/CordovaGradleConfigParserFactory');
function parseArguments (argv) {
return nopt({
@@ -44,15 +45,15 @@ function parseArguments (argv) {
}
module.exports.prepare = function (cordovaProject, options) {
var self = this;
const self = this;
let args = {};
if (options && options.options) {
args = parseArguments(options.options.argv);
}
var platformJson = PlatformJson.load(this.locations.root, this.platform);
var munger = new PlatformMunger(this.platform, this.locations.root, platformJson, new PluginInfoProvider());
const platformJson = PlatformJson.load(this.locations.root, this.platform);
const munger = new PlatformMunger(this.platform, this.locations.root, platformJson, new PluginInfoProvider());
this._config = updateConfigFilesFrom(cordovaProject.projectConfig, munger, this.locations);
@@ -63,16 +64,15 @@ module.exports.prepare = function (cordovaProject, options) {
updateUserProjectGradlePropertiesConfig(this, args);
// Update own www dir with project's www assets and plugins' assets and js-files
return Promise.resolve(updateWww(cordovaProject, this.locations)).then(function () {
// update project according to config.xml changes.
return updateProjectAccordingTo(self._config, self.locations);
}).then(function () {
updateIcons(cordovaProject, path.relative(cordovaProject.root, self.locations.res));
updateSplashes(cordovaProject, path.relative(cordovaProject.root, self.locations.res));
updateFileResources(cordovaProject, path.relative(cordovaProject.root, self.locations.root));
}).then(function () {
events.emit('verbose', 'Prepared android project successfully');
});
return Promise.resolve(updateWww(cordovaProject, this.locations))
.then(() => warnForDeprecatedSplashScreen(cordovaProject))
.then(() => updateProjectAccordingTo(self._config, self.locations))
.then(function () {
updateIcons(cordovaProject, path.relative(cordovaProject.root, self.locations.res));
updateFileResources(cordovaProject, path.relative(cordovaProject.root, self.locations.root));
}).then(function () {
events.emit('verbose', 'Prepared android project successfully');
});
};
/** @param {PlatformApi} project */
@@ -83,9 +83,17 @@ function updateUserProjectGradleConfig (project) {
...getUserGradleConfig(project._config)
};
// Check if compile sdk is valid.
// The returned result is iggnored and since we do not need and will not throw an error.
// Only using the valid check call for display the warning when target is greater then compiled.
checkReqs.isCompileSdkValid(
projectGradleConfig.COMPILE_SDK_VERSION,
projectGradleConfig.SDK_VERSION
);
// Write out changes
const projectGradleConfigPath = path.join(project.root, 'cdv-gradle-config.json');
fs.writeJSONSync(projectGradleConfigPath, projectGradleConfig, { spaces: 2 });
fs.writeFileSync(projectGradleConfigPath, JSON.stringify(projectGradleConfig, null, 2), 'utf-8');
}
function getUserGradleConfig (configXml) {
@@ -93,6 +101,7 @@ function getUserGradleConfig (configXml) {
{ xmlKey: 'android-minSdkVersion', gradleKey: 'MIN_SDK_VERSION', type: Number },
{ xmlKey: 'android-maxSdkVersion', gradleKey: 'MAX_SDK_VERSION', type: Number },
{ xmlKey: 'android-targetSdkVersion', gradleKey: 'SDK_VERSION', type: Number },
{ xmlKey: 'android-compileSdkVersion', gradleKey: 'COMPILE_SDK_VERSION', type: Number },
{ xmlKey: 'android-buildToolsVersion', gradleKey: 'BUILD_TOOLS_VERSION', type: String },
{ xmlKey: 'GradleVersion', gradleKey: 'GRADLE_VERSION', type: String },
{ xmlKey: 'AndroidGradlePluginVersion', gradleKey: 'AGP_VERSION', type: String },
@@ -101,7 +110,11 @@ function getUserGradleConfig (configXml) {
{ xmlKey: 'AndroidXWebKitVersion', gradleKey: 'ANDROIDX_WEBKIT_VERSION', type: String },
{ xmlKey: 'GradlePluginGoogleServicesVersion', gradleKey: 'GRADLE_PLUGIN_GOOGLE_SERVICES_VERSION', type: String },
{ xmlKey: 'GradlePluginGoogleServicesEnabled', gradleKey: 'IS_GRADLE_PLUGIN_GOOGLE_SERVICES_ENABLED', type: Boolean },
{ xmlKey: 'GradlePluginKotlinEnabled', gradleKey: 'IS_GRADLE_PLUGIN_KOTLIN_ENABLED', type: Boolean }
{ xmlKey: 'GradlePluginKotlinEnabled', gradleKey: 'IS_GRADLE_PLUGIN_KOTLIN_ENABLED', type: Boolean },
{ xmlKey: 'AndroidJavaSourceCompatibility', gradleKey: 'JAVA_SOURCE_COMPATIBILITY', type: Number },
{ xmlKey: 'AndroidJavaTargetCompatibility', gradleKey: 'JAVA_TARGET_COMPATIBILITY', type: Number },
{ xmlKey: 'AndroidKotlinJVMTarget', gradleKey: 'KOTLIN_JVM_TARGET', type: String },
{ xmlKey: 'AndroidShowDeprecations', gradleKey: 'JAVA_SHOW_DEPRECATIONS', type: Boolean }
];
return configXmlToGradleMapping.reduce((config, mapping) => {
@@ -151,19 +164,18 @@ module.exports.clean = function (options) {
// been called from the platform shell script rather than the CLI. Check for the
// noPrepare option passed in by the non-CLI clean script. If that's present, or if
// there's no config.xml found at the project root, then don't clean prepared files.
var projectRoot = path.resolve(this.root, '../..');
const projectRoot = path.resolve(this.root, '../..');
if ((options && options.noPrepare) || !fs.existsSync(this.locations.configXml) ||
!fs.existsSync(this.locations.configXml)) {
return Promise.resolve();
}
var projectConfig = new ConfigParser(this.locations.configXml);
const projectConfig = new ConfigParser(this.locations.configXml);
var self = this;
const self = this;
return Promise.resolve().then(function () {
cleanWww(projectRoot, self.locations);
cleanIcons(projectRoot, projectConfig, path.relative(projectRoot, self.locations.res));
cleanSplashes(projectRoot, projectConfig, path.relative(projectRoot, self.locations.res));
cleanFileResources(projectRoot, projectConfig, path.relative(projectRoot, self.locations.root));
});
};
@@ -187,7 +199,7 @@ function updateConfigFilesFrom (sourceConfig, configMunger, locations) {
// First cleanup current config and merge project's one into own
// Overwrite platform config.xml with defaults.xml.
fs.copySync(locations.defaultConfigXml, locations.configXml);
fs.cpSync(locations.defaultConfigXml, locations.configXml);
// Then apply config changes from global munge to all config files
// in project (including project's config)
@@ -195,7 +207,7 @@ function updateConfigFilesFrom (sourceConfig, configMunger, locations) {
events.emit('verbose', 'Merging project\'s config.xml into platform-specific android config.xml');
// Merge changes from app's config.xml into platform's one
var config = new ConfigParser(locations.configXml);
const config = new ConfigParser(locations.configXml);
xmlHelpers.mergeXml(sourceConfig.doc.getroot(),
config.doc.getroot(), 'android', /* clobber= */true);
@@ -220,19 +232,19 @@ function logFileOp (message) {
* paths for www files.
*/
function updateWww (cordovaProject, destinations) {
var sourceDirs = [
const sourceDirs = [
path.relative(cordovaProject.root, cordovaProject.locations.www),
path.relative(cordovaProject.root, destinations.platformWww)
];
// If project contains 'merges' for our platform, use them as another overrides
var merges_path = path.join(cordovaProject.root, 'merges', 'android');
const merges_path = path.join(cordovaProject.root, 'merges', 'android');
if (fs.existsSync(merges_path)) {
events.emit('verbose', 'Found "merges/android" folder. Copying its contents into the android project.');
sourceDirs.push(path.join('merges', 'android'));
}
var targetDir = path.relative(cordovaProject.root, destinations.www);
const targetDir = path.relative(cordovaProject.root, destinations.www);
events.emit(
'verbose', 'Merging and updating files from [' + sourceDirs.join(', ') + '] to ' + targetDir);
FileUpdater.mergeAndUpdateDir(
@@ -243,7 +255,7 @@ function updateWww (cordovaProject, destinations) {
* Cleans all files from the platform 'www' directory.
*/
function cleanWww (projectRoot, locations) {
var targetDir = path.relative(projectRoot, locations.www);
const targetDir = path.relative(projectRoot, locations.www);
events.emit('verbose', 'Cleaning ' + targetDir);
// No source paths are specified, so mergeAndUpdateDir() will clear the target directory.
@@ -259,37 +271,35 @@ function cleanWww (projectRoot, locations) {
* @param {Object} locations A map of locations for this platform
*/
function updateProjectAccordingTo (platformConfig, locations) {
// Update app name by editing res/values/strings.xml
var strings = xmlHelpers.parseElementtreeSync(locations.strings);
updateProjectStrings(platformConfig, locations);
updateProjectTheme(platformConfig, locations);
var name = platformConfig.name();
strings.find('string[@name="app_name"]').text = name.replace(/'/g, '\\\'');
const name = platformConfig.name();
var shortName = platformConfig.shortName && platformConfig.shortName();
if (shortName && shortName !== name) {
strings.find('string[@name="launcher_name"]').text = shortName.replace(/'/g, '\\\'');
}
fs.writeFileSync(locations.strings, strings.write({ indent: 4 }), 'utf-8');
events.emit('verbose', 'Wrote out android application name "' + name + '" to ' + locations.strings);
// Update app name for gradle project
fs.writeFileSync(path.join(locations.root, 'cdv-gradle-name.gradle'),
'// GENERATED FILE - DO NOT EDIT\n' +
'rootProject.name = "' + name.replace(/[/\\:<>"?*|]/g, '_') + '"\n');
// Java packages cannot support dashes
var androidPkgName = (platformConfig.android_packageName() || platformConfig.packageName()).replace(/-/g, '_');
const androidPkgName = (platformConfig.android_packageName() || platformConfig.packageName()).replace(/-/g, '_');
var manifest = new AndroidManifest(locations.manifest);
var manifestId = manifest.getPackageId();
// updating cdv-gradle-config with new androidPkgName.
const cdvGradleConfig = CordovaGradleConfigParserFactory.create(locations.root);
cdvGradleConfig.setPackageName(androidPkgName)
.write();
const manifest = new AndroidManifest(locations.manifest);
manifest.getActivity()
.setOrientation(platformConfig.getPreference('orientation'))
.setLaunchMode(findAndroidLaunchModePreference(platformConfig));
manifest.setVersionName(platformConfig.version())
.setVersionCode(platformConfig.android_versionCode() || default_versionCode(platformConfig.version()))
.setPackageId(androidPkgName)
.write();
// Java file paths shouldn't be hard coded
const javaDirectory = path.join(locations.javaSrc, manifestId.replace(/\./g, '/'));
const javaDirectory = path.join(locations.javaSrc, androidPkgName.replace(/\./g, '/'));
const java_files = glob.sync('**/*.java', { cwd: javaDirectory, absolute: true }).filter(f => {
const contents = fs.readFileSync(f, 'utf-8');
return /extends\s+CordovaActivity/.test(contents);
@@ -301,27 +311,23 @@ function updateProjectAccordingTo (platformConfig, locations) {
events.emit('log', 'Multiple candidate Java files that extend CordovaActivity found. Guessing at the first one, ' + java_files[0]);
}
const destFile = java_files[0];
const destFile = path.normalize(java_files[0]);
// var destFile = path.join(locations.root, 'app', 'src', 'main', 'java', androidPkgName.replace(/\./g, '/'), path.basename(java_files[0]));
// fs.ensureDirSync(path.dirname(destFile));
// events.emit('verbose', java_files[0]);
// events.emit('verbose', destFile);
// console.log(locations);
// fs.copySync(java_files[0], destFile);
utils.replaceFileContents(destFile, /package [\w.]*;/, 'package ' + androidPkgName + ';');
events.emit('verbose', 'Wrote out Android package name "' + androidPkgName + '" to ' + destFile);
var removeOrigPkg = checkReqs.isWindows() || checkReqs.isDarwin()
? manifestId.toUpperCase() !== androidPkgName.toUpperCase()
: manifestId !== androidPkgName;
if (removeOrigPkg) {
// if package name has changed, path to MainActivity.java has to track it
const newDestFile = path.join(locations.root, 'app', 'src', 'main', 'java', androidPkgName.replace(/\./g, '/'), path.basename(destFile));
if (newDestFile.toLowerCase() !== destFile.toLowerCase()) {
// If package was name changed we need to create new java with main activity in path matching new package name
fs.mkdirSync(path.dirname(newDestFile), { recursive: true });
events.emit('verbose', `copy ${destFile} to ${newDestFile}`);
fs.cpSync(destFile, newDestFile);
utils.replaceFileContents(newDestFile, /package [\w.]*;/, 'package ' + androidPkgName + ';');
events.emit('verbose', 'Wrote out Android package name "' + androidPkgName + '" to ' + newDestFile);
// If package was name changed we need to remove old java with main activity
fs.removeSync(java_files[0]);
events.emit('verbose', `remove ${destFile}`);
fs.rmSync(destFile);
// remove any empty directories
var currentDir = path.dirname(java_files[0]);
var sourcesRoot = path.resolve(locations.root, 'src');
let currentDir = path.dirname(destFile);
const sourcesRoot = path.resolve(locations.root, 'src');
while (currentDir !== sourcesRoot) {
if (fs.existsSync(currentDir) && fs.readdirSync(currentDir).length === 0) {
fs.rmdirSync(currentDir);
@@ -333,12 +339,283 @@ function updateProjectAccordingTo (platformConfig, locations) {
}
}
/**
* Updates project structure and AndroidManifest according to project's configuration.
*
* @param {ConfigParser} platformConfig A project's configuration that will
* be used to update project
* @param {Object} locations A map of locations for this platform
*/
function updateProjectStrings (platformConfig, locations) {
// Update app name by editing res/values/cdv_strings.xml
const strings = xmlHelpers.parseElementtreeSync(locations.strings);
const name = platformConfig.name();
strings.find('string[@name="app_name"]').text = name.replace(/'/g, '\\\'');
const shortName = platformConfig.shortName && platformConfig.shortName();
if (shortName && shortName !== name) {
strings.find('string[@name="launcher_name"]').text = shortName.replace(/'/g, '\\\'');
}
fs.writeFileSync(locations.strings, strings.write({ indent: 4 }), 'utf-8');
events.emit('verbose', 'Wrote out android application name "' + name + '" to ' + locations.strings);
}
function warnForDeprecatedSplashScreen (cordovaProject) {
const hasOldSplashTags = (
cordovaProject.projectConfig.doc.findall('./platform[@name="android"]/splash') || []
).length > 0;
if (hasOldSplashTags) {
events.emit('warn', 'The "<splash>" tags were detected and are no longer supported. Please migrate to the "preference" tag "AndroidWindowSplashScreenAnimatedIcon".');
}
}
/**
* @param {ConfigParser} platformConfig A project's configuration that will
* be used to update project
* @param {Object} locations A map of locations for this platform
*/
function updateProjectTheme (platformConfig, locations) {
// res/values/cdv_themes.xml
const themes = xmlHelpers.parseElementtreeSync(locations.themes);
const splashScreenTheme = themes.find('style[@name="Theme.App.SplashScreen"]');
let splashBg = platformConfig.getPreference('AndroidWindowSplashScreenBackground', this.platform);
if (!splashBg) {
splashBg = platformConfig.getPreference('SplashScreenBackgroundColor', this.platform);
}
if (!splashBg) {
splashBg = platformConfig.getPreference('BackgroundColor', this.platform);
}
if (!splashBg) {
splashBg = '@color/cdv_splashscreen_background';
}
events.emit('verbose', 'The Android Splash Screen background color was set to: ' +
(splashBg === '@color/cdv_splashscreen_background' ? 'Default' : splashBg)
);
// force the themes value to `@color/cdv_splashscreen_background`
const splashBgNode = splashScreenTheme.find('item[@name="windowSplashScreenBackground"]');
splashBgNode.text = splashBg;
[
// Splash Screen
'windowSplashScreenAnimatedIcon',
'windowSplashScreenAnimationDuration',
'android:windowSplashScreenBrandingImage',
'windowSplashScreenIconBackgroundColor',
'postSplashScreenTheme'
].forEach(themeKey => {
const index = themeKey.indexOf(':') + 1;
const cdvConfigPrefKey = 'Android' + themeKey.charAt(index).toUpperCase() + themeKey.slice(index + 1);
const cdvConfigPrefValue = platformConfig.getPreference(cdvConfigPrefKey, this.platform);
let themeTargetNode = splashScreenTheme.find(`item[@name="${themeKey}"]`);
switch (themeKey) {
case 'windowSplashScreenAnimatedIcon':
// handle here the cases of "png" vs "xml" (drawable)
// If "png":
// - Clear out default or previous set "drawable/ic_cdv_splashscreen.xml" if exisiting.
// - Copy png in correct mipmap dir with name "ic_cdv_splashscreen.png"
// If "xml":
// - Clear out "{mipmap}/ic_cdv_splashscreen.png" if exisiting.
// - Copy xml into drawable dir with name "ic_cdv_splashscreen.xml"
// updateProjectSplashScreenIcon()
// value should change depending on case:
// If "png": "@mipmap/ic_cdv_splashscreen"
// If "xml": "@drawable/ic_cdv_splashscreen"
updateProjectSplashScreenImage(locations, themeKey, cdvConfigPrefKey, cdvConfigPrefValue);
break;
case 'android:windowSplashScreenBrandingImage':
// display warning only when set.
if (cdvConfigPrefValue) {
events.emit('warn', `"${themeKey}" is currently not supported by the splash screen compatibility library. https://issuetracker.google.com/issues/194301890`);
}
updateProjectSplashScreenImage(locations, themeKey, cdvConfigPrefKey, cdvConfigPrefValue);
// force the themes value to `@color/cdv_splashscreen_icon_background`
if (!cdvConfigPrefValue && themeTargetNode) {
splashScreenTheme.remove(themeTargetNode);
delete themes.getroot().attrib['xmlns:tools'];
} else if (cdvConfigPrefValue) {
// if there is no current node, create a new node.
if (!themeTargetNode) {
themeTargetNode = themes.getroot().makeelement('item', { name: themeKey, 'tools:targetApi': '31' });
splashScreenTheme.append(themeTargetNode);
themes.getroot().attrib['xmlns:tools'] = 'http://schemas.android.com/tools';
}
// set the user defined color.
themeTargetNode.text = '@drawable/ic_cdv_splashscreen_branding';
}
break;
case 'windowSplashScreenIconBackgroundColor':
// use the user defined value for "cdv_colors.xml"
updateProjectSplashScreenIconBackgroundColor(cdvConfigPrefValue, locations);
// force the themes value to `@color/cdv_splashscreen_icon_background`
if (!cdvConfigPrefValue && themeTargetNode) {
// currentItem.remove();
splashScreenTheme.remove(themeTargetNode);
} else if (cdvConfigPrefValue) {
// if there is no current color, create a new node.
if (!themeTargetNode) {
themeTargetNode = themes.getroot().makeelement('item', { name: themeKey });
splashScreenTheme.append(themeTargetNode);
}
// set the user defined color.
themeTargetNode.text = '@color/cdv_splashscreen_icon_background';
}
break;
case 'windowSplashScreenAnimationDuration':
themeTargetNode.text = cdvConfigPrefValue || '200';
break;
case 'postSplashScreenTheme':
themeTargetNode.text = cdvConfigPrefValue || '@style/Theme.Cordova.App.DayNight';
break;
default:
events.emit('warn', `The theme property "${themeKey}" does not exist`);
}
});
fs.writeFileSync(locations.themes, themes.write({ indent: 4 }), 'utf-8');
events.emit('verbose', 'Wrote out Android application themes to ' + locations.themes);
}
/**
* @param {String} splashIconBackgroundColor SplashScreen Icon Background Color Hex Code
* be used to update project
* @param {Object} locations A map of locations for this platform
*/
function updateProjectSplashScreenIconBackgroundColor (splashIconBackgroundColor, locations) {
// res/values/cdv_colors.xml
const colors = xmlHelpers.parseElementtreeSync(locations.colors);
// node name
const name = 'cdv_splashscreen_icon_background';
// get the current defined color
let currentColor = colors.find(`color[@name="${name}"]`);
if (!splashIconBackgroundColor && currentColor) {
colors.getroot().remove(currentColor);
} else if (splashIconBackgroundColor) {
// if there is no current color, create a new node.
if (!currentColor) {
currentColor = colors.getroot().makeelement('color', { name });
colors.getroot().append(currentColor);
}
// set the user defined color.
currentColor.text = splashIconBackgroundColor.replace(/'/g, '\\\'');
}
// write out the changes.
fs.writeFileSync(locations.colors, colors.write({ indent: 4 }), 'utf-8');
events.emit('verbose', 'Wrote out Android application SplashScreen Icon Color to ' + locations.colors);
}
function cleanupAndSetProjectSplashScreenImage (srcFile, destFilePath, possiblePreviousDestFilePath, cleanupOnly = false) {
if (fs.existsSync(possiblePreviousDestFilePath)) {
fs.rmSync(possiblePreviousDestFilePath);
}
if (cleanupOnly && fs.existsSync(destFilePath)) {
// Also remove dest file path for cleanup even if previous was not use.
fs.rmSync(destFilePath);
}
if (!cleanupOnly && srcFile && fs.existsSync(srcFile)) {
fs.cpSync(srcFile, destFilePath);
}
}
function updateProjectSplashScreenImage (locations, themeKey, cdvConfigPrefKey, cdvConfigPrefValue = '') {
const SPLASH_SCREEN_IMAGE_BY_THEME_KEY = {
windowSplashScreenAnimatedIcon: 'ic_cdv_splashscreen',
'android:windowSplashScreenBrandingImage': 'ic_cdv_splashscreen_branding'
};
const destFileName = SPLASH_SCREEN_IMAGE_BY_THEME_KEY[themeKey] || null;
if (!destFileName) throw new CordovaError(`${themeKey} is not valid for image detection.`);
// Default paths of where images are saved
const destPngDir = path.join(locations.res, 'drawable-nodpi');
const destXmlDir = path.join(locations.res, 'drawable');
// Dest File Name and Path
const destFileNameExt = destFileName + '.xml';
let destFilePath = path.join(destXmlDir, destFileNameExt);
let possiblePreviousDestFilePath = path.join(destPngDir, destFileName + '.png');
// Default Drawable Source File
let defaultSrcFilePath = null;
if (themeKey !== 'android:windowSplashScreenBrandingImage') {
try {
// coming from user project
defaultSrcFilePath = require.resolve('cordova-android/templates/project/res/drawable/' + destFileNameExt);
} catch (e) {
// coming from repo test & coho
defaultSrcFilePath = require.resolve('../templates/project/res/drawable/' + destFileNameExt);
}
}
if (!cdvConfigPrefValue || !fs.existsSync(cdvConfigPrefValue)) {
let emitType = 'verbose';
let emmitMessage = `The "${cdvConfigPrefKey}" is undefined. Cordova's default will be used.`;
if (cdvConfigPrefValue && !fs.existsSync(cdvConfigPrefValue)) {
emitType = 'warn';
emmitMessage = `The "${cdvConfigPrefKey}" value does not exist. Cordova's default will be used.`;
}
events.emit(emitType, emmitMessage);
const cleanupOnly = themeKey === 'android:windowSplashScreenBrandingImage';
cleanupAndSetProjectSplashScreenImage(defaultSrcFilePath, destFilePath, possiblePreviousDestFilePath, cleanupOnly);
return;
}
const iconExtension = path.extname(cdvConfigPrefValue).toLowerCase();
if (iconExtension === '.png') {
// Put the image at this location.
destFilePath = path.join(destPngDir, destFileName + '.png');
// Check for this file and remove.
possiblePreviousDestFilePath = path.join(destXmlDir, destFileName + '.xml');
// copy the png to correct mipmap folder with name of ic_cdv_splashscreen.png
// delete ic_cdv_splashscreen.xml from drawable folder
// update cdv_themes.xml windowSplashScreenAnimatedIcon value to @mipmap/ic_cdv_splashscreen
cleanupAndSetProjectSplashScreenImage(cdvConfigPrefValue, destFilePath, possiblePreviousDestFilePath);
} else if (iconExtension === '.xml') {
// copy the xml to drawable folder with name of ic_cdv_splashscreen.xml
// delete ic_cdv_splashscreen.png from mipmap folder
// update cdv_themes.xml windowSplashScreenAnimatedIcon value to @drawable/ic_cdv_splashscreen
cleanupAndSetProjectSplashScreenImage(cdvConfigPrefValue, destFilePath, possiblePreviousDestFilePath);
} else {
// use the default destFilePath & possiblePreviousDestFilePath, no update require.
events.emit('warn', `The "${cdvConfigPrefKey}" had an unsupported extension. Cordova's default will be used.`);
cleanupAndSetProjectSplashScreenImage(defaultSrcFilePath, destFilePath, possiblePreviousDestFilePath);
}
}
// Consturct the default value for versionCode as
// PATCH + MINOR * 100 + MAJOR * 10000
// see http://developer.android.com/tools/publishing/versioning.html
function default_versionCode (version) {
var nums = version.split('-')[0].split('.');
var versionCode = 0;
const nums = version.split('-')[0].split('.');
let versionCode = 0;
if (+nums[0]) {
versionCode += +nums[0] * 10000;
}
@@ -356,7 +633,8 @@ function default_versionCode (version) {
function getImageResourcePath (resourcesDir, type, density, name, sourceName) {
// Use same extension as source with special case for 9-Patch files
const ext = sourceName.endsWith('.9.png')
? '.9.png' : path.extname(sourceName).toLowerCase();
? '.9.png'
: path.extname(sourceName).toLowerCase();
const subDir = density ? `${type}-${density}` : type;
return path.join(resourcesDir, subDir, name + ext);
@@ -366,72 +644,10 @@ function getAdaptiveImageResourcePath (resourcesDir, type, density, name, source
if (/\.9\.png$/.test(sourceName)) {
name = name.replace(/\.png$/, '.9.png');
}
var resourcePath = path.join(resourcesDir, (density ? type + '-' + density + '-v26' : type), name);
const resourcePath = path.join(resourcesDir, (density ? type + '-' + density + '-v26' : type), name);
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');
// if there are no "splash" elements in config.xml
if (resources.length === 0) {
events.emit('verbose', 'This app does not have splash screens defined');
// We must not return here!
// If the user defines no splash screens, the cleanup map will cause any
// existing splash screen images (e.g. the defaults that we copy into a
// new app) to be removed from the app folder, which is what we want.
}
// Build an initial resource map that deletes all existing splash screens
const resourceMap = makeSplashCleanupMap(cordovaProject.root, platformResourcesDir);
var hadMdpi = false;
resources.forEach(function (resource) {
if (!resource.density) {
return;
}
if (resource.density === 'mdpi') {
hadMdpi = true;
}
var targetPath = getImageResourcePath(
platformResourcesDir, 'drawable', resource.density, 'screen', path.basename(resource.src));
resourceMap[targetPath] = resource.src;
});
// There's no "default" drawable, so assume default == mdpi.
if (!hadMdpi && resources.defaultResource) {
var targetPath = getImageResourcePath(
platformResourcesDir, 'drawable', 'mdpi', 'screen', path.basename(resources.defaultResource.src));
resourceMap[targetPath] = resources.defaultResource.src;
}
events.emit('verbose', 'Updating splash screens at ' + platformResourcesDir);
FileUpdater.updatePaths(
resourceMap, { rootDir: cordovaProject.root }, logFileOp);
}
function cleanSplashes (projectRoot, projectConfig, platformResourcesDir) {
var resources = projectConfig.getSplashScreens('android');
if (resources.length > 0) {
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.
FileUpdater.updatePaths(
resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
}
}
function updateIcons (cordovaProject, platformResourcesDir) {
const icons = cordovaProject.projectConfig.getIcons('android');
@@ -490,8 +706,10 @@ function updateIcons (cordovaProject, platformResourcesDir) {
mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher.png'),
mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher_foreground.png'),
mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher_background.png'),
mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher_monochrome.png'),
mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher_foreground.xml'),
mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher_background.xml'),
mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher_monochrome.xml'),
mapImageResources(cordovaProject.root, platformResourcesDir, 'mipmap', 'ic_launcher.xml')
);
@@ -511,21 +729,42 @@ function updateIconResourceForAdaptive (preparedIcons, resourceMap, platformReso
const android_icons = preparedIcons.android_icons;
const default_icon = preparedIcons.default_icon;
// The source paths for icons and splashes are relative to
// The source paths for icons are relative to
// project's config.xml location, so we use it as base path.
let background;
let foreground;
let monochrome;
let targetPathBackground;
let targetPathForeground;
let targetPathMonochrome;
for (const density in android_icons) {
let backgroundVal = '@mipmap/ic_launcher_background';
let foregroundVal = '@mipmap/ic_launcher_foreground';
const monochromeVal = '@mipmap/ic_launcher_monochrome';
background = android_icons[density].background;
foreground = android_icons[density].foreground;
monochrome = android_icons[density].monochrome;
if (!background || !foreground) {
const hasAdaptiveIcons = !!background && !!foreground;
let hasMonochromeIcon = !!monochrome;
if (hasMonochromeIcon && !hasAdaptiveIcons) {
// If we have a monochrome icon, but no adaptive icons,
// then warn that in order to use monochrome, the adaptive icons
// must be supplied. We will ignore monochrome and proceed with the
// icon preparation however.
hasMonochromeIcon = false;
monochrome = undefined;
events.emit('warn', dedent`
Monochrome icon found but without adaptive properties.
Monochrome icon requires the adaptive background and foreground assets.
See https://cordova.apache.org/docs/en/latest/config_ref/images.html fore more information.
`);
}
if (!hasAdaptiveIcons) {
// This icon isn't an adaptive icon, so skip it
continue;
}
@@ -556,12 +795,38 @@ function updateIconResourceForAdaptive (preparedIcons, resourceMap, platformReso
resourceMap[targetPathForeground] = android_icons[density].foreground;
}
if (hasMonochromeIcon) {
if (path.extname(path.basename(monochrome)) === '.xml') {
// Vector Use Case
targetPathMonochrome = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', density, 'ic_launcher_monochrome.xml', path.basename(android_icons[density].monochrome));
resourceMap[targetPathMonochrome] = android_icons[density].monochrome;
} else if (path.extname(path.basename(monochrome)) === '.png') {
// Images Use Case
targetPathMonochrome = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', density, 'ic_launcher_monochrome.png', path.basename(android_icons[density].monochrome));
resourceMap[targetPathMonochrome] = android_icons[density].monochrome;
}
}
// create an XML for DPI and set color
const icLauncherTemplate = `<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="` + backgroundVal + `" />
<foreground android:drawable="` + foregroundVal + `" />
</adaptive-icon>`;
let icLauncherTemplate = '';
if (hasMonochromeIcon) {
icLauncherTemplate = dedent`
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="${backgroundVal}" />
<foreground android:drawable="${foregroundVal}" />
<monochrome android:drawable="${monochromeVal}" />
</adaptive-icon>
`;
} else {
icLauncherTemplate = dedent`
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="${backgroundVal}" />
<foreground android:drawable="${foregroundVal}" />
</adaptive-icon>
`;
}
const launcherXmlPath = path.join(platformResourcesDir, 'mipmap-' + density + '-v26', 'ic_launcher.xml');
@@ -575,6 +840,7 @@ function updateIconResourceForAdaptive (preparedIcons, resourceMap, platformReso
if (default_icon && !android_icons.mdpi) {
let defaultTargetPathBackground;
let defaultTargetPathForeground;
let defaultTargetPathMonochrome;
if (background.startsWith('@color')) {
// Colors Use Case
@@ -601,6 +867,18 @@ function updateIconResourceForAdaptive (preparedIcons, resourceMap, platformReso
defaultTargetPathForeground = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher_foreground.png', path.basename(default_icon.foreground));
resourceMap[defaultTargetPathForeground] = default_icon.foreground;
}
if (monochrome) {
if (path.extname(path.basename(monochrome)) === '.xml') {
// Vector Use Case
defaultTargetPathMonochrome = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher_monochrome.xml', path.basename(default_icon.monochrome));
resourceMap[defaultTargetPathMonochrome] = default_icon.monochrome;
} else if (path.extname(path.basename(monochrome)) === '.png') {
// Images Use Case
defaultTargetPathMonochrome = getAdaptiveImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher_monochrome.png', path.basename(default_icon.monochrome));
resourceMap[defaultTargetPathMonochrome] = default_icon.monochrome;
}
}
}
return resourceMap;
@@ -610,16 +888,16 @@ function updateIconResourceForLegacy (preparedIcons, resourceMap, platformResour
const android_icons = preparedIcons.android_icons;
const default_icon = preparedIcons.default_icon;
// The source paths for icons and splashes are relative to
// The source paths for icons are relative to
// project's config.xml location, so we use it as base path.
for (var density in android_icons) {
var targetPath = getImageResourcePath(platformResourcesDir, 'mipmap', density, 'ic_launcher', path.basename(android_icons[density].src));
for (const density in android_icons) {
const 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', path.basename(default_icon.src));
const defaultTargetPath = getImageResourcePath(platformResourcesDir, 'mipmap', 'mdpi', 'ic_launcher', path.basename(default_icon.src));
resourceMap[defaultTargetPath] = default_icon.src;
}
@@ -642,14 +920,14 @@ function prepareIcons (icons) {
// find the best matching icon for a given density or size
// @output android_icons
var parseIcon = function (icon, icon_size) {
const parseIcon = function (icon, icon_size) {
// do I have a platform icon for that density already
var density = icon.density || SIZE_TO_DENSITY_MAP[icon_size];
const density = icon.density || SIZE_TO_DENSITY_MAP[icon_size];
if (!density) {
// invalid icon defition ( or unsupported size)
return;
}
var previous = android_icons[density];
const previous = android_icons[density];
if (previous && previous.platform) {
return;
}
@@ -657,9 +935,9 @@ function prepareIcons (icons) {
};
// iterate over all icon elements to find the default icon and call parseIcon
for (var i = 0; i < icons.length; i++) {
var icon = icons[i];
var size = icon.width;
for (let i = 0; i < icons.length; i++) {
const icon = icons[i];
let size = icon.width;
if (!size) {
size = icon.height;
@@ -671,6 +949,11 @@ function prepareIcons (icons) {
const favor = {};
// populating found icon.
if (icon.background && icon.foreground && icon.monochrome) {
found.background = icon.background;
found.foreground = icon.foreground;
found.monochrome = icon.monochrome;
}
if (icon.background && icon.foreground) {
found.background = icon.background;
found.foreground = icon.foreground;
@@ -679,6 +962,11 @@ function prepareIcons (icons) {
found.src = icon.src;
}
if (default_icon.background && default_icon.foreground && default_icon.monochrome) {
favor.background = default_icon.background;
favor.foreground = default_icon.foreground;
favor.monochrome = default_icon.monochrome;
}
if (default_icon.background && default_icon.foreground) {
favor.background = default_icon.background;
favor.foreground = default_icon.foreground;
@@ -697,13 +985,13 @@ function prepareIcons (icons) {
}
return {
android_icons: android_icons,
default_icon: default_icon
android_icons,
default_icon
};
}
function cleanIcons (projectRoot, projectConfig, platformResourcesDir) {
var icons = projectConfig.getIcons('android');
const icons = projectConfig.getIcons('android');
// Skip if there are no app defined icons in config.xml
if (icons.length === 0) {
@@ -716,8 +1004,10 @@ function cleanIcons (projectRoot, projectConfig, platformResourcesDir) {
mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher.png'),
mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher_foreground.png'),
mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher_background.png'),
mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher_monochrome.png'),
mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher_foreground.xml'),
mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher_background.xml'),
mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher_monochrome.xml'),
mapImageResources(projectRoot, platformResourcesDir, 'mipmap', 'ic_launcher.xml')
);
@@ -740,18 +1030,8 @@ function mapImageResources (rootDir, subDir, type, resourceName) {
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');
const files = cordovaProject.projectConfig.getFileResources('android');
// if there are resource-file elements in config.xml
if (files.length === 0) {
@@ -759,9 +1039,9 @@ function updateFileResources (cordovaProject, platformDir) {
return;
}
var resourceMap = {};
const resourceMap = {};
files.forEach(function (res) {
var targetPath = path.join(platformDir, res.target);
const targetPath = path.join(platformDir, res.target);
resourceMap[targetPath] = res.src;
});
@@ -771,13 +1051,13 @@ function updateFileResources (cordovaProject, platformDir) {
}
function cleanFileResources (projectRoot, projectConfig, platformDir) {
var files = projectConfig.getFileResources('android', true);
const files = projectConfig.getFileResources('android', true);
if (files.length > 0) {
events.emit('verbose', 'Cleaning resource files at ' + platformDir);
var resourceMap = {};
const resourceMap = {};
files.forEach(function (res) {
var filePath = path.join(platformDir, res.target);
const filePath = path.join(platformDir, res.target);
resourceMap[filePath] = null;
});
@@ -798,14 +1078,14 @@ function cleanFileResources (projectRoot, projectConfig, platformDir) {
* 'singleTop'
*/
function findAndroidLaunchModePreference (platformConfig) {
var launchMode = platformConfig.getPreference('AndroidLaunchMode');
const launchMode = platformConfig.getPreference('AndroidLaunchMode');
if (!launchMode) {
// Return a default value
return 'singleTop';
}
var expectedValues = ['standard', 'singleTop', 'singleTask', 'singleInstance'];
var valid = expectedValues.indexOf(launchMode) >= 0;
const expectedValues = ['standard', 'singleTop', 'singleTask', 'singleInstance'];
const valid = expectedValues.indexOf(launchMode) >= 0;
if (!valid) {
// Note: warn, but leave the launch mode as developer wanted, in case the list of options changes in the future
events.emit('warn', 'Unrecognized value for AndroidLaunchMode preference: ' +

View File

@@ -19,7 +19,7 @@
'use strict';
var events = require('cordova-common').events;
const events = require('cordova-common').events;
/**
* Retry a promise-returning function a number of times, propagating its

View File

@@ -17,12 +17,13 @@
under the License.
*/
var emulator = require('./emulator');
const emulator = require('./emulator');
const target = require('./target');
const build = require('./build');
const PackageType = require('./PackageType');
const AndroidManifest = require('./AndroidManifest');
const { CordovaError, events } = require('cordova-common');
const CordovaGradleConfigParserFactory = require('./config/CordovaGradleConfigParserFactory');
/**
* Builds a target spec from a runOptions object
@@ -78,6 +79,53 @@ module.exports.run = async function (runOptions = {}) {
}
const manifest = new AndroidManifest(this.locations.manifest);
const cordovaGradleConfigParser = CordovaGradleConfigParserFactory.create(this.locations.root);
return target.install(resolvedTarget, { manifest, buildResults });
return target.install(resolvedTarget, { manifest, buildResults, cordovaGradleConfigParser });
};
module.exports.listDevices = async function () {
events.emit('log', `\nAvailable ${this.platform} devices:`);
const { list } = require('./target');
await 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);
});
};
module.exports.listEmulators = async function () {
events.emit('log', `\nAvailable ${this.platform} virtual devices:`);
const emulators = require('./emulator');
await emulators.list_images().then(function (emulator_list) {
emulator_list && emulator_list.forEach(function (emu) {
console.log(emu.name);
});
}, function (err) {
console.error('ERROR: ' + err);
process.exit(2);
});
};
module.exports.runListDevices = async function (options = {}) {
const { options: cliArgs = {} } = options;
if (cliArgs?.device) {
await module.exports.listDevices.call(this);
} else if (cliArgs?.emulator) {
await module.exports.listEmulators.call(this);
} else {
await module.exports.listDevices.call(this);
await module.exports.listEmulators.call(this);
}
return true;
};

View File

@@ -17,7 +17,7 @@
under the License.
*/
const { inspect } = require('util');
const { inspect } = require('node:util');
const execa = require('execa');
const Adb = require('./Adb');
const build = require('./build');
@@ -127,9 +127,9 @@ exports.resolve = async (spec, buildResults) => {
};
};
exports.install = async function ({ id: target, arch, type }, { manifest, buildResults }) {
exports.install = async function ({ id: target, arch, type }, { manifest, buildResults, cordovaGradleConfigParser }) {
const apk_path = build.findBestApkForArchitecture(buildResults, arch);
const pkgName = manifest.getPackageId();
const pkgName = cordovaGradleConfigParser.getPackageName();
const launchName = pkgName + '/.' + manifest.getActivity().getName();
events.emit('log', 'Using apk: ' + apk_path);

View File

@@ -23,9 +23,9 @@
// TODO: Perhaps this should live in cordova-common?
const fs = require('fs-extra');
const fs = require('node:fs');
const which = require('which');
const os = require('os');
const os = require('node:os');
/**
* Reads, searches, and replaces the found occurences with replacementString and then writes the file back out.
@@ -66,3 +66,21 @@ exports.forgivingWhichSync = (cmd) => {
exports.isWindows = () => os.platform() === 'win32';
exports.isDarwin = () => os.platform() === 'darwin';
const UNESCAPED_REGEX = /[&<>"']/g;
const escapes = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
};
/**
* Converts the characters "&", "<", ">", '"' and "'" in the given string to
* their corresponding escaped value
* @param {string} str the string to be escaped
* @returns the escaped string
*/
exports.escape = (str) => UNESCAPED_REGEX.test(str) ? str.replace(UNESCAPED_REGEX, (key) => escapes[key]) : str;

19
licence_checker.yml Normal file
View File

@@ -0,0 +1,19 @@
# 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.
# Empty for the release audit workflow.
# The `license-config` is required even if there are no custom configs

7169
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,8 @@
{
"name": "cordova-android",
"version": "10.0.1",
"version": "15.0.0-dev",
"description": "cordova-android release",
"types": "./types/index.d.ts",
"main": "lib/Api.js",
"repository": "github:apache/cordova-android",
"bugs": "https://github.com/apache/cordova-android/issues",
@@ -13,40 +14,43 @@
"scripts": {
"prepare": "cordova-js build > templates/project/assets/www/cordova.js",
"test": "npm run lint && npm run cover && npm run java-unit-tests",
"lint": "eslint lib spec test \"templates/cordova/**/!(*.*)\"",
"lint": "eslint .",
"lint:fix": "npm run lint -- --fix",
"unit-tests": "jasmine --config=spec/unit/jasmine.json",
"cover": "nyc jasmine --config=spec/coverage.json",
"cover": "c8 jasmine --config=spec/coverage.json",
"e2e-tests": "jasmine --config=spec/e2e/jasmine.json",
"java-unit-tests": "node test/run_java_unit_tests.js",
"clean:java-unit-tests": "node test/clean.js"
"java-unit-tests": "node test/run_java_unit_tests.js"
},
"author": "Apache Software Foundation",
"license": "Apache-2.0",
"dependencies": {
"android-versions": "^1.7.0",
"cordova-common": "^4.0.2",
"android-versions": "^2.1.0",
"cordova-common": "^6.0.0",
"dedent": "^1.7.0",
"execa": "^5.1.1",
"fast-glob": "^3.2.7",
"fs-extra": "^10.0.0",
"fast-glob": "^3.3.3",
"is-path-inside": "^3.0.3",
"nopt": "^5.0.0",
"properties-parser": "^0.3.1",
"semver": "^7.3.5",
"nopt": "^8.1.0",
"properties-parser": "^0.6.0",
"semver": "^7.7.1",
"string-argv": "^0.3.1",
"untildify": "^4.0.0",
"which": "^2.0.2"
"which": "^5.0.0"
},
"devDependencies": {
"@cordova/eslint-config": "^3.0.0",
"@cordova/eslint-config": "^6.0.0",
"c8": "^10.1.3",
"cordova-js": "^6.1.0",
"jasmine": "^3.8.0",
"elementtree": "^0.1.7",
"jasmine": "^5.10.0",
"jasmine-spec-reporter": "^7.0.0",
"nyc": "^15.1.0",
"rewire": "^5.0.0"
"rewire": "^9.0.1",
"tmp": "^0.2.5"
},
"engines": {
"node": ">=12.0.0"
"node": ">=20.9.0"
},
"nyc": {
"c8": {
"include": [
"lib"
],

View File

@@ -17,9 +17,9 @@
under the License.
*/
const os = require('os');
const fs = require('fs-extra');
const path = require('path');
const os = require('node:os');
const fs = require('node:fs');
const path = require('node:path');
const { EventEmitter } = require('events');
const { ConfigParser, PluginInfoProvider } = require('cordova-common');
const Api = require('../../lib/Api');
@@ -49,14 +49,15 @@ describe('E2E', function () {
api = await makeProject(projectPath);
});
afterEach(() => {
fs.removeSync(tmpDir);
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it('loads the API from a project directory', async () => {
// Allow test project to find the `cordova-android` module
fs.ensureSymlinkSync(
path.join(__dirname, '../..'),
path.join(tmpDir, 'node_modules/cordova-android'),
fs.mkdirSync(path.join(tmpDir, 'node_modules'), { recursive: true });
fs.symlinkSync(
path.join(__dirname, '..', '..'),
path.join(tmpDir, 'node_modules', 'cordova-android'),
'junction'
);

View File

@@ -3,7 +3,7 @@
package="com.example.anis.myapplication">
<application
android:allowBackup="true"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"

View File

@@ -1,6 +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.
#Mon Dec 28 10:00:20 PST 2015
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip

View File

@@ -0,0 +1,105 @@
/**
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
const fs = require('node:fs');
const path = require('node:path');
const AndroidCommandLineTools = require('../../lib/env/AndroidCommandLineTools');
describe('AndroidCommandLineTools', () => {
beforeAll(() => {
// For the purposes of these test, we will assume ANDROID_HOME is proper.
spyOn(fs, 'existsSync').and.returnValue(true);
});
describe('getAvailableVersions', () => {
describe('should return a list of command line versions', () => {
it('in descending order', () => {
spyOn(fs, 'readdirSync').and.returnValue([
'10.0',
'15.0',
'13'
]);
expect(AndroidCommandLineTools.getAvailableVersions()).toEqual([
'15.0',
'13',
'10.0'
]);
});
it('stable releases appear before prereleases', () => {
spyOn(fs, 'readdirSync').and.returnValue([
'15.0-rc01',
'15.0-alpha01',
'15.0'
]);
expect(AndroidCommandLineTools.getAvailableVersions()).toEqual([
'15.0',
'15.0-rc01',
'15.0-alpha01'
]);
});
it('"latest" should take all precedence', () => {
spyOn(fs, 'readdirSync').and.returnValue([
'15.0-rc01',
'15.0-alpha01',
'15.0',
'latest'
]);
expect(AndroidCommandLineTools.getAvailableVersions()).toEqual([
'latest',
'15.0',
'15.0-rc01',
'15.0-alpha01'
]);
});
it('invalid versions are ignored', () => {
spyOn(fs, 'readdirSync').and.returnValue([
'15.0-rc01',
'xyz',
'15.0'
]);
expect(AndroidCommandLineTools.getAvailableVersions()).toEqual([
'15.0',
'15.0-rc01'
]);
});
});
});
describe('getBinPath', () => {
beforeEach(() => {
spyOn(AndroidCommandLineTools, '__getAndroidHome').and.returnValue('/Android/Sdk');
});
it('should return the bin path of the latest version', () => {
spyOn(AndroidCommandLineTools, 'getAvailableVersions').and.returnValue([
'19.0',
'18.0'
]);
expect(AndroidCommandLineTools.getBinPath()).toBe(path.resolve('/Android/Sdk/cmdline-tools/19.0/bin'));
});
});
});

View File

@@ -17,9 +17,9 @@
under the License.
*/
const fs = require('fs');
const os = require('os');
const path = require('path');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const rewire = require('rewire');
describe('AndroidManifest', () => {
@@ -110,18 +110,6 @@ describe('AndroidManifest', () => {
});
});
describe('packageId', () => {
it('should get the package ID', () => {
expect(manifest.getPackageId()).toBe(PACKAGE_ID);
});
it('should set the package ID', () => {
const newPackageId = `${PACKAGE_ID}new`;
manifest.setPackageId(newPackageId);
expect(manifest.getPackageId()).toBe(newPackageId);
});
});
describe('activity', () => {
let activity;

View File

@@ -17,8 +17,10 @@
under the License.
*/
const path = require('path');
const path = require('node:path');
const rewire = require('rewire');
const MockCordovaGradleConfigParser = require('./mocks/config/MockCordovaGradleConfigParser');
const CordovaGradleConfigParserFactory = require('../../lib/config/CordovaGradleConfigParserFactory');
describe('AndroidProject', () => {
const PROJECT_DIR = 'platforms/android';
@@ -30,6 +32,8 @@ describe('AndroidProject', () => {
AndroidStudioSpy = jasmine.createSpyObj('AndroidStudio', ['isAndroidStudioProject']);
AndroidProject.__set__('AndroidStudio', AndroidStudioSpy);
spyOn(CordovaGradleConfigParserFactory, 'create').and.returnValue(new MockCordovaGradleConfigParser(PROJECT_DIR));
});
describe('constructor', () => {
@@ -87,26 +91,20 @@ describe('AndroidProject', () => {
});
describe('getPackageName', () => {
let AndroidManifestSpy;
let AndroidManifestFns;
let androidProject;
beforeEach(() => {
AndroidManifestFns = jasmine.createSpyObj('AndroidManifestFns', ['getPackageId']);
AndroidManifestSpy = jasmine.createSpy('AndroidManifest').and.returnValue(AndroidManifestFns);
AndroidProject.__set__('AndroidManifest', AndroidManifestSpy);
androidProject = new AndroidProject(PROJECT_DIR);
});
it('should get the package name AndroidManifest', () => {
it('should get the package name Cordova Gradle Config file', () => {
spyOn(MockCordovaGradleConfigParser.prototype, 'getPackageName');
androidProject.getPackageName();
expect(AndroidManifestSpy).toHaveBeenCalledWith(path.join(PROJECT_DIR, 'app/src/main/AndroidManifest.xml'));
expect(MockCordovaGradleConfigParser.prototype.getPackageName).toHaveBeenCalled();
});
it('should return the package name', () => {
const packageName = 'io.cordova.unittest';
AndroidManifestFns.getPackageId.and.returnValue(packageName);
expect(androidProject.getPackageName()).toBe(packageName);
});

View File

@@ -17,29 +17,31 @@
under the License.
*/
var os = require('os');
var path = require('path');
var common = require('cordova-common');
const os = require('node:os');
const path = require('node:path');
const common = require('cordova-common');
const EventEmitter = require('events');
var Api = require('../../lib/Api');
var AndroidProject = require('../../lib/AndroidProject');
const Api = require('../../lib/Api');
const AndroidProject = require('../../lib/AndroidProject');
const check_reqs = require('../../lib/check_reqs');
const run_mod = require('../../lib/run');
var PluginInfo = common.PluginInfo;
const PluginInfo = common.PluginInfo;
var FIXTURES = path.join(__dirname, '../e2e/fixtures');
var FAKE_PROJECT_DIR = path.join(os.tmpdir(), 'plugin-test-project');
const FIXTURES = path.join(__dirname, '../e2e/fixtures');
const FAKE_PROJECT_DIR = path.join(os.tmpdir(), 'plugin-test-project');
describe('Api', () => {
describe('addPlugin method', function () {
var api;
let api;
beforeEach(function () {
var pluginManager = jasmine.createSpyObj('pluginManager', ['addPlugin']);
const pluginManager = jasmine.createSpyObj('pluginManager', ['addPlugin']);
pluginManager.addPlugin.and.resolveTo();
spyOn(common.PluginManager, 'get').and.returnValue(pluginManager);
var projectSpy = jasmine.createSpyObj('AndroidProject', ['getPackageName', 'write', 'isClean']);
const projectSpy = jasmine.createSpyObj('AndroidProject', ['getPackageName', 'write', 'isClean']);
spyOn(AndroidProject, 'getProjectFile').and.returnValue(projectSpy);
api = new Api('android', FAKE_PROJECT_DIR, new EventEmitter());
@@ -60,4 +62,19 @@ describe('Api', () => {
});
});
});
describe('listTargets', () => {
let api;
beforeEach(() => {
api = new Api('android', FAKE_PROJECT_DIR, new EventEmitter());
spyOn(check_reqs, 'run').and.returnValue(Promise.resolve());
});
it('should call into lib/run module', () => {
spyOn(run_mod, 'runListDevices');
return api.listTargets().then(() => {
expect(run_mod.runListDevices).toHaveBeenCalled();
});
});
});
});

View File

@@ -17,8 +17,8 @@
under the License.
*/
const fs = require('fs');
const path = require('path');
const fs = require('node:fs');
const path = require('node:path');
const rewire = require('rewire');
describe('android_sdk', () => {

116
spec/unit/build.spec.js Normal file
View File

@@ -0,0 +1,116 @@
/*
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 builders = require('../../lib/builders/builders');
describe('build', () => {
let build;
const builder = builders.getBuilder('FakeRootPath');
beforeEach(() => {
build = rewire('../../lib/build');
build.__set__({
events: jasmine.createSpyObj('eventsSpy', ['emit'])
});
// run needs `this` to behave like an Api instance
build.run = build.run.bind({
_builder: builder
});
spyOn(builder, 'build').and.returnValue(Promise.resolve({
paths: ['fake.apk'],
buildtype: 'debug'
}));
});
describe('argument parsing', () => {
let prepEnvSpy;
beforeEach(() => {
prepEnvSpy = spyOn(builder, 'prepEnv').and.returnValue(Promise.resolve());
});
describe('gradleArg', () => {
const baseOptions = {
packageType: 'apk',
arch: undefined,
prepEnv: undefined,
buildType: 'debug'
};
it('can parse single gradle argument', async () => {
await build.run({
argv: [
'node',
'--gradleArg=--stacktrace'
]
});
expect(prepEnvSpy).toHaveBeenCalledWith({
...baseOptions,
extraArgs: ['--stacktrace']
});
});
it('can parse multiple gradle arguments', async () => {
await build.run({
argv: [
'node',
'--gradleArg=--stacktrace --info'
]
});
expect(prepEnvSpy).toHaveBeenCalledWith({
...baseOptions,
extraArgs: ['--stacktrace', '--info']
});
});
it('can parse multiple gradle arguments with strings', async () => {
await build.run({
argv: [
'node',
'--gradleArg=--testArg="hello world"'
]
});
expect(prepEnvSpy).toHaveBeenCalledWith({
...baseOptions,
extraArgs: ['--testArg="hello world"']
});
});
it('gradle args will split when necessary', async () => {
await build.run({
argv: [
'node',
'--gradleArg=--warning-mode all'
]
});
expect(prepEnvSpy).toHaveBeenCalledWith({
...baseOptions,
extraArgs: ['--warning-mode', 'all']
});
});
});
});
});

View File

@@ -17,11 +17,10 @@
under the License.
*/
const fs = require('fs-extra');
const path = require('path');
const fs = require('node:fs');
const path = require('node:path');
const rewire = require('rewire');
const CordovaError = require('cordova-common').CordovaError;
const { isWindows } = require('../../../lib/utils');
describe('ProjectBuilder', () => {
const rootDir = '/root';
@@ -55,40 +54,40 @@ describe('ProjectBuilder', () => {
it('should set release argument', () => {
const args = builder.getArgs('release', {});
expect(args[0]).toBe('cdvBuildRelease');
expect(args[args.length - 1]).toBe('cdvBuildRelease');
});
it('should set debug argument', () => {
const args = builder.getArgs('debug', {});
expect(args[0]).toBe('cdvBuildDebug');
expect(args[args.length - 1]).toBe('cdvBuildDebug');
});
it('should set apk release', () => {
const args = builder.getArgs('release', {
packageType: 'apk'
});
expect(args[0]).withContext(args).toBe('cdvBuildRelease');
expect(args[args.length - 1]).withContext(args).toBe('cdvBuildRelease');
});
it('should set apk debug', () => {
const args = builder.getArgs('debug', {
packageType: 'apk'
});
expect(args[0]).withContext(args).toBe('cdvBuildDebug');
expect(args[args.length - 1]).withContext(args).toBe('cdvBuildDebug');
});
it('should set bundle release', () => {
const args = builder.getArgs('release', {
packageType: 'bundle'
});
expect(args[0]).withContext(args).toBe(':app:bundleRelease');
expect(args[args.length - 1]).withContext(args).toBe(':app:bundleRelease');
});
it('should set bundle debug', () => {
const args = builder.getArgs('debug', {
packageType: 'bundle'
});
expect(args[0]).withContext(args).toBe(':app:bundleDebug');
expect(args[args.length - 1]).withContext(args).toBe(':app:bundleDebug');
});
it('should add architecture if it is passed', () => {
@@ -102,14 +101,14 @@ describe('ProjectBuilder', () => {
const args = builder.getArgs('clean', {
packageType: 'apk'
});
expect(args[0]).toBe('clean');
expect(args[args.length - 1]).toBe('clean');
});
it('should clean bundle', () => {
const args = builder.getArgs('clean', {
packageType: 'bundle'
});
expect(args[0]).toBe('clean');
expect(args[args.length - 1]).toBe('clean');
});
describe('should accept extra arguments', () => {
@@ -130,40 +129,21 @@ describe('ProjectBuilder', () => {
});
});
describe('runGradleWrapper', () => {
it('should run the provided gradle command if a gradle wrapper does not already exist', () => {
spyOn(fs, 'existsSync').and.returnValue(false);
builder.runGradleWrapper('/my/sweet/gradle');
expect(execaSpy).toHaveBeenCalledWith('/my/sweet/gradle', jasmine.any(Array), jasmine.any(Object));
describe('installGradleWrapper', () => {
beforeEach(() => {
execaSpy.and.resolveTo();
});
it('should do nothing if a gradle wrapper exists in the project directory', () => {
spyOn(fs, 'existsSync').and.returnValue(true);
builder.runGradleWrapper('/my/sweet/gradle');
expect(execaSpy).not.toHaveBeenCalledWith('/my/sweet/gradle', jasmine.any(Array), jasmine.any(Object));
});
});
describe('extractRealProjectNameFromManifest', () => {
it('should get the project name from the Android Manifest', () => {
const projectName = 'unittestproject';
const projectId = `io.cordova.${projectName}`;
const manifest = `<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="${projectId}"></manifest>`;
spyOn(fs, 'readFileSync').and.returnValue(manifest);
expect(builder.extractRealProjectNameFromManifest()).toBe(projectName);
it('should run gradle wrapper 8.13', async () => {
await builder.installGradleWrapper('8.13');
expect(execaSpy).toHaveBeenCalledWith('gradle', ['-p', path.normalize('/root/tools'), 'wrapper', '--gradle-version', '8.13'], jasmine.any(Object));
});
it('should throw an error if there is no package in the Android manifest', () => {
const manifest = `<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"></manifest>`;
spyOn(fs, 'readFileSync').and.returnValue(manifest);
expect(() => builder.extractRealProjectNameFromManifest()).toThrow(jasmine.any(CordovaError));
it('CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL should override gradle version', async () => {
process.env.CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL = 'https://dist.local';
await builder.installGradleWrapper('8.13');
delete process.env.CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL;
expect(execaSpy).toHaveBeenCalledWith('gradle', ['-p', path.normalize('/root/tools'), 'wrapper', '--gradle-distribution-url', 'https://dist.local'], jasmine.any(Object));
});
});
@@ -196,12 +176,18 @@ describe('ProjectBuilder', () => {
builder.build({});
expect(execaSpy).toHaveBeenCalledWith(path.join(rootDir, 'gradlew'), testArgs, jasmine.anything());
let gradle = path.join(rootDir, 'tools', 'gradlew');
if (isWindows()) {
gradle += '.bat';
}
expect(execaSpy).toHaveBeenCalledWith(gradle, testArgs, jasmine.anything());
});
it('should reject if the spawn fails', () => {
const errorMessage = 'Test error';
execaSpy.and.rejectWith(new Error(errorMessage));
builder.getArgs.and.returnValue([]);
return builder.build({}).then(
() => fail('Unexpectedly resolved'),
@@ -218,6 +204,7 @@ describe('ProjectBuilder', () => {
ProjectBuilder.__set__('check_reqs', checkReqsSpy);
checkReqsSpy.check_android_target.and.resolveTo();
execaSpy.and.rejectWith(testError);
builder.getArgs.and.returnValue([]);
return builder.build({}).then(
() => fail('Unexpectedly resolved'),
@@ -233,7 +220,7 @@ describe('ProjectBuilder', () => {
beforeEach(() => {
const marker = ProjectBuilder.__get__('MARKER');
spyOn(fs, 'readFileSync').and.returnValue(`Some Header Here: ${marker}`);
spyOn(fs, 'removeSync');
spyOn(fs, 'rmSync');
spyOn(builder, 'getArgs');
execaSpy.and.returnValue(Promise.resolve());
});
@@ -251,14 +238,19 @@ describe('ProjectBuilder', () => {
const gradleArgs = ['test', 'args', '-f'];
builder.getArgs.and.returnValue(gradleArgs);
let gradle = path.join(rootDir, 'tools', 'gradlew');
if (isWindows()) {
gradle += '.bat';
}
return builder.clean(opts).then(() => {
expect(execaSpy).toHaveBeenCalledWith(path.join(rootDir, 'gradlew'), gradleArgs, jasmine.anything());
expect(execaSpy).toHaveBeenCalledWith(gradle, gradleArgs, jasmine.anything());
});
});
it('should remove "out" folder', () => {
return builder.clean({}).then(() => {
expect(fs.removeSync).toHaveBeenCalledWith(path.join(rootDir, 'out'));
expect(fs.rmSync).toHaveBeenCalledWith(path.join(rootDir, 'out'), { recursive: true, force: true });
});
});
@@ -269,8 +261,8 @@ describe('ProjectBuilder', () => {
spyOn(fs, 'existsSync').and.returnValue(true);
return builder.clean({}).then(() => {
expect(fs.removeSync).toHaveBeenCalledWith(debugSigningFile);
expect(fs.removeSync).toHaveBeenCalledWith(releaseSigningFile);
expect(fs.rmSync).toHaveBeenCalledWith(debugSigningFile);
expect(fs.rmSync).toHaveBeenCalledWith(releaseSigningFile);
});
});
@@ -281,8 +273,8 @@ describe('ProjectBuilder', () => {
spyOn(fs, 'existsSync').and.returnValue(false);
return builder.clean({}).then(() => {
expect(fs.removeSync).not.toHaveBeenCalledWith(debugSigningFile);
expect(fs.removeSync).not.toHaveBeenCalledWith(releaseSigningFile);
expect(fs.rmSync).not.toHaveBeenCalledWith(debugSigningFile);
expect(fs.rmSync).not.toHaveBeenCalledWith(releaseSigningFile);
});
});
});

View File

@@ -17,16 +17,17 @@
under the License.
*/
var rewire = require('rewire');
var android_sdk = require('../../lib/android_sdk');
var fs = require('fs-extra');
var path = require('path');
var events = require('cordova-common').events;
var which = require('which');
const rewire = require('rewire');
const android_sdk = require('../../lib/android_sdk');
const fs = require('node:fs');
const path = require('node:path');
const events = require('cordova-common').events;
const which = require('which');
const {
SDK_VERSION: DEFAULT_TARGET_API
} = require('../../lib/gradle-config-defaults');
const AndroidCommandLineTools = require('../../lib/env/AndroidCommandLineTools');
describe('check_reqs', function () {
let check_reqs;
@@ -34,7 +35,7 @@ describe('check_reqs', function () {
check_reqs = rewire('../../lib/check_reqs');
});
var original_env;
let original_env;
beforeAll(function () {
original_env = Object.assign({}, process.env);
});
@@ -58,7 +59,11 @@ describe('check_reqs', function () {
});
describe('check_android', function () {
describe('find and set ANDROID_HOME when ANDROID_HOME and ANDROID_SDK_ROOT is not set', function () {
beforeAll(() => {
spyOn(AndroidCommandLineTools, 'getAvailableVersions').and.returnValue(['latest']);
});
describe('find and set ANDROID_HOME when neither ANDROID_HOME nor ANDROID_SDK_ROOT is set', function () {
beforeEach(function () {
delete process.env.ANDROID_HOME;
delete process.env.ANDROID_SDK_ROOT;
@@ -68,20 +73,20 @@ describe('check_reqs', function () {
spyOn(which, 'sync').and.returnValue(null);
spyOn(fs, 'existsSync').and.returnValue(true);
});
it('it should set ANDROID_SDK_ROOT on Windows', () => {
it('it should set ANDROID_HOME on Windows', () => {
spyOn(check_reqs, 'isWindows').and.returnValue(true);
process.env.LOCALAPPDATA = 'windows-local-app-data';
process.env.ProgramFiles = 'windows-program-files';
return check_reqs.check_android().then(function () {
expect(process.env.ANDROID_SDK_ROOT).toContain('windows-local-app-data');
expect(process.env.ANDROID_HOME).toContain('windows-local-app-data');
});
});
it('it should set ANDROID_SDK_ROOT on Darwin', () => {
it('it should set ANDROID_HOME on Darwin', () => {
spyOn(check_reqs, 'isWindows').and.returnValue(false);
spyOn(check_reqs, 'isDarwin').and.returnValue(true);
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');
expect(process.env.ANDROID_HOME).toContain('home is where the heart is');
});
});
});
@@ -91,17 +96,17 @@ describe('check_reqs', function () {
return path;
});
});
it('should set ANDROID_SDK_ROOT based on `adb` command if command exists in a SDK-like directory structure', () => {
it('should set ANDROID_HOME based on `adb` command if command exists in a SDK-like directory structure', () => {
spyOn(fs, 'existsSync').and.returnValue(true);
spyOn(which, 'sync').and.callFake(function (cmd) {
if (cmd === 'adb') {
return '/android/sdk/platform-tools/adb';
return path.normalize('/android/sdk/platform-tools/adb');
} else {
return null;
}
});
return check_reqs.check_android().then(function () {
expect(process.env.ANDROID_SDK_ROOT).toEqual('/android/sdk');
expect(process.env.ANDROID_HOME).toEqual(path.normalize('/android/sdk'));
});
});
it('should error out if `adb` command exists in a non-SDK-like directory structure', () => {
@@ -119,17 +124,17 @@ describe('check_reqs', function () {
expect(err.message).toContain('update your PATH to include valid path');
});
});
it('should set ANDROID_SDK_ROOT based on `avdmanager` command if command exists in a SDK-like directory structure', () => {
it('should set ANDROID_HOME based on `avdmanager` command if command exists in a SDK-like directory structure', () => {
spyOn(fs, 'existsSync').and.returnValue(true);
spyOn(which, 'sync').and.callFake(function (cmd) {
if (cmd === 'avdmanager') {
return '/android/sdk/tools/bin/avdmanager';
return path.resolve('/android/sdk/cmdline-tools/latest/bin/avdmanager');
} else {
return null;
}
});
return check_reqs.check_android().then(function () {
expect(process.env.ANDROID_SDK_ROOT).toEqual('/android/sdk');
expect(process.env.ANDROID_HOME).toEqual(path.resolve('/android/sdk'));
});
});
it('should error out if `avdmanager` command exists in a non-SDK-like directory structure', () => {
@@ -150,52 +155,52 @@ describe('check_reqs', function () {
});
});
describe('ANDROID_SDK_ROOT environment variable detection', () => {
describe('ANDROID_HOME environment variable detection', () => {
beforeEach(() => {
delete process.env.ANDROID_SDK_ROOT;
delete process.env.ANDROID_HOME;
delete process.env.ANDROID_SDK_ROOT;
check_reqs.__set__('forgivingWhichSync', jasmine.createSpy().and.returnValue(''));
});
const expectedAndroidSdkPath = path.sep + 'android' + path.sep + 'sdk';
const expectedAndroidRootSdkPath = path.sep + 'android' + path.sep + 'sdk' + path.sep + 'root';
it('should error if neither ANDROID_SDK_ROOT or ANDROID_HOME is defined', () => {
it('should error if neither ANDROID_HOME nor ANDROID_SDK_ROOT is defined', () => {
spyOn(fs, 'existsSync').and.returnValue(true);
return check_reqs.check_android().catch((error) => {
expect(error.toString()).toContain('Failed to find \'ANDROID_SDK_ROOT\' environment variable.');
expect(error.toString()).toContain('Failed to find \'ANDROID_HOME\' environment variable.');
});
});
it('should use ANDROID_SDK_ROOT if defined', () => {
it('should use ANDROID_HOME if defined', () => {
spyOn(fs, 'existsSync').and.returnValue(true);
process.env.ANDROID_SDK_ROOT = '/android/sdk';
process.env.ANDROID_HOME = path.normalize('/android/sdk');
return check_reqs.check_android().then(() => {
expect(process.env.ANDROID_SDK_ROOT).toContain(expectedAndroidSdkPath);
expect(process.env.ANDROID_HOME).toContain(expectedAndroidSdkPath);
});
});
it('should use ANDROID_HOME if defined and ANDROID_SDK_ROOT is not defined', () => {
it('should use ANDROID_SDK_ROOT if defined and ANDROID_HOME is not defined', () => {
spyOn(fs, 'existsSync').and.returnValue(true);
process.env.ANDROID_HOME = '/android/sdk';
return check_reqs.check_android().then(() => {
expect(process.env.ANDROID_SDK_ROOT).toContain(expectedAndroidSdkPath);
});
});
it('should use ANDROID_SDK_ROOT if defined and ANDROID_HOME is defined', () => {
spyOn(fs, 'existsSync').and.returnValue(true);
process.env.ANDROID_SDK_ROOT = '/android/sdk/root';
process.env.ANDROID_HOME = '/android/sdk';
process.env.ANDROID_SDK_ROOT = path.normalize('/android/sdk/root');
return check_reqs.check_android().then(() => {
expect(process.env.ANDROID_SDK_ROOT).toContain(expectedAndroidRootSdkPath);
});
});
it('should throw if ANDROID_SDK_ROOT points to an invalid path', () => {
process.env.ANDROID_SDK_ROOT = '/android/sdk';
it('should use ANDROID_HOME if defined and ANDROID_SDK_ROOT is defined', () => {
spyOn(fs, 'existsSync').and.returnValue(true);
process.env.ANDROID_HOME = path.normalize('/android/sdk');
process.env.ANDROID_SDK_ROOT = path.normalize('/android/sdk/root');
return check_reqs.check_android().then(() => {
expect(process.env.ANDROID_HOME).toContain(expectedAndroidSdkPath);
});
});
it('should throw if ANDROID_HOME points to an invalid path', () => {
process.env.ANDROID_HOME = path.normalize('/android/sdk');
return check_reqs.check_android().catch((error) => {
expect(error.toString()).toContain('\'ANDROID_SDK_ROOT\' environment variable is set to non-existent path:');
expect(error.toString()).toContain('\'ANDROID_HOME\' environment variable is set to non-existent path:');
});
});
});
@@ -203,14 +208,13 @@ describe('check_reqs', function () {
describe('set PATH for various Android binaries if not available', function () {
beforeEach(function () {
spyOn(which, 'sync').and.returnValue(null);
process.env.ANDROID_SDK_ROOT = 'let the children play';
process.env.ANDROID_HOME = 'let the children play';
spyOn(fs, 'existsSync').and.returnValue(true);
});
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');
expect(process.env.PATH).toContain('let the children play' + path.sep + 'platform-tools');
expect(process.env.PATH).toContain('let the children play' + path.sep + 'tools' + path.sep + 'bin');
expect(process.env.PATH).toContain('let the children play' + path.sep + 'cmdline-tools' + path.sep + 'latest' + path.sep + 'bin');
});
});
});
@@ -219,30 +223,30 @@ describe('check_reqs', function () {
describe('check_gradle', () => {
describe('environment variable checks', () => {
beforeEach(() => {
delete process.env.ANDROID_SDK_ROOT;
delete process.env.ANDROID_HOME;
delete process.env.ANDROID_SDK_ROOT;
spyOn(check_reqs, 'get_gradle_wrapper').and.callFake(() => {
return (process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME) + '/bin/gradle';
return path.normalize((process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT) + '/bin/gradle');
});
});
it('with ANDROID_SDK_ROOT / without ANDROID_HOME', async () => {
process.env.ANDROID_SDK_ROOT = '/android/sdk/root';
await expectAsync(check_reqs.check_gradle()).toBeResolvedTo('/android/sdk/root/bin/gradle');
it('with ANDROID_HOME / without ANDROID_SDK_ROOT', async () => {
process.env.ANDROID_HOME = path.normalize('/android/sdk/home');
await expectAsync(check_reqs.check_gradle()).toBeResolvedTo(path.normalize('/android/sdk/home/bin/gradle'));
});
it('with ANDROID_SDK_ROOT / with ANDROID_HOME', async () => {
process.env.ANDROID_SDK_ROOT = '/android/sdk/root';
process.env.ANDROID_HOME = '/android/sdk/home';
await expectAsync(check_reqs.check_gradle()).toBeResolvedTo('/android/sdk/root/bin/gradle');
it('without ANDROID_HOME / with ANDROID_SDK_ROOT', async () => {
process.env.ANDROID_SDK_ROOT = path.normalize('/android/sdk/root');
await expectAsync(check_reqs.check_gradle()).toBeResolvedTo(path.normalize('/android/sdk/root/bin/gradle'));
});
it('without ANDROID_SDK_ROOT / with ANDROID_HOME', async () => {
process.env.ANDROID_HOME = '/android/sdk/home';
await expectAsync(check_reqs.check_gradle()).toBeResolvedTo('/android/sdk/home/bin/gradle');
it('with ANDROID_HOME / with ANDROID_SDK_ROOT', async () => {
process.env.ANDROID_HOME = path.normalize('/android/sdk/home');
process.env.ANDROID_SDK_ROOT = path.normalize('/android/sdk/root');
await expectAsync(check_reqs.check_gradle()).toBeResolvedTo(path.normalize('/android/sdk/home/bin/gradle'));
});
it('without ANDROID_SDK_ROOT / without ANDROID_HOME', () => {
it('without ANDROID_HOME / without ANDROID_SDK_ROOT', () => {
return check_reqs.check_gradle().catch((error) => {
expect(error.toString()).toContain('Could not find gradle wrapper within Android SDK. Could not find Android SDK directory.');
});
@@ -250,7 +254,7 @@ describe('check_reqs', function () {
});
it('should error if sdk is installed but no gradle found', () => {
process.env.ANDROID_SDK_ROOT = '/android/sdk';
process.env.ANDROID_HOME = path.normalize('/android/sdk');
spyOn(check_reqs, 'get_gradle_wrapper').and.callFake(() => {
return '';
});
@@ -263,8 +267,8 @@ describe('check_reqs', function () {
describe('get_target', function () {
const projectRoot = 'fakeProjectRoot';
var ConfigParser;
var getPreferenceSpy;
let ConfigParser;
let getPreferenceSpy;
beforeEach(function () {
getPreferenceSpy = jasmine.createSpy();
ConfigParser = jasmine.createSpy().and.returnValue({
@@ -274,7 +278,7 @@ describe('check_reqs', function () {
});
it('should retrieve DEFAULT_TARGET_API', function () {
var target = check_reqs.get_target(projectRoot);
const target = check_reqs.get_target(projectRoot);
expect(target).toBeDefined();
expect(target).toContain('android-' + DEFAULT_TARGET_API);
});
@@ -283,7 +287,7 @@ describe('check_reqs', function () {
spyOn(fs, 'existsSync').and.returnValue(true);
getPreferenceSpy.and.returnValue(String(DEFAULT_TARGET_API + 1));
var target = check_reqs.get_target(projectRoot);
const target = check_reqs.get_target(projectRoot);
expect(getPreferenceSpy).toHaveBeenCalledWith('android-targetSdkVersion', 'android');
expect(target).toBe('android-' + (DEFAULT_TARGET_API + 1));
@@ -293,7 +297,7 @@ describe('check_reqs', function () {
spyOn(fs, 'existsSync').and.returnValue(true);
getPreferenceSpy.and.returnValue('android-99');
var target = check_reqs.get_target(projectRoot);
const target = check_reqs.get_target(projectRoot);
expect(getPreferenceSpy).toHaveBeenCalledWith('android-targetSdkVersion', 'android');
expect(target).toBe('android-' + DEFAULT_TARGET_API);
@@ -306,7 +310,7 @@ describe('check_reqs', function () {
getPreferenceSpy.and.returnValue(String(DEFAULT_TARGET_API - 1));
var target = check_reqs.get_target(projectRoot);
const target = check_reqs.get_target(projectRoot);
expect(getPreferenceSpy).toHaveBeenCalledWith('android-targetSdkVersion', 'android');
expect(target).toBe('android-' + DEFAULT_TARGET_API);
@@ -316,7 +320,7 @@ describe('check_reqs', function () {
describe('check_android_target', function () {
it('should should return full list of supported targets if there is a match to ideal api level', () => {
var fake_targets = ['you are my fire', 'my one desire'];
const fake_targets = ['you are my fire', 'my one desire'];
spyOn(android_sdk, 'list_targets').and.resolveTo(fake_targets);
spyOn(check_reqs, 'get_target').and.returnValue('you are my fire');
return check_reqs.check_android_target().then(function (targets) {
@@ -325,7 +329,7 @@ describe('check_reqs', function () {
});
});
it('should error out if there is no match between ideal api level and installed targets', () => {
var fake_targets = ['you are my fire', 'my one desire'];
const fake_targets = ['you are my fire', 'my one desire'];
spyOn(android_sdk, 'list_targets').and.resolveTo(fake_targets);
spyOn(check_reqs, 'get_target').and.returnValue('and i knowwwwwwwwwwww');
return check_reqs.check_android_target().then(() => {

View File

@@ -17,17 +17,25 @@
under the License.
*/
var rewire = require('rewire');
var utils = require('../../lib/utils');
var create = rewire('../../lib/create');
var check_reqs = require('../../lib/check_reqs');
var fs = require('fs-extra');
var path = require('path');
const rewire = require('rewire');
const utils = require('../../lib/utils');
const create = rewire('../../lib/create');
const check_reqs = require('../../lib/check_reqs');
const fs = require('node:fs');
const path = require('node:path');
const MockCordovaGradleConfigParser = require('./mocks/config/MockCordovaGradleConfigParser');
const CordovaGradleConfigParserFactory = require('../../lib/config/CordovaGradleConfigParserFactory');
describe('create', function () {
const PROJECT_DIR = 'platforms/android';
beforeAll(() => {
spyOn(CordovaGradleConfigParserFactory, 'create').and.returnValue(new MockCordovaGradleConfigParser(PROJECT_DIR));
});
describe('validatePackageName helper method', function () {
describe('happy path (valid package names)', function () {
var valid = [
const valid = [
'org.apache.mobilespec',
'com.example',
'com.floors42.package',
@@ -82,7 +90,7 @@ describe('create', function () {
describe('validateProjectName helper method', function () {
describe('happy path (valid project names)', function () {
var valid = [
const valid = [
'mobilespec',
'package_name',
'PackageName',
@@ -111,14 +119,14 @@ describe('create', function () {
});
describe('main method', function () {
var config_mock;
var events_mock;
var Manifest_mock = function () {};
var revert_manifest_mock;
var project_path = path.join('some', 'path');
var app_path = path.join(project_path, 'app', 'src', 'main');
var default_templates = path.join(__dirname, '..', '..', 'templates', 'project');
var fake_android_target = 'android-1337';
let config_mock;
let events_mock;
const Manifest_mock = function () {};
let revert_manifest_mock;
const project_path = path.join('some', 'path');
const app_path = path.join(project_path, 'app', 'src', 'main');
const default_templates = path.join(__dirname, '..', '..', 'templates', 'project');
const fake_android_target = 'android-1337';
beforeEach(function () {
Manifest_mock.prototype = jasmine.createSpyObj('AndroidManifest instance mock', ['setPackageId', 'getActivity', 'setName', 'write']);
@@ -132,11 +140,10 @@ 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');
spyOn(fs, 'ensureDirSync');
spyOn(fs, 'cpSync');
spyOn(fs, 'mkdirSync');
spyOn(utils, 'replaceFileContents');
config_mock = jasmine.createSpyObj('ConfigParser mock instance', ['packageName', 'android_packageName', 'name', 'android_activityName']);
events_mock = jasmine.createSpyObj('EventEmitter mock instance', ['emit']);
@@ -148,10 +155,10 @@ describe('create', function () {
});
describe('parameter values and defaults', function () {
it('should have a default package name of my.cordova.project', () => {
it('should have a default package name of org.apache.cordova.hellocordova', () => {
config_mock.packageName.and.returnValue(undefined);
return create.create(project_path, config_mock, {}, events_mock).then(() => {
expect(create.validatePackageName).toHaveBeenCalledWith('my.cordova.project');
expect(create.validatePackageName).toHaveBeenCalledWith('org.apache.cordova.hellocordova');
});
});
@@ -162,10 +169,10 @@ describe('create', function () {
});
});
it('should have a default project name of CordovaExample', () => {
it('should have a default project name of Hello Cordova', () => {
config_mock.name.and.returnValue(undefined);
return create.create(project_path, config_mock, {}, events_mock).then(() => {
expect(create.validateProjectName).toHaveBeenCalledWith('CordovaExample');
expect(create.validateProjectName).toHaveBeenCalledWith('Hello Cordova');
});
});
@@ -176,10 +183,10 @@ describe('create', function () {
});
});
it('should replace any non-word characters (including unicode and spaces) in the ConfigParser-provided project name with underscores', () => {
it('should keep non-word characters (including unicode and spaces) in the ConfigParser-provided project name', () => {
config_mock.name.and.returnValue('応応応応 hello 用用用用');
return create.create(project_path, config_mock, {}, events_mock).then(() => {
expect(create.validateProjectName).toHaveBeenCalledWith('_____hello_____');
expect(create.validateProjectName).toHaveBeenCalledWith('応応応応 hello 用用用用');
});
});
@@ -231,17 +238,17 @@ describe('create', function () {
describe('happy path', function () {
it('should copy project templates from a specified custom template', () => {
return create.create(project_path, config_mock, { customTemplate: '/template/path' }, events_mock).then(() => {
expect(fs.copySync).toHaveBeenCalledWith(path.join('/template/path', 'assets'), path.join(app_path, 'assets'));
expect(fs.copySync).toHaveBeenCalledWith(path.join('/template/path', 'res'), path.join(app_path, 'res'));
expect(fs.copySync).toHaveBeenCalledWith(path.join('/template/path', 'gitignore'), path.join(project_path, '.gitignore'));
expect(fs.cpSync).toHaveBeenCalledWith(path.join('/template/path', 'assets'), path.join(app_path, 'assets'), { recursive: true });
expect(fs.cpSync).toHaveBeenCalledWith(path.join('/template/path', 'res'), path.join(app_path, 'res'), { recursive: true });
expect(fs.cpSync).toHaveBeenCalledWith(path.join('/template/path', 'gitignore'), path.join(project_path, '.gitignore'));
});
});
it('should copy project templates from the default templates location if no custom template is provided', () => {
return create.create(project_path, config_mock, {}, events_mock).then(() => {
expect(fs.copySync).toHaveBeenCalledWith(path.join(default_templates, 'assets'), path.join(app_path, 'assets'));
expect(fs.copySync).toHaveBeenCalledWith(path.join(default_templates, 'res'), path.join(app_path, 'res'));
expect(fs.copySync).toHaveBeenCalledWith(path.join(default_templates, 'gitignore'), path.join(project_path, '.gitignore'));
expect(fs.cpSync).toHaveBeenCalledWith(path.join(default_templates, 'assets'), path.join(app_path, 'assets'), { recursive: true });
expect(fs.cpSync).toHaveBeenCalledWith(path.join(default_templates, 'res'), path.join(app_path, 'res'), { recursive: true });
expect(fs.cpSync).toHaveBeenCalledWith(path.join(default_templates, 'gitignore'), path.join(project_path, '.gitignore'));
});
});
@@ -254,25 +261,32 @@ describe('create', function () {
it('should create a java src directory based on the provided project package name', () => {
config_mock.packageName.and.returnValue('org.apache.cordova');
return create.create(project_path, config_mock, {}, events_mock).then(() => {
expect(fs.ensureDirSync).toHaveBeenCalledWith(path.join(app_path, 'java', 'org', 'apache', 'cordova'));
expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(app_path, 'java', 'org', 'apache', 'cordova'), { recursive: true });
});
});
it('should copy, rename and interpolate the template Activity java class with the project-specific activity name and package name', () => {
config_mock.packageName.and.returnValue('org.apache.cordova');
config_mock.android_activityName.and.returnValue('CEEDEEVEE');
var activity_path = path.join(app_path, 'java', 'org', 'apache', 'cordova', 'CEEDEEVEE.java');
const activity_path = path.join(app_path, 'java', 'org', 'apache', 'cordova', 'CEEDEEVEE.java');
return create.create(project_path, config_mock, {}, events_mock).then(() => {
expect(fs.copySync).toHaveBeenCalledWith(path.join(default_templates, 'Activity.java'), activity_path);
expect(fs.cpSync).toHaveBeenCalledWith(path.join(default_templates, 'Activity.java'), activity_path);
expect(utils.replaceFileContents).toHaveBeenCalledWith(activity_path, /__ACTIVITY__/, 'CEEDEEVEE');
expect(utils.replaceFileContents).toHaveBeenCalledWith(activity_path, /__ID__/, 'org.apache.cordova');
});
});
it('should interpolate the project name into strings.xml', () => {
it('should interpolate the project name into cdv_strings.xml', () => {
config_mock.name.and.returnValue('IncredibleApp');
return create.create(project_path, config_mock, {}, events_mock).then(() => {
expect(utils.replaceFileContents).toHaveBeenCalledWith(path.join(app_path, 'res', 'values', 'strings.xml'), /__NAME__/, 'IncredibleApp');
expect(utils.replaceFileContents).toHaveBeenCalledWith(path.join(app_path, 'res', 'values', 'cdv_strings.xml'), /__NAME__/, 'IncredibleApp');
});
});
it('should interpolate the escaped project name into cdv_strings.xml', () => {
config_mock.name.and.returnValue('<Incredible&App>');
return create.create(project_path, config_mock, {}, events_mock).then(() => {
expect(utils.replaceFileContents).toHaveBeenCalledWith(path.join(app_path, 'res', 'values', 'cdv_strings.xml'), /__NAME__/, '&lt;Incredible&amp;App&gt;');
});
});
@@ -301,24 +315,4 @@ describe('create', function () {
});
});
});
describe('writeNameForAndroidStudio', () => {
const project_path = path.join('some', 'path');
const appName = 'Test Cordova';
beforeEach(function () {
spyOn(fs, 'ensureDirSync');
spyOn(fs, 'writeFileSync');
});
it('should call ensureDirSync with path', () => {
create.writeNameForAndroidStudio(project_path, appName);
expect(fs.ensureDirSync).toHaveBeenCalledWith(path.join(project_path, '.idea'));
});
it('should call writeFileSync with content', () => {
create.writeNameForAndroidStudio(project_path, appName);
expect(fs.writeFileSync).toHaveBeenCalledWith(path.join(project_path, '.idea', '.name'), appName);
});
});
});

View File

@@ -17,8 +17,8 @@
under the License.
*/
const fs = require('fs-extra');
const path = require('path');
const fs = require('node:fs');
const path = require('node:path');
const rewire = require('rewire');
const which = require('which');

View File

@@ -17,7 +17,7 @@
under the License.
*/
const path = require('path');
const path = require('node:path');
const rewire = require('rewire');
const { CordovaError } = require('cordova-common');
const utils = require('../../lib/utils');

View File

@@ -0,0 +1,32 @@
/**
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 CordovaGradleConfigParser = require('../../../../lib/config/CordovaGradleConfigParser');
module.exports = class MockCordoCordovaGradleConfigParservaGradleConfigParser extends CordovaGradleConfigParser {
_readConfig (configPath) {
return {
PACKAGE_NAMESPACE: 'io.cordova.unittest'
};
}
write () {
// Pretend write :)
}
};

Some files were not shown because too many files have changed in this diff Show More