diff --git a/android/capacitor/build.gradle b/android/capacitor/build.gradle index 4fac5cbc4b..6d858a85e0 100644 --- a/android/capacitor/build.gradle +++ b/android/capacitor/build.gradle @@ -4,6 +4,7 @@ ext { androidxCoordinatorLayoutVersion = project.hasProperty('androidxCoordinatorLayoutVersion') ? rootProject.ext.androidxCoordinatorLayoutVersion : '1.2.0' androidxCoreVersion = project.hasProperty('androidxCoreVersion') ? rootProject.ext.androidxCoreVersion : '1.8.0' androidxFragmentVersion = project.hasProperty('androidxFragmentVersion') ? rootProject.ext.androidxFragmentVersion : '1.4.1' + androidxWebkitVersion = project.hasProperty('androidxWebkitVersion') ? rootProject.ext.androidxWebkitVersion : '1.4.0' junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2' androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.1.3' androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.4.0' @@ -64,6 +65,7 @@ dependencies { 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" diff --git a/android/capacitor/src/main/assets/native-bridge.js b/android/capacitor/src/main/assets/native-bridge.js index b0ab4993a4..30e97b22c8 100644 --- a/android/capacitor/src/main/assets/native-bridge.js +++ b/android/capacitor/src/main/assets/native-bridge.js @@ -384,10 +384,18 @@ const nativeBridge = (function (exports) { } 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); diff --git a/android/capacitor/src/main/java/com/getcapacitor/Bridge.java b/android/capacitor/src/main/java/com/getcapacitor/Bridge.java index 1d02383723..245dd21f9a 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/Bridge.java +++ b/android/capacitor/src/main/java/com/getcapacitor/Bridge.java @@ -40,9 +40,11 @@ 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 org.apache.cordova.ConfigXmlParser; import org.apache.cordova.CordovaPreferences; import org.apache.cordova.CordovaWebView; @@ -97,6 +99,7 @@ public class Bridge { private String appUrl; private String appUrlConfig; private HostMask appAllowNavigationMask; + private Set allowedOriginRules = new HashSet(); // A reference to the main WebView for the app private final WebView webView; public final MockCordovaInterfaceImpl cordovaInterface; @@ -186,6 +189,7 @@ private Bridge( // 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 @@ -198,6 +202,25 @@ private Bridge( 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); + } + } + } + } + public App getApp() { return app; } @@ -207,14 +230,13 @@ private void loadWebView() { String[] appAllowNavigationConfig = this.config.getAllowNavigation(); ArrayList authorities = new ArrayList<>(); + if (appAllowNavigationConfig != null) { authorities.addAll(Arrays.asList(appAllowNavigationConfig)); } this.appAllowNavigationMask = HostMask.Parser.parse(appAllowNavigationConfig); - String authority = this.getHost(); authorities.add(authority); - String scheme = this.getScheme(); localUrl = scheme + "://" + authority; @@ -241,7 +263,6 @@ private void loadWebView() { if (appUrlPath != null && !appUrlPath.trim().isEmpty()) { appUrl += appUrlPath; } - final boolean html5mode = this.config.isHTML5Mode(); // Start the local web server @@ -1267,6 +1288,10 @@ public HostMask getAppAllowNavigationMask() { return appAllowNavigationMask; } + public Set getAllowedOriginRules() { + return allowedOriginRules; + } + public BridgeWebViewClient getWebViewClient() { return this.webViewClient; } diff --git a/android/capacitor/src/main/java/com/getcapacitor/MessageHandler.java b/android/capacitor/src/main/java/com/getcapacitor/MessageHandler.java index bca7f5337e..76b4831bdc 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/MessageHandler.java +++ b/android/capacitor/src/main/java/com/getcapacitor/MessageHandler.java @@ -1,7 +1,12 @@ package com.getcapacitor; +import android.net.Uri; import android.webkit.JavascriptInterface; import android.webkit.WebView; +import androidx.webkit.JavaScriptReplyProxy; +import androidx.webkit.WebMessageCompat; +import androidx.webkit.WebViewCompat; +import androidx.webkit.WebViewFeature; import org.apache.cordova.PluginManager; /** @@ -13,13 +18,31 @@ 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; - webView.addJavascriptInterface(this, "androidBridge"); + 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"); + } + }; + + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { + try { + WebViewCompat.addWebMessageListener(webView, "androidBridge", bridge.getAllowedOriginRules(), capListener); + } catch (Exception ex) { + webView.addJavascriptInterface(this, "androidBridge"); + } + } else { + webView.addJavascriptInterface(this, "androidBridge"); + } } /** @@ -100,9 +123,13 @@ public void sendResponseMessage(PluginCall call, PluginResult successResult, Plu boolean isValidCallbackId = !call.getCallbackId().equals(PluginCall.CALLBACK_ID_DANGLING); if (isValidCallbackId) { - final String runScript = "window.Capacitor.fromNative(" + data.toString() + ")"; - final WebView webView = this.webView; - webView.post(() -> webView.evaluateJavascript(runScript, null)); + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER) && javaScriptReplyProxy != null) { + javaScriptReplyProxy.postMessage(data.toString()); + } else { + final String runScript = "window.Capacitor.fromNative(" + data.toString() + ")"; + final WebView webView = this.webView; + webView.post(() -> webView.evaluateJavascript(runScript, null)); + } } else { bridge.getApp().fireRestoredResult(data); } diff --git a/core/native-bridge.ts b/core/native-bridge.ts index 49505ab583..e13628fe40 100644 --- a/core/native-bridge.ts +++ b/core/native-bridge.ts @@ -457,10 +457,20 @@ const initBridge = (w: any): void => { return null; }; + if (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: any) => { if (cap.isLoggingEnabled && result.pluginId !== 'Console') { cap.logFromNative(result); } diff --git a/core/src/definitions-internal.ts b/core/src/definitions-internal.ts index b6af23d75b..6137d3dec9 100644 --- a/core/src/definitions-internal.ts +++ b/core/src/definitions-internal.ts @@ -188,6 +188,7 @@ export interface WindowCapacitor { WEBVIEW_SERVER_URL?: string; androidBridge?: { postMessage(data: string): void; + onmessage?: (event: { data: string }) => void; }; webkit?: { messageHandlers?: { diff --git a/ios/Capacitor/Capacitor/assets/native-bridge.js b/ios/Capacitor/Capacitor/assets/native-bridge.js index b0ab4993a4..30e97b22c8 100644 --- a/ios/Capacitor/Capacitor/assets/native-bridge.js +++ b/ios/Capacitor/Capacitor/assets/native-bridge.js @@ -384,10 +384,18 @@ const nativeBridge = (function (exports) { } 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);