From 8dfddc2e12b9c870bb73192496f581ed75861fdb Mon Sep 17 00:00:00 2001 From: Lorenzo Pichilli Date: Tue, 14 Nov 2023 18:39:25 +0100 Subject: [PATCH] Added ProcessGlobalConfig for Android WebViews, Added disableWebView static method on InAppWebViewController for Android, Added support for Android WebViewFeature.isStartupFeatureSupported, WebViewFeature.STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS, WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX, WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER, Added WebMessage.type property, WebMessage.data property is of type dynamic, JavaScriptReplyProxy.postMessage is of type WebMessage, changed WebMessageListener.onPostMessage and WebMessagePort.setWebMessageCallback methods signature --- CHANGELOG.md | 13 ++ .../InAppWebViewFlutterPlugin.java | 14 +- .../WebViewFeatureManager.java | 6 + .../ProcessGlobalConfigManager.java | 50 +++++ .../ProcessGlobalConfigSettings.java | 119 ++++++++++++ .../types/WebMessageCompatExt.java | 117 ++++++++++++ .../types/WebMessagePortCompatExt.java | 78 ++++++++ .../webview/InAppWebViewManager.java | 6 + .../webview/WebViewChannelDelegate.java | 55 +++--- .../web_message/WebMessageChannel.java | 29 +-- .../WebMessageChannelChannelDelegate.java | 7 +- .../web_message/WebMessageListener.java | 20 +- .../WebMessageListenerChannelDelegate.java | 8 +- .../CHANGELOG.md | 4 + .../lib/src/exchangeable_object.dart | 2 + .../pubspec.yaml | 4 +- .../src/exchangeable_object_generator.dart | 2 +- dev_packages/generators/pubspec.lock | 9 +- dev_packages/generators/pubspec.yaml | 2 +- .../in_app_webview/web_message.dart | 171 +++++++++++++++++- .../process_global_config/apply.dart | 38 ++++ .../process_global_config/main.dart | 20 ++ .../webview_flutter_test.dart | 2 + ios/Classes/InAppWebView/InAppWebView.swift | 18 +- .../WebMessage/WebMessageChannel.swift | 4 +- .../WebMessageChannelChannelDelegate.swift | 24 ++- .../WebMessageListenerChannelDelegate.swift | 9 +- .../InAppWebView/WebViewChannelDelegate.swift | 18 +- .../WebMessageListenerJS.swift | 6 +- ios/Classes/Types/WebMessage.swift | 48 ++++- ios/Classes/Types/WebMessagePort.swift | 32 +++- lib/src/android/main.dart | 5 + lib/src/android/process_global_config.dart | 150 +++++++++++++++ lib/src/android/process_global_config.g.dart | 142 +++++++++++++++ lib/src/android/proxy_controller.dart | 37 ---- lib/src/android/webview_feature.dart | 110 +++++++---- lib/src/android/webview_feature.g.dart | 110 +++++++---- .../in_app_webview_controller.dart | 19 ++ lib/src/types/on_post_message_callback.dart | 2 +- lib/src/types/web_message_callback.dart | 2 +- lib/src/web_message/main.dart | 2 +- lib/src/web_message/web_message.dart | 35 +++- lib/src/web_message/web_message.g.dart | 95 +++++++++- lib/src/web_message/web_message_channel.dart | 6 +- lib/src/web_message/web_message_listener.dart | 12 +- macos/Classes/InAppWebView/InAppWebView.swift | 18 +- .../WebMessage/WebMessageChannel.swift | 4 +- .../WebMessageChannelChannelDelegate.swift | 24 ++- .../WebMessageListenerChannelDelegate.swift | 9 +- .../InAppWebView/WebViewChannelDelegate.swift | 18 +- .../WebMessageListenerJS.swift | 6 +- macos/Classes/Types/WebMessage.swift | 49 ++++- macos/Classes/Types/WebMessagePort.swift | 32 +++- pubspec.yaml | 4 +- 54 files changed, 1560 insertions(+), 266 deletions(-) create mode 100755 android/src/main/java/com/pichillilorenzo/flutter_inappwebview/process_global_config/ProcessGlobalConfigManager.java create mode 100644 android/src/main/java/com/pichillilorenzo/flutter_inappwebview/process_global_config/ProcessGlobalConfigSettings.java create mode 100644 android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/WebMessageCompatExt.java create mode 100644 android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/WebMessagePortCompatExt.java create mode 100644 example/integration_test/process_global_config/apply.dart create mode 100644 example/integration_test/process_global_config/main.dart create mode 100644 lib/src/android/process_global_config.dart create mode 100644 lib/src/android/process_global_config.g.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 31439049..9db221b8 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## 6.0.0-beta.28 + +- Added `ProcessGlobalConfig` for Android WebViews +- Added `disableWebView` static method on `InAppWebViewController` for Android +- Added support for Android `WebViewFeature.isStartupFeatureSupported`, `WebViewFeature.STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS`, `WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX`, `WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER` +- Added `WebMessage.type` property + +### BREAKING CHANGES + +- `WebMessage.data` property is of type `dynamic` +- `JavaScriptReplyProxy.postMessage` is of type `WebMessage` +- `WebMessageListener.onPostMessage` and `WebMessagePort.setWebMessageCallback` methods signature + ## 6.0.0-beta.27 - Added `requestPostMessageChannel`, `postMessage`, `isEngagementSignalsApiAvailable` methods on `ChromeSafariBrowser` for Android diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewFlutterPlugin.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewFlutterPlugin.java index 97b668b3..c6c6f037 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewFlutterPlugin.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebViewFlutterPlugin.java @@ -2,29 +2,28 @@ package com.pichillilorenzo.flutter_inappwebview; import android.app.Activity; import android.content.Context; -import android.net.Uri; import android.os.Build; -import android.webkit.ValueCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.pichillilorenzo.flutter_inappwebview.chrome_custom_tabs.ChromeSafariBrowserManager; import com.pichillilorenzo.flutter_inappwebview.credential_database.CredentialDatabaseHandler; -import com.pichillilorenzo.flutter_inappwebview.in_app_browser.InAppBrowserManager; import com.pichillilorenzo.flutter_inappwebview.headless_in_app_webview.HeadlessInAppWebViewManager; +import com.pichillilorenzo.flutter_inappwebview.in_app_browser.InAppBrowserManager; import com.pichillilorenzo.flutter_inappwebview.print_job.PrintJobManager; +import com.pichillilorenzo.flutter_inappwebview.process_global_config.ProcessGlobalConfigManager; import com.pichillilorenzo.flutter_inappwebview.proxy.ProxyManager; import com.pichillilorenzo.flutter_inappwebview.service_worker.ServiceWorkerManager; import com.pichillilorenzo.flutter_inappwebview.tracing.TracingControllerManager; import com.pichillilorenzo.flutter_inappwebview.webview.FlutterWebViewFactory; import com.pichillilorenzo.flutter_inappwebview.webview.InAppWebViewManager; +import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.PluginRegistry; -import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.platform.PlatformViewRegistry; import io.flutter.view.FlutterView; @@ -58,6 +57,8 @@ public class InAppWebViewFlutterPlugin implements FlutterPlugin, ActivityAware { public PrintJobManager printJobManager; @Nullable public TracingControllerManager tracingControllerManager; + @Nullable + public ProcessGlobalConfigManager processGlobalConfigManager; public FlutterWebViewFactory flutterWebViewFactory; public Context applicationContext; public PluginRegistry.Registrar registrar; @@ -122,6 +123,7 @@ public class InAppWebViewFlutterPlugin implements FlutterPlugin, ActivityAware { printJobManager = new PrintJobManager(this); } tracingControllerManager = new TracingControllerManager(this); + processGlobalConfigManager = new ProcessGlobalConfigManager(this); } @Override @@ -178,6 +180,10 @@ public class InAppWebViewFlutterPlugin implements FlutterPlugin, ActivityAware { tracingControllerManager.dispose(); tracingControllerManager = null; } + if (processGlobalConfigManager != null) { + processGlobalConfigManager.dispose(); + processGlobalConfigManager = null; + } } @Override diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/WebViewFeatureManager.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/WebViewFeatureManager.java index 17f57baf..212e7a4f 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/WebViewFeatureManager.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/WebViewFeatureManager.java @@ -28,6 +28,12 @@ public class WebViewFeatureManager extends ChannelDelegateImpl { String feature = (String) call.argument("feature"); result.success(WebViewFeature.isFeatureSupported(feature)); break; + case "isStartupFeatureSupported": + if (plugin != null && plugin.activity != null) { + String startupFeature = (String) call.argument("startupFeature"); + result.success(WebViewFeature.isStartupFeatureSupported(plugin.activity, startupFeature)); + } + break; default: result.notImplemented(); } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/process_global_config/ProcessGlobalConfigManager.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/process_global_config/ProcessGlobalConfigManager.java new file mode 100755 index 00000000..d87244cd --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/process_global_config/ProcessGlobalConfigManager.java @@ -0,0 +1,50 @@ +package com.pichillilorenzo.flutter_inappwebview.process_global_config; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.ProcessGlobalConfig; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview.pull_to_refresh.PullToRefreshSettings; +import com.pichillilorenzo.flutter_inappwebview.types.ChannelDelegateImpl; + +import java.util.Map; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class ProcessGlobalConfigManager extends ChannelDelegateImpl { + protected static final String LOG_TAG = "ProcessGlobalConfigManager"; + public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_processglobalconfig"; + + @Nullable + public InAppWebViewFlutterPlugin plugin; + + public ProcessGlobalConfigManager(@NonNull final InAppWebViewFlutterPlugin plugin) { + super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME)); + this.plugin = plugin; + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + switch (call.method) { + case "apply": + if (plugin != null && plugin.activity != null) { + ProcessGlobalConfigSettings settings = (new ProcessGlobalConfigSettings()) + .parse((Map) call.argument("settings")); + ProcessGlobalConfig.apply(settings.toProcessGlobalConfig(plugin.activity)); + } + result.success(true); + break; + default: + result.notImplemented(); + } + } + + @Override + public void dispose() { + super.dispose(); + plugin = null; + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/process_global_config/ProcessGlobalConfigSettings.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/process_global_config/ProcessGlobalConfigSettings.java new file mode 100644 index 00000000..d8b70752 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/process_global_config/ProcessGlobalConfigSettings.java @@ -0,0 +1,119 @@ +package com.pichillilorenzo.flutter_inappwebview.process_global_config; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.ProcessGlobalConfig; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview.ISettings; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +public class ProcessGlobalConfigSettings implements ISettings { + public static final String LOG_TAG = "ProcessGlobalConfigSettings"; + + @Nullable + public String dataDirectorySuffix; + @Nullable + public DirectoryBasePaths directoryBasePaths; + + @NonNull + @Override + public ProcessGlobalConfigSettings parse(@NonNull Map settings) { + for (Map.Entry pair : settings.entrySet()) { + String key = pair.getKey(); + Object value = pair.getValue(); + if (value == null) { + continue; + } + + switch (key) { + case "dataDirectorySuffix": + dataDirectorySuffix = (String) value; + break; + case "directoryBasePaths": + directoryBasePaths = (new DirectoryBasePaths()).parse((Map) value); + break; + } + } + + return this; + } + + public ProcessGlobalConfig toProcessGlobalConfig(@NonNull Context context) { + ProcessGlobalConfig config = new ProcessGlobalConfig(); + if (dataDirectorySuffix != null && + WebViewFeature.isStartupFeatureSupported(context, WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX)) { + config.setDataDirectorySuffix(context, dataDirectorySuffix); + } + if (directoryBasePaths != null && + WebViewFeature.isStartupFeatureSupported(context, WebViewFeature.STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS)) { + config.setDirectoryBasePaths(context, + new File(directoryBasePaths.dataDirectoryBasePath), + new File(directoryBasePaths.cacheDirectoryBasePath)); + } + return config; + } + @NonNull + public Map toMap() { + Map settings = new HashMap<>(); + settings.put("dataDirectorySuffix", dataDirectorySuffix); + return settings; + } + + @NonNull + @Override + public Map getRealSettings(@NonNull ProcessGlobalConfig processGlobalConfig) { + Map realSettings = toMap(); + return realSettings; + } + + static class DirectoryBasePaths implements ISettings { + public static final String LOG_TAG = "ProcessGlobalConfigSettings"; + + public String cacheDirectoryBasePath; + public String dataDirectoryBasePath; + + @NonNull + @Override + public DirectoryBasePaths parse(@NonNull Map settings) { + for (Map.Entry pair : settings.entrySet()) { + String key = pair.getKey(); + Object value = pair.getValue(); + if (value == null) { + continue; + } + + switch (key) { + case "cacheDirectoryBasePath": + cacheDirectoryBasePath = (String) value; + break; + case "dataDirectoryBasePath": + dataDirectoryBasePath = (String) value; + break; + } + } + + return this; + } + + @NonNull + public Map toMap() { + Map settings = new HashMap<>(); + settings.put("cacheDirectoryBasePath", cacheDirectoryBasePath); + settings.put("dataDirectoryBasePath", dataDirectoryBasePath); + return settings; + } + + @NonNull + @Override + public Map getRealSettings(@NonNull Object obj) { + Map realSettings = toMap(); + return realSettings; + } + } +} \ No newline at end of file diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/WebMessageCompatExt.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/WebMessageCompatExt.java new file mode 100644 index 00000000..fca69bb8 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/WebMessageCompatExt.java @@ -0,0 +1,117 @@ +package com.pichillilorenzo.flutter_inappwebview.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebMessageCompat; +import androidx.webkit.WebViewFeature; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class WebMessageCompatExt { + @Nullable + private Object data; + private @WebMessageCompat.Type int type; + + @Nullable + private List ports; + + public WebMessageCompatExt(@Nullable Object data, int type, @Nullable List ports) { + this.data = data; + this.type = type; + this.ports = ports; + } + + public static WebMessageCompatExt fromMapWebMessageCompat(@NonNull WebMessageCompat message) { + Object data; + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER) && message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) { + data = message.getArrayBuffer(); + } else { + data = message.getData(); + } + return new WebMessageCompatExt(data, message.getType(), null); + } + + @Nullable + public static WebMessageCompatExt fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + Object data = map.get("data"); + Integer type = (Integer) map.get("type"); + List> portMapList = (List>) map.get("ports"); + List ports = null; + if (portMapList != null && !portMapList.isEmpty()) { + ports = new ArrayList<>(); + for (Map portMap : portMapList) { + ports.add(WebMessagePortCompatExt.fromMap(portMap)); + } + } + return new WebMessageCompatExt(data, type, ports); + } + + public Map toMap() { + Map proxyRuleMap = new HashMap<>(); + proxyRuleMap.put("data", data); + proxyRuleMap.put("type", type); + return proxyRuleMap; + } + + @Nullable + public Object getData() { + return data; + } + + public void setData(@Nullable Object data) { + this.data = data; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + @Nullable + public List getPorts() { + return ports; + } + + public void setPorts(@Nullable List ports) { + this.ports = ports; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + WebMessageCompatExt that = (WebMessageCompatExt) o; + + if (type != that.type) return false; + if (!Objects.equals(data, that.data)) return false; + return Objects.equals(ports, that.ports); + } + + @Override + public int hashCode() { + int result = data != null ? data.hashCode() : 0; + result = 31 * result + type; + result = 31 * result + (ports != null ? ports.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "WebMessageCompatExt{" + + "data=" + data + + ", type=" + type + + ", ports=" + ports + + '}'; + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/WebMessagePortCompatExt.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/WebMessagePortCompatExt.java new file mode 100644 index 00000000..db7c3b49 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/types/WebMessagePortCompatExt.java @@ -0,0 +1,78 @@ +package com.pichillilorenzo.flutter_inappwebview.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class WebMessagePortCompatExt { + private int index; + @NonNull + private String webMessageChannelId; + + public WebMessagePortCompatExt(int index, @NonNull String webMessageChannelId) { + this.index = index; + this.webMessageChannelId = webMessageChannelId; + } + + @Nullable + public static WebMessagePortCompatExt fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + Integer index = (Integer) map.get("index"); + String webMessageChannelId = (String) map.get("webMessageChannelId"); + return new WebMessagePortCompatExt(index, webMessageChannelId); + } + + public Map toMap() { + Map proxyRuleMap = new HashMap<>(); + proxyRuleMap.put("index", index); + proxyRuleMap.put("webMessageChannelId", webMessageChannelId); + return proxyRuleMap; + } + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + @NonNull + public String getWebMessageChannelId() { + return webMessageChannelId; + } + + public void setWebMessageChannelId(@NonNull String webMessageChannelId) { + this.webMessageChannelId = webMessageChannelId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + WebMessagePortCompatExt that = (WebMessagePortCompatExt) o; + + if (index != that.index) return false; + return webMessageChannelId.equals(that.webMessageChannelId); + } + + @Override + public int hashCode() { + int result = index; + result = 31 * result + webMessageChannelId.hashCode(); + return result; + } + + @Override + public String toString() { + return "WebMessagePortCompatExt{" + + "index=" + index + + ", webMessageChannelId='" + webMessageChannelId + '\'' + + '}'; + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/InAppWebViewManager.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/InAppWebViewManager.java index 881f1c07..8e5e7f0c 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/InAppWebViewManager.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/InAppWebViewManager.java @@ -133,6 +133,12 @@ public class InAppWebViewManager extends ChannelDelegateImpl { result.success(false); } break; + case "disableWebView": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + WebView.disableWebView(); + } + result.success(true); + break; case "disposeKeepAlive": final String keepAliveId = (String) call.argument("keepAliveId"); if (keepAliveId != null) { diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/WebViewChannelDelegate.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/WebViewChannelDelegate.java index 6e480a19..6324159f 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/WebViewChannelDelegate.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/WebViewChannelDelegate.java @@ -43,7 +43,9 @@ import com.pichillilorenzo.flutter_inappwebview.types.SslCertificateExt; import com.pichillilorenzo.flutter_inappwebview.types.SyncBaseCallbackResultImpl; import com.pichillilorenzo.flutter_inappwebview.types.URLRequest; import com.pichillilorenzo.flutter_inappwebview.types.UserScript; +import com.pichillilorenzo.flutter_inappwebview.types.WebMessageCompatExt; import com.pichillilorenzo.flutter_inappwebview.types.WebMessagePort; +import com.pichillilorenzo.flutter_inappwebview.types.WebMessagePortCompatExt; import com.pichillilorenzo.flutter_inappwebview.types.WebResourceErrorExt; import com.pichillilorenzo.flutter_inappwebview.types.WebResourceRequestExt; import com.pichillilorenzo.flutter_inappwebview.types.WebResourceResponseExt; @@ -63,10 +65,10 @@ import io.flutter.plugin.common.MethodChannel; public class WebViewChannelDelegate extends ChannelDelegateImpl { static final String LOG_TAG = "WebViewChannelDelegate"; - + @Nullable private InAppWebViewInterface webView; - + public WebViewChannelDelegate(@NonNull InAppWebViewInterface webView, @NonNull MethodChannel channel) { super(channel); this.webView = webView; @@ -141,8 +143,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.success(value); } }); - } - else { + } else { result.success(null); } break; @@ -210,8 +211,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { if (webView != null) { Map screenshotConfiguration = (Map) call.argument("screenshotConfiguration"); webView.takeScreenshot(screenshotConfiguration, result); - } - else + } else result.success(null); break; case setSettings: @@ -282,8 +282,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.success(success); } }); - } - else { + } else { result.success(false); } break; @@ -568,8 +567,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.success(value); } }); - } - else { + } else { result.success(null); } break; @@ -598,27 +596,33 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { break; case postWebMessage: if (webView != null && WebViewFeature.isFeatureSupported(WebViewFeature.POST_WEB_MESSAGE)) { - Map message = (Map) call.argument("message"); + WebMessageCompatExt message = WebMessageCompatExt.fromMap((Map) call.argument("message")); String targetOrigin = (String) call.argument("targetOrigin"); List compatPorts = new ArrayList<>(); - List ports = new ArrayList<>(); - List> portsMap = (List>) message.get("ports"); - if (portsMap != null) { - for (Map portMap : portsMap) { - String webMessageChannelId = (String) portMap.get("webMessageChannelId"); - Integer index = (Integer) portMap.get("index"); - WebMessageChannel webMessageChannel = webView.getWebMessageChannels().get(webMessageChannelId); + List portsExt = message.getPorts(); + if (portsExt != null) { + for (WebMessagePortCompatExt portExt : portsExt) { + WebMessageChannel webMessageChannel = webView.getWebMessageChannels().get(portExt.getWebMessageChannelId()); if (webMessageChannel != null) { if (webView instanceof InAppWebView) { - compatPorts.add(webMessageChannel.compatPorts.get(index)); + compatPorts.add(webMessageChannel.compatPorts.get(portExt.getIndex())); } } } } + Object data = message.getData(); if (webView instanceof InAppWebView) { - WebMessageCompat webMessage = new WebMessageCompat((String) message.get("data"), compatPorts.toArray(new WebMessagePortCompat[0])); try { - WebViewCompat.postWebMessage((WebView) webView, webMessage, Uri.parse(targetOrigin)); + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER) && data != null && + message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) { + WebViewCompat.postWebMessage((WebView) webView, + new WebMessageCompat((byte[]) data, compatPorts.toArray(new WebMessagePortCompat[0])), + Uri.parse(targetOrigin)); + } else { + WebViewCompat.postWebMessage((WebView) webView, + new WebMessageCompat(data != null ? data.toString() : null, compatPorts.toArray(new WebMessagePortCompat[0])), + Uri.parse(targetOrigin)); + } result.success(true); } catch (Exception e) { result.error(LOG_TAG, e.getMessage(), null); @@ -671,8 +675,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { } /** - * @deprecated - * Use {@link FindInteractionChannelDelegate#onFindResultReceived} instead. + * @deprecated Use {@link FindInteractionChannelDelegate#onFindResultReceived} instead. */ @Deprecated public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) { @@ -684,7 +687,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { obj.put("isDoneCounting", isDoneCounting); channel.invokeMethod("onFindResultReceived", obj); } - + public void onLongPressHitTestResult(HitTestResult hitTestResult) { MethodChannel channel = getChannel(); if (channel == null) return; @@ -762,7 +765,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { return JsAlertResponse.fromMap((Map) obj); } } - + public void onJsAlert(String url, String message, Boolean isMainFrame, @NonNull JsAlertCallback callback) { MethodChannel channel = getChannel(); if (channel == null) { @@ -1174,7 +1177,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { return (new LoadResourceWithCustomSchemeCallback()).decodeResult(obj); } } - + @Nullable public CustomSchemeResponse onLoadResourceWithCustomScheme(WebResourceRequestExt request) throws InterruptedException { MethodChannel channel = getChannel(); diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/web_message/WebMessageChannel.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/web_message/WebMessageChannel.java index 075ee37b..b5e15e26 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/web_message/WebMessageChannel.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/web_message/WebMessageChannel.java @@ -11,6 +11,8 @@ import androidx.webkit.WebViewFeature; import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.JavaScriptBridgeJS; import com.pichillilorenzo.flutter_inappwebview.types.Disposable; +import com.pichillilorenzo.flutter_inappwebview.types.WebMessageCompatExt; +import com.pichillilorenzo.flutter_inappwebview.types.WebMessagePortCompatExt; import com.pichillilorenzo.flutter_inappwebview.webview.InAppWebViewInterface; import com.pichillilorenzo.flutter_inappwebview.types.WebMessagePort; import com.pichillilorenzo.flutter_inappwebview.webview.in_app_webview.InAppWebView; @@ -77,7 +79,7 @@ public class WebMessageChannel implements Disposable { @Override public void onMessage(@NonNull WebMessagePortCompat port, @Nullable WebMessageCompat message) { super.onMessage(port, message); - webMessageChannel.onMessage(index, message != null ? message.getData() : null); + webMessageChannel.onMessage(index, message != null ? WebMessageCompatExt.fromMapWebMessageCompat(message) : null); } }); result.success(true); @@ -89,25 +91,28 @@ public class WebMessageChannel implements Disposable { } } - public void postMessageForInAppWebView(final Integer index, Map message, @NonNull MethodChannel.Result result) { + public void postMessageForInAppWebView(final Integer index, @NonNull WebMessageCompatExt message, @NonNull MethodChannel.Result result) { if (webView != null && compatPorts.size() > 0 && WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_PORT_POST_MESSAGE)) { WebMessagePortCompat port = compatPorts.get(index); List webMessagePorts = new ArrayList<>(); - List> portsMap = (List>) message.get("ports"); - if (portsMap != null) { - for (Map portMap : portsMap) { - String webMessageChannelId = (String) portMap.get("webMessageChannelId"); - Integer portIndex = (Integer) portMap.get("index"); - WebMessageChannel webMessageChannel = webView.getWebMessageChannels().get(webMessageChannelId); + List portsExt = message.getPorts(); + if (portsExt != null) { + for (WebMessagePortCompatExt portExt : portsExt) { + WebMessageChannel webMessageChannel = webView.getWebMessageChannels().get(portExt.getWebMessageChannelId()); if (webMessageChannel != null) { - webMessagePorts.add(webMessageChannel.compatPorts.get(portIndex)); + webMessagePorts.add(webMessageChannel.compatPorts.get(portExt.getIndex())); } } } - WebMessageCompat webMessage = new WebMessageCompat((String) message.get("data"), webMessagePorts.toArray(new WebMessagePortCompat[0])); + Object data = message.getData(); try { - port.postMessage(webMessage); + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER) && data != null && + message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) { + port.postMessage(new WebMessageCompat((byte[]) data, webMessagePorts.toArray(new WebMessagePortCompat[0]))); + } else { + port.postMessage(new WebMessageCompat(data != null ? data.toString() : null, webMessagePorts.toArray(new WebMessagePortCompat[0]))); + } result.success(true); } catch (Exception e) { result.error(LOG_TAG, e.getMessage(), null); @@ -132,7 +137,7 @@ public class WebMessageChannel implements Disposable { } } - public void onMessage(int index, String message) { + public void onMessage(int index, @Nullable WebMessageCompatExt message) { if (channelDelegate != null) { channelDelegate.onMessage(index, message); } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/web_message/WebMessageChannelChannelDelegate.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/web_message/WebMessageChannelChannelDelegate.java index c56fca49..cbc538d5 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/web_message/WebMessageChannelChannelDelegate.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/web_message/WebMessageChannelChannelDelegate.java @@ -5,6 +5,7 @@ import androidx.annotation.Nullable; import com.pichillilorenzo.flutter_inappwebview.types.ChannelDelegateImpl; import com.pichillilorenzo.flutter_inappwebview.types.Disposable; +import com.pichillilorenzo.flutter_inappwebview.types.WebMessageCompatExt; import com.pichillilorenzo.flutter_inappwebview.webview.in_app_webview.InAppWebView; import java.util.HashMap; @@ -36,7 +37,7 @@ public class WebMessageChannelChannelDelegate extends ChannelDelegateImpl { case "postMessage": if (webMessageChannel != null && webMessageChannel.webView instanceof InAppWebView) { final Integer index = (Integer) call.argument("index"); - Map message = (Map) call.argument("message"); + WebMessageCompatExt message = WebMessageCompatExt.fromMap((Map) call.argument("message")); webMessageChannel.postMessageForInAppWebView(index, message, result); } else { result.success(false); @@ -55,12 +56,12 @@ public class WebMessageChannelChannelDelegate extends ChannelDelegateImpl { } } - public void onMessage(int index, String message) { + public void onMessage(int index, @Nullable WebMessageCompatExt message) { MethodChannel channel = getChannel(); if (channel == null) return; Map obj = new HashMap<>(); obj.put("index", index); - obj.put("message", message ); + obj.put("message", message != null ? message.toMap() : null); channel.invokeMethod("onMessage", obj); } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/web_message/WebMessageListener.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/web_message/WebMessageListener.java index d36a9966..5cb35a80 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/web_message/WebMessageListener.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/web_message/WebMessageListener.java @@ -14,6 +14,7 @@ import androidx.webkit.WebViewFeature; import com.pichillilorenzo.flutter_inappwebview.Util; import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.JavaScriptBridgeJS; import com.pichillilorenzo.flutter_inappwebview.types.Disposable; +import com.pichillilorenzo.flutter_inappwebview.types.WebMessageCompatExt; import com.pichillilorenzo.flutter_inappwebview.webview.InAppWebViewInterface; import com.pichillilorenzo.flutter_inappwebview.types.PluginScript; import com.pichillilorenzo.flutter_inappwebview.types.UserScriptInjectionTime; @@ -61,7 +62,7 @@ public class WebMessageListener implements Disposable { boolean isMainFrame, @NonNull JavaScriptReplyProxy javaScriptReplyProxy) { replyProxy = javaScriptReplyProxy; if (channelDelegate != null) { - channelDelegate.onPostMessage(message.getData(), + channelDelegate.onPostMessage(WebMessageCompatExt.fromMapWebMessageCompat(message), sourceOrigin.toString().equals("null") ? null : sourceOrigin.toString(), isMainFrame); } @@ -171,9 +172,16 @@ public class WebMessageListener implements Disposable { } } - public void postMessageForInAppWebView(String message, @NonNull MethodChannel.Result result) { + public void postMessageForInAppWebView(WebMessageCompatExt message, @NonNull MethodChannel.Result result) { if (replyProxy != null && WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { - replyProxy.postMessage(message); + Object data = message.getData(); + if (data != null) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER) && message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) { + replyProxy.postMessage((byte[]) data); + } else { + replyProxy.postMessage(data.toString()); + } + } } result.success(true); } @@ -196,12 +204,14 @@ public class WebMessageListener implements Disposable { if (rule.getHost() != null && rule.getHost().startsWith("[")) { try { IPv6 = Util.normalizeIPv6(rule.getHost().substring(1, rule.getHost().length() - 1)); - } catch (Exception ignored) {} + } catch (Exception ignored) { + } } String hostIPv6 = null; try { hostIPv6 = Util.normalizeIPv6(host); - } catch (Exception ignored) {} + } catch (Exception ignored) { + } boolean schemeAllowed = rule.getScheme().equals(scheme); diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/web_message/WebMessageListenerChannelDelegate.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/web_message/WebMessageListenerChannelDelegate.java index 9b2155eb..c0f8ada8 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/web_message/WebMessageListenerChannelDelegate.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/web_message/WebMessageListenerChannelDelegate.java @@ -2,10 +2,12 @@ package com.pichillilorenzo.flutter_inappwebview.webview.web_message; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.webkit.WebMessageCompat; import com.pichillilorenzo.flutter_inappwebview.headless_in_app_webview.HeadlessInAppWebView; import com.pichillilorenzo.flutter_inappwebview.types.ChannelDelegateImpl; import com.pichillilorenzo.flutter_inappwebview.types.Disposable; +import com.pichillilorenzo.flutter_inappwebview.types.WebMessageCompatExt; import com.pichillilorenzo.flutter_inappwebview.webview.in_app_webview.InAppWebView; import java.util.HashMap; @@ -28,7 +30,7 @@ public class WebMessageListenerChannelDelegate extends ChannelDelegateImpl { switch (call.method) { case "postMessage": if (webMessageListener != null && webMessageListener.webView instanceof InAppWebView) { - String message = (String) call.argument("message"); + WebMessageCompatExt message = WebMessageCompatExt.fromMap((Map) call.argument("message")); webMessageListener.postMessageForInAppWebView(message, result); } else { result.success(false); @@ -39,11 +41,11 @@ public class WebMessageListenerChannelDelegate extends ChannelDelegateImpl { } } - public void onPostMessage(String message, String sourceOrigin, boolean isMainFrame) { + public void onPostMessage(@Nullable WebMessageCompatExt message, String sourceOrigin, boolean isMainFrame) { MethodChannel channel = getChannel(); if (channel == null) return; Map obj = new HashMap<>(); - obj.put("message", message); + obj.put("message", message != null ? message.toMap() : null); obj.put("sourceOrigin", sourceOrigin); obj.put("isMainFrame", isMainFrame); channel.invokeMethod("onPostMessage", obj); diff --git a/dev_packages/flutter_inappwebview_internal_annotations/CHANGELOG.md b/dev_packages/flutter_inappwebview_internal_annotations/CHANGELOG.md index a05f94be..b7586c31 100755 --- a/dev_packages/flutter_inappwebview_internal_annotations/CHANGELOG.md +++ b/dev_packages/flutter_inappwebview_internal_annotations/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.1.1 + +- Added `ExchangeableObject.fromMapForceAllInline`. + ## 1.1.0 - Added `ExchangeableObject.copyMethod`. diff --git a/dev_packages/flutter_inappwebview_internal_annotations/lib/src/exchangeable_object.dart b/dev_packages/flutter_inappwebview_internal_annotations/lib/src/exchangeable_object.dart index 1776e8fe..e98206ef 100644 --- a/dev_packages/flutter_inappwebview_internal_annotations/lib/src/exchangeable_object.dart +++ b/dev_packages/flutter_inappwebview_internal_annotations/lib/src/exchangeable_object.dart @@ -2,6 +2,7 @@ class ExchangeableObject { final bool toMapMethod; final bool toJsonMethod; final bool fromMapFactory; + final bool fromMapForceAllInline; final bool nullableFromMapFactory; final bool toStringMethod; final bool copyMethod; @@ -10,6 +11,7 @@ class ExchangeableObject { this.toMapMethod = true, this.toJsonMethod = true, this.fromMapFactory = true, + this.fromMapForceAllInline = false, this.nullableFromMapFactory = true, this.toStringMethod = true, this.copyMethod = false diff --git a/dev_packages/flutter_inappwebview_internal_annotations/pubspec.yaml b/dev_packages/flutter_inappwebview_internal_annotations/pubspec.yaml index 01aae9a6..210ffc15 100755 --- a/dev_packages/flutter_inappwebview_internal_annotations/pubspec.yaml +++ b/dev_packages/flutter_inappwebview_internal_annotations/pubspec.yaml @@ -1,10 +1,10 @@ name: flutter_inappwebview_internal_annotations description: Internal annotations used by the generator of flutter_inappwebview plugin -version: 1.1.0 +version: 1.1.1 homepage: https://github.com/pichillilorenzo/flutter_inappwebview environment: - sdk: ">=2.14.0 <3.0.0" + sdk: ">=2.15.0 <4.0.0" dev_dependencies: test: ^1.21.6 \ No newline at end of file diff --git a/dev_packages/generators/lib/src/exchangeable_object_generator.dart b/dev_packages/generators/lib/src/exchangeable_object_generator.dart index a076dbf4..1b3d935c 100644 --- a/dev_packages/generators/lib/src/exchangeable_object_generator.dart +++ b/dev_packages/generators/lib/src/exchangeable_object_generator.dart @@ -358,7 +358,7 @@ class ExchangeableObjectGenerator constructorParameter.isFinal || fieldElement.isFinal || !Util.typeIsNullable(constructorParameter.type)) && !constructorParameter.hasDefaultValue; - if (isRequiredParameter || fieldElement.isFinal) { + if (isRequiredParameter || fieldElement.isFinal || annotation.read("fromMapForceAllInline").boolValue) { requiredFields.add('$fieldName: $value,'); } else { nonRequiredFields.add("instance.$fieldName = $value;"); diff --git a/dev_packages/generators/pubspec.lock b/dev_packages/generators/pubspec.lock index 9291947a..bd52f163 100644 --- a/dev_packages/generators/pubspec.lock +++ b/dev_packages/generators/pubspec.lock @@ -209,11 +209,10 @@ packages: flutter_inappwebview_internal_annotations: dependency: "direct main" description: - name: flutter_inappwebview_internal_annotations - sha256: "064a8ccbc76217dcd3b0fd6c6ea6f549e69b2849a0233b5bb46af9632c3ce2ff" - url: "https://pub.dev" - source: hosted - version: "1.1.0" + path: "../flutter_inappwebview_internal_annotations" + relative: true + source: path + version: "1.1.1" frontend_server_client: dependency: transitive description: diff --git a/dev_packages/generators/pubspec.yaml b/dev_packages/generators/pubspec.yaml index ce517d0a..e0b29985 100755 --- a/dev_packages/generators/pubspec.yaml +++ b/dev_packages/generators/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: sdk: flutter build: ^2.4.0 source_gen: ^1.3.1 - flutter_inappwebview_internal_annotations: ^1.1.0 + flutter_inappwebview_internal_annotations: ^1.1.1 dev_dependencies: build_runner: ^2.4.2 diff --git a/example/integration_test/in_app_webview/web_message.dart b/example/integration_test/in_app_webview/web_message.dart index 2629e571..fe6e4327 100644 --- a/example/integration_test/in_app_webview/web_message.dart +++ b/example/integration_test/in_app_webview/web_message.dart @@ -10,7 +10,8 @@ void webMessage() { ].contains(defaultTargetPlatform); skippableGroup('WebMessage', () { - skippableTestWidgets('WebMessageChannel', (WidgetTester tester) async { + skippableTestWidgets('WebMessageChannel post String', + (WidgetTester tester) async { final Completer controllerCompleter = Completer(); final Completer webMessageCompleter = Completer(); @@ -61,7 +62,7 @@ void webMessage() { await port1.setWebMessageCallback((message) async { await port1 - .postMessage(WebMessage(data: message! + " and back")); + .postMessage(WebMessage(data: message!.data + " and back")); }); await controller.postWebMessage( message: WebMessage(data: "capturePort", ports: [port2]), @@ -78,7 +79,94 @@ void webMessage() { expect(message, 'JavaScript To Native and back'); }); - skippableTestWidgets('WebMessageListener', (WidgetTester tester) async { + skippableTestWidgets('WebMessageChannel post ArrayBuffer', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer webMessageCompleter = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + initialData: InAppWebViewInitialData(data: """ + + + + + WebMessageChannel Test + + + +
+ + + + + + """), + onWebViewCreated: (controller) { + controllerCompleter.complete(controller); + }, + onConsoleMessage: (controller, consoleMessage) { + webMessageCompleter.complete(consoleMessage.message); + }, + onLoadStop: (controller, url) async { + var webMessageChannel = + await controller.createWebMessageChannel(); + var port1 = webMessageChannel!.port1; + var port2 = webMessageChannel.port2; + + await port1.setWebMessageCallback((message) async { + await port1.postMessage(WebMessage( + data: utf8.encode(utf8.decode(message!.data) + " and back"), + type: WebMessageType.ARRAY_BUFFER)); + }); + await controller.postWebMessage( + message: WebMessage( + data: utf8.encode("capturePort"), + type: WebMessageType.ARRAY_BUFFER, + ports: [port2]), + targetOrigin: WebUri("*")); + await controller.evaluateJavascript( + source: "document.getElementById('button').click();"); + }, + ), + ), + ); + await controllerCompleter.future; + + final String message = await webMessageCompleter.future; + expect(message, 'JavaScript To Native and back'); + }); + + skippableTestWidgets('WebMessageListener post String', + (WidgetTester tester) async { final Completer controllerCompleter = Completer(); final Completer pageLoaded = Completer(); @@ -97,9 +185,10 @@ void webMessage() { if (isMainFrame && (sourceOrigin.toString() + '/') == TEST_URL_EXAMPLE.toString()) { - replyProxy.postMessage(message! + " and back"); + replyProxy.postMessage( + WebMessage(data: message!.data + " and back")); } else { - replyProxy.postMessage("Nope"); + replyProxy.postMessage(WebMessage(data: "Nope")); } }, )); @@ -130,5 +219,77 @@ void webMessage() { final String message = await webMessageCompleter.future; expect(message, 'JavaScript To Native and back'); }); + + skippableTestWidgets('WebMessageListener post ArrayBuffer', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageLoaded = Completer(); + final Completer webMessageCompleter = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + onWebViewCreated: (controller) async { + await controller.addWebMessageListener(WebMessageListener( + jsObjectName: "myTestObj", + allowedOriginRules: Set.from(["https://*.example.com"]), + onPostMessage: + (message, sourceOrigin, isMainFrame, replyProxy) { + if (isMainFrame && + (sourceOrigin.toString() + '/') == + TEST_URL_EXAMPLE.toString()) { + replyProxy.postMessage(WebMessage( + data: utf8 + .encode(utf8.decode(message!.data) + " and back"), + type: WebMessageType.ARRAY_BUFFER)); + } else { + replyProxy.postMessage(WebMessage( + data: utf8.encode("Nope"), + type: WebMessageType.ARRAY_BUFFER)); + } + }, + )); + controllerCompleter.complete(controller); + }, + onConsoleMessage: (controller, consoleMessage) { + webMessageCompleter.complete(consoleMessage.message); + }, + onLoadStop: (controller, url) async { + if (url.toString() == TEST_URL_EXAMPLE.toString()) { + pageLoaded.complete(); + } + }, + ), + ), + ); + final controller = await controllerCompleter.future; + await controller.loadUrl(urlRequest: URLRequest(url: TEST_URL_EXAMPLE)); + await pageLoaded.future; + + await controller.evaluateJavascript(source: """ + function bufferToString(buffer) { + return String.fromCharCode.apply(null, Array.from(new Uint8Array(buffer))); + } + + function stringToBuffer(value) { + var buffer = new ArrayBuffer(value.length); + var view = new Uint8Array(buffer); + for (var i = 0, length = value.length; i < length; i++) { + view[i] = value.charCodeAt(i); + } + return buffer; + } + + myTestObj.addEventListener('message', function(event) { + console.log(bufferToString(event.data)); + }); + myTestObj.postMessage(stringToBuffer('JavaScript To Native')); + """); + + final String message = await webMessageCompleter.future; + expect(message, 'JavaScript To Native and back'); + }); }, skip: shouldSkip); } diff --git a/example/integration_test/process_global_config/apply.dart b/example/integration_test/process_global_config/apply.dart new file mode 100644 index 00000000..285809cd --- /dev/null +++ b/example/integration_test/process_global_config/apply.dart @@ -0,0 +1,38 @@ +part of 'main.dart'; + +void apply() { + final shouldSkip = kIsWeb + ? true + : ![ + TargetPlatform.android, + ].contains(defaultTargetPlatform); + + skippableTestWidgets('apply', (WidgetTester tester) async { + await expectLater( + ProcessGlobalConfig.instance().apply( + settings: ProcessGlobalConfigSettings( + dataDirectorySuffix: + (await WebViewFeature.isStartupFeatureSupported( + WebViewFeature + .STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX)) + ? 'suffix_inappwebviewexample' + : null, + directoryBasePaths: + (await WebViewFeature.isStartupFeatureSupported( + WebViewFeature + .STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS)) + ? ProcessGlobalConfigDirectoryBasePaths( + cacheDirectoryBasePath: + (await getApplicationDocumentsDirectory()) + .absolute + .path + + '/inappwebviewexample/cache', + dataDirectoryBasePath: + (await getApplicationDocumentsDirectory()) + .absolute + .path + + '/inappwebviewexample/data') + : null)), + completes); + }, skip: shouldSkip); +} diff --git a/example/integration_test/process_global_config/main.dart b/example/integration_test/process_global_config/main.dart new file mode 100644 index 00000000..aef30436 --- /dev/null +++ b/example/integration_test/process_global_config/main.dart @@ -0,0 +1,20 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:path_provider/path_provider.dart'; +import '../constants.dart'; +import '../env.dart'; +import '../util.dart'; + +part 'apply.dart'; + +void main() { + final shouldSkip = kIsWeb; + + skippableGroup('Process Global Config', () { + apply(); + }, skip: shouldSkip); +} diff --git a/example/integration_test/webview_flutter_test.dart b/example/integration_test/webview_flutter_test.dart index fb3386f3..69b407b3 100644 --- a/example/integration_test/webview_flutter_test.dart +++ b/example/integration_test/webview_flutter_test.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:integration_test/integration_test.dart'; +import 'process_global_config/main.dart' as process_global_config_tests; import 'in_app_webview/main.dart' as in_app_webview_tests; import 'find_interaction_controller/main.dart' as find_interaction_controller_tests; @@ -34,6 +35,7 @@ void main() { FindInteractionController.debugLoggingSettings.usePrint = true; FindInteractionController.debugLoggingSettings.maxLogMessageLength = 7000; + process_global_config_tests.main(); in_app_webview_tests.main(); find_interaction_controller_tests.main(); service_worker_controller_tests.main(); diff --git a/ios/Classes/InAppWebView/InAppWebView.swift b/ios/Classes/InAppWebView/InAppWebView.swift index 8c035f7f..bc27b20d 100755 --- a/ios/Classes/InAppWebView/InAppWebView.swift +++ b/ios/Classes/InAppWebView/InAppWebView.swift @@ -2887,14 +2887,22 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { let body = message.body as! [String: Any?] let webMessageChannelId = body["webMessageChannelId"] as! String let index = body["index"] as! Int64 - let webMessage = body["message"] as? String + var webMessage: WebMessage? = nil + if let webMessageMap = body["message"] as? [String : Any?] { + webMessage = WebMessage.fromMap(map: webMessageMap) + } + if let webMessageChannel = webMessageChannels[webMessageChannelId] { webMessageChannel.channelDelegate?.onMessage(index: index, message: webMessage) } } else if message.name == "onWebMessageListenerPostMessageReceived" { let body = message.body as! [String: Any?] let jsObjectName = body["jsObjectName"] as! String - let messageData = body["message"] as? String + var webMessage: WebMessage? = nil + if let webMessageMap = body["message"] as? [String : Any?] { + webMessage = WebMessage.fromMap(map: webMessageMap) + } + if let webMessageListener = webMessageListeners.first(where: ({($0.jsObjectName == jsObjectName)})) { let isMainFrame = message.frameInfo.isMainFrame @@ -2920,7 +2928,7 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { if let scheme = scheme, !scheme.isEmpty, let host = host, !host.isEmpty { sourceOrigin = URL(string: "\(scheme)://\(host)\(port != nil && port != 0 ? ":" + String(port!) : "")") } - webMessageListener.channelDelegate?.onPostMessage(message: messageData, sourceOrigin: sourceOrigin, isMainFrame: isMainFrame) + webMessageListener.channelDelegate?.onPostMessage(message: webMessage, sourceOrigin: sourceOrigin, isMainFrame: isMainFrame) } } } @@ -3190,11 +3198,11 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { } portsString = "[" + portArrayString.joined(separator: ", ") + "]" } - let data = message.data?.replacingOccurrences(of: "\'", with: "\\'") ?? "null" + let url = URL(string: targetOrigin)?.absoluteString ?? "*" let source = """ (function() { - window.postMessage('\(data)', '\(url)', \(portsString)); + window.postMessage(\(message.jsData), '\(url)', \(portsString)); })(); """ evaluateJavascript(source: source, completionHandler: completionHandler) diff --git a/ios/Classes/InAppWebView/WebMessage/WebMessageChannel.swift b/ios/Classes/InAppWebView/WebMessage/WebMessageChannel.swift index 8110b940..96e918e2 100644 --- a/ios/Classes/InAppWebView/WebMessage/WebMessageChannel.swift +++ b/ios/Classes/InAppWebView/WebMessage/WebMessageChannel.swift @@ -25,8 +25,8 @@ public class WebMessageChannel : FlutterMethodCallDelegate { self.channelDelegate = WebMessageChannelChannelDelegate(webMessageChannel: self, channel: channel) } self.ports = [ - WebMessagePort(name: "port1", webMessageChannel: self), - WebMessagePort(name: "port2", webMessageChannel: self) + WebMessagePort(name: "port1", index: 0, webMessageChannelId: self.id, webMessageChannel: self), + WebMessagePort(name: "port2", index: 1, webMessageChannelId: self.id, webMessageChannel: self) ] } diff --git a/ios/Classes/InAppWebView/WebMessage/WebMessageChannelChannelDelegate.swift b/ios/Classes/InAppWebView/WebMessage/WebMessageChannelChannelDelegate.swift index aeefe888..c95e9a28 100644 --- a/ios/Classes/InAppWebView/WebMessage/WebMessageChannelChannelDelegate.swift +++ b/ios/Classes/InAppWebView/WebMessage/WebMessageChannelChannelDelegate.swift @@ -39,22 +39,20 @@ public class WebMessageChannelChannelDelegate : ChannelDelegate { if let webView = webMessageChannel?.webView, let ports = webMessageChannel?.ports, ports.count > 0 { let index = arguments!["index"] as! Int let port = ports[index] - let message = arguments!["message"] as! [String: Any?] + var message = WebMessage.fromMap(map: arguments!["message"] as! [String: Any?]) - var webMessagePorts: [WebMessagePort] = [] - let portsMap = message["ports"] as? [[String: Any?]] - if let portsMap = portsMap { - for portMap in portsMap { - let webMessageChannelId = portMap["webMessageChannelId"] as! String - let index = portMap["index"] as! Int - if let webMessageChannel = webView.webMessageChannels[webMessageChannelId] { - webMessagePorts.append(webMessageChannel.ports[index]) + var ports: [WebMessagePort] = [] + if let notConnectedPorts = message.ports { + for notConnectedPort in notConnectedPorts { + if let webMessageChannel = webView.webMessageChannels[notConnectedPort.webMessageChannelId] { + ports.append(webMessageChannel.ports[Int(notConnectedPort.index)]) } } } - let webMessage = WebMessage(data: message["data"] as? String, ports: webMessagePorts) + message.ports = ports + do { - try port.postMessage(message: webMessage) { (_) in + try port.postMessage(message: message) { (_) in result(true) } } catch let error as NSError { @@ -85,10 +83,10 @@ public class WebMessageChannelChannelDelegate : ChannelDelegate { } } - public func onMessage(index: Int64, message: String?) { + public func onMessage(index: Int64, message: WebMessage?) { let arguments: [String:Any?] = [ "index": index, - "message": message + "message": message?.toMap() ] channel?.invokeMethod("onMessage", arguments: arguments) } diff --git a/ios/Classes/InAppWebView/WebMessage/WebMessageListenerChannelDelegate.swift b/ios/Classes/InAppWebView/WebMessage/WebMessageListenerChannelDelegate.swift index 7872acb7..085a2282 100644 --- a/ios/Classes/InAppWebView/WebMessage/WebMessageListenerChannelDelegate.swift +++ b/ios/Classes/InAppWebView/WebMessage/WebMessageListenerChannelDelegate.swift @@ -22,12 +22,13 @@ public class WebMessageListenerChannelDelegate : ChannelDelegate { case "postMessage": if let webView = webMessageListener?.webView, let jsObjectName = webMessageListener?.jsObjectName { let jsObjectNameEscaped = jsObjectName.replacingOccurrences(of: "\'", with: "\\'") - let messageEscaped = (arguments!["message"] as! String).replacingOccurrences(of: "\'", with: "\\'") + let message = WebMessage.fromMap(map: arguments!["message"] as! [String: Any?]) + let source = """ (function() { var webMessageListener = window['\(jsObjectNameEscaped)']; if (webMessageListener != null) { - var event = {data: '\(messageEscaped)'}; + var event = {data: \(message.jsData)}; if (webMessageListener.onmessage != null) { webMessageListener.onmessage(event); } @@ -50,9 +51,9 @@ public class WebMessageListenerChannelDelegate : ChannelDelegate { } } - public func onPostMessage(message: String?, sourceOrigin: URL?, isMainFrame: Bool) { + public func onPostMessage(message: WebMessage?, sourceOrigin: URL?, isMainFrame: Bool) { let arguments: [String:Any?] = [ - "message": message, + "message": message?.toMap(), "sourceOrigin": sourceOrigin?.absoluteString, "isMainFrame": isMainFrame ] diff --git a/ios/Classes/InAppWebView/WebViewChannelDelegate.swift b/ios/Classes/InAppWebView/WebViewChannelDelegate.swift index 2ca6b080..880810f5 100644 --- a/ios/Classes/InAppWebView/WebViewChannelDelegate.swift +++ b/ios/Classes/InAppWebView/WebViewChannelDelegate.swift @@ -509,23 +509,21 @@ public class WebViewChannelDelegate : ChannelDelegate { break case .postWebMessage: if let webView = webView { - let message = arguments!["message"] as! [String: Any?] + var message = WebMessage.fromMap(map: arguments!["message"] as! [String: Any?]) let targetOrigin = arguments!["targetOrigin"] as! String var ports: [WebMessagePort] = [] - let portsMap = message["ports"] as? [[String: Any?]] - if let portsMap = portsMap { - for portMap in portsMap { - let webMessageChannelId = portMap["webMessageChannelId"] as! String - let index = portMap["index"] as! Int - if let webMessageChannel = webView.webMessageChannels[webMessageChannelId] { - ports.append(webMessageChannel.ports[index]) + if let notConnectedPorts = message.ports { + for notConnectedPort in notConnectedPorts { + if let webMessageChannel = webView.webMessageChannels[notConnectedPort.webMessageChannelId] { + ports.append(webMessageChannel.ports[Int(notConnectedPort.index)]) } } } - let webMessage = WebMessage(data: message["data"] as? String, ports: ports) + message.ports = ports + do { - try webView.postWebMessage(message: webMessage, targetOrigin: targetOrigin) { (_) in + try webView.postWebMessage(message: message, targetOrigin: targetOrigin) { (_) in result(true) } } catch let error as NSError { diff --git a/ios/Classes/PluginScriptsJS/WebMessageListenerJS.swift b/ios/Classes/PluginScriptsJS/WebMessageListenerJS.swift index 9d9fc60d..ab8edfae 100644 --- a/ios/Classes/PluginScriptsJS/WebMessageListenerJS.swift +++ b/ios/Classes/PluginScriptsJS/WebMessageListenerJS.swift @@ -13,7 +13,11 @@ function FlutterInAppWebViewWebMessageListener(jsObjectName) { this.listeners = []; this.onmessage = null; } -FlutterInAppWebViewWebMessageListener.prototype.postMessage = function(message) { +FlutterInAppWebViewWebMessageListener.prototype.postMessage = function(data) { + var message = { + "data": window.ArrayBuffer != null && data instanceof ArrayBuffer ? Array.from(new Uint8Array(data)) : (data != null ? data.toString() : null), + "type": window.ArrayBuffer != null && data instanceof ArrayBuffer ? 1 : 0 + }; window.webkit.messageHandlers['onWebMessageListenerPostMessageReceived'].postMessage({jsObjectName: this.jsObjectName, message: message}); }; FlutterInAppWebViewWebMessageListener.prototype.addEventListener = function(type, listener) { diff --git a/ios/Classes/Types/WebMessage.swift b/ios/Classes/Types/WebMessage.swift index 378bd67f..fca86ca6 100644 --- a/ios/Classes/Types/WebMessage.swift +++ b/ios/Classes/Types/WebMessage.swift @@ -7,16 +7,53 @@ import Foundation -public class WebMessage : NSObject { - var data: String? +public class WebMessage : NSObject, Disposable { + var data: Any? + var type: WebMessageType var ports: [WebMessagePort]? - public init(data: String?, ports: [WebMessagePort]?) { + var jsData: String { + var jsData: String = "null" + if let messageData = data { + if type == .arrayBuffer, let messageDataArrayBuffer = messageData as? FlutterStandardTypedData { + jsData = "new Uint8Array(\(Array(messageDataArrayBuffer.data))).buffer" + } else if let messageDataString = messageData as? String { + jsData = "'\(messageDataString.replacingOccurrences(of: "\'", with: "\\'"))'" + } + } + return jsData + } + + public init(data: Any?, type: WebMessageType, ports: [WebMessagePort]?) { + self.type = type super.init() self.data = data self.ports = ports } + public static func fromMap(map: [String: Any?]) -> WebMessage { + let portMapList = map["ports"] as? [[String: Any?]] + var ports: [WebMessagePort]? = nil + if let portMapList = portMapList, !portMapList.isEmpty { + ports = [] + portMapList.forEach { (portMap) in + ports?.append(WebMessagePort.fromMap(map: portMap)) + } + } + + return WebMessage( + data: map["data"] as? Any, + type: WebMessageType.init(rawValue: map["type"] as! Int)!, + ports: ports) + } + + public func toMap () -> [String: Any?] { + return [ + "data": type == .arrayBuffer && data is [UInt8] ? Data(data as! [UInt8]) : data, + "type": type.rawValue + ] + } + public func dispose() { ports?.removeAll() } @@ -26,3 +63,8 @@ public class WebMessage : NSObject { dispose() } } + +public enum WebMessageType: Int { + case string = 0 + case arrayBuffer = 1 +} diff --git a/ios/Classes/Types/WebMessagePort.swift b/ios/Classes/Types/WebMessagePort.swift index 9e823e92..824e9e4c 100644 --- a/ios/Classes/Types/WebMessagePort.swift +++ b/ios/Classes/Types/WebMessagePort.swift @@ -9,13 +9,17 @@ import Foundation public class WebMessagePort : NSObject { var name: String + var index: Int64 + var webMessageChannelId: String var webMessageChannel: WebMessageChannel? var isClosed = false var isTransferred = false var isStarted = false - public init(name: String, webMessageChannel: WebMessageChannel) { + public init(name: String, index: Int64, webMessageChannelId: String, webMessageChannel: WebMessageChannel?) { self.name = name + self.index = index + self.webMessageChannelId = webMessageChannelId super.init() self.webMessageChannel = webMessageChannel } @@ -35,7 +39,10 @@ public class WebMessagePort : NSObject { window.webkit.messageHandlers["onWebMessagePortMessageReceived"].postMessage({ "webMessageChannelId": "\(webMessageChannel.id)", "index": \(String(index)), - "message": event.data + "message": { + "data": window.ArrayBuffer != null && event.data instanceof ArrayBuffer ? Array.from(new Uint8Array(event.data)) : (event.data != null ? event.data.toString() : null), + "type": window.ArrayBuffer != null && event.data instanceof ArrayBuffer ? 1 : 0 + } }); } } @@ -71,12 +78,12 @@ public class WebMessagePort : NSObject { } portsString = "[" + portArrayString.joined(separator: ", ") + "]" } - let data = message.data?.replacingOccurrences(of: "\'", with: "\\'") ?? "null" + let source = """ (function() { var webMessageChannel = \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(webMessageChannel.id)"]; if (webMessageChannel != null) { - webMessageChannel.\(self.name).postMessage('\(data)', \(portsString)); + webMessageChannel.\(self.name).postMessage(\(message.jsData), \(portsString)); } })(); """ @@ -111,6 +118,23 @@ public class WebMessagePort : NSObject { } } + public static func fromMap(map: [String: Any?]) -> WebMessagePort { + let index = map["index"] as! Int64 + return WebMessagePort( + name: "port\(String(index + 1))", + index: index, + webMessageChannelId: map["webMessageChannelId"] as! String, + webMessageChannel: nil) + } + + public func toMap () -> [String: Any?] { + return [ + "name": name, + "index": index, + "webMessageChannelId": webMessageChannelId + ] + } + public func dispose() { isClosed = true webMessageChannel = nil diff --git a/lib/src/android/main.dart b/lib/src/android/main.dart index 210fcc42..3de3f67b 100644 --- a/lib/src/android/main.dart +++ b/lib/src/android/main.dart @@ -9,3 +9,8 @@ export 'webview_asset_loader.dart' ResourcesPathHandler, InternalStoragePathHandler; export 'tracing_controller.dart' show TracingController, TracingSettings; +export 'process_global_config.dart' + show + ProcessGlobalConfig, + ProcessGlobalConfigSettings, + ProcessGlobalConfigDirectoryBasePaths; diff --git a/lib/src/android/process_global_config.dart b/lib/src/android/process_global_config.dart new file mode 100644 index 00000000..9f367c54 --- /dev/null +++ b/lib/src/android/process_global_config.dart @@ -0,0 +1,150 @@ +import 'dart:async'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_internal_annotations/flutter_inappwebview_internal_annotations.dart'; +import 'webview_feature.dart'; +import '../in_app_webview/webview.dart'; +import '../in_app_webview/in_app_webview_controller.dart'; +import '../cookie_manager.dart'; + +part 'process_global_config.g.dart'; + +///Process Global Configuration for [WebView]. +///WebView has some process-global configuration parameters +///that cannot be changed once WebView has been loaded. +///This class allows apps to set these parameters. +/// +///If it is used, the configuration should be set and apply should +///be called prior to loading WebView into the calling process. +///Most of the methods in `android.webkit` and `androidx.webkit` packages load WebView, +///so the configuration should be applied before calling any of these methods. +/// +///The following code configures the data directory suffix that WebView uses and +///then applies the configuration. WebView uses this configuration when it is loaded. +/// +///[apply] can only be called once. +/// +///Only a single thread should access this class at a given time. +/// +///The configuration should be set up as early as possible during application startup, +///to ensure that it happens before any other thread can call a method that loads [WebView]. +/// +///**Supported Platforms/Implementations**: +///- Android native WebView ([Official API - ProcessGlobalConfig](https://developer.android.com/reference/androidx/webkit/ProcessGlobalConfig)) +class ProcessGlobalConfig { + static ProcessGlobalConfig? _instance; + static const MethodChannel _channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_processglobalconfig'); + + ProcessGlobalConfig._(); + + ///Gets the [ProcessGlobalConfig] shared instance. + static ProcessGlobalConfig instance() { + return (_instance != null) ? _instance! : _init(); + } + + static ProcessGlobalConfig _init() { + _channel.setMethodCallHandler((call) async { + try { + return await _handleMethod(call); + } on Error catch (e) { + print(e); + print(e.stackTrace); + } + }); + _instance = ProcessGlobalConfig._(); + return _instance!; + } + + static Future _handleMethod(MethodCall call) async { + // ProcessGlobalConfig controller = ProcessGlobalConfig.instance(); + switch (call.method) { + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + // return null; + } + + ///Applies the configuration to be used by [WebView] on loading. + ///This method can only be called once. + /// + ///Calling this method will not cause [WebView] to be loaded and will not block the calling thread. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView ([Official API - ProcessGlobalConfig.apply](https://developer.android.com/reference/androidx/webkit/ProcessGlobalConfig#apply(androidx.webkit.ProcessGlobalConfig))) + Future apply({required ProcessGlobalConfigSettings settings}) async { + Map args = {}; + args.putIfAbsent("settings", () => settings.toMap()); + return await _channel.invokeMethod('apply', args); + } +} + +///Class that represents the settings used to configure the [ProcessGlobalConfig]. +/// +///**Supported Platforms/Implementations**: +///- Android native WebView +@ExchangeableObject(copyMethod: true) +class ProcessGlobalConfigSettings_ { + ///The directory name suffix to be used for the current process. + ///Must not contain a path separator and should not be empty. + /// + ///Define the directory used to store WebView data for the current process. + ///The provided suffix will be used when constructing data and cache directory paths. + ///If this API is not called, no suffix will be used. + ///Each directory can be used by only one process in the application. + ///If more than one process in an app wishes to use WebView, + ///only one process can use the default directory, + ///and other processes must call this API to define a unique suffix. + /// + ///This means that different processes in the same application cannot directly share [WebView]-related data, + ///since the data directories must be distinct. + ///Applications that use this API may have to explicitly pass data between processes. + ///For example, login cookies may have to be copied from one process's cookie jar to the other using [CookieManager] if both processes' WebViews are intended to be logged in. + /// + ///Most applications should simply ensure that all components of the app that rely + ///on WebView are in the same process, to avoid needing multiple data directories. + ///The [InAppWebViewController.disableWebView] method can be used to ensure that the other processes do not use WebView by accident in this case. + /// + ///**NOTE**: available only if [WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX] feature is supported. + String? dataDirectorySuffix; + + ///Set the base directories that [WebView] will use for the current process. + ///If this method is not used, [WebView] uses the default base paths defined by the Android framework. + /// + ///WebView will create and use a subdirectory under each of the base paths supplied to this method. + /// + ///This method can be used in conjunction with setDataDirectorySuffix. + ///A different subdirectory is created for each suffix. + /// + ///The base paths must be absolute paths. + /// + ///The data directory must not be under the Android cache directory, + ///as Android may delete cache files when disk space is low and WebView may not function properly if this occurs. + ///Refer to [this link](https://developer.android.com/training/data-storage/app-specific#internal-remove-cache). + /// + ///If the specified directories already exist then they must be readable and writable by the current process. + ///If they do not already exist, [WebView] will attempt to create them during initialization, along with any missing parent directories. + ///In such a case, the directory in which [WebView] creates missing directories must be readable and writable by the current process. + /// + ///**NOTE**: available only if [WebViewFeature.STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS] feature is supported. + ProcessGlobalConfigDirectoryBasePaths_? directoryBasePaths; + + ProcessGlobalConfigSettings_( + {this.dataDirectorySuffix, this.directoryBasePaths}); +} + +///Class that represents the settings used to configure the [ProcessGlobalConfigSettings.directoryBasePaths]. +/// +///**Supported Platforms/Implementations**: +///- Android native WebView ([Official API - ProxyConfig](https://developer.android.com/reference/androidx/webkit/ProxyConfig)) +@ExchangeableObject() +class ProcessGlobalConfigDirectoryBasePaths_ { + ///The absolute base path for the WebView data directory. + String dataDirectoryBasePath; + + ///The absolute base path for the WebView cache directory. + String cacheDirectoryBasePath; + + ProcessGlobalConfigDirectoryBasePaths_( + {required this.dataDirectoryBasePath, + required this.cacheDirectoryBasePath}); +} diff --git a/lib/src/android/process_global_config.g.dart b/lib/src/android/process_global_config.g.dart new file mode 100644 index 00000000..ec44d74e --- /dev/null +++ b/lib/src/android/process_global_config.g.dart @@ -0,0 +1,142 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'process_global_config.dart'; + +// ************************************************************************** +// ExchangeableObjectGenerator +// ************************************************************************** + +///Class that represents the settings used to configure the [ProcessGlobalConfig]. +/// +///**Supported Platforms/Implementations**: +///- Android native WebView +class ProcessGlobalConfigSettings { + ///The directory name suffix to be used for the current process. + ///Must not contain a path separator and should not be empty. + /// + ///Define the directory used to store WebView data for the current process. + ///The provided suffix will be used when constructing data and cache directory paths. + ///If this API is not called, no suffix will be used. + ///Each directory can be used by only one process in the application. + ///If more than one process in an app wishes to use WebView, + ///only one process can use the default directory, + ///and other processes must call this API to define a unique suffix. + /// + ///This means that different processes in the same application cannot directly share [WebView]-related data, + ///since the data directories must be distinct. + ///Applications that use this API may have to explicitly pass data between processes. + ///For example, login cookies may have to be copied from one process's cookie jar to the other using [CookieManager] if both processes' WebViews are intended to be logged in. + /// + ///Most applications should simply ensure that all components of the app that rely + ///on WebView are in the same process, to avoid needing multiple data directories. + ///The [InAppWebViewController.disableWebView] method can be used to ensure that the other processes do not use WebView by accident in this case. + /// + ///**NOTE**: available only if [WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX] feature is supported. + String? dataDirectorySuffix; + + ///Set the base directories that [WebView] will use for the current process. + ///If this method is not used, [WebView] uses the default base paths defined by the Android framework. + /// + ///WebView will create and use a subdirectory under each of the base paths supplied to this method. + /// + ///This method can be used in conjunction with setDataDirectorySuffix. + ///A different subdirectory is created for each suffix. + /// + ///The base paths must be absolute paths. + /// + ///The data directory must not be under the Android cache directory, + ///as Android may delete cache files when disk space is low and WebView may not function properly if this occurs. + ///Refer to [this link](https://developer.android.com/training/data-storage/app-specific#internal-remove-cache). + /// + ///If the specified directories already exist then they must be readable and writable by the current process. + ///If they do not already exist, [WebView] will attempt to create them during initialization, along with any missing parent directories. + ///In such a case, the directory in which [WebView] creates missing directories must be readable and writable by the current process. + /// + ///**NOTE**: available only if [WebViewFeature.STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS] feature is supported. + ProcessGlobalConfigDirectoryBasePaths? directoryBasePaths; + ProcessGlobalConfigSettings( + {this.dataDirectorySuffix, this.directoryBasePaths}); + + ///Gets a possible [ProcessGlobalConfigSettings] instance from a [Map] value. + static ProcessGlobalConfigSettings? fromMap(Map? map) { + if (map == null) { + return null; + } + final instance = ProcessGlobalConfigSettings( + dataDirectorySuffix: map['dataDirectorySuffix'], + directoryBasePaths: ProcessGlobalConfigDirectoryBasePaths.fromMap( + map['directoryBasePaths']?.cast()), + ); + return instance; + } + + ///Converts instance to a map. + Map toMap() { + return { + "dataDirectorySuffix": dataDirectorySuffix, + "directoryBasePaths": directoryBasePaths?.toMap(), + }; + } + + ///Converts instance to a map. + Map toJson() { + return toMap(); + } + + ///Returns a copy of ProcessGlobalConfigSettings. + ProcessGlobalConfigSettings copy() { + return ProcessGlobalConfigSettings.fromMap(toMap()) ?? + ProcessGlobalConfigSettings(); + } + + @override + String toString() { + return 'ProcessGlobalConfigSettings{dataDirectorySuffix: $dataDirectorySuffix, directoryBasePaths: $directoryBasePaths}'; + } +} + +///Class that represents the settings used to configure the [ProcessGlobalConfigSettings.directoryBasePaths]. +/// +///**Supported Platforms/Implementations**: +///- Android native WebView ([Official API - ProxyConfig](https://developer.android.com/reference/androidx/webkit/ProxyConfig)) +class ProcessGlobalConfigDirectoryBasePaths { + ///The absolute base path for the WebView cache directory. + String cacheDirectoryBasePath; + + ///The absolute base path for the WebView data directory. + String dataDirectoryBasePath; + ProcessGlobalConfigDirectoryBasePaths( + {required this.cacheDirectoryBasePath, + required this.dataDirectoryBasePath}); + + ///Gets a possible [ProcessGlobalConfigDirectoryBasePaths] instance from a [Map] value. + static ProcessGlobalConfigDirectoryBasePaths? fromMap( + Map? map) { + if (map == null) { + return null; + } + final instance = ProcessGlobalConfigDirectoryBasePaths( + cacheDirectoryBasePath: map['cacheDirectoryBasePath'], + dataDirectoryBasePath: map['dataDirectoryBasePath'], + ); + return instance; + } + + ///Converts instance to a map. + Map toMap() { + return { + "cacheDirectoryBasePath": cacheDirectoryBasePath, + "dataDirectoryBasePath": dataDirectoryBasePath, + }; + } + + ///Converts instance to a map. + Map toJson() { + return toMap(); + } + + @override + String toString() { + return 'ProcessGlobalConfigDirectoryBasePaths{cacheDirectoryBasePath: $cacheDirectoryBasePath, dataDirectoryBasePath: $dataDirectoryBasePath}'; + } +} diff --git a/lib/src/android/proxy_controller.dart b/lib/src/android/proxy_controller.dart index 1bea3928..b2abac7c 100644 --- a/lib/src/android/proxy_controller.dart +++ b/lib/src/android/proxy_controller.dart @@ -139,41 +139,4 @@ class ProxySettings_ { this.bypassSimpleHostnames, this.removeImplicitRules, this.reverseBypassEnabled = false}); - - // Map toMap() { - // return { - // "bypassRules": bypassRules, - // "directs": directs, - // "proxyRules": proxyRules.map((e) => e.toMap()).toList(), - // "bypassSimpleHostnames": bypassSimpleHostnames, - // "removeImplicitRules": removeImplicitRules, - // "reverseBypassEnabled": reverseBypassEnabled - // }; - // } - // - // static ProxySettings fromMap(Map map) { - // var settings = ProxySettings(); - // settings.bypassRules = map["bypassRules"]; - // settings.directs = map["directs"]; - // settings.proxyRules = (map["proxyRules"].cast>() - // as List>) - // .map((e) => ProxyRule.fromMap(e)) as List; - // settings.bypassSimpleHostnames = map["bypassSimpleHostnames"]; - // settings.removeImplicitRules = map["removeImplicitRules"]; - // settings.reverseBypassEnabled = map["reverseBypassEnabled"]; - // return settings; - // } - // - // Map toJson() { - // return this.toMap(); - // } - // - // @override - // String toString() { - // return 'ProxySettings{bypassRules: $bypassRules, directs: $directs, proxyRules: $proxyRules, bypassSimpleHostnames: $bypassSimpleHostnames, removeImplicitRules: $removeImplicitRules, reverseBypassEnabled: $reverseBypassEnabled}'; - // } - // - // ProxySettings copy() { - // return ProxySettings.fromMap(this.toMap()); - // } } diff --git a/lib/src/android/webview_feature.dart b/lib/src/android/webview_feature.dart index a2d4a313..9b502643 100644 --- a/lib/src/android/webview_feature.dart +++ b/lib/src/android/webview_feature.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'package:flutter/services.dart'; -import 'package:flutter_inappwebview/src/cookie_manager.dart'; import 'package:flutter_inappwebview_internal_annotations/flutter_inappwebview_internal_annotations.dart'; +import 'process_global_config.dart'; +import '../cookie_manager.dart'; import '../in_app_webview/in_app_webview_controller.dart'; import '../in_app_webview/in_app_webview_settings.dart'; import 'proxy_controller.dart'; @@ -29,18 +30,18 @@ class WebViewFeature_ { @ExchangeableObjectMethod(ignore: true) String toNativeValue() => _value; - ///This feature covers [InAppWebViewController.createWebMessageChannel]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewController.createWebMessageChannel]. static const CREATE_WEB_MESSAGE_CHANNEL = const WebViewFeature_._internal("CREATE_WEB_MESSAGE_CHANNEL"); - ///This feature covers [InAppWebViewSettings.disabledActionModeMenuItems]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.disabledActionModeMenuItems]. static const DISABLED_ACTION_MODE_MENU_ITEMS = const WebViewFeature_._internal("DISABLED_ACTION_MODE_MENU_ITEMS"); - ///This feature covers [InAppWebViewSettings.forceDark]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.forceDark]. static const FORCE_DARK = const WebViewFeature_._internal("FORCE_DARK"); - ///This feature covers [InAppWebViewSettings.forceDarkStrategy]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.forceDarkStrategy]. static const FORCE_DARK_STRATEGY = const WebViewFeature_._internal("FORCE_DARK_STRATEGY"); @@ -59,19 +60,19 @@ class WebViewFeature_ { /// static const MULTI_PROCESS = const WebViewFeature_._internal("MULTI_PROCESS"); - ///This feature covers [InAppWebViewSettings.offscreenPreRaster]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.offscreenPreRaster]. static const OFF_SCREEN_PRERASTER = const WebViewFeature_._internal("OFF_SCREEN_PRERASTER"); - ///This feature covers [InAppWebViewController.postWebMessage]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewController.postWebMessage]. static const POST_WEB_MESSAGE = const WebViewFeature_._internal("POST_WEB_MESSAGE"); - ///This feature covers [ProxyController.setProxyOverride] and [ProxyController.clearProxyOverride]. + ///Feature for [isFeatureSupported]. This feature covers [ProxyController.setProxyOverride] and [ProxyController.clearProxyOverride]. static const PROXY_OVERRIDE = const WebViewFeature_._internal("PROXY_OVERRIDE"); - ///This feature covers [ProxySettings.reverseBypassEnabled]. + ///Feature for [isFeatureSupported]. This feature covers [ProxySettings.reverseBypassEnabled]. static const PROXY_OVERRIDE_REVERSE_BYPASS = const WebViewFeature_._internal("PROXY_OVERRIDE_REVERSE_BYPASS"); @@ -83,11 +84,11 @@ class WebViewFeature_ { static const RECEIVE_WEB_RESOURCE_ERROR = const WebViewFeature_._internal("RECEIVE_WEB_RESOURCE_ERROR"); - ///This feature covers [InAppWebViewController.setSafeBrowsingAllowlist]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewController.setSafeBrowsingAllowlist]. static const SAFE_BROWSING_ALLOWLIST = const WebViewFeature_._internal("SAFE_BROWSING_ALLOWLIST"); - ///This feature covers [InAppWebViewSettings.safeBrowsingEnabled]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.safeBrowsingEnabled]. static const SAFE_BROWSING_ENABLE = const WebViewFeature_._internal("SAFE_BROWSING_ENABLE"); @@ -95,7 +96,7 @@ class WebViewFeature_ { static const SAFE_BROWSING_HIT = const WebViewFeature_._internal("SAFE_BROWSING_HIT"); - ///This feature covers [InAppWebViewController.getSafeBrowsingPrivacyPolicyUrl]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewController.getSafeBrowsingPrivacyPolicyUrl]. static const SAFE_BROWSING_PRIVACY_POLICY_URL = const WebViewFeature_._internal("SAFE_BROWSING_PRIVACY_POLICY_URL"); @@ -117,27 +118,27 @@ class WebViewFeature_ { static const SAFE_BROWSING_WHITELIST = const WebViewFeature_._internal("SAFE_BROWSING_WHITELIST"); - ///This feature covers [ServiceWorkerController]. + ///Feature for [isFeatureSupported]. This feature covers [ServiceWorkerController]. static const SERVICE_WORKER_BASIC_USAGE = const WebViewFeature_._internal("SERVICE_WORKER_BASIC_USAGE"); - ///This feature covers [ServiceWorkerController.setBlockNetworkLoads] and [ServiceWorkerController.getBlockNetworkLoads]. + ///Feature for [isFeatureSupported]. This feature covers [ServiceWorkerController.setBlockNetworkLoads] and [ServiceWorkerController.getBlockNetworkLoads]. static const SERVICE_WORKER_BLOCK_NETWORK_LOADS = const WebViewFeature_._internal("SERVICE_WORKER_BLOCK_NETWORK_LOADS"); - ///This feature covers [ServiceWorkerController.setCacheMode] and [ServiceWorkerController.getCacheMode]. + ///Feature for [isFeatureSupported]. This feature covers [ServiceWorkerController.setCacheMode] and [ServiceWorkerController.getCacheMode]. static const SERVICE_WORKER_CACHE_MODE = const WebViewFeature_._internal("SERVICE_WORKER_CACHE_MODE"); - ///This feature covers [ServiceWorkerController.setAllowContentAccess] and [ServiceWorkerController.getAllowContentAccess]. + ///Feature for [isFeatureSupported]. This feature covers [ServiceWorkerController.setAllowContentAccess] and [ServiceWorkerController.getAllowContentAccess]. static const SERVICE_WORKER_CONTENT_ACCESS = const WebViewFeature_._internal("SERVICE_WORKER_CONTENT_ACCESS"); - ///This feature covers [ServiceWorkerController.setAllowFileAccess] and [ServiceWorkerController.getAllowFileAccess]. + ///Feature for [isFeatureSupported]. This feature covers [ServiceWorkerController.setAllowFileAccess] and [ServiceWorkerController.getAllowFileAccess]. static const SERVICE_WORKER_FILE_ACCESS = const WebViewFeature_._internal("SERVICE_WORKER_FILE_ACCESS"); - ///This feature covers [ServiceWorkerClient.shouldInterceptRequest]. + ///Feature for [isFeatureSupported]. This feature covers [ServiceWorkerClient.shouldInterceptRequest]. static const SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST = const WebViewFeature_._internal( "SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST"); @@ -146,7 +147,7 @@ class WebViewFeature_ { static const SHOULD_OVERRIDE_WITH_REDIRECTS = const WebViewFeature_._internal("SHOULD_OVERRIDE_WITH_REDIRECTS"); - ///This feature covers [InAppWebViewController.startSafeBrowsing]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewController.startSafeBrowsing]. static const START_SAFE_BROWSING = const WebViewFeature_._internal("START_SAFE_BROWSING"); @@ -162,7 +163,7 @@ class WebViewFeature_ { static const WEB_MESSAGE_CALLBACK_ON_MESSAGE = const WebViewFeature_._internal("WEB_MESSAGE_CALLBACK_ON_MESSAGE"); - ///This feature covers [WebMessageListener]. + ///Feature for [isFeatureSupported]. This feature covers [WebMessageListener]. static const WEB_MESSAGE_LISTENER = const WebViewFeature_._internal("WEB_MESSAGE_LISTENER"); @@ -198,45 +199,90 @@ class WebViewFeature_ { static const WEB_VIEW_RENDERER_TERMINATE = const WebViewFeature_._internal("WEB_VIEW_RENDERER_TERMINATE"); - ///This feature covers [UserScriptInjectionTime.AT_DOCUMENT_START]. + ///Feature for [isFeatureSupported]. This feature covers [UserScriptInjectionTime.AT_DOCUMENT_START]. static const DOCUMENT_START_SCRIPT = const WebViewFeature_._internal("DOCUMENT_START_SCRIPT"); - ///This feature covers [InAppWebViewSettings.willSuppressErrorPage]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.willSuppressErrorPage]. static const SUPPRESS_ERROR_PAGE = const WebViewFeature_._internal("SUPPRESS_ERROR_PAGE"); - ///This feature covers [InAppWebViewSettings.algorithmicDarkeningAllowed]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.algorithmicDarkeningAllowed]. static const ALGORITHMIC_DARKENING = const WebViewFeature_._internal("ALGORITHMIC_DARKENING"); - ///This feature covers [InAppWebViewSettings.enterpriseAuthenticationAppLinkPolicyEnabled]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.enterpriseAuthenticationAppLinkPolicyEnabled]. static const ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY = const WebViewFeature_._internal( "ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY"); - ///This feature covers [InAppWebViewController.getVariationsHeader]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewController.getVariationsHeader]. static const GET_VARIATIONS_HEADER = const WebViewFeature_._internal("GET_VARIATIONS_HEADER"); - ///This feature covers cookie attributes of [CookieManager.getCookie] and [CookieManager.getCookies] methods. + ///Feature for [isFeatureSupported]. This feature covers cookie attributes of [CookieManager.getCookie] and [CookieManager.getCookies] methods. static const GET_COOKIE_INFO = const WebViewFeature_._internal("GET_COOKIE_INFO"); - ///This feature covers cookie attributes of [CookieManager.getCookie] and [CookieManager.getCookies] methods. + ///Feature for [isFeatureSupported]. This feature covers cookie attributes of [CookieManager.getCookie] and [CookieManager.getCookies] methods. static const REQUESTED_WITH_HEADER_ALLOW_LIST = const WebViewFeature_._internal("REQUESTED_WITH_HEADER_ALLOW_LIST"); + ///Feature for [isFeatureSupported]. This feature covers [WebMessagePort.postMessage] with `ArrayBuffer` type, + ///[InAppWebViewController.postWebMessage] with `ArrayBuffer` type, and [JavaScriptReplyProxy.postMessage] with `ArrayBuffer` type. + static const WEB_MESSAGE_ARRAY_BUFFER = + const WebViewFeature_._internal("WEB_MESSAGE_ARRAY_BUFFER"); + + ///Feature for [isStartupFeatureSupported]. This feature covers [ProcessGlobalConfigSettings.dataDirectorySuffix]. + static const STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX = + const WebViewFeature_._internal( + "STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX"); + + ///Feature for [isStartupFeatureSupported]. This feature covers [ProcessGlobalConfigSettings.directoryBasePaths]. + static const STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS = + const WebViewFeature_._internal( + "STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS"); + ///Return whether a feature is supported at run-time. On devices running Android version `Build.VERSION_CODES.LOLLIPOP` and higher, ///this will check whether a feature is supported, depending on the combination of the desired feature, the Android version of device, ///and the WebView APK on the device. If running on a device with a lower API level, this will always return `false`. /// + ///**Note**: This method is different from [isStartupFeatureSupported] and this + ///method only accepts certain features. + ///Please verify that the correct feature checking method is used for a particular feature. + /// + ///**Note**: If this method returns `false`, it is not safe to invoke the methods + ///requiring the desired feature. + ///Furthermore, if this method returns `false` for a particular feature, any callback guarded by that feature will not be invoked. + /// ///**Official Android API**: https://developer.android.com/reference/androidx/webkit/WebViewFeature#isFeatureSupported(java.lang.String) static Future isFeatureSupported(WebViewFeature_ feature) async { Map args = {}; args.putIfAbsent("feature", () => feature.toNativeValue()); return await _channel.invokeMethod('isFeatureSupported', args); } + + ///Return whether a startup feature is supported at run-time. + ///On devices running Android version `Build.VERSION_CODES.LOLLIPOP` and higher, + ///this will check whether a startup feature is supported, + ///depending on the combination of the desired feature, + ///the Android version of device, and the WebView APK on the device. + ///If running on a device with a lower API level, this will always return `false`. + /// + ///**Note**: This method is different from [isFeatureSupported] and this method only accepts startup features. + ///Please verify that the correct feature checking method is used for a particular feature. + /// + ///**Note**: If this method returns `false`, it is not safe to invoke the methods requiring the desired feature. + ///Furthermore, if this method returns `false` for a particular feature, + ///any callback guarded by that feature will not be invoked. + /// + ///**Official Android API**: https://developer.android.com/reference/androidx/webkit/WebViewFeature#isFeatureSupported(java.lang.String) + static Future isStartupFeatureSupported( + WebViewFeature_ startupFeature) async { + Map args = {}; + args.putIfAbsent("startupFeature", () => startupFeature.toNativeValue()); + return await _channel.invokeMethod('isStartupFeatureSupported', args); + } } ///Class that represents an Android-specific utility class for checking which WebView Support Library features are supported on the device. @@ -430,23 +476,23 @@ class AndroidWebViewFeature_ { static const WEB_VIEW_RENDERER_TERMINATE = const AndroidWebViewFeature_._internal("WEB_VIEW_RENDERER_TERMINATE"); - ///This feature covers [UserScriptInjectionTime.AT_DOCUMENT_START]. + ///Feature for [isFeatureSupported]. This feature covers [UserScriptInjectionTime.AT_DOCUMENT_START]. static const DOCUMENT_START_SCRIPT = const AndroidWebViewFeature_._internal("DOCUMENT_START_SCRIPT"); - ///This feature covers [InAppWebViewSettings.willSuppressErrorPage]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.willSuppressErrorPage]. static const SUPPRESS_ERROR_PAGE = const AndroidWebViewFeature_._internal("SUPPRESS_ERROR_PAGE"); - ///This feature covers [InAppWebViewSettings.algorithmicDarkeningAllowed]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.algorithmicDarkeningAllowed]. static const ALGORITHMIC_DARKENING = const AndroidWebViewFeature_._internal("ALGORITHMIC_DARKENING"); - ///This feature covers [InAppWebViewSettings.requestedWithHeaderMode]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.requestedWithHeaderMode]. static const REQUESTED_WITH_HEADER_CONTROL = const AndroidWebViewFeature_._internal("REQUESTED_WITH_HEADER_CONTROL"); - ///This feature covers [InAppWebViewSettings.enterpriseAuthenticationAppLinkPolicyEnabled]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.enterpriseAuthenticationAppLinkPolicyEnabled]. static const ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY = const AndroidWebViewFeature_._internal( "ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY"); diff --git a/lib/src/android/webview_feature.g.dart b/lib/src/android/webview_feature.g.dart index aa330d1c..84cfe09c 100644 --- a/lib/src/android/webview_feature.g.dart +++ b/lib/src/android/webview_feature.g.dart @@ -19,40 +19,40 @@ class WebViewFeature { String value, Function nativeValue) => WebViewFeature._internal(value, nativeValue()); - ///This feature covers [InAppWebViewSettings.algorithmicDarkeningAllowed]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.algorithmicDarkeningAllowed]. static const ALGORITHMIC_DARKENING = WebViewFeature._internal( 'ALGORITHMIC_DARKENING', 'ALGORITHMIC_DARKENING'); - ///This feature covers [InAppWebViewController.createWebMessageChannel]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewController.createWebMessageChannel]. static const CREATE_WEB_MESSAGE_CHANNEL = WebViewFeature._internal( 'CREATE_WEB_MESSAGE_CHANNEL', 'CREATE_WEB_MESSAGE_CHANNEL'); - ///This feature covers [InAppWebViewSettings.disabledActionModeMenuItems]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.disabledActionModeMenuItems]. static const DISABLED_ACTION_MODE_MENU_ITEMS = WebViewFeature._internal( 'DISABLED_ACTION_MODE_MENU_ITEMS', 'DISABLED_ACTION_MODE_MENU_ITEMS'); - ///This feature covers [UserScriptInjectionTime.AT_DOCUMENT_START]. + ///Feature for [isFeatureSupported]. This feature covers [UserScriptInjectionTime.AT_DOCUMENT_START]. static const DOCUMENT_START_SCRIPT = WebViewFeature._internal( 'DOCUMENT_START_SCRIPT', 'DOCUMENT_START_SCRIPT'); - ///This feature covers [InAppWebViewSettings.enterpriseAuthenticationAppLinkPolicyEnabled]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.enterpriseAuthenticationAppLinkPolicyEnabled]. static const ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY = WebViewFeature._internal('ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY', 'ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY'); - ///This feature covers [InAppWebViewSettings.forceDark]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.forceDark]. static const FORCE_DARK = WebViewFeature._internal('FORCE_DARK', 'FORCE_DARK'); - ///This feature covers [InAppWebViewSettings.forceDarkStrategy]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.forceDarkStrategy]. static const FORCE_DARK_STRATEGY = WebViewFeature._internal('FORCE_DARK_STRATEGY', 'FORCE_DARK_STRATEGY'); - ///This feature covers cookie attributes of [CookieManager.getCookie] and [CookieManager.getCookies] methods. + ///Feature for [isFeatureSupported]. This feature covers cookie attributes of [CookieManager.getCookie] and [CookieManager.getCookies] methods. static const GET_COOKIE_INFO = WebViewFeature._internal('GET_COOKIE_INFO', 'GET_COOKIE_INFO'); - ///This feature covers [InAppWebViewController.getVariationsHeader]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewController.getVariationsHeader]. static const GET_VARIATIONS_HEADER = WebViewFeature._internal( 'GET_VARIATIONS_HEADER', 'GET_VARIATIONS_HEADER'); @@ -72,19 +72,19 @@ class WebViewFeature { static const MULTI_PROCESS = WebViewFeature._internal('MULTI_PROCESS', 'MULTI_PROCESS'); - ///This feature covers [InAppWebViewSettings.offscreenPreRaster]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.offscreenPreRaster]. static const OFF_SCREEN_PRERASTER = WebViewFeature._internal('OFF_SCREEN_PRERASTER', 'OFF_SCREEN_PRERASTER'); - ///This feature covers [InAppWebViewController.postWebMessage]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewController.postWebMessage]. static const POST_WEB_MESSAGE = WebViewFeature._internal('POST_WEB_MESSAGE', 'POST_WEB_MESSAGE'); - ///This feature covers [ProxyController.setProxyOverride] and [ProxyController.clearProxyOverride]. + ///Feature for [isFeatureSupported]. This feature covers [ProxyController.setProxyOverride] and [ProxyController.clearProxyOverride]. static const PROXY_OVERRIDE = WebViewFeature._internal('PROXY_OVERRIDE', 'PROXY_OVERRIDE'); - ///This feature covers [ProxySettings.reverseBypassEnabled]. + ///Feature for [isFeatureSupported]. This feature covers [ProxySettings.reverseBypassEnabled]. static const PROXY_OVERRIDE_REVERSE_BYPASS = WebViewFeature._internal( 'PROXY_OVERRIDE_REVERSE_BYPASS', 'PROXY_OVERRIDE_REVERSE_BYPASS'); @@ -96,15 +96,15 @@ class WebViewFeature { static const RECEIVE_WEB_RESOURCE_ERROR = WebViewFeature._internal( 'RECEIVE_WEB_RESOURCE_ERROR', 'RECEIVE_WEB_RESOURCE_ERROR'); - ///This feature covers cookie attributes of [CookieManager.getCookie] and [CookieManager.getCookies] methods. + ///Feature for [isFeatureSupported]. This feature covers cookie attributes of [CookieManager.getCookie] and [CookieManager.getCookies] methods. static const REQUESTED_WITH_HEADER_ALLOW_LIST = WebViewFeature._internal( 'REQUESTED_WITH_HEADER_ALLOW_LIST', 'REQUESTED_WITH_HEADER_ALLOW_LIST'); - ///This feature covers [InAppWebViewController.setSafeBrowsingAllowlist]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewController.setSafeBrowsingAllowlist]. static const SAFE_BROWSING_ALLOWLIST = WebViewFeature._internal( 'SAFE_BROWSING_ALLOWLIST', 'SAFE_BROWSING_ALLOWLIST'); - ///This feature covers [InAppWebViewSettings.safeBrowsingEnabled]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.safeBrowsingEnabled]. static const SAFE_BROWSING_ENABLE = WebViewFeature._internal('SAFE_BROWSING_ENABLE', 'SAFE_BROWSING_ENABLE'); @@ -112,7 +112,7 @@ class WebViewFeature { static const SAFE_BROWSING_HIT = WebViewFeature._internal('SAFE_BROWSING_HIT', 'SAFE_BROWSING_HIT'); - ///This feature covers [InAppWebViewController.getSafeBrowsingPrivacyPolicyUrl]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewController.getSafeBrowsingPrivacyPolicyUrl]. static const SAFE_BROWSING_PRIVACY_POLICY_URL = WebViewFeature._internal( 'SAFE_BROWSING_PRIVACY_POLICY_URL', 'SAFE_BROWSING_PRIVACY_POLICY_URL'); @@ -134,28 +134,28 @@ class WebViewFeature { static const SAFE_BROWSING_WHITELIST = WebViewFeature._internal( 'SAFE_BROWSING_WHITELIST', 'SAFE_BROWSING_WHITELIST'); - ///This feature covers [ServiceWorkerController]. + ///Feature for [isFeatureSupported]. This feature covers [ServiceWorkerController]. static const SERVICE_WORKER_BASIC_USAGE = WebViewFeature._internal( 'SERVICE_WORKER_BASIC_USAGE', 'SERVICE_WORKER_BASIC_USAGE'); - ///This feature covers [ServiceWorkerController.setBlockNetworkLoads] and [ServiceWorkerController.getBlockNetworkLoads]. + ///Feature for [isFeatureSupported]. This feature covers [ServiceWorkerController.setBlockNetworkLoads] and [ServiceWorkerController.getBlockNetworkLoads]. static const SERVICE_WORKER_BLOCK_NETWORK_LOADS = WebViewFeature._internal( 'SERVICE_WORKER_BLOCK_NETWORK_LOADS', 'SERVICE_WORKER_BLOCK_NETWORK_LOADS'); - ///This feature covers [ServiceWorkerController.setCacheMode] and [ServiceWorkerController.getCacheMode]. + ///Feature for [isFeatureSupported]. This feature covers [ServiceWorkerController.setCacheMode] and [ServiceWorkerController.getCacheMode]. static const SERVICE_WORKER_CACHE_MODE = WebViewFeature._internal( 'SERVICE_WORKER_CACHE_MODE', 'SERVICE_WORKER_CACHE_MODE'); - ///This feature covers [ServiceWorkerController.setAllowContentAccess] and [ServiceWorkerController.getAllowContentAccess]. + ///Feature for [isFeatureSupported]. This feature covers [ServiceWorkerController.setAllowContentAccess] and [ServiceWorkerController.getAllowContentAccess]. static const SERVICE_WORKER_CONTENT_ACCESS = WebViewFeature._internal( 'SERVICE_WORKER_CONTENT_ACCESS', 'SERVICE_WORKER_CONTENT_ACCESS'); - ///This feature covers [ServiceWorkerController.setAllowFileAccess] and [ServiceWorkerController.getAllowFileAccess]. + ///Feature for [isFeatureSupported]. This feature covers [ServiceWorkerController.setAllowFileAccess] and [ServiceWorkerController.getAllowFileAccess]. static const SERVICE_WORKER_FILE_ACCESS = WebViewFeature._internal( 'SERVICE_WORKER_FILE_ACCESS', 'SERVICE_WORKER_FILE_ACCESS'); - ///This feature covers [ServiceWorkerClient.shouldInterceptRequest]. + ///Feature for [isFeatureSupported]. This feature covers [ServiceWorkerClient.shouldInterceptRequest]. static const SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST = WebViewFeature._internal('SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST', 'SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST'); @@ -164,11 +164,21 @@ class WebViewFeature { static const SHOULD_OVERRIDE_WITH_REDIRECTS = WebViewFeature._internal( 'SHOULD_OVERRIDE_WITH_REDIRECTS', 'SHOULD_OVERRIDE_WITH_REDIRECTS'); - ///This feature covers [InAppWebViewController.startSafeBrowsing]. + ///Feature for [isStartupFeatureSupported]. This feature covers [ProcessGlobalConfigSettings.dataDirectorySuffix]. + static const STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX = + WebViewFeature._internal('STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX', + 'STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX'); + + ///Feature for [isStartupFeatureSupported]. This feature covers [ProcessGlobalConfigSettings.directoryBasePaths]. + static const STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS = + WebViewFeature._internal('STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS', + 'STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS'); + + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewController.startSafeBrowsing]. static const START_SAFE_BROWSING = WebViewFeature._internal('START_SAFE_BROWSING', 'START_SAFE_BROWSING'); - ///This feature covers [InAppWebViewSettings.willSuppressErrorPage]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.willSuppressErrorPage]. static const SUPPRESS_ERROR_PAGE = WebViewFeature._internal('SUPPRESS_ERROR_PAGE', 'SUPPRESS_ERROR_PAGE'); @@ -180,11 +190,16 @@ class WebViewFeature { static const VISUAL_STATE_CALLBACK = WebViewFeature._internal( 'VISUAL_STATE_CALLBACK', 'VISUAL_STATE_CALLBACK'); + ///Feature for [isFeatureSupported]. This feature covers [WebMessagePort.postMessage] with `ArrayBuffer` type, + ///[InAppWebViewController.postWebMessage] with `ArrayBuffer` type, and [JavaScriptReplyProxy.postMessage] with `ArrayBuffer` type. + static const WEB_MESSAGE_ARRAY_BUFFER = WebViewFeature._internal( + 'WEB_MESSAGE_ARRAY_BUFFER', 'WEB_MESSAGE_ARRAY_BUFFER'); + /// static const WEB_MESSAGE_CALLBACK_ON_MESSAGE = WebViewFeature._internal( 'WEB_MESSAGE_CALLBACK_ON_MESSAGE', 'WEB_MESSAGE_CALLBACK_ON_MESSAGE'); - ///This feature covers [WebMessageListener]. + ///Feature for [isFeatureSupported]. This feature covers [WebMessageListener]. static const WEB_MESSAGE_LISTENER = WebViewFeature._internal('WEB_MESSAGE_LISTENER', 'WEB_MESSAGE_LISTENER'); @@ -262,10 +277,13 @@ class WebViewFeature { WebViewFeature.SERVICE_WORKER_FILE_ACCESS, WebViewFeature.SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST, WebViewFeature.SHOULD_OVERRIDE_WITH_REDIRECTS, + WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX, + WebViewFeature.STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS, WebViewFeature.START_SAFE_BROWSING, WebViewFeature.SUPPRESS_ERROR_PAGE, WebViewFeature.TRACING_CONTROLLER_BASIC_USAGE, WebViewFeature.VISUAL_STATE_CALLBACK, + WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER, WebViewFeature.WEB_MESSAGE_CALLBACK_ON_MESSAGE, WebViewFeature.WEB_MESSAGE_LISTENER, WebViewFeature.WEB_MESSAGE_PORT_CLOSE, @@ -308,6 +326,14 @@ class WebViewFeature { ///this will check whether a feature is supported, depending on the combination of the desired feature, the Android version of device, ///and the WebView APK on the device. If running on a device with a lower API level, this will always return `false`. /// + ///**Note**: This method is different from [isStartupFeatureSupported] and this + ///method only accepts certain features. + ///Please verify that the correct feature checking method is used for a particular feature. + /// + ///**Note**: If this method returns `false`, it is not safe to invoke the methods + ///requiring the desired feature. + ///Furthermore, if this method returns `false` for a particular feature, any callback guarded by that feature will not be invoked. + /// ///**Official Android API**: https://developer.android.com/reference/androidx/webkit/WebViewFeature#isFeatureSupported(java.lang.String) static Future isFeatureSupported(WebViewFeature feature) async { Map args = {}; @@ -315,6 +341,28 @@ class WebViewFeature { return await _channel.invokeMethod('isFeatureSupported', args); } + ///Return whether a startup feature is supported at run-time. + ///On devices running Android version `Build.VERSION_CODES.LOLLIPOP` and higher, + ///this will check whether a startup feature is supported, + ///depending on the combination of the desired feature, + ///the Android version of device, and the WebView APK on the device. + ///If running on a device with a lower API level, this will always return `false`. + /// + ///**Note**: This method is different from [isFeatureSupported] and this method only accepts startup features. + ///Please verify that the correct feature checking method is used for a particular feature. + /// + ///**Note**: If this method returns `false`, it is not safe to invoke the methods requiring the desired feature. + ///Furthermore, if this method returns `false` for a particular feature, + ///any callback guarded by that feature will not be invoked. + /// + ///**Official Android API**: https://developer.android.com/reference/androidx/webkit/WebViewFeature#isFeatureSupported(java.lang.String) + static Future isStartupFeatureSupported( + WebViewFeature startupFeature) async { + Map args = {}; + args.putIfAbsent("startupFeature", () => startupFeature.toNativeValue()); + return await _channel.invokeMethod('isStartupFeatureSupported', args); + } + ///Gets [String] value. String toValue() => _value; @@ -345,7 +393,7 @@ class AndroidWebViewFeature { String value, Function nativeValue) => AndroidWebViewFeature._internal(value, nativeValue()); - ///This feature covers [InAppWebViewSettings.algorithmicDarkeningAllowed]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.algorithmicDarkeningAllowed]. static const ALGORITHMIC_DARKENING = AndroidWebViewFeature._internal( 'ALGORITHMIC_DARKENING', 'ALGORITHMIC_DARKENING'); @@ -358,11 +406,11 @@ class AndroidWebViewFeature { AndroidWebViewFeature._internal( 'DISABLED_ACTION_MODE_MENU_ITEMS', 'DISABLED_ACTION_MODE_MENU_ITEMS'); - ///This feature covers [UserScriptInjectionTime.AT_DOCUMENT_START]. + ///Feature for [isFeatureSupported]. This feature covers [UserScriptInjectionTime.AT_DOCUMENT_START]. static const DOCUMENT_START_SCRIPT = AndroidWebViewFeature._internal( 'DOCUMENT_START_SCRIPT', 'DOCUMENT_START_SCRIPT'); - ///This feature covers [InAppWebViewSettings.enterpriseAuthenticationAppLinkPolicyEnabled]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.enterpriseAuthenticationAppLinkPolicyEnabled]. static const ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY = AndroidWebViewFeature._internal( 'ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY', @@ -412,7 +460,7 @@ class AndroidWebViewFeature { static const RECEIVE_WEB_RESOURCE_ERROR = AndroidWebViewFeature._internal( 'RECEIVE_WEB_RESOURCE_ERROR', 'RECEIVE_WEB_RESOURCE_ERROR'); - ///This feature covers [InAppWebViewSettings.requestedWithHeaderMode]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.requestedWithHeaderMode]. static const REQUESTED_WITH_HEADER_CONTROL = AndroidWebViewFeature._internal( 'REQUESTED_WITH_HEADER_CONTROL', 'REQUESTED_WITH_HEADER_CONTROL'); @@ -486,7 +534,7 @@ class AndroidWebViewFeature { static const START_SAFE_BROWSING = AndroidWebViewFeature._internal( 'START_SAFE_BROWSING', 'START_SAFE_BROWSING'); - ///This feature covers [InAppWebViewSettings.willSuppressErrorPage]. + ///Feature for [isFeatureSupported]. This feature covers [InAppWebViewSettings.willSuppressErrorPage]. static const SUPPRESS_ERROR_PAGE = AndroidWebViewFeature._internal( 'SUPPRESS_ERROR_PAGE', 'SUPPRESS_ERROR_PAGE'); diff --git a/lib/src/in_app_webview/in_app_webview_controller.dart b/lib/src/in_app_webview/in_app_webview_controller.dart index 9b9c56fa..9a55d315 100644 --- a/lib/src/in_app_webview/in_app_webview_controller.dart +++ b/lib/src/in_app_webview/in_app_webview_controller.dart @@ -30,6 +30,7 @@ import 'in_app_webview_keep_alive.dart'; import '../print_job/main.dart'; import '../find_interaction/main.dart'; +import '../android/process_global_config.dart'; ///List of forbidden names for JavaScript handlers. // ignore: non_constant_identifier_names @@ -3870,6 +3871,24 @@ class InAppWebViewController extends ChannelController { false; } + ///Indicate that the current process does not intend to use WebView, + ///and that an exception should be thrown if a WebView is created or any other + ///methods in the `android.webkit` package are used. + /// + ///Applications with multiple processes may wish to call this in processes that + ///are not intended to use WebView to avoid accidentally incurring the memory usage + ///of initializing WebView in long-lived processes that have no need for it, + ///and to prevent potential data directory conflicts (see [ProcessGlobalConfigSettings.dataDirectorySuffix]). + /// + ///**NOTE for Android**: available only on Android 28+. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView ([Official API - WebView.disableWebView](https://developer.android.com/reference/android/webkit/WebView.html#disableWebView())) + static Future disableWebView() async { + Map args = {}; + await _staticChannel.invokeMethod('disableWebView', args); + } + ///Returns a Boolean value that indicates whether WebKit natively supports resources with the specified URL scheme. /// ///[urlScheme] represents the URL scheme associated with the resource. diff --git a/lib/src/types/on_post_message_callback.dart b/lib/src/types/on_post_message_callback.dart index f52512dd..de8b2ccb 100644 --- a/lib/src/types/on_post_message_callback.dart +++ b/lib/src/types/on_post_message_callback.dart @@ -2,5 +2,5 @@ import '../web_message/main.dart'; import '../web_uri.dart'; ///The listener for handling [WebMessageListener] events sent by a `postMessage()` on the injected JavaScript object. -typedef void OnPostMessageCallback(String? message, WebUri? sourceOrigin, +typedef void OnPostMessageCallback(WebMessage? message, WebUri? sourceOrigin, bool isMainFrame, JavaScriptReplyProxy replyProxy); diff --git a/lib/src/types/web_message_callback.dart b/lib/src/types/web_message_callback.dart index cc1a1afa..c7688a91 100644 --- a/lib/src/types/web_message_callback.dart +++ b/lib/src/types/web_message_callback.dart @@ -2,4 +2,4 @@ import '../web_message/main.dart'; ///The listener for handling [WebMessagePort] events. ///The message callback methods are called on the main thread. -typedef void WebMessageCallback(String? message); +typedef void WebMessageCallback(WebMessage? message); diff --git a/lib/src/web_message/main.dart b/lib/src/web_message/main.dart index f8d6852a..f09e957f 100644 --- a/lib/src/web_message/main.dart +++ b/lib/src/web_message/main.dart @@ -1,4 +1,4 @@ -export 'web_message.dart' show WebMessage; +export 'web_message.dart' show WebMessage, WebMessageType; export 'web_message_port.dart' show WebMessagePort; export 'web_message_channel.dart' show WebMessageChannel; export 'web_message_listener.dart'; diff --git a/lib/src/web_message/web_message.dart b/lib/src/web_message/web_message.dart index 52fc4603..58d2d061 100644 --- a/lib/src/web_message/web_message.dart +++ b/lib/src/web_message/web_message.dart @@ -1,18 +1,47 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter_inappwebview_internal_annotations/flutter_inappwebview_internal_annotations.dart'; +import '../android/webview_feature.dart'; import 'web_message_port.dart'; part 'web_message.g.dart'; ///The Dart representation of the HTML5 PostMessage event. ///See https://html.spec.whatwg.org/multipage/comms.html#the-messageevent-interfaces for definition of a MessageEvent in HTML5. -@ExchangeableObject() +@ExchangeableObject(fromMapForceAllInline: true) class WebMessage_ { ///The data of the message. - String? data; + dynamic data; + + ///The payload type of the message. + WebMessageType_ type; ///The ports that are sent with the message. List? ports; - WebMessage_({this.data, this.ports}); + @ExchangeableObjectConstructor() + WebMessage_({this.data, this.type = WebMessageType_.STRING, this.ports}) { + assert(((this.data == null || this.data is String) && + this.type == WebMessageType_.STRING) || + (this.data != null && + this.data is Uint8List && + this.type == WebMessageType_.ARRAY_BUFFER)); + } +} + +///The type corresponding to the [WebMessage]. +@ExchangeableEnum() +class WebMessageType_ { + // ignore: unused_field + final int _value; + + const WebMessageType_._internal(this._value); + + ///Indicates the payload of WebMessageCompat is String. + static const STRING = const WebMessageType_._internal(0); + + ///Indicates the payload of WebMessageCompat is JavaScript ArrayBuffer. + /// + ///**NOTE**: available only if [WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER] feature is supported. + static const ARRAY_BUFFER = const WebMessageType_._internal(1); } diff --git a/lib/src/web_message/web_message.g.dart b/lib/src/web_message/web_message.g.dart index 5e2c41b7..e7dc0c58 100644 --- a/lib/src/web_message/web_message.g.dart +++ b/lib/src/web_message/web_message.g.dart @@ -2,6 +2,84 @@ part of 'web_message.dart'; +// ************************************************************************** +// ExchangeableEnumGenerator +// ************************************************************************** + +///The type corresponding to the [WebMessage]. +class WebMessageType { + final int _value; + final int _nativeValue; + const WebMessageType._internal(this._value, this._nativeValue); +// ignore: unused_element + factory WebMessageType._internalMultiPlatform( + int value, Function nativeValue) => + WebMessageType._internal(value, nativeValue()); + + ///Indicates the payload of WebMessageCompat is JavaScript ArrayBuffer. + /// + ///**NOTE**: available only if [WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER] feature is supported. + static const ARRAY_BUFFER = WebMessageType._internal(1, 1); + + ///Indicates the payload of WebMessageCompat is String. + static const STRING = WebMessageType._internal(0, 0); + + ///Set of all values of [WebMessageType]. + static final Set values = [ + WebMessageType.ARRAY_BUFFER, + WebMessageType.STRING, + ].toSet(); + + ///Gets a possible [WebMessageType] instance from [int] value. + static WebMessageType? fromValue(int? value) { + if (value != null) { + try { + return WebMessageType.values + .firstWhere((element) => element.toValue() == value); + } catch (e) { + return null; + } + } + return null; + } + + ///Gets a possible [WebMessageType] instance from a native value. + static WebMessageType? fromNativeValue(int? value) { + if (value != null) { + try { + return WebMessageType.values + .firstWhere((element) => element.toNativeValue() == value); + } catch (e) { + return null; + } + } + return null; + } + + ///Gets [int] value. + int toValue() => _value; + + ///Gets [int] native value. + int toNativeValue() => _nativeValue; + + @override + int get hashCode => _value.hashCode; + + @override + bool operator ==(value) => value == _value; + + @override + String toString() { + switch (_value) { + case 1: + return 'ARRAY_BUFFER'; + case 0: + return 'STRING'; + } + return _value.toString(); + } +} + // ************************************************************************** // ExchangeableObjectGenerator // ************************************************************************** @@ -10,11 +88,20 @@ part of 'web_message.dart'; ///See https://html.spec.whatwg.org/multipage/comms.html#the-messageevent-interfaces for definition of a MessageEvent in HTML5. class WebMessage { ///The data of the message. - String? data; + dynamic data; ///The ports that are sent with the message. List? ports; - WebMessage({this.data, this.ports}); + + ///The payload type of the message. + WebMessageType type; + WebMessage({this.data, this.type = WebMessageType.STRING, this.ports}) { + assert(((this.data == null || this.data is String) && + this.type == WebMessageType.STRING) || + (this.data != null && + this.data is Uint8List && + this.type == WebMessageType.ARRAY_BUFFER)); + } ///Gets a possible [WebMessage] instance from a [Map] value. static WebMessage? fromMap(Map? map) { @@ -26,6 +113,7 @@ class WebMessage { ports: map['ports'] != null ? List.from(map['ports'].map((e) => e)) : null, + type: WebMessageType.fromNativeValue(map['type'])!, ); return instance; } @@ -35,6 +123,7 @@ class WebMessage { return { "data": data, "ports": ports?.map((e) => e.toMap()).toList(), + "type": type.toNativeValue(), }; } @@ -45,6 +134,6 @@ class WebMessage { @override String toString() { - return 'WebMessage{data: $data, ports: $ports}'; + return 'WebMessage{data: $data, ports: $ports, type: $type}'; } } diff --git a/lib/src/web_message/web_message_channel.dart b/lib/src/web_message/web_message_channel.dart index 0d5fffbf..8a9663fa 100644 --- a/lib/src/web_message/web_message_channel.dart +++ b/lib/src/web_message/web_message_channel.dart @@ -1,6 +1,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_inappwebview/src/util.dart'; import 'web_message_port.dart'; +import 'web_message.dart'; ///The representation of the [HTML5 message channels](https://html.spec.whatwg.org/multipage/web-messaging.html#message-channels). /// @@ -45,7 +46,10 @@ class WebMessageChannel extends ChannelController { int index = call.arguments["index"]; var port = index == 0 ? this.port1 : this.port2; if (port.onMessage != null) { - String? message = call.arguments["message"]; + WebMessage? message = call.arguments["message"] != null + ? WebMessage.fromMap( + call.arguments["message"].cast()) + : null; port.onMessage!(message); } break; diff --git a/lib/src/web_message/web_message_listener.dart b/lib/src/web_message/web_message_listener.dart index cdc24b51..24434fa6 100644 --- a/lib/src/web_message/web_message_listener.dart +++ b/lib/src/web_message/web_message_listener.dart @@ -3,6 +3,7 @@ import '../in_app_webview/in_app_webview_controller.dart'; import '../types/main.dart'; import '../util.dart'; import '../web_uri.dart'; +import 'web_message.dart'; ///This listener receives messages sent on the JavaScript object which was injected by [InAppWebViewController.addWebMessageListener]. /// @@ -56,7 +57,10 @@ class WebMessageListener extends ChannelController { _replyProxy = new JavaScriptReplyProxy(this); } if (onPostMessage != null) { - String? message = call.arguments["message"]; + WebMessage? message = call.arguments["message"] != null + ? WebMessage.fromMap( + call.arguments["message"].cast()) + : null; WebUri? sourceOrigin = call.arguments["sourceOrigin"] != null ? WebUri(call.arguments["sourceOrigin"]) : null; @@ -107,10 +111,12 @@ class JavaScriptReplyProxy { ///Post a [message] to the injected JavaScript object which sent this [JavaScriptReplyProxy]. /// + ///If [message] is of type [WebMessageType.ARRAY_BUFFER], be aware that large byte buffers can lead to out-of-memory crashes on low-end devices. + /// ///**Official Android API**: https://developer.android.com/reference/androidx/webkit/JavaScriptReplyProxy#postMessage(java.lang.String) - Future postMessage(String message) async { + Future postMessage(WebMessage message) async { Map args = {}; - args.putIfAbsent('message', () => message); + args.putIfAbsent('message', () => message.toMap()); await _webMessageListener.channel?.invokeMethod('postMessage', args); } diff --git a/macos/Classes/InAppWebView/InAppWebView.swift b/macos/Classes/InAppWebView/InAppWebView.swift index 36f6ebd4..a2de17f0 100755 --- a/macos/Classes/InAppWebView/InAppWebView.swift +++ b/macos/Classes/InAppWebView/InAppWebView.swift @@ -2201,14 +2201,22 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { let body = message.body as! [String: Any?] let webMessageChannelId = body["webMessageChannelId"] as! String let index = body["index"] as! Int64 - let webMessage = body["message"] as? String + var webMessage: WebMessage? = nil + if let webMessageMap = body["message"] as? [String : Any?] { + webMessage = WebMessage.fromMap(map: webMessageMap) + } + if let webMessageChannel = webMessageChannels[webMessageChannelId] { webMessageChannel.channelDelegate?.onMessage(index: index, message: webMessage) } } else if message.name == "onWebMessageListenerPostMessageReceived" { let body = message.body as! [String: Any?] let jsObjectName = body["jsObjectName"] as! String - let messageData = body["message"] as? String + var webMessage: WebMessage? = nil + if let webMessageMap = body["message"] as? [String : Any?] { + webMessage = WebMessage.fromMap(map: webMessageMap) + } + if let webMessageListener = webMessageListeners.first(where: ({($0.jsObjectName == jsObjectName)})) { let isMainFrame = message.frameInfo.isMainFrame @@ -2225,7 +2233,7 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { if !scheme.isEmpty, !host.isEmpty { sourceOrigin = URL(string: "\(scheme)://\(host)\(port != 0 ? ":" + String(port) : "")") } - webMessageListener.channelDelegate?.onPostMessage(message: messageData, sourceOrigin: sourceOrigin, isMainFrame: isMainFrame) + webMessageListener.channelDelegate?.onPostMessage(message: webMessage, sourceOrigin: sourceOrigin, isMainFrame: isMainFrame) } } } @@ -2514,11 +2522,11 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { } portsString = "[" + portArrayString.joined(separator: ", ") + "]" } - let data = message.data?.replacingOccurrences(of: "\'", with: "\\'") ?? "null" + let url = URL(string: targetOrigin)?.absoluteString ?? "*" let source = """ (function() { - window.postMessage('\(data)', '\(url)', \(portsString)); + window.postMessage(\(message.jsData), '\(url)', \(portsString)); })(); """ evaluateJavascript(source: source, completionHandler: completionHandler) diff --git a/macos/Classes/InAppWebView/WebMessage/WebMessageChannel.swift b/macos/Classes/InAppWebView/WebMessage/WebMessageChannel.swift index cb0e7be9..b5e2c88d 100644 --- a/macos/Classes/InAppWebView/WebMessage/WebMessageChannel.swift +++ b/macos/Classes/InAppWebView/WebMessage/WebMessageChannel.swift @@ -26,8 +26,8 @@ public class WebMessageChannel : FlutterMethodCallDelegate { self.channelDelegate = WebMessageChannelChannelDelegate(webMessageChannel: self, channel: channel) } self.ports = [ - WebMessagePort(name: "port1", webMessageChannel: self), - WebMessagePort(name: "port2", webMessageChannel: self) + WebMessagePort(name: "port1", index: 0, webMessageChannelId: self.id, webMessageChannel: self), + WebMessagePort(name: "port2", index: 1, webMessageChannelId: self.id, webMessageChannel: self) ] } diff --git a/macos/Classes/InAppWebView/WebMessage/WebMessageChannelChannelDelegate.swift b/macos/Classes/InAppWebView/WebMessage/WebMessageChannelChannelDelegate.swift index 1e841100..22049da7 100644 --- a/macos/Classes/InAppWebView/WebMessage/WebMessageChannelChannelDelegate.swift +++ b/macos/Classes/InAppWebView/WebMessage/WebMessageChannelChannelDelegate.swift @@ -40,22 +40,20 @@ public class WebMessageChannelChannelDelegate : ChannelDelegate { if let webView = webMessageChannel?.webView, let ports = webMessageChannel?.ports, ports.count > 0 { let index = arguments!["index"] as! Int let port = ports[index] - let message = arguments!["message"] as! [String: Any?] + var message = WebMessage.fromMap(map: arguments!["message"] as! [String: Any?]) - var webMessagePorts: [WebMessagePort] = [] - let portsMap = message["ports"] as? [[String: Any?]] - if let portsMap = portsMap { - for portMap in portsMap { - let webMessageChannelId = portMap["webMessageChannelId"] as! String - let index = portMap["index"] as! Int - if let webMessageChannel = webView.webMessageChannels[webMessageChannelId] { - webMessagePorts.append(webMessageChannel.ports[index]) + var ports: [WebMessagePort] = [] + if let notConnectedPorts = message.ports { + for notConnectedPort in notConnectedPorts { + if let webMessageChannel = webView.webMessageChannels[notConnectedPort.webMessageChannelId] { + ports.append(webMessageChannel.ports[Int(notConnectedPort.index)]) } } } - let webMessage = WebMessage(data: message["data"] as? String, ports: webMessagePorts) + message.ports = ports + do { - try port.postMessage(message: webMessage) { (_) in + try port.postMessage(message: message) { (_) in result(true) } } catch let error as NSError { @@ -86,10 +84,10 @@ public class WebMessageChannelChannelDelegate : ChannelDelegate { } } - public func onMessage(index: Int64, message: String?) { + public func onMessage(index: Int64, message: WebMessage?) { let arguments: [String:Any?] = [ "index": index, - "message": message + "message": message?.toMap() ] channel?.invokeMethod("onMessage", arguments: arguments) } diff --git a/macos/Classes/InAppWebView/WebMessage/WebMessageListenerChannelDelegate.swift b/macos/Classes/InAppWebView/WebMessage/WebMessageListenerChannelDelegate.swift index 76b91d03..198f36a4 100644 --- a/macos/Classes/InAppWebView/WebMessage/WebMessageListenerChannelDelegate.swift +++ b/macos/Classes/InAppWebView/WebMessage/WebMessageListenerChannelDelegate.swift @@ -23,12 +23,13 @@ public class WebMessageListenerChannelDelegate : ChannelDelegate { case "postMessage": if let webView = webMessageListener?.webView, let jsObjectName = webMessageListener?.jsObjectName { let jsObjectNameEscaped = jsObjectName.replacingOccurrences(of: "\'", with: "\\'") - let messageEscaped = (arguments!["message"] as! String).replacingOccurrences(of: "\'", with: "\\'") + let message = WebMessage.fromMap(map: arguments!["message"] as! [String: Any?]) + let source = """ (function() { var webMessageListener = window['\(jsObjectNameEscaped)']; if (webMessageListener != null) { - var event = {data: '\(messageEscaped)'}; + var event = {data: \(message.jsData)}; if (webMessageListener.onmessage != null) { webMessageListener.onmessage(event); } @@ -51,9 +52,9 @@ public class WebMessageListenerChannelDelegate : ChannelDelegate { } } - public func onPostMessage(message: String?, sourceOrigin: URL?, isMainFrame: Bool) { + public func onPostMessage(message: WebMessage?, sourceOrigin: URL?, isMainFrame: Bool) { let arguments: [String:Any?] = [ - "message": message, + "message": message?.toMap(), "sourceOrigin": sourceOrigin?.absoluteString, "isMainFrame": isMainFrame ] diff --git a/macos/Classes/InAppWebView/WebViewChannelDelegate.swift b/macos/Classes/InAppWebView/WebViewChannelDelegate.swift index f41511ba..b75c7046 100644 --- a/macos/Classes/InAppWebView/WebViewChannelDelegate.swift +++ b/macos/Classes/InAppWebView/WebViewChannelDelegate.swift @@ -480,23 +480,21 @@ public class WebViewChannelDelegate : ChannelDelegate { break case .postWebMessage: if let webView = webView { - let message = arguments!["message"] as! [String: Any?] + var message = WebMessage.fromMap(map: arguments!["message"] as! [String: Any?]) let targetOrigin = arguments!["targetOrigin"] as! String var ports: [WebMessagePort] = [] - let portsMap = message["ports"] as? [[String: Any?]] - if let portsMap = portsMap { - for portMap in portsMap { - let webMessageChannelId = portMap["webMessageChannelId"] as! String - let index = portMap["index"] as! Int - if let webMessageChannel = webView.webMessageChannels[webMessageChannelId] { - ports.append(webMessageChannel.ports[index]) + if let notConnectedPorts = message.ports { + for notConnectedPort in notConnectedPorts { + if let webMessageChannel = webView.webMessageChannels[notConnectedPort.webMessageChannelId] { + ports.append(webMessageChannel.ports[Int(notConnectedPort.index)]) } } } - let webMessage = WebMessage(data: message["data"] as? String, ports: ports) + message.ports = ports + do { - try webView.postWebMessage(message: webMessage, targetOrigin: targetOrigin) { (_) in + try webView.postWebMessage(message: message, targetOrigin: targetOrigin) { (_) in result(true) } } catch let error as NSError { diff --git a/macos/Classes/PluginScriptsJS/WebMessageListenerJS.swift b/macos/Classes/PluginScriptsJS/WebMessageListenerJS.swift index 9d9fc60d..ab8edfae 100644 --- a/macos/Classes/PluginScriptsJS/WebMessageListenerJS.swift +++ b/macos/Classes/PluginScriptsJS/WebMessageListenerJS.swift @@ -13,7 +13,11 @@ function FlutterInAppWebViewWebMessageListener(jsObjectName) { this.listeners = []; this.onmessage = null; } -FlutterInAppWebViewWebMessageListener.prototype.postMessage = function(message) { +FlutterInAppWebViewWebMessageListener.prototype.postMessage = function(data) { + var message = { + "data": window.ArrayBuffer != null && data instanceof ArrayBuffer ? Array.from(new Uint8Array(data)) : (data != null ? data.toString() : null), + "type": window.ArrayBuffer != null && data instanceof ArrayBuffer ? 1 : 0 + }; window.webkit.messageHandlers['onWebMessageListenerPostMessageReceived'].postMessage({jsObjectName: this.jsObjectName, message: message}); }; FlutterInAppWebViewWebMessageListener.prototype.addEventListener = function(type, listener) { diff --git a/macos/Classes/Types/WebMessage.swift b/macos/Classes/Types/WebMessage.swift index 378bd67f..8b958b80 100644 --- a/macos/Classes/Types/WebMessage.swift +++ b/macos/Classes/Types/WebMessage.swift @@ -6,17 +6,55 @@ // import Foundation +import FlutterMacOS -public class WebMessage : NSObject { - var data: String? +public class WebMessage : NSObject, Disposable { + var data: Any? + var type: WebMessageType var ports: [WebMessagePort]? - public init(data: String?, ports: [WebMessagePort]?) { + var jsData: String { + var jsData: String = "null" + if let messageData = data { + if type == .arrayBuffer, let messageDataArrayBuffer = messageData as? FlutterStandardTypedData { + jsData = "new Uint8Array(\(Array(messageDataArrayBuffer.data))).buffer" + } else if let messageDataString = messageData as? String { + jsData = "'\(messageDataString.replacingOccurrences(of: "\'", with: "\\'"))'" + } + } + return jsData + } + + public init(data: Any?, type: WebMessageType, ports: [WebMessagePort]?) { + self.type = type super.init() self.data = data self.ports = ports } + public static func fromMap(map: [String: Any?]) -> WebMessage { + let portMapList = map["ports"] as? [[String: Any?]] + var ports: [WebMessagePort]? = nil + if let portMapList = portMapList, !portMapList.isEmpty { + ports = [] + portMapList.forEach { (portMap) in + ports?.append(WebMessagePort.fromMap(map: portMap)) + } + } + + return WebMessage( + data: map["data"] as? Any, + type: WebMessageType.init(rawValue: map["type"] as! Int)!, + ports: ports) + } + + public func toMap () -> [String: Any?] { + return [ + "data": type == .arrayBuffer && data is [UInt8] ? Data(data as! [UInt8]) : data, + "type": type.rawValue + ] + } + public func dispose() { ports?.removeAll() } @@ -26,3 +64,8 @@ public class WebMessage : NSObject { dispose() } } + +public enum WebMessageType: Int { + case string = 0 + case arrayBuffer = 1 +} diff --git a/macos/Classes/Types/WebMessagePort.swift b/macos/Classes/Types/WebMessagePort.swift index 9e823e92..824e9e4c 100644 --- a/macos/Classes/Types/WebMessagePort.swift +++ b/macos/Classes/Types/WebMessagePort.swift @@ -9,13 +9,17 @@ import Foundation public class WebMessagePort : NSObject { var name: String + var index: Int64 + var webMessageChannelId: String var webMessageChannel: WebMessageChannel? var isClosed = false var isTransferred = false var isStarted = false - public init(name: String, webMessageChannel: WebMessageChannel) { + public init(name: String, index: Int64, webMessageChannelId: String, webMessageChannel: WebMessageChannel?) { self.name = name + self.index = index + self.webMessageChannelId = webMessageChannelId super.init() self.webMessageChannel = webMessageChannel } @@ -35,7 +39,10 @@ public class WebMessagePort : NSObject { window.webkit.messageHandlers["onWebMessagePortMessageReceived"].postMessage({ "webMessageChannelId": "\(webMessageChannel.id)", "index": \(String(index)), - "message": event.data + "message": { + "data": window.ArrayBuffer != null && event.data instanceof ArrayBuffer ? Array.from(new Uint8Array(event.data)) : (event.data != null ? event.data.toString() : null), + "type": window.ArrayBuffer != null && event.data instanceof ArrayBuffer ? 1 : 0 + } }); } } @@ -71,12 +78,12 @@ public class WebMessagePort : NSObject { } portsString = "[" + portArrayString.joined(separator: ", ") + "]" } - let data = message.data?.replacingOccurrences(of: "\'", with: "\\'") ?? "null" + let source = """ (function() { var webMessageChannel = \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(webMessageChannel.id)"]; if (webMessageChannel != null) { - webMessageChannel.\(self.name).postMessage('\(data)', \(portsString)); + webMessageChannel.\(self.name).postMessage(\(message.jsData), \(portsString)); } })(); """ @@ -111,6 +118,23 @@ public class WebMessagePort : NSObject { } } + public static func fromMap(map: [String: Any?]) -> WebMessagePort { + let index = map["index"] as! Int64 + return WebMessagePort( + name: "port\(String(index + 1))", + index: index, + webMessageChannelId: map["webMessageChannelId"] as! String, + webMessageChannel: nil) + } + + public func toMap () -> [String: Any?] { + return [ + "name": name, + "index": index, + "webMessageChannelId": webMessageChannelId + ] + } + public func dispose() { isClosed = true webMessageChannel = nil diff --git a/pubspec.yaml b/pubspec.yaml index 8ebd56da..f0e3a2c3 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_inappwebview description: A Flutter plugin that allows you to add an inline webview, to use an headless webview, and to open an in-app browser window. -version: 6.0.0-beta.27 +version: 6.0.0-beta.28 homepage: https://inappwebview.dev/ repository: https://github.com/pichillilorenzo/flutter_inappwebview issue_tracker: https://github.com/pichillilorenzo/flutter_inappwebview/issues @@ -21,7 +21,7 @@ dependencies: flutter_web_plugins: sdk: flutter js: ^0.6.4 - flutter_inappwebview_internal_annotations: ^1.1.0 + flutter_inappwebview_internal_annotations: ^1.1.1 dev_dependencies: flutter_test: