From f5a048cb69be2b34215da067a57ce6b512fbee65 Mon Sep 17 00:00:00 2001 From: Lorenzo Pichilli Date: Sat, 8 Oct 2022 14:19:35 +0200 Subject: [PATCH] added Find Interaction Controller --- CHANGELOG.md | 3 +- .../FindInteractionChannelDelegate.java | 66 +++++ .../FindInteractionController.java | 42 ++++ .../FindInteractionSettings.java | 46 ++++ .../in_app_browser/InAppBrowserActivity.java | 5 + .../webview/WebViewChannelDelegate.java | 159 ++++++------ .../WebViewChannelDelegateMethods.java | 75 ++++++ .../in_app_webview/FlutterWebView.java | 5 + .../webview/in_app_webview/InAppWebView.java | 13 +- .../find_interactions.dart | 118 +++++++++ .../find_interaction_controller/main.dart | 12 + .../integration_test/in_app_webview/main.dart | 2 - .../on_find_result_received.dart | 56 ----- .../webview_flutter_test.dart | 6 + example/lib/in_app_webiew_example.screen.dart | 40 +++- .../FindInteractionChannelDelegate.swift | 168 +++++++++++++ .../FindInteractionController.swift | 108 +++++++++ .../FindInteractionSettings.swift | 25 ++ .../InAppBrowserWebViewController.swift | 6 + .../FlutterWebViewController.swift | 6 + ios/Classes/InAppWebView/InAppWebView.swift | 49 ++-- .../InAppWebView/InAppWebViewSettings.swift | 6 + .../InAppWebView/WebViewChannelDelegate.swift | 182 +++++++------- .../WebViewChannelDelegateMethods.swift | 89 +++++++ .../PluginScriptsJS/FindTextHighlightJS.swift | 2 +- .../PullToRefresh/PullToRefreshControl.swift | 10 +- ios/Classes/Types/UIFindSession.swift | 19 ++ lib/src/android/webview_feature.g.dart | 5 + .../find_interaction_controller.dart | 226 ++++++++++++++++++ lib/src/find_interaction/main.dart | 1 + lib/src/in_app_browser/in_app_browser.dart | 19 +- .../headless_in_app_webview.dart | 9 + lib/src/in_app_webview/in_app_webview.dart | 9 + .../in_app_webview_controller.dart | 63 +++-- .../in_app_webview_settings.dart | 26 ++ lib/src/in_app_webview/main.dart | 3 +- lib/src/in_app_webview/webview.dart | 24 +- lib/src/main.dart | 1 + .../pull_to_refresh_controller.dart | 57 ++++- lib/src/types/find_session.dart | 24 ++ lib/src/types/find_session.g.dart | 56 +++++ lib/src/types/main.dart | 2 + .../types/search_result_display_style.dart | 20 ++ .../types/search_result_display_style.g.dart | 85 +++++++ 44 files changed, 1630 insertions(+), 318 deletions(-) create mode 100644 android/src/main/java/com/pichillilorenzo/flutter_inappwebview/find_interaction/FindInteractionChannelDelegate.java create mode 100644 android/src/main/java/com/pichillilorenzo/flutter_inappwebview/find_interaction/FindInteractionController.java create mode 100644 android/src/main/java/com/pichillilorenzo/flutter_inappwebview/find_interaction/FindInteractionSettings.java create mode 100644 android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/WebViewChannelDelegateMethods.java create mode 100644 example/integration_test/find_interaction_controller/find_interactions.dart create mode 100644 example/integration_test/find_interaction_controller/main.dart delete mode 100644 example/integration_test/in_app_webview/on_find_result_received.dart create mode 100644 ios/Classes/FindInteraction/FindInteractionChannelDelegate.swift create mode 100644 ios/Classes/FindInteraction/FindInteractionController.swift create mode 100644 ios/Classes/FindInteraction/FindInteractionSettings.swift create mode 100644 ios/Classes/InAppWebView/WebViewChannelDelegateMethods.swift create mode 100644 ios/Classes/Types/UIFindSession.swift create mode 100644 lib/src/find_interaction/find_interaction_controller.dart create mode 100644 lib/src/find_interaction/main.dart create mode 100644 lib/src/types/find_session.dart create mode 100644 lib/src/types/find_session.g.dart create mode 100644 lib/src/types/search_result_display_style.dart create mode 100644 lib/src/types/search_result_display_style.g.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 43639d1c..c7799bba 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,9 @@ - Added `ProxyController` for Android - Added `PrintJobController` to manage print jobs - Added `WebAuthenticationSession` for iOS +- Added `FindInteractionController` for Android and iOS - Added `pauseAllMediaPlayback`, `setAllMediaPlaybackSuspended`, `closeAllMediaPresentations`, `requestMediaPlaybackState`, `isInFullscreen`, `getCameraCaptureState`, `setCameraCaptureState`, `getMicrophoneCaptureState`, `setMicrophoneCaptureState` WebView controller methods -- Added `underPageBackgroundColor`, `isTextInteractionEnabled`, `isSiteSpecificQuirksModeEnabled`, `upgradeKnownHostsToHTTPS`, `forceDarkStrategy`, `willSuppressErrorPage`, `algorithmicDarkeningAllowed`, `requestedWithHeaderMode`, `enterpriseAuthenticationAppLinkPolicyEnabled` WebView settings +- Added `underPageBackgroundColor`, `isTextInteractionEnabled`, `isSiteSpecificQuirksModeEnabled`, `upgradeKnownHostsToHTTPS`, `forceDarkStrategy`, `willSuppressErrorPage`, `algorithmicDarkeningAllowed`, `requestedWithHeaderMode`, `enterpriseAuthenticationAppLinkPolicyEnabled`, `isElementFullscreenEnabled`, `isFindInteractionEnabled` WebView settings - Added `onCameraCaptureStateChanged`, `onMicrophoneCaptureStateChanged` WebView events - Added support for `onPermissionRequest` event on iOS 15.0+ - Added `debugLoggingSettings` static property for WebView and ChromeSafariBrowser diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/find_interaction/FindInteractionChannelDelegate.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/find_interaction/FindInteractionChannelDelegate.java new file mode 100644 index 00000000..ffda5420 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/find_interaction/FindInteractionChannelDelegate.java @@ -0,0 +1,66 @@ +package com.pichillilorenzo.flutter_inappwebview.find_interaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview.types.ChannelDelegateImpl; + +import java.util.HashMap; +import java.util.Map; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class FindInteractionChannelDelegate extends ChannelDelegateImpl { + @Nullable + private FindInteractionController findInteractionController; + + public FindInteractionChannelDelegate(@NonNull FindInteractionController findInteractionController, @NonNull MethodChannel channel) { + super(channel); + this.findInteractionController = findInteractionController; + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull final MethodChannel.Result result) { + switch (call.method) { + case "findAllAsync": + if (findInteractionController != null && findInteractionController.webView != null) { + String find = (String) call.argument("find"); + findInteractionController.webView.findAllAsync(find); + } + result.success(true); + break; + case "findNext": + if (findInteractionController != null && findInteractionController.webView != null) { + Boolean forward = (Boolean) call.argument("forward"); + findInteractionController.webView.findNext(forward); + } + result.success(true); + break; + case "clearMatches": + if (findInteractionController != null && findInteractionController.webView != null) { + findInteractionController.webView.clearMatches(); + } + result.success(true); + break; + default: + result.notImplemented(); + } + } + + public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("activeMatchOrdinal", activeMatchOrdinal); + obj.put("numberOfMatches", numberOfMatches); + obj.put("isDoneCounting", isDoneCounting); + channel.invokeMethod("onFindResultReceived", obj); + } + + @Override + public void dispose() { + super.dispose(); + findInteractionController = null; + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/find_interaction/FindInteractionController.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/find_interaction/FindInteractionController.java new file mode 100644 index 00000000..4f7f8033 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/find_interaction/FindInteractionController.java @@ -0,0 +1,42 @@ +package com.pichillilorenzo.flutter_inappwebview.find_interaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview.types.Disposable; +import com.pichillilorenzo.flutter_inappwebview.webview.InAppWebViewInterface; + +import io.flutter.plugin.common.MethodChannel; + +public class FindInteractionController implements Disposable { + static final String LOG_TAG = "FindInteractionController"; + public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_find_interaction_"; + + @Nullable + public InAppWebViewInterface webView; + @Nullable + public FindInteractionChannelDelegate channelDelegate; + @Nullable + public FindInteractionSettings settings; + + public FindInteractionController(@NonNull InAppWebViewInterface webView, @NonNull InAppWebViewFlutterPlugin plugin, + @NonNull Object id, @Nullable FindInteractionSettings settings) { + this.webView = webView; + this.settings = settings; + final MethodChannel channel = new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME_PREFIX + id); + this.channelDelegate = new FindInteractionChannelDelegate(this, channel); + } + + public void prepare() { + + } + + public void dispose() { + if (channelDelegate != null) { + channelDelegate.dispose(); + channelDelegate = null; + } + webView = null; + } +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/find_interaction/FindInteractionSettings.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/find_interaction/FindInteractionSettings.java new file mode 100644 index 00000000..621f3c1b --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/find_interaction/FindInteractionSettings.java @@ -0,0 +1,46 @@ +package com.pichillilorenzo.flutter_inappwebview.find_interaction; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview.ISettings; + +import java.util.HashMap; +import java.util.Map; + +public class FindInteractionSettings implements ISettings { + public static final String LOG_TAG = "FindInteractionSettings"; + + + @NonNull + @Override + public FindInteractionSettings parse(@NonNull Map settings) { +// for (Map.Entry pair : settings.entrySet()) { +// String key = pair.getKey(); +// Object value = pair.getValue(); +// if (value == null) { +// continue; +// } +// +// switch (key) { +// +// } +// } + + return this; + } + + @NonNull + public Map toMap() { + Map settings = new HashMap<>(); + return settings; + } + + @NonNull + @Override + public Map getRealSettings(@NonNull FindInteractionController findInteractionController) { + Map realSettings = toMap(); + return realSettings; + } + +} \ No newline at end of file diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserActivity.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserActivity.java index f5e7c5ee..7d0c2a4f 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserActivity.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/in_app_browser/InAppBrowserActivity.java @@ -24,6 +24,7 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; +import com.pichillilorenzo.flutter_inappwebview.find_interaction.FindInteractionController; import com.pichillilorenzo.flutter_inappwebview.types.Disposable; import com.pichillilorenzo.flutter_inappwebview.R; import com.pichillilorenzo.flutter_inappwebview.Util; @@ -109,6 +110,10 @@ public class InAppBrowserActivity extends AppCompatActivity implements InAppBrow webView.inAppBrowserDelegate = this; webView.plugin = manager.plugin; + FindInteractionController findInteractionController = new FindInteractionController(webView, manager.plugin, id, null); + webView.findInteractionController = findInteractionController; + findInteractionController.prepare(); + final MethodChannel channel = new MethodChannel(manager.plugin.messenger, METHOD_CHANNEL_NAME_PREFIX + id); channelDelegate = new InAppBrowserChannelDelegate(channel); webView.channelDelegate = new WebViewChannelDelegate(webView, channel); 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 3667b54c..8ed95ad1 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 @@ -13,6 +13,7 @@ import androidx.webkit.WebViewCompat; import androidx.webkit.WebViewFeature; import com.pichillilorenzo.flutter_inappwebview.Util; +import com.pichillilorenzo.flutter_inappwebview.find_interaction.FindInteractionChannelDelegate; import com.pichillilorenzo.flutter_inappwebview.in_app_browser.InAppBrowserActivity; import com.pichillilorenzo.flutter_inappwebview.in_app_browser.InAppBrowserSettings; import com.pichillilorenzo.flutter_inappwebview.print_job.PrintJobSettings; @@ -73,24 +74,31 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { @Override public void onMethodCall(@NonNull MethodCall call, @NonNull final MethodChannel.Result result) { - switch (call.method) { - case "getUrl": + WebViewChannelDelegateMethods method = null; + try { + method = WebViewChannelDelegateMethods.valueOf(call.method); + } catch (IllegalArgumentException e) { + result.notImplemented(); + return; + } + switch (method) { + case getUrl: result.success((webView != null) ? webView.getUrl() : null); break; - case "getTitle": + case getTitle: result.success((webView != null) ? webView.getTitle() : null); break; - case "getProgress": + case getProgress: result.success((webView != null) ? webView.getProgress() : null); break; - case "loadUrl": + case loadUrl: if (webView != null) { Map urlRequest = (Map) call.argument("urlRequest"); webView.loadUrl(URLRequest.fromMap(urlRequest)); } result.success(true); break; - case "postUrl": + case postUrl: if (webView != null) { String url = (String) call.argument("url"); byte[] postData = (byte[]) call.argument("postData"); @@ -98,7 +106,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { } result.success(true); break; - case "loadData": + case loadData: if (webView != null) { String data = (String) call.argument("data"); String mimeType = (String) call.argument("mimeType"); @@ -109,7 +117,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { } result.success(true); break; - case "loadFile": + case loadFile: if (webView != null) { String assetFilePath = (String) call.argument("assetFilePath"); try { @@ -122,7 +130,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { } result.success(true); break; - case "evaluateJavascript": + case evaluateJavascript: if (webView != null) { String source = (String) call.argument("source"); Map contentWorldMap = (Map) call.argument("contentWorld"); @@ -138,7 +146,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.success(null); } break; - case "injectJavascriptFileFromUrl": + case injectJavascriptFileFromUrl: if (webView != null) { String urlFile = (String) call.argument("urlFile"); Map scriptHtmlTagAttributes = (Map) call.argument("scriptHtmlTagAttributes"); @@ -146,14 +154,14 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { } result.success(true); break; - case "injectCSSCode": + case injectCSSCode: if (webView != null) { String source = (String) call.argument("source"); webView.injectCSSCode(source); } result.success(true); break; - case "injectCSSFileFromUrl": + case injectCSSFileFromUrl: if (webView != null) { String urlFile = (String) call.argument("urlFile"); Map cssLinkHtmlTagAttributes = (Map) call.argument("cssLinkHtmlTagAttributes"); @@ -161,44 +169,44 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { } result.success(true); break; - case "reload": + case reload: if (webView != null) webView.reload(); result.success(true); break; - case "goBack": + case goBack: if (webView != null) webView.goBack(); result.success(true); break; - case "canGoBack": + case canGoBack: result.success((webView != null) && webView.canGoBack()); break; - case "goForward": + case goForward: if (webView != null) webView.goForward(); result.success(true); break; - case "canGoForward": + case canGoForward: result.success((webView != null) && webView.canGoForward()); break; - case "goBackOrForward": + case goBackOrForward: if (webView != null) webView.goBackOrForward((Integer) call.argument("steps")); result.success(true); break; - case "canGoBackOrForward": + case canGoBackOrForward: result.success((webView != null) && webView.canGoBackOrForward((Integer) call.argument("steps"))); break; - case "stopLoading": + case stopLoading: if (webView != null) webView.stopLoading(); result.success(true); break; - case "isLoading": + case isLoading: result.success((webView != null) && webView.isLoading()); break; - case "takeScreenshot": + case takeScreenshot: if (webView != null) { Map screenshotConfiguration = (Map) call.argument("screenshotConfiguration"); webView.takeScreenshot(screenshotConfiguration, result); @@ -206,7 +214,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { else result.success(null); break; - case "setSettings": + case setSettings: if (webView != null && webView.getInAppBrowserDelegate() instanceof InAppBrowserActivity) { InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.getInAppBrowserDelegate(); InAppBrowserSettings inAppBrowserSettings = new InAppBrowserSettings(); @@ -221,7 +229,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { } result.success(true); break; - case "getSettings": + case getSettings: if (webView != null && webView.getInAppBrowserDelegate() instanceof InAppBrowserActivity) { InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.getInAppBrowserDelegate(); result.success(inAppBrowserActivity.getCustomSettings()); @@ -229,7 +237,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.success((webView != null) ? webView.getCustomSettings() : null); } break; - case "close": + case close: if (webView != null && webView.getInAppBrowserDelegate() instanceof InAppBrowserActivity) { InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.getInAppBrowserDelegate(); inAppBrowserActivity.close(result); @@ -237,7 +245,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.notImplemented(); } break; - case "show": + case show: if (webView != null && webView.getInAppBrowserDelegate() instanceof InAppBrowserActivity) { InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.getInAppBrowserDelegate(); inAppBrowserActivity.show(); @@ -246,7 +254,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.notImplemented(); } break; - case "hide": + case hide: if (webView != null && webView.getInAppBrowserDelegate() instanceof InAppBrowserActivity) { InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.getInAppBrowserDelegate(); inAppBrowserActivity.hide(); @@ -255,10 +263,10 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.notImplemented(); } break; - case "getCopyBackForwardList": + case getCopyBackForwardList: result.success((webView != null) ? webView.getCopyBackForwardList() : null); break; - case "startSafeBrowsing": + case startSafeBrowsing: if (webView != null && WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING)) { WebViewCompat.startSafeBrowsing(webView.getContext(), new ValueCallback() { @Override @@ -271,37 +279,37 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.success(false); } break; - case "clearCache": + case clearCache: if (webView != null) webView.clearAllCache(); result.success(true); break; - case "clearSslPreferences": + case clearSslPreferences: if (webView != null) webView.clearSslPreferences(); result.success(true); break; - case "findAllAsync": + case findAllAsync: if (webView != null) { String find = (String) call.argument("find"); webView.findAllAsync(find); } result.success(true); break; - case "findNext": + case findNext: if (webView != null) { Boolean forward = (Boolean) call.argument("forward"); webView.findNext(forward); } result.success(true); break; - case "clearMatches": + case clearMatches: if (webView != null) { webView.clearMatches(); } result.success(true); break; - case "scrollTo": + case scrollTo: if (webView != null) { Integer x = (Integer) call.argument("x"); Integer y = (Integer) call.argument("y"); @@ -310,7 +318,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { } result.success(true); break; - case "scrollBy": + case scrollBy: if (webView != null) { Integer x = (Integer) call.argument("x"); Integer y = (Integer) call.argument("y"); @@ -319,31 +327,31 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { } result.success(true); break; - case "pause": + case pause: if (webView != null) { webView.onPause(); } result.success(true); break; - case "resume": + case resume: if (webView != null) { webView.onResume(); } result.success(true); break; - case "pauseTimers": + case pauseTimers: if (webView != null) { webView.pauseTimers(); } result.success(true); break; - case "resumeTimers": + case resumeTimers: if (webView != null) { webView.resumeTimers(); } result.success(true); break; - case "printCurrentPage": + case printCurrentPage: if (webView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { PrintJobSettings settings = new PrintJobSettings(); Map settingsMap = (Map) call.argument("settings"); @@ -355,31 +363,31 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.success(null); } break; - case "getContentHeight": + case getContentHeight: if (webView instanceof InAppWebView) { result.success(webView.getContentHeight()); } else { result.success(null); } break; - case "zoomBy": + case zoomBy: if (webView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { double zoomFactor = (double) call.argument("zoomFactor"); webView.zoomBy((float) zoomFactor); } result.success(true); break; - case "getOriginalUrl": + case getOriginalUrl: result.success((webView != null) ? webView.getOriginalUrl() : null); break; - case "getZoomScale": + case getZoomScale: if (webView instanceof InAppWebView) { result.success(webView.getZoomScale()); } else { result.success(null); } break; - case "getSelectedText": + case getSelectedText: if ((webView instanceof InAppWebView && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)) { webView.getSelectedText(new ValueCallback() { @Override @@ -391,14 +399,14 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.success(null); } break; - case "getHitTestResult": + case getHitTestResult: if (webView instanceof InAppWebView) { result.success(HitTestResult.fromWebViewHitTestResult(webView.getHitTestResult()).toMap()); } else { result.success(null); } break; - case "pageDown": + case pageDown: if (webView != null) { boolean bottom = (boolean) call.argument("bottom"); result.success(webView.pageDown(bottom)); @@ -406,7 +414,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.success(false); } break; - case "pageUp": + case pageUp: if (webView != null) { boolean top = (boolean) call.argument("top"); result.success(webView.pageUp(top)); @@ -414,7 +422,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.success(false); } break; - case "saveWebArchive": + case saveWebArchive: if (webView != null) { String filePath = (String) call.argument("filePath"); boolean autoname = (boolean) call.argument("autoname"); @@ -428,75 +436,75 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.success(null); } break; - case "zoomIn": + case zoomIn: if (webView != null) { result.success(webView.zoomIn()); } else { result.success(false); } break; - case "zoomOut": + case zoomOut: if (webView != null) { result.success(webView.zoomOut()); } else { result.success(false); } break; - case "clearFocus": + case clearFocus: if (webView != null) { webView.clearFocus(); } result.success(true); break; - case "setContextMenu": + case setContextMenu: if (webView != null) { Map contextMenu = (Map) call.argument("contextMenu"); webView.setContextMenu(contextMenu); } result.success(true); break; - case "requestFocusNodeHref": + case requestFocusNodeHref: if (webView != null) { result.success(webView.requestFocusNodeHref()); } else { result.success(null); } break; - case "requestImageRef": + case requestImageRef: if (webView != null) { result.success(webView.requestImageRef()); } else { result.success(null); } break; - case "getScrollX": + case getScrollX: if (webView != null) { result.success(webView.getScrollX()); } else { result.success(null); } break; - case "getScrollY": + case getScrollY: if (webView != null) { result.success(webView.getScrollY()); } else { result.success(null); } break; - case "getCertificate": + case getCertificate: if (webView != null) { result.success(SslCertificateExt.toMap(webView.getCertificate())); } else { result.success(null); } break; - case "clearHistory": + case clearHistory: if (webView != null) { webView.clearHistory(); } result.success(true); break; - case "addUserScript": + case addUserScript: if (webView != null && webView.getUserContentController() != null) { Map userScriptMap = (Map) call.argument("userScript"); UserScript userScript = UserScript.fromMap(userScriptMap); @@ -505,7 +513,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.success(false); } break; - case "removeUserScript": + case removeUserScript: if (webView != null && webView.getUserContentController() != null) { Integer index = (Integer) call.argument("index"); Map userScriptMap = (Map) call.argument("userScript"); @@ -515,20 +523,20 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.success(false); } break; - case "removeUserScriptsByGroupName": + case removeUserScriptsByGroupName: if (webView != null && webView.getUserContentController() != null) { String groupName = (String) call.argument("groupName"); webView.getUserContentController().removeUserOnlyScriptsByGroupName(groupName); } result.success(true); break; - case "removeAllUserScripts": + case removeAllUserScripts: if (webView != null && webView.getUserContentController() != null) { webView.getUserContentController().removeAllUserOnlyScripts(); } result.success(true); break; - case "callAsyncJavaScript": + case callAsyncJavaScript: if (webView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { String functionBody = (String) call.argument("functionBody"); Map functionArguments = (Map) call.argument("arguments"); @@ -545,7 +553,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.success(null); } break; - case "isSecureContext": + case isSecureContext: if (webView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { webView.isSecureContext(new ValueCallback() { @Override @@ -557,7 +565,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.success(false); } break; - case "createWebMessageChannel": + case createWebMessageChannel: if (webView != null) { if (webView instanceof InAppWebView && WebViewFeature.isFeatureSupported(WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL)) { result.success(webView.createCompatWebMessageChannel().toMap()); @@ -568,7 +576,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.success(null); } break; - case "postWebMessage": + case postWebMessage: if (webView != null && WebViewFeature.isFeatureSupported(WebViewFeature.POST_WEB_MESSAGE)) { Map message = (Map) call.argument("message"); String targetOrigin = (String) call.argument("targetOrigin"); @@ -600,7 +608,7 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.success(true); } break; - case "addWebMessageListener": + case addWebMessageListener: if (webView != null) { Map webMessageListenerMap = (Map) call.argument("webMessageListener"); WebMessageListener webMessageListener = WebMessageListener.fromMap(webView, webView.getPlugin().messenger, webMessageListenerMap); @@ -618,32 +626,35 @@ public class WebViewChannelDelegate extends ChannelDelegateImpl { result.success(true); } break; - case "canScrollVertically": + case canScrollVertically: if (webView != null) { result.success(webView.canScrollVertically()); } else { result.success(false); } break; - case "canScrollHorizontally": + case canScrollHorizontally: if (webView != null) { result.success(webView.canScrollHorizontally()); } else { result.success(false); } break; - case "isInFullscreen": + case isInFullscreen: if (webView != null) { result.success(webView.isInFullscreen()); } else { result.success(false); } break; - default: - result.notImplemented(); } } + /** + * @deprecated + * Use {@link FindInteractionChannelDelegate#onFindResultReceived} instead. + */ + @Deprecated public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) { MethodChannel channel = getChannel(); if (channel == null) return; diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/WebViewChannelDelegateMethods.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/WebViewChannelDelegateMethods.java new file mode 100644 index 00000000..d29c52c6 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/WebViewChannelDelegateMethods.java @@ -0,0 +1,75 @@ +package com.pichillilorenzo.flutter_inappwebview.webview; + +public enum WebViewChannelDelegateMethods { + getUrl, + getTitle, + getProgress, + loadUrl, + postUrl, + loadData, + loadFile, + evaluateJavascript, + injectJavascriptFileFromUrl, + injectCSSCode, + injectCSSFileFromUrl, + reload, + goBack, + canGoBack, + goForward, + canGoForward, + goBackOrForward, + canGoBackOrForward, + stopLoading, + isLoading, + takeScreenshot, + setSettings, + getSettings, + close, + show, + hide, + getCopyBackForwardList, + startSafeBrowsing, + clearCache, + clearSslPreferences, + findAllAsync, + findNext, + clearMatches, + scrollTo, + scrollBy, + pause, + resume, + pauseTimers, + resumeTimers, + printCurrentPage, + getContentHeight, + zoomBy, + getOriginalUrl, + getZoomScale, + getSelectedText, + getHitTestResult, + pageDown, + pageUp, + saveWebArchive, + zoomIn, + zoomOut, + clearFocus, + setContextMenu, + requestFocusNodeHref, + requestImageRef, + getScrollX, + getScrollY, + getCertificate, + clearHistory, + addUserScript, + removeUserScript, + removeUserScriptsByGroupName, + removeAllUserScripts, + callAsyncJavaScript, + isSecureContext, + createWebMessageChannel, + postWebMessage, + addWebMessageListener, + canScrollVertically, + canScrollHorizontally, + isInFullscreen +} diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/in_app_webview/FlutterWebView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/in_app_webview/FlutterWebView.java index 55464317..3ff6ddc9 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/in_app_webview/FlutterWebView.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/in_app_webview/FlutterWebView.java @@ -19,6 +19,7 @@ import androidx.webkit.WebViewCompat; import androidx.webkit.WebViewFeature; import com.pichillilorenzo.flutter_inappwebview.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview.find_interaction.FindInteractionController; import com.pichillilorenzo.flutter_inappwebview.plugin_scripts_js.JavaScriptBridgeJS; import com.pichillilorenzo.flutter_inappwebview.pull_to_refresh.PullToRefreshLayout; import com.pichillilorenzo.flutter_inappwebview.pull_to_refresh.PullToRefreshSettings; @@ -84,6 +85,10 @@ public class FlutterWebView implements PlatformWebView { pullToRefreshLayout.prepare(); } + FindInteractionController findInteractionController = new FindInteractionController(webView, plugin, id, null); + webView.findInteractionController = findInteractionController; + findInteractionController.prepare(); + webView.prepare(); } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/in_app_webview/InAppWebView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/in_app_webview/InAppWebView.java index 096452bc..76e56855 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/in_app_webview/InAppWebView.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/webview/in_app_webview/InAppWebView.java @@ -56,6 +56,7 @@ import androidx.webkit.WebViewCompat; import androidx.webkit.WebViewFeature; import com.pichillilorenzo.flutter_inappwebview.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview.find_interaction.FindInteractionController; import com.pichillilorenzo.flutter_inappwebview.print_job.PrintJobController; import com.pichillilorenzo.flutter_inappwebview.print_job.PrintJobManager; import com.pichillilorenzo.flutter_inappwebview.print_job.PrintJobSettings; @@ -167,6 +168,9 @@ final public class InAppWebView extends InputAwareWebView implements InAppWebVie private List initialUserOnlyScript = new ArrayList<>(); + @Nullable + public FindInteractionController findInteractionController; + public InAppWebView(Context context) { super(context); } @@ -406,7 +410,10 @@ final public class InAppWebView extends InputAwareWebView implements InAppWebVie setFindListener(new FindListener() { @Override public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) { - if (channelDelegate != null) channelDelegate.onFindResultReceived(activeMatchOrdinal, numberOfMatches, isDoneCounting); + if (findInteractionController != null && findInteractionController.channelDelegate != null) + findInteractionController.channelDelegate.onFindResultReceived(activeMatchOrdinal, numberOfMatches, isDoneCounting); + if (channelDelegate != null) + channelDelegate.onFindResultReceived(activeMatchOrdinal, numberOfMatches, isDoneCounting); } }); @@ -1929,6 +1936,10 @@ final public class InAppWebView extends InputAwareWebView implements InAppWebVie @Override public void dispose() { userContentController.dispose(); + if (findInteractionController != null) { + findInteractionController.dispose(); + findInteractionController = null; + } if (windowId != null) { InAppWebViewChromeClient.windowWebViewMessages.remove(windowId); } diff --git a/example/integration_test/find_interaction_controller/find_interactions.dart b/example/integration_test/find_interaction_controller/find_interactions.dart new file mode 100644 index 00000000..3d008089 --- /dev/null +++ b/example/integration_test/find_interaction_controller/find_interactions.dart @@ -0,0 +1,118 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void findInteractions() { + final shouldSkip = kIsWeb + ? true + : ![ + TargetPlatform.android, + TargetPlatform.iOS, + TargetPlatform.macOS, + ].contains(defaultTargetPlatform); + + testWidgets('find interactions', (WidgetTester tester) async { + final Completer controllerCompleter = Completer(); + final Completer pageLoaded = Completer(); + final findInteractionController = FindInteractionController(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + initialFile: "test_assets/in_app_webview_initial_file_test.html", + findInteractionController: findInteractionController, + initialSettings: InAppWebViewSettings( + clearCache: true, + isFindInteractionEnabled: true + ), + onWebViewCreated: (controller) { + controllerCompleter.complete(controller); + }, + onLoadStop: (controller, url) { + pageLoaded.complete(); + }, + ), + ), + ); + + await pageLoaded.future; + + await tester.pump(); + await Future.delayed(Duration(seconds: 1)); + + const firstSearchText = "InAppWebViewInitialFileTest"; + await expectLater(findInteractionController.findAllAsync(find: firstSearchText), completes); + if ([TargetPlatform.iOS, TargetPlatform.macOS].contains(defaultTargetPlatform)) { + expect(await findInteractionController.getSearchText(), firstSearchText); + final session = await findInteractionController.getActiveFindSession(); + expect(session!.resultCount, 2); + } + await expectLater(findInteractionController.findNext(forward: true), completes); + await expectLater(findInteractionController.findNext(forward: false), completes); + await expectLater(findInteractionController.clearMatches(), completes); + + if ([TargetPlatform.iOS, TargetPlatform.macOS].contains(defaultTargetPlatform)) { + const secondSearchText = "text"; + await expectLater( + findInteractionController.setSearchText(secondSearchText), completes); + await expectLater( + findInteractionController.presentFindNavigator(), completes); + expect(await findInteractionController.getSearchText(), secondSearchText); + expect(await findInteractionController.isFindNavigatorVisible(), true); + await expectLater(findInteractionController.updateResultCount(), completes); + await expectLater( + findInteractionController.dismissFindNavigator(), completes); + expect(await findInteractionController.isFindNavigatorVisible(), false); + } + }, skip: shouldSkip); + + testWidgets('onFindResultReceived', (WidgetTester tester) async { + final Completer controllerCompleter = Completer(); + final Completer pageLoaded = Completer(); + final Completer numberOfMatchesCompleter = Completer(); + final findInteractionController = FindInteractionController( + onFindResultReceived: (controller, int activeMatchOrdinal, + int numberOfMatches, bool isDoneCounting) async { + if (isDoneCounting && !numberOfMatchesCompleter.isCompleted) { + numberOfMatchesCompleter.complete(numberOfMatches); + } + }, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: InAppWebView( + key: GlobalKey(), + initialFile: "test_assets/in_app_webview_initial_file_test.html", + initialSettings: InAppWebViewSettings( + clearCache: true, + isFindInteractionEnabled: false + ), + findInteractionController: findInteractionController, + onWebViewCreated: (controller) { + controllerCompleter.complete(controller); + }, + onLoadStop: (controller, url) { + pageLoaded.complete(); + }, + ), + ), + ); + + var controller = await controllerCompleter.future; + await pageLoaded.future; + + await tester.pump(); + await Future.delayed(Duration(seconds: 1)); + + await controller.findAllAsync(find: "InAppWebViewInitialFileTest"); + final int numberOfMatches = await numberOfMatchesCompleter.future; + expect(numberOfMatches, 2); + }, skip: shouldSkip); +} diff --git a/example/integration_test/find_interaction_controller/main.dart b/example/integration_test/find_interaction_controller/main.dart new file mode 100644 index 00000000..ce8dc9d3 --- /dev/null +++ b/example/integration_test/find_interaction_controller/main.dart @@ -0,0 +1,12 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'find_interactions.dart'; + +void main() { + final shouldSkip = kIsWeb; + + group('FindInteractionController', () { + findInteractions(); + }, skip: shouldSkip); +} diff --git a/example/integration_test/in_app_webview/main.dart b/example/integration_test/in_app_webview/main.dart index 0f136e75..3dfccc2a 100644 --- a/example/integration_test/in_app_webview/main.dart +++ b/example/integration_test/in_app_webview/main.dart @@ -37,7 +37,6 @@ import 'load_file_url.dart'; import 'load_url.dart'; import 'on_console_message.dart'; import 'on_download_start_request.dart'; -import 'on_find_result_received.dart'; import 'on_js_before_unload.dart'; import 'on_received_error.dart'; import 'on_received_http_error.dart'; @@ -106,7 +105,6 @@ void main() { contentBlocker(); httpAuthCredentialDatabase(); onConsoleMessage(); - onFindResultReceived(); onDownloadStartRequest(); javascriptDialogs(); onReceivedHttpError(); diff --git a/example/integration_test/in_app_webview/on_find_result_received.dart b/example/integration_test/in_app_webview/on_find_result_received.dart deleted file mode 100644 index f23d01f4..00000000 --- a/example/integration_test/in_app_webview/on_find_result_received.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void onFindResultReceived() { - final shouldSkip = kIsWeb - ? true - : ![ - TargetPlatform.android, - TargetPlatform.iOS, - TargetPlatform.macOS, - ].contains(defaultTargetPlatform); - - testWidgets('onFindResultReceived', (WidgetTester tester) async { - final Completer controllerCompleter = Completer(); - final Completer pageLoaded = Completer(); - final Completer numberOfMatchesCompleter = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: InAppWebView( - key: GlobalKey(), - initialFile: "test_assets/in_app_webview_initial_file_test.html", - initialSettings: InAppWebViewSettings( - clearCache: true, - ), - onWebViewCreated: (controller) { - controllerCompleter.complete(controller); - }, - onLoadStop: (controller, url) { - pageLoaded.complete(); - }, - onFindResultReceived: (controller, int activeMatchOrdinal, - int numberOfMatches, bool isDoneCounting) async { - if (isDoneCounting && !numberOfMatchesCompleter.isCompleted) { - numberOfMatchesCompleter.complete(numberOfMatches); - } - }, - ), - ), - ); - - var controller = await controllerCompleter.future; - await pageLoaded.future; - - await tester.pump(); - await Future.delayed(Duration(seconds: 1)); - - await controller.findAllAsync(find: "InAppWebViewInitialFileTest"); - final int numberOfMatches = await numberOfMatchesCompleter.future; - expect(numberOfMatches, 2); - }, skip: shouldSkip); -} diff --git a/example/integration_test/webview_flutter_test.dart b/example/integration_test/webview_flutter_test.dart index 00016670..d46b2be1 100644 --- a/example/integration_test/webview_flutter_test.dart +++ b/example/integration_test/webview_flutter_test.dart @@ -3,6 +3,7 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:integration_test/integration_test.dart'; import 'in_app_webview/main.dart' as in_app_webview_tests; +import 'find_interaction_controller/main.dart' as find_interaction_controller_tests; import 'service_worker_controller/main.dart' as service_worker_controller_tests; import 'proxy_controller/main.dart' as proxy_controller_tests; import 'headless_in_app_webview/main.dart' as headless_in_app_webview_tests; @@ -26,8 +27,13 @@ void main() { ChromeSafariBrowser.debugLoggingSettings.maxLogMessageLength = 7000; WebAuthenticationSession.debugLoggingSettings.usePrint = true; WebAuthenticationSession.debugLoggingSettings.maxLogMessageLength = 7000; + PullToRefreshController.debugLoggingSettings.usePrint = true; + PullToRefreshController.debugLoggingSettings.maxLogMessageLength = 7000; + FindInteractionController.debugLoggingSettings.usePrint = true; + FindInteractionController.debugLoggingSettings.maxLogMessageLength = 7000; in_app_webview_tests.main(); + find_interaction_controller_tests.main(); service_worker_controller_tests.main(); proxy_controller_tests.main(); headless_in_app_webview_tests.main(); diff --git a/example/lib/in_app_webiew_example.screen.dart b/example/lib/in_app_webiew_example.screen.dart index f2e08d87..6e884859 100755 --- a/example/lib/in_app_webiew_example.screen.dart +++ b/example/lib/in_app_webiew_example.screen.dart @@ -19,12 +19,15 @@ class _InAppWebViewExampleScreenState extends State { InAppWebViewSettings settings = InAppWebViewSettings( useShouldOverrideUrlLoading: true, mediaPlaybackRequiresUserGesture: false, + isFindInteractionEnabled: false, allowsInlineMediaPlayback: true, iframeAllow: "camera; microphone", iframeAllowFullscreen: true ); PullToRefreshController? pullToRefreshController; + FindInteractionController? findInteractionController; + late ContextMenu contextMenu; String url = ""; double progress = 0; @@ -79,6 +82,14 @@ class _InAppWebViewExampleScreenState extends State { } }, ); + + findInteractionController = kIsWeb + ? null + : FindInteractionController( + onFindResultReceived: (controller, activeMatchOrdinal, numberOfMatches, isDoneCounting) => { + print("$activeMatchOrdinal $numberOfMatches $isDoneCounting") + }, + ); } @override @@ -114,7 +125,7 @@ class _InAppWebViewExampleScreenState extends State { InAppWebView( key: webViewKey, initialUrlRequest: - URLRequest(url: Uri.parse('https://github.com/flutter/')), + URLRequest(url: Uri.parse('https://developer.apple.com/videos/play/wwdc2022/10049/?time=264')), // initialUrlRequest: // URLRequest(url: Uri.parse(Uri.base.toString().replaceFirst("/#/", "/") + 'page.html')), // initialFile: "assets/index.html", @@ -122,6 +133,7 @@ class _InAppWebViewExampleScreenState extends State { initialSettings: settings, // contextMenu: contextMenu, pullToRefreshController: pullToRefreshController, + findInteractionController: findInteractionController, onWebViewCreated: (controller) async { webViewController = controller; }, @@ -167,6 +179,32 @@ class _InAppWebViewExampleScreenState extends State { this.url = url.toString(); urlController.text = this.url; }); + await findInteractionController?.findAllAsync(find: "video"); + // print(await findInteractionController?.getActiveFindSession()); + await Future.delayed(Duration(seconds: 1)); + findInteractionController?.findNext(forward: true); + findInteractionController?.findNext(forward: true); + findInteractionController?.findNext(forward: true); + await Future.delayed(Duration(seconds: 1)); + // findInteractionController?.clearMatches(); + findInteractionController?.findNext(forward: true); + findInteractionController?.findNext(forward: true); + findInteractionController?.findNext(forward: true); + findInteractionController?.findNext(forward: true); + await Future.delayed(Duration(seconds: 1)); + findInteractionController?.clearMatches(); + // print(await findInteractionController?.getSearchText()); + // findInteractionController?.findNext(forward: true); + // findInteractionController?.findNext(forward: false); + // findInteractionController?.setSearchText("text"); + // print(await findInteractionController?.getSearchText()); + // print(await findInteractionController?.isFindNavigatorVisible()); + // findInteractionController?.updateResultCount(); + // findInteractionController?.clearMatches(); + // findInteractionController?.presentFindNavigator(); + // await Future.delayed(Duration(milliseconds: 500)); + // findInteractionController?.dismissFindNavigator(); + // print(await findInteractionController?.isFindNavigatorVisible()); }, onReceivedError: (controller, request, error) { pullToRefreshController?.endRefreshing(); diff --git a/ios/Classes/FindInteraction/FindInteractionChannelDelegate.swift b/ios/Classes/FindInteraction/FindInteractionChannelDelegate.swift new file mode 100644 index 00000000..12d91234 --- /dev/null +++ b/ios/Classes/FindInteraction/FindInteractionChannelDelegate.swift @@ -0,0 +1,168 @@ +// +// FindInteractionChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/10/22. +// + +import Foundation + +public class FindInteractionChannelDelegate : ChannelDelegate { + private weak var findInteractionController: FindInteractionController? + + public init(findInteractionController: FindInteractionController, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.findInteractionController = findInteractionController + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "findAllAsync": + if let findInteractionController = findInteractionController { + let find = arguments!["find"] as! String + findInteractionController.findAllAsync(find: find, completionHandler: {(value, error) in + if error != nil { + result(FlutterError(code: "FindInteractionChannelDelegate", message: error?.localizedDescription, details: nil)) + return + } + result(true) + }) + } else { + result(false) + } + break + case "findNext": + if let findInteractionController = findInteractionController { + let forward = arguments!["forward"] as! Bool + findInteractionController.findNext(forward: forward, completionHandler: {(value, error) in + if error != nil { + result(FlutterError(code: "FindInteractionChannelDelegate", message: error?.localizedDescription, details: nil)) + return + } + result(true) + }) + } else { + result(false) + } + break + case "clearMatches": + if let findInteractionController = findInteractionController { + findInteractionController.clearMatches(completionHandler: {(value, error) in + if error != nil { + result(FlutterError(code: "FindInteractionChannelDelegate", message: error?.localizedDescription, details: nil)) + return + } + result(true) + }) + } else { + result(false) + } + break + case "setSearchText": + if #available(iOS 16.0, *) { + if let interaction = findInteractionController?.webView?.findInteraction { + let searchText = arguments!["searchText"] as? String + interaction.searchText = searchText + result(true) + } else { + result(false) + } + } else { + result(false) + } + break + case "getSearchText": + if #available(iOS 16.0, *) { + if let interaction = findInteractionController?.webView?.findInteraction { + result(interaction.searchText) + } else { + result(nil) + } + } else { + result(nil) + } + break + case "isFindNavigatorVisible": + if #available(iOS 16.0, *) { + if let interaction = findInteractionController?.webView?.findInteraction { + result(interaction.isFindNavigatorVisible) + } else { + result(false) + } + } else { + result(false) + } + break + case "updateResultCount": + if #available(iOS 16.0, *) { + if let interaction = findInteractionController?.webView?.findInteraction { + interaction.updateResultCount() + result(true) + } else { + result(false) + } + } else { + result(false) + } + break + case "presentFindNavigator": + if #available(iOS 16.0, *) { + if let interaction = findInteractionController?.webView?.findInteraction { + interaction.presentFindNavigator(showingReplace: false) + result(true) + } else { + result(false) + } + } else { + result(false) + } + break + case "dismissFindNavigator": + if #available(iOS 16.0, *) { + if let interaction = findInteractionController?.webView?.findInteraction { + interaction.dismissFindNavigator() + result(true) + } else { + result(false) + } + } else { + result(false) + } + break + case "getActiveFindSession": + if #available(iOS 16.0, *) { + if let interaction = findInteractionController?.webView?.findInteraction { + result(interaction.activeFindSession?.toMap()) + } else { + result(nil) + } + } else { + result(nil) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onFindResultReceived(activeMatchOrdinal: Int, numberOfMatches: Int, isDoneCounting: Bool) { + let arguments: [String : Any?] = [ + "activeMatchOrdinal": activeMatchOrdinal, + "numberOfMatches": numberOfMatches, + "isDoneCounting": isDoneCounting + ] + channel?.invokeMethod("onFindResultReceived", arguments: arguments) + } + + public override func dispose() { + super.dispose() + findInteractionController = nil + } + + deinit { + dispose() + } +} diff --git a/ios/Classes/FindInteraction/FindInteractionController.swift b/ios/Classes/FindInteraction/FindInteractionController.swift new file mode 100644 index 00000000..92794690 --- /dev/null +++ b/ios/Classes/FindInteraction/FindInteractionController.swift @@ -0,0 +1,108 @@ +// +// FindInteractionController.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/10/22. +// + +import Foundation +import Flutter + +public class FindInteractionController : NSObject, Disposable { + + static var METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_find_interaction_"; + var webView: InAppWebView? + var channelDelegate: FindInteractionChannelDelegate? + var settings: FindInteractionSettings? + var shouldCallOnRefresh = false + + public init(registrar: FlutterPluginRegistrar, id: Any, webView: InAppWebView, settings: FindInteractionSettings?) { + super.init() + self.webView = webView + self.settings = settings + let channel = FlutterMethodChannel(name: FindInteractionController.METHOD_CHANNEL_NAME_PREFIX + String(describing: id), + binaryMessenger: registrar.messenger()) + self.channelDelegate = FindInteractionChannelDelegate(findInteractionController: self, channel: channel) + } + + public func prepare() { +// if let settings = settings { +// +// } + } + + public func findAllAsync(find: String?, completionHandler: ((Any?, Error?) -> Void)?) { + guard let webView else { + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + return + } + if #available(iOS 16.0, *), webView.isFindInteractionEnabled { + if let interaction = webView.findInteraction { + interaction.searchText = find + interaction.presentFindNavigator(showingReplace: false) + } + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + } else { + let startSearch = "window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsync('\(find ?? "")');" + webView.evaluateJavaScript(startSearch, completionHandler: completionHandler) + } + } + + public func findNext(forward: Bool, completionHandler: ((Any?, Error?) -> Void)?) { + guard let webView else { + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + return + } + if #available(iOS 16.0, *), webView.isFindInteractionEnabled { + if let interaction = webView.findInteraction { + if forward { + interaction.findNext() + } else { + interaction.findPrevious() + } + } + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + } else { + webView.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._findNext(\(forward ? "true" : "false"));", completionHandler: completionHandler) + } + } + + public func clearMatches(completionHandler: ((Any?, Error?) -> Void)?) { + guard let webView else { + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + return + } + if #available(iOS 16.0, *), webView.isFindInteractionEnabled { + if let interaction = webView.findInteraction { + interaction.searchText = nil + interaction.dismissFindNavigator() + } + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + } else { + webView.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatches();", completionHandler: completionHandler) + } + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + webView = nil + } + + deinit { + debugPrint("FindInteractionControl - dealloc") + dispose() + } +} diff --git a/ios/Classes/FindInteraction/FindInteractionSettings.swift b/ios/Classes/FindInteraction/FindInteractionSettings.swift new file mode 100644 index 00000000..1c34c5bd --- /dev/null +++ b/ios/Classes/FindInteraction/FindInteractionSettings.swift @@ -0,0 +1,25 @@ +// +// FindInteractionSettings.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/10/22. +// + +import Foundation + +public class FindInteractionSettings : ISettings { + + override init(){ + super.init() + } + + override func parse(settings: [String: Any?]) -> FindInteractionSettings { + let _ = super.parse(settings: settings) + return self + } + + override func getRealSettings(obj: FindInteractionController?) -> [String: Any?] { + let realSettings: [String: Any?] = toMap() + return realSettings + } +} diff --git a/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift b/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift index 3801196e..e79a62ec 100755 --- a/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift +++ b/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift @@ -77,6 +77,12 @@ public class InAppBrowserWebViewController: UIViewController, InAppBrowserDelega pullToRefreshControl.delegate = webView pullToRefreshControl.prepare() + let findInteractionController = FindInteractionController( + registrar: SwiftFlutterPlugin.instance!.registrar!, + id: id, webView: webView, settings: nil) + webView.findInteractionController = findInteractionController + findInteractionController.prepare() + prepareWebView() webView.windowCreated = true diff --git a/ios/Classes/InAppWebView/FlutterWebViewController.swift b/ios/Classes/InAppWebView/FlutterWebViewController.swift index 4fd4b76b..5c50084c 100755 --- a/ios/Classes/InAppWebView/FlutterWebViewController.swift +++ b/ios/Classes/InAppWebView/FlutterWebViewController.swift @@ -61,6 +61,12 @@ public class FlutterWebViewController: NSObject, FlutterPlatformView, Disposable webView!.pullToRefreshControl = pullToRefreshControl pullToRefreshControl.delegate = webView! pullToRefreshControl.prepare() + + let findInteractionController = FindInteractionController( + registrar: SwiftFlutterPlugin.instance!.registrar!, + id: viewId, webView: webView!, settings: nil) + webView!.findInteractionController = findInteractionController + findInteractionController.prepare() webView!.autoresizingMask = [.flexibleWidth, .flexibleHeight] myView!.autoresizesSubviews = true diff --git a/ios/Classes/InAppWebView/InAppWebView.swift b/ios/Classes/InAppWebView/InAppWebView.swift index 32c01fd9..011d748f 100755 --- a/ios/Classes/InAppWebView/InAppWebView.swift +++ b/ios/Classes/InAppWebView/InAppWebView.swift @@ -12,7 +12,8 @@ import WebKit public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavigationDelegate, WKScriptMessageHandler, UIGestureRecognizerDelegate, WKDownloadDelegate, - PullToRefreshDelegate, Disposable { + PullToRefreshDelegate, + Disposable { static var METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_" var id: Any? // viewId @@ -22,6 +23,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, var channelDelegate: WebViewChannelDelegate? var settings: InAppWebViewSettings? var pullToRefreshControl: PullToRefreshControl? + var findInteractionController: FindInteractionController? var webMessageChannels: [String:WebMessageChannel] = [:] var webMessageListeners: [WebMessageListener] = [] var currentOriginalUrl: URL? @@ -329,7 +331,8 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, name: UIMenuController.didHideMenuNotification, object: nil) -// if #available(iOS 15.0, *) { + // TODO: Still not working on iOS 16.0! +// if #available(iOS 16.0, *) { // addObserver(self, // forKeyPath: #keyPath(WKWebView.fullscreenState), // options: .new, @@ -413,6 +416,10 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, } } + if #available(iOS 16.0, *) { + isFindInteractionEnabled = settings.isFindInteractionEnabled + } + // debugging is always enabled for iOS, // there isn't any option to set about it such as on Android. @@ -456,6 +463,7 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, if #available(iOS 15.4, *) { configuration.preferences.isSiteSpecificQuirksModeEnabled = settings.isSiteSpecificQuirksModeEnabled + configuration.preferences.isElementFullscreenEnabled = settings.isElementFullscreenEnabled } } } @@ -669,7 +677,9 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, } } } -// else if keyPath == #keyPath(WKWebView.fullscreenState) { + } else if #available(iOS 16.0, *) { + // TODO: Still not working on iOS 16.0! +// if keyPath == #keyPath(WKWebView.fullscreenState) { // if fullscreenState == .enteringFullscreen { // channelDelegate?.onEnterFullscreen() // } else if fullscreenState == .exitingFullscreen { @@ -2513,6 +2523,12 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, } @objc func onEnterFullscreen(_ notification: Notification) { + // TODO: Still not working on iOS 16.0! +// if #available(iOS 16.0, *) { +// channelDelegate?.onEnterFullscreen() +// inFullscreen = true +// } +// else if (isVideoPlayerWindow(notification.object as AnyObject?)) { channelDelegate?.onEnterFullscreen() inFullscreen = true @@ -2520,6 +2536,12 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, } @objc func onExitFullscreen(_ notification: Notification) { + // TODO: Still not working on iOS 16.0! +// if #available(iOS 16.0, *) { +// channelDelegate?.onExitFullscreen() +// inFullscreen = false +// } +// else if (isVideoPlayerWindow(notification.object as AnyObject?)) { channelDelegate?.onExitFullscreen() inFullscreen = false @@ -2648,6 +2670,7 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { if let wId = _windowId, let webViewTransport = InAppWebView.windowWebViews[wId] { webView = webViewTransport.webView } + webView.findInteractionController?.channelDelegate?.onFindResultReceived(activeMatchOrdinal: activeMatchOrdinal, numberOfMatches: numberOfMatches, isDoneCounting: isDoneCounting) webView.channelDelegate?.onFindResultReceived(activeMatchOrdinal: activeMatchOrdinal, numberOfMatches: numberOfMatches, isDoneCounting: isDoneCounting) } else if message.name == "onCallAsyncJavaScriptResultBelowIOS14Received" { let body = message.body as! [String: Any?] @@ -2701,19 +2724,6 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { } } - public func findAllAsync(find: String?, completionHandler: ((Any?, Error?) -> Void)?) { - let startSearch = "window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsync('\(find ?? "")');" - evaluateJavaScript(startSearch, completionHandler: completionHandler) - } - - public func findNext(forward: Bool, completionHandler: ((Any?, Error?) -> Void)?) { - evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._findNext(\(forward ? "true" : "false"));", completionHandler: completionHandler) - } - - public func clearMatches(completionHandler: ((Any?, Error?) -> Void)?) { - evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatches();", completionHandler: completionHandler) - } - public func scrollTo(x: Int, y: Int, animated: Bool) { scrollView.setContentOffset(CGPoint(x: x, y: y), animated: animated) } @@ -3004,8 +3014,11 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { if #available(iOS 15.0, *) { removeObserver(self, forKeyPath: #keyPath(WKWebView.cameraCaptureState)) removeObserver(self, forKeyPath: #keyPath(WKWebView.microphoneCaptureState)) -// removeObserver(self, forKeyPath: #keyPath(WKWebView.fullscreenState)) } + // TODO: Still not working on iOS 16.0! +// if #available(iOS 16.0, *) { +// removeObserver(self, forKeyPath: #keyPath(WKWebView.fullscreenState)) +// } scrollView.removeObserver(self, forKeyPath: #keyPath(UIScrollView.contentOffset)) scrollView.removeObserver(self, forKeyPath: #keyPath(UIScrollView.zoomScale)) resumeTimers() @@ -3044,6 +3057,8 @@ if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { disablePullToRefresh() pullToRefreshControl?.dispose() pullToRefreshControl = nil + findInteractionController?.dispose() + findInteractionController = nil uiDelegate = nil navigationDelegate = nil scrollView.delegate = nil diff --git a/ios/Classes/InAppWebView/InAppWebViewSettings.swift b/ios/Classes/InAppWebView/InAppWebViewSettings.swift index 84c4fd26..60aaf1a8 100755 --- a/ios/Classes/InAppWebView/InAppWebViewSettings.swift +++ b/ios/Classes/InAppWebView/InAppWebViewSettings.swift @@ -74,6 +74,8 @@ public class InAppWebViewSettings: ISettings { var isTextInteractionEnabled = true var isSiteSpecificQuirksModeEnabled = true var upgradeKnownHostsToHTTPS = true + var isElementFullscreenEnabled = true + var isFindInteractionEnabled = false override init(){ super.init() @@ -146,6 +148,10 @@ public class InAppWebViewSettings: ISettings { } if #available(iOS 15.4, *) { realSettings["isSiteSpecificQuirksModeEnabled"] = configuration.preferences.isSiteSpecificQuirksModeEnabled + realSettings["isElementFullscreenEnabled"] = configuration.preferences.isElementFullscreenEnabled + } + if #available(iOS 16.0, *) { + realSettings["isFindInteractionEnabled"] = webView.isFindInteractionEnabled } } return realSettings diff --git a/ios/Classes/InAppWebView/WebViewChannelDelegate.swift b/ios/Classes/InAppWebView/WebViewChannelDelegate.swift index c0efe12f..0af660c7 100644 --- a/ios/Classes/InAppWebView/WebViewChannelDelegate.swift +++ b/ios/Classes/InAppWebView/WebViewChannelDelegate.swift @@ -19,17 +19,22 @@ public class WebViewChannelDelegate : ChannelDelegate { public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { let arguments = call.arguments as? NSDictionary - switch call.method { - case "getUrl": + guard let method = WebViewChannelDelegateMethods.init(rawValue: call.method) else { + result(FlutterMethodNotImplemented) + return + } + + switch method { + case .getUrl: result(webView?.url?.absoluteString) break - case "getTitle": + case .getTitle: result(webView?.title) break - case "getProgress": + case .getProgress: result( (webView != nil) ? Int(webView!.estimatedProgress * 100) : nil ) break - case "loadUrl": + case .loadUrl: let urlRequest = arguments!["urlRequest"] as! [String:Any?] let allowingReadAccessTo = arguments!["allowingReadAccessTo"] as? String var allowingReadAccessToURL: URL? = nil @@ -39,7 +44,7 @@ public class WebViewChannelDelegate : ChannelDelegate { webView?.loadUrl(urlRequest: URLRequest.init(fromPluginMap: urlRequest), allowingReadAccessTo: allowingReadAccessToURL) result(true) break - case "postUrl": + case .postUrl: if let webView = webView { let url = arguments!["url"] as! String let postData = arguments!["postData"] as! FlutterStandardTypedData @@ -47,7 +52,7 @@ public class WebViewChannelDelegate : ChannelDelegate { } result(true) break - case "loadData": + case .loadData: let data = arguments!["data"] as! String let mimeType = arguments!["mimeType"] as! String let encoding = arguments!["encoding"] as! String @@ -60,7 +65,7 @@ public class WebViewChannelDelegate : ChannelDelegate { webView?.loadData(data: data, mimeType: mimeType, encoding: encoding, baseUrl: baseUrl, allowingReadAccessTo: allowingReadAccessToURL) result(true) break - case "loadFile": + case .loadFile: let assetFilePath = arguments!["assetFilePath"] as! String do { @@ -72,7 +77,7 @@ public class WebViewChannelDelegate : ChannelDelegate { } result(true) break - case "evaluateJavascript": + case .evaluateJavascript: if let webView = webView { let source = arguments!["source"] as! String let contentWorldMap = arguments!["contentWorld"] as? [String:Any?] @@ -91,58 +96,58 @@ public class WebViewChannelDelegate : ChannelDelegate { result(nil) } break - case "injectJavascriptFileFromUrl": + case .injectJavascriptFileFromUrl: let urlFile = arguments!["urlFile"] as! String let scriptHtmlTagAttributes = arguments!["scriptHtmlTagAttributes"] as? [String:Any?] webView?.injectJavascriptFileFromUrl(urlFile: urlFile, scriptHtmlTagAttributes: scriptHtmlTagAttributes) result(true) break - case "injectCSSCode": + case .injectCSSCode: let source = arguments!["source"] as! String webView?.injectCSSCode(source: source) result(true) break - case "injectCSSFileFromUrl": + case .injectCSSFileFromUrl: let urlFile = arguments!["urlFile"] as! String let cssLinkHtmlTagAttributes = arguments!["cssLinkHtmlTagAttributes"] as? [String:Any?] webView?.injectCSSFileFromUrl(urlFile: urlFile, cssLinkHtmlTagAttributes: cssLinkHtmlTagAttributes) result(true) break - case "reload": + case .reload: webView?.reload() result(true) break - case "goBack": + case .goBack: webView?.goBack() result(true) break - case "canGoBack": + case .canGoBack: result(webView?.canGoBack ?? false) break - case "goForward": + case .goForward: webView?.goForward() result(true) break - case "canGoForward": + case .canGoForward: result(webView?.canGoForward ?? false) break - case "goBackOrForward": + case .goBackOrForward: let steps = arguments!["steps"] as! Int webView?.goBackOrForward(steps: steps) result(true) break - case "canGoBackOrForward": + case .canGoBackOrForward: let steps = arguments!["steps"] as! Int result(webView?.canGoBackOrForward(steps: steps) ?? false) break - case "stopLoading": + case .stopLoading: webView?.stopLoading() result(true) break - case "isLoading": + case .isLoading: result(webView?.isLoading ?? false) break - case "takeScreenshot": + case .takeScreenshot: if let webView = webView, #available(iOS 11.0, *) { let screenshotConfiguration = arguments!["screenshotConfiguration"] as? [String: Any?] webView.takeScreenshot(with: screenshotConfiguration, completionHandler: { (screenshot) -> Void in @@ -153,7 +158,7 @@ public class WebViewChannelDelegate : ChannelDelegate { result(nil) } break - case "setSettings": + case .setSettings: if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { let inAppBrowserSettings = InAppBrowserSettings() let inAppBrowserSettingsMap = arguments!["settings"] as! [String: Any] @@ -167,14 +172,14 @@ public class WebViewChannelDelegate : ChannelDelegate { } result(true) break - case "getSettings": + case .getSettings: if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { result(iabController.getSettings()) } else { result(webView?.getSettings()) } break - case "close": + case .close: if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { iabController.close { result(true) @@ -183,7 +188,7 @@ public class WebViewChannelDelegate : ChannelDelegate { result(FlutterMethodNotImplemented) } break - case "show": + case .show: if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { iabController.show { result(true) @@ -192,7 +197,7 @@ public class WebViewChannelDelegate : ChannelDelegate { result(FlutterMethodNotImplemented) } break - case "hide": + case .hide: if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { iabController.hide { result(true) @@ -201,13 +206,13 @@ public class WebViewChannelDelegate : ChannelDelegate { result(FlutterMethodNotImplemented) } break - case "getCopyBackForwardList": + case .getCopyBackForwardList: result(webView?.getCopyBackForwardList()) break - case "findAllAsync": - if let webView = webView { + case .findAllAsync: + if let webView = webView, let findInteractionController = webView.findInteractionController { let find = arguments!["find"] as! String - webView.findAllAsync(find: find, completionHandler: {(value, error) in + findInteractionController.findAllAsync(find: find, completionHandler: {(value, error) in if error != nil { result(FlutterError(code: "WebViewChannelDelegate", message: error?.localizedDescription, details: nil)) return @@ -218,10 +223,10 @@ public class WebViewChannelDelegate : ChannelDelegate { result(false) } break - case "findNext": - if let webView = webView { + case .findNext: + if let webView = webView, let findInteractionController = webView.findInteractionController { let forward = arguments!["forward"] as! Bool - webView.findNext(forward: forward, completionHandler: {(value, error) in + findInteractionController.findNext(forward: forward, completionHandler: {(value, error) in if error != nil { result(FlutterError(code: "WebViewChannelDelegate", message: error?.localizedDescription, details: nil)) return @@ -232,9 +237,9 @@ public class WebViewChannelDelegate : ChannelDelegate { result(false) } break - case "clearMatches": - if let webView = webView { - webView.clearMatches(completionHandler: {(value, error) in + case .clearMatches: + if let webView = webView, let findInteractionController = webView.findInteractionController { + findInteractionController.clearMatches(completionHandler: {(value, error) in if error != nil { result(FlutterError(code: "WebViewChannelDelegate", message: error?.localizedDescription, details: nil)) return @@ -245,33 +250,33 @@ public class WebViewChannelDelegate : ChannelDelegate { result(false) } break - case "clearCache": + case .clearCache: webView?.clearCache() result(true) break - case "scrollTo": + case .scrollTo: let x = arguments!["x"] as! Int let y = arguments!["y"] as! Int let animated = arguments!["animated"] as! Bool webView?.scrollTo(x: x, y: y, animated: animated) result(true) break - case "scrollBy": + case .scrollBy: let x = arguments!["x"] as! Int let y = arguments!["y"] as! Int let animated = arguments!["animated"] as! Bool webView?.scrollBy(x: x, y: y, animated: animated) result(true) break - case "pauseTimers": + case .pauseTimers: webView?.pauseTimers() result(true) break - case "resumeTimers": + case .resumeTimers: webView?.resumeTimers() result(true) break - case "printCurrentPage": + case .printCurrentPage: if let webView = webView { let settings = PrintJobSettings() if let settingsMap = arguments!["settings"] as? [String: Any?] { @@ -282,29 +287,29 @@ public class WebViewChannelDelegate : ChannelDelegate { result(nil) } break - case "getContentHeight": + case .getContentHeight: result(webView?.getContentHeight()) break - case "zoomBy": + case .zoomBy: let zoomFactor = (arguments!["zoomFactor"] as! NSNumber).floatValue let animated = arguments!["animated"] as! Bool webView?.zoomBy(zoomFactor: zoomFactor, animated: animated) result(true) break - case "reloadFromOrigin": + case .reloadFromOrigin: webView?.reloadFromOrigin() result(true) break - case "getOriginalUrl": + case .getOriginalUrl: result(webView?.getOriginalUrl()?.absoluteString) break - case "getZoomScale": + case .getZoomScale: result(webView?.getZoomScale()) break - case "hasOnlySecureContent": + case .hasOnlySecureContent: result(webView?.hasOnlySecureContent ?? false) break - case "getSelectedText": + case .getSelectedText: if let webView = webView { webView.getSelectedText { (value, error) in if let err = error { @@ -319,7 +324,7 @@ public class WebViewChannelDelegate : ChannelDelegate { result(nil) } break - case "getHitTestResult": + case .getHitTestResult: if let webView = webView { webView.getHitTestResult { (hitTestResult) in result(hitTestResult.toMap()) @@ -329,11 +334,11 @@ public class WebViewChannelDelegate : ChannelDelegate { result(nil) } break - case "clearFocus": + case .clearFocus: webView?.clearFocus() result(true) break - case "setContextMenu": + case .setContextMenu: if let webView = webView { let contextMenu = arguments!["contextMenu"] as? [String: Any] webView.contextMenu = contextMenu @@ -342,7 +347,7 @@ public class WebViewChannelDelegate : ChannelDelegate { result(false) } break - case "requestFocusNodeHref": + case .requestFocusNodeHref: if let webView = webView { webView.requestFocusNodeHref { (value, error) in if let err = error { @@ -356,7 +361,7 @@ public class WebViewChannelDelegate : ChannelDelegate { result(nil) } break - case "requestImageRef": + case .requestImageRef: if let webView = webView { webView.requestImageRef { (value, error) in if let err = error { @@ -370,24 +375,24 @@ public class WebViewChannelDelegate : ChannelDelegate { result(nil) } break - case "getScrollX": + case .getScrollX: if let webView = webView { result(Int(webView.scrollView.contentOffset.x)) } else { result(nil) } break - case "getScrollY": + case .getScrollY: if let webView = webView { result(Int(webView.scrollView.contentOffset.y)) } else { result(nil) } break - case "getCertificate": + case .getCertificate: result(webView?.getCertificate()?.toMap()) break - case "addUserScript": + case .addUserScript: if let webView = webView { let userScriptMap = arguments!["userScript"] as! [String: Any?] let userScript = UserScript.fromMap(map: userScriptMap, windowId: webView.windowId)! @@ -396,23 +401,23 @@ public class WebViewChannelDelegate : ChannelDelegate { } result(true) break - case "removeUserScript": + case .removeUserScript: let index = arguments!["index"] as! Int let userScriptMap = arguments!["userScript"] as! [String: Any?] let userScript = UserScript.fromMap(map: userScriptMap, windowId: webView?.windowId)! webView?.configuration.userContentController.removeUserOnlyScript(at: index, injectionTime: userScript.injectionTime) result(true) break - case "removeUserScriptsByGroupName": + case .removeUserScriptsByGroupName: let groupName = arguments!["groupName"] as! String webView?.configuration.userContentController.removeUserOnlyScripts(with: groupName) result(true) break - case "removeAllUserScripts": + case .removeAllUserScripts: webView?.configuration.userContentController.removeAllUserOnlyScripts() result(true) break - case "callAsyncJavaScript": + case .callAsyncJavaScript: if let webView = webView, #available(iOS 10.3, *) { if #available(iOS 14.0, *) { let functionBody = arguments!["functionBody"] as! String @@ -436,7 +441,7 @@ public class WebViewChannelDelegate : ChannelDelegate { result(nil) } break - case "createPdf": + case .createPdf: if let webView = webView, #available(iOS 14.0, *) { let configuration = arguments!["pdfConfiguration"] as? [String: Any?] webView.createPdf(configuration: configuration, completionHandler: { (pdf) -> Void in @@ -447,7 +452,7 @@ public class WebViewChannelDelegate : ChannelDelegate { result(nil) } break - case "createWebArchiveData": + case .createWebArchiveData: if let webView = webView, #available(iOS 14.0, *) { webView.createWebArchiveData(dataCompletionHandler: { (webArchiveData) -> Void in result(webArchiveData) @@ -457,7 +462,7 @@ public class WebViewChannelDelegate : ChannelDelegate { result(nil) } break - case "saveWebArchive": + case .saveWebArchive: if let webView = webView, #available(iOS 14.0, *) { let filePath = arguments!["filePath"] as! String let autoname = arguments!["autoname"] as! Bool @@ -469,7 +474,7 @@ public class WebViewChannelDelegate : ChannelDelegate { result(nil) } break - case "isSecureContext": + case .isSecureContext: if let webView = webView { webView.isSecureContext(completionHandler: { (isSecureContext) in result(isSecureContext) @@ -479,7 +484,7 @@ public class WebViewChannelDelegate : ChannelDelegate { result(false) } break - case "createWebMessageChannel": + case .createWebMessageChannel: if let webView = webView { let _ = webView.createWebMessageChannel { (webMessageChannel) in result(webMessageChannel.toMap()) @@ -488,7 +493,7 @@ public class WebViewChannelDelegate : ChannelDelegate { result(nil) } break - case "postWebMessage": + case .postWebMessage: if let webView = webView { let message = arguments!["message"] as! [String: Any?] let targetOrigin = arguments!["targetOrigin"] as! String @@ -516,7 +521,7 @@ public class WebViewChannelDelegate : ChannelDelegate { result(false) } break - case "addWebMessageListener": + case .addWebMessageListener: if let webView = webView { let webMessageListenerMap = arguments!["webMessageListener"] as! [String: Any?] let webMessageListener = WebMessageListener.fromMap(map: webMessageListenerMap)! @@ -530,21 +535,21 @@ public class WebViewChannelDelegate : ChannelDelegate { result(false) } break - case "canScrollVertically": + case .canScrollVertically: if let webView = webView { result(webView.canScrollVertically()) } else { result(false) } break - case "canScrollHorizontally": + case .canScrollHorizontally: if let webView = webView { result(webView.canScrollHorizontally()) } else { result(false) } break - case "pauseAllMediaPlayback": + case .pauseAllMediaPlayback: if let webView = webView, #available(iOS 15.0, *) { webView.pauseAllMediaPlayback(completionHandler: { () -> Void in result(true) @@ -553,7 +558,7 @@ public class WebViewChannelDelegate : ChannelDelegate { result(false) } break - case "setAllMediaPlaybackSuspended": + case .setAllMediaPlaybackSuspended: if let webView = webView, #available(iOS 15.0, *) { let suspended = arguments!["suspended"] as! Bool webView.setAllMediaPlaybackSuspended(suspended, completionHandler: { () -> Void in @@ -563,7 +568,7 @@ public class WebViewChannelDelegate : ChannelDelegate { result(false) } break - case "closeAllMediaPresentations": + case .closeAllMediaPresentations: if let webView = self.webView, #available(iOS 14.5, *) { // closeAllMediaPresentations with completionHandler v15.0 makes the app crash // with error EXC_BAD_ACCESS, so use closeAllMediaPresentations v14.5 @@ -573,7 +578,7 @@ public class WebViewChannelDelegate : ChannelDelegate { result(false) } break - case "requestMediaPlaybackState": + case .requestMediaPlaybackState: if let webView = webView, #available(iOS 15.0, *) { webView.requestMediaPlaybackState(completionHandler: { (state) -> Void in result(state.rawValue) @@ -582,32 +587,33 @@ public class WebViewChannelDelegate : ChannelDelegate { result(nil) } break - case "getMetaThemeColor": + case .getMetaThemeColor: if let webView = webView, #available(iOS 15.0, *) { result(webView.themeColor?.hexString) } else { result(nil) } break - case "isInFullscreen": - // if let webView = webView, #available(iOS 15.0, *) { - // result(webView.fullscreenState == .inFullscreen) - // } + case .isInFullscreen: if let webView = webView { - result(webView.inFullscreen) + if #available(iOS 16.0, *) { + result(webView.fullscreenState == .inFullscreen) + } else { + result(webView.inFullscreen) + } } else { result(false) } break - case "getCameraCaptureState": + case .getCameraCaptureState: if let webView = webView, #available(iOS 15.0, *) { result(webView.cameraCaptureState.rawValue) } else { result(nil) } break - case "setCameraCaptureState": + case .setCameraCaptureState: if let webView = webView, #available(iOS 15.0, *) { let state = WKMediaCaptureState.init(rawValue: arguments!["state"] as! Int) ?? WKMediaCaptureState.none webView.setCameraCaptureState(state) { @@ -617,14 +623,14 @@ public class WebViewChannelDelegate : ChannelDelegate { result(false) } break - case "getMicrophoneCaptureState": + case .getMicrophoneCaptureState: if let webView = webView, #available(iOS 15.0, *) { result(webView.microphoneCaptureState.rawValue) } else { result(nil) } break - case "setMicrophoneCaptureState": + case .setMicrophoneCaptureState: if let webView = webView, #available(iOS 15.0, *) { let state = WKMediaCaptureState.init(rawValue: arguments!["state"] as! Int) ?? WKMediaCaptureState.none webView.setMicrophoneCaptureState(state) { @@ -634,12 +640,10 @@ public class WebViewChannelDelegate : ChannelDelegate { result(false) } break - default: - result(FlutterMethodNotImplemented) - break } } + @available(*, deprecated, message: "Use FindInteractionChannelDelegate.onFindResultReceived instead.") public func onFindResultReceived(activeMatchOrdinal: Int, numberOfMatches: Int, isDoneCounting: Bool) { let arguments: [String : Any?] = [ "activeMatchOrdinal": activeMatchOrdinal, diff --git a/ios/Classes/InAppWebView/WebViewChannelDelegateMethods.swift b/ios/Classes/InAppWebView/WebViewChannelDelegateMethods.swift new file mode 100644 index 00000000..8ba787ed --- /dev/null +++ b/ios/Classes/InAppWebView/WebViewChannelDelegateMethods.swift @@ -0,0 +1,89 @@ +// +// WebViewChannelDelegateMethods.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/10/22. +// + +import Foundation + +public enum WebViewChannelDelegateMethods: String { + case getUrl = "getUrl" + case getTitle = "getTitle" + case getProgress = "getProgress" + case loadUrl = "loadUrl" + case postUrl = "postUrl" + case loadData = "loadData" + case loadFile = "loadFile" + case evaluateJavascript = "evaluateJavascript" + case injectJavascriptFileFromUrl = "injectJavascriptFileFromUrl" + case injectCSSCode = "injectCSSCode" + case injectCSSFileFromUrl = "injectCSSFileFromUrl" + case reload = "reload" + case goBack = "goBack" + case canGoBack = "canGoBack" + case goForward = "goForward" + case canGoForward = "canGoForward" + case goBackOrForward = "goBackOrForward" + case canGoBackOrForward = "canGoBackOrForward" + case stopLoading = "stopLoading" + case isLoading = "isLoading" + case takeScreenshot = "takeScreenshot" + case setSettings = "setSettings" + case getSettings = "getSettings" + case close = "close" + case show = "show" + case hide = "hide" + case getCopyBackForwardList = "getCopyBackForwardList" + @available(*, deprecated, message: "Use FindInteractionController.findAllAsync instead.") + case findAllAsync = "findAllAsync" + @available(*, deprecated, message: "Use FindInteractionController.findNext instead.") + case findNext = "findNext" + @available(*, deprecated, message: "Use FindInteractionController.clearMatches instead.") + case clearMatches = "clearMatches" + case clearCache = "clearCache" + case scrollTo = "scrollTo" + case scrollBy = "scrollBy" + case pauseTimers = "pauseTimers" + case resumeTimers = "resumeTimers" + case printCurrentPage = "printCurrentPage" + case getContentHeight = "getContentHeight" + case zoomBy = "zoomBy" + case reloadFromOrigin = "reloadFromOrigin" + case getOriginalUrl = "getOriginalUrl" + case getZoomScale = "getZoomScale" + case hasOnlySecureContent = "hasOnlySecureContent" + case getSelectedText = "getSelectedText" + case getHitTestResult = "getHitTestResult" + case clearFocus = "clearFocus" + case setContextMenu = "setContextMenu" + case requestFocusNodeHref = "requestFocusNodeHref" + case requestImageRef = "requestImageRef" + case getScrollX = "getScrollX" + case getScrollY = "getScrollY" + case getCertificate = "getCertificate" + case addUserScript = "addUserScript" + case removeUserScript = "removeUserScript" + case removeUserScriptsByGroupName = "removeUserScriptsByGroupName" + case removeAllUserScripts = "removeAllUserScripts" + case callAsyncJavaScript = "callAsyncJavaScript" + case createPdf = "createPdf" + case createWebArchiveData = "createWebArchiveData" + case saveWebArchive = "saveWebArchive" + case isSecureContext = "isSecureContext" + case createWebMessageChannel = "createWebMessageChannel" + case postWebMessage = "postWebMessage" + case addWebMessageListener = "addWebMessageListener" + case canScrollVertically = "canScrollVertically" + case canScrollHorizontally = "canScrollHorizontally" + case pauseAllMediaPlayback = "pauseAllMediaPlayback" + case setAllMediaPlaybackSuspended = "setAllMediaPlaybackSuspended" + case closeAllMediaPresentations = "closeAllMediaPresentations" + case requestMediaPlaybackState = "requestMediaPlaybackState" + case getMetaThemeColor = "getMetaThemeColor" + case isInFullscreen = "isInFullscreen" + case getCameraCaptureState = "getCameraCaptureState" + case setCameraCaptureState = "setCameraCaptureState" + case getMicrophoneCaptureState = "getMicrophoneCaptureState" + case setMicrophoneCaptureState = "setMicrophoneCaptureState" +} diff --git a/ios/Classes/PluginScriptsJS/FindTextHighlightJS.swift b/ios/Classes/PluginScriptsJS/FindTextHighlightJS.swift index ac93adf0..a61a05b7 100644 --- a/ios/Classes/PluginScriptsJS/FindTextHighlightJS.swift +++ b/ios/Classes/PluginScriptsJS/FindTextHighlightJS.swift @@ -42,7 +42,7 @@ window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsyncForElement = function(element, key span.setAttribute( "id", - "WKWEBVIEW_SEARCH_WORD_" + \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) + "\(JAVASCRIPT_BRIDGE_NAME)_SEARCH_WORD_" + \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) ); span.setAttribute("class", "\(JAVASCRIPT_BRIDGE_NAME)_Highlight"); var backgroundColor = \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) == 0 ? "#FF9732" : "#FFFF00"; diff --git a/ios/Classes/PullToRefresh/PullToRefreshControl.swift b/ios/Classes/PullToRefresh/PullToRefreshControl.swift index f2583988..d73bc1e0 100644 --- a/ios/Classes/PullToRefresh/PullToRefreshControl.swift +++ b/ios/Classes/PullToRefresh/PullToRefreshControl.swift @@ -28,17 +28,17 @@ public class PullToRefreshControl : UIRefreshControl, Disposable { } public func prepare() { - if let options = settings { - if options.enabled { + if let settings = settings { + if settings.enabled { delegate?.enablePullToRefresh() } - if let color = options.color, !color.isEmpty { + if let color = settings.color, !color.isEmpty { tintColor = UIColor(hexString: color) } - if let backgroundTintColor = options.backgroundColor, !backgroundTintColor.isEmpty { + if let backgroundTintColor = settings.backgroundColor, !backgroundTintColor.isEmpty { backgroundColor = UIColor(hexString: backgroundTintColor) } - if let attributedTitleMap = options.attributedTitle { + if let attributedTitleMap = settings.attributedTitle { attributedTitle = NSAttributedString.fromMap(map: attributedTitleMap) } } diff --git a/ios/Classes/Types/UIFindSession.swift b/ios/Classes/Types/UIFindSession.swift new file mode 100644 index 00000000..fb19d340 --- /dev/null +++ b/ios/Classes/Types/UIFindSession.swift @@ -0,0 +1,19 @@ +// +// UIFindSession.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/10/22. +// + +import Foundation + +@available(iOS 16.0, *) +extension UIFindSession { + public func toMap () -> [String:Any?] { + return [ + "resultCount": resultCount, + "highlightedResultIndex": highlightedResultIndex, + "searchResultDisplayStyle": searchResultDisplayStyle.rawValue + ] + } +} diff --git a/lib/src/android/webview_feature.g.dart b/lib/src/android/webview_feature.g.dart index d43b7692..1d3ce030 100644 --- a/lib/src/android/webview_feature.g.dart +++ b/lib/src/android/webview_feature.g.dart @@ -62,6 +62,10 @@ class WebViewFeature { static const PROXY_OVERRIDE = WebViewFeature._internal('PROXY_OVERRIDE', 'PROXY_OVERRIDE'); + ///This feature covers [ProxySettings.reverseBypassEnabled]. + static const PROXY_OVERRIDE_REVERSE_BYPASS = WebViewFeature._internal( + 'PROXY_OVERRIDE_REVERSE_BYPASS', 'PROXY_OVERRIDE_REVERSE_BYPASS'); + /// static const RECEIVE_HTTP_ERROR = WebViewFeature._internal('RECEIVE_HTTP_ERROR', 'RECEIVE_HTTP_ERROR'); @@ -223,6 +227,7 @@ class WebViewFeature { WebViewFeature.OFF_SCREEN_PRERASTER, WebViewFeature.POST_WEB_MESSAGE, WebViewFeature.PROXY_OVERRIDE, + WebViewFeature.PROXY_OVERRIDE_REVERSE_BYPASS, WebViewFeature.RECEIVE_HTTP_ERROR, WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR, WebViewFeature.SAFE_BROWSING_ALLOWLIST, diff --git a/lib/src/find_interaction/find_interaction_controller.dart b/lib/src/find_interaction/find_interaction_controller.dart new file mode 100644 index 00000000..eda3b359 --- /dev/null +++ b/lib/src/find_interaction/find_interaction_controller.dart @@ -0,0 +1,226 @@ +import 'dart:developer' as developer; + +import 'package:flutter/services.dart'; +import '../in_app_webview/in_app_webview_settings.dart'; +import '../debug_logging_settings.dart'; +import '../types/main.dart'; + +///**Supported Platforms/Implementations**: +///- Android native WebView +///- iOS +class FindInteractionController { + MethodChannel? _channel; + + ///Debug settings. + static DebugLoggingSettings debugLoggingSettings = DebugLoggingSettings(); + + ///Event fired as find-on-page operations progress. + ///The listener may be notified multiple times while the operation is underway, and the [numberOfMatches] value should not be considered final unless [isDoneCounting] is true. + /// + ///[activeMatchOrdinal] represents the zero-based ordinal of the currently selected match. + /// + ///[numberOfMatches] represents how many matches have been found. + /// + ///[isDoneCounting] whether the find operation has actually completed. + /// + ///**NOTE**: on iOS, if [InAppWebViewSettings.isFindInteractionEnabled] is `true`, this event will not be called. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView ([Official API - WebView.FindListener.onFindResultReceived](https://developer.android.com/reference/android/webkit/WebView.FindListener#onFindResultReceived(int,%20int,%20boolean))) + ///- iOS + final void Function( + FindInteractionController controller, + int activeMatchOrdinal, + int numberOfMatches, + bool isDoneCounting)? onFindResultReceived; + + FindInteractionController({this.onFindResultReceived}) {} + + void initMethodChannel(dynamic id) { + this._channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_find_interaction_$id'); + + this._channel?.setMethodCallHandler((call) async { + try { + return await _handleMethod(call); + } on Error catch (e) { + print(e); + print(e.stackTrace); + } + }); + } + + _debugLog(String method, dynamic args) { + if (FindInteractionController.debugLoggingSettings.enabled) { + for (var regExp + in FindInteractionController.debugLoggingSettings.excludeFilter) { + if (regExp.hasMatch(method)) return; + } + var maxLogMessageLength = + FindInteractionController.debugLoggingSettings.maxLogMessageLength; + String message = "FindInteractionController " + + " calling \"" + + method.toString() + + "\" using " + + args.toString(); + if (maxLogMessageLength >= 0 && message.length > maxLogMessageLength) { + message = message.substring(0, maxLogMessageLength) + "..."; + } + if (!FindInteractionController.debugLoggingSettings.usePrint) { + developer.log(message, name: this.runtimeType.toString()); + } else { + print("[${this.runtimeType.toString()}] $message"); + } + } + } + + Future _handleMethod(MethodCall call) async { + _debugLog(call.method, call.arguments); + + switch (call.method) { + case "onFindResultReceived": + if (onFindResultReceived != null) { + int activeMatchOrdinal = call.arguments["activeMatchOrdinal"]; + int numberOfMatches = call.arguments["numberOfMatches"]; + bool isDoneCounting = call.arguments["isDoneCounting"]; + onFindResultReceived!( + this, activeMatchOrdinal, numberOfMatches, isDoneCounting); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + ///Finds all instances of find on the page and highlights them. Notifies [FindInteractionController.onFindResultReceived] listener. + /// + ///[find] represents the string to find. + /// + ///**NOTE**: on Android native WebView, it finds all instances asynchronously. Successive calls to this will cancel any pending searches. + /// + ///**NOTE**: on iOS, if [InAppWebViewSettings.isFindInteractionEnabled] is `true`, + ///it uses the built-in find interaction native UI, + ///otherwise this is implemented using CSS and Javascript. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView ([Official API - WebView.findAllAsync](https://developer.android.com/reference/android/webkit/WebView#findAllAsync(java.lang.String))) + ///- iOS (if [InAppWebViewSettings.isFindInteractionEnabled] is `true`: [Official API - UIFindInteraction.presentFindNavigator](https://developer.apple.com/documentation/uikit/uifindinteraction/3975832-presentfindnavigator?changes=_2) with [Official API - UIFindInteraction.searchText](https://developer.apple.com/documentation/uikit/uifindinteraction/3975834-searchtext?changes=_2)) + Future findAllAsync({required String find}) async { + Map args = {}; + args.putIfAbsent('find', () => find); + await _channel?.invokeMethod('findAllAsync', args); + } + + ///Highlights and scrolls to the next match found by [findAllAsync]. Notifies [FindInteractionController.onFindResultReceived] listener. + /// + ///[forward] represents the direction to search. + /// + ///**NOTE**: on iOS, if [InAppWebViewSettings.isFindInteractionEnabled] is `true`, + ///it uses the built-in find interaction native UI, + ///otherwise this is implemented using CSS and Javascript. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView ([Official API - WebView.findNext](https://developer.android.com/reference/android/webkit/WebView#findNext(boolean))) + ///- iOS (if [InAppWebViewSettings.isFindInteractionEnabled] is `true`: [Official API - UIFindInteraction.findNext](https://developer.apple.com/documentation/uikit/uifindinteraction/3975829-findnext?changes=_2) and ([Official API - UIFindInteraction.findPrevious](https://developer.apple.com/documentation/uikit/uifindinteraction/3975830-findprevious?changes=_2))) + Future findNext({required bool forward}) async { + Map args = {}; + args.putIfAbsent('forward', () => forward); + await _channel?.invokeMethod('findNext', args); + } + + ///Clears the highlighting surrounding text matches created by [findAllAsync]. + /// + ///**NOTE**: on iOS, if [InAppWebViewSettings.isFindInteractionEnabled] is `true`, + ///it uses the built-in find interaction native UI, + ///otherwise this is implemented using CSS and Javascript. + /// + ///**Supported Platforms/Implementations**: + ///- Android native WebView ([Official API - WebView.clearMatches](https://developer.android.com/reference/android/webkit/WebView#clearMatches())) + ///- iOS (if [InAppWebViewSettings.isFindInteractionEnabled] is `true`: [Official API - UIFindInteraction.dismissFindNavigator](https://developer.apple.com/documentation/uikit/uifindinteraction/3975827-dismissfindnavigator?changes=_2)) + Future clearMatches() async { + Map args = {}; + await _channel?.invokeMethod('clearMatches', args); + } + + ///Pre-populate the system find panel's search text field with a search query. + /// + ///**NOTE**: available only on iOS and only if [InAppWebViewSettings.isFindInteractionEnabled] is `true`. + /// + ///**Supported Platforms/Implementations**: + ///- iOS ([Official API - UIFindInteraction.searchText](https://developer.apple.com/documentation/uikit/uifindinteraction/3975834-searchtext?changes=_2)) + Future setSearchText(String? searchText) async { + Map args = {}; + args.putIfAbsent('searchText', () => searchText); + await _channel?.invokeMethod('setSearchText', args); + } + + ///Get the system find panel's search text field value. + /// + ///**NOTE**: available only on iOS and only if [InAppWebViewSettings.isFindInteractionEnabled] is `true`. + /// + ///**Supported Platforms/Implementations**: + ///- iOS ([Official API - UIFindInteraction.searchText](https://developer.apple.com/documentation/uikit/uifindinteraction/3975834-searchtext?changes=_2)) + Future getSearchText() async { + Map args = {}; + return await _channel?.invokeMethod('getSearchText', args); + } + + ///A Boolean value that indicates when the find panel displays onscreen. + /// + ///**NOTE**: available only on iOS and only if [InAppWebViewSettings.isFindInteractionEnabled] is `true`. + /// + ///**Supported Platforms/Implementations**: + ///- iOS ([Official API - UIFindInteraction.isFindNavigatorVisible](https://developer.apple.com/documentation/uikit/uifindinteraction/3975828-isfindnavigatorvisible?changes=_2)) + Future isFindNavigatorVisible() async { + Map args = {}; + return await _channel?.invokeMethod('isFindNavigatorVisible', args); + } + + ///Updates the results the interface displays for the active search. + /// + ///**NOTE**: available only on iOS and only if [InAppWebViewSettings.isFindInteractionEnabled] is `true`. + /// + ///**Supported Platforms/Implementations**: + ///- iOS ([Official API - UIFindInteraction.updateResultCount](https://developer.apple.com/documentation/uikit/uifindinteraction/3975835-updateresultcount?changes=_2)) + Future updateResultCount() async { + Map args = {}; + await _channel?.invokeMethod('updateResultCount', args); + } + + ///Begins a search, displaying the find panel. + /// + ///**NOTE**: available only on iOS and only if [InAppWebViewSettings.isFindInteractionEnabled] is `true`. + /// + ///**Supported Platforms/Implementations**: + ///- iOS ([Official API - UIFindInteraction.presentFindNavigator](https://developer.apple.com/documentation/uikit/uifindinteraction/3975832-presentfindnavigator?changes=_2)) + Future presentFindNavigator() async { + Map args = {}; + await _channel?.invokeMethod('presentFindNavigator', args); + } + + ///Dismisses the find panel, if present. + /// + ///**NOTE**: available only on iOS and only if [InAppWebViewSettings.isFindInteractionEnabled] is `true`. + /// + ///**Supported Platforms/Implementations**: + ///- iOS ([Official API - UIFindInteraction.dismissFindNavigator](https://developer.apple.com/documentation/uikit/uifindinteraction/3975827-dismissfindnavigator?changes=_2)) + Future dismissFindNavigator() async { + Map args = {}; + await _channel?.invokeMethod('dismissFindNavigator', args); + } + + ///If there's a currently active find session (implying [isFindNavigatorVisible] is `true`), returns the active find session. + /// + ///**NOTE**: available only on iOS and only if [InAppWebViewSettings.isFindInteractionEnabled] is `true`. + /// + ///**Supported Platforms/Implementations**: + ///- iOS ([Official API - UIFindInteraction.activeFindSession](https://developer.apple.com/documentation/uikit/uifindinteraction/3975825-activefindsession?changes=_7____4_8&language=objc)) + Future getActiveFindSession() async { + Map args = {}; + Map? result = + (await _channel?.invokeMethod('getActiveFindSession', args)) + ?.cast(); + return FindSession.fromMap(result); + } +} diff --git a/lib/src/find_interaction/main.dart b/lib/src/find_interaction/main.dart new file mode 100644 index 00000000..99676fa3 --- /dev/null +++ b/lib/src/find_interaction/main.dart @@ -0,0 +1 @@ +export 'find_interaction_controller.dart'; \ No newline at end of file diff --git a/lib/src/in_app_browser/in_app_browser.dart b/lib/src/in_app_browser/in_app_browser.dart index 81d48cf7..9ed69288 100755 --- a/lib/src/in_app_browser/in_app_browser.dart +++ b/lib/src/in_app_browser/in_app_browser.dart @@ -6,6 +6,7 @@ import 'dart:developer' as developer; import 'package:flutter/services.dart'; import '../context_menu.dart'; +import '../find_interaction/find_interaction_controller.dart'; import '../pull_to_refresh/main.dart'; import '../types/main.dart'; @@ -60,6 +61,9 @@ class InAppBrowser { ///Represents the pull-to-refresh feature controller. PullToRefreshController? pullToRefreshController; + ///Represents the find interaction feature controller. + FindInteractionController? findInteractionController; + ///Initial list of user scripts to be loaded at start or end of a page loading. final UnmodifiableListView? initialUserScripts; @@ -129,6 +133,7 @@ class InAppBrowser { _debugLog(call.method, call.arguments); this._isOpened = true; this.pullToRefreshController?.initMethodChannel(id); + this.findInteractionController?.initMethodChannel(id); onBrowserCreated(); break; case "onExit": @@ -662,18 +667,8 @@ class InAppBrowser { return null; } - ///Event fired as find-on-page operations progress. - ///The listener may be notified multiple times while the operation is underway, and the [numberOfMatches] value should not be considered final unless [isDoneCounting] is true. - /// - ///[activeMatchOrdinal] represents the zero-based ordinal of the currently selected match. - /// - ///[numberOfMatches] represents how many matches have been found. - /// - ///[isDoneCounting] whether the find operation has actually completed. - /// - ///**Supported Platforms/Implementations**: - ///- Android native WebView ([Official API - WebView.FindListener.onFindResultReceived](https://developer.android.com/reference/android/webkit/WebView.FindListener#onFindResultReceived(int,%20int,%20boolean))) - ///- iOS + ///Use [FindInteractionController.onFindResultReceived] instead. + @Deprecated('Use FindInteractionController.onFindResultReceived instead') void onFindResultReceived( int activeMatchOrdinal, int numberOfMatches, bool isDoneCounting) {} diff --git a/lib/src/in_app_webview/headless_in_app_webview.dart b/lib/src/in_app_webview/headless_in_app_webview.dart index 51bdb496..bf9b7249 100644 --- a/lib/src/in_app_webview/headless_in_app_webview.dart +++ b/lib/src/in_app_webview/headless_in_app_webview.dart @@ -6,6 +6,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_inappwebview/src/util.dart'; import '../context_menu.dart'; +import '../find_interaction/find_interaction_controller.dart'; import '../types/main.dart'; import '../print_job/main.dart'; import 'webview.dart'; @@ -61,6 +62,7 @@ class HeadlessInAppWebView implements WebView, Disposable { this.contextMenu, this.initialUserScripts, this.pullToRefreshController, + this.findInteractionController, this.implementation = WebViewImplementation.NATIVE, this.onWebViewCreated, this.onLoadStart, @@ -87,6 +89,7 @@ class HeadlessInAppWebView implements WebView, Disposable { this.onReceivedHttpAuthRequest, this.onReceivedServerTrustAuthRequest, this.onReceivedClientCertRequest, + @Deprecated('Use FindInteractionController.onFindResultReceived instead') this.onFindResultReceived, this.shouldInterceptAjaxRequest, this.onAjaxReadyStateChange, @@ -173,6 +176,7 @@ class HeadlessInAppWebView implements WebView, Disposable { switch (call.method) { case "onWebViewCreated": pullToRefreshController?.initMethodChannel(id); + findInteractionController?.initMethodChannel(id); if (onWebViewCreated != null) { onWebViewCreated!(webViewController); } @@ -323,6 +327,9 @@ class HeadlessInAppWebView implements WebView, Disposable { @override final PullToRefreshController? pullToRefreshController; + @override + final FindInteractionController? findInteractionController; + @override final WebViewImplementation implementation; @@ -422,6 +429,8 @@ class HeadlessInAppWebView implements WebView, Disposable { void Function(InAppWebViewController controller, DownloadStartRequest downloadStartRequest)? onDownloadStartRequest; + ///Use [FindInteractionController.onFindResultReceived] instead. + @Deprecated('Use FindInteractionController.onFindResultReceived instead') @override void Function(InAppWebViewController controller, int activeMatchOrdinal, int numberOfMatches, bool isDoneCounting)? onFindResultReceived; diff --git a/lib/src/in_app_webview/in_app_webview.dart b/lib/src/in_app_webview/in_app_webview.dart index b304b29a..4b2acb41 100755 --- a/lib/src/in_app_webview/in_app_webview.dart +++ b/lib/src/in_app_webview/in_app_webview.dart @@ -9,6 +9,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/gestures.dart'; +import '../find_interaction/find_interaction_controller.dart'; import '../web/web_platform_manager.dart'; import '../context_menu.dart'; @@ -50,6 +51,7 @@ class InAppWebView extends StatefulWidget implements WebView { this.initialSettings, this.initialUserScripts, this.pullToRefreshController, + this.findInteractionController, this.implementation = WebViewImplementation.NATIVE, this.contextMenu, this.onWebViewCreated, @@ -77,6 +79,7 @@ class InAppWebView extends StatefulWidget implements WebView { this.onReceivedHttpAuthRequest, this.onReceivedServerTrustAuthRequest, this.onReceivedClientCertRequest, + @Deprecated('Use FindInteractionController.onFindResultReceived instead') this.onFindResultReceived, this.shouldInterceptAjaxRequest, this.onAjaxReadyStateChange, @@ -206,6 +209,9 @@ class InAppWebView extends StatefulWidget implements WebView { @override final PullToRefreshController? pullToRefreshController; + @override + final FindInteractionController? findInteractionController; + @override final ContextMenu? contextMenu; @@ -294,6 +300,8 @@ class InAppWebView extends StatefulWidget implements WebView { final void Function(InAppWebViewController controller, DownloadStartRequest downloadStartRequest)? onDownloadStartRequest; + ///Use [FindInteractionController.onFindResultReceived] instead. + @Deprecated('Use FindInteractionController.onFindResultReceived instead') @override final void Function(InAppWebViewController controller, int activeMatchOrdinal, int numberOfMatches, bool isDoneCounting)? onFindResultReceived; @@ -725,6 +733,7 @@ class _InAppWebViewState extends State { void _onPlatformViewCreated(int id) { _controller = InAppWebViewController(id, widget); widget.pullToRefreshController?.initMethodChannel(id); + widget.findInteractionController?.initMethodChannel(id); if (widget.onWebViewCreated != null) { widget.onWebViewCreated!(_controller!); } 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 000e3863..7abe8fec 100644 --- a/lib/src/in_app_webview/in_app_webview_controller.dart +++ b/lib/src/in_app_webview/in_app_webview_controller.dart @@ -29,6 +29,7 @@ import 'webview.dart'; import '_static_channel.dart'; import '../print_job/main.dart'; +import '../find_interaction/main.dart'; ///List of forbidden names for JavaScript handlers. // ignore: non_constant_identifier_names @@ -804,17 +805,34 @@ class InAppWebViewController { } break; case "onFindResultReceived": - if ((_webview != null && _webview!.onFindResultReceived != null) || + if ((_webview != null && (_webview!.onFindResultReceived != null || + (_webview!.findInteractionController != null && _webview!.findInteractionController!.onFindResultReceived != null))) || _inAppBrowser != null) { int activeMatchOrdinal = call.arguments["activeMatchOrdinal"]; int numberOfMatches = call.arguments["numberOfMatches"]; bool isDoneCounting = call.arguments["isDoneCounting"]; - if (_webview != null && _webview!.onFindResultReceived != null) - _webview!.onFindResultReceived!( + if (_webview != null) { + if (_webview!.findInteractionController != null && + _webview!.findInteractionController!.onFindResultReceived != + null) + _webview!.findInteractionController!.onFindResultReceived!( + _webview!.findInteractionController!, activeMatchOrdinal, + numberOfMatches, isDoneCounting); + else + _webview!.onFindResultReceived!( this, activeMatchOrdinal, numberOfMatches, isDoneCounting); - else - _inAppBrowser!.onFindResultReceived( - activeMatchOrdinal, numberOfMatches, isDoneCounting); + } + else { + if (_inAppBrowser!.findInteractionController != null && + _inAppBrowser!.findInteractionController! + .onFindResultReceived != null) + _inAppBrowser!.findInteractionController!.onFindResultReceived!( + _webview!.findInteractionController!, activeMatchOrdinal, + numberOfMatches, isDoneCounting); + else + _inAppBrowser!.onFindResultReceived( + activeMatchOrdinal, numberOfMatches, isDoneCounting); + } } break; case "onPermissionRequest": @@ -2159,45 +2177,24 @@ class InAppWebViewController { await _channel.invokeMethod('clearCache', args); } - ///Finds all instances of find on the page and highlights them. Notifies [WebView.onFindResultReceived] listener. - /// - ///[find] represents the string to find. - /// - ///**NOTE**: on Android native WebView, it finds all instances asynchronously. Successive calls to this will cancel any pending searches. - /// - ///**NOTE**: on iOS, this is implemented using CSS and Javascript. - /// - ///**Supported Platforms/Implementations**: - ///- Android native WebView ([Official API - WebView.findAllAsync](https://developer.android.com/reference/android/webkit/WebView#findAllAsync(java.lang.String))) - ///- iOS + ///Use [FindInteractionController.findAllAsync] instead. + @Deprecated("Use FindInteractionController.findAllAsync instead") Future findAllAsync({required String find}) async { Map args = {}; args.putIfAbsent('find', () => find); await _channel.invokeMethod('findAllAsync', args); } - ///Highlights and scrolls to the next match found by [findAllAsync]. Notifies [WebView.onFindResultReceived] listener. - /// - ///[forward] represents the direction to search. - /// - ///**NOTE**: on iOS, this is implemented using CSS and Javascript. - /// - ///**Supported Platforms/Implementations**: - ///- Android native WebView ([Official API - WebView.findNext](https://developer.android.com/reference/android/webkit/WebView#findNext(boolean))) - ///- iOS + ///Use [FindInteractionController.findNext] instead. + @Deprecated("Use FindInteractionController.findNext instead") Future findNext({required bool forward}) async { Map args = {}; args.putIfAbsent('forward', () => forward); await _channel.invokeMethod('findNext', args); } - ///Clears the highlighting surrounding text matches created by [findAllAsync()]. - /// - ///**NOTE**: on iOS, this is implemented using CSS and Javascript. - /// - ///**Supported Platforms/Implementations**: - ///- Android native WebView ([Official API - WebView.clearMatches](https://developer.android.com/reference/android/webkit/WebView#clearMatches())) - ///- iOS + ///Use [FindInteractionController.clearMatches] instead. + @Deprecated("Use FindInteractionController.clearMatches instead") Future clearMatches() async { Map args = {}; await _channel.invokeMethod('clearMatches', args); diff --git a/lib/src/in_app_webview/in_app_webview_settings.dart b/lib/src/in_app_webview/in_app_webview_settings.dart index 8a696154..93ad6078 100755 --- a/lib/src/in_app_webview/in_app_webview_settings.dart +++ b/lib/src/in_app_webview/in_app_webview_settings.dart @@ -1057,6 +1057,26 @@ class InAppWebViewSettings { ///- iOS bool upgradeKnownHostsToHTTPS; + ///Sets whether fullscreen API is enabled or not. + /// + ///The default value is `true`. + /// + ///**NOTE**: available on iOS 15.4+. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + bool isElementFullscreenEnabled; + + ///Sets whether the web view's built-in find interaction native UI is enabled or not. + /// + ///The default value is `false`. + /// + ///**NOTE**: available on iOS 16.0+. + /// + ///**Supported Platforms/Implementations**: + ///- iOS + bool isFindInteractionEnabled; + ///Specifies a feature policy for the `