Эх сурвалжийг харах

init: capacitor/android 5.7.0

ryanemax 2 долоо хоног өмнө
commit
50998ddaec
76 өөрчлөгдсөн 11736 нэмэгдсэн , 0 устгасан
  1. 1 0
      .gitignore
  2. 21 0
      5.7.0/LICENSE
  3. 96 0
      5.7.0/capacitor/build.gradle
  4. 136 0
      5.7.0/capacitor/lint-baseline.xml
  5. 9 0
      5.7.0/capacitor/lint.xml
  6. 28 0
      5.7.0/capacitor/proguard-rules.pro
  7. 3 0
      5.7.0/capacitor/src/main/AndroidManifest.xml
  8. 994 0
      5.7.0/capacitor/src/main/assets/native-bridge.js
  9. 94 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/AndroidProtocolHandler.java
  10. 61 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/App.java
  11. 65 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/AppUUID.java
  12. 1565 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/Bridge.java
  13. 197 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/BridgeActivity.java
  14. 134 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/BridgeFragment.java
  15. 510 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java
  16. 111 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/BridgeWebViewClient.java
  17. 670 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/CapConfig.java
  18. 52 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/CapacitorWebView.java
  19. 292 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/FileUtils.java
  20. 8 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/InvalidPluginException.java
  21. 16 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/InvalidPluginMethodException.java
  22. 51 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/JSArray.java
  23. 193 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/JSExport.java
  24. 16 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/JSExportException.java
  25. 102 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/JSInjector.java
  26. 164 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/JSObject.java
  27. 65 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/JSValue.java
  28. 103 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/Logger.java
  29. 159 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/MessageHandler.java
  30. 37 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/NativePlugin.java
  31. 31 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/PermissionState.java
  32. 1046 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/Plugin.java
  33. 440 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/PluginCall.java
  34. 116 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/PluginConfig.java
  35. 160 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/PluginHandle.java
  36. 16 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/PluginInvocationException.java
  37. 19 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/PluginLoadException.java
  38. 56 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/PluginManager.java
  39. 15 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/PluginMethod.java
  40. 33 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/PluginMethodHandle.java
  41. 84 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/PluginResult.java
  42. 37 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/ProcessedRoute.java
  43. 8 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/RouteProcessor.java
  44. 25 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/ServerPath.java
  45. 180 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/UriMatcher.java
  46. 57 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/WebViewListener.java
  47. 615 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java
  48. 11 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/annotation/ActivityCallback.java
  49. 35 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/annotation/CapacitorPlugin.java
  50. 22 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/annotation/Permission.java
  51. 11 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/annotation/PermissionCallback.java
  52. 42 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/cordova/CapacitorCordovaCookieManager.java
  53. 39 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/cordova/MockCordovaInterfaceImpl.java
  54. 284 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/cordova/MockCordovaWebViewImpl.java
  55. 236 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookieManager.java
  56. 137 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookies.java
  57. 119 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorHttp.java
  58. 48 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/plugin/WebView.java
  59. 358 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/plugin/util/AssetUtil.java
  60. 473 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/plugin/util/CapacitorHttpUrlConnection.java
  61. 443 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/plugin/util/HttpRequestHandler.java
  62. 15 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/plugin/util/ICapacitorHttpUrlConnection.java
  63. 17 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/plugin/util/MimeType.java
  64. 123 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/util/HostMask.java
  65. 27 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/util/InternalUtils.java
  66. 166 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/util/JSONUtils.java
  67. 114 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/util/PermissionHelper.java
  68. 28 0
      5.7.0/capacitor/src/main/java/com/getcapacitor/util/WebColor.java
  69. 15 0
      5.7.0/capacitor/src/main/res/layout/bridge_layout_main.xml
  70. 13 0
      5.7.0/capacitor/src/main/res/layout/fragment_bridge.xml
  71. 6 0
      5.7.0/capacitor/src/main/res/values/attrs.xml
  72. 6 0
      5.7.0/capacitor/src/main/res/values/colors.xml
  73. 2 0
      5.7.0/capacitor/src/main/res/values/strings.xml
  74. 6 0
      5.7.0/capacitor/src/main/res/values/styles.xml
  75. 34 0
      5.7.0/package.json
  76. 15 0
      package.json

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+node_modules

+ 21 - 0
5.7.0/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017-present Drifty Co.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 96 - 0
5.7.0/capacitor/build.gradle

@@ -0,0 +1,96 @@
+ext {
+    androidxActivityVersion = project.hasProperty('androidxActivityVersion') ? rootProject.ext.androidxActivityVersion : '1.7.0'
+    androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.6.1'
+    androidxCoordinatorLayoutVersion = project.hasProperty('androidxCoordinatorLayoutVersion') ? rootProject.ext.androidxCoordinatorLayoutVersion : '1.2.0'
+    androidxCoreVersion = project.hasProperty('androidxCoreVersion') ? rootProject.ext.androidxCoreVersion : '1.10.0'
+    androidxFragmentVersion = project.hasProperty('androidxFragmentVersion') ? rootProject.ext.androidxFragmentVersion : '1.5.6'
+    androidxWebkitVersion = project.hasProperty('androidxWebkitVersion') ? rootProject.ext.androidxWebkitVersion : '1.6.1'
+    junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
+    androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.1.5'
+    androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.5.1'
+    cordovaAndroidVersion = project.hasProperty('cordovaAndroidVersion') ? rootProject.ext.cordovaAndroidVersion : '10.1.1'
+}
+
+
+buildscript {
+    ext.kotlin_version = project.hasProperty("kotlin_version") ? rootProject.ext.kotlin_version : '1.8.20'
+    repositories {
+        google()
+        mavenCentral()
+        maven {
+            url "https://plugins.gradle.org/m2/"
+        }
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:8.0.0'
+
+        if (System.getenv("CAP_PUBLISH") == "true") {
+            classpath 'io.github.gradle-nexus:publish-plugin:1.3.0'
+        }
+    }
+}
+
+tasks.withType(Javadoc).all { enabled = false }
+
+apply plugin: 'com.android.library'
+
+if (System.getenv("CAP_PUBLISH") == "true") {
+    apply plugin: 'io.github.gradle-nexus.publish-plugin'
+    apply from: file('../scripts/publish-root.gradle')
+    apply from: file('../scripts/publish-module.gradle')
+}
+
+android {
+    namespace "com.getcapacitor.android"
+    compileSdkVersion project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 33
+    defaultConfig {
+        minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 22
+        targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 33
+        versionCode 1
+        versionName "1.0"
+        consumerProguardFiles 'proguard-rules.pro'
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+    lintOptions {
+        baseline file("lint-baseline.xml")
+        abortOnError true
+        warningsAsErrors true
+        lintConfig file('lint.xml')
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_17
+        targetCompatibility JavaVersion.VERSION_17
+    }
+    publishing {
+        singleVariant("release")
+    }
+}
+
+repositories {
+    google()
+    mavenCentral()
+}
+
+dependencies {
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+    implementation platform("org.jetbrains.kotlin:kotlin-bom:$kotlin_version")
+    implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
+    implementation "androidx.core:core:$androidxCoreVersion"
+    implementation "androidx.activity:activity:$androidxActivityVersion"
+    implementation "androidx.fragment:fragment:$androidxFragmentVersion"
+    implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
+    implementation "androidx.webkit:webkit:$androidxWebkitVersion"
+    testImplementation "junit:junit:$junitVersion"
+    androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
+    androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
+    implementation "org.apache.cordova:framework:$cordovaAndroidVersion"
+    testImplementation 'org.json:json:20231013'
+    testImplementation 'org.mockito:mockito-inline:5.2.0'
+}
+

+ 136 - 0
5.7.0/capacitor/lint-baseline.xml

@@ -0,0 +1,136 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.1.1" client="gradle" variant="all" version="4.1.1">
+
+    <issue
+        id="DefaultLocale"
+        message="Implicitly using the default locale is a common source of bugs: Use `String.format(Locale, ...)` instead"
+        errorLine1="            String msg = String.format("
+        errorLine2="                         ^">
+        <location
+            file="src/main/java/com/getcapacitor/BridgeWebChromeClient.java"
+            line="474"
+            column="26"/>
+    </issue>
+
+    <issue
+        id="DefaultLocale"
+        message="Implicitly using the default locale is a common source of bugs: Use `toUpperCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
+        errorLine1="                return mask.toUpperCase().equals(string.toUpperCase());"
+        errorLine2="                            ~~~~~~~~~~~">
+        <location
+            file="src/main/java/com/getcapacitor/util/HostMask.java"
+            line="110"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="DefaultLocale"
+        message="Implicitly using the default locale is a common source of bugs: Use `toUpperCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
+        errorLine1="                return mask.toUpperCase().equals(string.toUpperCase());"
+        errorLine2="                                                        ~~~~~~~~~~~">
+        <location
+            file="src/main/java/com/getcapacitor/util/HostMask.java"
+            line="110"
+            column="57"/>
+    </issue>
+
+    <issue
+        id="DefaultLocale"
+        message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
+        errorLine1="                switch (spinnerStyle.toLowerCase()) {"
+        errorLine2="                                     ~~~~~~~~~~~">
+        <location
+            file="src/main/java/com/getcapacitor/Splash.java"
+            line="127"
+            column="38"/>
+    </issue>
+
+    <issue
+        id="DefaultLocale"
+        message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
+        errorLine1="                    if (header.getKey().equalsIgnoreCase(&quot;Accept&quot;) &amp;&amp; header.getValue().toLowerCase().contains(&quot;text/html&quot;)) {"
+        errorLine2="                                                                                        ~~~~~~~~~~~">
+        <location
+            file="src/main/java/com/getcapacitor/WebViewLocalServer.java"
+            line="327"
+            column="89"/>
+    </issue>
+
+    <issue
+        id="SimpleDateFormat"
+        message="To get local formatting use `getDateInstance()`, `getDateTimeInstance()`, or `getTimeInstance()`, or use `new SimpleDateFormat(String template, Locale locale)` with for example `Locale.US` for ASCII dates."
+        errorLine1="        String timeStamp = new SimpleDateFormat(&quot;yyyyMMdd_HHmmss&quot;).format(new Date());"
+        errorLine2="                           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/com/getcapacitor/BridgeWebChromeClient.java"
+            line="511"
+            column="28"/>
+    </issue>
+
+    <issue
+        id="SimpleDateFormat"
+        message="To get local formatting use `getDateInstance()`, `getDateTimeInstance()`, or `getTimeInstance()`, or use `new SimpleDateFormat(String template, Locale locale)` with for example `Locale.US` for ASCII dates."
+        errorLine1="        DateFormat df = new SimpleDateFormat(&quot;yyyy-MM-dd&apos;T&apos;HH:mm&apos;Z&apos;&quot;);"
+        errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/com/getcapacitor/PluginResult.java"
+            line="44"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="SetJavaScriptEnabled"
+        message="Using `setJavaScriptEnabled` can introduce XSS vulnerabilities into your application, review carefully"
+        errorLine1="        settings.setJavaScriptEnabled(true);"
+        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/com/getcapacitor/Bridge.java"
+            line="384"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="Recycle"
+        message="This `TypedArray` should be recycled after use with `#recycle()`"
+        errorLine1="        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.bridge_fragment);"
+        errorLine2="                               ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/com/getcapacitor/BridgeFragment.java"
+            line="84"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="StaticFieldLeak"
+        message="Do not place Android context classes in static fields; this is a memory leak"
+        errorLine1="    private static ImageView splashImage;"
+        errorLine2="            ~~~~~~">
+        <location
+            file="src/main/java/com/getcapacitor/Splash.java"
+            line="41"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="StaticFieldLeak"
+        message="Do not place Android context classes in static fields; this is a memory leak"
+        errorLine1="    private static ProgressBar spinnerBar;"
+        errorLine2="            ~~~~~~">
+        <location
+            file="src/main/java/com/getcapacitor/Splash.java"
+            line="42"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="Overdraw"
+        message="Possible overdraw: Root element paints background `#F0FF1414` with a theme that also paints a background (inferred theme is `@android:style/Theme.Holo`)"
+        errorLine1="    android:background=&quot;#F0FF1414&quot;"
+        errorLine2="    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/res/layout/fragment_bridge.xml"
+            line="5"
+            column="5"/>
+    </issue>
+
+</issues>

+ 9 - 0
5.7.0/capacitor/lint.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<lint>
+    <issue id="GradleDependency" severity="ignore" />
+    <issue id="AndroidGradlePluginVersion" severity="ignore" />
+    <issue id="DiscouragedApi">
+        <ignore path="src/main/java/com/getcapacitor/plugin/util/AssetUtil.java" />
+    </issue>
+    <issue id="ObsoleteSdkInt" severity="informational" />
+</lint>

+ 28 - 0
5.7.0/capacitor/proguard-rules.pro

@@ -0,0 +1,28 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Rules for Capacitor v3 plugins and annotations
+ -keep @com.getcapacitor.annotation.CapacitorPlugin public class * {
+     @com.getcapacitor.annotation.PermissionCallback <methods>;
+     @com.getcapacitor.annotation.ActivityCallback <methods>;
+     @com.getcapacitor.annotation.Permission <methods>;
+     @com.getcapacitor.PluginMethod public <methods>;
+ }
+
+ -keep public class * extends com.getcapacitor.Plugin { *; }
+
+# Rules for Capacitor v2 plugins and annotations
+# These are deprecated but can still be used with Capacitor for now
+-keep @com.getcapacitor.NativePlugin public class * {
+  @com.getcapacitor.PluginMethod public <methods>;
+}
+
+# Rules for Cordova plugins
+-keep public class * extends org.apache.cordova.* {
+  public <methods>;
+  public <fields>;
+}

+ 3 - 0
5.7.0/capacitor/src/main/AndroidManifest.xml

@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+</manifest>

+ 994 - 0
5.7.0/capacitor/src/main/assets/native-bridge.js

@@ -0,0 +1,994 @@
+
+/*! Capacitor: https://capacitorjs.com/ - MIT License */
+/* Generated File. Do not edit. */
+
+var nativeBridge = (function (exports) {
+    'use strict';
+
+    var ExceptionCode;
+    (function (ExceptionCode) {
+        /**
+         * API is not implemented.
+         *
+         * This usually means the API can't be used because it is not implemented for
+         * the current platform.
+         */
+        ExceptionCode["Unimplemented"] = "UNIMPLEMENTED";
+        /**
+         * API is not available.
+         *
+         * This means the API can't be used right now because:
+         *   - it is currently missing a prerequisite, such as network connectivity
+         *   - it requires a particular platform or browser version
+         */
+        ExceptionCode["Unavailable"] = "UNAVAILABLE";
+    })(ExceptionCode || (ExceptionCode = {}));
+    class CapacitorException extends Error {
+        constructor(message, code, data) {
+            super(message);
+            this.message = message;
+            this.code = code;
+            this.data = data;
+        }
+    }
+
+    // For removing exports for iOS/Android, keep let for reassignment
+    // eslint-disable-next-line
+    let dummy = {};
+    const readFileAsBase64 = (file) => new Promise((resolve, reject) => {
+        const reader = new FileReader();
+        reader.onloadend = () => {
+            const data = reader.result;
+            resolve(btoa(data));
+        };
+        reader.onerror = reject;
+        reader.readAsBinaryString(file);
+    });
+    const convertFormData = async (formData) => {
+        const newFormData = [];
+        for (const pair of formData.entries()) {
+            const [key, value] = pair;
+            if (value instanceof File) {
+                const base64File = await readFileAsBase64(value);
+                newFormData.push({
+                    key,
+                    value: base64File,
+                    type: 'base64File',
+                    contentType: value.type,
+                    fileName: value.name,
+                });
+            }
+            else {
+                newFormData.push({ key, value, type: 'string' });
+            }
+        }
+        return newFormData;
+    };
+    const convertBody = async (body, contentType) => {
+        if (body instanceof ReadableStream) {
+            const reader = body.getReader();
+            const chunks = [];
+            while (true) {
+                const { done, value } = await reader.read();
+                if (done)
+                    break;
+                chunks.push(value);
+            }
+            const concatenated = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0));
+            let position = 0;
+            for (const chunk of chunks) {
+                concatenated.set(chunk, position);
+                position += chunk.length;
+            }
+            let data = new TextDecoder().decode(concatenated);
+            let type;
+            if (contentType === 'application/json') {
+                try {
+                    data = JSON.parse(data);
+                }
+                catch (ignored) {
+                    // ignore
+                }
+                type = 'json';
+            }
+            else if (contentType === 'multipart/form-data') {
+                type = 'formData';
+            }
+            else if (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith('image')) {
+                type = 'image';
+            }
+            else if (contentType === 'application/octet-stream') {
+                type = 'binary';
+            }
+            else {
+                type = 'text';
+            }
+            return {
+                data,
+                type,
+                headers: { 'Content-Type': contentType || 'application/octet-stream' },
+            };
+        }
+        else if (body instanceof FormData) {
+            const formData = await convertFormData(body);
+            const boundary = `${Date.now()}`;
+            return {
+                data: formData,
+                type: 'formData',
+                headers: {
+                    'Content-Type': `multipart/form-data; boundary=--${boundary}`,
+                },
+            };
+        }
+        else if (body instanceof File) {
+            const fileData = await readFileAsBase64(body);
+            return {
+                data: fileData,
+                type: 'file',
+                headers: { 'Content-Type': body.type },
+            };
+        }
+        return { data: body, type: 'json' };
+    };
+    const initBridge = (w) => {
+        const getPlatformId = (win) => {
+            var _a, _b;
+            if (win === null || win === void 0 ? void 0 : win.androidBridge) {
+                return 'android';
+            }
+            else if ((_b = (_a = win === null || win === void 0 ? void 0 : win.webkit) === null || _a === void 0 ? void 0 : _a.messageHandlers) === null || _b === void 0 ? void 0 : _b.bridge) {
+                return 'ios';
+            }
+            else {
+                return 'web';
+            }
+        };
+        const convertFileSrcServerUrl = (webviewServerUrl, filePath) => {
+            if (typeof filePath === 'string') {
+                if (filePath.startsWith('/')) {
+                    return webviewServerUrl + '/_capacitor_file_' + filePath;
+                }
+                else if (filePath.startsWith('file://')) {
+                    return (webviewServerUrl + filePath.replace('file://', '/_capacitor_file_'));
+                }
+                else if (filePath.startsWith('content://')) {
+                    return (webviewServerUrl +
+                        filePath.replace('content:/', '/_capacitor_content_'));
+                }
+            }
+            return filePath;
+        };
+        const initEvents = (win, cap) => {
+            cap.addListener = (pluginName, eventName, callback) => {
+                const callbackId = cap.nativeCallback(pluginName, 'addListener', {
+                    eventName: eventName,
+                }, callback);
+                return {
+                    remove: async () => {
+                        var _a;
+                        (_a = win === null || win === void 0 ? void 0 : win.console) === null || _a === void 0 ? void 0 : _a.debug('Removing listener', pluginName, eventName);
+                        cap.removeListener(pluginName, callbackId, eventName, callback);
+                    },
+                };
+            };
+            cap.removeListener = (pluginName, callbackId, eventName, callback) => {
+                cap.nativeCallback(pluginName, 'removeListener', {
+                    callbackId: callbackId,
+                    eventName: eventName,
+                }, callback);
+            };
+            cap.createEvent = (eventName, eventData) => {
+                const doc = win.document;
+                if (doc) {
+                    const ev = doc.createEvent('Events');
+                    ev.initEvent(eventName, false, false);
+                    if (eventData && typeof eventData === 'object') {
+                        for (const i in eventData) {
+                            // eslint-disable-next-line no-prototype-builtins
+                            if (eventData.hasOwnProperty(i)) {
+                                ev[i] = eventData[i];
+                            }
+                        }
+                    }
+                    return ev;
+                }
+                return null;
+            };
+            cap.triggerEvent = (eventName, target, eventData) => {
+                const doc = win.document;
+                const cordova = win.cordova;
+                eventData = eventData || {};
+                const ev = cap.createEvent(eventName, eventData);
+                if (ev) {
+                    if (target === 'document') {
+                        if (cordova === null || cordova === void 0 ? void 0 : cordova.fireDocumentEvent) {
+                            cordova.fireDocumentEvent(eventName, eventData);
+                            return true;
+                        }
+                        else if (doc === null || doc === void 0 ? void 0 : doc.dispatchEvent) {
+                            return doc.dispatchEvent(ev);
+                        }
+                    }
+                    else if (target === 'window' && win.dispatchEvent) {
+                        return win.dispatchEvent(ev);
+                    }
+                    else if (doc === null || doc === void 0 ? void 0 : doc.querySelector) {
+                        const targetEl = doc.querySelector(target);
+                        if (targetEl) {
+                            return targetEl.dispatchEvent(ev);
+                        }
+                    }
+                }
+                return false;
+            };
+            win.Capacitor = cap;
+        };
+        const initLegacyHandlers = (win, cap) => {
+            // define cordova if it's not there already
+            win.cordova = win.cordova || {};
+            const doc = win.document;
+            const nav = win.navigator;
+            if (nav) {
+                nav.app = nav.app || {};
+                nav.app.exitApp = () => {
+                    var _a;
+                    if (!((_a = cap.Plugins) === null || _a === void 0 ? void 0 : _a.App)) {
+                        win.console.warn('App plugin not installed');
+                    }
+                    else {
+                        cap.nativeCallback('App', 'exitApp', {});
+                    }
+                };
+            }
+            if (doc) {
+                const docAddEventListener = doc.addEventListener;
+                doc.addEventListener = (...args) => {
+                    var _a;
+                    const eventName = args[0];
+                    const handler = args[1];
+                    if (eventName === 'deviceready' && handler) {
+                        Promise.resolve().then(handler);
+                    }
+                    else if (eventName === 'backbutton' && cap.Plugins.App) {
+                        // Add a dummy listener so Capacitor doesn't do the default
+                        // back button action
+                        if (!((_a = cap.Plugins) === null || _a === void 0 ? void 0 : _a.App)) {
+                            win.console.warn('App plugin not installed');
+                        }
+                        else {
+                            cap.Plugins.App.addListener('backButton', () => {
+                                // ignore
+                            });
+                        }
+                    }
+                    return docAddEventListener.apply(doc, args);
+                };
+            }
+            // deprecated in v3, remove from v4
+            cap.platform = cap.getPlatform();
+            cap.isNative = cap.isNativePlatform();
+            win.Capacitor = cap;
+        };
+        const initVendor = (win, cap) => {
+            const Ionic = (win.Ionic = win.Ionic || {});
+            const IonicWebView = (Ionic.WebView = Ionic.WebView || {});
+            const Plugins = cap.Plugins;
+            IonicWebView.getServerBasePath = (callback) => {
+                var _a;
+                (_a = Plugins === null || Plugins === void 0 ? void 0 : Plugins.WebView) === null || _a === void 0 ? void 0 : _a.getServerBasePath().then((result) => {
+                    callback(result.path);
+                });
+            };
+            IonicWebView.setServerAssetPath = (path) => {
+                var _a;
+                (_a = Plugins === null || Plugins === void 0 ? void 0 : Plugins.WebView) === null || _a === void 0 ? void 0 : _a.setServerAssetPath({ path });
+            };
+            IonicWebView.setServerBasePath = (path) => {
+                var _a;
+                (_a = Plugins === null || Plugins === void 0 ? void 0 : Plugins.WebView) === null || _a === void 0 ? void 0 : _a.setServerBasePath({ path });
+            };
+            IonicWebView.persistServerBasePath = () => {
+                var _a;
+                (_a = Plugins === null || Plugins === void 0 ? void 0 : Plugins.WebView) === null || _a === void 0 ? void 0 : _a.persistServerBasePath();
+            };
+            IonicWebView.convertFileSrc = (url) => cap.convertFileSrc(url);
+            win.Capacitor = cap;
+            win.Ionic.WebView = IonicWebView;
+        };
+        const initLogger = (win, cap) => {
+            const BRIDGED_CONSOLE_METHODS = [
+                'debug',
+                'error',
+                'info',
+                'log',
+                'trace',
+                'warn',
+            ];
+            const createLogFromNative = (c) => (result) => {
+                if (isFullConsole(c)) {
+                    const success = result.success === true;
+                    const tagStyles = success
+                        ? 'font-style: italic; font-weight: lighter; color: gray'
+                        : 'font-style: italic; font-weight: lighter; color: red';
+                    c.groupCollapsed('%cresult %c' +
+                        result.pluginId +
+                        '.' +
+                        result.methodName +
+                        ' (#' +
+                        result.callbackId +
+                        ')', tagStyles, 'font-style: italic; font-weight: bold; color: #444');
+                    if (result.success === false) {
+                        c.error(result.error);
+                    }
+                    else {
+                        c.dir(result.data);
+                    }
+                    c.groupEnd();
+                }
+                else {
+                    if (result.success === false) {
+                        c.error('LOG FROM NATIVE', result.error);
+                    }
+                    else {
+                        c.log('LOG FROM NATIVE', result.data);
+                    }
+                }
+            };
+            const createLogToNative = (c) => (call) => {
+                if (isFullConsole(c)) {
+                    c.groupCollapsed('%cnative %c' +
+                        call.pluginId +
+                        '.' +
+                        call.methodName +
+                        ' (#' +
+                        call.callbackId +
+                        ')', 'font-weight: lighter; color: gray', 'font-weight: bold; color: #000');
+                    c.dir(call);
+                    c.groupEnd();
+                }
+                else {
+                    c.log('LOG TO NATIVE: ', call);
+                }
+            };
+            const isFullConsole = (c) => {
+                if (!c) {
+                    return false;
+                }
+                return (typeof c.groupCollapsed === 'function' ||
+                    typeof c.groupEnd === 'function' ||
+                    typeof c.dir === 'function');
+            };
+            const serializeConsoleMessage = (msg) => {
+                if (typeof msg === 'object') {
+                    try {
+                        msg = JSON.stringify(msg);
+                    }
+                    catch (e) {
+                        // ignore
+                    }
+                }
+                return String(msg);
+            };
+            const platform = getPlatformId(win);
+            if (platform == 'android' || platform == 'ios') {
+                // patch document.cookie on Android/iOS
+                win.CapacitorCookiesDescriptor =
+                    Object.getOwnPropertyDescriptor(Document.prototype, 'cookie') ||
+                        Object.getOwnPropertyDescriptor(HTMLDocument.prototype, 'cookie');
+                let doPatchCookies = false;
+                // check if capacitor cookies is disabled before patching
+                if (platform === 'ios') {
+                    // Use prompt to synchronously get capacitor cookies config.
+                    // https://stackoverflow.com/questions/29249132/wkwebview-complex-communication-between-javascript-native-code/49474323#49474323
+                    const payload = {
+                        type: 'CapacitorCookies.isEnabled',
+                    };
+                    const isCookiesEnabled = prompt(JSON.stringify(payload));
+                    if (isCookiesEnabled === 'true') {
+                        doPatchCookies = true;
+                    }
+                }
+                else if (typeof win.CapacitorCookiesAndroidInterface !== 'undefined') {
+                    const isCookiesEnabled = win.CapacitorCookiesAndroidInterface.isEnabled();
+                    if (isCookiesEnabled === true) {
+                        doPatchCookies = true;
+                    }
+                }
+                if (doPatchCookies) {
+                    Object.defineProperty(document, 'cookie', {
+                        get: function () {
+                            var _a, _b, _c;
+                            if (platform === 'ios') {
+                                // Use prompt to synchronously get cookies.
+                                // https://stackoverflow.com/questions/29249132/wkwebview-complex-communication-between-javascript-native-code/49474323#49474323
+                                const payload = {
+                                    type: 'CapacitorCookies.get',
+                                };
+                                const res = prompt(JSON.stringify(payload));
+                                return res;
+                            }
+                            else if (typeof win.CapacitorCookiesAndroidInterface !== 'undefined') {
+                                // return original document.cookie since Android does not support filtering of `httpOnly` cookies
+                                return (_c = (_b = (_a = win.CapacitorCookiesDescriptor) === null || _a === void 0 ? void 0 : _a.get) === null || _b === void 0 ? void 0 : _b.call(document)) !== null && _c !== void 0 ? _c : '';
+                            }
+                        },
+                        set: function (val) {
+                            const cookiePairs = val.split(';');
+                            const domainSection = val.toLowerCase().split('domain=')[1];
+                            const domain = cookiePairs.length > 1 &&
+                                domainSection != null &&
+                                domainSection.length > 0
+                                ? domainSection.split(';')[0].trim()
+                                : '';
+                            if (platform === 'ios') {
+                                // Use prompt to synchronously set cookies.
+                                // https://stackoverflow.com/questions/29249132/wkwebview-complex-communication-between-javascript-native-code/49474323#49474323
+                                const payload = {
+                                    type: 'CapacitorCookies.set',
+                                    action: val,
+                                    domain,
+                                };
+                                prompt(JSON.stringify(payload));
+                            }
+                            else if (typeof win.CapacitorCookiesAndroidInterface !== 'undefined') {
+                                win.CapacitorCookiesAndroidInterface.setCookie(domain, val);
+                            }
+                        },
+                    });
+                }
+                // patch fetch / XHR on Android/iOS
+                // store original fetch & XHR functions
+                win.CapacitorWebFetch = window.fetch;
+                win.CapacitorWebXMLHttpRequest = {
+                    abort: window.XMLHttpRequest.prototype.abort,
+                    constructor: window.XMLHttpRequest.prototype.constructor,
+                    fullObject: window.XMLHttpRequest,
+                    getAllResponseHeaders: window.XMLHttpRequest.prototype.getAllResponseHeaders,
+                    getResponseHeader: window.XMLHttpRequest.prototype.getResponseHeader,
+                    open: window.XMLHttpRequest.prototype.open,
+                    prototype: window.XMLHttpRequest.prototype,
+                    send: window.XMLHttpRequest.prototype.send,
+                    setRequestHeader: window.XMLHttpRequest.prototype.setRequestHeader,
+                };
+                let doPatchHttp = false;
+                // check if capacitor http is disabled before patching
+                if (platform === 'ios') {
+                    // Use prompt to synchronously get capacitor http config.
+                    // https://stackoverflow.com/questions/29249132/wkwebview-complex-communication-between-javascript-native-code/49474323#49474323
+                    const payload = {
+                        type: 'CapacitorHttp',
+                    };
+                    const isHttpEnabled = prompt(JSON.stringify(payload));
+                    if (isHttpEnabled === 'true') {
+                        doPatchHttp = true;
+                    }
+                }
+                else if (typeof win.CapacitorHttpAndroidInterface !== 'undefined') {
+                    const isHttpEnabled = win.CapacitorHttpAndroidInterface.isEnabled();
+                    if (isHttpEnabled === true) {
+                        doPatchHttp = true;
+                    }
+                }
+                if (doPatchHttp) {
+                    // fetch patch
+                    window.fetch = async (resource, options) => {
+                        const request = new Request(resource, options);
+                        if (request.url.startsWith(`${cap.getServerUrl()}/`)) {
+                            return win.CapacitorWebFetch(resource, options);
+                        }
+                        const tag = `CapacitorHttp fetch ${Date.now()} ${resource}`;
+                        console.time(tag);
+                        try {
+                            // intercept request & pass to the bridge
+                            const { body, method } = request;
+                            const optionHeaders = Object.fromEntries(request.headers.entries());
+                            const { data: requestData, type, headers, } = await convertBody((options === null || options === void 0 ? void 0 : options.body) || body || undefined, optionHeaders['Content-Type'] || optionHeaders['content-type']);
+                            const nativeResponse = await cap.nativePromise('CapacitorHttp', 'request', {
+                                url: request.url,
+                                method: method,
+                                data: requestData,
+                                dataType: type,
+                                headers: Object.assign(Object.assign({}, headers), optionHeaders),
+                            });
+                            const contentType = nativeResponse.headers['Content-Type'] ||
+                                nativeResponse.headers['content-type'];
+                            let data = (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith('application/json'))
+                                ? JSON.stringify(nativeResponse.data)
+                                : nativeResponse.data;
+                            // use null data for 204 No Content HTTP response
+                            if (nativeResponse.status === 204) {
+                                data = null;
+                            }
+                            // intercept & parse response before returning
+                            const response = new Response(data, {
+                                headers: nativeResponse.headers,
+                                status: nativeResponse.status,
+                            });
+                            /*
+                             * copy url to response, `cordova-plugin-ionic` uses this url from the response
+                             * we need `Object.defineProperty` because url is an inherited getter on the Response
+                             * see: https://stackoverflow.com/a/57382543
+                             * */
+                            Object.defineProperty(response, 'url', {
+                                value: nativeResponse.url,
+                            });
+                            console.timeEnd(tag);
+                            return response;
+                        }
+                        catch (error) {
+                            console.timeEnd(tag);
+                            return Promise.reject(error);
+                        }
+                    };
+                    window.XMLHttpRequest = function () {
+                        const xhr = new win.CapacitorWebXMLHttpRequest.constructor();
+                        Object.defineProperties(xhr, {
+                            _headers: {
+                                value: {},
+                                writable: true,
+                            },
+                            _method: {
+                                value: xhr.method,
+                                writable: true,
+                            },
+                            readyState: {
+                                get: function () {
+                                    var _a;
+                                    return (_a = this._readyState) !== null && _a !== void 0 ? _a : 0;
+                                },
+                                set: function (val) {
+                                    this._readyState = val;
+                                    setTimeout(() => {
+                                        this.dispatchEvent(new Event('readystatechange'));
+                                    });
+                                },
+                            },
+                        });
+                        xhr.readyState = 0;
+                        const prototype = win.CapacitorWebXMLHttpRequest.prototype;
+                        const isRelativeURL = (url) => !url || !(url.startsWith('http:') || url.startsWith('https:'));
+                        const isProgressEventAvailable = () => typeof ProgressEvent !== 'undefined' &&
+                            ProgressEvent.prototype instanceof Event;
+                        // XHR patch abort
+                        prototype.abort = function () {
+                            if (isRelativeURL(this._url)) {
+                                return win.CapacitorWebXMLHttpRequest.abort.call(this);
+                            }
+                            this.readyState = 0;
+                            setTimeout(() => {
+                                this.dispatchEvent(new Event('abort'));
+                                this.dispatchEvent(new Event('loadend'));
+                            });
+                        };
+                        // XHR patch open
+                        prototype.open = function (method, url) {
+                            this._url = url;
+                            this._method = method;
+                            if (isRelativeURL(url)) {
+                                return win.CapacitorWebXMLHttpRequest.open.call(this, method, url);
+                            }
+                            setTimeout(() => {
+                                this.dispatchEvent(new Event('loadstart'));
+                            });
+                            this.readyState = 1;
+                        };
+                        // XHR patch set request header
+                        prototype.setRequestHeader = function (header, value) {
+                            if (isRelativeURL(this._url)) {
+                                return win.CapacitorWebXMLHttpRequest.setRequestHeader.call(this, header, value);
+                            }
+                            this._headers[header] = value;
+                        };
+                        // XHR patch send
+                        prototype.send = function (body) {
+                            if (isRelativeURL(this._url)) {
+                                return win.CapacitorWebXMLHttpRequest.send.call(this, body);
+                            }
+                            const tag = `CapacitorHttp XMLHttpRequest ${Date.now()} ${this._url}`;
+                            console.time(tag);
+                            try {
+                                this.readyState = 2;
+                                Object.defineProperties(this, {
+                                    response: {
+                                        value: '',
+                                        writable: true,
+                                    },
+                                    responseText: {
+                                        value: '',
+                                        writable: true,
+                                    },
+                                    responseURL: {
+                                        value: '',
+                                        writable: true,
+                                    },
+                                    status: {
+                                        value: 0,
+                                        writable: true,
+                                    },
+                                });
+                                convertBody(body).then(({ data, type, headers }) => {
+                                    const otherHeaders = this._headers != null && Object.keys(this._headers).length > 0
+                                        ? this._headers
+                                        : undefined;
+                                    // intercept request & pass to the bridge
+                                    cap
+                                        .nativePromise('CapacitorHttp', 'request', {
+                                        url: this._url,
+                                        method: this._method,
+                                        data: data !== null ? data : undefined,
+                                        headers: Object.assign(Object.assign({}, headers), otherHeaders),
+                                        dataType: type,
+                                    })
+                                        .then((nativeResponse) => {
+                                        var _a;
+                                        // intercept & parse response before returning
+                                        if (this.readyState == 2) {
+                                            //TODO: Add progress event emission on native side
+                                            if (isProgressEventAvailable()) {
+                                                this.dispatchEvent(new ProgressEvent('progress', {
+                                                    lengthComputable: true,
+                                                    loaded: nativeResponse.data.length,
+                                                    total: nativeResponse.data.length,
+                                                }));
+                                            }
+                                            this._headers = nativeResponse.headers;
+                                            this.status = nativeResponse.status;
+                                            const responseString = typeof nativeResponse.data !== 'string'
+                                                ? JSON.stringify(nativeResponse.data)
+                                                : nativeResponse.data;
+                                            if (this.responseType === '' ||
+                                                this.responseType === 'text') {
+                                                this.response = responseString;
+                                            }
+                                            else if (this.responseType === 'blob') {
+                                                this.response = new Blob([responseString], {
+                                                    type: 'application/json',
+                                                });
+                                            }
+                                            else if (this.responseType === 'arraybuffer') {
+                                                const encoder = new TextEncoder();
+                                                const uint8Array = encoder.encode(responseString);
+                                                this.response = uint8Array.buffer;
+                                            }
+                                            else {
+                                                this.response = nativeResponse.data;
+                                            }
+                                            this.responseText = ((_a = nativeResponse.headers['Content-Type']) === null || _a === void 0 ? void 0 : _a.startsWith('application/json'))
+                                                ? JSON.stringify(nativeResponse.data)
+                                                : nativeResponse.data;
+                                            this.responseURL = nativeResponse.url;
+                                            this.readyState = 4;
+                                            setTimeout(() => {
+                                                this.dispatchEvent(new Event('load'));
+                                                this.dispatchEvent(new Event('loadend'));
+                                            });
+                                        }
+                                        console.timeEnd(tag);
+                                    })
+                                        .catch((error) => {
+                                        this.status = error.status;
+                                        this._headers = error.headers;
+                                        this.response = error.data;
+                                        this.responseText = JSON.stringify(error.data);
+                                        this.responseURL = error.url;
+                                        this.readyState = 4;
+                                        if (isProgressEventAvailable()) {
+                                            this.dispatchEvent(new ProgressEvent('progress', {
+                                                lengthComputable: false,
+                                                loaded: 0,
+                                                total: 0,
+                                            }));
+                                        }
+                                        setTimeout(() => {
+                                            this.dispatchEvent(new Event('error'));
+                                            this.dispatchEvent(new Event('loadend'));
+                                        });
+                                        console.timeEnd(tag);
+                                    });
+                                });
+                            }
+                            catch (error) {
+                                this.status = 500;
+                                this._headers = {};
+                                this.response = error;
+                                this.responseText = error.toString();
+                                this.responseURL = this._url;
+                                this.readyState = 4;
+                                if (isProgressEventAvailable()) {
+                                    this.dispatchEvent(new ProgressEvent('progress', {
+                                        lengthComputable: false,
+                                        loaded: 0,
+                                        total: 0,
+                                    }));
+                                }
+                                setTimeout(() => {
+                                    this.dispatchEvent(new Event('error'));
+                                    this.dispatchEvent(new Event('loadend'));
+                                });
+                                console.timeEnd(tag);
+                            }
+                        };
+                        // XHR patch getAllResponseHeaders
+                        prototype.getAllResponseHeaders = function () {
+                            if (isRelativeURL(this._url)) {
+                                return win.CapacitorWebXMLHttpRequest.getAllResponseHeaders.call(this);
+                            }
+                            let returnString = '';
+                            for (const key in this._headers) {
+                                if (key.toLowerCase() !== 'set-cookie') {
+                                    returnString += key + ': ' + this._headers[key] + '\r\n';
+                                }
+                            }
+                            return returnString;
+                        };
+                        // XHR patch getResponseHeader
+                        prototype.getResponseHeader = function (name) {
+                            if (isRelativeURL(this._url)) {
+                                return win.CapacitorWebXMLHttpRequest.getResponseHeader.call(this, name);
+                            }
+                            for (const key in this._headers) {
+                                if (key.toLowerCase() === name.toLowerCase()) {
+                                    return this._headers[key];
+                                }
+                            }
+                            return null;
+                        };
+                        Object.setPrototypeOf(xhr, prototype);
+                        return xhr;
+                    };
+                    Object.assign(window.XMLHttpRequest, win.CapacitorWebXMLHttpRequest.fullObject);
+                }
+            }
+            // patch window.console on iOS and store original console fns
+            const isIos = getPlatformId(win) === 'ios';
+            if (win.console && isIos) {
+                Object.defineProperties(win.console, BRIDGED_CONSOLE_METHODS.reduce((props, method) => {
+                    const consoleMethod = win.console[method].bind(win.console);
+                    props[method] = {
+                        value: (...args) => {
+                            const msgs = [...args];
+                            cap.toNative('Console', 'log', {
+                                level: method,
+                                message: msgs.map(serializeConsoleMessage).join(' '),
+                            });
+                            return consoleMethod(...args);
+                        },
+                    };
+                    return props;
+                }, {}));
+            }
+            cap.logJs = (msg, level) => {
+                switch (level) {
+                    case 'error':
+                        win.console.error(msg);
+                        break;
+                    case 'warn':
+                        win.console.warn(msg);
+                        break;
+                    case 'info':
+                        win.console.info(msg);
+                        break;
+                    default:
+                        win.console.log(msg);
+                }
+            };
+            cap.logToNative = createLogToNative(win.console);
+            cap.logFromNative = createLogFromNative(win.console);
+            cap.handleError = err => win.console.error(err);
+            win.Capacitor = cap;
+        };
+        function initNativeBridge(win) {
+            const cap = win.Capacitor || {};
+            // keep a collection of callbacks for native response data
+            const callbacks = new Map();
+            const webviewServerUrl = typeof win.WEBVIEW_SERVER_URL === 'string' ? win.WEBVIEW_SERVER_URL : '';
+            cap.getServerUrl = () => webviewServerUrl;
+            cap.convertFileSrc = filePath => convertFileSrcServerUrl(webviewServerUrl, filePath);
+            // Counter of callback ids, randomized to avoid
+            // any issues during reloads if a call comes back with
+            // an existing callback id from an old session
+            let callbackIdCount = Math.floor(Math.random() * 134217728);
+            let postToNative = null;
+            const isNativePlatform = () => true;
+            const getPlatform = () => getPlatformId(win);
+            cap.getPlatform = getPlatform;
+            cap.isPluginAvailable = name => Object.prototype.hasOwnProperty.call(cap.Plugins, name);
+            cap.isNativePlatform = isNativePlatform;
+            // create the postToNative() fn if needed
+            if (getPlatformId(win) === 'android') {
+                // android platform
+                postToNative = data => {
+                    var _a;
+                    try {
+                        win.androidBridge.postMessage(JSON.stringify(data));
+                    }
+                    catch (e) {
+                        (_a = win === null || win === void 0 ? void 0 : win.console) === null || _a === void 0 ? void 0 : _a.error(e);
+                    }
+                };
+            }
+            else if (getPlatformId(win) === 'ios') {
+                // ios platform
+                postToNative = data => {
+                    var _a;
+                    try {
+                        data.type = data.type ? data.type : 'message';
+                        win.webkit.messageHandlers.bridge.postMessage(data);
+                    }
+                    catch (e) {
+                        (_a = win === null || win === void 0 ? void 0 : win.console) === null || _a === void 0 ? void 0 : _a.error(e);
+                    }
+                };
+            }
+            cap.handleWindowError = (msg, url, lineNo, columnNo, err) => {
+                const str = msg.toLowerCase();
+                if (str.indexOf('script error') > -1) ;
+                else {
+                    const errObj = {
+                        type: 'js.error',
+                        error: {
+                            message: msg,
+                            url: url,
+                            line: lineNo,
+                            col: columnNo,
+                            errorObject: JSON.stringify(err),
+                        },
+                    };
+                    if (err !== null) {
+                        cap.handleError(err);
+                    }
+                    postToNative(errObj);
+                }
+                return false;
+            };
+            if (cap.DEBUG) {
+                window.onerror = cap.handleWindowError;
+            }
+            initLogger(win, cap);
+            /**
+             * Send a plugin method call to the native layer
+             */
+            cap.toNative = (pluginName, methodName, options, storedCallback) => {
+                var _a, _b;
+                try {
+                    if (typeof postToNative === 'function') {
+                        let callbackId = '-1';
+                        if (storedCallback &&
+                            (typeof storedCallback.callback === 'function' ||
+                                typeof storedCallback.resolve === 'function')) {
+                            // store the call for later lookup
+                            callbackId = String(++callbackIdCount);
+                            callbacks.set(callbackId, storedCallback);
+                        }
+                        const callData = {
+                            callbackId: callbackId,
+                            pluginId: pluginName,
+                            methodName: methodName,
+                            options: options || {},
+                        };
+                        if (cap.isLoggingEnabled && pluginName !== 'Console') {
+                            cap.logToNative(callData);
+                        }
+                        // post the call data to native
+                        postToNative(callData);
+                        return callbackId;
+                    }
+                    else {
+                        (_a = win === null || win === void 0 ? void 0 : win.console) === null || _a === void 0 ? void 0 : _a.warn(`implementation unavailable for: ${pluginName}`);
+                    }
+                }
+                catch (e) {
+                    (_b = win === null || win === void 0 ? void 0 : win.console) === null || _b === void 0 ? void 0 : _b.error(e);
+                }
+                return null;
+            };
+            if (win === null || win === void 0 ? void 0 : win.androidBridge) {
+                win.androidBridge.onmessage = function (event) {
+                    returnResult(JSON.parse(event.data));
+                };
+            }
+            /**
+             * Process a response from the native layer.
+             */
+            cap.fromNative = result => {
+                returnResult(result);
+            };
+            const returnResult = (result) => {
+                var _a, _b;
+                if (cap.isLoggingEnabled && result.pluginId !== 'Console') {
+                    cap.logFromNative(result);
+                }
+                // get the stored call, if it exists
+                try {
+                    const storedCall = callbacks.get(result.callbackId);
+                    if (storedCall) {
+                        // looks like we've got a stored call
+                        if (result.error) {
+                            // ensure stacktraces by copying error properties to an Error
+                            result.error = Object.keys(result.error).reduce((err, key) => {
+                                // use any type to avoid importing util and compiling most of .ts files
+                                err[key] = result.error[key];
+                                return err;
+                            }, new cap.Exception(''));
+                        }
+                        if (typeof storedCall.callback === 'function') {
+                            // callback
+                            if (result.success) {
+                                storedCall.callback(result.data);
+                            }
+                            else {
+                                storedCall.callback(null, result.error);
+                            }
+                        }
+                        else if (typeof storedCall.resolve === 'function') {
+                            // promise
+                            if (result.success) {
+                                storedCall.resolve(result.data);
+                            }
+                            else {
+                                storedCall.reject(result.error);
+                            }
+                            // no need to keep this stored callback
+                            // around for a one time resolve promise
+                            callbacks.delete(result.callbackId);
+                        }
+                    }
+                    else if (!result.success && result.error) {
+                        // no stored callback, but if there was an error let's log it
+                        (_a = win === null || win === void 0 ? void 0 : win.console) === null || _a === void 0 ? void 0 : _a.warn(result.error);
+                    }
+                    if (result.save === false) {
+                        callbacks.delete(result.callbackId);
+                    }
+                }
+                catch (e) {
+                    (_b = win === null || win === void 0 ? void 0 : win.console) === null || _b === void 0 ? void 0 : _b.error(e);
+                }
+                // always delete to prevent memory leaks
+                // overkill but we're not sure what apps will do with this data
+                delete result.data;
+                delete result.error;
+            };
+            cap.nativeCallback = (pluginName, methodName, options, callback) => {
+                if (typeof options === 'function') {
+                    console.warn(`Using a callback as the 'options' parameter of 'nativeCallback()' is deprecated.`);
+                    callback = options;
+                    options = null;
+                }
+                return cap.toNative(pluginName, methodName, options, { callback });
+            };
+            cap.nativePromise = (pluginName, methodName, options) => {
+                return new Promise((resolve, reject) => {
+                    cap.toNative(pluginName, methodName, options, {
+                        resolve: resolve,
+                        reject: reject,
+                    });
+                });
+            };
+            // eslint-disable-next-line @typescript-eslint/no-unused-vars
+            cap.withPlugin = (_pluginId, _fn) => dummy;
+            cap.Exception = CapacitorException;
+            initEvents(win, cap);
+            initLegacyHandlers(win, cap);
+            initVendor(win, cap);
+            win.Capacitor = cap;
+        }
+        initNativeBridge(w);
+    };
+    initBridge(typeof globalThis !== 'undefined'
+        ? globalThis
+        : typeof self !== 'undefined'
+            ? self
+            : typeof window !== 'undefined'
+                ? window
+                : typeof global !== 'undefined'
+                    ? global
+                    : {});
+
+    dummy = initBridge;
+
+    Object.defineProperty(exports, '__esModule', { value: true });
+
+    return exports;
+
+})({});

+ 94 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/AndroidProtocolHandler.java

@@ -0,0 +1,94 @@
+// Copyright 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package com.getcapacitor;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.net.Uri;
+import android.util.TypedValue;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+public class AndroidProtocolHandler {
+
+    private Context context;
+
+    public AndroidProtocolHandler(Context context) {
+        this.context = context;
+    }
+
+    public InputStream openAsset(String path) throws IOException {
+        return context.getAssets().open(path, AssetManager.ACCESS_STREAMING);
+    }
+
+    public InputStream openResource(Uri uri) {
+        assert uri.getPath() != null;
+        // The path must be of the form ".../asset_type/asset_name.ext".
+        List<String> pathSegments = uri.getPathSegments();
+        String assetType = pathSegments.get(pathSegments.size() - 2);
+        String assetName = pathSegments.get(pathSegments.size() - 1);
+
+        // Drop the file extension.
+        assetName = assetName.split("\\.")[0];
+        try {
+            // Use the application context for resolving the resource package name so that we do
+            // not use the browser's own resources. Note that if 'context' here belongs to the
+            // test suite, it does not have a separate application context. In that case we use
+            // the original context object directly.
+            if (context.getApplicationContext() != null) {
+                context = context.getApplicationContext();
+            }
+            int fieldId = getFieldId(context, assetType, assetName);
+            int valueType = getValueType(context, fieldId);
+            if (valueType == TypedValue.TYPE_STRING) {
+                return context.getResources().openRawResource(fieldId);
+            } else {
+                Logger.error("Asset not of type string: " + uri);
+            }
+        } catch (ClassNotFoundException | IllegalAccessException | NoSuchFieldException e) {
+            Logger.error("Unable to open resource URL: " + uri, e);
+        }
+        return null;
+    }
+
+    private static int getFieldId(Context context, String assetType, String assetName)
+        throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
+        Class<?> d = context.getClassLoader().loadClass(context.getPackageName() + ".R$" + assetType);
+        java.lang.reflect.Field field = d.getField(assetName);
+        return field.getInt(null);
+    }
+
+    public InputStream openFile(String filePath) throws IOException {
+        String realPath = filePath.replace(Bridge.CAPACITOR_FILE_START, "");
+        File localFile = new File(realPath);
+        return new FileInputStream(localFile);
+    }
+
+    public InputStream openContentUrl(Uri uri) throws IOException {
+        Integer port = uri.getPort();
+        String baseUrl = uri.getScheme() + "://" + uri.getHost();
+        if (port != -1) {
+            baseUrl += ":" + port;
+        }
+        String realPath = uri.toString().replace(baseUrl + Bridge.CAPACITOR_CONTENT_START, "content:/");
+
+        InputStream stream = null;
+        try {
+            stream = context.getContentResolver().openInputStream(Uri.parse(realPath));
+        } catch (SecurityException e) {
+            Logger.error("Unable to open content URL: " + uri, e);
+        }
+        return stream;
+    }
+
+    private static int getValueType(Context context, int fieldId) {
+        TypedValue value = new TypedValue();
+        context.getResources().getValue(fieldId, value, true);
+        return value.type;
+    }
+}

+ 61 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/App.java

@@ -0,0 +1,61 @@
+package com.getcapacitor;
+
+import androidx.annotation.Nullable;
+
+public class App {
+
+    /**
+     * Interface for callbacks when app status changes.
+     */
+    public interface AppStatusChangeListener {
+        void onAppStatusChanged(Boolean isActive);
+    }
+
+    /**
+     * Interface for callbacks when app is restored with pending plugin call.
+     */
+    public interface AppRestoredListener {
+        void onAppRestored(PluginResult result);
+    }
+
+    @Nullable
+    private AppStatusChangeListener statusChangeListener;
+
+    @Nullable
+    private AppRestoredListener appRestoredListener;
+
+    private boolean isActive = false;
+
+    public boolean isActive() {
+        return isActive;
+    }
+
+    /**
+     * Set the object to receive callbacks.
+     * @param listener
+     */
+    public void setStatusChangeListener(@Nullable AppStatusChangeListener listener) {
+        this.statusChangeListener = listener;
+    }
+
+    /**
+     * Set the object to receive callbacks.
+     * @param listener
+     */
+    public void setAppRestoredListener(@Nullable AppRestoredListener listener) {
+        this.appRestoredListener = listener;
+    }
+
+    protected void fireRestoredResult(PluginResult result) {
+        if (appRestoredListener != null) {
+            appRestoredListener.onAppRestored(result);
+        }
+    }
+
+    public void fireStatusChange(boolean isActive) {
+        this.isActive = isActive;
+        if (statusChangeListener != null) {
+            statusChangeListener.onAppStatusChanged(isActive);
+        }
+    }
+}

+ 65 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/AppUUID.java

@@ -0,0 +1,65 @@
+package com.getcapacitor;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import androidx.appcompat.app.AppCompatActivity;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Objects;
+import java.util.UUID;
+
+public final class AppUUID {
+
+    private static final String KEY = "CapacitorAppUUID";
+
+    public static String getAppUUID(AppCompatActivity activity) throws Exception {
+        assertAppUUID(activity);
+        return readUUID(activity);
+    }
+
+    public static void regenerateAppUUID(AppCompatActivity activity) throws Exception {
+        try {
+            String uuid = generateUUID();
+            writeUUID(activity, uuid);
+        } catch (NoSuchAlgorithmException ex) {
+            throw new Exception("Capacitor App UUID could not be generated.");
+        }
+    }
+
+    private static void assertAppUUID(AppCompatActivity activity) throws Exception {
+        String uuid = readUUID(activity);
+        if (uuid.equals("")) {
+            regenerateAppUUID(activity);
+        }
+    }
+
+    private static String generateUUID() throws NoSuchAlgorithmException {
+        MessageDigest salt = MessageDigest.getInstance("SHA-256");
+        salt.update(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8));
+        return bytesToHex(salt.digest());
+    }
+
+    private static String readUUID(AppCompatActivity activity) {
+        SharedPreferences sharedPref = activity.getPreferences(Context.MODE_PRIVATE);
+        return sharedPref.getString(KEY, "");
+    }
+
+    private static void writeUUID(AppCompatActivity activity, String uuid) {
+        SharedPreferences sharedPref = activity.getPreferences(Context.MODE_PRIVATE);
+        SharedPreferences.Editor editor = sharedPref.edit();
+        editor.putString(KEY, uuid);
+        editor.apply();
+    }
+
+    private static String bytesToHex(byte[] bytes) {
+        byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII);
+        byte[] hexChars = new byte[bytes.length * 2];
+        for (int j = 0; j < bytes.length; j++) {
+            int v = bytes[j] & 0xFF;
+            hexChars[j * 2] = HEX_ARRAY[v >>> 4];
+            hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
+        }
+        return new String(hexChars, StandardCharsets.UTF_8);
+    }
+}

+ 1565 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/Bridge.java

@@ -0,0 +1,1565 @@
+package com.getcapacitor;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.webkit.ValueCallback;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import androidx.activity.result.ActivityResultCallback;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContract;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.pm.PackageInfoCompat;
+import androidx.fragment.app.Fragment;
+import com.getcapacitor.android.R;
+import com.getcapacitor.annotation.CapacitorPlugin;
+import com.getcapacitor.annotation.Permission;
+import com.getcapacitor.cordova.MockCordovaInterfaceImpl;
+import com.getcapacitor.cordova.MockCordovaWebViewImpl;
+import com.getcapacitor.util.HostMask;
+import com.getcapacitor.util.InternalUtils;
+import com.getcapacitor.util.PermissionHelper;
+import com.getcapacitor.util.WebColor;
+import java.io.File;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.cordova.ConfigXmlParser;
+import org.apache.cordova.CordovaPreferences;
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.PluginEntry;
+import org.apache.cordova.PluginManager;
+import org.json.JSONException;
+
+/**
+ * The Bridge class is the main engine of Capacitor. It manages
+ * loading and communicating with all Plugins,
+ * proxying Native events to Plugins, executing Plugin methods,
+ * communicating with the WebView, and a whole lot more.
+ *
+ * Generally, you'll not use Bridge directly, instead, extend from BridgeActivity
+ * to get a WebView instance and proxy native events automatically.
+ *
+ * If you want to use this Bridge in an existing Android app, please
+ * see the source for BridgeActivity for the methods you'll need to
+ * pass through to Bridge:
+ * <a href="https://github.com/ionic-team/capacitor/blob/HEAD/android/capacitor/src/main/java/com/getcapacitor/BridgeActivity.java">
+ *   BridgeActivity.java</a>
+ */
+public class Bridge {
+
+    private static final String PREFS_NAME = "CapacitorSettings";
+    private static final String PERMISSION_PREFS_NAME = "PluginPermStates";
+    private static final String BUNDLE_LAST_PLUGIN_ID_KEY = "capacitorLastActivityPluginId";
+    private static final String BUNDLE_LAST_PLUGIN_CALL_METHOD_NAME_KEY = "capacitorLastActivityPluginMethod";
+    private static final String BUNDLE_PLUGIN_CALL_OPTIONS_SAVED_KEY = "capacitorLastPluginCallOptions";
+    private static final String BUNDLE_PLUGIN_CALL_BUNDLE_KEY = "capacitorLastPluginCallBundle";
+    private static final String LAST_BINARY_VERSION_CODE = "lastBinaryVersionCode";
+    private static final String LAST_BINARY_VERSION_NAME = "lastBinaryVersionName";
+    private static final String MINIMUM_ANDROID_WEBVIEW_ERROR = "System WebView is not supported";
+
+    // The name of the directory we use to look for index.html and the rest of our web assets
+    public static final String DEFAULT_WEB_ASSET_DIR = "public";
+    public static final String CAPACITOR_HTTP_SCHEME = "http";
+    public static final String CAPACITOR_HTTPS_SCHEME = "https";
+    public static final String CAPACITOR_FILE_START = "/_capacitor_file_";
+    public static final String CAPACITOR_CONTENT_START = "/_capacitor_content_";
+    public static final int DEFAULT_ANDROID_WEBVIEW_VERSION = 60;
+    public static final int MINIMUM_ANDROID_WEBVIEW_VERSION = 55;
+    public static final int DEFAULT_HUAWEI_WEBVIEW_VERSION = 10;
+    public static final int MINIMUM_HUAWEI_WEBVIEW_VERSION = 10;
+
+    // Loaded Capacitor config
+    private CapConfig config;
+
+    // A reference to the main activity for the app
+    private final AppCompatActivity context;
+    // A reference to the containing Fragment if used
+    private final Fragment fragment;
+    private WebViewLocalServer localServer;
+    private String localUrl;
+    private String appUrl;
+    private String appUrlConfig;
+    private HostMask appAllowNavigationMask;
+    private Set<String> allowedOriginRules = new HashSet<String>();
+    private ArrayList<String> authorities = new ArrayList<>();
+    // A reference to the main WebView for the app
+    private final WebView webView;
+    public final MockCordovaInterfaceImpl cordovaInterface;
+    private CordovaWebView cordovaWebView;
+    private CordovaPreferences preferences;
+    private BridgeWebViewClient webViewClient;
+    private App app;
+
+    // Our MessageHandler for sending and receiving data to the WebView
+    private final MessageHandler msgHandler;
+
+    // The ThreadHandler for executing plugin calls
+    private final HandlerThread handlerThread = new HandlerThread("CapacitorPlugins");
+
+    // Our Handler for posting plugin calls. Created from the ThreadHandler
+    private Handler taskHandler = null;
+
+    private final List<Class<? extends Plugin>> initialPlugins;
+
+    private final List<Plugin> pluginInstances;
+
+    // A map of Plugin Id's to PluginHandle's
+    private Map<String, PluginHandle> plugins = new HashMap<>();
+
+    // Stored plugin calls that we're keeping around to call again someday
+    private Map<String, PluginCall> savedCalls = new HashMap<>();
+
+    // The call IDs of saved plugin calls with associated plugin id for handling permissions
+    private Map<String, LinkedList<String>> savedPermissionCallIds = new HashMap<>();
+
+    // Store a plugin that started a new activity, in case we need to resume
+    // the app and return that data back
+    private PluginCall pluginCallForLastActivity;
+
+    // Any URI that was passed to the app on start
+    private Uri intentUri;
+
+    // A list of listeners that trigger when webView events occur
+    private List<WebViewListener> webViewListeners = new ArrayList<>();
+
+    // An interface to manipulate route resolving
+    private RouteProcessor routeProcessor;
+
+    // A pre-determined path to load the bridge
+    private ServerPath serverPath;
+
+    /**
+     * Create the Bridge with a reference to the main {@link Activity} for the
+     * app, and a reference to the {@link WebView} our app will use.
+     * @param context
+     * @param webView
+     * @deprecated Use {@link Bridge.Builder} to create Bridge instances
+     */
+    @Deprecated
+    public Bridge(
+        AppCompatActivity context,
+        WebView webView,
+        List<Class<? extends Plugin>> initialPlugins,
+        MockCordovaInterfaceImpl cordovaInterface,
+        PluginManager pluginManager,
+        CordovaPreferences preferences,
+        CapConfig config
+    ) {
+        this(context, null, null, webView, initialPlugins, new ArrayList<>(), cordovaInterface, pluginManager, preferences, config);
+    }
+
+    private Bridge(
+        AppCompatActivity context,
+        ServerPath serverPath,
+        Fragment fragment,
+        WebView webView,
+        List<Class<? extends Plugin>> initialPlugins,
+        List<Plugin> pluginInstances,
+        MockCordovaInterfaceImpl cordovaInterface,
+        PluginManager pluginManager,
+        CordovaPreferences preferences,
+        CapConfig config
+    ) {
+        this.app = new App();
+        this.serverPath = serverPath;
+        this.context = context;
+        this.fragment = fragment;
+        this.webView = webView;
+        this.webViewClient = new BridgeWebViewClient(this);
+        this.initialPlugins = initialPlugins;
+        this.pluginInstances = pluginInstances;
+        this.cordovaInterface = cordovaInterface;
+        this.preferences = preferences;
+
+        // Start our plugin execution threads and handlers
+        handlerThread.start();
+        taskHandler = new Handler(handlerThread.getLooper());
+
+        this.config = config != null ? config : CapConfig.loadDefault(getActivity());
+        Logger.init(this.config);
+
+        // Initialize web view and message handler for it
+        this.initWebView();
+        this.setAllowedOriginRules();
+        this.msgHandler = new MessageHandler(this, webView, pluginManager);
+
+        // Grab any intent info that our app was launched with
+        Intent intent = context.getIntent();
+        this.intentUri = intent.getData();
+        // Register our core plugins
+        this.registerAllPlugins();
+
+        this.loadWebView();
+    }
+
+    private void setAllowedOriginRules() {
+        String[] appAllowNavigationConfig = this.config.getAllowNavigation();
+        String authority = this.getHost();
+        String scheme = this.getScheme();
+        allowedOriginRules.add(scheme + "://" + authority);
+        if (this.getServerUrl() != null) {
+            allowedOriginRules.add(this.getServerUrl());
+        }
+        if (appAllowNavigationConfig != null) {
+            for (String allowNavigation : appAllowNavigationConfig) {
+                if (!allowNavigation.startsWith("http")) {
+                    allowedOriginRules.add("https://" + allowNavigation);
+                } else {
+                    allowedOriginRules.add(allowNavigation);
+                }
+            }
+            authorities.addAll(Arrays.asList(appAllowNavigationConfig));
+        }
+        this.appAllowNavigationMask = HostMask.Parser.parse(appAllowNavigationConfig);
+    }
+
+    public App getApp() {
+        return app;
+    }
+
+    private void loadWebView() {
+        final boolean html5mode = this.config.isHTML5Mode();
+
+        // Start the local web server
+        localServer = new WebViewLocalServer(context, this, getJSInjector(), authorities, html5mode);
+        localServer.hostAssets(DEFAULT_WEB_ASSET_DIR);
+
+        Logger.debug("Loading app at " + appUrl);
+
+        webView.setWebChromeClient(new BridgeWebChromeClient(this));
+        webView.setWebViewClient(this.webViewClient);
+
+        if (!isDeployDisabled() && !isNewBinary()) {
+            SharedPreferences prefs = getContext()
+                .getSharedPreferences(com.getcapacitor.plugin.WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE);
+            String path = prefs.getString(com.getcapacitor.plugin.WebView.CAP_SERVER_PATH, null);
+            if (path != null && !path.isEmpty() && new File(path).exists()) {
+                setServerBasePath(path);
+            }
+        }
+        if (!this.isMinimumWebViewInstalled()) {
+            String errorUrl = this.getErrorUrl();
+            if (errorUrl != null) {
+                webView.loadUrl(errorUrl);
+                return;
+            } else {
+                Logger.error(MINIMUM_ANDROID_WEBVIEW_ERROR);
+            }
+        }
+
+        // If serverPath configured, start server based on provided path
+        if (serverPath != null) {
+            if (serverPath.getType() == ServerPath.PathType.ASSET_PATH) {
+                setServerAssetPath(serverPath.getPath());
+            } else {
+                setServerBasePath(serverPath.getPath());
+            }
+        } else {
+            // Get to work
+            webView.loadUrl(appUrl);
+        }
+    }
+
+    @SuppressLint("WebViewApiAvailability")
+    public boolean isMinimumWebViewInstalled() {
+        PackageManager pm = getContext().getPackageManager();
+
+        // Check getCurrentWebViewPackage() directly if above Android 8
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            PackageInfo info = WebView.getCurrentWebViewPackage();
+            Pattern pattern = Pattern.compile("(\\d+)");
+            Matcher matcher = pattern.matcher(info.versionName);
+            if (matcher.find()) {
+                String majorVersionStr = matcher.group(0);
+                int majorVersion = Integer.parseInt(majorVersionStr);
+                if (info.packageName.equals("com.huawei.webview")) {
+                    return majorVersion >= config.getMinHuaweiWebViewVersion();
+                }
+                return majorVersion >= config.getMinWebViewVersion();
+            } else {
+                return false;
+            }
+        }
+
+        // Otherwise manually check WebView versions
+        try {
+            String webViewPackage = "com.google.android.webview";
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                webViewPackage = "com.android.chrome";
+            }
+            PackageInfo info = InternalUtils.getPackageInfo(pm, webViewPackage);
+            String majorVersionStr = info.versionName.split("\\.")[0];
+            int majorVersion = Integer.parseInt(majorVersionStr);
+            return majorVersion >= config.getMinWebViewVersion();
+        } catch (Exception ex) {
+            Logger.warn("Unable to get package info for 'com.google.android.webview'" + ex.toString());
+        }
+
+        try {
+            PackageInfo info = InternalUtils.getPackageInfo(pm, "com.android.webview");
+            String majorVersionStr = info.versionName.split("\\.")[0];
+            int majorVersion = Integer.parseInt(majorVersionStr);
+            return majorVersion >= config.getMinWebViewVersion();
+        } catch (Exception ex) {
+            Logger.warn("Unable to get package info for 'com.android.webview'" + ex.toString());
+        }
+
+        final int amazonFireMajorWebViewVersion = extractWebViewMajorVersion(pm, "com.amazon.webview.chromium");
+        if (amazonFireMajorWebViewVersion >= config.getMinWebViewVersion()) {
+            return true;
+        }
+
+        // Could not detect any webview, return false
+        return false;
+    }
+
+    private int extractWebViewMajorVersion(final PackageManager pm, final String webViewPackageName) {
+        try {
+            final PackageInfo info = InternalUtils.getPackageInfo(pm, webViewPackageName);
+            final String majorVersionStr = info.versionName.split("\\.")[0];
+            final int majorVersion = Integer.parseInt(majorVersionStr);
+            return majorVersion;
+        } catch (Exception ex) {
+            Logger.warn(String.format("Unable to get package info for '%s' with err '%s'", webViewPackageName, ex));
+        }
+        return 0;
+    }
+
+    public boolean launchIntent(Uri url) {
+        /*
+         * Give plugins the chance to handle the url
+         */
+        for (Map.Entry<String, PluginHandle> entry : plugins.entrySet()) {
+            Plugin plugin = entry.getValue().getInstance();
+            if (plugin != null) {
+                Boolean shouldOverrideLoad = plugin.shouldOverrideLoad(url);
+                if (shouldOverrideLoad != null) {
+                    return shouldOverrideLoad;
+                }
+            }
+        }
+
+        if (url.getScheme().equals("data")) {
+            return false;
+        }
+
+        Uri appUri = Uri.parse(appUrl);
+        if (
+            !(appUri.getHost().equals(url.getHost()) && url.getScheme().equals(appUri.getScheme())) &&
+            !appAllowNavigationMask.matches(url.getHost())
+        ) {
+            try {
+                Intent openIntent = new Intent(Intent.ACTION_VIEW, url);
+                getContext().startActivity(openIntent);
+            } catch (ActivityNotFoundException e) {
+                // TODO - trigger an event
+            }
+            return true;
+        }
+        return false;
+    }
+
+    private boolean isNewBinary() {
+        String versionCode = "";
+        String versionName = "";
+        SharedPreferences prefs = getContext()
+            .getSharedPreferences(com.getcapacitor.plugin.WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE);
+        String lastVersionCode = prefs.getString(LAST_BINARY_VERSION_CODE, null);
+        String lastVersionName = prefs.getString(LAST_BINARY_VERSION_NAME, null);
+
+        try {
+            PackageManager pm = getContext().getPackageManager();
+            PackageInfo pInfo = InternalUtils.getPackageInfo(pm, getContext().getPackageName());
+            versionCode = Integer.toString((int) PackageInfoCompat.getLongVersionCode(pInfo));
+            versionName = pInfo.versionName;
+        } catch (Exception ex) {
+            Logger.error("Unable to get package info", ex);
+        }
+
+        if (!versionCode.equals(lastVersionCode) || !versionName.equals(lastVersionName)) {
+            SharedPreferences.Editor editor = prefs.edit();
+            editor.putString(LAST_BINARY_VERSION_CODE, versionCode);
+            editor.putString(LAST_BINARY_VERSION_NAME, versionName);
+            editor.putString(com.getcapacitor.plugin.WebView.CAP_SERVER_PATH, "");
+            editor.apply();
+            return true;
+        }
+        return false;
+    }
+
+    public boolean isDeployDisabled() {
+        return preferences.getBoolean("DisableDeploy", false);
+    }
+
+    public boolean shouldKeepRunning() {
+        return preferences.getBoolean("KeepRunning", true);
+    }
+
+    public void handleAppUrlLoadError(Exception ex) {
+        if (ex instanceof SocketTimeoutException) {
+            Logger.error(
+                "Unable to load app. Ensure the server is running at " +
+                appUrl +
+                ", or modify the " +
+                "appUrl setting in capacitor.config.json (make sure to npx cap copy after to commit changes).",
+                ex
+            );
+        }
+    }
+
+    public boolean isDevMode() {
+        return (getActivity().getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
+    }
+
+    protected void setCordovaWebView(CordovaWebView cordovaWebView) {
+        this.cordovaWebView = cordovaWebView;
+    }
+
+    /**
+     * Get the Context for the App
+     * @return
+     */
+    public Context getContext() {
+        return this.context;
+    }
+
+    /**
+     * Get the activity for the app
+     * @return
+     */
+    public AppCompatActivity getActivity() {
+        return this.context;
+    }
+
+    /**
+     * Get the fragment for the app, if applicable. This will likely be null unless Capacitor
+     * is being used embedded in a Native Android app.
+     *
+     * @return The fragment containing the Capacitor WebView.
+     */
+    public Fragment getFragment() {
+        return this.fragment;
+    }
+
+    /**
+     * Get the core WebView under Capacitor's control
+     * @return
+     */
+    public WebView getWebView() {
+        return this.webView;
+    }
+
+    /**
+     * Get the URI that was used to launch the app (if any)
+     * @return
+     */
+    public Uri getIntentUri() {
+        return intentUri;
+    }
+
+    /**
+     * Get scheme that is used to serve content
+     * @return
+     */
+    public String getScheme() {
+        return this.config.getAndroidScheme();
+    }
+
+    /**
+     * Get host name that is used to serve content
+     * @return
+     */
+    public String getHost() {
+        return this.config.getHostname();
+    }
+
+    /**
+     * Get the server url that is used to serve content
+     * @return
+     */
+    public String getServerUrl() {
+        return this.config.getServerUrl();
+    }
+
+    public String getErrorUrl() {
+        String errorPath = this.config.getErrorPath();
+
+        if (errorPath != null && !errorPath.trim().isEmpty()) {
+            String authority = this.getHost();
+            String scheme = this.getScheme();
+
+            String localUrl = scheme + "://" + authority;
+
+            return localUrl + "/" + errorPath;
+        }
+
+        return null;
+    }
+
+    public String getAppUrl() {
+        return appUrl;
+    }
+
+    public CapConfig getConfig() {
+        return this.config;
+    }
+
+    public void reset() {
+        savedCalls = new HashMap<>();
+    }
+
+    /**
+     * Initialize the WebView, setting required flags
+     */
+    @SuppressLint("SetJavaScriptEnabled")
+    private void initWebView() {
+        WebSettings settings = webView.getSettings();
+        settings.setJavaScriptEnabled(true);
+        settings.setDomStorageEnabled(true);
+        settings.setGeolocationEnabled(true);
+        settings.setDatabaseEnabled(true);
+        settings.setMediaPlaybackRequiresUserGesture(false);
+        settings.setJavaScriptCanOpenWindowsAutomatically(true);
+        if (this.config.isMixedContentAllowed()) {
+            settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
+        }
+
+        String appendUserAgent = this.config.getAppendedUserAgentString();
+        if (appendUserAgent != null) {
+            String defaultUserAgent = settings.getUserAgentString();
+            settings.setUserAgentString(defaultUserAgent + " " + appendUserAgent);
+        }
+        String overrideUserAgent = this.config.getOverriddenUserAgentString();
+        if (overrideUserAgent != null) {
+            settings.setUserAgentString(overrideUserAgent);
+        }
+
+        String backgroundColor = this.config.getBackgroundColor();
+        try {
+            if (backgroundColor != null) {
+                webView.setBackgroundColor(WebColor.parseColor(backgroundColor));
+            }
+        } catch (IllegalArgumentException ex) {
+            Logger.debug("WebView background color not applied");
+        }
+
+        if (config.isInitialFocus()) {
+            webView.requestFocusFromTouch();
+        }
+
+        WebView.setWebContentsDebuggingEnabled(this.config.isWebContentsDebuggingEnabled());
+
+        appUrlConfig = this.getServerUrl();
+        String authority = this.getHost();
+        authorities.add(authority);
+        String scheme = this.getScheme();
+
+        localUrl = scheme + "://" + authority;
+
+        if (appUrlConfig != null) {
+            try {
+                URL appUrlObject = new URL(appUrlConfig);
+                authorities.add(appUrlObject.getAuthority());
+            } catch (Exception ex) {
+                Logger.error("Provided server url is invalid: " + ex.getMessage());
+                return;
+            }
+            localUrl = appUrlConfig;
+            appUrl = appUrlConfig;
+        } else {
+            appUrl = localUrl;
+            // custom URL schemes requires path ending with /
+            if (!scheme.equals(Bridge.CAPACITOR_HTTP_SCHEME) && !scheme.equals(CAPACITOR_HTTPS_SCHEME)) {
+                appUrl += "/";
+            }
+        }
+
+        String appUrlPath = this.config.getStartPath();
+        if (appUrlPath != null && !appUrlPath.trim().isEmpty()) {
+            appUrl += appUrlPath;
+        }
+    }
+
+    /**
+     * Register our core Plugin APIs
+     */
+    private void registerAllPlugins() {
+        this.registerPlugin(com.getcapacitor.plugin.CapacitorCookies.class);
+        this.registerPlugin(com.getcapacitor.plugin.WebView.class);
+        this.registerPlugin(com.getcapacitor.plugin.CapacitorHttp.class);
+
+        for (Class<? extends Plugin> pluginClass : this.initialPlugins) {
+            this.registerPlugin(pluginClass);
+        }
+
+        for (Plugin plugin : pluginInstances) {
+            registerPluginInstance(plugin);
+        }
+    }
+
+    /**
+     * Register additional plugins
+     * @param pluginClasses the plugins to register
+     */
+    public void registerPlugins(Class<? extends Plugin>[] pluginClasses) {
+        for (Class<? extends Plugin> plugin : pluginClasses) {
+            this.registerPlugin(plugin);
+        }
+    }
+
+    public void registerPluginInstances(Plugin[] pluginInstances) {
+        for (Plugin plugin : pluginInstances) {
+            this.registerPluginInstance(plugin);
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    private String getLegacyPluginName(Class<? extends Plugin> pluginClass) {
+        NativePlugin legacyPluginAnnotation = pluginClass.getAnnotation(NativePlugin.class);
+        if (legacyPluginAnnotation == null) {
+            Logger.error("Plugin doesn't have the @CapacitorPlugin annotation. Please add it");
+            return null;
+        }
+
+        return legacyPluginAnnotation.name();
+    }
+
+    /**
+     * Register a plugin class
+     * @param pluginClass a class inheriting from Plugin
+     */
+    public void registerPlugin(Class<? extends Plugin> pluginClass) {
+        String pluginId = pluginId(pluginClass);
+        if (pluginId == null) return;
+
+        try {
+            this.plugins.put(pluginId, new PluginHandle(this, pluginClass));
+        } catch (InvalidPluginException ex) {
+            logInvalidPluginException(pluginClass);
+        } catch (PluginLoadException ex) {
+            logPluginLoadException(pluginClass, ex);
+        }
+    }
+
+    public void registerPluginInstance(Plugin plugin) {
+        Class<? extends Plugin> clazz = plugin.getClass();
+        String pluginId = pluginId(clazz);
+        if (pluginId == null) return;
+
+        try {
+            this.plugins.put(pluginId, new PluginHandle(this, plugin));
+        } catch (InvalidPluginException ex) {
+            logInvalidPluginException(clazz);
+        }
+    }
+
+    private String pluginId(Class<? extends Plugin> clazz) {
+        String pluginName = pluginName(clazz);
+        String pluginId = clazz.getSimpleName();
+        if (pluginName == null) return null;
+
+        if (!pluginName.equals("")) {
+            pluginId = pluginName;
+        }
+        Logger.debug("Registering plugin instance: " + pluginId);
+        return pluginId;
+    }
+
+    private String pluginName(Class<? extends Plugin> clazz) {
+        String pluginName;
+        CapacitorPlugin pluginAnnotation = clazz.getAnnotation(CapacitorPlugin.class);
+        if (pluginAnnotation == null) {
+            pluginName = this.getLegacyPluginName(clazz);
+        } else {
+            pluginName = pluginAnnotation.name();
+        }
+
+        return pluginName;
+    }
+
+    private void logInvalidPluginException(Class<? extends Plugin> clazz) {
+        Logger.error(
+            "NativePlugin " +
+            clazz.getName() +
+            " is invalid. Ensure the @CapacitorPlugin annotation exists on the plugin class and" +
+            " the class extends Plugin"
+        );
+    }
+
+    private void logPluginLoadException(Class<? extends Plugin> clazz, Exception ex) {
+        Logger.error("NativePlugin " + clazz.getName() + " failed to load", ex);
+    }
+
+    public PluginHandle getPlugin(String pluginId) {
+        return this.plugins.get(pluginId);
+    }
+
+    /**
+     * Find the plugin handle that responds to the given request code. This will
+     * fire after certain Android OS intent results/permission checks/etc.
+     * @param requestCode
+     * @return
+     */
+    @Deprecated
+    @SuppressWarnings("deprecation")
+    public PluginHandle getPluginWithRequestCode(int requestCode) {
+        for (PluginHandle handle : this.plugins.values()) {
+            int[] requestCodes;
+
+            CapacitorPlugin pluginAnnotation = handle.getPluginAnnotation();
+            if (pluginAnnotation == null) {
+                // Check for legacy plugin annotation, @NativePlugin
+                NativePlugin legacyPluginAnnotation = handle.getLegacyPluginAnnotation();
+                if (legacyPluginAnnotation == null) {
+                    continue;
+                }
+
+                if (legacyPluginAnnotation.permissionRequestCode() == requestCode) {
+                    return handle;
+                }
+
+                requestCodes = legacyPluginAnnotation.requestCodes();
+
+                for (int rc : requestCodes) {
+                    if (rc == requestCode) {
+                        return handle;
+                    }
+                }
+            } else {
+                requestCodes = pluginAnnotation.requestCodes();
+
+                for (int rc : requestCodes) {
+                    if (rc == requestCode) {
+                        return handle;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Call a method on a plugin.
+     * @param pluginId the plugin id to use to lookup the plugin handle
+     * @param methodName the name of the method to call
+     * @param call the call object to pass to the method
+     */
+    public void callPluginMethod(String pluginId, final String methodName, final PluginCall call) {
+        try {
+            final PluginHandle plugin = this.getPlugin(pluginId);
+
+            if (plugin == null) {
+                Logger.error("unable to find plugin : " + pluginId);
+                call.errorCallback("unable to find plugin : " + pluginId);
+                return;
+            }
+
+            if (Logger.shouldLog()) {
+                Logger.verbose(
+                    "callback: " +
+                    call.getCallbackId() +
+                    ", pluginId: " +
+                    plugin.getId() +
+                    ", methodName: " +
+                    methodName +
+                    ", methodData: " +
+                    call.getData().toString()
+                );
+            }
+
+            Runnable currentThreadTask = () -> {
+                try {
+                    plugin.invoke(methodName, call);
+
+                    if (call.isKeptAlive()) {
+                        saveCall(call);
+                    }
+                } catch (PluginLoadException | InvalidPluginMethodException ex) {
+                    Logger.error("Unable to execute plugin method", ex);
+                } catch (Exception ex) {
+                    Logger.error("Serious error executing plugin", ex);
+                    throw new RuntimeException(ex);
+                }
+            };
+
+            taskHandler.post(currentThreadTask);
+        } catch (Exception ex) {
+            Logger.error(Logger.tags("callPluginMethod"), "error : " + ex, null);
+            call.errorCallback(ex.toString());
+        }
+    }
+
+    /**
+     * Evaluate JavaScript in the web view. This method
+     * executes on the main thread automatically.
+     * @param js the JS to execute
+     * @param callback an optional ValueCallback that will synchronously receive a value
+     *                 after calling the JS
+     */
+    public void eval(final String js, final ValueCallback<String> callback) {
+        Handler mainHandler = new Handler(context.getMainLooper());
+        mainHandler.post(() -> webView.evaluateJavascript(js, callback));
+    }
+
+    public void logToJs(final String message, final String level) {
+        eval("window.Capacitor.logJs(\"" + message + "\", \"" + level + "\")", null);
+    }
+
+    public void logToJs(final String message) {
+        logToJs(message, "log");
+    }
+
+    public void triggerJSEvent(final String eventName, final String target) {
+        eval("window.Capacitor.triggerEvent(\"" + eventName + "\", \"" + target + "\")", s -> {});
+    }
+
+    public void triggerJSEvent(final String eventName, final String target, final String data) {
+        eval("window.Capacitor.triggerEvent(\"" + eventName + "\", \"" + target + "\", " + data + ")", s -> {});
+    }
+
+    public void triggerWindowJSEvent(final String eventName) {
+        this.triggerJSEvent(eventName, "window");
+    }
+
+    public void triggerWindowJSEvent(final String eventName, final String data) {
+        this.triggerJSEvent(eventName, "window", data);
+    }
+
+    public void triggerDocumentJSEvent(final String eventName) {
+        this.triggerJSEvent(eventName, "document");
+    }
+
+    public void triggerDocumentJSEvent(final String eventName, final String data) {
+        this.triggerJSEvent(eventName, "document", data);
+    }
+
+    public void execute(Runnable runnable) {
+        taskHandler.post(runnable);
+    }
+
+    public void executeOnMainThread(Runnable runnable) {
+        Handler mainHandler = new Handler(context.getMainLooper());
+
+        mainHandler.post(runnable);
+    }
+
+    /**
+     * Retain a call between plugin invocations
+     * @param call
+     */
+    public void saveCall(PluginCall call) {
+        this.savedCalls.put(call.getCallbackId(), call);
+    }
+
+    /**
+     * Get a retained plugin call
+     * @param callbackId the callbackId to use to lookup the call with
+     * @return the stored call
+     */
+    public PluginCall getSavedCall(String callbackId) {
+        if (callbackId == null) {
+            return null;
+        }
+
+        return this.savedCalls.get(callbackId);
+    }
+
+    PluginCall getPluginCallForLastActivity() {
+        PluginCall pluginCallForLastActivity = this.pluginCallForLastActivity;
+        this.pluginCallForLastActivity = null;
+        return pluginCallForLastActivity;
+    }
+
+    void setPluginCallForLastActivity(PluginCall pluginCallForLastActivity) {
+        this.pluginCallForLastActivity = pluginCallForLastActivity;
+    }
+
+    /**
+     * Release a retained call
+     * @param call a call to release
+     */
+    public void releaseCall(PluginCall call) {
+        releaseCall(call.getCallbackId());
+    }
+
+    /**
+     * Release a retained call by its ID
+     * @param callbackId an ID of a callback to release
+     */
+    public void releaseCall(String callbackId) {
+        this.savedCalls.remove(callbackId);
+    }
+
+    /**
+     * Removes the earliest saved call prior to a permissions request for a given plugin and
+     * returns it.
+     *
+     * @return The saved plugin call
+     */
+    protected PluginCall getPermissionCall(String pluginId) {
+        LinkedList<String> permissionCallIds = this.savedPermissionCallIds.get(pluginId);
+        String savedCallId = null;
+        if (permissionCallIds != null) {
+            savedCallId = permissionCallIds.poll();
+        }
+
+        return getSavedCall(savedCallId);
+    }
+
+    /**
+     * Save a call to be retrieved after requesting permissions. Calls are saved in order.
+     *
+     * @param call The plugin call to save.
+     */
+    protected void savePermissionCall(PluginCall call) {
+        if (call != null) {
+            if (!savedPermissionCallIds.containsKey(call.getPluginId())) {
+                savedPermissionCallIds.put(call.getPluginId(), new LinkedList<>());
+            }
+
+            savedPermissionCallIds.get(call.getPluginId()).add(call.getCallbackId());
+            saveCall(call);
+        }
+    }
+
+    /**
+     * Register an Activity Result Launcher to the containing Fragment or Activity.
+     *
+     * @param contract A contract specifying that an activity can be called with an input of
+     *                 type I and produce an output of type O.
+     * @param callback The callback run on Activity Result.
+     * @return A registered Activity Result Launcher.
+     */
+    public <I, O> ActivityResultLauncher<I> registerForActivityResult(
+        @NonNull final ActivityResultContract<I, O> contract,
+        @NonNull final ActivityResultCallback<O> callback
+    ) {
+        if (fragment != null) {
+            return fragment.registerForActivityResult(contract, callback);
+        } else {
+            return context.registerForActivityResult(contract, callback);
+        }
+    }
+
+    /**
+     * Build the JSInjector that will be used to inject JS into files served to the app,
+     * to ensure that Capacitor's JS and the JS for all the plugins is loaded each time.
+     */
+    private JSInjector getJSInjector() {
+        try {
+            String globalJS = JSExport.getGlobalJS(context, config.isLoggingEnabled(), isDevMode());
+            String bridgeJS = JSExport.getBridgeJS(context);
+            String pluginJS = JSExport.getPluginJS(plugins.values());
+            String cordovaJS = JSExport.getCordovaJS(context);
+            String cordovaPluginsJS = JSExport.getCordovaPluginJS(context);
+            String cordovaPluginsFileJS = JSExport.getCordovaPluginsFileJS(context);
+            String localUrlJS = "window.WEBVIEW_SERVER_URL = '" + localUrl + "';";
+
+            return new JSInjector(globalJS, bridgeJS, pluginJS, cordovaJS, cordovaPluginsJS, cordovaPluginsFileJS, localUrlJS);
+        } catch (Exception ex) {
+            Logger.error("Unable to export Capacitor JS. App will not function!", ex);
+        }
+        return null;
+    }
+
+    /**
+     * Restore any saved bundle state data
+     * @param savedInstanceState
+     */
+    public void restoreInstanceState(Bundle savedInstanceState) {
+        String lastPluginId = savedInstanceState.getString(BUNDLE_LAST_PLUGIN_ID_KEY);
+        String lastPluginCallMethod = savedInstanceState.getString(BUNDLE_LAST_PLUGIN_CALL_METHOD_NAME_KEY);
+        String lastOptionsJson = savedInstanceState.getString(BUNDLE_PLUGIN_CALL_OPTIONS_SAVED_KEY);
+
+        if (lastPluginId != null) {
+            // If we have JSON blob saved, create a new plugin call with the original options
+            if (lastOptionsJson != null) {
+                try {
+                    JSObject options = new JSObject(lastOptionsJson);
+
+                    pluginCallForLastActivity =
+                        new PluginCall(msgHandler, lastPluginId, PluginCall.CALLBACK_ID_DANGLING, lastPluginCallMethod, options);
+                } catch (JSONException ex) {
+                    Logger.error("Unable to restore plugin call, unable to parse persisted JSON object", ex);
+                }
+            }
+
+            // Let the plugin restore any state it needs
+            Bundle bundleData = savedInstanceState.getBundle(BUNDLE_PLUGIN_CALL_BUNDLE_KEY);
+            PluginHandle lastPlugin = getPlugin(lastPluginId);
+            if (bundleData != null && lastPlugin != null) {
+                lastPlugin.getInstance().restoreState(bundleData);
+            } else {
+                Logger.error("Unable to restore last plugin call");
+            }
+        }
+    }
+
+    public void saveInstanceState(Bundle outState) {
+        Logger.debug("Saving instance state!");
+
+        // If there was a last PluginCall for a started activity, we need to
+        // persist it so we can load it again in case our app gets terminated
+        if (pluginCallForLastActivity != null) {
+            PluginCall call = pluginCallForLastActivity;
+            PluginHandle handle = getPlugin(call.getPluginId());
+
+            if (handle != null) {
+                Bundle bundle = handle.getInstance().saveInstanceState();
+                if (bundle != null) {
+                    outState.putString(BUNDLE_LAST_PLUGIN_ID_KEY, call.getPluginId());
+                    outState.putString(BUNDLE_LAST_PLUGIN_CALL_METHOD_NAME_KEY, call.getMethodName());
+                    outState.putString(BUNDLE_PLUGIN_CALL_OPTIONS_SAVED_KEY, call.getData().toString());
+                    outState.putBundle(BUNDLE_PLUGIN_CALL_BUNDLE_KEY, bundle);
+                } else {
+                    Logger.error("Couldn't save last " + call.getPluginId() + "'s Plugin " + call.getMethodName() + " call");
+                }
+            }
+        }
+    }
+
+    @Deprecated
+    @SuppressWarnings("deprecation")
+    public void startActivityForPluginWithResult(PluginCall call, Intent intent, int requestCode) {
+        Logger.debug("Starting activity for result");
+
+        pluginCallForLastActivity = call;
+
+        getActivity().startActivityForResult(intent, requestCode);
+    }
+
+    /**
+     * Check for legacy Capacitor or Cordova plugins that may have registered to handle a permission
+     * request, and handle them if so. If not handled, false is returned.
+     *
+     * @param requestCode the code that was requested
+     * @param permissions the permissions requested
+     * @param grantResults the set of granted/denied permissions
+     * @return true if permission code was handled by a plugin explicitly, false if not
+     */
+    @SuppressWarnings("deprecation")
+    boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+        PluginHandle plugin = getPluginWithRequestCode(requestCode);
+
+        if (plugin == null) {
+            boolean permissionHandled = false;
+            Logger.debug("Unable to find a Capacitor plugin to handle permission requestCode, trying Cordova plugins " + requestCode);
+            try {
+                permissionHandled = cordovaInterface.handlePermissionResult(requestCode, permissions, grantResults);
+            } catch (JSONException e) {
+                Logger.debug("Error on Cordova plugin permissions request " + e.getMessage());
+            }
+            return permissionHandled;
+        }
+
+        // Call deprecated method if using deprecated NativePlugin annotation
+        if (plugin.getPluginAnnotation() == null) {
+            plugin.getInstance().handleRequestPermissionsResult(requestCode, permissions, grantResults);
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Saves permission states and rejects if permissions were not correctly defined in
+     * the AndroidManifest.xml file.
+     *
+     * @param plugin
+     * @param savedCall
+     * @param permissions
+     * @return true if permissions were saved and defined correctly, false if not
+     */
+    protected boolean validatePermissions(Plugin plugin, PluginCall savedCall, Map<String, Boolean> permissions) {
+        SharedPreferences prefs = getContext().getSharedPreferences(PERMISSION_PREFS_NAME, Activity.MODE_PRIVATE);
+
+        for (Map.Entry<String, Boolean> permission : permissions.entrySet()) {
+            String permString = permission.getKey();
+            boolean isGranted = permission.getValue();
+
+            if (isGranted) {
+                // Permission granted. If previously denied, remove cached state
+                String state = prefs.getString(permString, null);
+
+                if (state != null) {
+                    SharedPreferences.Editor editor = prefs.edit();
+                    editor.remove(permString);
+                    editor.apply();
+                }
+            } else {
+                SharedPreferences.Editor editor = prefs.edit();
+
+                if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), permString)) {
+                    // Permission denied, can prompt again with rationale
+                    editor.putString(permString, PermissionState.PROMPT_WITH_RATIONALE.toString());
+                } else {
+                    // Permission denied permanently, store this state for future reference
+                    editor.putString(permString, PermissionState.DENIED.toString());
+                }
+
+                editor.apply();
+            }
+        }
+
+        String[] permStrings = permissions.keySet().toArray(new String[0]);
+
+        if (!PermissionHelper.hasDefinedPermissions(getContext(), permStrings)) {
+            StringBuilder builder = new StringBuilder();
+            builder.append("Missing the following permissions in AndroidManifest.xml:\n");
+            String[] missing = PermissionHelper.getUndefinedPermissions(getContext(), permStrings);
+            for (String perm : missing) {
+                builder.append(perm + "\n");
+            }
+            savedCall.reject(builder.toString());
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Helper to check all permissions and see the current states of each permission.
+     *
+     * @since 3.0.0
+     * @return A mapping of permission aliases to the associated granted status.
+     */
+    protected Map<String, PermissionState> getPermissionStates(Plugin plugin) {
+        Map<String, PermissionState> permissionsResults = new HashMap<>();
+        CapacitorPlugin annotation = plugin.getPluginHandle().getPluginAnnotation();
+        for (Permission perm : annotation.permissions()) {
+            // If a permission is defined with no permission constants, return GRANTED for it.
+            // Otherwise, get its true state.
+            if (perm.strings().length == 0 || (perm.strings().length == 1 && perm.strings()[0].isEmpty())) {
+                String key = perm.alias();
+                if (!key.isEmpty()) {
+                    PermissionState existingResult = permissionsResults.get(key);
+
+                    // auto set permission state to GRANTED if the alias is empty.
+                    if (existingResult == null) {
+                        permissionsResults.put(key, PermissionState.GRANTED);
+                    }
+                }
+            } else {
+                for (String permString : perm.strings()) {
+                    String key = perm.alias().isEmpty() ? permString : perm.alias();
+                    PermissionState permissionStatus;
+                    if (ActivityCompat.checkSelfPermission(this.getContext(), permString) == PackageManager.PERMISSION_GRANTED) {
+                        permissionStatus = PermissionState.GRANTED;
+                    } else {
+                        permissionStatus = PermissionState.PROMPT;
+
+                        // Check if there is a cached permission state for the "Never ask again" state
+                        SharedPreferences prefs = getContext().getSharedPreferences(PERMISSION_PREFS_NAME, Activity.MODE_PRIVATE);
+                        String state = prefs.getString(permString, null);
+
+                        if (state != null) {
+                            permissionStatus = PermissionState.byState(state);
+                        }
+                    }
+
+                    PermissionState existingResult = permissionsResults.get(key);
+
+                    // multiple permissions with the same alias must all be true, otherwise all false.
+                    if (existingResult == null || existingResult == PermissionState.GRANTED) {
+                        permissionsResults.put(key, permissionStatus);
+                    }
+                }
+            }
+        }
+
+        return permissionsResults;
+    }
+
+    /**
+     * Handle an activity result and pass it to a plugin that has indicated it wants to
+     * handle the result.
+     * @param requestCode
+     * @param resultCode
+     * @param data
+     */
+    @SuppressWarnings("deprecation")
+    boolean onActivityResult(int requestCode, int resultCode, Intent data) {
+        PluginHandle plugin = getPluginWithRequestCode(requestCode);
+
+        if (plugin == null || plugin.getInstance() == null) {
+            Logger.debug("Unable to find a Capacitor plugin to handle requestCode, trying Cordova plugins " + requestCode);
+            return cordovaInterface.onActivityResult(requestCode, resultCode, data);
+        }
+
+        // deprecated, to be removed
+        PluginCall lastCall = plugin.getInstance().getSavedCall();
+
+        // If we don't have a saved last call (because our app was killed and restarted, for example),
+        // Then we should see if we have any saved plugin call information and generate a new,
+        // "dangling" plugin call (a plugin call that doesn't have a corresponding web callback)
+        // and then send that to the plugin
+        if (lastCall == null && pluginCallForLastActivity != null) {
+            plugin.getInstance().saveCall(pluginCallForLastActivity);
+        }
+
+        plugin.getInstance().handleOnActivityResult(requestCode, resultCode, data);
+
+        // Clear the plugin call we may have re-hydrated on app launch
+        pluginCallForLastActivity = null;
+
+        return true;
+    }
+
+    /**
+     * Handle an onNewIntent lifecycle event and notify the plugins
+     * @param intent
+     */
+    public void onNewIntent(Intent intent) {
+        for (PluginHandle plugin : plugins.values()) {
+            plugin.getInstance().handleOnNewIntent(intent);
+        }
+
+        if (cordovaWebView != null) {
+            cordovaWebView.onNewIntent(intent);
+        }
+    }
+
+    /**
+     * Handle an onConfigurationChanged event and notify the plugins
+     * @param newConfig
+     */
+    public void onConfigurationChanged(Configuration newConfig) {
+        for (PluginHandle plugin : plugins.values()) {
+            plugin.getInstance().handleOnConfigurationChanged(newConfig);
+        }
+    }
+
+    /**
+     * Handle onRestart lifecycle event and notify the plugins
+     */
+    public void onRestart() {
+        for (PluginHandle plugin : plugins.values()) {
+            plugin.getInstance().handleOnRestart();
+        }
+    }
+
+    /**
+     * Handle onStart lifecycle event and notify the plugins
+     */
+    public void onStart() {
+        for (PluginHandle plugin : plugins.values()) {
+            plugin.getInstance().handleOnStart();
+        }
+
+        if (cordovaWebView != null) {
+            cordovaWebView.handleStart();
+        }
+    }
+
+    /**
+     * Handle onResume lifecycle event and notify the plugins
+     */
+    public void onResume() {
+        for (PluginHandle plugin : plugins.values()) {
+            plugin.getInstance().handleOnResume();
+        }
+
+        if (cordovaWebView != null) {
+            cordovaWebView.handleResume(this.shouldKeepRunning());
+        }
+    }
+
+    /**
+     * Handle onPause lifecycle event and notify the plugins
+     */
+    public void onPause() {
+        for (PluginHandle plugin : plugins.values()) {
+            plugin.getInstance().handleOnPause();
+        }
+
+        if (cordovaWebView != null) {
+            boolean keepRunning = this.shouldKeepRunning() || cordovaInterface.getActivityResultCallback() != null;
+            cordovaWebView.handlePause(keepRunning);
+        }
+    }
+
+    /**
+     * Handle onStop lifecycle event and notify the plugins
+     */
+    public void onStop() {
+        for (PluginHandle plugin : plugins.values()) {
+            plugin.getInstance().handleOnStop();
+        }
+
+        if (cordovaWebView != null) {
+            cordovaWebView.handleStop();
+        }
+    }
+
+    /**
+     * Handle onDestroy lifecycle event and notify the plugins
+     */
+    public void onDestroy() {
+        for (PluginHandle plugin : plugins.values()) {
+            plugin.getInstance().handleOnDestroy();
+        }
+
+        handlerThread.quitSafely();
+
+        if (cordovaWebView != null) {
+            cordovaWebView.handleDestroy();
+        }
+    }
+
+    /**
+     * Handle onDetachedFromWindow lifecycle event
+     */
+    public void onDetachedFromWindow() {
+        webView.removeAllViews();
+        webView.destroy();
+    }
+
+    public String getServerBasePath() {
+        return this.localServer.getBasePath();
+    }
+
+    /**
+     * Tell the local server to load files from the given
+     * file path instead of the assets path.
+     * @param path
+     */
+    public void setServerBasePath(String path) {
+        localServer.hostFiles(path);
+        webView.post(() -> webView.loadUrl(appUrl));
+    }
+
+    /**
+     * Tell the local server to load files from the given
+     * asset path.
+     * @param path
+     */
+    public void setServerAssetPath(String path) {
+        localServer.hostAssets(path);
+        webView.post(() -> webView.loadUrl(appUrl));
+    }
+
+    /**
+     * Reload the WebView
+     */
+    public void reload() {
+        webView.post(() -> webView.loadUrl(appUrl));
+    }
+
+    public String getLocalUrl() {
+        return localUrl;
+    }
+
+    public WebViewLocalServer getLocalServer() {
+        return localServer;
+    }
+
+    public HostMask getAppAllowNavigationMask() {
+        return appAllowNavigationMask;
+    }
+
+    public Set<String> getAllowedOriginRules() {
+        return allowedOriginRules;
+    }
+
+    public BridgeWebViewClient getWebViewClient() {
+        return this.webViewClient;
+    }
+
+    public void setWebViewClient(BridgeWebViewClient client) {
+        this.webViewClient = client;
+        webView.setWebViewClient(client);
+    }
+
+    List<WebViewListener> getWebViewListeners() {
+        return webViewListeners;
+    }
+
+    void setWebViewListeners(List<WebViewListener> webViewListeners) {
+        this.webViewListeners = webViewListeners;
+    }
+
+    RouteProcessor getRouteProcessor() {
+        return routeProcessor;
+    }
+
+    void setRouteProcessor(RouteProcessor routeProcessor) {
+        this.routeProcessor = routeProcessor;
+    }
+
+    ServerPath getServerPath() {
+        return serverPath;
+    }
+
+    /**
+     * Add a listener that the WebViewClient can trigger on certain events.
+     * @param webViewListener A {@link WebViewListener} to add.
+     */
+    public void addWebViewListener(WebViewListener webViewListener) {
+        webViewListeners.add(webViewListener);
+    }
+
+    /**
+     * Remove a listener that the WebViewClient triggers on certain events.
+     * @param webViewListener A {@link WebViewListener} to remove.
+     */
+    public void removeWebViewListener(WebViewListener webViewListener) {
+        webViewListeners.remove(webViewListener);
+    }
+
+    public static class Builder {
+
+        private Bundle instanceState = null;
+        private CapConfig config = null;
+        private List<Class<? extends Plugin>> plugins = new ArrayList<>();
+        private List<Plugin> pluginInstances = new ArrayList<>();
+        private AppCompatActivity activity;
+        private Fragment fragment;
+        private RouteProcessor routeProcessor;
+        private final List<WebViewListener> webViewListeners = new ArrayList<>();
+        private ServerPath serverPath;
+
+        public Builder(AppCompatActivity activity) {
+            this.activity = activity;
+        }
+
+        public Builder(Fragment fragment) {
+            this.activity = (AppCompatActivity) fragment.getActivity();
+            this.fragment = fragment;
+        }
+
+        public Builder setInstanceState(Bundle instanceState) {
+            this.instanceState = instanceState;
+            return this;
+        }
+
+        public Builder setConfig(CapConfig config) {
+            this.config = config;
+            return this;
+        }
+
+        public Builder setPlugins(List<Class<? extends Plugin>> plugins) {
+            this.plugins = plugins;
+            return this;
+        }
+
+        public Builder addPlugin(Class<? extends Plugin> plugin) {
+            this.plugins.add(plugin);
+            return this;
+        }
+
+        public Builder addPlugins(List<Class<? extends Plugin>> plugins) {
+            for (Class<? extends Plugin> cls : plugins) {
+                this.addPlugin(cls);
+            }
+
+            return this;
+        }
+
+        public Builder addPluginInstance(Plugin plugin) {
+            this.pluginInstances.add(plugin);
+            return this;
+        }
+
+        public Builder addPluginInstances(List<Plugin> plugins) {
+            this.pluginInstances.addAll(plugins);
+            return this;
+        }
+
+        public Builder addWebViewListener(WebViewListener webViewListener) {
+            webViewListeners.add(webViewListener);
+            return this;
+        }
+
+        public Builder addWebViewListeners(List<WebViewListener> webViewListeners) {
+            for (WebViewListener listener : webViewListeners) {
+                this.addWebViewListener(listener);
+            }
+
+            return this;
+        }
+
+        public Builder setRouteProcessor(RouteProcessor routeProcessor) {
+            this.routeProcessor = routeProcessor;
+            return this;
+        }
+
+        public Builder setServerPath(ServerPath serverPath) {
+            this.serverPath = serverPath;
+            return this;
+        }
+
+        public Bridge create() {
+            // Cordova initialization
+            ConfigXmlParser parser = new ConfigXmlParser();
+            parser.parse(activity.getApplicationContext());
+            CordovaPreferences preferences = parser.getPreferences();
+            preferences.setPreferencesBundle(activity.getIntent().getExtras());
+            List<PluginEntry> pluginEntries = parser.getPluginEntries();
+
+            MockCordovaInterfaceImpl cordovaInterface = new MockCordovaInterfaceImpl(activity);
+            if (instanceState != null) {
+                cordovaInterface.restoreInstanceState(instanceState);
+            }
+
+            WebView webView = this.fragment != null ? fragment.getView().findViewById(R.id.webview) : activity.findViewById(R.id.webview);
+            MockCordovaWebViewImpl mockWebView = new MockCordovaWebViewImpl(activity.getApplicationContext());
+            mockWebView.init(cordovaInterface, pluginEntries, preferences, webView);
+            PluginManager pluginManager = mockWebView.getPluginManager();
+            cordovaInterface.onCordovaInit(pluginManager);
+
+            // Bridge initialization
+            Bridge bridge = new Bridge(
+                activity,
+                serverPath,
+                fragment,
+                webView,
+                plugins,
+                pluginInstances,
+                cordovaInterface,
+                pluginManager,
+                preferences,
+                config
+            );
+
+            if (webView instanceof CapacitorWebView) {
+                CapacitorWebView capacitorWebView = (CapacitorWebView) webView;
+                capacitorWebView.setBridge(bridge);
+            }
+
+            bridge.setCordovaWebView(mockWebView);
+            bridge.setWebViewListeners(webViewListeners);
+            bridge.setRouteProcessor(routeProcessor);
+
+            if (instanceState != null) {
+                bridge.restoreInstanceState(instanceState);
+            }
+
+            return bridge;
+        }
+    }
+}

+ 197 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/BridgeActivity.java

@@ -0,0 +1,197 @@
+package com.getcapacitor;
+
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import androidx.appcompat.app.AppCompatActivity;
+import com.getcapacitor.android.R;
+import java.util.ArrayList;
+import java.util.List;
+
+public class BridgeActivity extends AppCompatActivity {
+
+    protected Bridge bridge;
+    protected boolean keepRunning = true;
+    protected CapConfig config;
+
+    protected int activityDepth = 0;
+    protected List<Class<? extends Plugin>> initialPlugins = new ArrayList<>();
+    protected final Bridge.Builder bridgeBuilder = new Bridge.Builder(this);
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        bridgeBuilder.setInstanceState(savedInstanceState);
+        getApplication().setTheme(R.style.AppTheme_NoActionBar);
+        setTheme(R.style.AppTheme_NoActionBar);
+        setContentView(R.layout.bridge_layout_main);
+        PluginManager loader = new PluginManager(getAssets());
+
+        try {
+            bridgeBuilder.addPlugins(loader.loadPluginClasses());
+        } catch (PluginLoadException ex) {
+            Logger.error("Error loading plugins.", ex);
+        }
+
+        this.load();
+    }
+
+    protected void load() {
+        Logger.debug("Starting BridgeActivity");
+
+        bridge = bridgeBuilder.addPlugins(initialPlugins).setConfig(config).create();
+
+        this.keepRunning = bridge.shouldKeepRunning();
+        this.onNewIntent(getIntent());
+    }
+
+    public void registerPlugin(Class<? extends Plugin> plugin) {
+        bridgeBuilder.addPlugin(plugin);
+    }
+
+    public void registerPlugins(List<Class<? extends Plugin>> plugins) {
+        bridgeBuilder.addPlugins(plugins);
+    }
+
+    public Bridge getBridge() {
+        return this.bridge;
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        bridge.saveInstanceState(outState);
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        activityDepth++;
+        this.bridge.onStart();
+        Logger.debug("App started");
+    }
+
+    @Override
+    public void onRestart() {
+        super.onRestart();
+        this.bridge.onRestart();
+        Logger.debug("App restarted");
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        bridge.getApp().fireStatusChange(true);
+        this.bridge.onResume();
+        Logger.debug("App resumed");
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        this.bridge.onPause();
+        Logger.debug("App paused");
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+
+        activityDepth = Math.max(0, activityDepth - 1);
+        if (activityDepth == 0) {
+            bridge.getApp().fireStatusChange(false);
+        }
+
+        this.bridge.onStop();
+        Logger.debug("App stopped");
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        this.bridge.onDestroy();
+        Logger.debug("App destroyed");
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        this.bridge.onDetachedFromWindow();
+    }
+
+    /**
+     * Handles permission request results.
+     *
+     * Capacitor is backwards compatible such that plugins using legacy permission request codes
+     * may coexist with plugins using the AndroidX Activity v1.2 permission callback flow introduced
+     * in Capacitor 3.0.
+     *
+     * In this method, plugins are checked first for ownership of the legacy permission request code.
+     * If the {@link Bridge#onRequestPermissionsResult(int, String[], int[])} method indicates it has
+     * handled the permission, then the permission callback will be considered complete. Otherwise,
+     * the permission will be handled using the AndroidX Activity flow.
+     *
+     * @param requestCode the request code associated with the permission request
+     * @param permissions the Android permission strings requested
+     * @param grantResults the status result of the permission request
+     */
+    @Override
+    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+        if (this.bridge == null) {
+            return;
+        }
+
+        if (!bridge.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
+            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+        }
+    }
+
+    /**
+     * Handles activity results.
+     *
+     * Capacitor is backwards compatible such that plugins using legacy activity result codes
+     * may coexist with plugins using the AndroidX Activity v1.2 activity callback flow introduced
+     * in Capacitor 3.0.
+     *
+     * In this method, plugins are checked first for ownership of the legacy request code. If the
+     * {@link Bridge#onActivityResult(int, int, Intent)} method indicates it has handled the activity
+     * result, then the callback will be considered complete. Otherwise, the result will be handled
+     * using the AndroidX Activiy flow.
+     *
+     * @param requestCode the request code associated with the activity result
+     * @param resultCode the result code
+     * @param data any data included with the activity result
+     */
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (this.bridge == null) {
+            return;
+        }
+
+        if (!bridge.onActivityResult(requestCode, resultCode, data)) {
+            super.onActivityResult(requestCode, resultCode, data);
+        }
+    }
+
+    @Override
+    protected void onNewIntent(Intent intent) {
+        super.onNewIntent(intent);
+
+        if (this.bridge == null || intent == null) {
+            return;
+        }
+
+        this.bridge.onNewIntent(intent);
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+
+        if (this.bridge == null) {
+            return;
+        }
+
+        this.bridge.onConfigurationChanged(newConfig);
+    }
+}

+ 134 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/BridgeFragment.java

@@ -0,0 +1,134 @@
+package com.getcapacitor;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.fragment.app.Fragment;
+import com.getcapacitor.android.R;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A simple {@link Fragment} subclass.
+ * Use the {@link BridgeFragment#newInstance} factory method to
+ * create an instance of this fragment.
+ */
+public class BridgeFragment extends Fragment {
+
+    private static final String ARG_START_DIR = "startDir";
+
+    protected Bridge bridge;
+    protected boolean keepRunning = true;
+
+    private final List<Class<? extends Plugin>> initialPlugins = new ArrayList<>();
+    private CapConfig config = null;
+
+    private final List<WebViewListener> webViewListeners = new ArrayList<>();
+
+    public BridgeFragment() {
+        // Required empty public constructor
+    }
+
+    /**
+     * Use this factory method to create a new instance of
+     * this fragment using the provided parameters.
+     *
+     * @param startDir the directory to serve content from
+     * @return A new instance of fragment BridgeFragment.
+     */
+    public static BridgeFragment newInstance(String startDir) {
+        BridgeFragment fragment = new BridgeFragment();
+        Bundle args = new Bundle();
+        args.putString(ARG_START_DIR, startDir);
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    public void addPlugin(Class<? extends Plugin> plugin) {
+        this.initialPlugins.add(plugin);
+    }
+
+    public void setConfig(CapConfig config) {
+        this.config = config;
+    }
+
+    public Bridge getBridge() {
+        return bridge;
+    }
+
+    public void addWebViewListener(WebViewListener webViewListener) {
+        webViewListeners.add(webViewListener);
+    }
+
+    /**
+     * Load the WebView and create the Bridge
+     */
+    protected void load(Bundle savedInstanceState) {
+        Logger.debug("Loading Bridge with BridgeFragment");
+
+        Bundle args = getArguments();
+        String startDir = null;
+
+        if (args != null) {
+            startDir = getArguments().getString(ARG_START_DIR);
+        }
+
+        bridge =
+            new Bridge.Builder(this)
+                .setInstanceState(savedInstanceState)
+                .setPlugins(initialPlugins)
+                .setConfig(config)
+                .addWebViewListeners(webViewListeners)
+                .create();
+
+        if (startDir != null) {
+            bridge.setServerAssetPath(startDir);
+        }
+
+        this.keepRunning = bridge.shouldKeepRunning();
+    }
+
+    @Override
+    public void onInflate(Context context, AttributeSet attrs, Bundle savedInstanceState) {
+        super.onInflate(context, attrs, savedInstanceState);
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.bridge_fragment);
+        CharSequence c = a.getString(R.styleable.bridge_fragment_start_dir);
+
+        if (c != null) {
+            String startDir = c.toString();
+            Bundle args = new Bundle();
+            args.putString(ARG_START_DIR, startDir);
+            setArguments(args);
+        }
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        // Inflate the layout for this fragment
+        return inflater.inflate(R.layout.fragment_bridge, container, false);
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        this.load(savedInstanceState);
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (this.bridge != null) {
+            this.bridge.onDestroy();
+        }
+    }
+}

+ 510 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java

@@ -0,0 +1,510 @@
+package com.getcapacitor;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.view.View;
+import android.webkit.ConsoleMessage;
+import android.webkit.GeolocationPermissions;
+import android.webkit.JsPromptResult;
+import android.webkit.JsResult;
+import android.webkit.MimeTypeMap;
+import android.webkit.PermissionRequest;
+import android.webkit.ValueCallback;
+import android.webkit.WebChromeClient;
+import android.webkit.WebView;
+import android.widget.EditText;
+import androidx.activity.result.ActivityResult;
+import androidx.activity.result.ActivityResultCallback;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.core.content.FileProvider;
+import com.getcapacitor.util.PermissionHelper;
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * Custom WebChromeClient handler, required for showing dialogs, confirms, etc. in our
+ * WebView instance.
+ */
+public class BridgeWebChromeClient extends WebChromeClient {
+
+    private interface PermissionListener {
+        void onPermissionSelect(Boolean isGranted);
+    }
+
+    private interface ActivityResultListener {
+        void onActivityResult(ActivityResult result);
+    }
+
+    private ActivityResultLauncher permissionLauncher;
+    private ActivityResultLauncher activityLauncher;
+    private PermissionListener permissionListener;
+    private ActivityResultListener activityListener;
+
+    private Bridge bridge;
+
+    public BridgeWebChromeClient(Bridge bridge) {
+        this.bridge = bridge;
+
+        ActivityResultCallback<Map<String, Boolean>> permissionCallback = (Map<String, Boolean> isGranted) -> {
+            if (permissionListener != null) {
+                boolean granted = true;
+                for (Map.Entry<String, Boolean> permission : isGranted.entrySet()) {
+                    if (!permission.getValue()) granted = false;
+                }
+                permissionListener.onPermissionSelect(granted);
+            }
+        };
+
+        permissionLauncher = bridge.registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), permissionCallback);
+        activityLauncher =
+            bridge.registerForActivityResult(
+                new ActivityResultContracts.StartActivityForResult(),
+                result -> {
+                    if (activityListener != null) {
+                        activityListener.onActivityResult(result);
+                    }
+                }
+            );
+    }
+
+    /**
+     * Render web content in `view`.
+     *
+     * Both this method and {@link #onHideCustomView()} are required for
+     * rendering web content in full screen.
+     *
+     * @see <a href="https://developer.android.com/reference/android/webkit/WebChromeClient#onShowCustomView(android.view.View,%20android.webkit.WebChromeClient.CustomViewCallback)">onShowCustomView() docs</a>
+     */
+    @Override
+    public void onShowCustomView(View view, CustomViewCallback callback) {
+        callback.onCustomViewHidden();
+        super.onShowCustomView(view, callback);
+    }
+
+    /**
+     * Render web content in the original Web View again.
+     *
+     * Do not remove this method--@see #onShowCustomView(View, CustomViewCallback).
+     */
+    @Override
+    public void onHideCustomView() {
+        super.onHideCustomView();
+    }
+
+    @Override
+    public void onPermissionRequest(final PermissionRequest request) {
+        boolean isRequestPermissionRequired = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M;
+
+        List<String> permissionList = new ArrayList<>();
+        if (Arrays.asList(request.getResources()).contains("android.webkit.resource.VIDEO_CAPTURE")) {
+            permissionList.add(Manifest.permission.CAMERA);
+        }
+        if (Arrays.asList(request.getResources()).contains("android.webkit.resource.AUDIO_CAPTURE")) {
+            permissionList.add(Manifest.permission.MODIFY_AUDIO_SETTINGS);
+            permissionList.add(Manifest.permission.RECORD_AUDIO);
+        }
+        if (!permissionList.isEmpty() && isRequestPermissionRequired) {
+            String[] permissions = permissionList.toArray(new String[0]);
+            permissionListener =
+                isGranted -> {
+                    if (isGranted) {
+                        request.grant(request.getResources());
+                    } else {
+                        request.deny();
+                    }
+                };
+            permissionLauncher.launch(permissions);
+        } else {
+            request.grant(request.getResources());
+        }
+    }
+
+    /**
+     * Show the browser alert modal
+     * @param view
+     * @param url
+     * @param message
+     * @param result
+     * @return
+     */
+    @Override
+    public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
+        if (bridge.getActivity().isFinishing()) {
+            return true;
+        }
+
+        AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext());
+        builder
+            .setMessage(message)
+            .setPositiveButton(
+                "OK",
+                (dialog, buttonIndex) -> {
+                    dialog.dismiss();
+                    result.confirm();
+                }
+            )
+            .setOnCancelListener(
+                dialog -> {
+                    dialog.dismiss();
+                    result.cancel();
+                }
+            );
+
+        AlertDialog dialog = builder.create();
+
+        dialog.show();
+
+        return true;
+    }
+
+    /**
+     * Show the browser confirm modal
+     * @param view
+     * @param url
+     * @param message
+     * @param result
+     * @return
+     */
+    @Override
+    public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
+        if (bridge.getActivity().isFinishing()) {
+            return true;
+        }
+
+        final AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext());
+
+        builder
+            .setMessage(message)
+            .setPositiveButton(
+                "OK",
+                (dialog, buttonIndex) -> {
+                    dialog.dismiss();
+                    result.confirm();
+                }
+            )
+            .setNegativeButton(
+                "Cancel",
+                (dialog, buttonIndex) -> {
+                    dialog.dismiss();
+                    result.cancel();
+                }
+            )
+            .setOnCancelListener(
+                dialog -> {
+                    dialog.dismiss();
+                    result.cancel();
+                }
+            );
+
+        AlertDialog dialog = builder.create();
+
+        dialog.show();
+
+        return true;
+    }
+
+    /**
+     * Show the browser prompt modal
+     * @param view
+     * @param url
+     * @param message
+     * @param defaultValue
+     * @param result
+     * @return
+     */
+    @Override
+    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, final JsPromptResult result) {
+        if (bridge.getActivity().isFinishing()) {
+            return true;
+        }
+
+        final AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext());
+        final EditText input = new EditText(view.getContext());
+
+        builder
+            .setMessage(message)
+            .setView(input)
+            .setPositiveButton(
+                "OK",
+                (dialog, buttonIndex) -> {
+                    dialog.dismiss();
+
+                    String inputText1 = input.getText().toString().trim();
+                    result.confirm(inputText1);
+                }
+            )
+            .setNegativeButton(
+                "Cancel",
+                (dialog, buttonIndex) -> {
+                    dialog.dismiss();
+                    result.cancel();
+                }
+            )
+            .setOnCancelListener(
+                dialog -> {
+                    dialog.dismiss();
+                    result.cancel();
+                }
+            );
+
+        AlertDialog dialog = builder.create();
+
+        dialog.show();
+
+        return true;
+    }
+
+    /**
+     * Handle the browser geolocation permission prompt
+     * @param origin
+     * @param callback
+     */
+    @Override
+    public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
+        super.onGeolocationPermissionsShowPrompt(origin, callback);
+        Logger.debug("onGeolocationPermissionsShowPrompt: DOING IT HERE FOR ORIGIN: " + origin);
+        final String[] geoPermissions = { Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION };
+
+        if (!PermissionHelper.hasPermissions(bridge.getContext(), geoPermissions)) {
+            permissionListener =
+                isGranted -> {
+                    if (isGranted) {
+                        callback.invoke(origin, true, false);
+                    } else {
+                        final String[] coarsePermission = { Manifest.permission.ACCESS_COARSE_LOCATION };
+                        if (
+                            Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
+                            PermissionHelper.hasPermissions(bridge.getContext(), coarsePermission)
+                        ) {
+                            callback.invoke(origin, true, false);
+                        } else {
+                            callback.invoke(origin, false, false);
+                        }
+                    }
+                };
+            permissionLauncher.launch(geoPermissions);
+        } else {
+            // permission is already granted
+            callback.invoke(origin, true, false);
+            Logger.debug("onGeolocationPermissionsShowPrompt: has required permission");
+        }
+    }
+
+    @Override
+    public boolean onShowFileChooser(
+        WebView webView,
+        final ValueCallback<Uri[]> filePathCallback,
+        final FileChooserParams fileChooserParams
+    ) {
+        List<String> acceptTypes = Arrays.asList(fileChooserParams.getAcceptTypes());
+        boolean captureEnabled = fileChooserParams.isCaptureEnabled();
+        boolean capturePhoto = captureEnabled && acceptTypes.contains("image/*");
+        final boolean captureVideo = captureEnabled && acceptTypes.contains("video/*");
+        if ((capturePhoto || captureVideo)) {
+            if (isMediaCaptureSupported()) {
+                showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo);
+            } else {
+                permissionListener =
+                    isGranted -> {
+                        if (isGranted) {
+                            showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo);
+                        } else {
+                            Logger.warn(Logger.tags("FileChooser"), "Camera permission not granted");
+                            filePathCallback.onReceiveValue(null);
+                        }
+                    };
+                final String[] camPermission = { Manifest.permission.CAMERA };
+                permissionLauncher.launch(camPermission);
+            }
+        } else {
+            showFilePicker(filePathCallback, fileChooserParams);
+        }
+
+        return true;
+    }
+
+    private boolean isMediaCaptureSupported() {
+        String[] permissions = { Manifest.permission.CAMERA };
+        return (
+            PermissionHelper.hasPermissions(bridge.getContext(), permissions) ||
+            !PermissionHelper.hasDefinedPermission(bridge.getContext(), Manifest.permission.CAMERA)
+        );
+    }
+
+    private void showMediaCaptureOrFilePicker(ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams, boolean isVideo) {
+        // TODO: add support for video capture on Android M and older
+        // On Android M and lower the VIDEO_CAPTURE_INTENT (e.g.: intent.getData())
+        // returns a file:// URI instead of the expected content:// URI.
+        // So we disable it for now because it requires a bit more work
+        boolean isVideoCaptureSupported = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N;
+        boolean shown = false;
+        if (isVideo && isVideoCaptureSupported) {
+            shown = showVideoCapturePicker(filePathCallback);
+        } else {
+            shown = showImageCapturePicker(filePathCallback);
+        }
+        if (!shown) {
+            Logger.warn(Logger.tags("FileChooser"), "Media capture intent could not be launched. Falling back to default file picker.");
+            showFilePicker(filePathCallback, fileChooserParams);
+        }
+    }
+
+    @SuppressLint("QueryPermissionsNeeded")
+    private boolean showImageCapturePicker(final ValueCallback<Uri[]> filePathCallback) {
+        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+        if (takePictureIntent.resolveActivity(bridge.getActivity().getPackageManager()) == null) {
+            return false;
+        }
+
+        final Uri imageFileUri;
+        try {
+            imageFileUri = createImageFileUri();
+        } catch (Exception ex) {
+            Logger.error("Unable to create temporary media capture file: " + ex.getMessage());
+            return false;
+        }
+        takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri);
+        activityListener =
+            activityResult -> {
+                Uri[] result = null;
+                if (activityResult.getResultCode() == Activity.RESULT_OK) {
+                    result = new Uri[] { imageFileUri };
+                }
+                filePathCallback.onReceiveValue(result);
+            };
+        activityLauncher.launch(takePictureIntent);
+
+        return true;
+    }
+
+    @SuppressLint("QueryPermissionsNeeded")
+    private boolean showVideoCapturePicker(final ValueCallback<Uri[]> filePathCallback) {
+        Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
+        if (takeVideoIntent.resolveActivity(bridge.getActivity().getPackageManager()) == null) {
+            return false;
+        }
+
+        activityListener =
+            activityResult -> {
+                Uri[] result = null;
+                if (activityResult.getResultCode() == Activity.RESULT_OK) {
+                    result = new Uri[] { activityResult.getData().getData() };
+                }
+                filePathCallback.onReceiveValue(result);
+            };
+        activityLauncher.launch(takeVideoIntent);
+
+        return true;
+    }
+
+    private void showFilePicker(final ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
+        Intent intent = fileChooserParams.createIntent();
+        if (fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE) {
+            intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
+        }
+        if (fileChooserParams.getAcceptTypes().length > 1 || intent.getType().startsWith(".")) {
+            String[] validTypes = getValidTypes(fileChooserParams.getAcceptTypes());
+            intent.putExtra(Intent.EXTRA_MIME_TYPES, validTypes);
+            if (intent.getType().startsWith(".")) {
+                intent.setType(validTypes[0]);
+            }
+        }
+        try {
+            activityListener =
+                activityResult -> {
+                    Uri[] result;
+                    Intent resultIntent = activityResult.getData();
+                    if (activityResult.getResultCode() == Activity.RESULT_OK && resultIntent.getClipData() != null) {
+                        final int numFiles = resultIntent.getClipData().getItemCount();
+                        result = new Uri[numFiles];
+                        for (int i = 0; i < numFiles; i++) {
+                            result[i] = resultIntent.getClipData().getItemAt(i).getUri();
+                        }
+                    } else {
+                        result = WebChromeClient.FileChooserParams.parseResult(activityResult.getResultCode(), resultIntent);
+                    }
+                    filePathCallback.onReceiveValue(result);
+                };
+            activityLauncher.launch(intent);
+        } catch (ActivityNotFoundException e) {
+            filePathCallback.onReceiveValue(null);
+        }
+    }
+
+    private String[] getValidTypes(String[] currentTypes) {
+        List<String> validTypes = new ArrayList<>();
+        MimeTypeMap mtm = MimeTypeMap.getSingleton();
+        for (String mime : currentTypes) {
+            if (mime.startsWith(".")) {
+                String extension = mime.substring(1);
+                String extensionMime = mtm.getMimeTypeFromExtension(extension);
+                if (extensionMime != null && !validTypes.contains(extensionMime)) {
+                    validTypes.add(extensionMime);
+                }
+            } else if (!validTypes.contains(mime)) {
+                validTypes.add(mime);
+            }
+        }
+        Object[] validObj = validTypes.toArray();
+        return Arrays.copyOf(validObj, validObj.length, String[].class);
+    }
+
+    @Override
+    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
+        String tag = Logger.tags("Console");
+        if (consoleMessage.message() != null && isValidMsg(consoleMessage.message())) {
+            String msg = String.format(
+                "File: %s - Line %d - Msg: %s",
+                consoleMessage.sourceId(),
+                consoleMessage.lineNumber(),
+                consoleMessage.message()
+            );
+            String level = consoleMessage.messageLevel().name();
+            if ("ERROR".equalsIgnoreCase(level)) {
+                Logger.error(tag, msg, null);
+            } else if ("WARNING".equalsIgnoreCase(level)) {
+                Logger.warn(tag, msg);
+            } else if ("TIP".equalsIgnoreCase(level)) {
+                Logger.debug(tag, msg);
+            } else {
+                Logger.info(tag, msg);
+            }
+        }
+        return true;
+    }
+
+    public boolean isValidMsg(String msg) {
+        return !(
+            msg.contains("%cresult %c") ||
+            (msg.contains("%cnative %c")) ||
+            msg.equalsIgnoreCase("[object Object]") ||
+            msg.equalsIgnoreCase("console.groupEnd")
+        );
+    }
+
+    private Uri createImageFileUri() throws IOException {
+        Activity activity = bridge.getActivity();
+        File photoFile = createImageFile(activity);
+        return FileProvider.getUriForFile(activity, bridge.getContext().getPackageName() + ".fileprovider", photoFile);
+    }
+
+    private File createImageFile(Activity activity) throws IOException {
+        // Create an image file name
+        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
+        String imageFileName = "JPEG_" + timeStamp + "_";
+        File storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
+
+        return File.createTempFile(imageFileName, ".jpg", storageDir);
+    }
+}

+ 111 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/BridgeWebViewClient.java

@@ -0,0 +1,111 @@
+package com.getcapacitor;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.webkit.RenderProcessGoneDetail;
+import android.webkit.WebResourceError;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import java.util.List;
+
+public class BridgeWebViewClient extends WebViewClient {
+
+    private Bridge bridge;
+
+    public BridgeWebViewClient(Bridge bridge) {
+        this.bridge = bridge;
+    }
+
+    @Override
+    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
+        return bridge.getLocalServer().shouldInterceptRequest(request);
+    }
+
+    @Override
+    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
+        Uri url = request.getUrl();
+        return bridge.launchIntent(url);
+    }
+
+    @Deprecated
+    @Override
+    public boolean shouldOverrideUrlLoading(WebView view, String url) {
+        return bridge.launchIntent(Uri.parse(url));
+    }
+
+    @Override
+    public void onPageFinished(WebView view, String url) {
+        super.onPageFinished(view, url);
+        List<WebViewListener> webViewListeners = bridge.getWebViewListeners();
+
+        if (webViewListeners != null && view.getProgress() == 100) {
+            for (WebViewListener listener : bridge.getWebViewListeners()) {
+                listener.onPageLoaded(view);
+            }
+        }
+    }
+
+    @Override
+    public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
+        super.onReceivedError(view, request, error);
+
+        List<WebViewListener> webViewListeners = bridge.getWebViewListeners();
+        if (webViewListeners != null) {
+            for (WebViewListener listener : bridge.getWebViewListeners()) {
+                listener.onReceivedError(view);
+            }
+        }
+
+        String errorPath = bridge.getErrorUrl();
+        if (errorPath != null && request.isForMainFrame()) {
+            view.loadUrl(errorPath);
+        }
+    }
+
+    @Override
+    public void onPageStarted(WebView view, String url, Bitmap favicon) {
+        super.onPageStarted(view, url, favicon);
+        bridge.reset();
+        List<WebViewListener> webViewListeners = bridge.getWebViewListeners();
+
+        if (webViewListeners != null) {
+            for (WebViewListener listener : bridge.getWebViewListeners()) {
+                listener.onPageStarted(view);
+            }
+        }
+    }
+
+    @Override
+    public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
+        super.onReceivedHttpError(view, request, errorResponse);
+
+        List<WebViewListener> webViewListeners = bridge.getWebViewListeners();
+        if (webViewListeners != null) {
+            for (WebViewListener listener : bridge.getWebViewListeners()) {
+                listener.onReceivedHttpError(view);
+            }
+        }
+
+        String errorPath = bridge.getErrorUrl();
+        if (errorPath != null && request.isForMainFrame()) {
+            view.loadUrl(errorPath);
+        }
+    }
+
+    @Override
+    public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) {
+        super.onRenderProcessGone(view, detail);
+        boolean result = false;
+
+        List<WebViewListener> webViewListeners = bridge.getWebViewListeners();
+        if (webViewListeners != null) {
+            for (WebViewListener listener : bridge.getWebViewListeners()) {
+                result = listener.onRenderProcessGone(view, detail) || result;
+            }
+        }
+
+        return result;
+    }
+}

+ 670 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/CapConfig.java

@@ -0,0 +1,670 @@
+package com.getcapacitor;
+
+import static com.getcapacitor.Bridge.CAPACITOR_HTTP_SCHEME;
+import static com.getcapacitor.Bridge.DEFAULT_ANDROID_WEBVIEW_VERSION;
+import static com.getcapacitor.Bridge.DEFAULT_HUAWEI_WEBVIEW_VERSION;
+import static com.getcapacitor.Bridge.MINIMUM_ANDROID_WEBVIEW_VERSION;
+import static com.getcapacitor.Bridge.MINIMUM_HUAWEI_WEBVIEW_VERSION;
+import static com.getcapacitor.FileUtils.readFileFromAssets;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.res.AssetManager;
+import androidx.annotation.Nullable;
+import com.getcapacitor.util.JSONUtils;
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Represents the configuration options for Capacitor
+ */
+public class CapConfig {
+
+    private static final String LOG_BEHAVIOR_NONE = "none";
+    private static final String LOG_BEHAVIOR_DEBUG = "debug";
+    private static final String LOG_BEHAVIOR_PRODUCTION = "production";
+
+    // Server Config
+    private boolean html5mode = true;
+    private String serverUrl;
+    private String hostname = "localhost";
+    private String androidScheme = CAPACITOR_HTTP_SCHEME;
+    private String[] allowNavigation;
+
+    // Android Config
+    private String overriddenUserAgentString;
+    private String appendedUserAgentString;
+    private String backgroundColor;
+    private boolean allowMixedContent = false;
+    private boolean captureInput = false;
+    private boolean webContentsDebuggingEnabled = false;
+    private boolean loggingEnabled = true;
+    private boolean initialFocus = true;
+    private boolean useLegacyBridge = false;
+    private int minWebViewVersion = DEFAULT_ANDROID_WEBVIEW_VERSION;
+    private int minHuaweiWebViewVersion = DEFAULT_HUAWEI_WEBVIEW_VERSION;
+    private String errorPath;
+
+    // Embedded
+    private String startPath;
+
+    // Plugins
+    private Map<String, PluginConfig> pluginsConfiguration = null;
+
+    // Config Object JSON (legacy)
+    private JSONObject configJSON = new JSONObject();
+
+    /**
+     * Constructs an empty config file.
+     */
+    private CapConfig() {}
+
+    /**
+     * Get an instance of the Config file object.
+     * @deprecated use {@link #loadDefault(Context)} to load an instance of the Config object
+     * from the capacitor.config.json file, or use the {@link CapConfig.Builder} to construct
+     * a CapConfig for embedded use.
+     *
+     * @param assetManager The AssetManager used to load the config file
+     * @param config JSON describing a configuration to use
+     */
+    @Deprecated
+    public CapConfig(AssetManager assetManager, JSONObject config) {
+        if (config != null) {
+            this.configJSON = config;
+        } else {
+            // Load the capacitor.config.json
+            loadConfigFromAssets(assetManager, null);
+        }
+
+        deserializeConfig(null);
+    }
+
+    /**
+     * Constructs a Capacitor Configuration from config.json file.
+     *
+     * @param context The context.
+     * @return A loaded config file, if successful.
+     */
+    public static CapConfig loadDefault(Context context) {
+        CapConfig config = new CapConfig();
+
+        if (context == null) {
+            Logger.error("Capacitor Config could not be created from file. Context must not be null.");
+            return config;
+        }
+
+        config.loadConfigFromAssets(context.getAssets(), null);
+        config.deserializeConfig(context);
+        return config;
+    }
+
+    /**
+     * Constructs a Capacitor Configuration from config.json file within the app assets.
+     *
+     * @param context The context.
+     * @param path A path relative to the root assets directory.
+     * @return A loaded config file, if successful.
+     */
+    public static CapConfig loadFromAssets(Context context, String path) {
+        CapConfig config = new CapConfig();
+
+        if (context == null) {
+            Logger.error("Capacitor Config could not be created from file. Context must not be null.");
+            return config;
+        }
+
+        config.loadConfigFromAssets(context.getAssets(), path);
+        config.deserializeConfig(context);
+        return config;
+    }
+
+    /**
+     * Constructs a Capacitor Configuration from config.json file within the app file-space.
+     *
+     * @param context The context.
+     * @param path A path relative to the root of the app file-space.
+     * @return A loaded config file, if successful.
+     */
+    public static CapConfig loadFromFile(Context context, String path) {
+        CapConfig config = new CapConfig();
+
+        if (context == null) {
+            Logger.error("Capacitor Config could not be created from file. Context must not be null.");
+            return config;
+        }
+
+        config.loadConfigFromFile(path);
+        config.deserializeConfig(context);
+        return config;
+    }
+
+    /**
+     * Constructs a Capacitor Configuration using ConfigBuilder.
+     *
+     * @param builder A config builder initialized with values
+     */
+    private CapConfig(Builder builder) {
+        // Server Config
+        this.html5mode = builder.html5mode;
+        this.serverUrl = builder.serverUrl;
+        this.hostname = builder.hostname;
+
+        if (this.validateScheme(builder.androidScheme)) {
+            this.androidScheme = builder.androidScheme;
+        }
+
+        this.allowNavigation = builder.allowNavigation;
+
+        // Android Config
+        this.overriddenUserAgentString = builder.overriddenUserAgentString;
+        this.appendedUserAgentString = builder.appendedUserAgentString;
+        this.backgroundColor = builder.backgroundColor;
+        this.allowMixedContent = builder.allowMixedContent;
+        this.captureInput = builder.captureInput;
+        this.webContentsDebuggingEnabled = builder.webContentsDebuggingEnabled;
+        this.loggingEnabled = builder.loggingEnabled;
+        this.initialFocus = builder.initialFocus;
+        this.useLegacyBridge = builder.useLegacyBridge;
+        this.minWebViewVersion = builder.minWebViewVersion;
+        this.minHuaweiWebViewVersion = builder.minHuaweiWebViewVersion;
+        this.errorPath = builder.errorPath;
+
+        // Embedded
+        this.startPath = builder.startPath;
+
+        // Plugins Config
+        this.pluginsConfiguration = builder.pluginsConfiguration;
+    }
+
+    /**
+     * Loads a Capacitor Configuration JSON file into a Capacitor Configuration object.
+     * An optional path string can be provided to look for the config in a subdirectory path.
+     */
+    private void loadConfigFromAssets(AssetManager assetManager, String path) {
+        if (path == null) {
+            path = "";
+        } else {
+            // Add slash at the end to form a proper file path if going deeper in assets dir
+            if (path.charAt(path.length() - 1) != '/') {
+                path = path + "/";
+            }
+        }
+
+        try {
+            String jsonString = readFileFromAssets(assetManager, path + "capacitor.config.json");
+            configJSON = new JSONObject(jsonString);
+        } catch (IOException ex) {
+            Logger.error("Unable to load capacitor.config.json. Run npx cap copy first", ex);
+        } catch (JSONException ex) {
+            Logger.error("Unable to parse capacitor.config.json. Make sure it's valid json", ex);
+        }
+    }
+
+    /**
+     * Loads a Capacitor Configuration JSON file into a Capacitor Configuration object.
+     * An optional path string can be provided to look for the config in a subdirectory path.
+     */
+    private void loadConfigFromFile(String path) {
+        if (path == null) {
+            path = "";
+        } else {
+            // Add slash at the end to form a proper file path if going deeper in assets dir
+            if (path.charAt(path.length() - 1) != '/') {
+                path = path + "/";
+            }
+        }
+
+        try {
+            File configFile = new File(path + "capacitor.config.json");
+            String jsonString = FileUtils.readFileFromDisk(configFile);
+            configJSON = new JSONObject(jsonString);
+        } catch (JSONException ex) {
+            Logger.error("Unable to parse capacitor.config.json. Make sure it's valid json", ex);
+        } catch (IOException ex) {
+            Logger.error("Unable to load capacitor.config.json.", ex);
+        }
+    }
+
+    /**
+     * Deserializes the config from JSON into a Capacitor Configuration object.
+     */
+    private void deserializeConfig(@Nullable Context context) {
+        boolean isDebug = context != null && (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
+
+        // Server
+        html5mode = JSONUtils.getBoolean(configJSON, "server.html5mode", html5mode);
+        serverUrl = JSONUtils.getString(configJSON, "server.url", null);
+        hostname = JSONUtils.getString(configJSON, "server.hostname", hostname);
+        errorPath = JSONUtils.getString(configJSON, "server.errorPath", null);
+
+        String configSchema = JSONUtils.getString(configJSON, "server.androidScheme", androidScheme);
+        if (this.validateScheme(configSchema)) {
+            androidScheme = configSchema;
+        }
+
+        allowNavigation = JSONUtils.getArray(configJSON, "server.allowNavigation", null);
+
+        // Android
+        overriddenUserAgentString =
+            JSONUtils.getString(configJSON, "android.overrideUserAgent", JSONUtils.getString(configJSON, "overrideUserAgent", null));
+        appendedUserAgentString =
+            JSONUtils.getString(configJSON, "android.appendUserAgent", JSONUtils.getString(configJSON, "appendUserAgent", null));
+        backgroundColor =
+            JSONUtils.getString(configJSON, "android.backgroundColor", JSONUtils.getString(configJSON, "backgroundColor", null));
+        allowMixedContent =
+            JSONUtils.getBoolean(
+                configJSON,
+                "android.allowMixedContent",
+                JSONUtils.getBoolean(configJSON, "allowMixedContent", allowMixedContent)
+            );
+        minWebViewVersion = JSONUtils.getInt(configJSON, "android.minWebViewVersion", DEFAULT_ANDROID_WEBVIEW_VERSION);
+        minHuaweiWebViewVersion = JSONUtils.getInt(configJSON, "android.minHuaweiWebViewVersion", DEFAULT_HUAWEI_WEBVIEW_VERSION);
+        captureInput = JSONUtils.getBoolean(configJSON, "android.captureInput", captureInput);
+        useLegacyBridge = JSONUtils.getBoolean(configJSON, "android.useLegacyBridge", useLegacyBridge);
+        webContentsDebuggingEnabled = JSONUtils.getBoolean(configJSON, "android.webContentsDebuggingEnabled", isDebug);
+
+        String logBehavior = JSONUtils.getString(
+            configJSON,
+            "android.loggingBehavior",
+            JSONUtils.getString(configJSON, "loggingBehavior", LOG_BEHAVIOR_DEBUG)
+        );
+        switch (logBehavior.toLowerCase(Locale.ROOT)) {
+            case LOG_BEHAVIOR_PRODUCTION:
+                loggingEnabled = true;
+                break;
+            case LOG_BEHAVIOR_NONE:
+                loggingEnabled = false;
+                break;
+            default: // LOG_BEHAVIOR_DEBUG
+                loggingEnabled = isDebug;
+        }
+
+        initialFocus = JSONUtils.getBoolean(configJSON, "android.initialFocus", initialFocus);
+
+        // Plugins
+        pluginsConfiguration = deserializePluginsConfig(JSONUtils.getObject(configJSON, "plugins"));
+    }
+
+    private boolean validateScheme(String scheme) {
+        List<String> invalidSchemes = Arrays.asList("file", "ftp", "ftps", "ws", "wss", "about", "blob", "data");
+        if (invalidSchemes.contains(scheme)) {
+            Logger.warn(scheme + " is not an allowed scheme.  Defaulting to http.");
+            return false;
+        }
+
+        // Non-http(s) schemes are not allowed to modify the URL path as of Android Webview 117
+        if (!scheme.equals("http") && !scheme.equals("https")) {
+            Logger.warn(
+                "Using a non-standard scheme: " + scheme + " for Android. This is known to cause issues as of Android Webview 117."
+            );
+        }
+
+        return true;
+    }
+
+    public boolean isHTML5Mode() {
+        return html5mode;
+    }
+
+    public String getServerUrl() {
+        return serverUrl;
+    }
+
+    public String getErrorPath() {
+        return errorPath;
+    }
+
+    public String getHostname() {
+        return hostname;
+    }
+
+    public String getStartPath() {
+        return startPath;
+    }
+
+    public String getAndroidScheme() {
+        return androidScheme;
+    }
+
+    public String[] getAllowNavigation() {
+        return allowNavigation;
+    }
+
+    public String getOverriddenUserAgentString() {
+        return overriddenUserAgentString;
+    }
+
+    public String getAppendedUserAgentString() {
+        return appendedUserAgentString;
+    }
+
+    public String getBackgroundColor() {
+        return backgroundColor;
+    }
+
+    public boolean isMixedContentAllowed() {
+        return allowMixedContent;
+    }
+
+    public boolean isInputCaptured() {
+        return captureInput;
+    }
+
+    public boolean isWebContentsDebuggingEnabled() {
+        return webContentsDebuggingEnabled;
+    }
+
+    public boolean isLoggingEnabled() {
+        return loggingEnabled;
+    }
+
+    public boolean isInitialFocus() {
+        return initialFocus;
+    }
+
+    public boolean isUsingLegacyBridge() {
+        return useLegacyBridge;
+    }
+
+    public int getMinWebViewVersion() {
+        if (minWebViewVersion < MINIMUM_ANDROID_WEBVIEW_VERSION) {
+            Logger.warn("Specified minimum webview version is too low, defaulting to " + MINIMUM_ANDROID_WEBVIEW_VERSION);
+            return MINIMUM_ANDROID_WEBVIEW_VERSION;
+        }
+
+        return minWebViewVersion;
+    }
+
+    public int getMinHuaweiWebViewVersion() {
+        if (minHuaweiWebViewVersion < MINIMUM_HUAWEI_WEBVIEW_VERSION) {
+            Logger.warn("Specified minimum Huawei webview version is too low, defaulting to " + MINIMUM_HUAWEI_WEBVIEW_VERSION);
+            return MINIMUM_HUAWEI_WEBVIEW_VERSION;
+        }
+
+        return minHuaweiWebViewVersion;
+    }
+
+    public PluginConfig getPluginConfiguration(String pluginId) {
+        PluginConfig pluginConfig = pluginsConfiguration.get(pluginId);
+        if (pluginConfig == null) {
+            pluginConfig = new PluginConfig(new JSONObject());
+        }
+
+        return pluginConfig;
+    }
+
+    /**
+     * Get a JSON object value from the Capacitor config.
+     * @deprecated use {@link PluginConfig#getObject(String)}  to access plugin config values.
+     * For main Capacitor config values, use the appropriate getter.
+     *
+     * @param key A key to fetch from the config
+     * @return The value from the config, if exists. Null if not
+     */
+    @Deprecated
+    public JSONObject getObject(String key) {
+        try {
+            return configJSON.getJSONObject(key);
+        } catch (Exception ex) {}
+        return null;
+    }
+
+    /**
+     * Get a string value from the Capacitor config.
+     * @deprecated use {@link PluginConfig#getString(String, String)} to access plugin config
+     * values. For main Capacitor config values, use the appropriate getter.
+     *
+     * @param key A key to fetch from the config
+     * @return The value from the config, if exists. Null if not
+     */
+    @Deprecated
+    public String getString(String key) {
+        return JSONUtils.getString(configJSON, key, null);
+    }
+
+    /**
+     * Get a string value from the Capacitor config.
+     * @deprecated use {@link PluginConfig#getString(String, String)} to access plugin config
+     * values. For main Capacitor config values, use the appropriate getter.
+     *
+     * @param key A key to fetch from the config
+     * @param defaultValue A default value to return if the key does not exist in the config
+     * @return The value from the config, if key exists. Default value returned if not
+     */
+    @Deprecated
+    public String getString(String key, String defaultValue) {
+        return JSONUtils.getString(configJSON, key, defaultValue);
+    }
+
+    /**
+     * Get a boolean value from the Capacitor config.
+     * @deprecated use {@link PluginConfig#getBoolean(String, boolean)} to access plugin config
+     * values. For main Capacitor config values, use the appropriate getter.
+     *
+     * @param key A key to fetch from the config
+     * @param defaultValue A default value to return if the key does not exist in the config
+     * @return The value from the config, if key exists. Default value returned if not
+     */
+    @Deprecated
+    public boolean getBoolean(String key, boolean defaultValue) {
+        return JSONUtils.getBoolean(configJSON, key, defaultValue);
+    }
+
+    /**
+     * Get an integer value from the Capacitor config.
+     * @deprecated use {@link PluginConfig#getInt(String, int)}  to access the plugin config
+     * values. For main Capacitor config values, use the appropriate getter.
+     *
+     * @param key A key to fetch from the config
+     * @param defaultValue A default value to return if the key does not exist in the config
+     * @return The value from the config, if key exists. Default value returned if not
+     */
+    @Deprecated
+    public int getInt(String key, int defaultValue) {
+        return JSONUtils.getInt(configJSON, key, defaultValue);
+    }
+
+    /**
+     * Get a string array value from the Capacitor config.
+     * @deprecated use {@link PluginConfig#getArray(String)}  to access the plugin config
+     * values. For main Capacitor config values, use the appropriate getter.
+     *
+     * @param key A key to fetch from the config
+     * @return The value from the config, if exists. Null if not
+     */
+    @Deprecated
+    public String[] getArray(String key) {
+        return JSONUtils.getArray(configJSON, key, null);
+    }
+
+    /**
+     * Get a string array value from the Capacitor config.
+     * @deprecated use {@link PluginConfig#getArray(String, String[])}  to access the plugin
+     * config values. For main Capacitor config values, use the appropriate getter.
+     *
+     * @param key A key to fetch from the config
+     * @param defaultValue A default value to return if the key does not exist in the config
+     * @return The value from the config, if key exists. Default value returned if not
+     */
+    @Deprecated
+    public String[] getArray(String key, String[] defaultValue) {
+        return JSONUtils.getArray(configJSON, key, defaultValue);
+    }
+
+    private static Map<String, PluginConfig> deserializePluginsConfig(JSONObject pluginsConfig) {
+        Map<String, PluginConfig> pluginsMap = new HashMap<>();
+
+        // return an empty map if there is no pluginsConfig json
+        if (pluginsConfig == null) {
+            return pluginsMap;
+        }
+
+        Iterator<String> pluginIds = pluginsConfig.keys();
+
+        while (pluginIds.hasNext()) {
+            String pluginId = pluginIds.next();
+            JSONObject value = null;
+
+            try {
+                value = pluginsConfig.getJSONObject(pluginId);
+                PluginConfig pluginConfig = new PluginConfig(value);
+                pluginsMap.put(pluginId, pluginConfig);
+            } catch (JSONException e) {
+                e.printStackTrace();
+            }
+        }
+
+        return pluginsMap;
+    }
+
+    /**
+     * Builds a Capacitor Configuration in code
+     */
+    public static class Builder {
+
+        private Context context;
+
+        // Server Config Values
+        private boolean html5mode = true;
+        private String serverUrl;
+        private String errorPath;
+        private String hostname = "localhost";
+        private String androidScheme = CAPACITOR_HTTP_SCHEME;
+        private String[] allowNavigation;
+
+        // Android Config Values
+        private String overriddenUserAgentString;
+        private String appendedUserAgentString;
+        private String backgroundColor;
+        private boolean allowMixedContent = false;
+        private boolean captureInput = false;
+        private Boolean webContentsDebuggingEnabled = null;
+        private boolean loggingEnabled = true;
+        private boolean initialFocus = false;
+        private boolean useLegacyBridge = false;
+        private int minWebViewVersion = DEFAULT_ANDROID_WEBVIEW_VERSION;
+        private int minHuaweiWebViewVersion = DEFAULT_HUAWEI_WEBVIEW_VERSION;
+
+        // Embedded
+        private String startPath = null;
+
+        // Plugins Config Object
+        private Map<String, PluginConfig> pluginsConfiguration = new HashMap<>();
+
+        /**
+         * Constructs a new CapConfig Builder.
+         *
+         * @param context The context
+         */
+        public Builder(Context context) {
+            this.context = context;
+        }
+
+        /**
+         * Builds a Capacitor Config from the builder.
+         *
+         * @return A new Capacitor Config
+         */
+        public CapConfig create() {
+            if (webContentsDebuggingEnabled == null) {
+                webContentsDebuggingEnabled = (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
+            }
+
+            return new CapConfig(this);
+        }
+
+        public Builder setPluginsConfiguration(JSONObject pluginsConfiguration) {
+            this.pluginsConfiguration = deserializePluginsConfig(pluginsConfiguration);
+            return this;
+        }
+
+        public Builder setHTML5mode(boolean html5mode) {
+            this.html5mode = html5mode;
+            return this;
+        }
+
+        public Builder setServerUrl(String serverUrl) {
+            this.serverUrl = serverUrl;
+            return this;
+        }
+
+        public Builder setErrorPath(String errorPath) {
+            this.errorPath = errorPath;
+            return this;
+        }
+
+        public Builder setHostname(String hostname) {
+            this.hostname = hostname;
+            return this;
+        }
+
+        public Builder setStartPath(String path) {
+            this.startPath = path;
+            return this;
+        }
+
+        public Builder setAndroidScheme(String androidScheme) {
+            this.androidScheme = androidScheme;
+            return this;
+        }
+
+        public Builder setAllowNavigation(String[] allowNavigation) {
+            this.allowNavigation = allowNavigation;
+            return this;
+        }
+
+        public Builder setOverriddenUserAgentString(String overriddenUserAgentString) {
+            this.overriddenUserAgentString = overriddenUserAgentString;
+            return this;
+        }
+
+        public Builder setAppendedUserAgentString(String appendedUserAgentString) {
+            this.appendedUserAgentString = appendedUserAgentString;
+            return this;
+        }
+
+        public Builder setBackgroundColor(String backgroundColor) {
+            this.backgroundColor = backgroundColor;
+            return this;
+        }
+
+        public Builder setAllowMixedContent(boolean allowMixedContent) {
+            this.allowMixedContent = allowMixedContent;
+            return this;
+        }
+
+        public Builder setCaptureInput(boolean captureInput) {
+            this.captureInput = captureInput;
+            return this;
+        }
+
+        public Builder setUseLegacyBridge(boolean useLegacyBridge) {
+            this.useLegacyBridge = useLegacyBridge;
+            return this;
+        }
+
+        public Builder setWebContentsDebuggingEnabled(boolean webContentsDebuggingEnabled) {
+            this.webContentsDebuggingEnabled = webContentsDebuggingEnabled;
+            return this;
+        }
+
+        public Builder setLoggingEnabled(boolean enabled) {
+            this.loggingEnabled = enabled;
+            return this;
+        }
+
+        public Builder setInitialFocus(boolean focus) {
+            this.initialFocus = focus;
+            return this;
+        }
+    }
+}

+ 52 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/CapacitorWebView.java

@@ -0,0 +1,52 @@
+package com.getcapacitor;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.webkit.WebView;
+
+public class CapacitorWebView extends WebView {
+
+    private BaseInputConnection capInputConnection;
+    private Bridge bridge;
+
+    public CapacitorWebView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public void setBridge(Bridge bridge) {
+        this.bridge = bridge;
+    }
+
+    @Override
+    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+        CapConfig config;
+        if (bridge != null) {
+            config = bridge.getConfig();
+        } else {
+            config = CapConfig.loadDefault(getContext());
+        }
+
+        boolean captureInput = config.isInputCaptured();
+        if (captureInput) {
+            if (capInputConnection == null) {
+                capInputConnection = new BaseInputConnection(this, false);
+            }
+            return capInputConnection;
+        }
+        return super.onCreateInputConnection(outAttrs);
+    }
+
+    @Override
+    @SuppressWarnings("deprecation")
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        if (event.getAction() == KeyEvent.ACTION_MULTIPLE) {
+            evaluateJavascript("document.activeElement.value = document.activeElement.value + '" + event.getCharacters() + "';", null);
+            return false;
+        }
+        return super.dispatchKeyEvent(event);
+    }
+}

+ 292 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/FileUtils.java

@@ -0,0 +1,292 @@
+/**
+ * Portions adopted from react-native-image-crop-picker
+ *
+ * MIT License
+
+ * Copyright (c) 2017 Ivan Pusic
+
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package com.getcapacitor;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.DocumentsContract;
+import android.provider.MediaStore;
+import android.provider.OpenableColumns;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+/**
+ * Common File utilities, such as resolve content URIs and
+ * creating portable web paths from low-level files
+ */
+public class FileUtils {
+
+    private static String CapacitorFileScheme = Bridge.CAPACITOR_FILE_START;
+
+    public enum Type {
+        IMAGE("image");
+
+        private String type;
+
+        Type(String type) {
+            this.type = type;
+        }
+    }
+
+    public static String getPortablePath(Context c, String host, Uri u) {
+        String path = getFileUrlForUri(c, u);
+        if (path.startsWith("file://")) {
+            path = path.replace("file://", "");
+        }
+        return host + Bridge.CAPACITOR_FILE_START + path;
+    }
+
+    public static String getFileUrlForUri(final Context context, final Uri uri) {
+        // DocumentProvider
+        if (DocumentsContract.isDocumentUri(context, uri)) {
+            // ExternalStorageProvider
+            if (isExternalStorageDocument(uri)) {
+                final String docId = DocumentsContract.getDocumentId(uri);
+                final String[] split = docId.split(":");
+                final String type = split[0];
+
+                if ("primary".equalsIgnoreCase(type)) {
+                    return legacyPrimaryPath(split[1]);
+                } else {
+                    final int splitIndex = docId.indexOf(':', 1);
+                    final String tag = docId.substring(0, splitIndex);
+                    final String path = docId.substring(splitIndex + 1);
+
+                    String nonPrimaryVolume = getPathToNonPrimaryVolume(context, tag);
+                    if (nonPrimaryVolume != null) {
+                        String result = nonPrimaryVolume + "/" + path;
+                        File file = new File(result);
+                        if (file.exists() && file.canRead()) {
+                            return result;
+                        }
+                        return null;
+                    }
+                }
+            }
+            // DownloadsProvider
+            else if (isDownloadsDocument(uri)) {
+                final String id = DocumentsContract.getDocumentId(uri);
+                final Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
+
+                return getDataColumn(context, contentUri, null, null);
+            }
+            // MediaProvider
+            else if (isMediaDocument(uri)) {
+                final String docId = DocumentsContract.getDocumentId(uri);
+                final String[] split = docId.split(":");
+                final String type = split[0];
+
+                Uri contentUri = null;
+                if ("image".equals(type)) {
+                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+                } else if ("video".equals(type)) {
+                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+                } else if ("audio".equals(type)) {
+                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+                }
+
+                final String selection = "_id=?";
+                final String[] selectionArgs = new String[] { split[1] };
+
+                return getDataColumn(context, contentUri, selection, selectionArgs);
+            }
+        }
+        // MediaStore (and general)
+        else if ("content".equalsIgnoreCase(uri.getScheme())) {
+            // Return the remote address
+            if (isGooglePhotosUri(uri)) return uri.getLastPathSegment();
+            return getDataColumn(context, uri, null, null);
+        }
+        // File
+        else if ("file".equalsIgnoreCase(uri.getScheme())) {
+            return uri.getPath();
+        }
+
+        return null;
+    }
+
+    @SuppressWarnings("deprecation")
+    private static String legacyPrimaryPath(String pathPart) {
+        return Environment.getExternalStorageDirectory() + "/" + pathPart;
+    }
+
+    /**
+     * Read a plaintext file from the assets directory.
+     *
+     * @param assetManager Used to open the file.
+     * @param fileName The path of the file to read.
+     * @return The contents of the file path.
+     * @throws IOException Thrown if any issues reading the provided file path.
+     */
+    static String readFileFromAssets(AssetManager assetManager, String fileName) throws IOException {
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(assetManager.open(fileName)))) {
+            StringBuilder buffer = new StringBuilder();
+            String line;
+            while ((line = reader.readLine()) != null) {
+                buffer.append(line).append("\n");
+            }
+
+            return buffer.toString();
+        }
+    }
+
+    /**
+     * Read a plaintext file from within the app disk space.
+     *
+     * @param file The file to read.
+     * @return The contents of the file path.
+     * @throws IOException Thrown if any issues reading the provided file path.
+     */
+    static String readFileFromDisk(File file) throws IOException {
+        try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
+            StringBuilder buffer = new StringBuilder();
+            String line;
+            while ((line = reader.readLine()) != null) {
+                buffer.append(line).append("\n");
+            }
+
+            return buffer.toString();
+        }
+    }
+
+    /**
+     * Get the value of the data column for this Uri. This is useful for
+     * MediaStore Uris, and other file-based ContentProviders.
+     *
+     * @param context The context.
+     * @param uri The Uri to query.
+     * @param selection (Optional) Filter used in the query.
+     * @param selectionArgs (Optional) Selection arguments used in the query.
+     * @return The value of the _data column, which is typically a file path.
+     */
+    private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {
+        String path = null;
+        Cursor cursor = null;
+        final String column = "_data";
+        final String[] projection = { column };
+
+        try {
+            cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);
+            if (cursor != null && cursor.moveToFirst()) {
+                final int index = cursor.getColumnIndexOrThrow(column);
+                path = cursor.getString(index);
+            }
+        } catch (IllegalArgumentException ex) {
+            return getCopyFilePath(uri, context);
+        } finally {
+            if (cursor != null) cursor.close();
+        }
+        if (path == null) {
+            return getCopyFilePath(uri, context);
+        }
+        return path;
+    }
+
+    private static String getCopyFilePath(Uri uri, Context context) {
+        Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
+        int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+        cursor.moveToFirst();
+        String name = (cursor.getString(nameIndex));
+        File file = new File(context.getFilesDir(), name);
+        try {
+            InputStream inputStream = context.getContentResolver().openInputStream(uri);
+            FileOutputStream outputStream = new FileOutputStream(file);
+            int read = 0;
+            int maxBufferSize = 1024 * 1024;
+            int bufferSize = Math.min(inputStream.available(), maxBufferSize);
+            final byte[] buffers = new byte[bufferSize];
+            while ((read = inputStream.read(buffers)) != -1) {
+                outputStream.write(buffers, 0, read);
+            }
+            inputStream.close();
+            outputStream.close();
+        } catch (Exception e) {
+            return null;
+        } finally {
+            if (cursor != null) cursor.close();
+        }
+        return file.getPath();
+    }
+
+    /**
+     * @param uri The Uri to check.
+     * @return Whether the Uri authority is ExternalStorageProvider.
+     */
+    private static boolean isExternalStorageDocument(Uri uri) {
+        return "com.android.externalstorage.documents".equals(uri.getAuthority());
+    }
+
+    /**
+     * @param uri The Uri to check.
+     * @return Whether the Uri authority is DownloadsProvider.
+     */
+    private static boolean isDownloadsDocument(Uri uri) {
+        return "com.android.providers.downloads.documents".equals(uri.getAuthority());
+    }
+
+    /**
+     * @param uri The Uri to check.
+     * @return Whether the Uri authority is MediaProvider.
+     */
+    private static boolean isMediaDocument(Uri uri) {
+        return "com.android.providers.media.documents".equals(uri.getAuthority());
+    }
+
+    /**
+     * @param uri The Uri to check.
+     * @return Whether the Uri authority is Google Photos.
+     */
+    private static boolean isGooglePhotosUri(Uri uri) {
+        return "com.google.android.apps.photos.content".equals(uri.getAuthority());
+    }
+
+    private static String getPathToNonPrimaryVolume(Context context, String tag) {
+        File[] volumes = context.getExternalCacheDirs();
+        if (volumes != null) {
+            for (File volume : volumes) {
+                if (volume != null) {
+                    String path = volume.getAbsolutePath();
+                    if (path != null) {
+                        int index = path.indexOf(tag);
+                        if (index != -1) {
+                            return path.substring(0, index) + tag;
+                        }
+                    }
+                }
+            }
+        }
+        return null;
+    }
+}

+ 8 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/InvalidPluginException.java

@@ -0,0 +1,8 @@
+package com.getcapacitor;
+
+class InvalidPluginException extends Exception {
+
+    public InvalidPluginException(String s) {
+        super(s);
+    }
+}

+ 16 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/InvalidPluginMethodException.java

@@ -0,0 +1,16 @@
+package com.getcapacitor;
+
+class InvalidPluginMethodException extends Exception {
+
+    public InvalidPluginMethodException(String s) {
+        super(s);
+    }
+
+    public InvalidPluginMethodException(Throwable t) {
+        super(t);
+    }
+
+    public InvalidPluginMethodException(String s, Throwable t) {
+        super(s, t);
+    }
+}

+ 51 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/JSArray.java

@@ -0,0 +1,51 @@
+package com.getcapacitor;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import org.json.JSONArray;
+import org.json.JSONException;
+
+public class JSArray extends JSONArray {
+
+    public JSArray() {
+        super();
+    }
+
+    public JSArray(String json) throws JSONException {
+        super(json);
+    }
+
+    public JSArray(Collection copyFrom) {
+        super(copyFrom);
+    }
+
+    public JSArray(Object array) throws JSONException {
+        super(array);
+    }
+
+    @SuppressWarnings("unchecked")
+    public <E> List<E> toList() throws JSONException {
+        List<E> items = new ArrayList<>();
+        Object o = null;
+        for (int i = 0; i < this.length(); i++) {
+            o = this.get(i);
+            try {
+                items.add((E) this.get(i));
+            } catch (Exception ex) {
+                throw new JSONException("Not all items are instances of the given type");
+            }
+        }
+        return items;
+    }
+
+    /**
+     * Create a new JSArray without throwing a error
+     */
+    public static JSArray from(Object array) {
+        try {
+            return new JSArray(array);
+        } catch (JSONException ex) {}
+        return null;
+    }
+}

+ 193 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/JSExport.java

@@ -0,0 +1,193 @@
+package com.getcapacitor;
+
+import static com.getcapacitor.FileUtils.readFileFromAssets;
+
+import android.content.Context;
+import android.text.TextUtils;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class JSExport {
+
+    private static String CATCHALL_OPTIONS_PARAM = "_options";
+    private static String CALLBACK_PARAM = "_callback";
+
+    public static String getGlobalJS(Context context, boolean loggingEnabled, boolean isDebug) {
+        return "window.Capacitor = { DEBUG: " + isDebug + ", isLoggingEnabled: " + loggingEnabled + ", Plugins: {} };";
+    }
+
+    public static String getCordovaJS(Context context) {
+        String fileContent = "";
+        try {
+            fileContent = readFileFromAssets(context.getAssets(), "public/cordova.js");
+        } catch (IOException ex) {
+            Logger.error("Unable to read public/cordova.js file, Cordova plugins will not work");
+        }
+        return fileContent;
+    }
+
+    public static String getCordovaPluginsFileJS(Context context) {
+        String fileContent = "";
+        try {
+            fileContent = readFileFromAssets(context.getAssets(), "public/cordova_plugins.js");
+        } catch (IOException ex) {
+            Logger.error("Unable to read public/cordova_plugins.js file, Cordova plugins will not work");
+        }
+        return fileContent;
+    }
+
+    public static String getPluginJS(Collection<PluginHandle> plugins) {
+        List<String> lines = new ArrayList<>();
+        JSONArray pluginArray = new JSONArray();
+
+        lines.add("// Begin: Capacitor Plugin JS");
+        for (PluginHandle plugin : plugins) {
+            lines.add(
+                "(function(w) {\n" +
+                "var a = (w.Capacitor = w.Capacitor || {});\n" +
+                "var p = (a.Plugins = a.Plugins || {});\n" +
+                "var t = (p['" +
+                plugin.getId() +
+                "'] = {});\n" +
+                "t.addListener = function(eventName, callback) {\n" +
+                "  return w.Capacitor.addListener('" +
+                plugin.getId() +
+                "', eventName, callback);\n" +
+                "}"
+            );
+            Collection<PluginMethodHandle> methods = plugin.getMethods();
+            for (PluginMethodHandle method : methods) {
+                if (method.getName().equals("addListener") || method.getName().equals("removeListener")) {
+                    // Don't export add/remove listener, we do that automatically above as they are "special snowflakes"
+                    continue;
+                }
+                lines.add(generateMethodJS(plugin, method));
+            }
+
+            lines.add("})(window);\n");
+            pluginArray.put(createPluginHeader(plugin));
+        }
+
+        return TextUtils.join("\n", lines) + "\nwindow.Capacitor.PluginHeaders = " + pluginArray.toString() + ";";
+    }
+
+    public static String getCordovaPluginJS(Context context) {
+        return getFilesContent(context, "public/plugins");
+    }
+
+    public static String getFilesContent(Context context, String path) {
+        StringBuilder builder = new StringBuilder();
+        try {
+            String[] content = context.getAssets().list(path);
+            if (content.length > 0) {
+                for (String file : content) {
+                    if (!file.endsWith(".map")) {
+                        builder.append(getFilesContent(context, path + "/" + file));
+                    }
+                }
+            } else {
+                return readFileFromAssets(context.getAssets(), path);
+            }
+        } catch (IOException ex) {
+            Logger.warn("Unable to read file at path " + path);
+        }
+        return builder.toString();
+    }
+
+    private static JSONObject createPluginHeader(PluginHandle plugin) {
+        JSONObject pluginObj = new JSONObject();
+        Collection<PluginMethodHandle> methods = plugin.getMethods();
+        try {
+            String id = plugin.getId();
+            JSONArray methodArray = new JSONArray();
+            pluginObj.put("name", id);
+
+            for (PluginMethodHandle method : methods) {
+                methodArray.put(createPluginMethodHeader(method));
+            }
+
+            pluginObj.put("methods", methodArray);
+        } catch (JSONException e) {
+            // ignore
+        }
+        return pluginObj;
+    }
+
+    private static JSONObject createPluginMethodHeader(PluginMethodHandle method) {
+        JSONObject methodObj = new JSONObject();
+
+        try {
+            methodObj.put("name", method.getName());
+            if (!method.getReturnType().equals(PluginMethod.RETURN_NONE)) {
+                methodObj.put("rtype", method.getReturnType());
+            }
+        } catch (JSONException e) {
+            // ignore
+        }
+
+        return methodObj;
+    }
+
+    public static String getBridgeJS(Context context) throws JSExportException {
+        return getFilesContent(context, "native-bridge.js");
+    }
+
+    private static String generateMethodJS(PluginHandle plugin, PluginMethodHandle method) {
+        List<String> lines = new ArrayList<>();
+
+        List<String> args = new ArrayList<>();
+        // Add the catch all param that will take a full javascript object to pass to the plugin
+        args.add(CATCHALL_OPTIONS_PARAM);
+
+        String returnType = method.getReturnType();
+        if (returnType.equals(PluginMethod.RETURN_CALLBACK)) {
+            args.add(CALLBACK_PARAM);
+        }
+
+        // Create the method function declaration
+        lines.add("t['" + method.getName() + "'] = function(" + TextUtils.join(", ", args) + ") {");
+
+        switch (returnType) {
+            case PluginMethod.RETURN_NONE:
+                lines.add(
+                    "return w.Capacitor.nativeCallback('" +
+                    plugin.getId() +
+                    "', '" +
+                    method.getName() +
+                    "', " +
+                    CATCHALL_OPTIONS_PARAM +
+                    ")"
+                );
+                break;
+            case PluginMethod.RETURN_PROMISE:
+                lines.add(
+                    "return w.Capacitor.nativePromise('" + plugin.getId() + "', '" + method.getName() + "', " + CATCHALL_OPTIONS_PARAM + ")"
+                );
+                break;
+            case PluginMethod.RETURN_CALLBACK:
+                lines.add(
+                    "return w.Capacitor.nativeCallback('" +
+                    plugin.getId() +
+                    "', '" +
+                    method.getName() +
+                    "', " +
+                    CATCHALL_OPTIONS_PARAM +
+                    ", " +
+                    CALLBACK_PARAM +
+                    ")"
+                );
+                break;
+            default:
+            // TODO: Do something here?
+        }
+
+        lines.add("}");
+
+        return TextUtils.join("\n", lines);
+    }
+}

+ 16 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/JSExportException.java

@@ -0,0 +1,16 @@
+package com.getcapacitor;
+
+public class JSExportException extends Exception {
+
+    public JSExportException(String s) {
+        super(s);
+    }
+
+    public JSExportException(Throwable t) {
+        super(t);
+    }
+
+    public JSExportException(String s, Throwable t) {
+        super(s, t);
+    }
+}

+ 102 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/JSInjector.java

@@ -0,0 +1,102 @@
+package com.getcapacitor;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * JSInject is responsible for returning Capacitor's core
+ * runtime JS and any plugin JS back into HTML page responses
+ * to the client.
+ */
+class JSInjector {
+
+    private String globalJS;
+    private String bridgeJS;
+    private String pluginJS;
+    private String cordovaJS;
+    private String cordovaPluginsJS;
+    private String cordovaPluginsFileJS;
+    private String localUrlJS;
+
+    public JSInjector(
+        String globalJS,
+        String bridgeJS,
+        String pluginJS,
+        String cordovaJS,
+        String cordovaPluginsJS,
+        String cordovaPluginsFileJS,
+        String localUrlJS
+    ) {
+        this.globalJS = globalJS;
+        this.bridgeJS = bridgeJS;
+        this.pluginJS = pluginJS;
+        this.cordovaJS = cordovaJS;
+        this.cordovaPluginsJS = cordovaPluginsJS;
+        this.cordovaPluginsFileJS = cordovaPluginsFileJS;
+        this.localUrlJS = localUrlJS;
+    }
+
+    /**
+     * Generates injectable JS content.
+     * This may be used in other forms of injecting that aren't using an InputStream.
+     * @return
+     */
+    public String getScriptString() {
+        return (
+            globalJS +
+            "\n\n" +
+            localUrlJS +
+            "\n\n" +
+            bridgeJS +
+            "\n\n" +
+            pluginJS +
+            "\n\n" +
+            cordovaJS +
+            "\n\n" +
+            cordovaPluginsFileJS +
+            "\n\n" +
+            cordovaPluginsJS
+        );
+    }
+
+    /**
+     * Given an InputStream from the web server, prepend it with
+     * our JS stream
+     * @param responseStream
+     * @return
+     */
+    public InputStream getInjectedStream(InputStream responseStream) {
+        String js = "<script type=\"text/javascript\">" + getScriptString() + "</script>";
+        String html = this.readAssetStream(responseStream);
+        if (html.contains("<head>")) {
+            html = html.replace("<head>", "<head>\n" + js + "\n");
+        } else if (html.contains("</head>")) {
+            html = html.replace("</head>", js + "\n" + "</head>");
+        } else {
+            Logger.error("Unable to inject Capacitor, Plugins won't work");
+        }
+        return new ByteArrayInputStream(html.getBytes(StandardCharsets.UTF_8));
+    }
+
+    private String readAssetStream(InputStream stream) {
+        try {
+            final int bufferSize = 1024;
+            final char[] buffer = new char[bufferSize];
+            final StringBuilder out = new StringBuilder();
+            Reader in = new InputStreamReader(stream, StandardCharsets.UTF_8);
+            for (;;) {
+                int rsz = in.read(buffer, 0, buffer.length);
+                if (rsz < 0) break;
+                out.append(buffer, 0, rsz);
+            }
+            return out.toString();
+        } catch (Exception e) {
+            Logger.error("Unable to process HTML asset file. This is a fatal error", e);
+        }
+
+        return "";
+    }
+}

+ 164 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/JSObject.java

@@ -0,0 +1,164 @@
+package com.getcapacitor;
+
+import androidx.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * A wrapper around JSONObject that isn't afraid to do simple
+ * JSON put operations without having to throw an exception
+ * for every little thing jeez
+ */
+public class JSObject extends JSONObject {
+
+    public JSObject() {
+        super();
+    }
+
+    public JSObject(String json) throws JSONException {
+        super(json);
+    }
+
+    public JSObject(JSONObject obj, String[] names) throws JSONException {
+        super(obj, names);
+    }
+
+    /**
+     * Convert a pathetic JSONObject into a JSObject
+     * @param obj
+     */
+    public static JSObject fromJSONObject(JSONObject obj) throws JSONException {
+        Iterator<String> keysIter = obj.keys();
+        List<String> keys = new ArrayList<>();
+        while (keysIter.hasNext()) {
+            keys.add(keysIter.next());
+        }
+
+        return new JSObject(obj, keys.toArray(new String[keys.size()]));
+    }
+
+    @Override
+    @Nullable
+    public String getString(String key) {
+        return getString(key, null);
+    }
+
+    @Nullable
+    public String getString(String key, @Nullable String defaultValue) {
+        try {
+            String value = super.getString(key);
+            if (!super.isNull(key)) {
+                return value;
+            }
+        } catch (JSONException ex) {}
+        return defaultValue;
+    }
+
+    @Nullable
+    public Integer getInteger(String key) {
+        return getInteger(key, null);
+    }
+
+    @Nullable
+    public Integer getInteger(String key, @Nullable Integer defaultValue) {
+        try {
+            return super.getInt(key);
+        } catch (JSONException e) {}
+        return defaultValue;
+    }
+
+    @Nullable
+    public Boolean getBoolean(String key, @Nullable Boolean defaultValue) {
+        try {
+            return super.getBoolean(key);
+        } catch (JSONException e) {}
+        return defaultValue;
+    }
+
+    /**
+     * Fetch boolean from jsonObject
+     */
+    @Nullable
+    public Boolean getBool(String key) {
+        return getBoolean(key, null);
+    }
+
+    @Nullable
+    public JSObject getJSObject(String name) {
+        try {
+            return getJSObject(name, null);
+        } catch (JSONException e) {}
+        return null;
+    }
+
+    @Nullable
+    public JSObject getJSObject(String name, @Nullable JSObject defaultValue) throws JSONException {
+        try {
+            Object obj = get(name);
+            if (obj instanceof JSONObject) {
+                Iterator<String> keysIter = ((JSONObject) obj).keys();
+                List<String> keys = new ArrayList<>();
+                while (keysIter.hasNext()) {
+                    keys.add(keysIter.next());
+                }
+
+                return new JSObject((JSONObject) obj, keys.toArray(new String[keys.size()]));
+            }
+        } catch (JSONException ex) {}
+        return defaultValue;
+    }
+
+    @Override
+    public JSObject put(String key, boolean value) {
+        try {
+            super.put(key, value);
+        } catch (JSONException ex) {}
+        return this;
+    }
+
+    @Override
+    public JSObject put(String key, int value) {
+        try {
+            super.put(key, value);
+        } catch (JSONException ex) {}
+        return this;
+    }
+
+    @Override
+    public JSObject put(String key, long value) {
+        try {
+            super.put(key, value);
+        } catch (JSONException ex) {}
+        return this;
+    }
+
+    @Override
+    public JSObject put(String key, double value) {
+        try {
+            super.put(key, value);
+        } catch (JSONException ex) {}
+        return this;
+    }
+
+    @Override
+    public JSObject put(String key, Object value) {
+        try {
+            super.put(key, value);
+        } catch (JSONException ex) {}
+        return this;
+    }
+
+    public JSObject put(String key, String value) {
+        try {
+            super.put(key, value);
+        } catch (JSONException ex) {}
+        return this;
+    }
+
+    public JSObject putSafe(String key, Object value) throws JSONException {
+        return (JSObject) super.put(key, value);
+    }
+}

+ 65 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/JSValue.java

@@ -0,0 +1,65 @@
+package com.getcapacitor;
+
+import org.json.JSONException;
+
+/**
+ * Represents a single user-data value of any type on the capacitor PluginCall object.
+ */
+public class JSValue {
+
+    private final Object value;
+
+    /**
+     * @param call The capacitor plugin call, used for accessing the value safely.
+     * @param name The name of the property to access.
+     */
+    public JSValue(PluginCall call, String name) {
+        this.value = this.toValue(call, name);
+    }
+
+    /**
+     * Returns the coerced but uncasted underlying value.
+     */
+    public Object getValue() {
+        return this.value;
+    }
+
+    @Override
+    public String toString() {
+        return this.getValue().toString();
+    }
+
+    /**
+     * Returns the underlying value as a JSObject, or throwing if it cannot.
+     *
+     * @throws JSONException If the underlying value is not a JSObject.
+     */
+    public JSObject toJSObject() throws JSONException {
+        if (this.value instanceof JSObject) return (JSObject) this.value;
+        throw new JSONException("JSValue could not be coerced to JSObject.");
+    }
+
+    /**
+     * Returns the underlying value as a JSArray, or throwing if it cannot.
+     *
+     * @throws JSONException If the underlying value is not a JSArray.
+     */
+    public JSArray toJSArray() throws JSONException {
+        if (this.value instanceof JSArray) return (JSArray) this.value;
+        throw new JSONException("JSValue could not be coerced to JSArray.");
+    }
+
+    /**
+     * Returns the underlying value this object represents, coercing it into a capacitor-friendly object if supported.
+     */
+    private Object toValue(PluginCall call, String name) {
+        Object value = null;
+        value = call.getArray(name, null);
+        if (value != null) return value;
+        value = call.getObject(name, null);
+        if (value != null) return value;
+        value = call.getString(name, null);
+        if (value != null) return value;
+        return call.getData().opt(name);
+    }
+}

+ 103 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/Logger.java

@@ -0,0 +1,103 @@
+package com.getcapacitor;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+public class Logger {
+
+    public static final String LOG_TAG_CORE = "Capacitor";
+    public static CapConfig config;
+
+    private static Logger instance;
+
+    private static Logger getInstance() {
+        if (instance == null) {
+            instance = new Logger();
+        }
+        return instance;
+    }
+
+    public static void init(CapConfig config) {
+        Logger.getInstance().loadConfig(config);
+    }
+
+    private void loadConfig(CapConfig config) {
+        Logger.config = config;
+    }
+
+    public static String tags(String... subtags) {
+        if (subtags != null && subtags.length > 0) {
+            return LOG_TAG_CORE + "/" + TextUtils.join("/", subtags);
+        }
+
+        return LOG_TAG_CORE;
+    }
+
+    public static void verbose(String message) {
+        verbose(LOG_TAG_CORE, message);
+    }
+
+    public static void verbose(String tag, String message) {
+        if (!shouldLog()) {
+            return;
+        }
+
+        Log.v(tag, message);
+    }
+
+    public static void debug(String message) {
+        debug(LOG_TAG_CORE, message);
+    }
+
+    public static void debug(String tag, String message) {
+        if (!shouldLog()) {
+            return;
+        }
+
+        Log.d(tag, message);
+    }
+
+    public static void info(String message) {
+        info(LOG_TAG_CORE, message);
+    }
+
+    public static void info(String tag, String message) {
+        if (!shouldLog()) {
+            return;
+        }
+
+        Log.i(tag, message);
+    }
+
+    public static void warn(String message) {
+        warn(LOG_TAG_CORE, message);
+    }
+
+    public static void warn(String tag, String message) {
+        if (!shouldLog()) {
+            return;
+        }
+
+        Log.w(tag, message);
+    }
+
+    public static void error(String message) {
+        error(LOG_TAG_CORE, message, null);
+    }
+
+    public static void error(String message, Throwable e) {
+        error(LOG_TAG_CORE, message, e);
+    }
+
+    public static void error(String tag, String message, Throwable e) {
+        if (!shouldLog()) {
+            return;
+        }
+
+        Log.e(tag, message, e);
+    }
+
+    public static boolean shouldLog() {
+        return config == null || config.isLoggingEnabled();
+    }
+}

+ 159 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/MessageHandler.java

@@ -0,0 +1,159 @@
+package com.getcapacitor;
+
+import android.webkit.JavascriptInterface;
+import android.webkit.WebView;
+import androidx.webkit.JavaScriptReplyProxy;
+import androidx.webkit.WebViewCompat;
+import androidx.webkit.WebViewFeature;
+import org.apache.cordova.PluginManager;
+
+/**
+ * MessageHandler handles messages from the WebView, dispatching them
+ * to plugins.
+ */
+public class MessageHandler {
+
+    private Bridge bridge;
+    private WebView webView;
+    private PluginManager cordovaPluginManager;
+    private JavaScriptReplyProxy javaScriptReplyProxy;
+
+    public MessageHandler(Bridge bridge, WebView webView, PluginManager cordovaPluginManager) {
+        this.bridge = bridge;
+        this.webView = webView;
+        this.cordovaPluginManager = cordovaPluginManager;
+
+        if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER) && !bridge.getConfig().isUsingLegacyBridge()) {
+            WebViewCompat.WebMessageListener capListener = (view, message, sourceOrigin, isMainFrame, replyProxy) -> {
+                if (isMainFrame) {
+                    postMessage(message.getData());
+                    javaScriptReplyProxy = replyProxy;
+                } else {
+                    Logger.warn("Plugin execution is allowed in Main Frame only");
+                }
+            };
+            try {
+                WebViewCompat.addWebMessageListener(webView, "androidBridge", bridge.getAllowedOriginRules(), capListener);
+            } catch (Exception ex) {
+                webView.addJavascriptInterface(this, "androidBridge");
+            }
+        } else {
+            webView.addJavascriptInterface(this, "androidBridge");
+        }
+    }
+
+    /**
+     * The main message handler that will be called from JavaScript
+     * to send a message to the native bridge.
+     * @param jsonStr
+     */
+    @JavascriptInterface
+    @SuppressWarnings("unused")
+    public void postMessage(String jsonStr) {
+        try {
+            JSObject postData = new JSObject(jsonStr);
+
+            String type = postData.getString("type");
+
+            boolean typeIsNotNull = type != null;
+            boolean isCordovaPlugin = typeIsNotNull && type.equals("cordova");
+            boolean isJavaScriptError = typeIsNotNull && type.equals("js.error");
+
+            String callbackId = postData.getString("callbackId");
+
+            if (isCordovaPlugin) {
+                String service = postData.getString("service");
+                String action = postData.getString("action");
+                String actionArgs = postData.getString("actionArgs");
+
+                Logger.verbose(
+                    Logger.tags("Plugin"),
+                    "To native (Cordova plugin): callbackId: " +
+                    callbackId +
+                    ", service: " +
+                    service +
+                    ", action: " +
+                    action +
+                    ", actionArgs: " +
+                    actionArgs
+                );
+
+                this.callCordovaPluginMethod(callbackId, service, action, actionArgs);
+            } else if (isJavaScriptError) {
+                Logger.error("JavaScript Error: " + jsonStr);
+            } else {
+                String pluginId = postData.getString("pluginId");
+                String methodName = postData.getString("methodName");
+                JSObject methodData = postData.getJSObject("options", new JSObject());
+
+                Logger.verbose(
+                    Logger.tags("Plugin"),
+                    "To native (Capacitor plugin): callbackId: " + callbackId + ", pluginId: " + pluginId + ", methodName: " + methodName
+                );
+
+                this.callPluginMethod(callbackId, pluginId, methodName, methodData);
+            }
+        } catch (Exception ex) {
+            Logger.error("Post message error:", ex);
+        }
+    }
+
+    public void sendResponseMessage(PluginCall call, PluginResult successResult, PluginResult errorResult) {
+        try {
+            PluginResult data = new PluginResult();
+            data.put("save", call.isKeptAlive());
+            data.put("callbackId", call.getCallbackId());
+            data.put("pluginId", call.getPluginId());
+            data.put("methodName", call.getMethodName());
+
+            boolean pluginResultInError = errorResult != null;
+            if (pluginResultInError) {
+                data.put("success", false);
+                data.put("error", errorResult);
+                Logger.debug("Sending plugin error: " + data.toString());
+            } else {
+                data.put("success", true);
+                if (successResult != null) {
+                    data.put("data", successResult);
+                }
+            }
+
+            boolean isValidCallbackId = !call.getCallbackId().equals(PluginCall.CALLBACK_ID_DANGLING);
+            if (isValidCallbackId) {
+                if (bridge.getConfig().isUsingLegacyBridge()) {
+                    legacySendResponseMessage(data);
+                } else if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER) && javaScriptReplyProxy != null) {
+                    javaScriptReplyProxy.postMessage(data.toString());
+                } else {
+                    legacySendResponseMessage(data);
+                }
+            } else {
+                bridge.getApp().fireRestoredResult(data);
+            }
+        } catch (Exception ex) {
+            Logger.error("sendResponseMessage: error: " + ex);
+        }
+        if (!call.isKeptAlive()) {
+            call.release(bridge);
+        }
+    }
+
+    private void legacySendResponseMessage(PluginResult data) {
+        final String runScript = "window.Capacitor.fromNative(" + data.toString() + ")";
+        final WebView webView = this.webView;
+        webView.post(() -> webView.evaluateJavascript(runScript, null));
+    }
+
+    private void callPluginMethod(String callbackId, String pluginId, String methodName, JSObject methodData) {
+        PluginCall call = new PluginCall(this, pluginId, callbackId, methodName, methodData);
+        bridge.callPluginMethod(pluginId, methodName, call);
+    }
+
+    private void callCordovaPluginMethod(String callbackId, String service, String action, String actionArgs) {
+        bridge.execute(
+            () -> {
+                cordovaPluginManager.exec(service, action, callbackId, actionArgs);
+            }
+        );
+    }
+}

+ 37 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/NativePlugin.java

@@ -0,0 +1,37 @@
+package com.getcapacitor;
+
+import com.getcapacitor.annotation.CapacitorPlugin;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Base annotation for all Plugins
+ * @deprecated
+ * <p> Use {@link CapacitorPlugin} instead
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Deprecated
+public @interface NativePlugin {
+    /**
+     * Request codes this plugin uses and responds to, in order to tie
+     * Android events back the plugin to handle
+     */
+    int[] requestCodes() default {};
+
+    /**
+     * Permissions this plugin needs, in order to make permission requests
+     * easy if the plugin only needs basic permission prompting
+     */
+    String[] permissions() default {};
+
+    /**
+     * The request code to use when automatically requesting permissions
+     */
+    int permissionRequestCode() default 9000;
+
+    /**
+     * A custom name for the plugin, otherwise uses the
+     * simple class name.
+     */
+    String name() default "";
+}

+ 31 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/PermissionState.java

@@ -0,0 +1,31 @@
+package com.getcapacitor;
+
+import java.util.Locale;
+
+/**
+ * Represents the state of a permission
+ *
+ * @since 3.0.0
+ */
+public enum PermissionState {
+    GRANTED("granted"),
+    DENIED("denied"),
+    PROMPT("prompt"),
+    PROMPT_WITH_RATIONALE("prompt-with-rationale");
+
+    private String state;
+
+    PermissionState(String state) {
+        this.state = state;
+    }
+
+    @Override
+    public String toString() {
+        return state;
+    }
+
+    public static PermissionState byState(String state) {
+        state = state.toUpperCase(Locale.ROOT).replace('-', '_');
+        return valueOf(state);
+    }
+}

+ 1046 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/Plugin.java

@@ -0,0 +1,1046 @@
+package com.getcapacitor;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.net.Uri;
+import android.os.Bundle;
+import androidx.activity.result.ActivityResult;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+import com.getcapacitor.annotation.ActivityCallback;
+import com.getcapacitor.annotation.CapacitorPlugin;
+import com.getcapacitor.annotation.Permission;
+import com.getcapacitor.annotation.PermissionCallback;
+import com.getcapacitor.util.PermissionHelper;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import org.json.JSONException;
+
+/**
+ * Plugin is the base class for all plugins, containing a number of
+ * convenient features for interacting with the {@link Bridge}, managing
+ * plugin permissions, tracking lifecycle events, and more.
+ *
+ * You should inherit from this class when creating new plugins, along with
+ * adding the {@link CapacitorPlugin} annotation to add additional required
+ * metadata about the Plugin
+ */
+public class Plugin {
+
+    // The key we will use inside of a persisted Bundle for the JSON blob
+    // for a plugin call options.
+    private static final String BUNDLE_PERSISTED_OPTIONS_JSON_KEY = "_json";
+
+    // Reference to the Bridge
+    protected Bridge bridge;
+
+    // Reference to the PluginHandle wrapper for this Plugin
+    protected PluginHandle handle;
+
+    /**
+     * A way for plugins to quickly save a call that they will need to reference
+     * between activity/permissions starts/requests
+     *
+     * @deprecated store calls on the bridge using the methods
+     * {@link com.getcapacitor.Bridge#saveCall(PluginCall)},
+     * {@link com.getcapacitor.Bridge#getSavedCall(String)} and
+     * {@link com.getcapacitor.Bridge#releaseCall(PluginCall)}
+     */
+    @Deprecated
+    protected PluginCall savedLastCall;
+
+    // Stored event listeners
+    private final Map<String, List<PluginCall>> eventListeners;
+
+    /**
+     * Launchers used by the plugin to handle activity results
+     */
+    private final Map<String, ActivityResultLauncher<Intent>> activityLaunchers = new HashMap<>();
+
+    /**
+     * Launchers used by the plugin to handle permission results
+     */
+    private final Map<String, ActivityResultLauncher<String[]>> permissionLaunchers = new HashMap<>();
+
+    private String lastPluginCallId;
+
+    // Stored results of an event if an event was fired and
+    // no listeners were attached yet. Only stores the last value.
+    private final Map<String, List<JSObject>> retainedEventArguments;
+
+    public Plugin() {
+        eventListeners = new HashMap<>();
+        retainedEventArguments = new HashMap<>();
+    }
+
+    /**
+     * Called when the plugin has been connected to the bridge
+     * and is ready to start initializing.
+     */
+    public void load() {}
+
+    /**
+     * Registers activity result launchers defined on plugins, used for permission requests and
+     * activities started for result.
+     */
+    void initializeActivityLaunchers() {
+        List<Method> pluginClassMethods = new ArrayList<>();
+        for (
+            Class<?> pluginCursor = getClass();
+            !pluginCursor.getName().equals(Object.class.getName());
+            pluginCursor = pluginCursor.getSuperclass()
+        ) {
+            pluginClassMethods.addAll(Arrays.asList(pluginCursor.getDeclaredMethods()));
+        }
+
+        for (final Method method : pluginClassMethods) {
+            if (method.isAnnotationPresent(ActivityCallback.class)) {
+                // register callbacks annotated with ActivityCallback for activity results
+                ActivityResultLauncher<Intent> launcher = bridge.registerForActivityResult(
+                    new ActivityResultContracts.StartActivityForResult(),
+                    result -> triggerActivityCallback(method, result)
+                );
+
+                activityLaunchers.put(method.getName(), launcher);
+            } else if (method.isAnnotationPresent(PermissionCallback.class)) {
+                // register callbacks annotated with PermissionCallback for permission results
+                ActivityResultLauncher<String[]> launcher = bridge.registerForActivityResult(
+                    new ActivityResultContracts.RequestMultiplePermissions(),
+                    permissions -> triggerPermissionCallback(method, permissions)
+                );
+
+                permissionLaunchers.put(method.getName(), launcher);
+            }
+        }
+    }
+
+    private void triggerPermissionCallback(Method method, Map<String, Boolean> permissionResultMap) {
+        PluginCall savedCall = bridge.getPermissionCall(handle.getId());
+
+        // validate permissions and invoke the permission result callback
+        if (bridge.validatePermissions(this, savedCall, permissionResultMap)) {
+            try {
+                method.setAccessible(true);
+                method.invoke(this, savedCall);
+            } catch (IllegalAccessException | InvocationTargetException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    private void triggerActivityCallback(Method method, ActivityResult result) {
+        PluginCall savedCall = bridge.getSavedCall(lastPluginCallId);
+        if (savedCall == null) {
+            savedCall = bridge.getPluginCallForLastActivity();
+        }
+        // invoke the activity result callback
+        try {
+            method.setAccessible(true);
+            method.invoke(this, savedCall, result);
+        } catch (IllegalAccessException | InvocationTargetException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * Start activity for result with the provided Intent and resolve with the provided callback method name.
+     * <p>
+     * If there is no registered activity callback for the method name passed in, the call will
+     * be rejected. Make sure a valid activity result callback method is registered using the
+     * {@link ActivityCallback} annotation.
+     *
+     * @param call the plugin call
+     * @param intent the intent used to start an activity
+     * @param callbackName the name of the callback to run when the launched activity is finished
+     * @since 3.0.0
+     */
+    public void startActivityForResult(PluginCall call, Intent intent, String callbackName) {
+        ActivityResultLauncher<Intent> activityResultLauncher = getActivityLauncherOrReject(call, callbackName);
+        if (activityResultLauncher == null) {
+            // return when null since call was rejected in getLauncherOrReject
+            return;
+        }
+        bridge.setPluginCallForLastActivity(call);
+        lastPluginCallId = call.getCallbackId();
+        bridge.saveCall(call);
+        activityResultLauncher.launch(intent);
+    }
+
+    private void permissionActivityResult(PluginCall call, String[] permissionStrings, String callbackName) {
+        ActivityResultLauncher<String[]> permissionResultLauncher = getPermissionLauncherOrReject(call, callbackName);
+        if (permissionResultLauncher == null) {
+            // return when null since call was rejected in getLauncherOrReject
+            return;
+        }
+
+        bridge.savePermissionCall(call);
+        permissionResultLauncher.launch(permissionStrings);
+    }
+
+    /**
+     * Get the main {@link Context} for the current Activity (your app)
+     * @return the Context for the current activity
+     */
+    public Context getContext() {
+        return this.bridge.getContext();
+    }
+
+    /**
+     * Get the main {@link Activity} for the app
+     * @return the Activity for the current app
+     */
+    public AppCompatActivity getActivity() {
+        return this.bridge.getActivity();
+    }
+
+    /**
+     * Set the Bridge instance for this plugin
+     * @param bridge
+     */
+    public void setBridge(Bridge bridge) {
+        this.bridge = bridge;
+    }
+
+    /**
+     * Get the Bridge instance for this plugin
+     */
+    public Bridge getBridge() {
+        return this.bridge;
+    }
+
+    /**
+     * Set the wrapper {@link PluginHandle} instance for this plugin that
+     * contains additional metadata about the Plugin instance (such
+     * as indexed methods for reflection, and {@link CapacitorPlugin} annotation data).
+     * @param pluginHandle
+     */
+    public void setPluginHandle(PluginHandle pluginHandle) {
+        this.handle = pluginHandle;
+    }
+
+    /**
+     * Return the wrapper {@link PluginHandle} for this plugin.
+     *
+     * This wrapper contains additional metadata about the plugin instance,
+     * such as indexed methods for reflection, and {@link CapacitorPlugin} annotation data).
+     * @return
+     */
+    public PluginHandle getPluginHandle() {
+        return this.handle;
+    }
+
+    /**
+     * Get the root App ID
+     * @return
+     */
+    public String getAppId() {
+        return getContext().getPackageName();
+    }
+
+    /**
+     * Called to save a {@link PluginCall} in order to reference it
+     * later, such as in an activity or permissions result handler
+     * @deprecated use {@link Bridge#saveCall(PluginCall)}
+     *
+     * @param lastCall
+     */
+    @Deprecated
+    public void saveCall(PluginCall lastCall) {
+        this.savedLastCall = lastCall;
+    }
+
+    /**
+     * Set the last saved call to null to free memory
+     * @deprecated use {@link PluginCall#release(Bridge)}
+     */
+    @Deprecated
+    public void freeSavedCall() {
+        this.savedLastCall.release(bridge);
+        this.savedLastCall = null;
+    }
+
+    /**
+     * Get the last saved call, if any
+     * @deprecated use {@link Bridge#getSavedCall(String)}
+     *
+     * @return
+     */
+    @Deprecated
+    public PluginCall getSavedCall() {
+        return this.savedLastCall;
+    }
+
+    /**
+     * Get the config options for this plugin.
+     *
+     * @return a config object representing the plugin config options, or an empty config
+     * if none exists
+     */
+    public PluginConfig getConfig() {
+        return bridge.getConfig().getPluginConfiguration(handle.getId());
+    }
+
+    /**
+     * Get the value for a key on the config for this plugin.
+     * @deprecated use {@link #getConfig()} and access config values using the methods available
+     * depending on the type.
+     *
+     * @param key the key for the config value
+     * @return some object containing the value from the config
+     */
+    @Deprecated
+    public Object getConfigValue(String key) {
+        try {
+            PluginConfig pluginConfig = getConfig();
+            return pluginConfig.getConfigJSON().get(key);
+        } catch (JSONException ex) {
+            return null;
+        }
+    }
+
+    /**
+     * Check whether any of the given permissions has been defined in the AndroidManifest.xml
+     * @deprecated use {@link #isPermissionDeclared(String)}
+     *
+     * @param permissions
+     * @return
+     */
+    @Deprecated
+    public boolean hasDefinedPermissions(String[] permissions) {
+        for (String permission : permissions) {
+            if (!PermissionHelper.hasDefinedPermission(getContext(), permission)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Check if all annotated permissions have been defined in the AndroidManifest.xml
+     * @deprecated use {@link #isPermissionDeclared(String)}
+     *
+     * @return true if permissions are all defined in the Manifest
+     */
+    @Deprecated
+    public boolean hasDefinedRequiredPermissions() {
+        CapacitorPlugin annotation = handle.getPluginAnnotation();
+        if (annotation == null) {
+            // Check for legacy plugin annotation, @NativePlugin
+            NativePlugin legacyAnnotation = handle.getLegacyPluginAnnotation();
+            return hasDefinedPermissions(legacyAnnotation.permissions());
+        } else {
+            for (Permission perm : annotation.permissions()) {
+                for (String permString : perm.strings()) {
+                    if (!PermissionHelper.hasDefinedPermission(getContext(), permString)) {
+                        return false;
+                    }
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Checks if the given permission alias is correctly declared in AndroidManifest.xml
+     * @param alias a permission alias defined on the plugin
+     * @return true only if all permissions associated with the given alias are declared in the manifest
+     */
+    public boolean isPermissionDeclared(String alias) {
+        CapacitorPlugin annotation = handle.getPluginAnnotation();
+        if (annotation != null) {
+            for (Permission perm : annotation.permissions()) {
+                if (alias.equalsIgnoreCase(perm.alias())) {
+                    boolean result = true;
+                    for (String permString : perm.strings()) {
+                        result = result && PermissionHelper.hasDefinedPermission(getContext(), permString);
+                    }
+
+                    return result;
+                }
+            }
+        }
+
+        Logger.error(String.format("isPermissionDeclared: No alias defined for %s " + "or missing @CapacitorPlugin annotation.", alias));
+        return false;
+    }
+
+    /**
+     * Check whether the given permission has been granted by the user
+     * @deprecated use {@link #getPermissionState(String)} and {@link #getPermissionStates()} to get
+     * the states of permissions defined on the Plugin in conjunction with the @CapacitorPlugin
+     * annotation. Use the Android API {@link ActivityCompat#checkSelfPermission(Context, String)}
+     * methods to check permissions with Android permission strings
+     *
+     * @param permission
+     * @return
+     */
+    @Deprecated
+    public boolean hasPermission(String permission) {
+        return ActivityCompat.checkSelfPermission(this.getContext(), permission) == PackageManager.PERMISSION_GRANTED;
+    }
+
+    /**
+     * If the plugin annotation specified a set of permissions, this method checks if each is
+     * granted
+     * @deprecated use {@link #getPermissionState(String)} or {@link #getPermissionStates()} to
+     * check whether permissions are granted or not
+     *
+     * @return
+     */
+    @Deprecated
+    public boolean hasRequiredPermissions() {
+        CapacitorPlugin annotation = handle.getPluginAnnotation();
+        if (annotation == null) {
+            // Check for legacy plugin annotation, @NativePlugin
+            NativePlugin legacyAnnotation = handle.getLegacyPluginAnnotation();
+            for (String perm : legacyAnnotation.permissions()) {
+                if (ActivityCompat.checkSelfPermission(this.getContext(), perm) != PackageManager.PERMISSION_GRANTED) {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        for (Permission perm : annotation.permissions()) {
+            for (String permString : perm.strings()) {
+                if (ActivityCompat.checkSelfPermission(this.getContext(), permString) != PackageManager.PERMISSION_GRANTED) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Request all of the specified permissions in the CapacitorPlugin annotation (if any)
+     *
+     * If there is no registered permission callback for the PluginCall passed in, the call will
+     * be rejected. Make sure a valid permission callback method is registered using the
+     * {@link PermissionCallback} annotation.
+     *
+     * @since 3.0.0
+     * @param call the plugin call
+     * @param callbackName the name of the callback to run when the permission request is complete
+     */
+    protected void requestAllPermissions(@NonNull PluginCall call, @NonNull String callbackName) {
+        CapacitorPlugin annotation = handle.getPluginAnnotation();
+        if (annotation != null) {
+            HashSet<String> perms = new HashSet<>();
+            for (Permission perm : annotation.permissions()) {
+                perms.addAll(Arrays.asList(perm.strings()));
+            }
+
+            permissionActivityResult(call, perms.toArray(new String[0]), callbackName);
+        }
+    }
+
+    /**
+     * Request permissions using an alias defined on the plugin.
+     *
+     * If there is no registered permission callback for the PluginCall passed in, the call will
+     * be rejected. Make sure a valid permission callback method is registered using the
+     * {@link PermissionCallback} annotation.
+     *
+     * @param alias an alias defined on the plugin
+     * @param call  the plugin call involved in originating the request
+     * @param callbackName the name of the callback to run when the permission request is complete
+     */
+    protected void requestPermissionForAlias(@NonNull String alias, @NonNull PluginCall call, @NonNull String callbackName) {
+        requestPermissionForAliases(new String[] { alias }, call, callbackName);
+    }
+
+    /**
+     * Request permissions using aliases defined on the plugin.
+     *
+     * If there is no registered permission callback for the PluginCall passed in, the call will
+     * be rejected. Make sure a valid permission callback method is registered using the
+     * {@link PermissionCallback} annotation.
+     *
+     * @param aliases a set of aliases defined on the plugin
+     * @param call    the plugin call involved in originating the request
+     * @param callbackName the name of the callback to run when the permission request is complete
+     */
+    protected void requestPermissionForAliases(@NonNull String[] aliases, @NonNull PluginCall call, @NonNull String callbackName) {
+        if (aliases.length == 0) {
+            Logger.error("No permission alias was provided");
+            return;
+        }
+
+        String[] permissions = getPermissionStringsForAliases(aliases);
+
+        if (permissions.length > 0) {
+            permissionActivityResult(call, permissions, callbackName);
+        }
+    }
+
+    /**
+     * Gets the Android permission strings defined on the {@link CapacitorPlugin} annotation with
+     * the provided aliases.
+     *
+     * @param aliases aliases for permissions defined on the plugin
+     * @return Android permission strings associated with the provided aliases, if exists
+     */
+    private String[] getPermissionStringsForAliases(@NonNull String[] aliases) {
+        CapacitorPlugin annotation = handle.getPluginAnnotation();
+        HashSet<String> perms = new HashSet<>();
+        for (Permission perm : annotation.permissions()) {
+            if (Arrays.asList(aliases).contains(perm.alias())) {
+                perms.addAll(Arrays.asList(perm.strings()));
+            }
+        }
+
+        return perms.toArray(new String[0]);
+    }
+
+    /**
+     * Gets the activity launcher associated with the calling methodName, or rejects the call if
+     * no registered launcher exists
+     *
+     * @param call       the plugin call
+     * @param methodName the name of the activity callback method
+     * @return a launcher, or null if none found
+     */
+    private @Nullable ActivityResultLauncher<Intent> getActivityLauncherOrReject(PluginCall call, String methodName) {
+        ActivityResultLauncher<Intent> activityLauncher = activityLaunchers.get(methodName);
+
+        // if there is no registered launcher, reject the call with an error and return null
+        if (activityLauncher == null) {
+            String registerError =
+                "There is no ActivityCallback method registered for the name: %s. " +
+                "Please define a callback method annotated with @ActivityCallback " +
+                "that receives arguments: (PluginCall, ActivityResult)";
+            registerError = String.format(Locale.US, registerError, methodName);
+            Logger.error(registerError);
+            call.reject(registerError);
+            return null;
+        }
+
+        return activityLauncher;
+    }
+
+    /**
+     * Gets the permission launcher associated with the calling methodName, or rejects the call if
+     * no registered launcher exists
+     *
+     * @param call       the plugin call
+     * @param methodName the name of the permission callback method
+     * @return a launcher, or null if none found
+     */
+    private @Nullable ActivityResultLauncher<String[]> getPermissionLauncherOrReject(PluginCall call, String methodName) {
+        ActivityResultLauncher<String[]> permissionLauncher = permissionLaunchers.get(methodName);
+
+        // if there is no registered launcher, reject the call with an error and return null
+        if (permissionLauncher == null) {
+            String registerError =
+                "There is no PermissionCallback method registered for the name: %s. " +
+                "Please define a callback method annotated with @PermissionCallback " +
+                "that receives arguments: (PluginCall)";
+            registerError = String.format(Locale.US, registerError, methodName);
+            Logger.error(registerError);
+            call.reject(registerError);
+            return null;
+        }
+
+        return permissionLauncher;
+    }
+
+    /**
+     * Request all of the specified permissions in the CapacitorPlugin annotation (if any)
+     *
+     * @deprecated use {@link #requestAllPermissions(PluginCall, String)} in conjunction with @CapacitorPlugin
+     */
+    @Deprecated
+    public void pluginRequestAllPermissions() {
+        NativePlugin legacyAnnotation = handle.getLegacyPluginAnnotation();
+        ActivityCompat.requestPermissions(getActivity(), legacyAnnotation.permissions(), legacyAnnotation.permissionRequestCode());
+    }
+
+    /**
+     * Helper for requesting a specific permission
+     *
+     * @param permission  the permission to request
+     * @param requestCode the requestCode to use to associate the result with the plugin
+     * @deprecated use {@link #requestPermissionForAlias(String, PluginCall, String)} in conjunction with @CapacitorPlugin
+     */
+    @Deprecated
+    public void pluginRequestPermission(String permission, int requestCode) {
+        ActivityCompat.requestPermissions(getActivity(), new String[] { permission }, requestCode);
+    }
+
+    /**
+     * Helper for requesting specific permissions
+     * @deprecated use {@link #requestPermissionForAliases(String[], PluginCall, String)} in conjunction
+     * with @CapacitorPlugin
+     *
+     * @param permissions the set of permissions to request
+     * @param requestCode the requestCode to use to associate the result with the plugin
+     */
+    @Deprecated
+    public void pluginRequestPermissions(String[] permissions, int requestCode) {
+        ActivityCompat.requestPermissions(getActivity(), permissions, requestCode);
+    }
+
+    /**
+     * Get the permission state for the provided permission alias.
+     *
+     * @param alias the permission alias to get
+     * @return the state of the provided permission alias or null
+     */
+    public PermissionState getPermissionState(String alias) {
+        return getPermissionStates().get(alias);
+    }
+
+    /**
+     * Helper to check all permissions defined on a plugin and see the state of each.
+     *
+     * @since 3.0.0
+     * @return A mapping of permission aliases to the associated granted status.
+     */
+    public Map<String, PermissionState> getPermissionStates() {
+        return bridge.getPermissionStates(this);
+    }
+
+    /**
+     * Add a listener for the given event
+     * @param eventName
+     * @param call
+     */
+    private void addEventListener(String eventName, PluginCall call) {
+        List<PluginCall> listeners = eventListeners.get(eventName);
+        if (listeners == null || listeners.isEmpty()) {
+            listeners = new ArrayList<>();
+            eventListeners.put(eventName, listeners);
+
+            // Must add the call before sending retained arguments
+            listeners.add(call);
+
+            sendRetainedArgumentsForEvent(eventName);
+        } else {
+            listeners.add(call);
+        }
+    }
+
+    /**
+     * Remove a listener from the given event
+     * @param eventName
+     * @param call
+     */
+    private void removeEventListener(String eventName, PluginCall call) {
+        List<PluginCall> listeners = eventListeners.get(eventName);
+        if (listeners == null) {
+            return;
+        }
+
+        listeners.remove(call);
+    }
+
+    /**
+     * Notify all listeners that an event occurred
+     * @param eventName
+     * @param data
+     */
+    protected void notifyListeners(String eventName, JSObject data, boolean retainUntilConsumed) {
+        Logger.verbose(getLogTag(), "Notifying listeners for event " + eventName);
+        List<PluginCall> listeners = eventListeners.get(eventName);
+        if (listeners == null || listeners.isEmpty()) {
+            Logger.debug(getLogTag(), "No listeners found for event " + eventName);
+            if (retainUntilConsumed) {
+                List<JSObject> argList = retainedEventArguments.get(eventName);
+
+                if (argList == null) {
+                    argList = new ArrayList<JSObject>();
+                }
+
+                argList.add(data);
+                retainedEventArguments.put(eventName, argList);
+            }
+            return;
+        }
+
+        CopyOnWriteArrayList<PluginCall> listenersCopy = new CopyOnWriteArrayList(listeners);
+        for (PluginCall call : listenersCopy) {
+            call.resolve(data);
+        }
+    }
+
+    /**
+     * Notify all listeners that an event occurred
+     * This calls {@link Plugin#notifyListeners(String, JSObject, boolean)}
+     * with retainUntilConsumed set to false
+     * @param eventName
+     * @param data
+     */
+    protected void notifyListeners(String eventName, JSObject data) {
+        notifyListeners(eventName, data, false);
+    }
+
+    /**
+     * Check if there are any listeners for the given event
+     */
+    protected boolean hasListeners(String eventName) {
+        List<PluginCall> listeners = eventListeners.get(eventName);
+        if (listeners == null) {
+            return false;
+        }
+        return !listeners.isEmpty();
+    }
+
+    /**
+     * Send retained arguments (if any) for this event. This
+     * is called only when the first listener for an event is added
+     * @param eventName
+     */
+    private void sendRetainedArgumentsForEvent(String eventName) {
+        // copy retained args and null source to prevent potential race conditions
+        List<JSObject> retainedArgs = retainedEventArguments.get(eventName);
+        if (retainedArgs == null) {
+            return;
+        }
+
+        retainedEventArguments.remove(eventName);
+
+        for (JSObject retained : retainedArgs) {
+            notifyListeners(eventName, retained);
+        }
+    }
+
+    /**
+     * Exported plugin call for adding a listener to this plugin
+     * @param call
+     */
+    @SuppressWarnings("unused")
+    @PluginMethod(returnType = PluginMethod.RETURN_NONE)
+    public void addListener(PluginCall call) {
+        String eventName = call.getString("eventName");
+        call.setKeepAlive(true);
+        addEventListener(eventName, call);
+    }
+
+    /**
+     * Exported plugin call to remove a listener from this plugin
+     * @param call
+     */
+    @SuppressWarnings("unused")
+    @PluginMethod(returnType = PluginMethod.RETURN_NONE)
+    public void removeListener(PluginCall call) {
+        String eventName = call.getString("eventName");
+        String callbackId = call.getString("callbackId");
+        PluginCall savedCall = bridge.getSavedCall(callbackId);
+        if (savedCall != null) {
+            removeEventListener(eventName, savedCall);
+            bridge.releaseCall(savedCall);
+        }
+    }
+
+    /**
+     * Exported plugin call to remove all listeners from this plugin
+     * @param call
+     */
+    @SuppressWarnings("unused")
+    @PluginMethod(returnType = PluginMethod.RETURN_PROMISE)
+    public void removeAllListeners(PluginCall call) {
+        eventListeners.clear();
+        call.resolve();
+    }
+
+    /**
+     * Exported plugin call for checking the granted status for each permission
+     * declared on the plugin. This plugin call responds with a mapping of permissions to
+     * the associated granted status.
+     *
+     * @since 3.0.0
+     */
+    @PluginMethod
+    @PermissionCallback
+    public void checkPermissions(PluginCall pluginCall) {
+        Map<String, PermissionState> permissionsResult = getPermissionStates();
+
+        if (permissionsResult.size() == 0) {
+            // if no permissions are defined on the plugin, resolve undefined
+            pluginCall.resolve();
+        } else {
+            JSObject permissionsResultJSON = new JSObject();
+            for (Map.Entry<String, PermissionState> entry : permissionsResult.entrySet()) {
+                permissionsResultJSON.put(entry.getKey(), entry.getValue());
+            }
+
+            pluginCall.resolve(permissionsResultJSON);
+        }
+    }
+
+    /**
+     * Exported plugin call to request all permissions for this plugin.
+     * To manually request permissions within a plugin use:
+     * {@link #requestAllPermissions(PluginCall, String)}, or
+     * {@link #requestPermissionForAlias(String, PluginCall, String)}, or
+     * {@link #requestPermissionForAliases(String[], PluginCall, String)}
+     *
+     * @param call the plugin call
+     */
+    @PluginMethod
+    public void requestPermissions(PluginCall call) {
+        CapacitorPlugin annotation = handle.getPluginAnnotation();
+        if (annotation == null) {
+            handleLegacyPermission(call);
+        } else {
+            // handle permission requests for plugins defined with @CapacitorPlugin (since 3.0.0)
+            String[] permAliases = null;
+            Set<String> autoGrantPerms = new HashSet<>();
+
+            // If call was made with a list of specific permission aliases to request, save them
+            // to be requested
+            JSArray providedPerms = call.getArray("permissions");
+            List<String> providedPermsList = null;
+
+            if (providedPerms != null) {
+                try {
+                    providedPermsList = providedPerms.toList();
+                } catch (JSONException ignore) {
+                    // do nothing
+                }
+            }
+
+            // If call was made without any custom permissions, request all from plugin annotation
+            Set<String> aliasSet = new HashSet<>();
+            if (providedPermsList == null || providedPermsList.isEmpty()) {
+                for (Permission perm : annotation.permissions()) {
+                    // If a permission is defined with no permission strings, separate it for auto-granting.
+                    // Otherwise, the alias is added to the list to be requested.
+                    if (perm.strings().length == 0 || (perm.strings().length == 1 && perm.strings()[0].isEmpty())) {
+                        if (!perm.alias().isEmpty()) {
+                            autoGrantPerms.add(perm.alias());
+                        }
+                    } else {
+                        aliasSet.add(perm.alias());
+                    }
+                }
+
+                permAliases = aliasSet.toArray(new String[0]);
+            } else {
+                for (Permission perm : annotation.permissions()) {
+                    if (providedPermsList.contains(perm.alias())) {
+                        aliasSet.add(perm.alias());
+                    }
+                }
+
+                if (aliasSet.isEmpty()) {
+                    call.reject("No valid permission alias was requested of this plugin.");
+                } else {
+                    permAliases = aliasSet.toArray(new String[0]);
+                }
+            }
+
+            if (permAliases != null && permAliases.length > 0) {
+                // request permissions using provided aliases or all defined on the plugin
+                requestPermissionForAliases(permAliases, call, "checkPermissions");
+            } else if (!autoGrantPerms.isEmpty()) {
+                // if the plugin only has auto-grant permissions, return all as GRANTED
+                JSObject permissionsResults = new JSObject();
+
+                for (String perm : autoGrantPerms) {
+                    permissionsResults.put(perm, PermissionState.GRANTED.toString());
+                }
+
+                call.resolve(permissionsResults);
+            } else {
+                // no permissions are defined on the plugin, resolve undefined
+                call.resolve();
+            }
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    private void handleLegacyPermission(PluginCall call) {
+        // handle permission requests for plugins defined with @NativePlugin (prior to 3.0.0)
+        NativePlugin legacyAnnotation = this.handle.getLegacyPluginAnnotation();
+        String[] perms = legacyAnnotation.permissions();
+        if (perms.length > 0) {
+            saveCall(call);
+            pluginRequestPermissions(perms, legacyAnnotation.permissionRequestCode());
+        } else {
+            call.resolve();
+        }
+    }
+
+    /**
+     * Handle request permissions result. A plugin using the deprecated {@link NativePlugin}
+     * should override this to handle the result, or this method will handle the result
+     * for our convenient requestPermissions call.
+     * @deprecated in favor of using callbacks in conjunction with {@link CapacitorPlugin}
+     *
+     * @param requestCode
+     * @param permissions
+     * @param grantResults
+     */
+    @Deprecated
+    protected void handleRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+        if (!hasDefinedPermissions(permissions)) {
+            StringBuilder builder = new StringBuilder();
+            builder.append("Missing the following permissions in AndroidManifest.xml:\n");
+            String[] missing = PermissionHelper.getUndefinedPermissions(getContext(), permissions);
+            for (String perm : missing) {
+                builder.append(perm + "\n");
+            }
+            savedLastCall.reject(builder.toString());
+            savedLastCall = null;
+        }
+    }
+
+    /**
+     * Called before the app is destroyed to give a plugin the chance to
+     * save the last call options for a saved plugin. By default, this
+     * method saves the full JSON blob of the options call. Since Bundle sizes
+     * may be limited, plugins that expect to be called with large data
+     * objects (such as a file), should override this method and selectively
+     * store option values in a {@link Bundle} to avoid exceeding limits.
+     * @return a new {@link Bundle} with fields set from the options of the last saved {@link PluginCall}
+     */
+    protected Bundle saveInstanceState() {
+        PluginCall savedCall = bridge.getSavedCall(lastPluginCallId);
+
+        if (savedCall == null) {
+            return null;
+        }
+
+        Bundle ret = new Bundle();
+        JSObject callData = savedCall.getData();
+
+        if (callData != null) {
+            ret.putString(BUNDLE_PERSISTED_OPTIONS_JSON_KEY, callData.toString());
+        }
+
+        return ret;
+    }
+
+    /**
+     * Called when the app is opened with a previously un-handled
+     * activity response. If the plugin that started the activity
+     * stored data in {@link Plugin#saveInstanceState()} then this
+     * method will be called to allow the plugin to restore from that.
+     * @param state
+     */
+    protected void restoreState(Bundle state) {}
+
+    /**
+     * Handle activity result, should be overridden by each plugin
+     *
+     * @deprecated provide a callback method using the {@link ActivityCallback} annotation and use
+     * the {@link #startActivityForResult(PluginCall, Intent, String)} method
+     *
+     * @param requestCode
+     * @param resultCode
+     * @param data
+     */
+    @Deprecated
+    protected void handleOnActivityResult(int requestCode, int resultCode, Intent data) {}
+
+    /**
+     * Handle onNewIntent
+     * @param intent
+     */
+    protected void handleOnNewIntent(Intent intent) {}
+
+    /**
+     * Handle onConfigurationChanged
+     * @param newConfig
+     */
+    protected void handleOnConfigurationChanged(Configuration newConfig) {}
+
+    /**
+     * Handle onStart
+     */
+    protected void handleOnStart() {}
+
+    /**
+     * Handle onRestart
+     */
+    protected void handleOnRestart() {}
+
+    /**
+     * Handle onResume
+     */
+    protected void handleOnResume() {}
+
+    /**
+     * Handle onPause
+     */
+    protected void handleOnPause() {}
+
+    /**
+     * Handle onStop
+     */
+    protected void handleOnStop() {}
+
+    /**
+     * Handle onDestroy
+     */
+    protected void handleOnDestroy() {}
+
+    /**
+     * Give the plugins a chance to take control when a URL is about to be loaded in the WebView.
+     * Returning true causes the WebView to abort loading the URL.
+     * Returning false causes the WebView to continue loading the URL.
+     * Returning null will defer to the default Capacitor policy
+     */
+    @SuppressWarnings("unused")
+    public Boolean shouldOverrideLoad(Uri url) {
+        return null;
+    }
+
+    /**
+     * Start a new Activity.
+     *
+     * Note: This method must be used by all plugins instead of calling
+     * {@link Activity#startActivityForResult} as it associates the plugin with
+     * any resulting data from the new Activity even if this app
+     * is destroyed by the OS (to free up memory, for example).
+     * @param intent
+     * @param resultCode
+     */
+    @Deprecated
+    protected void startActivityForResult(PluginCall call, Intent intent, int resultCode) {
+        bridge.startActivityForPluginWithResult(call, intent, resultCode);
+    }
+
+    /**
+     * Execute the given runnable on the Bridge's task handler
+     * @param runnable
+     */
+    public void execute(Runnable runnable) {
+        bridge.execute(runnable);
+    }
+
+    /**
+     * Shortcut for getting the plugin log tag
+     * @param subTags
+     */
+    protected String getLogTag(String... subTags) {
+        return Logger.tags(subTags);
+    }
+
+    /**
+     * Gets a plugin log tag with the child's class name as subTag.
+     */
+    protected String getLogTag() {
+        return Logger.tags(this.getClass().getSimpleName());
+    }
+}

+ 440 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/PluginCall.java

@@ -0,0 +1,440 @@
+package com.getcapacitor;
+
+import androidx.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.List;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Wraps a call from the web layer to native
+ */
+public class PluginCall {
+
+    /**
+     * A special callback id that indicates there is no matching callback
+     * on the client to associate any PluginCall results back to. This is used
+     * in the case of an app resuming with saved instance data, for example.
+     */
+    public static final String CALLBACK_ID_DANGLING = "-1";
+
+    private final MessageHandler msgHandler;
+    private final String pluginId;
+    private final String callbackId;
+    private final String methodName;
+    private final JSObject data;
+
+    private boolean keepAlive = false;
+
+    /**
+     * Indicates that this PluginCall was released, and should no longer be used
+     */
+    @Deprecated
+    private boolean isReleased = false;
+
+    public PluginCall(MessageHandler msgHandler, String pluginId, String callbackId, String methodName, JSObject data) {
+        this.msgHandler = msgHandler;
+        this.pluginId = pluginId;
+        this.callbackId = callbackId;
+        this.methodName = methodName;
+        this.data = data;
+    }
+
+    public void successCallback(PluginResult successResult) {
+        if (CALLBACK_ID_DANGLING.equals(this.callbackId)) {
+            // don't send back response if the callbackId was "-1"
+            return;
+        }
+
+        this.msgHandler.sendResponseMessage(this, successResult, null);
+    }
+
+    /**
+     * @deprecated
+     * Use {@link #resolve(JSObject data)}
+     */
+    @Deprecated
+    public void success(JSObject data) {
+        PluginResult result = new PluginResult(data);
+        this.msgHandler.sendResponseMessage(this, result, null);
+    }
+
+    /**
+     * @deprecated
+     * Use {@link #resolve()}
+     */
+    @Deprecated
+    public void success() {
+        this.resolve(new JSObject());
+    }
+
+    public void resolve(JSObject data) {
+        PluginResult result = new PluginResult(data);
+        this.msgHandler.sendResponseMessage(this, result, null);
+    }
+
+    public void resolve() {
+        this.msgHandler.sendResponseMessage(this, null, null);
+    }
+
+    public void errorCallback(String msg) {
+        PluginResult errorResult = new PluginResult();
+
+        try {
+            errorResult.put("message", msg);
+        } catch (Exception jsonEx) {
+            Logger.error(Logger.tags("Plugin"), jsonEx.toString(), null);
+        }
+
+        this.msgHandler.sendResponseMessage(this, null, errorResult);
+    }
+
+    /**
+     * @deprecated
+     * Use {@link #reject(String msg, Exception ex)}
+     */
+    @Deprecated
+    public void error(String msg, Exception ex) {
+        reject(msg, ex);
+    }
+
+    /**
+     * @deprecated
+     * Use {@link #reject(String msg, String code, Exception ex)}
+     */
+    @Deprecated
+    public void error(String msg, String code, Exception ex) {
+        reject(msg, code, ex);
+    }
+
+    /**
+     * @deprecated
+     * Use {@link #reject(String msg)}
+     */
+    @Deprecated
+    public void error(String msg) {
+        reject(msg);
+    }
+
+    public void reject(String msg, String code, Exception ex, JSObject data) {
+        PluginResult errorResult = new PluginResult();
+
+        if (ex != null) {
+            Logger.error(Logger.tags("Plugin"), msg, ex);
+        }
+
+        try {
+            errorResult.put("message", msg);
+            errorResult.put("code", code);
+            if (null != data) {
+                errorResult.put("data", data);
+            }
+        } catch (Exception jsonEx) {
+            Logger.error(Logger.tags("Plugin"), jsonEx.getMessage(), jsonEx);
+        }
+
+        this.msgHandler.sendResponseMessage(this, null, errorResult);
+    }
+
+    public void reject(String msg, Exception ex, JSObject data) {
+        reject(msg, null, ex, data);
+    }
+
+    public void reject(String msg, String code, JSObject data) {
+        reject(msg, code, null, data);
+    }
+
+    public void reject(String msg, String code, Exception ex) {
+        reject(msg, code, ex, null);
+    }
+
+    public void reject(String msg, JSObject data) {
+        reject(msg, null, null, data);
+    }
+
+    public void reject(String msg, Exception ex) {
+        reject(msg, null, ex, null);
+    }
+
+    public void reject(String msg, String code) {
+        reject(msg, code, null, null);
+    }
+
+    public void reject(String msg) {
+        reject(msg, null, null, null);
+    }
+
+    public void unimplemented() {
+        unimplemented("not implemented");
+    }
+
+    public void unimplemented(String msg) {
+        reject(msg, "UNIMPLEMENTED", null, null);
+    }
+
+    public void unavailable() {
+        unavailable("not available");
+    }
+
+    public void unavailable(String msg) {
+        reject(msg, "UNAVAILABLE", null, null);
+    }
+
+    public String getPluginId() {
+        return this.pluginId;
+    }
+
+    public String getCallbackId() {
+        return this.callbackId;
+    }
+
+    public String getMethodName() {
+        return this.methodName;
+    }
+
+    public JSObject getData() {
+        return this.data;
+    }
+
+    @Nullable
+    public String getString(String name) {
+        return this.getString(name, null);
+    }
+
+    @Nullable
+    public String getString(String name, @Nullable String defaultValue) {
+        Object value = this.data.opt(name);
+        if (value == null) {
+            return defaultValue;
+        }
+
+        if (value instanceof String) {
+            return (String) value;
+        }
+        return defaultValue;
+    }
+
+    @Nullable
+    public Integer getInt(String name) {
+        return this.getInt(name, null);
+    }
+
+    @Nullable
+    public Integer getInt(String name, @Nullable Integer defaultValue) {
+        Object value = this.data.opt(name);
+        if (value == null) {
+            return defaultValue;
+        }
+
+        if (value instanceof Integer) {
+            return (Integer) value;
+        }
+        return defaultValue;
+    }
+
+    @Nullable
+    public Long getLong(String name) {
+        return this.getLong(name, null);
+    }
+
+    @Nullable
+    public Long getLong(String name, @Nullable Long defaultValue) {
+        Object value = this.data.opt(name);
+        if (value == null) {
+            return defaultValue;
+        }
+
+        if (value instanceof Long) {
+            return (Long) value;
+        }
+        return defaultValue;
+    }
+
+    @Nullable
+    public Float getFloat(String name) {
+        return this.getFloat(name, null);
+    }
+
+    @Nullable
+    public Float getFloat(String name, @Nullable Float defaultValue) {
+        Object value = this.data.opt(name);
+        if (value == null) {
+            return defaultValue;
+        }
+
+        if (value instanceof Float) {
+            return (Float) value;
+        }
+        if (value instanceof Double) {
+            return ((Double) value).floatValue();
+        }
+        if (value instanceof Integer) {
+            return ((Integer) value).floatValue();
+        }
+        return defaultValue;
+    }
+
+    @Nullable
+    public Double getDouble(String name) {
+        return this.getDouble(name, null);
+    }
+
+    @Nullable
+    public Double getDouble(String name, @Nullable Double defaultValue) {
+        Object value = this.data.opt(name);
+        if (value == null) {
+            return defaultValue;
+        }
+
+        if (value instanceof Double) {
+            return (Double) value;
+        }
+        if (value instanceof Float) {
+            return ((Float) value).doubleValue();
+        }
+        if (value instanceof Integer) {
+            return ((Integer) value).doubleValue();
+        }
+        return defaultValue;
+    }
+
+    @Nullable
+    public Boolean getBoolean(String name) {
+        return this.getBoolean(name, null);
+    }
+
+    @Nullable
+    public Boolean getBoolean(String name, @Nullable Boolean defaultValue) {
+        Object value = this.data.opt(name);
+        if (value == null) {
+            return defaultValue;
+        }
+
+        if (value instanceof Boolean) {
+            return (Boolean) value;
+        }
+        return defaultValue;
+    }
+
+    public JSObject getObject(String name) {
+        return this.getObject(name, null);
+    }
+
+    @Nullable
+    public JSObject getObject(String name, JSObject defaultValue) {
+        Object value = this.data.opt(name);
+        if (value == null) {
+            return defaultValue;
+        }
+
+        if (value instanceof JSONObject) {
+            try {
+                return JSObject.fromJSONObject((JSONObject) value);
+            } catch (JSONException ex) {
+                return defaultValue;
+            }
+        }
+        return defaultValue;
+    }
+
+    public JSArray getArray(String name) {
+        return this.getArray(name, null);
+    }
+
+    /**
+     * Get a JSONArray and turn it into a JSArray
+     * @param name
+     * @param defaultValue
+     * @return
+     */
+    @Nullable
+    public JSArray getArray(String name, JSArray defaultValue) {
+        Object value = this.data.opt(name);
+        if (value == null) {
+            return defaultValue;
+        }
+
+        if (value instanceof JSONArray) {
+            try {
+                JSONArray valueArray = (JSONArray) value;
+                List<Object> items = new ArrayList<>();
+                for (int i = 0; i < valueArray.length(); i++) {
+                    items.add(valueArray.get(i));
+                }
+                return new JSArray(items.toArray());
+            } catch (JSONException ex) {
+                return defaultValue;
+            }
+        }
+        return defaultValue;
+    }
+
+    /**
+     * @param name of the option to check
+     * @return boolean indicating if the plugin call has an option for the provided name.
+     * @deprecated Presence of a key should not be considered significant.
+     * Use typed accessors to check the value instead.
+     */
+    @Deprecated
+    public boolean hasOption(String name) {
+        return this.data.has(name);
+    }
+
+    /**
+     * Indicate that the Bridge should cache this call in order to call
+     * it again later. For example, the addListener system uses this to
+     * continuously call the call's callback (😆).
+     * @deprecated use {@link #setKeepAlive(Boolean)} instead
+     */
+    @Deprecated
+    public void save() {
+        setKeepAlive(true);
+    }
+
+    /**
+     * Indicate that the Bridge should cache this call in order to call
+     * it again later. For example, the addListener system uses this to
+     * continuously call the call's callback.
+     *
+     * @param keepAlive whether to keep the callback saved
+     */
+    public void setKeepAlive(Boolean keepAlive) {
+        this.keepAlive = keepAlive;
+    }
+
+    public void release(Bridge bridge) {
+        this.keepAlive = false;
+        bridge.releaseCall(this);
+        this.isReleased = true;
+    }
+
+    /**
+     * @deprecated use {@link #isKeptAlive()}
+     * @return true if the plugin call is kept alive
+     */
+    @Deprecated
+    public boolean isSaved() {
+        return isKeptAlive();
+    }
+
+    /**
+     * Gets the keepAlive value of the plugin call
+     * @return true if the plugin call is kept alive
+     */
+    public boolean isKeptAlive() {
+        return keepAlive;
+    }
+
+    @Deprecated
+    public boolean isReleased() {
+        return isReleased;
+    }
+
+    class PluginCallDataTypeException extends Exception {
+
+        PluginCallDataTypeException(String m) {
+            super(m);
+        }
+    }
+}

+ 116 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/PluginConfig.java

@@ -0,0 +1,116 @@
+package com.getcapacitor;
+
+import com.getcapacitor.util.JSONUtils;
+import org.json.JSONObject;
+
+/**
+ * Represents the configuration options for plugins used by Capacitor
+ */
+public class PluginConfig {
+
+    /**
+     * The object containing plugin config values.
+     */
+    private final JSONObject config;
+
+    /**
+     * Constructs a PluginsConfig with the provided JSONObject value.
+     *
+     * @param config A plugin configuration expressed as a JSON Object
+     */
+    PluginConfig(JSONObject config) {
+        this.config = config;
+    }
+
+    /**
+     * Get a string value for a plugin in the Capacitor config.
+     *
+     * @param configKey The key of the value to retrieve
+     * @return The value from the config, if exists. Null if not
+     */
+    public String getString(String configKey) {
+        return getString(configKey, null);
+    }
+
+    /**
+     * Get a string value for a plugin in the Capacitor config.
+     *
+     * @param configKey The key of the value to retrieve
+     * @param defaultValue A default value to return if the key does not exist in the config
+     * @return The value from the config, if key exists. Default value returned if not
+     */
+    public String getString(String configKey, String defaultValue) {
+        return JSONUtils.getString(config, configKey, defaultValue);
+    }
+
+    /**
+     * Get a boolean value for a plugin in the Capacitor config.
+     *
+     * @param configKey The key of the value to retrieve
+     * @param defaultValue A default value to return if the key does not exist in the config
+     * @return The value from the config, if key exists. Default value returned if not
+     */
+    public boolean getBoolean(String configKey, boolean defaultValue) {
+        return JSONUtils.getBoolean(config, configKey, defaultValue);
+    }
+
+    /**
+     * Get an integer value for a plugin in the Capacitor config.
+     *
+     * @param configKey The key of the value to retrieve
+     * @param defaultValue A default value to return if the key does not exist in the config
+     * @return The value from the config, if key exists. Default value returned if not
+     */
+    public int getInt(String configKey, int defaultValue) {
+        return JSONUtils.getInt(config, configKey, defaultValue);
+    }
+
+    /**
+     * Get a string array value for a plugin in the Capacitor config.
+     *
+     * @param configKey The key of the value to retrieve
+     * @return The value from the config, if exists. Null if not
+     */
+    public String[] getArray(String configKey) {
+        return getArray(configKey, null);
+    }
+
+    /**
+     * Get a string array value for a plugin in the Capacitor config.
+     *
+     * @param configKey The key of the value to retrieve
+     * @param defaultValue A default value to return if the key does not exist in the config
+     * @return The value from the config, if key exists. Default value returned if not
+     */
+    public String[] getArray(String configKey, String[] defaultValue) {
+        return JSONUtils.getArray(config, configKey, defaultValue);
+    }
+
+    /**
+     * Get a JSON object value for a plugin in the Capacitor config.
+     *
+     * @param configKey The key of the value to retrieve
+     * @return The value from the config, if exists. Null if not
+     */
+    public JSONObject getObject(String configKey) {
+        return JSONUtils.getObject(config, configKey);
+    }
+
+    /**
+     * Check if the PluginConfig is empty.
+     *
+     * @return true if the plugin config has no entries
+     */
+    public boolean isEmpty() {
+        return config.length() == 0;
+    }
+
+    /**
+     * Gets the JSON Object containing the config of the the provided plugin ID.
+     *
+     * @return The config for that plugin
+     */
+    public JSONObject getConfigJSON() {
+        return config;
+    }
+}

+ 160 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/PluginHandle.java

@@ -0,0 +1,160 @@
+package com.getcapacitor;
+
+import com.getcapacitor.annotation.CapacitorPlugin;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * PluginHandle is an instance of a plugin that has been registered
+ * and indexed. Think of it as a Plugin instance with extra metadata goodies
+ */
+public class PluginHandle {
+
+    private final Bridge bridge;
+    private final Class<? extends Plugin> pluginClass;
+
+    private final Map<String, PluginMethodHandle> pluginMethods = new HashMap<>();
+
+    private final String pluginId;
+
+    @SuppressWarnings("deprecation")
+    private NativePlugin legacyPluginAnnotation;
+
+    private CapacitorPlugin pluginAnnotation;
+
+    private Plugin instance;
+
+    @SuppressWarnings("deprecation")
+    private PluginHandle(Class<? extends Plugin> clazz, Bridge bridge) throws InvalidPluginException {
+        this.bridge = bridge;
+        this.pluginClass = clazz;
+
+        CapacitorPlugin pluginAnnotation = pluginClass.getAnnotation(CapacitorPlugin.class);
+        if (pluginAnnotation == null) {
+            // Check for legacy plugin annotation, @NativePlugin
+            NativePlugin legacyPluginAnnotation = pluginClass.getAnnotation(NativePlugin.class);
+            if (legacyPluginAnnotation == null) {
+                throw new InvalidPluginException("No @CapacitorPlugin annotation found for plugin " + pluginClass.getName());
+            }
+
+            if (!legacyPluginAnnotation.name().equals("")) {
+                this.pluginId = legacyPluginAnnotation.name();
+            } else {
+                this.pluginId = pluginClass.getSimpleName();
+            }
+
+            this.legacyPluginAnnotation = legacyPluginAnnotation;
+        } else {
+            if (!pluginAnnotation.name().equals("")) {
+                this.pluginId = pluginAnnotation.name();
+            } else {
+                this.pluginId = pluginClass.getSimpleName();
+            }
+
+            this.pluginAnnotation = pluginAnnotation;
+        }
+
+        this.indexMethods(clazz);
+    }
+
+    public PluginHandle(Bridge bridge, Class<? extends Plugin> pluginClass) throws InvalidPluginException, PluginLoadException {
+        this(pluginClass, bridge);
+        this.load();
+    }
+
+    public PluginHandle(Bridge bridge, Plugin plugin) throws InvalidPluginException {
+        this(plugin.getClass(), bridge);
+        this.loadInstance(plugin);
+    }
+
+    public Class<? extends Plugin> getPluginClass() {
+        return pluginClass;
+    }
+
+    public String getId() {
+        return this.pluginId;
+    }
+
+    @SuppressWarnings("deprecation")
+    public NativePlugin getLegacyPluginAnnotation() {
+        return this.legacyPluginAnnotation;
+    }
+
+    public CapacitorPlugin getPluginAnnotation() {
+        return this.pluginAnnotation;
+    }
+
+    public Plugin getInstance() {
+        return this.instance;
+    }
+
+    public Collection<PluginMethodHandle> getMethods() {
+        return this.pluginMethods.values();
+    }
+
+    public Plugin load() throws PluginLoadException {
+        if (this.instance != null) {
+            return this.instance;
+        }
+
+        try {
+            this.instance = this.pluginClass.newInstance();
+            return this.loadInstance(instance);
+        } catch (InstantiationException | IllegalAccessException ex) {
+            throw new PluginLoadException("Unable to load plugin instance. Ensure plugin is publicly accessible");
+        }
+    }
+
+    public Plugin loadInstance(Plugin plugin) {
+        this.instance = plugin;
+        this.instance.setPluginHandle(this);
+        this.instance.setBridge(this.bridge);
+        this.instance.load();
+        this.instance.initializeActivityLaunchers();
+        return this.instance;
+    }
+
+    /**
+     * Call a method on a plugin.
+     * @param methodName the name of the method to call
+     * @param call the constructed PluginCall with parameters from the caller
+     * @throws InvalidPluginMethodException if no method was found on that plugin
+     */
+    public void invoke(String methodName, PluginCall call)
+        throws PluginLoadException, InvalidPluginMethodException, InvocationTargetException, IllegalAccessException {
+        if (this.instance == null) {
+            // Can throw PluginLoadException
+            this.load();
+        }
+
+        PluginMethodHandle methodMeta = pluginMethods.get(methodName);
+        if (methodMeta == null) {
+            throw new InvalidPluginMethodException("No method " + methodName + " found for plugin " + pluginClass.getName());
+        }
+
+        methodMeta.getMethod().invoke(this.instance, call);
+    }
+
+    /**
+     * Index all the known callable methods for a plugin for faster
+     * invocation later
+     */
+    private void indexMethods(Class<? extends Plugin> plugin) {
+        //Method[] methods = pluginClass.getDeclaredMethods();
+        Method[] methods = pluginClass.getMethods();
+
+        for (Method methodReflect : methods) {
+            PluginMethod method = methodReflect.getAnnotation(PluginMethod.class);
+
+            if (method == null) {
+                continue;
+            }
+
+            PluginMethodHandle methodMeta = new PluginMethodHandle(methodReflect, method);
+            pluginMethods.put(methodReflect.getName(), methodMeta);
+        }
+    }
+}

+ 16 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/PluginInvocationException.java

@@ -0,0 +1,16 @@
+package com.getcapacitor;
+
+class PluginInvocationException extends Exception {
+
+    public PluginInvocationException(String s) {
+        super(s);
+    }
+
+    public PluginInvocationException(Throwable t) {
+        super(t);
+    }
+
+    public PluginInvocationException(String s, Throwable t) {
+        super(s, t);
+    }
+}

+ 19 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/PluginLoadException.java

@@ -0,0 +1,19 @@
+package com.getcapacitor;
+
+/**
+ * Thrown when a plugin fails to instantiate
+ */
+public class PluginLoadException extends Exception {
+
+    public PluginLoadException(String s) {
+        super(s);
+    }
+
+    public PluginLoadException(Throwable t) {
+        super(t);
+    }
+
+    public PluginLoadException(String s, Throwable t) {
+        super(s, t);
+    }
+}

+ 56 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/PluginManager.java

@@ -0,0 +1,56 @@
+package com.getcapacitor;
+
+import android.content.res.AssetManager;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class PluginManager {
+
+    private final AssetManager assetManager;
+
+    public PluginManager(AssetManager assetManager) {
+        this.assetManager = assetManager;
+    }
+
+    public List<Class<? extends Plugin>> loadPluginClasses() throws PluginLoadException {
+        JSONArray pluginsJSON = parsePluginsJSON();
+        ArrayList<Class<? extends Plugin>> pluginList = new ArrayList<>();
+
+        try {
+            for (int i = 0, size = pluginsJSON.length(); i < size; i++) {
+                JSONObject pluginJSON = pluginsJSON.getJSONObject(i);
+                String classPath = pluginJSON.getString("classpath");
+                Class<?> c = Class.forName(classPath);
+                pluginList.add(c.asSubclass(Plugin.class));
+            }
+        } catch (JSONException e) {
+            throw new PluginLoadException("Could not parse capacitor.plugins.json as JSON");
+        } catch (ClassNotFoundException e) {
+            throw new PluginLoadException("Could not find class by class path: " + e.getMessage());
+        }
+
+        return pluginList;
+    }
+
+    private JSONArray parsePluginsJSON() throws PluginLoadException {
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(assetManager.open("capacitor.plugins.json")))) {
+            StringBuilder builder = new StringBuilder();
+            String line;
+            while ((line = reader.readLine()) != null) {
+                builder.append(line);
+            }
+            String jsonString = builder.toString();
+            return new JSONArray(jsonString);
+        } catch (IOException e) {
+            throw new PluginLoadException("Could not load capacitor.plugins.json");
+        } catch (JSONException e) {
+            throw new PluginLoadException("Could not parse capacitor.plugins.json as JSON");
+        }
+    }
+}

+ 15 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/PluginMethod.java

@@ -0,0 +1,15 @@
+package com.getcapacitor;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.RUNTIME)
+public @interface PluginMethod {
+    String RETURN_PROMISE = "promise";
+
+    String RETURN_CALLBACK = "callback";
+
+    String RETURN_NONE = "none";
+
+    String returnType() default RETURN_PROMISE;
+}

+ 33 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/PluginMethodHandle.java

@@ -0,0 +1,33 @@
+package com.getcapacitor;
+
+import java.lang.reflect.Method;
+
+public class PluginMethodHandle {
+
+    // The reflect method reference
+    private final Method method;
+    // The name of the method
+    private final String name;
+    // The return type of the method (see PluginMethod for constants)
+    private final String returnType;
+
+    public PluginMethodHandle(Method method, PluginMethod methodDecorator) {
+        this.method = method;
+
+        this.name = method.getName();
+
+        this.returnType = methodDecorator.returnType();
+    }
+
+    public String getReturnType() {
+        return returnType;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public Method getMethod() {
+        return method;
+    }
+}

+ 84 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/PluginResult.java

@@ -0,0 +1,84 @@
+package com.getcapacitor;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+
+/**
+ * Wraps a result for web from calling a native plugin.
+ */
+public class PluginResult {
+
+    private final JSObject json;
+
+    public PluginResult() {
+        this(new JSObject());
+    }
+
+    public PluginResult(JSObject json) {
+        this.json = json;
+    }
+
+    public PluginResult put(String name, boolean value) {
+        return this.jsonPut(name, value);
+    }
+
+    public PluginResult put(String name, double value) {
+        return this.jsonPut(name, value);
+    }
+
+    public PluginResult put(String name, int value) {
+        return this.jsonPut(name, value);
+    }
+
+    public PluginResult put(String name, long value) {
+        return this.jsonPut(name, value);
+    }
+
+    /**
+     * Format a date as an ISO string
+     */
+    public PluginResult put(String name, Date value) {
+        TimeZone tz = TimeZone.getTimeZone("UTC");
+        DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'");
+        df.setTimeZone(tz);
+        return this.jsonPut(name, df.format(value));
+    }
+
+    public PluginResult put(String name, Object value) {
+        return this.jsonPut(name, value);
+    }
+
+    public PluginResult put(String name, PluginResult value) {
+        return this.jsonPut(name, value.json);
+    }
+
+    PluginResult jsonPut(String name, Object value) {
+        try {
+            this.json.put(name, value);
+        } catch (Exception ex) {
+            Logger.error(Logger.tags("Plugin"), "", ex);
+        }
+        return this;
+    }
+
+    public String toString() {
+        return this.json.toString();
+    }
+
+    /**
+     * Return plugin metadata and information about the result, if it succeeded the data, or error information if it didn't.
+     * This is used for appRestoredResult, as it's technically a raw data response from a plugin.
+     * @return the raw data response from the plugin.
+     */
+    public JSObject getWrappedResult() {
+        JSObject ret = new JSObject();
+        ret.put("pluginId", this.json.getString("pluginId"));
+        ret.put("methodName", this.json.getString("methodName"));
+        ret.put("success", this.json.getBoolean("success", false));
+        ret.put("data", this.json.getJSObject("data"));
+        ret.put("error", this.json.getJSObject("error"));
+        return ret;
+    }
+}

+ 37 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/ProcessedRoute.java

@@ -0,0 +1,37 @@
+package com.getcapacitor;
+
+/**
+ * An data class used in conjunction with RouteProcessor.
+ *
+ * @see com.getcapacitor.RouteProcessor
+ */
+public class ProcessedRoute {
+
+    private String path;
+    private boolean isAsset;
+    private boolean ignoreAssetPath;
+
+    public String getPath() {
+        return path;
+    }
+
+    public void setPath(String path) {
+        this.path = path;
+    }
+
+    public boolean isAsset() {
+        return isAsset;
+    }
+
+    public void setAsset(boolean asset) {
+        isAsset = asset;
+    }
+
+    public boolean isIgnoreAssetPath() {
+        return ignoreAssetPath;
+    }
+
+    public void setIgnoreAssetPath(boolean ignoreAssetPath) {
+        this.ignoreAssetPath = ignoreAssetPath;
+    }
+}

+ 8 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/RouteProcessor.java

@@ -0,0 +1,8 @@
+package com.getcapacitor;
+
+/**
+ * An interface used in the processing of routes
+ */
+public interface RouteProcessor {
+    ProcessedRoute process(String basePath, String path);
+}

+ 25 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/ServerPath.java

@@ -0,0 +1,25 @@
+package com.getcapacitor;
+
+public class ServerPath {
+
+    public enum PathType {
+        BASE_PATH,
+        ASSET_PATH
+    }
+
+    private final PathType type;
+    private final String path;
+
+    public ServerPath(PathType type, String path) {
+        this.type = type;
+        this.path = path;
+    }
+
+    public PathType getType() {
+        return type;
+    }
+
+    public String getPath() {
+        return path;
+    }
+}

+ 180 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/UriMatcher.java

@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+//package com.google.webviewlocalserver.third_party.android;
+package com.getcapacitor;
+
+import android.net.Uri;
+import com.getcapacitor.util.HostMask;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+public class UriMatcher {
+
+    /**
+     * Creates the root node of the URI tree.
+     *
+     * @param code the code to match for the root URI
+     */
+    public UriMatcher(Object code) {
+        mCode = code;
+        mWhich = -1;
+        mChildren = new ArrayList<>();
+        mText = null;
+    }
+
+    private UriMatcher() {
+        mCode = null;
+        mWhich = -1;
+        mChildren = new ArrayList<>();
+        mText = null;
+    }
+
+    /**
+     * Add a URI to match, and the code to return when this URI is
+     * matched. URI nodes may be exact match string, the token "*"
+     * that matches any text, or the token "#" that matches only
+     * numbers.
+     * <p>
+     * Starting from API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
+     * this method will accept a leading slash in the path.
+     *
+     * @param authority the authority to match
+     * @param path      the path to match. * may be used as a wild card for
+     *                  any text, and # may be used as a wild card for numbers.
+     * @param code      the code that is returned when a URI is matched
+     *                  against the given components. Must be positive.
+     */
+    public void addURI(String scheme, String authority, String path, Object code) {
+        if (code == null) {
+            throw new IllegalArgumentException("Code can't be null");
+        }
+
+        String[] tokens = null;
+        if (path != null) {
+            String newPath = path;
+            // Strip leading slash if present.
+            if (!path.isEmpty() && path.charAt(0) == '/') {
+                newPath = path.substring(1);
+            }
+            tokens = PATH_SPLIT_PATTERN.split(newPath);
+        }
+
+        int numTokens = tokens != null ? tokens.length : 0;
+        UriMatcher node = this;
+        for (int i = -2; i < numTokens; i++) {
+            String token;
+            if (i == -2) token = scheme; else if (i == -1) token = authority; else token = tokens[i];
+            ArrayList<UriMatcher> children = node.mChildren;
+            int numChildren = children.size();
+            UriMatcher child;
+            int j;
+            for (j = 0; j < numChildren; j++) {
+                child = children.get(j);
+                if (token.equals(child.mText)) {
+                    node = child;
+                    break;
+                }
+            }
+            if (j == numChildren) {
+                // Child not found, create it
+                child = new UriMatcher();
+                if (i == -1 && token.contains("*")) {
+                    child.mWhich = MASK;
+                } else if (token.equals("**")) {
+                    child.mWhich = REST;
+                } else if (token.equals("*")) {
+                    child.mWhich = TEXT;
+                } else {
+                    child.mWhich = EXACT;
+                }
+                child.mText = token;
+                node.mChildren.add(child);
+                node = child;
+            }
+        }
+        node.mCode = code;
+    }
+
+    static final Pattern PATH_SPLIT_PATTERN = Pattern.compile("/");
+
+    /**
+     * Try to match against the path in a url.
+     *
+     * @param uri The url whose path we will match against.
+     * @return The code for the matched node (added using addURI),
+     * or null if there is no matched node.
+     */
+    public Object match(Uri uri) {
+        final List<String> pathSegments = uri.getPathSegments();
+        final int li = pathSegments.size();
+
+        UriMatcher node = this;
+
+        if (li == 0 && uri.getAuthority() == null) {
+            return this.mCode;
+        }
+
+        for (int i = -2; i < li; i++) {
+            String u;
+            if (i == -2) u = uri.getScheme(); else if (i == -1) u = uri.getAuthority(); else u = pathSegments.get(i);
+            ArrayList<UriMatcher> list = node.mChildren;
+            if (list == null) {
+                break;
+            }
+            node = null;
+            int lj = list.size();
+            for (int j = 0; j < lj; j++) {
+                UriMatcher n = list.get(j);
+                which_switch:switch (n.mWhich) {
+                    case MASK:
+                        if (HostMask.Parser.parse(n.mText).matches(u)) {
+                            node = n;
+                        }
+                        break;
+                    case EXACT:
+                        if (n.mText.equals(u)) {
+                            node = n;
+                        }
+                        break;
+                    case TEXT:
+                        node = n;
+                        break;
+                    case REST:
+                        return n.mCode;
+                }
+                if (node != null) {
+                    break;
+                }
+            }
+            if (node == null) {
+                return null;
+            }
+        }
+
+        return node.mCode;
+    }
+
+    private static final int EXACT = 0;
+    private static final int TEXT = 1;
+    private static final int REST = 2;
+    private static final int MASK = 3;
+
+    private Object mCode;
+    private int mWhich;
+    private String mText;
+    private ArrayList<UriMatcher> mChildren;
+}

+ 57 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/WebViewListener.java

@@ -0,0 +1,57 @@
+package com.getcapacitor;
+
+import android.webkit.RenderProcessGoneDetail;
+import android.webkit.WebView;
+
+/**
+ * Provides callbacks associated with the {@link BridgeWebViewClient}
+ */
+public abstract class WebViewListener {
+
+    /**
+     * Callback for page load event.
+     *
+     * @param webView The WebView that loaded
+     */
+    public void onPageLoaded(WebView webView) {
+        // Override me to add behavior to the page loaded event
+    }
+
+    /**
+     * Callback for onReceivedError event.
+     *
+     * @param webView The WebView that loaded
+     */
+    public void onReceivedError(WebView webView) {
+        // Override me to add behavior to handle the onReceivedError event
+    }
+
+    /**
+     * Callback for onReceivedHttpError event.
+     *
+     * @param webView The WebView that loaded
+     */
+    public void onReceivedHttpError(WebView webView) {
+        // Override me to add behavior to handle the onReceivedHttpError event
+    }
+
+    /**
+     * Callback for page start event.
+     *
+     * @param webView The WebView that loaded
+     */
+    public void onPageStarted(WebView webView) {
+        // Override me to add behavior to the page started event
+    }
+
+    /**
+     * Callback for render process gone event. Return true if the state is handled.
+     *
+     * @param webView The WebView that loaded
+     * @return returns false by default if the listener is not overridden and used
+     */
+    public boolean onRenderProcessGone(WebView webView, RenderProcessGoneDetail detail) {
+        // Override me to add behavior to the web view render process gone event
+        return false;
+    }
+}

+ 615 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java

@@ -0,0 +1,615 @@
+/*
+Copyright 2015 Google Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ */
+package com.getcapacitor;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.Base64;
+import android.webkit.CookieManager;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Helper class meant to be used with the android.webkit.WebView class to enable hosting assets,
+ * resources and other data on 'virtual' https:// URL.
+ * Hosting assets and resources on https:// URLs is desirable as it is compatible with the
+ * Same-Origin policy.
+ * <p>
+ * This class is intended to be used from within the
+ * {@link android.webkit.WebViewClient#shouldInterceptRequest(android.webkit.WebView, String)} and
+ * {@link android.webkit.WebViewClient#shouldInterceptRequest(android.webkit.WebView,
+ * android.webkit.WebResourceRequest)}
+ * methods.
+ */
+public class WebViewLocalServer {
+
+    private static final String capacitorFileStart = Bridge.CAPACITOR_FILE_START;
+    private static final String capacitorContentStart = Bridge.CAPACITOR_CONTENT_START;
+    private String basePath;
+
+    private final UriMatcher uriMatcher;
+    private final AndroidProtocolHandler protocolHandler;
+    private final ArrayList<String> authorities;
+    private boolean isAsset;
+    // Whether to route all requests to paths without extensions back to `index.html`
+    private final boolean html5mode;
+    private final JSInjector jsInjector;
+    private final Bridge bridge;
+
+    /**
+     * A handler that produces responses for paths on the virtual asset server.
+     * <p>
+     * Methods of this handler will be invoked on a background thread and care must be taken to
+     * correctly synchronize access to any shared state.
+     * <p>
+     * On Android KitKat and above these methods may be called on more than one thread. This thread
+     * may be different than the thread on which the shouldInterceptRequest method was invoke.
+     * This means that on Android KitKat and above it is possible to block in this method without
+     * blocking other resources from loading. The number of threads used to parallelize loading
+     * is an internal implementation detail of the WebView and may change between updates which
+     * means that the amount of time spend blocking in this method should be kept to an absolute
+     * minimum.
+     */
+    public abstract static class PathHandler {
+
+        protected String mimeType;
+        private String encoding;
+        private String charset;
+        private int statusCode;
+        private String reasonPhrase;
+        private Map<String, String> responseHeaders;
+
+        public PathHandler() {
+            this(null, null, 200, "OK", null);
+        }
+
+        public PathHandler(String encoding, String charset, int statusCode, String reasonPhrase, Map<String, String> responseHeaders) {
+            this.encoding = encoding;
+            this.charset = charset;
+            this.statusCode = statusCode;
+            this.reasonPhrase = reasonPhrase;
+            Map<String, String> tempResponseHeaders;
+            if (responseHeaders == null) {
+                tempResponseHeaders = new HashMap<>();
+            } else {
+                tempResponseHeaders = responseHeaders;
+            }
+            tempResponseHeaders.put("Cache-Control", "no-cache");
+            this.responseHeaders = tempResponseHeaders;
+        }
+
+        public InputStream handle(WebResourceRequest request) {
+            return handle(request.getUrl());
+        }
+
+        public abstract InputStream handle(Uri url);
+
+        public String getEncoding() {
+            return encoding;
+        }
+
+        public String getCharset() {
+            return charset;
+        }
+
+        public int getStatusCode() {
+            return statusCode;
+        }
+
+        public String getReasonPhrase() {
+            return reasonPhrase;
+        }
+
+        public Map<String, String> getResponseHeaders() {
+            return responseHeaders;
+        }
+    }
+
+    WebViewLocalServer(Context context, Bridge bridge, JSInjector jsInjector, ArrayList<String> authorities, boolean html5mode) {
+        uriMatcher = new UriMatcher(null);
+        this.html5mode = html5mode;
+        this.protocolHandler = new AndroidProtocolHandler(context.getApplicationContext());
+        this.authorities = authorities;
+        this.bridge = bridge;
+        this.jsInjector = jsInjector;
+    }
+
+    private static Uri parseAndVerifyUrl(String url) {
+        if (url == null) {
+            return null;
+        }
+        Uri uri = Uri.parse(url);
+        if (uri == null) {
+            Logger.error("Malformed URL: " + url);
+            return null;
+        }
+        String path = uri.getPath();
+        if (path == null || path.isEmpty()) {
+            Logger.error("URL does not have a path: " + url);
+            return null;
+        }
+        return uri;
+    }
+
+    /**
+     * Attempt to retrieve the WebResourceResponse associated with the given <code>request</code>.
+     * This method should be invoked from within
+     * {@link android.webkit.WebViewClient#shouldInterceptRequest(android.webkit.WebView,
+     * android.webkit.WebResourceRequest)}.
+     *
+     * @param request the request to process.
+     * @return a response if the request URL had a matching handler, null if no handler was found.
+     */
+    public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) {
+        Uri loadingUrl = request.getUrl();
+        PathHandler handler;
+        synchronized (uriMatcher) {
+            handler = (PathHandler) uriMatcher.match(request.getUrl());
+        }
+        if (handler == null) {
+            return null;
+        }
+
+        if (isLocalFile(loadingUrl) || isMainUrl(loadingUrl) || !isAllowedUrl(loadingUrl) || isErrorUrl(loadingUrl)) {
+            Logger.debug("Handling local request: " + request.getUrl().toString());
+            return handleLocalRequest(request, handler);
+        } else {
+            return handleProxyRequest(request, handler);
+        }
+    }
+
+    private boolean isLocalFile(Uri uri) {
+        String path = uri.getPath();
+        return path.startsWith(capacitorContentStart) || path.startsWith(capacitorFileStart);
+    }
+
+    private boolean isErrorUrl(Uri uri) {
+        String url = uri.toString();
+        return url.equals(bridge.getErrorUrl());
+    }
+
+    private boolean isMainUrl(Uri loadingUrl) {
+        return (bridge.getServerUrl() == null && loadingUrl.getHost().equalsIgnoreCase(bridge.getHost()));
+    }
+
+    private boolean isAllowedUrl(Uri loadingUrl) {
+        return !(bridge.getServerUrl() == null && !bridge.getAppAllowNavigationMask().matches(loadingUrl.getHost()));
+    }
+
+    private WebResourceResponse handleLocalRequest(WebResourceRequest request, PathHandler handler) {
+        String path = request.getUrl().getPath();
+
+        if (request.getRequestHeaders().get("Range") != null) {
+            InputStream responseStream = new LollipopLazyInputStream(handler, request);
+            String mimeType = getMimeType(path, responseStream);
+            Map<String, String> tempResponseHeaders = handler.getResponseHeaders();
+            int statusCode = 206;
+            try {
+                int totalRange = responseStream.available();
+                String rangeString = request.getRequestHeaders().get("Range");
+                String[] parts = rangeString.split("=");
+                String[] streamParts = parts[1].split("-");
+                String fromRange = streamParts[0];
+                int range = totalRange - 1;
+                if (streamParts.length > 1) {
+                    range = Integer.parseInt(streamParts[1]);
+                }
+                tempResponseHeaders.put("Accept-Ranges", "bytes");
+                tempResponseHeaders.put("Content-Range", "bytes " + fromRange + "-" + range + "/" + totalRange);
+            } catch (IOException e) {
+                statusCode = 404;
+            }
+            return new WebResourceResponse(
+                mimeType,
+                handler.getEncoding(),
+                statusCode,
+                handler.getReasonPhrase(),
+                tempResponseHeaders,
+                responseStream
+            );
+        }
+
+        if (isLocalFile(request.getUrl()) || isErrorUrl(request.getUrl())) {
+            InputStream responseStream = new LollipopLazyInputStream(handler, request);
+            String mimeType = getMimeType(request.getUrl().getPath(), responseStream);
+            int statusCode = getStatusCode(responseStream, handler.getStatusCode());
+            return new WebResourceResponse(
+                mimeType,
+                handler.getEncoding(),
+                statusCode,
+                handler.getReasonPhrase(),
+                handler.getResponseHeaders(),
+                responseStream
+            );
+        }
+
+        if (path.equals("/cordova.js")) {
+            return new WebResourceResponse(
+                "application/javascript",
+                handler.getEncoding(),
+                handler.getStatusCode(),
+                handler.getReasonPhrase(),
+                handler.getResponseHeaders(),
+                null
+            );
+        }
+
+        if (path.equals("/") || (!request.getUrl().getLastPathSegment().contains(".") && html5mode)) {
+            InputStream responseStream;
+            try {
+                String startPath = this.basePath + "/index.html";
+                if (bridge.getRouteProcessor() != null) {
+                    ProcessedRoute processedRoute = bridge.getRouteProcessor().process(this.basePath, "/index.html");
+                    startPath = processedRoute.getPath();
+                    isAsset = processedRoute.isAsset();
+                }
+
+                if (isAsset) {
+                    responseStream = protocolHandler.openAsset(startPath);
+                } else {
+                    responseStream = protocolHandler.openFile(startPath);
+                }
+            } catch (IOException e) {
+                Logger.error("Unable to open index.html", e);
+                return null;
+            }
+
+            responseStream = jsInjector.getInjectedStream(responseStream);
+
+            int statusCode = getStatusCode(responseStream, handler.getStatusCode());
+            return new WebResourceResponse(
+                "text/html",
+                handler.getEncoding(),
+                statusCode,
+                handler.getReasonPhrase(),
+                handler.getResponseHeaders(),
+                responseStream
+            );
+        }
+
+        if ("/favicon.ico".equalsIgnoreCase(path)) {
+            try {
+                return new WebResourceResponse("image/png", null, null);
+            } catch (Exception e) {
+                Logger.error("favicon handling failed", e);
+            }
+        }
+
+        int periodIndex = path.lastIndexOf(".");
+        if (periodIndex >= 0) {
+            String ext = path.substring(path.lastIndexOf("."));
+
+            InputStream responseStream = new LollipopLazyInputStream(handler, request);
+
+            // TODO: Conjure up a bit more subtlety than this
+            if (ext.equals(".html")) {
+                responseStream = jsInjector.getInjectedStream(responseStream);
+            }
+
+            String mimeType = getMimeType(path, responseStream);
+            int statusCode = getStatusCode(responseStream, handler.getStatusCode());
+            return new WebResourceResponse(
+                mimeType,
+                handler.getEncoding(),
+                statusCode,
+                handler.getReasonPhrase(),
+                handler.getResponseHeaders(),
+                responseStream
+            );
+        }
+
+        return null;
+    }
+
+    /**
+     * Instead of reading files from the filesystem/assets, proxy through to the URL
+     * and let an external server handle it.
+     * @param request
+     * @param handler
+     * @return
+     */
+    private WebResourceResponse handleProxyRequest(WebResourceRequest request, PathHandler handler) {
+        final String method = request.getMethod();
+        if (method.equals("GET")) {
+            try {
+                String url = request.getUrl().toString();
+                Map<String, String> headers = request.getRequestHeaders();
+                boolean isHtmlText = false;
+                for (Map.Entry<String, String> header : headers.entrySet()) {
+                    if (header.getKey().equalsIgnoreCase("Accept") && header.getValue().toLowerCase().contains("text/html")) {
+                        isHtmlText = true;
+                        break;
+                    }
+                }
+                if (isHtmlText) {
+                    HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
+                    for (Map.Entry<String, String> header : headers.entrySet()) {
+                        conn.setRequestProperty(header.getKey(), header.getValue());
+                    }
+                    String getCookie = CookieManager.getInstance().getCookie(url);
+                    if (getCookie != null) {
+                        conn.setRequestProperty("Cookie", getCookie);
+                    }
+                    conn.setRequestMethod(method);
+                    conn.setReadTimeout(30 * 1000);
+                    conn.setConnectTimeout(30 * 1000);
+                    if (request.getUrl().getUserInfo() != null) {
+                        byte[] userInfoBytes = request.getUrl().getUserInfo().getBytes(StandardCharsets.UTF_8);
+                        String base64 = Base64.encodeToString(userInfoBytes, Base64.NO_WRAP);
+                        conn.setRequestProperty("Authorization", "Basic " + base64);
+                    }
+
+                    List<String> cookies = conn.getHeaderFields().get("Set-Cookie");
+                    if (cookies != null) {
+                        for (String cookie : cookies) {
+                            CookieManager.getInstance().setCookie(url, cookie);
+                        }
+                    }
+                    InputStream responseStream = conn.getInputStream();
+                    responseStream = jsInjector.getInjectedStream(responseStream);
+                    return new WebResourceResponse(
+                        "text/html",
+                        handler.getEncoding(),
+                        handler.getStatusCode(),
+                        handler.getReasonPhrase(),
+                        handler.getResponseHeaders(),
+                        responseStream
+                    );
+                }
+            } catch (Exception ex) {
+                bridge.handleAppUrlLoadError(ex);
+            }
+        }
+        return null;
+    }
+
+    private String getMimeType(String path, InputStream stream) {
+        String mimeType = null;
+        try {
+            mimeType = URLConnection.guessContentTypeFromName(path); // Does not recognize *.js
+            if (mimeType != null && path.endsWith(".js") && mimeType.equals("image/x-icon")) {
+                Logger.debug("We shouldn't be here");
+            }
+            if (mimeType == null) {
+                if (path.endsWith(".js") || path.endsWith(".mjs")) {
+                    // Make sure JS files get the proper mimetype to support ES modules
+                    mimeType = "application/javascript";
+                } else if (path.endsWith(".wasm")) {
+                    mimeType = "application/wasm";
+                } else {
+                    mimeType = URLConnection.guessContentTypeFromStream(stream);
+                }
+            }
+        } catch (Exception ex) {
+            Logger.error("Unable to get mime type" + path, ex);
+        }
+        return mimeType;
+    }
+
+    private int getStatusCode(InputStream stream, int defaultCode) {
+        int finalStatusCode = defaultCode;
+        try {
+            if (stream.available() == -1) {
+                finalStatusCode = 404;
+            }
+        } catch (IOException e) {
+            finalStatusCode = 500;
+        }
+        return finalStatusCode;
+    }
+
+    /**
+     * Registers a handler for the given <code>uri</code>. The <code>handler</code> will be invoked
+     * every time the <code>shouldInterceptRequest</code> method of the instance is called with
+     * a matching <code>uri</code>.
+     *
+     * @param uri     the uri to use the handler for. The scheme and authority (domain) will be matched
+     *                exactly. The path may contain a '*' element which will match a single element of
+     *                a path (so a handler registered for /a/* will be invoked for /a/b and /a/c.html
+     *                but not for /a/b/b) or the '**' element which will match any number of path
+     *                elements.
+     * @param handler the handler to use for the uri.
+     */
+    void register(Uri uri, PathHandler handler) {
+        synchronized (uriMatcher) {
+            uriMatcher.addURI(uri.getScheme(), uri.getAuthority(), uri.getPath(), handler);
+        }
+    }
+
+    /**
+     * Hosts the application's assets on an https:// URL. Assets from the local path
+     * <code>assetPath/...</code> will be available under
+     * <code>https://{uuid}.androidplatform.net/assets/...</code>.
+     *
+     * @param assetPath the local path in the application's asset folder which will be made
+     *                  available by the server (for example "/www").
+     * @return prefixes under which the assets are hosted.
+     */
+    public void hostAssets(String assetPath) {
+        this.isAsset = true;
+        this.basePath = assetPath;
+        createHostingDetails();
+    }
+
+    /**
+     * Hosts the application's files on an https:// URL. Files from the basePath
+     * <code>basePath/...</code> will be available under
+     * <code>https://{uuid}.androidplatform.net/...</code>.
+     *
+     * @param basePath the local path in the application's data folder which will be made
+     *                  available by the server (for example "/www").
+     * @return prefixes under which the assets are hosted.
+     */
+    public void hostFiles(final String basePath) {
+        this.isAsset = false;
+        this.basePath = basePath;
+        createHostingDetails();
+    }
+
+    private void createHostingDetails() {
+        final String assetPath = this.basePath;
+
+        if (assetPath.indexOf('*') != -1) {
+            throw new IllegalArgumentException("assetPath cannot contain the '*' character.");
+        }
+
+        PathHandler handler = new PathHandler() {
+            @Override
+            public InputStream handle(Uri url) {
+                InputStream stream = null;
+                String path = url.getPath();
+
+                // Pass path to routeProcessor if present
+                RouteProcessor routeProcessor = bridge.getRouteProcessor();
+                boolean ignoreAssetPath = false;
+                if (routeProcessor != null) {
+                    ProcessedRoute processedRoute = bridge.getRouteProcessor().process("", path);
+                    path = processedRoute.getPath();
+                    isAsset = processedRoute.isAsset();
+                    ignoreAssetPath = processedRoute.isIgnoreAssetPath();
+                }
+
+                try {
+                    if (path.startsWith(capacitorContentStart)) {
+                        stream = protocolHandler.openContentUrl(url);
+                    } else if (path.startsWith(capacitorFileStart)) {
+                        stream = protocolHandler.openFile(path);
+                    } else if (!isAsset) {
+                        if (routeProcessor == null) {
+                            path = basePath + url.getPath();
+                        }
+
+                        stream = protocolHandler.openFile(path);
+                    } else if (ignoreAssetPath) {
+                        stream = protocolHandler.openAsset(path);
+                    } else {
+                        stream = protocolHandler.openAsset(assetPath + path);
+                    }
+                } catch (IOException e) {
+                    Logger.error("Unable to open asset URL: " + url);
+                    return null;
+                }
+
+                return stream;
+            }
+        };
+
+        for (String authority : authorities) {
+            registerUriForScheme(Bridge.CAPACITOR_HTTP_SCHEME, handler, authority);
+            registerUriForScheme(Bridge.CAPACITOR_HTTPS_SCHEME, handler, authority);
+
+            String customScheme = this.bridge.getScheme();
+            if (!customScheme.equals(Bridge.CAPACITOR_HTTP_SCHEME) && !customScheme.equals(Bridge.CAPACITOR_HTTPS_SCHEME)) {
+                registerUriForScheme(customScheme, handler, authority);
+            }
+        }
+    }
+
+    private void registerUriForScheme(String scheme, PathHandler handler, String authority) {
+        Uri.Builder uriBuilder = new Uri.Builder();
+        uriBuilder.scheme(scheme);
+        uriBuilder.authority(authority);
+        uriBuilder.path("");
+        Uri uriPrefix = uriBuilder.build();
+
+        register(Uri.withAppendedPath(uriPrefix, "/"), handler);
+        register(Uri.withAppendedPath(uriPrefix, "**"), handler);
+    }
+
+    /**
+     * The KitKat WebView reads the InputStream on a separate threadpool. We can use that to
+     * parallelize loading.
+     */
+    private abstract static class LazyInputStream extends InputStream {
+
+        protected final PathHandler handler;
+        private InputStream is = null;
+
+        public LazyInputStream(PathHandler handler) {
+            this.handler = handler;
+        }
+
+        private InputStream getInputStream() {
+            if (is == null) {
+                is = handle();
+            }
+            return is;
+        }
+
+        protected abstract InputStream handle();
+
+        @Override
+        public int available() throws IOException {
+            InputStream is = getInputStream();
+            return (is != null) ? is.available() : -1;
+        }
+
+        @Override
+        public int read() throws IOException {
+            InputStream is = getInputStream();
+            return (is != null) ? is.read() : -1;
+        }
+
+        @Override
+        public int read(byte[] b) throws IOException {
+            InputStream is = getInputStream();
+            return (is != null) ? is.read(b) : -1;
+        }
+
+        @Override
+        public int read(byte[] b, int off, int len) throws IOException {
+            InputStream is = getInputStream();
+            return (is != null) ? is.read(b, off, len) : -1;
+        }
+
+        @Override
+        public long skip(long n) throws IOException {
+            InputStream is = getInputStream();
+            return (is != null) ? is.skip(n) : 0;
+        }
+    }
+
+    // For L and above.
+    private static class LollipopLazyInputStream extends LazyInputStream {
+
+        private WebResourceRequest request;
+        private InputStream is;
+
+        public LollipopLazyInputStream(PathHandler handler, WebResourceRequest request) {
+            super(handler);
+            this.request = request;
+        }
+
+        @Override
+        protected InputStream handle() {
+            return handler.handle(request);
+        }
+    }
+
+    public String getBasePath() {
+        return this.basePath;
+    }
+}

+ 11 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/annotation/ActivityCallback.java

@@ -0,0 +1,11 @@
+package com.getcapacitor.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface ActivityCallback {
+}

+ 35 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/annotation/CapacitorPlugin.java

@@ -0,0 +1,35 @@
+package com.getcapacitor.annotation;
+
+import android.content.Intent;
+import com.getcapacitor.PluginCall;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Base annotation for all Plugins
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface CapacitorPlugin {
+    /**
+     * A custom name for the plugin, otherwise uses the
+     * simple class name.
+     */
+    String name() default "";
+
+    /**
+     * Request codes this plugin uses and responds to, in order to tie
+     * Android events back the plugin to handle.
+     *
+     * NOTE: This is a legacy option provided to support third party libraries
+     * not currently implementing the new AndroidX Activity Results API. Plugins
+     * without this limitation should use a registered callback with
+     * {@link com.getcapacitor.Plugin#startActivityForResult(PluginCall, Intent, String)}
+     */
+    int[] requestCodes() default {};
+
+    /**
+     * Permissions this plugin needs, in order to make permission requests
+     * easy if the plugin only needs basic permission prompting
+     */
+    Permission[] permissions() default {};
+}

+ 22 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/annotation/Permission.java

@@ -0,0 +1,22 @@
+package com.getcapacitor.annotation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Permission annotation for use with @CapacitorPlugin
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Permission {
+    /**
+     * An array of Android permission strings.
+     * Eg: {Manifest.permission.ACCESS_COARSE_LOCATION}
+     *     or {"android.permission.ACCESS_COARSE_LOCATION"}
+     */
+    String[] strings() default {};
+
+    /**
+     * An optional name to use instead of the Android permission string.
+     */
+    String alias() default "";
+}

+ 11 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/annotation/PermissionCallback.java

@@ -0,0 +1,11 @@
+package com.getcapacitor.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface PermissionCallback {
+}

+ 42 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/cordova/CapacitorCordovaCookieManager.java

@@ -0,0 +1,42 @@
+package com.getcapacitor.cordova;
+
+import android.webkit.CookieManager;
+import android.webkit.WebView;
+import org.apache.cordova.ICordovaCookieManager;
+
+class CapacitorCordovaCookieManager implements ICordovaCookieManager {
+
+    protected final WebView webView;
+    private final CookieManager cookieManager;
+
+    public CapacitorCordovaCookieManager(WebView webview) {
+        webView = webview;
+        cookieManager = CookieManager.getInstance();
+        cookieManager.setAcceptThirdPartyCookies(webView, 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);
+    }
+
+    @Override
+    public void clearCookies() {
+        cookieManager.removeAllCookies(null);
+    }
+
+    @Override
+    public void flush() {
+        cookieManager.flush();
+    }
+}

+ 39 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/cordova/MockCordovaInterfaceImpl.java

@@ -0,0 +1,39 @@
+package com.getcapacitor.cordova;
+
+import android.util.Pair;
+import androidx.appcompat.app.AppCompatActivity;
+import java.util.concurrent.Executors;
+import org.apache.cordova.CordovaInterfaceImpl;
+import org.apache.cordova.CordovaPlugin;
+import org.json.JSONException;
+
+public class MockCordovaInterfaceImpl extends CordovaInterfaceImpl {
+
+    public MockCordovaInterfaceImpl(AppCompatActivity activity) {
+        super(activity, Executors.newCachedThreadPool());
+    }
+
+    public CordovaPlugin getActivityResultCallback() {
+        return this.activityResultCallback;
+    }
+
+    /**
+     * Checks Cordova permission callbacks to handle permissions defined by a Cordova plugin.
+     * Returns true if Cordova is handling the permission request with a registered code.
+     *
+     * @param requestCode
+     * @param permissions
+     * @param grantResults
+     * @return true if Cordova handled the permission request, false if not
+     */
+    @SuppressWarnings("deprecation")
+    public boolean handlePermissionResult(int requestCode, String[] permissions, int[] grantResults) throws JSONException {
+        Pair<CordovaPlugin, Integer> callback = permissionResultCallbacks.getAndRemoveCallback(requestCode);
+        if (callback != null) {
+            callback.first.onRequestPermissionResult(callback.second, permissions, grantResults);
+            return true;
+        }
+
+        return false;
+    }
+}

+ 284 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/cordova/MockCordovaWebViewImpl.java

@@ -0,0 +1,284 @@
+package com.getcapacitor.cordova;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.view.View;
+import android.webkit.ValueCallback;
+import android.webkit.WebChromeClient;
+import android.webkit.WebView;
+import java.util.List;
+import java.util.Map;
+import org.apache.cordova.CordovaInterface;
+import org.apache.cordova.CordovaPreferences;
+import org.apache.cordova.CordovaResourceApi;
+import org.apache.cordova.CordovaWebView;
+import org.apache.cordova.CordovaWebViewEngine;
+import org.apache.cordova.ICordovaCookieManager;
+import org.apache.cordova.NativeToJsMessageQueue;
+import org.apache.cordova.PluginEntry;
+import org.apache.cordova.PluginManager;
+import org.apache.cordova.PluginResult;
+
+public class MockCordovaWebViewImpl implements CordovaWebView {
+
+    private Context context;
+    private PluginManager pluginManager;
+    private CordovaPreferences preferences;
+    private CordovaResourceApi resourceApi;
+    private NativeToJsMessageQueue nativeToJsMessageQueue;
+    private CordovaInterface cordova;
+    private CapacitorCordovaCookieManager cookieManager;
+    private WebView webView;
+    private boolean hasPausedEver;
+
+    public MockCordovaWebViewImpl(Context context) {
+        this.context = context;
+    }
+
+    @Override
+    public void init(CordovaInterface cordova, List<PluginEntry> pluginEntries, CordovaPreferences preferences) {
+        this.cordova = cordova;
+        this.preferences = preferences;
+        this.pluginManager = new PluginManager(this, this.cordova, pluginEntries);
+        this.resourceApi = new CordovaResourceApi(this.context, this.pluginManager);
+        this.pluginManager.init();
+    }
+
+    public void init(CordovaInterface cordova, List<PluginEntry> pluginEntries, CordovaPreferences preferences, WebView webView) {
+        this.cordova = cordova;
+        this.webView = webView;
+        this.preferences = preferences;
+        this.pluginManager = new PluginManager(this, this.cordova, pluginEntries);
+        this.resourceApi = new CordovaResourceApi(this.context, this.pluginManager);
+        nativeToJsMessageQueue = new NativeToJsMessageQueue();
+        nativeToJsMessageQueue.addBridgeMode(new CapacitorEvalBridgeMode(webView, this.cordova));
+        nativeToJsMessageQueue.setBridgeMode(0);
+        this.cookieManager = new CapacitorCordovaCookieManager(webView);
+        this.pluginManager.init();
+    }
+
+    public static class CapacitorEvalBridgeMode extends NativeToJsMessageQueue.BridgeMode {
+
+        private final WebView webView;
+        private final CordovaInterface cordova;
+
+        public CapacitorEvalBridgeMode(WebView webView, CordovaInterface cordova) {
+            this.webView = webView;
+            this.cordova = cordova;
+        }
+
+        @Override
+        public void onNativeToJsMessageAvailable(final NativeToJsMessageQueue queue) {
+            cordova
+                .getActivity()
+                .runOnUiThread(
+                    () -> {
+                        String js = queue.popAndEncodeAsJs();
+                        if (js != null) {
+                            webView.evaluateJavascript(js, null);
+                        }
+                    }
+                );
+        }
+    }
+
+    @Override
+    public boolean isInitialized() {
+        return cordova != null;
+    }
+
+    @Override
+    public View getView() {
+        return this.webView;
+    }
+
+    @Override
+    public void loadUrlIntoView(String url, boolean recreatePlugins) {
+        if (url.equals("about:blank") || url.startsWith("javascript:")) {
+            webView.loadUrl(url);
+            return;
+        }
+    }
+
+    @Override
+    public void stopLoading() {}
+
+    @Override
+    public boolean canGoBack() {
+        return false;
+    }
+
+    @Override
+    public void clearCache() {}
+
+    @Deprecated
+    @Override
+    public void clearCache(boolean b) {}
+
+    @Override
+    public void clearHistory() {}
+
+    @Override
+    public boolean backHistory() {
+        return false;
+    }
+
+    @Override
+    public void handlePause(boolean keepRunning) {
+        if (!isInitialized()) {
+            return;
+        }
+        hasPausedEver = true;
+        pluginManager.onPause(keepRunning);
+        triggerDocumentEvent("pause");
+        // If app doesn't want to run in background
+        if (!keepRunning) {
+            // Pause JavaScript timers. This affects all webviews within the app!
+            this.setPaused(true);
+        }
+    }
+
+    @Override
+    public void onNewIntent(Intent intent) {
+        if (this.pluginManager != null) {
+            this.pluginManager.onNewIntent(intent);
+        }
+    }
+
+    @Override
+    public void handleResume(boolean keepRunning) {
+        if (!isInitialized()) {
+            return;
+        }
+        this.setPaused(false);
+        this.pluginManager.onResume(keepRunning);
+        if (hasPausedEver) {
+            triggerDocumentEvent("resume");
+        }
+    }
+
+    @Override
+    public void handleStart() {
+        if (!isInitialized()) {
+            return;
+        }
+        pluginManager.onStart();
+    }
+
+    @Override
+    public void handleStop() {
+        if (!isInitialized()) {
+            return;
+        }
+        pluginManager.onStop();
+    }
+
+    @Override
+    public void handleDestroy() {
+        if (!isInitialized()) {
+            return;
+        }
+        this.pluginManager.onDestroy();
+    }
+
+    @Deprecated
+    @Override
+    public void sendJavascript(String statememt) {
+        nativeToJsMessageQueue.addJavaScript(statememt);
+    }
+
+    public void eval(final String js, final ValueCallback<String> callback) {
+        Handler mainHandler = new Handler(context.getMainLooper());
+        mainHandler.post(() -> webView.evaluateJavascript(js, callback));
+    }
+
+    public void triggerDocumentEvent(final String eventName) {
+        eval("window.Capacitor.triggerEvent('" + eventName + "', 'document');", s -> {});
+    }
+
+    @Override
+    public void showWebPage(String url, boolean openExternal, boolean clearHistory, Map<String, Object> params) {}
+
+    @Deprecated
+    @Override
+    public boolean isCustomViewShowing() {
+        return false;
+    }
+
+    @Deprecated
+    @Override
+    public void showCustomView(View view, WebChromeClient.CustomViewCallback callback) {}
+
+    @Deprecated
+    @Override
+    public void hideCustomView() {}
+
+    @Override
+    public CordovaResourceApi getResourceApi() {
+        return this.resourceApi;
+    }
+
+    @Override
+    public void setButtonPlumbedToJs(int keyCode, boolean override) {}
+
+    @Override
+    public boolean isButtonPlumbedToJs(int keyCode) {
+        return false;
+    }
+
+    @Override
+    public void sendPluginResult(PluginResult cr, String callbackId) {
+        nativeToJsMessageQueue.addPluginResult(cr, callbackId);
+    }
+
+    @Override
+    public PluginManager getPluginManager() {
+        return this.pluginManager;
+    }
+
+    @Override
+    public CordovaWebViewEngine getEngine() {
+        return null;
+    }
+
+    @Override
+    public CordovaPreferences getPreferences() {
+        return this.preferences;
+    }
+
+    @Override
+    public ICordovaCookieManager getCookieManager() {
+        return cookieManager;
+    }
+
+    @Override
+    public String getUrl() {
+        return webView.getUrl();
+    }
+
+    @Override
+    public Context getContext() {
+        return this.webView.getContext();
+    }
+
+    @Override
+    public void loadUrl(String url) {
+        loadUrlIntoView(url, true);
+    }
+
+    @Override
+    public Object postMessage(String id, Object data) {
+        return pluginManager.postMessage(id, data);
+    }
+
+    public void setPaused(boolean value) {
+        if (value) {
+            webView.onPause();
+            webView.pauseTimers();
+        } else {
+            webView.onResume();
+            webView.resumeTimers();
+        }
+    }
+}

+ 236 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookieManager.java

@@ -0,0 +1,236 @@
+package com.getcapacitor.plugin;
+
+import com.getcapacitor.Bridge;
+import com.getcapacitor.Logger;
+import java.net.CookieManager;
+import java.net.CookiePolicy;
+import java.net.CookieStore;
+import java.net.HttpCookie;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+public class CapacitorCookieManager extends CookieManager {
+
+    private final android.webkit.CookieManager webkitCookieManager;
+
+    private final String localUrl;
+
+    private final String serverUrl;
+
+    private final String TAG = "CapacitorCookies";
+
+    /**
+     * Create a new cookie manager with the default cookie store and policy
+     */
+    public CapacitorCookieManager(Bridge bridge) {
+        this(null, null, bridge);
+    }
+
+    /**
+     * Create a new cookie manager with specified cookie store and cookie policy.
+     * @param store a {@code CookieStore} to be used by CookieManager. if {@code null}, cookie
+     *              manager will use a default one, which is an in-memory CookieStore implementation.
+     * @param policy a {@code CookiePolicy} instance to be used by cookie manager as policy
+     *               callback. if {@code null}, ACCEPT_ORIGINAL_SERVER will be used.
+     */
+    public CapacitorCookieManager(CookieStore store, CookiePolicy policy, Bridge bridge) {
+        super(store, policy);
+        webkitCookieManager = android.webkit.CookieManager.getInstance();
+        this.localUrl = bridge.getLocalUrl();
+        this.serverUrl = bridge.getServerUrl();
+    }
+
+    public void removeSessionCookies() {
+        this.webkitCookieManager.removeSessionCookies(null);
+    }
+
+    public String getSanitizedDomain(String url) throws URISyntaxException {
+        if (url == null || url.isEmpty()) {
+            url = this.serverUrl;
+        }
+
+        try {
+            new URI(url);
+        } catch (Exception ignored) {
+            url = this.localUrl;
+
+            try {
+                new URI(url);
+            } catch (Exception error) {
+                Logger.error(TAG, "Failed to get sanitized URL.", error);
+                throw error;
+            }
+        }
+
+        return url;
+    }
+
+    private String getDomainFromCookieString(String cookie) throws URISyntaxException {
+        String[] domain = cookie.toLowerCase(Locale.ROOT).split("domain=");
+        return getSanitizedDomain(domain.length <= 1 ? null : domain[1].split(";")[0].trim());
+    }
+
+    /**
+     * Gets the cookies for the given URL.
+     * @param url the URL for which the cookies are requested
+     * @return value the cookies as a string, using the format of the 'Cookie' HTTP request header
+     */
+    public String getCookieString(String url) {
+        try {
+            url = getSanitizedDomain(url);
+            Logger.info(TAG, "Getting cookies at: '" + url + "'");
+            return webkitCookieManager.getCookie(url);
+        } catch (Exception error) {
+            Logger.error(TAG, "Failed to get cookies at the given URL.", error);
+        }
+
+        return null;
+    }
+
+    /**
+     * Gets a cookie value for the given URL and key.
+     * @param url the URL for which the cookies are requested
+     * @param key the key of the cookie to search for
+     * @return the {@code HttpCookie} value of the cookie at the key,
+     *         otherwise it will return a new empty {@code HttpCookie}
+     */
+    public HttpCookie getCookie(String url, String key) {
+        HttpCookie[] cookies = getCookies(url);
+        for (HttpCookie cookie : cookies) {
+            if (cookie.getName().equals(key)) {
+                return cookie;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Gets an array of {@code HttpCookie} given a URL.
+     * @param url the URL for which the cookies are requested
+     * @return an {@code HttpCookie} array of non-expired cookies
+     */
+    public HttpCookie[] getCookies(String url) {
+        try {
+            ArrayList<HttpCookie> cookieList = new ArrayList<>();
+            String cookieString = getCookieString(url);
+            if (cookieString != null) {
+                String[] singleCookie = cookieString.split(";");
+                for (String c : singleCookie) {
+                    HttpCookie parsed = HttpCookie.parse(c).get(0);
+                    parsed.setValue(parsed.getValue());
+                    cookieList.add(parsed);
+                }
+            }
+            HttpCookie[] cookies = new HttpCookie[cookieList.size()];
+            return cookieList.toArray(cookies);
+        } catch (Exception ex) {
+            return new HttpCookie[0];
+        }
+    }
+
+    /**
+     * Sets a cookie for the given URL. Any existing cookie with the same host, path and name will
+     *  be replaced with the new cookie. The cookie being set will be ignored if it is expired.
+     * @param url the URL for which the cookie is to be set
+     * @param value the cookie as a string, using the format of the 'Set-Cookie' HTTP response header
+     */
+    public void setCookie(String url, String value) {
+        try {
+            url = getSanitizedDomain(url);
+            Logger.info(TAG, "Setting cookie '" + value + "' at: '" + url + "'");
+            webkitCookieManager.setCookie(url, value);
+            flush();
+        } catch (Exception error) {
+            Logger.error(TAG, "Failed to set cookie.", error);
+        }
+    }
+
+    /**
+     * Sets a cookie for the given URL. Any existing cookie with the same host, path and name will
+     *  be replaced with the new cookie. The cookie being set will be ignored if it is expired.
+     * @param url the URL for which the cookie is to be set
+     * @param key the {@code HttpCookie} name to use for lookup
+     * @param value the value of the {@code HttpCookie} given a key
+     */
+    public void setCookie(String url, String key, String value) {
+        String cookieValue = key + "=" + value;
+        setCookie(url, cookieValue);
+    }
+
+    public void setCookie(String url, String key, String value, String expires, String path) {
+        String cookieValue = key + "=" + value + "; expires=" + expires + "; path=" + path;
+        setCookie(url, cookieValue);
+    }
+
+    /**
+     * Removes all cookies. This method is asynchronous.
+     */
+    public void removeAllCookies() {
+        webkitCookieManager.removeAllCookies(null);
+        flush();
+    }
+
+    /**
+     * Ensures all cookies currently accessible through the getCookie API are written to persistent
+     *  storage. This call will block the caller until it is done and may perform I/O.
+     */
+    public void flush() {
+        webkitCookieManager.flush();
+    }
+
+    @Override
+    public void put(URI uri, Map<String, List<String>> responseHeaders) {
+        // make sure our args are valid
+        if ((uri == null) || (responseHeaders == null)) return;
+
+        // go over the headers
+        for (String headerKey : responseHeaders.keySet()) {
+            // ignore headers which aren't cookie related
+            if ((headerKey == null) || !(headerKey.equalsIgnoreCase("Set-Cookie2") || headerKey.equalsIgnoreCase("Set-Cookie"))) continue;
+
+            // process each of the headers
+            for (String headerValue : Objects.requireNonNull(responseHeaders.get(headerKey))) {
+                try {
+                    // Set at the requested server url
+                    setCookie(uri.toString(), headerValue);
+
+                    // Set at the defined domain in the response or at default capacitor hosted url
+                    setCookie(getDomainFromCookieString(headerValue), headerValue);
+                } catch (Exception ignored) {}
+            }
+        }
+    }
+
+    @Override
+    public Map<String, List<String>> get(URI uri, Map<String, List<String>> requestHeaders) {
+        // make sure our args are valid
+        if ((uri == null) || (requestHeaders == null)) throw new IllegalArgumentException("Argument is null");
+
+        // save our url once
+        String url = uri.toString();
+
+        // prepare our response
+        Map<String, List<String>> res = new HashMap<>();
+
+        // get the cookie
+        String cookie = getCookieString(url);
+
+        // return it
+        if (cookie != null) res.put("Cookie", Collections.singletonList(cookie));
+        return res;
+    }
+
+    @Override
+    public CookieStore getCookieStore() {
+        // we don't want anyone to work with this cookie store directly
+        throw new UnsupportedOperationException();
+    }
+}

+ 137 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookies.java

@@ -0,0 +1,137 @@
+package com.getcapacitor.plugin;
+
+import android.webkit.JavascriptInterface;
+import com.getcapacitor.JSObject;
+import com.getcapacitor.Plugin;
+import com.getcapacitor.PluginCall;
+import com.getcapacitor.PluginConfig;
+import com.getcapacitor.PluginMethod;
+import com.getcapacitor.annotation.CapacitorPlugin;
+import java.io.UnsupportedEncodingException;
+import java.net.CookieHandler;
+import java.net.HttpCookie;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+
+@CapacitorPlugin
+public class CapacitorCookies extends Plugin {
+
+    CapacitorCookieManager cookieManager;
+
+    @Override
+    public void load() {
+        this.bridge.getWebView().addJavascriptInterface(this, "CapacitorCookiesAndroidInterface");
+        this.cookieManager = new CapacitorCookieManager(null, java.net.CookiePolicy.ACCEPT_ALL, this.bridge);
+        this.cookieManager.removeSessionCookies();
+        CookieHandler.setDefault(this.cookieManager);
+        super.load();
+    }
+
+    @Override
+    protected void handleOnDestroy() {
+        super.handleOnDestroy();
+        this.cookieManager.removeSessionCookies();
+    }
+
+    @JavascriptInterface
+    public boolean isEnabled() {
+        PluginConfig pluginConfig = getBridge().getConfig().getPluginConfiguration("CapacitorCookies");
+        return pluginConfig.getBoolean("enabled", false);
+    }
+
+    private boolean isAllowingInsecureCookies() {
+        PluginConfig pluginConfig = getBridge().getConfig().getPluginConfiguration("CapacitorCookies");
+        return pluginConfig.getBoolean("androidCustomSchemeAllowInsecureAccess", false);
+    }
+
+    @JavascriptInterface
+    public void setCookie(String domain, String action) {
+        cookieManager.setCookie(domain, action);
+    }
+
+    @PluginMethod
+    public void getCookies(PluginCall call) {
+        if (isAllowingInsecureCookies()) {
+            String url = call.getString("url");
+            JSObject cookiesMap = new JSObject();
+            HttpCookie[] cookies = cookieManager.getCookies(url);
+            for (HttpCookie cookie : cookies) {
+                cookiesMap.put(cookie.getName(), cookie.getValue());
+            }
+            call.resolve(cookiesMap);
+        } else {
+            this.bridge.eval(
+                    "document.cookie",
+                    value -> {
+                        String cookies = value.substring(1, value.length() - 1);
+                        String[] cookieArray = cookies.split(";");
+
+                        JSObject cookieMap = new JSObject();
+
+                        for (String cookie : cookieArray) {
+                            if (cookie.length() > 0) {
+                                String[] keyValue = cookie.split("=", 2);
+
+                                if (keyValue.length == 2) {
+                                    String key = keyValue[0].trim();
+                                    String val = keyValue[1].trim();
+                                    try {
+                                        key = URLDecoder.decode(keyValue[0].trim(), StandardCharsets.UTF_8.name());
+                                        val = URLDecoder.decode(keyValue[1].trim(), StandardCharsets.UTF_8.name());
+                                    } catch (UnsupportedEncodingException ignored) {}
+
+                                    cookieMap.put(key, val);
+                                }
+                            }
+                        }
+
+                        call.resolve(cookieMap);
+                    }
+                );
+        }
+    }
+
+    @PluginMethod
+    public void setCookie(PluginCall call) {
+        String key = call.getString("key");
+        if (null == key) {
+            call.reject("Must provide key");
+        }
+        String value = call.getString("value");
+        if (null == value) {
+            call.reject("Must provide value");
+        }
+        String url = call.getString("url");
+        String expires = call.getString("expires", "");
+        String path = call.getString("path", "/");
+        cookieManager.setCookie(url, key, value, expires, path);
+        call.resolve();
+    }
+
+    @PluginMethod
+    public void deleteCookie(PluginCall call) {
+        String key = call.getString("key");
+        if (null == key) {
+            call.reject("Must provide key");
+        }
+        String url = call.getString("url");
+        cookieManager.setCookie(url, key + "=; Expires=Wed, 31 Dec 2000 23:59:59 GMT");
+        call.resolve();
+    }
+
+    @PluginMethod
+    public void clearCookies(PluginCall call) {
+        String url = call.getString("url");
+        HttpCookie[] cookies = cookieManager.getCookies(url);
+        for (HttpCookie cookie : cookies) {
+            cookieManager.setCookie(url, cookie.getName() + "=; Expires=Wed, 31 Dec 2000 23:59:59 GMT");
+        }
+        call.resolve();
+    }
+
+    @PluginMethod
+    public void clearAllCookies(PluginCall call) {
+        cookieManager.removeAllCookies();
+        call.resolve();
+    }
+}

+ 119 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorHttp.java

@@ -0,0 +1,119 @@
+package com.getcapacitor.plugin;
+
+import android.Manifest;
+import android.webkit.JavascriptInterface;
+import com.getcapacitor.JSObject;
+import com.getcapacitor.Plugin;
+import com.getcapacitor.PluginCall;
+import com.getcapacitor.PluginConfig;
+import com.getcapacitor.PluginMethod;
+import com.getcapacitor.annotation.CapacitorPlugin;
+import com.getcapacitor.annotation.Permission;
+import com.getcapacitor.plugin.util.CapacitorHttpUrlConnection;
+import com.getcapacitor.plugin.util.HttpRequestHandler;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+@CapacitorPlugin(
+    permissions = {
+        @Permission(strings = { Manifest.permission.WRITE_EXTERNAL_STORAGE }, alias = "HttpWrite"),
+        @Permission(strings = { Manifest.permission.READ_EXTERNAL_STORAGE }, alias = "HttpRead")
+    }
+)
+public class CapacitorHttp extends Plugin {
+
+    private final Map<Runnable, PluginCall> activeRequests = new HashMap<>();
+    private final ExecutorService executor = Executors.newCachedThreadPool();
+
+    @Override
+    public void load() {
+        this.bridge.getWebView().addJavascriptInterface(this, "CapacitorHttpAndroidInterface");
+        super.load();
+    }
+
+    @Override
+    protected void handleOnDestroy() {
+        super.handleOnDestroy();
+
+        for (Map.Entry<Runnable, PluginCall> entry : activeRequests.entrySet()) {
+            Runnable job = entry.getKey();
+            PluginCall call = entry.getValue();
+
+            if (call.getData().has("activeCapacitorHttpUrlConnection")) {
+                try {
+                    CapacitorHttpUrlConnection connection = (CapacitorHttpUrlConnection) call
+                        .getData()
+                        .get("activeCapacitorHttpUrlConnection");
+                    connection.disconnect();
+                    call.getData().remove("activeCapacitorHttpUrlConnection");
+                } catch (Exception ignored) {}
+            }
+
+            getBridge().releaseCall(call);
+        }
+
+        activeRequests.clear();
+        executor.shutdownNow();
+    }
+
+    private void http(final PluginCall call, final String httpMethod) {
+        Runnable asyncHttpCall = new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    JSObject response = HttpRequestHandler.request(call, httpMethod, getBridge());
+                    call.resolve(response);
+                } catch (Exception e) {
+                    call.reject(e.getLocalizedMessage(), e.getClass().getSimpleName(), e);
+                } finally {
+                    activeRequests.remove(this);
+                }
+            }
+        };
+
+        if (!executor.isShutdown()) {
+            activeRequests.put(asyncHttpCall, call);
+            executor.submit(asyncHttpCall);
+        } else {
+            call.reject("Failed to execute request - Http Plugin was shutdown");
+        }
+    }
+
+    @JavascriptInterface
+    public boolean isEnabled() {
+        PluginConfig pluginConfig = getBridge().getConfig().getPluginConfiguration("CapacitorHttp");
+        return pluginConfig.getBoolean("enabled", false);
+    }
+
+    @PluginMethod
+    public void request(final PluginCall call) {
+        this.http(call, null);
+    }
+
+    @PluginMethod
+    public void get(final PluginCall call) {
+        this.http(call, "GET");
+    }
+
+    @PluginMethod
+    public void post(final PluginCall call) {
+        this.http(call, "POST");
+    }
+
+    @PluginMethod
+    public void put(final PluginCall call) {
+        this.http(call, "PUT");
+    }
+
+    @PluginMethod
+    public void patch(final PluginCall call) {
+        this.http(call, "PATCH");
+    }
+
+    @PluginMethod
+    public void delete(final PluginCall call) {
+        this.http(call, "DELETE");
+    }
+}

+ 48 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/plugin/WebView.java

@@ -0,0 +1,48 @@
+package com.getcapacitor.plugin;
+
+import android.app.Activity;
+import android.content.SharedPreferences;
+import com.getcapacitor.JSObject;
+import com.getcapacitor.Plugin;
+import com.getcapacitor.PluginCall;
+import com.getcapacitor.PluginMethod;
+import com.getcapacitor.annotation.CapacitorPlugin;
+
+@CapacitorPlugin
+public class WebView extends Plugin {
+
+    public static final String WEBVIEW_PREFS_NAME = "CapWebViewSettings";
+    public static final String CAP_SERVER_PATH = "serverBasePath";
+
+    @PluginMethod
+    public void setServerAssetPath(PluginCall call) {
+        String path = call.getString("path");
+        bridge.setServerAssetPath(path);
+        call.resolve();
+    }
+
+    @PluginMethod
+    public void setServerBasePath(PluginCall call) {
+        String path = call.getString("path");
+        bridge.setServerBasePath(path);
+        call.resolve();
+    }
+
+    @PluginMethod
+    public void getServerBasePath(PluginCall call) {
+        String path = bridge.getServerBasePath();
+        JSObject ret = new JSObject();
+        ret.put("path", path);
+        call.resolve(ret);
+    }
+
+    @PluginMethod
+    public void persistServerBasePath(PluginCall call) {
+        String path = bridge.getServerBasePath();
+        SharedPreferences prefs = getContext().getSharedPreferences(WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE);
+        SharedPreferences.Editor editor = prefs.edit();
+        editor.putString(CAP_SERVER_PATH, path);
+        editor.apply();
+        call.resolve();
+    }
+}

+ 358 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/plugin/util/AssetUtil.java

@@ -0,0 +1,358 @@
+package com.getcapacitor.plugin.util;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.StrictMode;
+import androidx.core.content.FileProvider;
+import com.getcapacitor.Logger;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.UUID;
+
+/**
+ * Manager for assets.
+ */
+public final class AssetUtil {
+
+    public static final int RESOURCE_ID_ZERO_VALUE = 0;
+    // Name of the storage folder
+    private static final String STORAGE_FOLDER = "/capacitorassets";
+
+    // Ref to the context passed through the constructor to access the
+    // resources and app directory.
+    private final Context context;
+
+    /**
+     * Constructor
+     *
+     * @param context Application context.
+     */
+    private AssetUtil(Context context) {
+        this.context = context;
+    }
+
+    /**
+     * Static method to retrieve class instance.
+     *
+     * @param context Application context.
+     */
+    public static AssetUtil getInstance(Context context) {
+        return new AssetUtil(context);
+    }
+
+    /**
+     * The URI for a path.
+     *
+     * @param path The given path.
+     */
+    public Uri parse(String path) {
+        if (path == null || path.isEmpty()) {
+            return Uri.EMPTY;
+        } else if (path.startsWith("res:")) {
+            return getUriForResourcePath(path);
+        } else if (path.startsWith("file:///")) {
+            return getUriFromPath(path);
+        } else if (path.startsWith("file://")) {
+            return getUriFromAsset(path);
+        } else if (path.startsWith("http")) {
+            return getUriFromRemote(path);
+        } else if (path.startsWith("content://")) {
+            return Uri.parse(path);
+        }
+
+        return Uri.EMPTY;
+    }
+
+    /**
+     * URI for a file.
+     *
+     * @param path Absolute path like file:///...
+     *
+     * @return URI pointing to the given path.
+     */
+    private Uri getUriFromPath(String path) {
+        String absPath = path.replaceFirst("file://", "").replaceFirst("\\?.*$", "");
+        File file = new File(absPath);
+
+        if (!file.exists()) {
+            Logger.error("File not found: " + file.getAbsolutePath());
+            return Uri.EMPTY;
+        }
+
+        return getUriFromFile(file);
+    }
+
+    /**
+     * URI for an asset.
+     *
+     * @param path Asset path like file://...
+     *
+     * @return URI pointing to the given path.
+     */
+    private Uri getUriFromAsset(String path) {
+        String resPath = path.replaceFirst("file:/", "www").replaceFirst("\\?.*$", "");
+        String fileName = resPath.substring(resPath.lastIndexOf('/') + 1);
+        File file = getTmpFile(fileName);
+
+        if (file == null) return Uri.EMPTY;
+
+        try {
+            AssetManager assets = context.getAssets();
+            InputStream in = assets.open(resPath);
+            FileOutputStream out = new FileOutputStream(file);
+            copyFile(in, out);
+        } catch (Exception e) {
+            Logger.error("File not found: assets/" + resPath);
+            return Uri.EMPTY;
+        }
+
+        return getUriFromFile(file);
+    }
+
+    /**
+     * The URI for a resource.
+     *
+     * @param path The given relative path.
+     *
+     * @return URI pointing to the given path.
+     */
+    private Uri getUriForResourcePath(String path) {
+        Resources res = context.getResources();
+        String resPath = path.replaceFirst("res://", "");
+        int resId = getResId(resPath);
+
+        if (resId == 0) {
+            Logger.error("File not found: " + resPath);
+            return Uri.EMPTY;
+        }
+
+        return new Uri.Builder()
+            .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
+            .authority(res.getResourcePackageName(resId))
+            .appendPath(res.getResourceTypeName(resId))
+            .appendPath(res.getResourceEntryName(resId))
+            .build();
+    }
+
+    /**
+     * Uri from remote located content.
+     *
+     * @param path Remote address.
+     *
+     * @return Uri of the downloaded file.
+     */
+    private Uri getUriFromRemote(String path) {
+        File file = getTmpFile();
+
+        if (file == null) return Uri.EMPTY;
+
+        try {
+            URL url = new URL(path);
+            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+
+            StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
+
+            StrictMode.setThreadPolicy(policy);
+
+            connection.setRequestProperty("Connection", "close");
+            connection.setConnectTimeout(5000);
+            connection.connect();
+
+            InputStream in = connection.getInputStream();
+            FileOutputStream out = new FileOutputStream(file);
+
+            copyFile(in, out);
+            return getUriFromFile(file);
+        } catch (MalformedURLException e) {
+            Logger.error(Logger.tags("Asset"), "Incorrect URL", e);
+        } catch (FileNotFoundException e) {
+            Logger.error(Logger.tags("Asset"), "Failed to create new File from HTTP Content", e);
+        } catch (IOException e) {
+            Logger.error(Logger.tags("Asset"), "No Input can be created from http Stream", e);
+        }
+
+        return Uri.EMPTY;
+    }
+
+    /**
+     * Copy content from input stream into output stream.
+     *
+     * @param in  The input stream.
+     * @param out The output stream.
+     */
+    private void copyFile(InputStream in, FileOutputStream out) {
+        byte[] buffer = new byte[1024];
+        int read;
+
+        try {
+            while ((read = in.read(buffer)) != -1) {
+                out.write(buffer, 0, read);
+            }
+            out.flush();
+            out.close();
+        } catch (Exception e) {
+            Logger.error("Error copying", e);
+        }
+    }
+
+    /**
+     * Resource ID for drawable.
+     *
+     * @param resPath Resource path as string.
+     *
+     * @return The resource ID or 0 if not found.
+     */
+    public int getResId(String resPath) {
+        int resId = getResId(context.getResources(), resPath);
+
+        if (resId == 0) {
+            resId = getResId(Resources.getSystem(), resPath);
+        }
+
+        return resId;
+    }
+
+    /**
+     * Get resource ID.
+     *
+     * @param res     The resources where to look for.
+     * @param resPath The name of the resource.
+     *
+     * @return The resource ID or 0 if not found.
+     */
+    private int getResId(Resources res, String resPath) {
+        String pkgName = getPkgName(res);
+        String resName = getBaseName(resPath);
+        int resId;
+
+        resId = res.getIdentifier(resName, "mipmap", pkgName);
+
+        if (resId == 0) {
+            resId = res.getIdentifier(resName, "drawable", pkgName);
+        }
+
+        if (resId == 0) {
+            resId = res.getIdentifier(resName, "raw", pkgName);
+        }
+
+        return resId;
+    }
+
+    /**
+     * Convert URI to Bitmap.
+     *
+     * @param uri Internal image URI
+     */
+    public Bitmap getIconFromUri(Uri uri) throws IOException {
+        InputStream input = context.getContentResolver().openInputStream(uri);
+        return BitmapFactory.decodeStream(input);
+    }
+
+    /**
+     * Extract name of drawable resource from path.
+     *
+     * @param resPath Resource path as string.
+     */
+    private String getBaseName(String resPath) {
+        String drawable = resPath;
+
+        if (drawable.contains("/")) {
+            drawable = drawable.substring(drawable.lastIndexOf('/') + 1);
+        }
+
+        if (resPath.contains(".")) {
+            drawable = drawable.substring(0, drawable.lastIndexOf('.'));
+        }
+
+        return drawable;
+    }
+
+    /**
+     * Returns a file located under the external cache dir of that app.
+     *
+     * @return File with a random UUID name.
+     */
+    private File getTmpFile() {
+        return getTmpFile(UUID.randomUUID().toString());
+    }
+
+    /**
+     * Returns a file located under the external cache dir of that app.
+     *
+     * @param name The name of the file.
+     *
+     * @return File with the provided name.
+     */
+    private File getTmpFile(String name) {
+        File dir = context.getExternalCacheDir();
+
+        if (dir == null) {
+            dir = context.getCacheDir();
+        }
+
+        if (dir == null) {
+            Logger.error(Logger.tags("Asset"), "Missing cache dir", null);
+            return null;
+        }
+
+        String storage = dir.toString() + STORAGE_FOLDER;
+
+        //noinspection ResultOfMethodCallIgnored
+        new File(storage).mkdir();
+
+        return new File(storage, name);
+    }
+
+    /**
+     * Get content URI for the specified file.
+     *
+     * @param file The file to get the URI.
+     *
+     * @return content://...
+     */
+    private Uri getUriFromFile(File file) {
+        try {
+            String authority = context.getPackageName() + ".provider";
+            return FileProvider.getUriForFile(context, authority, file);
+        } catch (IllegalArgumentException e) {
+            Logger.error("File not supported by provider", e);
+            return Uri.EMPTY;
+        }
+    }
+
+    /**
+     * Package name specified by the resource bundle.
+     */
+    private String getPkgName(Resources res) {
+        return res == Resources.getSystem() ? "android" : context.getPackageName();
+    }
+
+    public static int getResourceID(Context context, String resourceName, String dir) {
+        return context.getResources().getIdentifier(resourceName, dir, context.getPackageName());
+    }
+
+    public static String getResourceBaseName(String resPath) {
+        if (resPath == null) return null;
+
+        if (resPath.contains("/")) {
+            return resPath.substring(resPath.lastIndexOf('/') + 1);
+        }
+
+        if (resPath.contains(".")) {
+            return resPath.substring(0, resPath.lastIndexOf('.'));
+        }
+
+        return resPath;
+    }
+}

+ 473 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/plugin/util/CapacitorHttpUrlConnection.java

@@ -0,0 +1,473 @@
+package com.getcapacitor.plugin.util;
+
+import android.os.Build;
+import android.os.LocaleList;
+import android.text.TextUtils;
+import com.getcapacitor.Bridge;
+import com.getcapacitor.JSArray;
+import com.getcapacitor.JSObject;
+import com.getcapacitor.JSValue;
+import com.getcapacitor.PluginCall;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.net.HttpURLConnection;
+import java.net.ProtocolException;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.net.UnknownServiceException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLSocketFactory;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class CapacitorHttpUrlConnection implements ICapacitorHttpUrlConnection {
+
+    private final HttpURLConnection connection;
+
+    /**
+     * Make a new CapacitorHttpUrlConnection instance, which wraps around HttpUrlConnection
+     * and provides some helper functions for setting request headers and the request body
+     * @param conn the base HttpUrlConnection. You can pass the value from
+     *             {@code (HttpUrlConnection) URL.openConnection()}
+     */
+    public CapacitorHttpUrlConnection(HttpURLConnection conn) {
+        connection = conn;
+        this.setDefaultRequestProperties();
+    }
+
+    /**
+     * Returns the underlying HttpUrlConnection value
+     * @return the underlying HttpUrlConnection value
+     */
+    public HttpURLConnection getHttpConnection() {
+        return connection;
+    }
+
+    public void disconnect() {
+        connection.disconnect();
+    }
+
+    /**
+     * Set the value of the {@code allowUserInteraction} field of
+     * this {@code URLConnection}.
+     *
+     * @param   isAllowedInteraction   the new value.
+     * @throws IllegalStateException if already connected
+     */
+    public void setAllowUserInteraction(boolean isAllowedInteraction) {
+        connection.setAllowUserInteraction(isAllowedInteraction);
+    }
+
+    /**
+     * Set the method for the URL request, one of:
+     * <UL>
+     *  <LI>GET
+     *  <LI>POST
+     *  <LI>HEAD
+     *  <LI>OPTIONS
+     *  <LI>PUT
+     *  <LI>DELETE
+     *  <LI>TRACE
+     * </UL> are legal, subject to protocol restrictions.  The default
+     * method is GET.
+     *
+     * @param method the HTTP method
+     * @exception ProtocolException if the method cannot be reset or if
+     *              the requested method isn't valid for HTTP.
+     * @exception SecurityException if a security manager is set and the
+     *              method is "TRACE", but the "allowHttpTrace"
+     *              NetPermission is not granted.
+     */
+    public void setRequestMethod(String method) throws ProtocolException {
+        connection.setRequestMethod(method);
+    }
+
+    /**
+     * Sets a specified timeout value, in milliseconds, to be used
+     * when opening a communications link to the resource referenced
+     * by this URLConnection.  If the timeout expires before the
+     * connection can be established, a
+     * java.net.SocketTimeoutException is raised. A timeout of zero is
+     * interpreted as an infinite timeout.
+     *
+     * <p><strong>Warning</strong>: If the hostname resolves to multiple IP
+     * addresses, Android's default implementation of {@link HttpURLConnection}
+     * will try each in
+     * <a href="http://www.ietf.org/rfc/rfc3484.txt">RFC 3484</a> order. If
+     * connecting to each of these addresses fails, multiple timeouts will
+     * elapse before the connect attempt throws an exception. Host names
+     * that support both IPv6 and IPv4 always have at least 2 IP addresses.
+     *
+     * @param timeout an {@code int} that specifies the connect
+     *               timeout value in milliseconds
+     * @throws IllegalArgumentException if the timeout parameter is negative
+     */
+    public void setConnectTimeout(int timeout) {
+        if (timeout < 0) {
+            throw new IllegalArgumentException("timeout can not be negative");
+        }
+        connection.setConnectTimeout(timeout);
+    }
+
+    /**
+     * Sets the read timeout to a specified timeout, in
+     * milliseconds. A non-zero value specifies the timeout when
+     * reading from Input stream when a connection is established to a
+     * resource. If the timeout expires before there is data available
+     * for read, a java.net.SocketTimeoutException is raised. A
+     * timeout of zero is interpreted as an infinite timeout.
+     *
+     * @param timeout an {@code int} that specifies the timeout
+     * value to be used in milliseconds
+     * @throws IllegalArgumentException if the timeout parameter is negative
+     */
+    public void setReadTimeout(int timeout) {
+        if (timeout < 0) {
+            throw new IllegalArgumentException("timeout can not be negative");
+        }
+        connection.setReadTimeout(timeout);
+    }
+
+    /**
+     * Sets whether automatic HTTP redirects should be disabled
+     * @param disableRedirects the flag to determine if redirects should be followed
+     */
+    public void setDisableRedirects(boolean disableRedirects) {
+        connection.setInstanceFollowRedirects(!disableRedirects);
+    }
+
+    /**
+     * Sets the request headers given a JSObject of key-value pairs
+     * @param headers the JSObject values to map to the HttpUrlConnection request headers
+     */
+    public void setRequestHeaders(JSObject headers) {
+        Iterator<String> keys = headers.keys();
+        while (keys.hasNext()) {
+            String key = keys.next();
+            String value = headers.getString(key);
+            connection.setRequestProperty(key, value);
+        }
+    }
+
+    /**
+     * Sets the value of the {@code doOutput} field for this
+     * {@code URLConnection} to the specified value.
+     * <p>
+     * A URL connection can be used for input and/or output.  Set the DoOutput
+     * flag to true if you intend to use the URL connection for output,
+     * false if not.  The default is false.
+     *
+     * @param  shouldDoOutput   the new value.
+     * @throws IllegalStateException if already connected
+     */
+    public void setDoOutput(boolean shouldDoOutput) {
+        connection.setDoOutput(shouldDoOutput);
+    }
+
+    /**
+     *
+     * @param call
+     * @throws JSONException
+     * @throws IOException
+     */
+    public void setRequestBody(PluginCall call, JSValue body) throws JSONException, IOException {
+        setRequestBody(call, body, null);
+    }
+
+    /**
+     *
+     * @param call
+     * @throws JSONException
+     * @throws IOException
+     */
+    public void setRequestBody(PluginCall call, JSValue body, String bodyType) throws JSONException, IOException {
+        String contentType = connection.getRequestProperty("Content-Type");
+        String dataString = "";
+
+        if (contentType == null || contentType.isEmpty()) return;
+
+        if (contentType.contains("application/json")) {
+            JSArray jsArray = null;
+            if (body != null) {
+                dataString = body.toString();
+            } else {
+                jsArray = call.getArray("data", null);
+            }
+            if (jsArray != null) {
+                dataString = jsArray.toString();
+            } else if (body == null) {
+                dataString = call.getString("data");
+            }
+            this.writeRequestBody(dataString != null ? dataString : "");
+        } else if (bodyType != null && bodyType.equals("file")) {
+            try (DataOutputStream os = new DataOutputStream(connection.getOutputStream())) {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                    os.write(Base64.getDecoder().decode(body.toString()));
+                }
+                os.flush();
+            }
+        } else if (contentType.contains("application/x-www-form-urlencoded")) {
+            try {
+                JSObject obj = body.toJSObject();
+                this.writeObjectRequestBody(obj);
+            } catch (Exception e) {
+                // Body is not a valid JSON, treat it as an already formatted string
+                this.writeRequestBody(body.toString());
+            }
+        } else if (bodyType != null && bodyType.equals("formData")) {
+            this.writeFormDataRequestBody(contentType, body.toJSArray());
+        } else {
+            this.writeRequestBody(body.toString());
+        }
+    }
+
+    /**
+     * Writes the provided string to the HTTP connection managed by this instance.
+     *
+     * @param body The string value to write to the connection stream.
+     */
+    private void writeRequestBody(String body) throws IOException {
+        try (DataOutputStream os = new DataOutputStream(connection.getOutputStream())) {
+            os.write(body.getBytes(StandardCharsets.UTF_8));
+            os.flush();
+        }
+    }
+
+    private void writeObjectRequestBody(JSObject object) throws IOException, JSONException {
+        try (DataOutputStream os = new DataOutputStream(connection.getOutputStream())) {
+            Iterator<String> keys = object.keys();
+            while (keys.hasNext()) {
+                String key = keys.next();
+                Object d = object.get(key);
+                os.writeBytes(key);
+                os.writeBytes("=");
+                os.writeBytes(URLEncoder.encode(d.toString(), "UTF-8"));
+
+                if (keys.hasNext()) {
+                    os.writeBytes("&");
+                }
+            }
+            os.flush();
+        }
+    }
+
+    private void writeFormDataRequestBody(String contentType, JSArray entries) throws IOException, JSONException {
+        try (DataOutputStream os = new DataOutputStream(connection.getOutputStream())) {
+            String boundary = contentType.split(";")[1].split("=")[1];
+            String lineEnd = "\r\n";
+            String twoHyphens = "--";
+
+            for (Object e : entries.toList()) {
+                if (e instanceof JSONObject) {
+                    JSONObject entry = (JSONObject) e;
+                    String type = entry.getString("type");
+                    String key = entry.getString("key");
+                    String value = entry.getString("value");
+                    if (type.equals("string")) {
+                        os.writeBytes(twoHyphens + boundary + lineEnd);
+                        os.writeBytes("Content-Disposition: form-data; name=\"" + key + "\"" + lineEnd + lineEnd);
+                        os.writeBytes(value);
+                        os.writeBytes(lineEnd);
+                    } else if (type.equals("base64File")) {
+                        String fileName = entry.getString("fileName");
+                        String fileContentType = entry.getString("contentType");
+
+                        os.writeBytes(twoHyphens + boundary + lineEnd);
+                        os.writeBytes("Content-Disposition: form-data; name=\"" + key + "\"; filename=\"" + fileName + "\"" + lineEnd);
+                        os.writeBytes("Content-Type: " + fileContentType + lineEnd);
+                        os.writeBytes("Content-Transfer-Encoding: binary" + lineEnd);
+                        os.writeBytes(lineEnd);
+
+                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                            os.write(Base64.getDecoder().decode(value));
+                        }
+
+                        os.writeBytes(lineEnd);
+                    }
+                }
+            }
+
+            os.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);
+            os.flush();
+        }
+    }
+
+    /**
+     * Opens a communications link to the resource referenced by this
+     * URL, if such a connection has not already been established.
+     * <p>
+     * If the {@code connect} method is called when the connection
+     * has already been opened (indicated by the {@code connected}
+     * field having the value {@code true}), the call is ignored.
+     * <p>
+     * URLConnection objects go through two phases: first they are
+     * created, then they are connected.  After being created, and
+     * before being connected, various options can be specified
+     * (e.g., doInput and UseCaches).  After connecting, it is an
+     * error to try to set them.  Operations that depend on being
+     * connected, like getContentLength, will implicitly perform the
+     * connection, if necessary.
+     *
+     * @throws SocketTimeoutException if the timeout expires before
+     *               the connection can be established
+     * @exception  IOException  if an I/O error occurs while opening the
+     *               connection.
+     */
+    public void connect() throws IOException {
+        connection.connect();
+    }
+
+    /**
+     * Gets the status code from an HTTP response message.
+     * For example, in the case of the following status lines:
+     * <PRE>
+     * HTTP/1.0 200 OK
+     * HTTP/1.0 401 Unauthorized
+     * </PRE>
+     * It will return 200 and 401 respectively.
+     * Returns -1 if no code can be discerned
+     * from the response (i.e., the response is not valid HTTP).
+     * @throws IOException if an error occurred connecting to the server.
+     * @return the HTTP Status-Code, or -1
+     */
+    public int getResponseCode() throws IOException {
+        return connection.getResponseCode();
+    }
+
+    /**
+     * Returns the value of this {@code URLConnection}'s {@code URL}
+     * field.
+     *
+     * @return  the value of this {@code URLConnection}'s {@code URL}
+     *          field.
+     */
+    public URL getURL() {
+        return connection.getURL();
+    }
+
+    /**
+     * Returns the error stream if the connection failed
+     * but the server sent useful data nonetheless. The
+     * typical example is when an HTTP server responds
+     * with a 404, which will cause a FileNotFoundException
+     * to be thrown in connect, but the server sent an HTML
+     * help page with suggestions as to what to do.
+     *
+     * <p>This method will not cause a connection to be initiated.  If
+     * the connection was not connected, or if the server did not have
+     * an error while connecting or if the server had an error but
+     * no error data was sent, this method will return null. This is
+     * the default.
+     *
+     * @return an error stream if any, null if there have been no
+     * errors, the connection is not connected or the server sent no
+     * useful data.
+     */
+    @Override
+    public InputStream getErrorStream() {
+        return connection.getErrorStream();
+    }
+
+    /**
+     * Returns the value of the named header field.
+     * <p>
+     * If called on a connection that sets the same header multiple times
+     * with possibly different values, only the last value is returned.
+     *
+     *
+     * @param   name   the name of a header field.
+     * @return  the value of the named header field, or {@code null}
+     *          if there is no such field in the header.
+     */
+    @Override
+    public String getHeaderField(String name) {
+        return connection.getHeaderField(name);
+    }
+
+    /**
+     * Returns an input stream that reads from this open connection.
+     *
+     * A SocketTimeoutException can be thrown when reading from the
+     * returned input stream if the read timeout expires before data
+     * is available for read.
+     *
+     * @return     an input stream that reads from this open connection.
+     * @exception  IOException              if an I/O error occurs while
+     *               creating the input stream.
+     * @exception UnknownServiceException  if the protocol does not support
+     *               input.
+     * @see #setReadTimeout(int)
+     */
+    @Override
+    public InputStream getInputStream() throws IOException {
+        return connection.getInputStream();
+    }
+
+    /**
+     * Returns an unmodifiable Map of the header fields.
+     * The Map keys are Strings that represent the
+     * response-header field names. Each Map value is an
+     * unmodifiable List of Strings that represents
+     * the corresponding field values.
+     *
+     * @return a Map of header fields
+     */
+    public Map<String, List<String>> getHeaderFields() {
+        return connection.getHeaderFields();
+    }
+
+    /**
+     * Sets the default request properties on the newly created connection.
+     * This is called as early as possible to allow overrides by user-provided values.
+     */
+    private void setDefaultRequestProperties() {
+        String acceptLanguage = buildDefaultAcceptLanguageProperty();
+        if (!TextUtils.isEmpty(acceptLanguage)) {
+            connection.setRequestProperty("Accept-Language", acceptLanguage);
+        }
+    }
+
+    /**
+     * Builds and returns a locale string describing the device's current locale preferences.
+     */
+    private String buildDefaultAcceptLanguageProperty() {
+        Locale locale;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            locale = LocaleList.getDefault().get(0);
+        } else {
+            locale = Locale.getDefault();
+        }
+        String result = "";
+        String lang = locale.getLanguage();
+        String country = locale.getCountry();
+        if (!TextUtils.isEmpty(lang)) {
+            if (!TextUtils.isEmpty(country)) {
+                result = String.format("%s-%s,%s;q=0.5", lang, country, lang);
+            } else {
+                result = String.format("%s;q=0.5", lang);
+            }
+        }
+        return result;
+    }
+
+    public void setSSLSocketFactory(Bridge bridge) {
+        // Attach SSL Certificates if Enterprise Plugin is available
+        try {
+            Class<?> sslPinningImpl = Class.forName("io.ionic.sslpinning.SSLPinning");
+            Method method = sslPinningImpl.getDeclaredMethod("getSSLSocketFactory", Bridge.class);
+            SSLSocketFactory sslSocketFactory = (SSLSocketFactory) method.invoke(sslPinningImpl.newInstance(), bridge);
+            if (sslSocketFactory != null) {
+                ((HttpsURLConnection) this.connection).setSSLSocketFactory(sslSocketFactory);
+            }
+        } catch (Exception ignored) {}
+    }
+}

+ 443 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/plugin/util/HttpRequestHandler.java

@@ -0,0 +1,443 @@
+package com.getcapacitor.plugin.util;
+
+import android.text.TextUtils;
+import android.util.Base64;
+import com.getcapacitor.Bridge;
+import com.getcapacitor.JSArray;
+import com.getcapacitor.JSObject;
+import com.getcapacitor.JSValue;
+import com.getcapacitor.PluginCall;
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Method;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class HttpRequestHandler {
+
+    /**
+     * An enum specifying conventional HTTP Response Types
+     * See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType
+     */
+    public enum ResponseType {
+        ARRAY_BUFFER("arraybuffer"),
+        BLOB("blob"),
+        DOCUMENT("document"),
+        JSON("json"),
+        TEXT("text");
+
+        private final String name;
+
+        ResponseType(String name) {
+            this.name = name;
+        }
+
+        static final ResponseType DEFAULT = TEXT;
+
+        public static ResponseType parse(String value) {
+            for (ResponseType responseType : values()) {
+                if (responseType.name.equalsIgnoreCase(value)) {
+                    return responseType;
+                }
+            }
+            return DEFAULT;
+        }
+    }
+
+    /**
+     * Internal builder class for building a CapacitorHttpUrlConnection
+     */
+    public static class HttpURLConnectionBuilder {
+
+        public Integer connectTimeout;
+        public Integer readTimeout;
+        public Boolean disableRedirects;
+        public JSObject headers;
+        public String method;
+        public URL url;
+
+        public CapacitorHttpUrlConnection connection;
+
+        public HttpURLConnectionBuilder setConnectTimeout(Integer connectTimeout) {
+            this.connectTimeout = connectTimeout;
+            return this;
+        }
+
+        public HttpURLConnectionBuilder setReadTimeout(Integer readTimeout) {
+            this.readTimeout = readTimeout;
+            return this;
+        }
+
+        public HttpURLConnectionBuilder setDisableRedirects(Boolean disableRedirects) {
+            this.disableRedirects = disableRedirects;
+            return this;
+        }
+
+        public HttpURLConnectionBuilder setHeaders(JSObject headers) {
+            this.headers = headers;
+            return this;
+        }
+
+        public HttpURLConnectionBuilder setMethod(String method) {
+            this.method = method;
+            return this;
+        }
+
+        public HttpURLConnectionBuilder setUrl(URL url) {
+            this.url = url;
+            return this;
+        }
+
+        public HttpURLConnectionBuilder openConnection() throws IOException {
+            connection = new CapacitorHttpUrlConnection((HttpURLConnection) url.openConnection());
+
+            connection.setAllowUserInteraction(false);
+            connection.setRequestMethod(method);
+
+            if (connectTimeout != null) connection.setConnectTimeout(connectTimeout);
+            if (readTimeout != null) connection.setReadTimeout(readTimeout);
+            if (disableRedirects != null) connection.setDisableRedirects(disableRedirects);
+
+            connection.setRequestHeaders(headers);
+            return this;
+        }
+
+        public HttpURLConnectionBuilder setUrlParams(JSObject params) throws MalformedURLException, URISyntaxException, JSONException {
+            return this.setUrlParams(params, true);
+        }
+
+        public HttpURLConnectionBuilder setUrlParams(JSObject params, boolean shouldEncode)
+            throws URISyntaxException, MalformedURLException {
+            String initialQuery = url.getQuery();
+            String initialQueryBuilderStr = initialQuery == null ? "" : initialQuery;
+
+            Iterator<String> keys = params.keys();
+
+            if (!keys.hasNext()) {
+                return this;
+            }
+
+            StringBuilder urlQueryBuilder = new StringBuilder(initialQueryBuilderStr);
+
+            // Build the new query string
+            while (keys.hasNext()) {
+                String key = keys.next();
+
+                // Attempt as JSONArray and fallback to string if it fails
+                try {
+                    StringBuilder value = new StringBuilder();
+                    JSONArray arr = params.getJSONArray(key);
+                    for (int x = 0; x < arr.length(); x++) {
+                        value.append(key).append("=").append(arr.getString(x));
+                        if (x != arr.length() - 1) {
+                            value.append("&");
+                        }
+                    }
+                    if (urlQueryBuilder.length() > 0) {
+                        urlQueryBuilder.append("&");
+                    }
+                    urlQueryBuilder.append(value);
+                } catch (JSONException e) {
+                    if (urlQueryBuilder.length() > 0) {
+                        urlQueryBuilder.append("&");
+                    }
+                    urlQueryBuilder.append(key).append("=").append(params.getString(key));
+                }
+            }
+
+            String urlQuery = urlQueryBuilder.toString();
+
+            URI uri = url.toURI();
+            if (shouldEncode) {
+                URI encodedUri = new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), urlQuery, uri.getFragment());
+                this.url = encodedUri.toURL();
+            } else {
+                String unEncodedUrlString =
+                    uri.getScheme() +
+                    "://" +
+                    uri.getAuthority() +
+                    uri.getPath() +
+                    ((!urlQuery.equals("")) ? "?" + urlQuery : "") +
+                    ((uri.getFragment() != null) ? uri.getFragment() : "");
+                this.url = new URL(unEncodedUrlString);
+            }
+
+            return this;
+        }
+
+        public CapacitorHttpUrlConnection build() {
+            return connection;
+        }
+    }
+
+    /**
+     * Builds an HTTP Response given CapacitorHttpUrlConnection and ResponseType objects.
+     *   Defaults to ResponseType.DEFAULT
+     * @param connection The CapacitorHttpUrlConnection to respond with
+     * @throws IOException Thrown if the InputStream is unable to be parsed correctly
+     * @throws JSONException Thrown if the JSON is unable to be parsed
+     */
+    public static JSObject buildResponse(CapacitorHttpUrlConnection connection) throws IOException, JSONException {
+        return buildResponse(connection, ResponseType.DEFAULT);
+    }
+
+    /**
+     * Builds an HTTP Response given CapacitorHttpUrlConnection and ResponseType objects
+     * @param connection The CapacitorHttpUrlConnection to respond with
+     * @param responseType The requested ResponseType
+     * @return A JSObject that contains the HTTPResponse to return to the browser
+     * @throws IOException Thrown if the InputStream is unable to be parsed correctly
+     * @throws JSONException Thrown if the JSON is unable to be parsed
+     */
+    public static JSObject buildResponse(CapacitorHttpUrlConnection connection, ResponseType responseType)
+        throws IOException, JSONException {
+        int statusCode = connection.getResponseCode();
+
+        JSObject output = new JSObject();
+        output.put("status", statusCode);
+        output.put("headers", buildResponseHeaders(connection));
+        output.put("url", connection.getURL());
+        output.put("data", readData(connection, responseType));
+
+        InputStream errorStream = connection.getErrorStream();
+        if (errorStream != null) {
+            output.put("error", true);
+        }
+
+        return output;
+    }
+
+    /**
+     * Read the existing ICapacitorHttpUrlConnection data
+     * @param connection The ICapacitorHttpUrlConnection object to read in
+     * @param responseType The type of HTTP response to return to the API
+     * @return The parsed data from the connection
+     * @throws IOException Thrown if the InputStreams cannot be properly parsed
+     * @throws JSONException Thrown if the JSON is malformed when parsing as JSON
+     */
+    public static Object readData(ICapacitorHttpUrlConnection connection, ResponseType responseType) throws IOException, JSONException {
+        InputStream errorStream = connection.getErrorStream();
+        String contentType = connection.getHeaderField("Content-Type");
+
+        if (errorStream != null) {
+            if (isOneOf(contentType, MimeType.APPLICATION_JSON, MimeType.APPLICATION_VND_API_JSON)) {
+                return parseJSON(readStreamAsString(errorStream));
+            } else {
+                return readStreamAsString(errorStream);
+            }
+        } else if (contentType != null && contentType.contains(MimeType.APPLICATION_JSON.getValue())) {
+            // backward compatibility
+            return parseJSON(readStreamAsString(connection.getInputStream()));
+        } else {
+            InputStream stream = connection.getInputStream();
+            switch (responseType) {
+                case ARRAY_BUFFER:
+                case BLOB:
+                    return readStreamAsBase64(stream);
+                case JSON:
+                    return parseJSON(readStreamAsString(stream));
+                case DOCUMENT:
+                case TEXT:
+                default:
+                    return readStreamAsString(stream);
+            }
+        }
+    }
+
+    /**
+     * Helper function for determining if the Content-Type is a typeof an existing Mime-Type
+     * @param contentType The Content-Type string to check for
+     * @param mimeTypes The Mime-Type values to check against
+     * @return
+     */
+    public static boolean isOneOf(String contentType, MimeType... mimeTypes) {
+        if (contentType != null) {
+            for (MimeType mimeType : mimeTypes) {
+                if (contentType.contains(mimeType.getValue())) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Build the JSObject response headers based on the connection header map
+     * @param connection The CapacitorHttpUrlConnection connection
+     * @return A JSObject of the header values from the CapacitorHttpUrlConnection
+     */
+    public static JSObject buildResponseHeaders(CapacitorHttpUrlConnection connection) {
+        JSObject output = new JSObject();
+
+        for (Map.Entry<String, List<String>> entry : connection.getHeaderFields().entrySet()) {
+            String valuesString = TextUtils.join(", ", entry.getValue());
+            output.put(entry.getKey(), valuesString);
+        }
+
+        return output;
+    }
+
+    /**
+     * Returns a JSObject or a JSArray based on a string-ified input
+     * @param input String-ified JSON that needs parsing
+     * @return A JSObject or JSArray
+     * @throws JSONException thrown if the JSON is malformed
+     */
+    public static Object parseJSON(String input) throws JSONException {
+        JSONObject json = new JSONObject();
+        try {
+            if ("null".equals(input.trim())) {
+                return JSONObject.NULL;
+            } else if ("true".equals(input.trim())) {
+                return true;
+            } else if ("false".equals(input.trim())) {
+                return false;
+            } else if (input.trim().length() <= 0) {
+                return "";
+            } else if (input.trim().matches("^\".*\"$")) {
+                // a string enclosed in " " is a json value, return the string without the quotes
+                return input.trim().substring(1, input.trim().length() - 1);
+            } else if (input.trim().matches("^-?\\d+$")) {
+                return Integer.parseInt(input.trim());
+            } else if (input.trim().matches("^-?\\d+(\\.\\d+)?$")) {
+                return Double.parseDouble(input.trim());
+            } else {
+                try {
+                    return new JSObject(input);
+                } catch (JSONException e) {
+                    return new JSArray(input);
+                }
+            }
+        } catch (JSONException e) {
+            return input;
+        }
+    }
+
+    /**
+     * Returns a string based on a base64 InputStream
+     * @param in The base64 InputStream to convert to a String
+     * @return String value of InputStream
+     * @throws IOException thrown if the InputStream is unable to be read as base64
+     */
+    public static String readStreamAsBase64(InputStream in) throws IOException {
+        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+            byte[] buffer = new byte[1024];
+            int readBytes;
+            while ((readBytes = in.read(buffer)) != -1) {
+                out.write(buffer, 0, readBytes);
+            }
+            byte[] result = out.toByteArray();
+            return Base64.encodeToString(result, 0, result.length, Base64.DEFAULT);
+        }
+    }
+
+    /**
+     * Returns a string based on an InputStream
+     * @param in The InputStream to convert to a String
+     * @return String value of InputStream
+     * @throws IOException thrown if the InputStream is unable to be read
+     */
+    public static String readStreamAsString(InputStream in) throws IOException {
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
+            StringBuilder builder = new StringBuilder();
+            String line = reader.readLine();
+            while (line != null) {
+                builder.append(line);
+                line = reader.readLine();
+                if (line != null) {
+                    builder.append(System.getProperty("line.separator"));
+                }
+            }
+            return builder.toString();
+        }
+    }
+
+    /**
+     * Makes an Http Request based on the PluginCall parameters
+     * @param call The Capacitor PluginCall that contains the options need for an Http request
+     * @param httpMethod The HTTP method that overrides the PluginCall HTTP method
+     * @throws IOException throws an IO request when a connection can't be made
+     * @throws URISyntaxException thrown when the URI is malformed
+     * @throws JSONException thrown when the incoming JSON is malformed
+     */
+    public static JSObject request(PluginCall call, String httpMethod, Bridge bridge)
+        throws IOException, URISyntaxException, JSONException {
+        String urlString = call.getString("url", "");
+        JSObject headers = call.getObject("headers", new JSObject());
+        JSObject params = call.getObject("params", new JSObject());
+        Integer connectTimeout = call.getInt("connectTimeout");
+        Integer readTimeout = call.getInt("readTimeout");
+        Boolean disableRedirects = call.getBoolean("disableRedirects");
+        Boolean shouldEncode = call.getBoolean("shouldEncodeUrlParams", true);
+        ResponseType responseType = ResponseType.parse(call.getString("responseType"));
+        String dataType = call.getString("dataType");
+
+        String method = httpMethod != null ? httpMethod.toUpperCase(Locale.ROOT) : call.getString("method", "GET").toUpperCase(Locale.ROOT);
+
+        boolean isHttpMutate = method.equals("DELETE") || method.equals("PATCH") || method.equals("POST") || method.equals("PUT");
+
+        URL url = new URL(urlString);
+        HttpURLConnectionBuilder connectionBuilder = new HttpURLConnectionBuilder()
+            .setUrl(url)
+            .setMethod(method)
+            .setHeaders(headers)
+            .setUrlParams(params, shouldEncode)
+            .setConnectTimeout(connectTimeout)
+            .setReadTimeout(readTimeout)
+            .setDisableRedirects(disableRedirects)
+            .openConnection();
+
+        CapacitorHttpUrlConnection connection = connectionBuilder.build();
+
+        if (null != bridge && !isDomainExcludedFromSSL(bridge, url)) {
+            connection.setSSLSocketFactory(bridge);
+        }
+
+        // Set HTTP body on a non GET or HEAD request
+        if (isHttpMutate) {
+            JSValue data = new JSValue(call, "data");
+            if (data.getValue() != null) {
+                connection.setDoOutput(true);
+                connection.setRequestBody(call, data, dataType);
+            }
+        }
+
+        call.getData().put("activeCapacitorHttpUrlConnection", connection);
+        connection.connect();
+
+        JSObject response = buildResponse(connection, responseType);
+
+        connection.disconnect();
+        call.getData().remove("activeCapacitorHttpUrlConnection");
+
+        return response;
+    }
+
+    private static Boolean isDomainExcludedFromSSL(Bridge bridge, URL url) {
+        try {
+            Class<?> sslPinningImpl = Class.forName("io.ionic.sslpinning.SSLPinning");
+            Method method = sslPinningImpl.getDeclaredMethod("isDomainExcluded", Bridge.class, URL.class);
+            return (Boolean) method.invoke(sslPinningImpl.newInstance(), bridge, url);
+        } catch (Exception ignored) {
+            return false;
+        }
+    }
+
+    @FunctionalInterface
+    public interface ProgressEmitter {
+        void emit(Integer bytes, Integer contentLength);
+    }
+}

+ 15 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/plugin/util/ICapacitorHttpUrlConnection.java

@@ -0,0 +1,15 @@
+package com.getcapacitor.plugin.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * This interface was extracted from {@link CapacitorHttpUrlConnection} to enable mocking that class.
+ */
+public interface ICapacitorHttpUrlConnection {
+    InputStream getErrorStream();
+
+    String getHeaderField(String name);
+
+    InputStream getInputStream() throws IOException;
+}

+ 17 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/plugin/util/MimeType.java

@@ -0,0 +1,17 @@
+package com.getcapacitor.plugin.util;
+
+enum MimeType {
+    APPLICATION_JSON("application/json"),
+    APPLICATION_VND_API_JSON("application/vnd.api+json"), // https://jsonapi.org
+    TEXT_HTML("text/html");
+
+    private final String value;
+
+    MimeType(String value) {
+        this.value = value;
+    }
+
+    String getValue() {
+        return value;
+    }
+}

+ 123 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/util/HostMask.java

@@ -0,0 +1,123 @@
+package com.getcapacitor.util;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public interface HostMask {
+    boolean matches(String host);
+
+    class Parser {
+
+        private static HostMask NOTHING = new Nothing();
+
+        public static HostMask parse(String[] masks) {
+            return masks == null ? NOTHING : HostMask.Any.parse(masks);
+        }
+
+        public static HostMask parse(String mask) {
+            return mask == null ? NOTHING : HostMask.Simple.parse(mask);
+        }
+    }
+
+    class Simple implements HostMask {
+
+        private final List<String> maskParts;
+
+        private Simple(List<String> maskParts) {
+            if (maskParts == null) {
+                throw new IllegalArgumentException("Mask parts can not be null");
+            }
+            this.maskParts = maskParts;
+        }
+
+        static Simple parse(String mask) {
+            List<String> parts = Util.splitAndReverse(mask);
+            return new Simple(parts);
+        }
+
+        @Override
+        public boolean matches(String host) {
+            if (host == null) {
+                return false;
+            }
+            List<String> hostParts = Util.splitAndReverse(host);
+            int hostSize = hostParts.size();
+            int maskSize = maskParts.size();
+            if (maskSize > 1 && hostSize != maskSize) {
+                return false;
+            }
+
+            int minSize = Math.min(hostSize, maskSize);
+
+            for (int i = 0; i < minSize; i++) {
+                String maskPart = maskParts.get(i);
+                String hostPart = hostParts.get(i);
+                if (!Util.matches(maskPart, hostPart)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
+    class Any implements HostMask {
+
+        private final List<? extends HostMask> masks;
+
+        Any(List<? extends HostMask> masks) {
+            this.masks = masks;
+        }
+
+        @Override
+        public boolean matches(String host) {
+            for (HostMask mask : masks) {
+                if (mask.matches(host)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        static Any parse(String... rawMasks) {
+            List<HostMask.Simple> masks = new ArrayList<>();
+            for (String raw : rawMasks) {
+                masks.add(HostMask.Simple.parse(raw));
+            }
+            return new Any(masks);
+        }
+    }
+
+    class Nothing implements HostMask {
+
+        @Override
+        public boolean matches(String host) {
+            return false;
+        }
+    }
+
+    class Util {
+
+        static boolean matches(String mask, String string) {
+            if (mask == null) {
+                return false;
+            } else if ("*".equals(mask)) {
+                return true;
+            } else if (string == null) {
+                return false;
+            } else {
+                return mask.toUpperCase().equals(string.toUpperCase());
+            }
+        }
+
+        static List<String> splitAndReverse(String string) {
+            if (string == null) {
+                throw new IllegalArgumentException("Can not split null argument");
+            }
+            List<String> parts = Arrays.asList(string.split("\\."));
+            Collections.reverse(parts);
+            return parts;
+        }
+    }
+}

+ 27 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/util/InternalUtils.java

@@ -0,0 +1,27 @@
+package com.getcapacitor.util;
+
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+
+public class InternalUtils {
+
+    public static PackageInfo getPackageInfo(PackageManager pm, String packageName) throws PackageManager.NameNotFoundException {
+        return InternalUtils.getPackageInfo(pm, packageName, 0);
+    }
+
+    public static PackageInfo getPackageInfo(PackageManager pm, String packageName, long flags)
+        throws PackageManager.NameNotFoundException {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            return pm.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags));
+        } else {
+            return getPackageInfoLegacy(pm, packageName, (int) flags);
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    private static PackageInfo getPackageInfoLegacy(PackageManager pm, String packageName, long flags)
+        throws PackageManager.NameNotFoundException {
+        return pm.getPackageInfo(packageName, (int) flags);
+    }
+}

+ 166 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/util/JSONUtils.java

@@ -0,0 +1,166 @@
+package com.getcapacitor.util;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Helper methods for parsing JSON objects.
+ */
+public class JSONUtils {
+
+    /**
+     * Get a string value from the given JSON object.
+     *
+     * @param jsonObject A JSON object to search
+     * @param key A key to fetch from the JSON object
+     * @param defaultValue A default value to return if the key cannot be found
+     * @return The value at the given key in the JSON object, or the default value
+     */
+    public static String getString(JSONObject jsonObject, String key, String defaultValue) {
+        String k = getDeepestKey(key);
+        try {
+            JSONObject o = getDeepestObject(jsonObject, key);
+
+            String value = o.getString(k);
+            if (value == null) {
+                return defaultValue;
+            }
+            return value;
+        } catch (JSONException ignore) {
+            // value was not found
+        }
+
+        return defaultValue;
+    }
+
+    /**
+     * Get a boolean value from the given JSON object.
+     *
+     * @param jsonObject A JSON object to search
+     * @param key A key to fetch from the JSON object
+     * @param defaultValue A default value to return if the key cannot be found
+     * @return The value at the given key in the JSON object, or the default value
+     */
+    public static boolean getBoolean(JSONObject jsonObject, String key, boolean defaultValue) {
+        String k = getDeepestKey(key);
+        try {
+            JSONObject o = getDeepestObject(jsonObject, key);
+
+            return o.getBoolean(k);
+        } catch (JSONException ignore) {
+            // value was not found
+        }
+
+        return defaultValue;
+    }
+
+    /**
+     * Get an int value from the given JSON object.
+     *
+     * @param jsonObject A JSON object to search
+     * @param key A key to fetch from the JSON object
+     * @param defaultValue A default value to return if the key cannot be found
+     * @return The value at the given key in the JSON object, or the default value
+     */
+    public static int getInt(JSONObject jsonObject, String key, int defaultValue) {
+        String k = getDeepestKey(key);
+        try {
+            JSONObject o = getDeepestObject(jsonObject, key);
+            return o.getInt(k);
+        } catch (JSONException ignore) {
+            // value was not found
+        }
+
+        return defaultValue;
+    }
+
+    /**
+     * Get a JSON object value from the given JSON object.
+     *
+     * @param jsonObject A JSON object to search
+     * @param key A key to fetch from the JSON object
+     * @return The value from the config, if exists. Null if not
+     */
+    public static JSONObject getObject(JSONObject jsonObject, String key) {
+        String k = getDeepestKey(key);
+        try {
+            JSONObject o = getDeepestObject(jsonObject, key);
+
+            return o.getJSONObject(k);
+        } catch (JSONException ignore) {
+            // value was not found
+        }
+
+        return null;
+    }
+
+    /**
+     * Get a string array value from the given JSON object.
+     *
+     * @param jsonObject A JSON object to search
+     * @param key A key to fetch from the JSON object
+     * @param defaultValue A default value to return if the key cannot be found
+     * @return The value at the given key in the JSON object, or the default value
+     */
+    public static String[] getArray(JSONObject jsonObject, String key, String[] defaultValue) {
+        String k = getDeepestKey(key);
+        try {
+            JSONObject o = getDeepestObject(jsonObject, key);
+
+            JSONArray a = o.getJSONArray(k);
+            if (a == null) {
+                return defaultValue;
+            }
+
+            int l = a.length();
+            String[] value = new String[l];
+
+            for (int i = 0; i < l; i++) {
+                value[i] = (String) a.get(i);
+            }
+
+            return value;
+        } catch (JSONException ignore) {
+            // value was not found
+        }
+
+        return defaultValue;
+    }
+
+    /**
+     * Given a JSON key path, gets the deepest key.
+     *
+     * @param key The key path
+     * @return The deepest key
+     */
+    private static String getDeepestKey(String key) {
+        String[] parts = key.split("\\.");
+        if (parts.length > 0) {
+            return parts[parts.length - 1];
+        }
+
+        return null;
+    }
+
+    /**
+     * Given a JSON object and key path, gets the deepest object in the path.
+     *
+     * @param jsonObject A JSON object
+     * @param key The key path to follow
+     * @return The deepest object along the key path
+     * @throws JSONException Thrown if any JSON errors
+     */
+    private static JSONObject getDeepestObject(JSONObject jsonObject, String key) throws JSONException {
+        String[] parts = key.split("\\.");
+        JSONObject o = jsonObject;
+
+        // Search until the second to last part of the key
+        for (int i = 0; i < parts.length - 1; i++) {
+            String k = parts[i];
+            o = o.getJSONObject(k);
+        }
+
+        return o;
+    }
+}

+ 114 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/util/PermissionHelper.java

@@ -0,0 +1,114 @@
+package com.getcapacitor.util;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import androidx.core.app.ActivityCompat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A helper class for checking permissions.
+ *
+ * @since 3.0.0
+ */
+public class PermissionHelper {
+
+    /**
+     * Checks if a list of given permissions are all granted by the user
+     *
+     * @since 3.0.0
+     * @param permissions Permissions to check.
+     * @return True if all permissions are granted, false if at least one is not.
+     */
+    public static boolean hasPermissions(Context context, String[] permissions) {
+        for (String perm : permissions) {
+            if (ActivityCompat.checkSelfPermission(context, perm) != PackageManager.PERMISSION_GRANTED) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Check whether the given permission has been defined in the AndroidManifest.xml
+     *
+     * @since 3.0.0
+     * @param permission A permission to check.
+     * @return True if the permission has been defined in the Manifest, false if not.
+     */
+    public static boolean hasDefinedPermission(Context context, String permission) {
+        boolean hasPermission = false;
+        String[] requestedPermissions = PermissionHelper.getManifestPermissions(context);
+        if (requestedPermissions != null && requestedPermissions.length > 0) {
+            List<String> requestedPermissionsList = Arrays.asList(requestedPermissions);
+            ArrayList<String> requestedPermissionsArrayList = new ArrayList<>(requestedPermissionsList);
+            if (requestedPermissionsArrayList.contains(permission)) {
+                hasPermission = true;
+            }
+        }
+        return hasPermission;
+    }
+
+    /**
+     * Check whether all of the given permissions have been defined in the AndroidManifest.xml
+     * @param context the app context
+     * @param permissions a list of permissions
+     * @return true only if all permissions are defined in the AndroidManifest.xml
+     */
+    public static boolean hasDefinedPermissions(Context context, String[] permissions) {
+        for (String permission : permissions) {
+            if (!PermissionHelper.hasDefinedPermission(context, permission)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Get the permissions defined in AndroidManifest.xml
+     *
+     * @since 3.0.0
+     * @return The permissions defined in AndroidManifest.xml
+     */
+    public static String[] getManifestPermissions(Context context) {
+        String[] requestedPermissions = null;
+        try {
+            PackageManager pm = context.getPackageManager();
+            PackageInfo packageInfo = InternalUtils.getPackageInfo(pm, context.getPackageName(), PackageManager.GET_PERMISSIONS);
+
+            if (packageInfo != null) {
+                requestedPermissions = packageInfo.requestedPermissions;
+            }
+        } catch (Exception ex) {}
+        return requestedPermissions;
+    }
+
+    /**
+     * Given a list of permissions, return a new list with the ones not present in AndroidManifest.xml
+     *
+     * @since 3.0.0
+     * @param neededPermissions The permissions needed.
+     * @return The permissions not present in AndroidManifest.xml
+     */
+    public static String[] getUndefinedPermissions(Context context, String[] neededPermissions) {
+        ArrayList<String> undefinedPermissions = new ArrayList<>();
+        String[] requestedPermissions = getManifestPermissions(context);
+        if (requestedPermissions != null && requestedPermissions.length > 0) {
+            List<String> requestedPermissionsList = Arrays.asList(requestedPermissions);
+            ArrayList<String> requestedPermissionsArrayList = new ArrayList<>(requestedPermissionsList);
+            for (String permission : neededPermissions) {
+                if (!requestedPermissionsArrayList.contains(permission)) {
+                    undefinedPermissions.add(permission);
+                }
+            }
+            String[] undefinedPermissionArray = new String[undefinedPermissions.size()];
+            undefinedPermissionArray = undefinedPermissions.toArray(undefinedPermissionArray);
+
+            return undefinedPermissionArray;
+        }
+        return neededPermissions;
+    }
+}

+ 28 - 0
5.7.0/capacitor/src/main/java/com/getcapacitor/util/WebColor.java

@@ -0,0 +1,28 @@
+package com.getcapacitor.util;
+
+import android.graphics.Color;
+
+public class WebColor {
+
+    /**
+     * Parse the color string, and return the corresponding color-int. If the string cannot be parsed, throws an IllegalArgumentException exception.
+     * @param colorString The hexadecimal color string. The format is an RGB or RGBA hex string.
+     * @return The corresponding color as an int.
+     */
+    public static int parseColor(String colorString) {
+        String formattedColor = colorString;
+        if (colorString.charAt(0) != '#') {
+            formattedColor = "#" + formattedColor;
+        }
+
+        if (formattedColor.length() != 7 && formattedColor.length() != 9) {
+            throw new IllegalArgumentException("The encoded color space is invalid or unknown");
+        } else if (formattedColor.length() == 7) {
+            return Color.parseColor(formattedColor);
+        } else {
+            // Convert to Android format #AARRGGBB from #RRGGBBAA
+            formattedColor = "#" + formattedColor.substring(7) + formattedColor.substring(1, 7);
+            return Color.parseColor(formattedColor);
+        }
+    }
+}

+ 15 - 0
5.7.0/capacitor/src/main/res/layout/bridge_layout_main.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context="com.getcapacitor.BridgeActivity"
+    >
+
+    <com.getcapacitor.CapacitorWebView
+        android:id="@+id/webview"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent" />
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>

+ 13 - 0
5.7.0/capacitor/src/main/res/layout/fragment_bridge.xml

@@ -0,0 +1,13 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="#F0FF1414"
+    tools:context="com.getcapacitor.BridgeFragment">
+
+  <com.getcapacitor.CapacitorWebView
+      android:id="@+id/webview"
+      android:layout_width="fill_parent"
+      android:layout_height="fill_parent" />
+
+</FrameLayout>

+ 6 - 0
5.7.0/capacitor/src/main/res/values/attrs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <declare-styleable name="bridge_fragment">
+    <attr name="start_dir" format="string"/>
+  </declare-styleable>
+</resources>

+ 6 - 0
5.7.0/capacitor/src/main/res/values/colors.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+    <color tools:ignore="UnusedResources" name="colorPrimary">#3F51B5</color>
+    <color tools:ignore="UnusedResources" name="colorPrimaryDark">#303F9F</color>
+    <color tools:ignore="UnusedResources" name="colorAccent">#FF4081</color>
+</resources>

+ 2 - 0
5.7.0/capacitor/src/main/res/values/strings.xml

@@ -0,0 +1,2 @@
+<resources>
+</resources>

+ 6 - 0
5.7.0/capacitor/src/main/res/values/styles.xml

@@ -0,0 +1,6 @@
+<resources>
+    <style name="AppTheme.NoActionBar" parent="Theme.AppCompat.NoActionBar">
+        <item name="windowActionBar">false</item>
+        <item name="windowNoTitle">true</item>
+    </style>
+</resources>

+ 34 - 0
5.7.0/package.json

@@ -0,0 +1,34 @@
+{
+  "name": "@capacitor/android",
+  "version": "5.7.0",
+  "description": "Capacitor: Cross-platform apps with JavaScript and the web",
+  "homepage": "https://capacitorjs.com",
+  "author": "Ionic Team <hi@ionic.io> (https://ionic.io)",
+  "license": "MIT",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/ionic-team/capacitor.git"
+  },
+  "bugs": {
+    "url": "https://github.com/ionic-team/capacitor/issues"
+  },
+  "files": [
+    "capacitor/build.gradle",
+    "capacitor/lint-baseline.xml",
+    "capacitor/lint.xml",
+    "capacitor/proguard-rules.pro",
+    "capacitor/src/main/"
+  ],
+  "scripts": {
+    "verify": "./gradlew clean lint build test -b capacitor/build.gradle"
+  },
+  "peerDependencies": {
+    "@capacitor/core": "^5.7.0"
+  },
+  "publishConfig": {
+    "access": "public"
+  },
+  "__npminstall_done": true,
+  "_from": "@capacitor/android@5.7.0",
+  "_resolved": "https://registry.npmmirror.com/@capacitor/android/-/android-5.7.0.tgz"
+}

+ 15 - 0
package.json

@@ -0,0 +1,15 @@
+{
+  "name": "capacitor-geckoview-fmode",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "@capacitor/android": "^5.7.0",
+    "@web-media/capacitor-geckoview": "^5.7.0-experimental.0"
+  }
+}