Added callAsyncJavaScript WebView method, fix #642, fix #614

This commit is contained in:
Lorenzo Pichilli 2021-02-07 16:05:39 +01:00
parent 9b3ea1c6a2
commit 7c5931b0f9
14 changed files with 404 additions and 30 deletions

View File

@ -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

View File

@ -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<String, MethodChannel.Result> 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<String, Object> arguments, @Nullable String contentWorldName, @NonNull MethodChannel.Result result) {
String resultUuid = UUID.randomUUID().toString();
callAsyncJavaScriptResults.put(resultUuid, result);
JSONObject functionArguments = new JSONObject(arguments);
Iterator<String> keys = functionArguments.keys();
List<String> functionArgumentNamesList = new ArrayList<>();
List<String> 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();
}

View File

@ -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<String, Object> 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<String, Object> obj = new HashMap<>();

View File

@ -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<String, Object> functionArguments = (Map<String, Object>) call.argument("arguments");
String contentWorldName = (String) call.argument("contentWorld");
webView.callAsyncJavaScript(functionBody, functionArguments, contentWorldName, result);
}
else {
result.success(null);
}
break;
default:
result.notImplemented();
}

View File

@ -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() {

View File

@ -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<Map<String, Object>> 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<Boolean>() {

View File

@ -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<String, Object>) value).toString();
} else if (value instanceof List) {
return new JSONArray((List<Object>) value).toString();
} else if (value instanceof String) {
return JSONObject.quote((String) value);
} else {
return JSONObject.wrap(value).toString();
}
}
}

View File

@ -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"}
{"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"}

View File

@ -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 = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
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 = "<group>"; };
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 = {

View File

@ -59,7 +59,7 @@
</array>
<key>NSBonjourServices</key>
<array>
<string></string>
<string>_dartobservatory._tcp</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>

View File

@ -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()
}

View File

@ -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

View File

@ -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 doesnt apply to changes you make to the underlying web content, such as the documents 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<CallAsyncJavaScriptResult?> callAsyncJavaScript({required String functionBody, Map<String, dynamic> arguments = const <String, dynamic>{}, ContentWorld? contentWorld}) async {
Map<String, dynamic> args = <String, dynamic>{};
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)

View File

@ -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<String, dynamic> toMap() {
return {"name": name};
}
Map<String, dynamic> 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<String, dynamic> toMap() {
return {"value": value, "error": error};
}
Map<String, dynamic> toJson() {
return this.toMap();
}
@override
String toString() {
return toMap().toString();
}
}