diff --git a/CHANGELOG.md b/CHANGELOG.md index 584c8225..18daf960 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ - Added `IOSCookieManager` class and `CookieManager.instance().ios.getAllCookies` iOS-specific method - Added `UserScript`, `UserScriptInjectionTime`, `ContentWorld`, `AndroidWebViewFeature`, `AndroidServiceWorkerController`, `AndroidServiceWorkerClient` classes - Added `initialUserScripts` WebView option -- Added `addUserScript`, `addUserScripts`, `removeUserScript`, `removeUserScripts`, `removeAllUserScripts` WebView methods +- Added `addUserScript`, `addUserScripts`, `removeUserScript`, `removeUserScripts`, `removeAllUserScripts`, `callAsyncJavaScript` WebView methods +- Added `contentWorld` argument to `evaluateJavascript` WebView method - Added `isDirectionalLockEnabled`, `mediaType`, `pageZoom`, `limitsNavigationsToAppBoundDomains` iOS-specific webview options - Added `handlesURLScheme` iOS-specific webview method - Updated integration tests @@ -29,13 +30,16 @@ - Fixed missing `clearHistory` webview method implementation on Android - Fixed iOS crash when using CookieManager getCookies for an URL and the host URL is `null` - Fixed "IOS does not support allowUniversalAccessFromFileURLs" [#654](https://github.com/pichillilorenzo/flutter_inappwebview/issues/654) +- Fixed "Failed to load WebView provider: No WebView installed" [#642](https://github.com/pichillilorenzo/flutter_inappwebview/issues/642) +- Fixed "java.net.MalformedURLException: unknown protocol: wss - Error using library sipml5 in flutter_inappwebview" [#614](https://github.com/pichillilorenzo/flutter_inappwebview/issues/614) ### BREAKING CHANGES - Minimum Flutter version required is `1.22.0` and Dart SDK `>=2.12.0-0 <3.0.0` - iOS Xcode version `>= 12` - Removed `debuggingEnabled` WebView option; on Android you should use now the `AndroidInAppWebViewController.setWebContentsDebuggingEnabled(bool debuggingEnabled)` static method; on iOS, debugging is always enabled -- `allowUniversalAccessFromFileURLs` and `allowFileAccessFromFileURLs` WebView options moved from Android-specific options to cross-platform options. +- `allowUniversalAccessFromFileURLs` and `allowFileAccessFromFileURLs` WebView options moved from Android-specific options to cross-platform options +- Added `callAsyncJavaScript` name to the list of javaScriptHandlerForbiddenNames ## 4.0.0+4 diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java index a19bde1f..a07ff4c9 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebView.java @@ -16,6 +16,7 @@ import android.os.Message; import android.print.PrintAttributes; import android.print.PrintDocumentAdapter; import android.print.PrintManager; +import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.view.ActionMode; @@ -42,6 +43,7 @@ import android.widget.HorizontalScrollView; import android.widget.LinearLayout; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.webkit.WebViewCompat; @@ -67,9 +69,11 @@ import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import java.util.regex.Pattern; import io.flutter.plugin.common.MethodChannel; @@ -116,6 +120,8 @@ final public class InAppWebView extends InputAwareWebView { add("page"); }}; + public Map callAsyncJavaScriptResults = new HashMap<>(); + static final String pluginScriptsWrapperJS = "(function(){" + " if (window." + JavaScriptBridgeInterface.name + " == null || window." + JavaScriptBridgeInterface.name + "._pluginScriptsLoaded == null || !window." + JavaScriptBridgeInterface.name + "._pluginScriptsLoaded) {" + " $PLACEHOLDER_VALUE" + @@ -659,11 +665,22 @@ final public class InAppWebView extends InputAwareWebView { " });" + "})();"; - static final String onWindowBlurEventJS = "(function(){" + - " window.addEventListener('blur', function(e) {" + - " window." + JavaScriptBridgeInterface.name + ".callHandler('onWindowBlur');" + - " });" + - "})();"; + static final String onWindowBlurEventJS = "(function(){" + + " window.addEventListener('blur', function(e) {" + + " window." + JavaScriptBridgeInterface.name + ".callHandler('onWindowBlur');" + + " });" + + "})();"; + + static final String callAsyncJavaScriptWrapperJS = "(function(obj) {" + + " (async function($FUNCTION_ARGUMENT_NAMES) {" + + " $FUNCTION_BODY" + + " })($FUNCTION_ARGUMENT_VALUES).then(function(value) {" + + " window." + JavaScriptBridgeInterface.name + ".callHandler('callAsyncJavaScript', {'value': value, 'error': null, 'resultUuid': '$RESULT_UUID'});" + + " }).catch(function(error) {" + + " window." + JavaScriptBridgeInterface.name + ".callHandler('callAsyncJavaScript', {'value': null, 'error': error, 'resultUuid': '$RESULT_UUID'});" + + " });" + + " return null;" + + "})($FUNCTION_ARGUMENTS_OBJ);"; public InAppWebView(Context context) { super(context); @@ -1521,7 +1538,7 @@ final public class InAppWebView extends InputAwareWebView { }); } - public void evaluateJavascript(String source, String contentWorldName, MethodChannel.Result result) { + public void evaluateJavascript(String source, @Nullable String contentWorldName, MethodChannel.Result result) { injectDeferredObject(source, contentWorldName, null, result); } @@ -2093,6 +2110,47 @@ final public class InAppWebView extends InputAwareWebView { return sourceWrapped; } + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public void callAsyncJavaScript(String functionBody, Map arguments, @Nullable String contentWorldName, @NonNull MethodChannel.Result result) { + String resultUuid = UUID.randomUUID().toString(); + callAsyncJavaScriptResults.put(resultUuid, result); + + JSONObject functionArguments = new JSONObject(arguments); + Iterator keys = functionArguments.keys(); + + List functionArgumentNamesList = new ArrayList<>(); + List functionArgumentValuesList = new ArrayList<>(); + while (keys.hasNext()) { + String key = keys.next(); + functionArgumentNamesList.add(key); + functionArgumentValuesList.add("obj." + key); + } + + String functionArgumentNames = TextUtils.join(", ", functionArgumentNamesList); + String functionArgumentValues = TextUtils.join(", ", functionArgumentValuesList); + String functionArgumentsObj = Util.JSONStringify(arguments); + + String sourceToInject = InAppWebView.callAsyncJavaScriptWrapperJS + .replace("$FUNCTION_ARGUMENT_NAMES", functionArgumentNames) + .replace("$FUNCTION_ARGUMENT_VALUES", functionArgumentValues) + .replace("$FUNCTION_ARGUMENTS_OBJ", functionArgumentsObj) + .replace("$FUNCTION_BODY", functionBody) + .replace("$RESULT_UUID", resultUuid); + + if (contentWorldName != null && !contentWorldName.equals("page")) { + if (!userScriptsContentWorlds.contains(contentWorldName)) { + userScriptsContentWorlds.add(contentWorldName); + // Add only the first time all the plugin scripts needed. + String jsPluginScripts = prepareAndWrapPluginUserScripts(); + sourceToInject = jsPluginScripts + "\n" + sourceToInject; + } + sourceToInject = wrapSourceCodeInContentWorld(contentWorldName, sourceToInject); + + } + + evaluateJavascript(sourceToInject, null); + } + @Override public void dispose() { if (windowId != null && InAppWebViewChromeClient.windowWebViewMessages.containsKey(windowId)) { @@ -2106,6 +2164,7 @@ final public class InAppWebView extends InputAwareWebView { removeCallbacks(checkContextMenuShouldBeClosedTask); if (checkScrollStoppedTask != null) removeCallbacks(checkScrollStoppedTask); + callAsyncJavaScriptResults.clear(); super.dispose(); } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewClient.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewClient.java index 54b9340f..065e02b0 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewClient.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewClient.java @@ -29,7 +29,6 @@ import com.pichillilorenzo.flutter_inappwebview.InAppBrowser.InAppBrowserActivit import com.pichillilorenzo.flutter_inappwebview.Util; import java.io.ByteArrayInputStream; -import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -341,10 +340,10 @@ public class InAppWebViewClient extends WebViewClient { @Override public void onReceivedHttpAuthRequest(final WebView view, final HttpAuthHandler handler, final String host, final String realm) { - URL url; + URI uri; try { - url = new URL(view.getUrl()); - } catch (MalformedURLException e) { + uri = new URI(view.getUrl()); + } catch (URISyntaxException e) { e.printStackTrace(); credentialsProposed = null; @@ -354,8 +353,8 @@ public class InAppWebViewClient extends WebViewClient { return; } - final String protocol = url.getProtocol(); - final int port = url.getPort(); + final String protocol = uri.getScheme(); + final int port = uri.getPort(); previousAuthRequestFailureCount++; @@ -422,19 +421,19 @@ public class InAppWebViewClient extends WebViewClient { @Override public void onReceivedSslError(final WebView view, final SslErrorHandler handler, final SslError error) { - URL url; + URI uri; try { - url = new URL(error.getUrl()); - } catch (MalformedURLException e) { + uri = new URI(view.getUrl()); + } catch (URISyntaxException e) { e.printStackTrace(); handler.cancel(); return; } - final String host = url.getHost(); - final String protocol = url.getProtocol(); + final String host = uri.getHost(); + final String protocol = uri.getScheme(); final String realm = null; - final int port = url.getPort(); + final int port = uri.getPort(); Map obj = new HashMap<>(); obj.put("host", host); @@ -507,16 +506,16 @@ public class InAppWebViewClient extends WebViewClient { @Override public void onReceivedClientCertRequest(final WebView view, final ClientCertRequest request) { - URL url; + URI uri; try { - url = new URL(view.getUrl()); - } catch (MalformedURLException e) { + uri = new URI(view.getUrl()); + } catch (URISyntaxException e) { e.printStackTrace(); request.cancel(); return; } - final String protocol = url.getProtocol(); + final String protocol = uri.getScheme(); final String realm = null; Map obj = new HashMap<>(); diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewMethodHandler.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewMethodHandler.java index ae35ff70..68fcbb01 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewMethodHandler.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewMethodHandler.java @@ -438,6 +438,17 @@ public class InAppWebViewMethodHandler implements MethodChannel.MethodCallHandle } result.success(true); break; + case "callAsyncJavaScript": + if (webView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + String functionBody = (String) call.argument("functionBody"); + Map functionArguments = (Map) call.argument("arguments"); + String contentWorldName = (String) call.argument("contentWorld"); + webView.callAsyncJavaScript(functionBody, functionArguments, contentWorldName, result); + } + else { + result.success(null); + } + break; default: result.notImplemented(); } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/JavaScriptBridgeInterface.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/JavaScriptBridgeInterface.java index 3453e152..56d1b481 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/JavaScriptBridgeInterface.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/JavaScriptBridgeInterface.java @@ -11,6 +11,10 @@ import com.pichillilorenzo.flutter_inappwebview.InAppBrowser.InAppBrowserActivit import com.pichillilorenzo.flutter_inappwebview.InAppWebView.FlutterWebView; import com.pichillilorenzo.flutter_inappwebview.InAppWebView.InAppWebView; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + import java.util.HashMap; import java.util.Map; @@ -96,6 +100,21 @@ public class JavaScriptBridgeInterface { if (handlerName.equals("onPrint") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { webView.printCurrentPage(); + } else if (handlerName.equals("callAsyncJavaScript")) { + try { + JSONArray arguments = new JSONArray(args); + JSONObject jsonObject = arguments.getJSONObject(0); + String resultUuid = jsonObject.getString("resultUuid"); + if (webView.callAsyncJavaScriptResults.containsKey(resultUuid)) { + MethodChannel.Result callAsyncJavaScriptResult = webView.callAsyncJavaScriptResults.get(resultUuid); + callAsyncJavaScriptResult.success(jsonObject.toString()); + + webView.callAsyncJavaScriptResults.remove(resultUuid); + } + } catch (JSONException e) { + e.printStackTrace(); + } + return; } channel.invokeMethod("onCallJsHandler", obj, new MethodChannel.Result() { diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/MyCookieManager.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/MyCookieManager.java index 1156d8db..a48ab55f 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/MyCookieManager.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/MyCookieManager.java @@ -1,11 +1,19 @@ package com.pichillilorenzo.flutter_inappwebview; +import android.annotation.TargetApi; +import android.content.Context; +import android.os.AsyncTask; import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; import android.util.Log; import android.webkit.CookieManager; import android.webkit.CookieSyncManager; import android.webkit.ValueCallback; +import androidx.annotation.Nullable; + import java.lang.reflect.Field; import java.lang.reflect.Method; import java.text.SimpleDateFormat; @@ -16,6 +24,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TimeZone; +import java.util.concurrent.Executors; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; @@ -29,10 +38,13 @@ public class MyCookieManager implements MethodChannel.MethodCallHandler { public static MethodChannel channel; public static CookieManager cookieManager; + // As CookieManager was synchronous before API 21 this class emulates the async behavior on <21. + private static final boolean USES_LEGACY_STORE = Build.VERSION.SDK_INT < 21; + public MyCookieManager(BinaryMessenger messenger) { channel = new MethodChannel(messenger, "com.pichillilorenzo/flutter_inappwebview_cookiemanager"); channel.setMethodCallHandler(this); - cookieManager = CookieManager.getInstance(); + cookieManager = getCookieManager(); } @Override @@ -92,6 +104,40 @@ public class MyCookieManager implements MethodChannel.MethodCallHandler { } } + /** + * Instantiating CookieManager will load the Chromium task taking a 100ish ms so we do it lazily + * to make sure it's done on a background thread as needed. + * + * https://github.com/facebook/react-native/blob/1903f6680d9750e244d97c3cd4a9f755a9a47c61/ReactAndroid/src/main/java/com/facebook/react/modules/network/ForwardingCookieHandler.java#L132 + */ + static private @Nullable CookieManager getCookieManager() { + if (cookieManager == null) { + try { + cookieManager = CookieManager.getInstance(); + } catch (IllegalArgumentException ex) { + // https://bugs.chromium.org/p/chromium/issues/detail?id=559720 + return null; + } catch (Exception exception) { + String message = exception.getMessage(); + // We cannot catch MissingWebViewPackageException as it is in a private / system API + // class. This validates the exception's message to ensure we are only handling this + // specific exception. + // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/webkit/WebViewFactory.java#348 + if (message != null + && exception + .getClass() + .getCanonicalName() + .equals("android.webkit.WebViewFactory.MissingWebViewPackageException")) { + return null; + } else { + throw exception; + } + } + } + + return cookieManager; + } + public static void setCookie(String url, String name, String value, @@ -103,6 +149,8 @@ public class MyCookieManager implements MethodChannel.MethodCallHandler { Boolean isHttpOnly, String sameSite, final MethodChannel.Result result) { + cookieManager = getCookieManager(); + if (cookieManager == null) return; String cookieValue = name + "=" + value + "; Domain=" + domain + "; Path=" + path; @@ -146,6 +194,9 @@ public class MyCookieManager implements MethodChannel.MethodCallHandler { final List> cookieListMap = new ArrayList<>(); + cookieManager = getCookieManager(); + if (cookieManager == null) return cookieListMap; + String cookiesString = cookieManager.getCookie(url); if (cookiesString != null) { @@ -173,6 +224,8 @@ public class MyCookieManager implements MethodChannel.MethodCallHandler { } public static void deleteCookie(String url, String name, String domain, String path, final MethodChannel.Result result) { + cookieManager = getCookieManager(); + if (cookieManager == null) return; String cookieValue = name + "=; Path=" + path + "; Domain=" + domain + "; Max-Age=-1;"; @@ -196,6 +249,8 @@ public class MyCookieManager implements MethodChannel.MethodCallHandler { } public static void deleteCookies(String url, String domain, String path, final MethodChannel.Result result) { + cookieManager = getCookieManager(); + if (cookieManager == null) return; CookieSyncManager cookieSyncMngr = null; @@ -228,6 +283,8 @@ public class MyCookieManager implements MethodChannel.MethodCallHandler { } public static void deleteAllCookies(final MethodChannel.Result result) { + cookieManager = getCookieManager(); + if (cookieManager == null) return; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { cookieManager.removeAllCookies(new ValueCallback() { diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/Util.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/Util.java index 0d05da5e..7a3a88b1 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/Util.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/Util.java @@ -2,6 +2,7 @@ package com.pichillilorenzo.flutter_inappwebview; import android.content.res.AssetManager; import android.net.http.SslCertificate; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -9,6 +10,12 @@ import android.os.Looper; import android.os.Parcelable; import android.util.Log; +import androidx.annotation.RequiresApi; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -22,6 +29,7 @@ import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.Enumeration; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -234,4 +242,20 @@ public class Util { return x509Certificate; } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + public static String JSONStringify(Object value) { + if (value == null) { + return "null"; + } + if (value instanceof Map) { + return new JSONObject((Map) value).toString(); + } else if (value instanceof List) { + return new JSONArray((List) value).toString(); + } else if (value instanceof String) { + return JSONObject.quote((String) value); + } else { + return JSONObject.wrap(value).toString(); + } + } } diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index fd3d72d9..e01a7fe8 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"device_info","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/device_info-2.0.0-nullsafety.2/","dependencies":[]},{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.5.2/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":["device_info"]},{"name":"integration_test","path":"/Users/lorenzopichilli/flutter/.pub-cache/git/plugins-16f3281b04b0db12e609352b1c9544901392e428/packages/integration_test/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.27/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.0.1+1/","dependencies":[]},{"name":"url_launcher","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.0-nullsafety.4/","dependencies":[]}],"android":[{"name":"device_info","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/device_info-2.0.0-nullsafety.2/","dependencies":[]},{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.5.2/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":["device_info"]},{"name":"integration_test","path":"/Users/lorenzopichilli/flutter/.pub-cache/git/plugins-16f3281b04b0db12e609352b1c9544901392e428/packages/integration_test/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.27/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.0.1+1/","dependencies":[]},{"name":"url_launcher","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.0-nullsafety.4/","dependencies":[]}],"macos":[{"name":"path_provider_macos","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_macos-0.0.4+8/","dependencies":[]},{"name":"url_launcher_macos","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_macos-0.1.0-nullsafety.2/","dependencies":[]}],"linux":[{"name":"path_provider_linux","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_linux-0.0.1+2/","dependencies":[]},{"name":"url_launcher_linux","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_linux-0.1.0-nullsafety.3/","dependencies":[]}],"windows":[{"name":"path_provider_windows","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_windows-0.0.4+3/","dependencies":[]},{"name":"url_launcher_windows","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_windows-0.1.0-nullsafety.2/","dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"device_info","dependencies":[]},{"name":"flutter_downloader","dependencies":[]},{"name":"flutter_inappwebview","dependencies":["device_info"]},{"name":"integration_test","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_macos","path_provider_linux","path_provider_windows"]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_macos","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_linux","url_launcher_macos","url_launcher_windows"]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2021-02-06 02:03:14.260971","version":"1.26.0-18.0.pre.90"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"device_info","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/device_info-2.0.0-nullsafety.2/","dependencies":[]},{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.5.2/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":["device_info"]},{"name":"integration_test","path":"/Users/lorenzopichilli/flutter/.pub-cache/git/plugins-16f3281b04b0db12e609352b1c9544901392e428/packages/integration_test/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.27/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.0.1+1/","dependencies":[]},{"name":"url_launcher","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.0-nullsafety.4/","dependencies":[]}],"android":[{"name":"device_info","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/device_info-2.0.0-nullsafety.2/","dependencies":[]},{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.5.2/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":["device_info"]},{"name":"integration_test","path":"/Users/lorenzopichilli/flutter/.pub-cache/git/plugins-16f3281b04b0db12e609352b1c9544901392e428/packages/integration_test/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.27/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.0.1+1/","dependencies":[]},{"name":"url_launcher","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-6.0.0-nullsafety.4/","dependencies":[]}],"macos":[{"name":"path_provider_macos","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_macos-0.0.4+8/","dependencies":[]},{"name":"url_launcher_macos","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_macos-0.1.0-nullsafety.2/","dependencies":[]}],"linux":[{"name":"path_provider_linux","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_linux-0.0.1+2/","dependencies":[]},{"name":"url_launcher_linux","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_linux-0.1.0-nullsafety.3/","dependencies":[]}],"windows":[{"name":"path_provider_windows","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_windows-0.0.4+3/","dependencies":[]},{"name":"url_launcher_windows","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_windows-0.1.0-nullsafety.2/","dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"device_info","dependencies":[]},{"name":"flutter_downloader","dependencies":[]},{"name":"flutter_inappwebview","dependencies":["device_info"]},{"name":"integration_test","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_macos","path_provider_linux","path_provider_windows"]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_macos","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_linux","url_launcher_macos","url_launcher_windows"]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2021-02-07 16:00:15.712688","version":"1.26.0-18.0.pre.90"} \ No newline at end of file diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index d6eb46d1..a6a7ee19 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -18,10 +18,22 @@ EDC1147F21735BC200D2247A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; /* End PBXBuildFile section */ +/* Begin PBXCopyFilesBuildPhase section */ + 6174FE1725CEB74E00A5020C /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 26ADC1E5EAF404A509D528C5 /* Pods_Runner_copy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner_copy.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 61FF72FF23634CA10069C557 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; 61FF730123634DD10069C557 /* flutter_downloader.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = flutter_downloader.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -59,7 +71,6 @@ children = ( 61FF730123634DD10069C557 /* flutter_downloader.framework */, 61FF72FF23634CA10069C557 /* libsqlite3.tbd */, - 26ADC1E5EAF404A509D528C5 /* Pods_Runner_copy.framework */, B0FC2CF7A6002799890B3102 /* Pods_Runner.framework */, ); name = Frameworks; @@ -141,6 +152,7 @@ 97C146EC1CF9000F007C117D /* Resources */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 903A9F2558754FA70D0A7EA8 /* [CP] Embed Pods Frameworks */, + 6174FE1725CEB74E00A5020C /* Embed App Extensions */, ); buildRules = ( ); @@ -157,6 +169,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftUpdateCheck = 1240; LastUpgradeCheck = 1110; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index cf19d06e..5daa91b8 100755 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -59,7 +59,7 @@ NSBonjourServices - + _dartobservatory._tcp UIViewControllerBasedStatusBarAppearance diff --git a/ios/Classes/InAppWebView.swift b/ios/Classes/InAppWebView.swift index c69126cc..40255de4 100755 --- a/ios/Classes/InAppWebView.swift +++ b/ios/Classes/InAppWebView.swift @@ -827,6 +827,19 @@ let onWindowBlurEventJS = """ })(); """ +let callAsyncJavaScriptBelowIOS14WrapperJS = """ +(function(obj) { + (async function($FUNCTION_ARGUMENT_NAMES) { + $FUNCTION_BODY + })($FUNCTION_ARGUMENT_VALUES).then(function(value) { + window.webkit.messageHandlers['onCallAsyncJavaScriptResultBelowIOS14Received'].postMessage({'value': value, 'error': null, 'resultUuid': '$RESULT_UUID'}); + }).catch(function(error) { + window.webkit.messageHandlers['onCallAsyncJavaScriptResultBelowIOS14Received'].postMessage({'value': null, 'error': error, 'resultUuid': '$RESULT_UUID'}); + }); + return null; +})($FUNCTION_ARGUMENTS_OBJ); +""" + var SharedLastTouchPointTimestamp: [InAppWebView: Int64] = [:] public class WebViewTransport: NSObject { @@ -879,6 +892,8 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi var userScriptsContentWorlds: [String] = ["page"] + var callAsyncJavaScriptBelowIOS14Results: [String:FlutterResult] = [:] + init(frame: CGRect, configuration: WKWebViewConfiguration, IABController: InAppBrowserWebViewController?, contextMenu: [String: Any]?, channel: FlutterMethodChannel?) { super.init(frame: frame, configuration: configuration) self.channel = channel @@ -1333,6 +1348,9 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi let printJSScript = WKUserScript(source: printJS, injectionTime: .atDocumentStart, forMainFrameOnly: false) configuration.userContentController.addUserScript(printJSScript) + + configuration.userContentController.removeScriptMessageHandler(forName: "onCallAsyncJavaScriptResultBelowIOS14Received") + configuration.userContentController.add(self, name: "onCallAsyncJavaScriptResultBelowIOS14Received") } } @@ -2113,10 +2131,79 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi } } - public func evaluateJavascript(source: String, contentWorldName: String?, result: FlutterResult?) { + public func evaluateJavascript(source: String, contentWorldName: String?, result: @escaping FlutterResult) { injectDeferredObject(source: source, contentWorldName: contentWorldName, withWrapper: nil, result: result) } + @available(iOS 10.3, *) + public func callAsyncJavaScript(functionBody: String, arguments: [String:Any], contentWorldName: String?, result: @escaping FlutterResult) { + var jsToInject = functionBody + if #available(iOS 14.0, *) { + var contentWorld = WKContentWorld.page + if let contentWorldName = contentWorldName { + contentWorld = getContentWorld(name: contentWorldName) + if !userScriptsContentWorlds.contains(contentWorldName) { + userScriptsContentWorlds.append(contentWorldName) + addSharedPluginUserScriptsInContentWorld(contentWorldName: contentWorldName) + // Add only the first time all the plugin user scripts needed. + // In the next page load, it will use the WKUserScripts loaded + jsToInject = getAllPluginUserScriptMergedJS() + "\n" + jsToInject + } + } + callAsyncJavaScript(jsToInject, arguments: arguments, in: nil, in: contentWorld) { (evalResult) in + var body: [String: Any?] = [ + "value": nil, + "error": nil + ] + + switch (evalResult) { + case .success(let value): + body["value"] = value + break + case .failure(let error): + body["error"] = error + break + } + + result(body) + } + } else { + let resultUuid = NSUUID().uuidString + callAsyncJavaScriptBelowIOS14Results[resultUuid] = result + + var functionArgumentNamesList: [String] = [] + var functionArgumentValuesList: [String] = [] + let keys = arguments.keys + keys.forEach { (key) in + functionArgumentNamesList.append(key) + functionArgumentValuesList.append("obj.\(key)") + } + + let functionArgumentNames = functionArgumentNamesList.joined(separator: ", ") + let functionArgumentValues = functionArgumentValuesList.joined(separator: ", ") + + jsToInject = callAsyncJavaScriptBelowIOS14WrapperJS + .replacingOccurrences(of: "$FUNCTION_ARGUMENT_NAMES", with: functionArgumentNames) + .replacingOccurrences(of: "$FUNCTION_ARGUMENT_VALUES", with: functionArgumentValues) + .replacingOccurrences(of: "$FUNCTION_ARGUMENTS_OBJ", with: JSONStringify(value: arguments)) + .replacingOccurrences(of: "$FUNCTION_BODY", with: jsToInject) + .replacingOccurrences(of: "$RESULT_UUID", with: resultUuid) + + evaluateJavaScript(jsToInject) { (value, error) in + if error != nil { + let userInfo = (error! as NSError).userInfo + self.onConsoleMessage(message: + userInfo["WKJavaScriptExceptionMessage"] as? String ?? + userInfo["NSLocalizedDescription"] as? String ?? + "", + messageLevel: 3) + result(nil) + self.callAsyncJavaScriptBelowIOS14Results.removeValue(forKey: resultUuid) + } + } + } + } + public func injectJavascriptFileFromUrl(urlFile: String) { let jsWrapper = "(function(d) { var c = d.createElement('script'); c.src = %@; d.body.appendChild(c); })(document);" injectDeferredObject(source: urlFile, contentWorldName: nil, withWrapper: jsWrapper, result: nil) @@ -3270,6 +3357,16 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { webView = webViewTransport.webView } webView.onFindResultReceived(activeMatchOrdinal: activeMatchOrdinal, numberOfMatches: numberOfMatches, isDoneCounting: isDoneCounting) + } else if message.name == "onCallAsyncJavaScriptResultBelowIOS14Received" { + let body = message.body as! [String: Any?] + let resultUuid = body["resultUuid"] as! String + if let result = callAsyncJavaScriptBelowIOS14Results[resultUuid] { + result([ + "value": body["value"], + "error": body["error"] + ]) + callAsyncJavaScriptBelowIOS14Results.removeValue(forKey: resultUuid) + } } } @@ -3436,6 +3533,7 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { configuration.userContentController.removeScriptMessageHandler(forName: "consoleWarn") configuration.userContentController.removeScriptMessageHandler(forName: "callHandler") configuration.userContentController.removeScriptMessageHandler(forName: "onFindResultReceived") + configuration.userContentController.removeScriptMessageHandler(forName: "onCallAsyncJavaScriptResultBelowIOS14Received") if #available(iOS 14.0, *) { configuration.userContentController.removeAllScriptMessageHandlers() for contentWorldName in userScriptsContentWorlds { @@ -3469,6 +3567,7 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { if let wId = windowId, InAppWebView.windowWebViews[wId] != nil { InAppWebView.windowWebViews.removeValue(forKey: wId) } + callAsyncJavaScriptBelowIOS14Results.removeAll() super.removeFromSuperview() } diff --git a/ios/Classes/InAppWebViewMethodHandler.swift b/ios/Classes/InAppWebViewMethodHandler.swift index 9c36f406..3ac6e562 100644 --- a/ios/Classes/InAppWebViewMethodHandler.swift +++ b/ios/Classes/InAppWebViewMethodHandler.swift @@ -384,6 +384,17 @@ class InAppWebViewMethodHandler: FlutterMethodCallDelegate { webView?.removeAllUserScripts() result(true) break + case "callAsyncJavaScript": + if webView != nil, #available(iOS 10.3, *) { + let functionBody = arguments!["functionBody"] as! String + let functionArguments = arguments!["arguments"] as! [String:Any] + let contentWorldName = arguments!["contentWorld"] as? String + webView!.callAsyncJavaScript(functionBody: functionBody, arguments: functionArguments, contentWorldName: contentWorldName, result: result) + } + else { + result(nil) + } + break default: result(FlutterMethodNotImplemented) break diff --git a/lib/src/in_app_webview_controller.dart b/lib/src/in_app_webview_controller.dart index 9c406530..cb89ad77 100644 --- a/lib/src/in_app_webview_controller.dart +++ b/lib/src/in_app_webview_controller.dart @@ -32,6 +32,7 @@ const javaScriptHandlerForbiddenNames = [ "onPrint", "onWindowFocus", "onWindowBlur", + "callAsyncJavaScript" ]; ///Controls a WebView, such as an [InAppWebView] widget instance, a [HeadlessInAppWebView] instance or [InAppBrowser] WebView instance. @@ -2039,6 +2040,46 @@ class InAppWebViewController { await _channel.invokeMethod('removeAllUserScripts', args); } + ///Executes the specified string as an asynchronous JavaScript function. + /// + ///[functionBody] is the JavaScript string to use as the function body. + ///This method treats the string as an anonymous JavaScript function body and calls it with the named arguments in the arguments parameter. + /// + ///[arguments] is a dictionary of the arguments to pass to the function call. + ///Each key in the dictionary corresponds to the name of an argument in the [functionBody] string, + ///and the value of that key is the value to use during the evaluation of the code. + ///Supported value types can be found in the official Flutter docs: + ///[Platform channel data types support and codecs](https://flutter.dev/docs/development/platform-integration/platform-channels#codec), + ///except for [Uint8List], [Int32List], [Int64List], and [Float64List] that should be converted into a [List]. + ///All items in an array or dictionary must also be one of the supported types. + /// + ///[contentWorld], on iOS, it represents the namespace in which to evaluate the JavaScript [source] code. + ///Instead, on Android, it will run the [source] code into an iframe. + ///This parameter doesn’t apply to changes you make to the underlying web content, such as the document’s DOM structure. + ///Those changes remain visible to all scripts, regardless of which content world you specify. + ///For more information about content worlds, see [ContentWorld]. + ///Available on iOS 14.0+. + /// + ///**NOTE for iOS**: available only on iOS 10.3+. + /// + ///**NOTE for Android**: available only on Android 21+. + /// + ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wkwebview/3656441-callasyncjavascript + Future callAsyncJavaScript({required String functionBody, Map arguments = const {}, ContentWorld? contentWorld}) async { + Map args = {}; + args.putIfAbsent('functionBody', () => functionBody); + args.putIfAbsent('arguments', () => arguments); + args.putIfAbsent('contentWorld', () => contentWorld?.name); + var data = await _channel.invokeMethod('callAsyncJavaScript', args); + if (data == null) { + return null; + } + if (defaultTargetPlatform == TargetPlatform.android) { + data = json.decode(data); + } + return CallAsyncJavaScriptResult(value: data["value"], error: data["error"]); + } + ///Gets the default user agent. /// ///**Official Android API**: https://developer.android.com/reference/android/webkit/WebSettings#getDefaultUserAgent(android.content.Context) diff --git a/lib/src/types.dart b/lib/src/types.dart index 1c368c1f..841d2b5b 100755 --- a/lib/src/types.dart +++ b/lib/src/types.dart @@ -4623,4 +4623,41 @@ class ContentWorld { ///Be careful when manipulating variables in this content world. ///If you modify a variable with the same name as one the webpage uses, you may unintentionally disrupt the normal operation of that page. static ContentWorld page = ContentWorld.world(name: "page"); + + Map toMap() { + return {"name": name}; + } + + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } +} + +///Class that represents either a success or a failure, including an associated value in each case for [InAppWebViewController.callAsyncJavaScript]. +class CallAsyncJavaScriptResult { + ///It contains the success value. + dynamic value; + + ///It contains the failure value. + dynamic error; + + CallAsyncJavaScriptResult({this.value, this.error}); + + Map toMap() { + return {"value": value, "error": error}; + } + + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } } \ No newline at end of file