diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index c0837263509d..49269e0cb899 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.1 + +* Replace platform implementation with API built with pigeon. + ## 2.1.0 * Add `zoomEnabled` functionality. @@ -8,7 +12,7 @@ ## 2.0.14 -* Update example App so navigation menu loads immediatly but only becomes available when `WebViewController` is available (same behavior as example App in webview_flutter package). +* Update example App so navigation menu loads immediately but only becomes available when `WebViewController` is available (same behavior as example App in webview_flutter package). ## 2.0.13 diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java index 202be87d7d1e..0d7804c19d4f 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java @@ -12,12 +12,40 @@ class DownloadListenerHostApiImpl implements GeneratedAndroidWebView.DownloadLis private final DownloadListenerCreator downloadListenerCreator; private final GeneratedAndroidWebView.DownloadListenerFlutterApi downloadListenerFlutterApi; + static class DownloadListenerImpl implements DownloadListener, Releasable { + private final Long instanceId; + private final DownloadListenerFlutterApi flutterApi; + private boolean ignoreCallbacks = false; + + DownloadListenerImpl(Long instanceId, DownloadListenerFlutterApi downloadListenerFlutterApi) { + this.instanceId = instanceId; + this.flutterApi = downloadListenerFlutterApi; + } + + @Override + public void onDownloadStart( + String url, + String userAgent, + String contentDisposition, + String mimetype, + long contentLength) { + if (!ignoreCallbacks) { + flutterApi.onDownloadStart( + instanceId, url, userAgent, contentDisposition, mimetype, contentLength, reply -> {}); + } + } + + @Override + public void release() { + ignoreCallbacks = true; + flutterApi.dispose(instanceId, reply -> {}); + } + } + static class DownloadListenerCreator { DownloadListener createDownloadListener( - Long instanceId, DownloadListenerFlutterApi downloadListenerFlutterApi) { - return (url, userAgent, contentDisposition, mimetype, contentLength) -> - downloadListenerFlutterApi.onDownloadStart( - instanceId, url, userAgent, contentDisposition, mimetype, contentLength, reply -> {}); + Long instanceId, DownloadListenerFlutterApi flutterApi) { + return new DownloadListenerImpl(instanceId, flutterApi); } } @@ -36,9 +64,4 @@ public void create(Long instanceId) { downloadListenerCreator.createDownloadListener(instanceId, downloadListenerFlutterApi); instanceManager.addInstance(downloadListener, instanceId); } - - @Override - public void dispose(Long instanceId) { - instanceManager.removeInstance(instanceId); - } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java deleted file mode 100644 index cfad4e315514..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import android.webkit.DownloadListener; -import android.webkit.WebView; - -/** DownloadListener to notify the {@link FlutterWebViewClient} of download starts */ -public class FlutterDownloadListener implements DownloadListener { - private final FlutterWebViewClient webViewClient; - private WebView webView; - - public FlutterDownloadListener(FlutterWebViewClient webViewClient) { - this.webViewClient = webViewClient; - } - - /** Sets the {@link WebView} that the result of the navigation delegate will be send to. */ - public void setWebView(WebView webView) { - this.webView = webView; - } - - @Override - public void onDownloadStart( - String url, - String userAgent, - String contentDisposition, - String mimetype, - long contentLength) { - webViewClient.notifyDownload(webView, url); - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java deleted file mode 100644 index b2a453adcdac..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ /dev/null @@ -1,486 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import android.annotation.TargetApi; -import android.content.Context; -import android.hardware.display.DisplayManager; -import android.os.Build; -import android.os.Handler; -import android.os.Message; -import android.view.View; -import android.webkit.DownloadListener; -import android.webkit.WebChromeClient; -import android.webkit.WebResourceRequest; -import android.webkit.WebStorage; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.platform.PlatformView; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -public class FlutterWebView implements PlatformView, MethodCallHandler { - - private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames"; - private final WebView webView; - private final MethodChannel methodChannel; - private final FlutterWebViewClient flutterWebViewClient; - private final Handler platformThreadHandler; - - // Verifies that a url opened by `Window.open` has a secure url. - private class FlutterWebChromeClient extends WebChromeClient { - - @Override - public boolean onCreateWindow( - final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { - final WebViewClient webViewClient = - new WebViewClient() { - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public boolean shouldOverrideUrlLoading( - @NonNull WebView view, @NonNull WebResourceRequest request) { - final String url = request.getUrl().toString(); - if (!flutterWebViewClient.shouldOverrideUrlLoading( - FlutterWebView.this.webView, request)) { - webView.loadUrl(url); - } - return true; - } - - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (!flutterWebViewClient.shouldOverrideUrlLoading( - FlutterWebView.this.webView, url)) { - webView.loadUrl(url); - } - return true; - } - }; - - final WebView newWebView = new WebView(view.getContext()); - newWebView.setWebViewClient(webViewClient); - - final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; - transport.setWebView(newWebView); - resultMsg.sendToTarget(); - - return true; - } - - @Override - public void onProgressChanged(WebView view, int progress) { - flutterWebViewClient.onLoadingProgress(progress); - } - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) - @SuppressWarnings("unchecked") - FlutterWebView( - final Context context, - MethodChannel methodChannel, - Map params, - View containerView) { - - DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy(); - DisplayManager displayManager = - (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); - displayListenerProxy.onPreWebViewInitialization(displayManager); - - this.methodChannel = methodChannel; - this.methodChannel.setMethodCallHandler(this); - - flutterWebViewClient = new FlutterWebViewClient(methodChannel); - - FlutterDownloadListener flutterDownloadListener = - new FlutterDownloadListener(flutterWebViewClient); - webView = - createWebView( - new WebViewBuilder(context, containerView), - params, - new FlutterWebChromeClient(), - flutterDownloadListener); - flutterDownloadListener.setWebView(webView); - - displayListenerProxy.onPostWebViewInitialization(displayManager); - - platformThreadHandler = new Handler(context.getMainLooper()); - - Map settings = (Map) params.get("settings"); - if (settings != null) { - applySettings(settings); - } - - if (params.containsKey(JS_CHANNEL_NAMES_FIELD)) { - List names = (List) params.get(JS_CHANNEL_NAMES_FIELD); - if (names != null) { - registerJavaScriptChannelNames(names); - } - } - - Integer autoMediaPlaybackPolicy = (Integer) params.get("autoMediaPlaybackPolicy"); - if (autoMediaPlaybackPolicy != null) { - updateAutoMediaPlaybackPolicy(autoMediaPlaybackPolicy); - } - if (params.containsKey("userAgent")) { - String userAgent = (String) params.get("userAgent"); - updateUserAgent(userAgent); - } - if (params.containsKey("initialUrl")) { - String url = (String) params.get("initialUrl"); - webView.loadUrl(url); - } - } - - /** - * Creates a {@link android.webkit.WebView} and configures it according to the supplied - * parameters. - * - *

The {@link WebView} is configured with the following predefined settings: - * - *

    - *
  • always enable the DOM storage API; - *
  • always allow JavaScript to automatically open windows; - *
  • always allow support for multiple windows; - *
  • always use the {@link FlutterWebChromeClient} as web Chrome client. - *
- * - *

Important: This method is visible for testing purposes only and should - * never be called from outside this class. - * - * @param webViewBuilder a {@link WebViewBuilder} which is responsible for building the {@link - * WebView}. - * @param params creation parameters received over the method channel. - * @param webChromeClient an implementation of WebChromeClient This value may be null. - * @return The new {@link android.webkit.WebView} object. - */ - @VisibleForTesting - static WebView createWebView( - WebViewBuilder webViewBuilder, - Map params, - WebChromeClient webChromeClient, - @Nullable DownloadListener downloadListener) { - boolean usesHybridComposition = Boolean.TRUE.equals(params.get("usesHybridComposition")); - webViewBuilder - .setUsesHybridComposition(usesHybridComposition) - .setDomStorageEnabled(true) // Always enable DOM storage API. - .setJavaScriptCanOpenWindowsAutomatically( - true) // Always allow automatically opening of windows. - .setSupportMultipleWindows(true) // Always support multiple windows. - .setWebChromeClient( - webChromeClient) // Always use {@link FlutterWebChromeClient} as web Chrome client. - .setDownloadListener(downloadListener) - .setZoomControlsEnabled(true); // Always use built-in zoom mechanisms. - - return webViewBuilder.build(); - } - - @Override - public View getView() { - return webView; - } - - @Override - public void onInputConnectionUnlocked() { - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).unlockInputConnection(); - } - } - - @Override - public void onInputConnectionLocked() { - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).lockInputConnection(); - } - } - - @Override - public void onFlutterViewAttached(View flutterView) { - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).setContainerView(flutterView); - } - } - - @Override - public void onFlutterViewDetached() { - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).setContainerView(null); - } - } - - @Override - public void onMethodCall(MethodCall methodCall, Result result) { - switch (methodCall.method) { - case "loadUrl": - loadUrl(methodCall, result); - break; - case "updateSettings": - updateSettings(methodCall, result); - break; - case "canGoBack": - canGoBack(result); - break; - case "canGoForward": - canGoForward(result); - break; - case "goBack": - goBack(result); - break; - case "goForward": - goForward(result); - break; - case "reload": - reload(result); - break; - case "currentUrl": - currentUrl(result); - break; - case "evaluateJavascript": - evaluateJavaScript(methodCall, result); - break; - case "addJavascriptChannels": - addJavaScriptChannels(methodCall, result); - break; - case "removeJavascriptChannels": - removeJavaScriptChannels(methodCall, result); - break; - case "clearCache": - clearCache(result); - break; - case "getTitle": - getTitle(result); - break; - case "scrollTo": - scrollTo(methodCall, result); - break; - case "scrollBy": - scrollBy(methodCall, result); - break; - case "getScrollX": - getScrollX(result); - break; - case "getScrollY": - getScrollY(result); - break; - default: - result.notImplemented(); - } - } - - @SuppressWarnings("unchecked") - private void loadUrl(MethodCall methodCall, Result result) { - Map request = (Map) methodCall.arguments; - String url = (String) request.get("url"); - Map headers = (Map) request.get("headers"); - if (headers == null) { - headers = Collections.emptyMap(); - } - webView.loadUrl(url, headers); - result.success(null); - } - - private void canGoBack(Result result) { - result.success(webView.canGoBack()); - } - - private void canGoForward(Result result) { - result.success(webView.canGoForward()); - } - - private void goBack(Result result) { - if (webView.canGoBack()) { - webView.goBack(); - } - result.success(null); - } - - private void goForward(Result result) { - if (webView.canGoForward()) { - webView.goForward(); - } - result.success(null); - } - - private void reload(Result result) { - webView.reload(); - result.success(null); - } - - private void currentUrl(Result result) { - result.success(webView.getUrl()); - } - - @SuppressWarnings("unchecked") - private void updateSettings(MethodCall methodCall, Result result) { - applySettings((Map) methodCall.arguments); - result.success(null); - } - - @TargetApi(Build.VERSION_CODES.KITKAT) - private void evaluateJavaScript(MethodCall methodCall, final Result result) { - String jsString = (String) methodCall.arguments; - if (jsString == null) { - throw new UnsupportedOperationException("JavaScript string cannot be null"); - } - webView.evaluateJavascript( - jsString, - new android.webkit.ValueCallback() { - @Override - public void onReceiveValue(String value) { - result.success(value); - } - }); - } - - @SuppressWarnings("unchecked") - private void addJavaScriptChannels(MethodCall methodCall, Result result) { - List channelNames = (List) methodCall.arguments; - registerJavaScriptChannelNames(channelNames); - result.success(null); - } - - @SuppressWarnings("unchecked") - private void removeJavaScriptChannels(MethodCall methodCall, Result result) { - List channelNames = (List) methodCall.arguments; - for (String channelName : channelNames) { - webView.removeJavascriptInterface(channelName); - } - result.success(null); - } - - private void clearCache(Result result) { - webView.clearCache(true); - WebStorage.getInstance().deleteAllData(); - result.success(null); - } - - private void getTitle(Result result) { - result.success(webView.getTitle()); - } - - private void scrollTo(MethodCall methodCall, Result result) { - Map request = methodCall.arguments(); - int x = (int) request.get("x"); - int y = (int) request.get("y"); - - webView.scrollTo(x, y); - - result.success(null); - } - - private void scrollBy(MethodCall methodCall, Result result) { - Map request = methodCall.arguments(); - int x = (int) request.get("x"); - int y = (int) request.get("y"); - - webView.scrollBy(x, y); - result.success(null); - } - - private void getScrollX(Result result) { - result.success(webView.getScrollX()); - } - - private void getScrollY(Result result) { - result.success(webView.getScrollY()); - } - - private void applySettings(Map settings) { - for (String key : settings.keySet()) { - switch (key) { - case "jsMode": - Integer mode = (Integer) settings.get(key); - if (mode != null) { - updateJsMode(mode); - } - break; - case "hasNavigationDelegate": - final boolean hasNavigationDelegate = (boolean) settings.get(key); - - final WebViewClient webViewClient = - flutterWebViewClient.createWebViewClient(hasNavigationDelegate); - - webView.setWebViewClient(webViewClient); - break; - case "debuggingEnabled": - final boolean debuggingEnabled = (boolean) settings.get(key); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - webView.setWebContentsDebuggingEnabled(debuggingEnabled); - } - break; - case "hasProgressTracking": - flutterWebViewClient.hasProgressTracking = (boolean) settings.get(key); - break; - case "gestureNavigationEnabled": - break; - case "userAgent": - updateUserAgent((String) settings.get(key)); - break; - case "allowsInlineMediaPlayback": - // no-op inline media playback is always allowed on Android. - break; - case "zoomEnabled": - setZoomEnabled((boolean) settings.get(key)); - break; - default: - throw new IllegalArgumentException("Unknown WebView setting: " + key); - } - } - } - - private void updateJsMode(int mode) { - switch (mode) { - case 0: // disabled - webView.getSettings().setJavaScriptEnabled(false); - break; - case 1: // unrestricted - webView.getSettings().setJavaScriptEnabled(true); - break; - default: - throw new IllegalArgumentException("Trying to set unknown JavaScript mode: " + mode); - } - } - - private void updateAutoMediaPlaybackPolicy(int mode) { - // This is the index of the AutoMediaPlaybackPolicy enum, index 1 is always_allow, for all - // other values we require a user gesture. - boolean requireUserGesture = mode != 1; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - webView.getSettings().setMediaPlaybackRequiresUserGesture(requireUserGesture); - } - } - - private void registerJavaScriptChannelNames(List channelNames) { - for (String channelName : channelNames) { - webView.addJavascriptInterface( - new JavaScriptChannel(methodChannel, channelName, platformThreadHandler), channelName); - } - } - - private void updateUserAgent(String userAgent) { - webView.getSettings().setUserAgentString(userAgent); - } - - private void setZoomEnabled(boolean shouldEnable) { - webView.getSettings().setSupportZoom(shouldEnable); - } - - @Override - public void dispose() { - methodChannel.setMethodCallHandler(null); - if (webView instanceof InputAwareWebView) { - ((InputAwareWebView) webView).dispose(); - } - webView.destroy(); - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java deleted file mode 100644 index a86d3e8a4b63..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java +++ /dev/null @@ -1,323 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.graphics.Bitmap; -import android.os.Build; -import android.util.Log; -import android.view.KeyEvent; -import android.webkit.WebResourceError; -import android.webkit.WebResourceRequest; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; -import androidx.webkit.WebResourceErrorCompat; -import androidx.webkit.WebViewClientCompat; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; - -// We need to use WebViewClientCompat to get -// shouldOverrideUrlLoading(WebView view, WebResourceRequest request) -// invoked by the webview on older Android devices, without it pages that use iframes will -// be broken when a navigationDelegate is set on Android version earlier than N. -class FlutterWebViewClient { - private static final String TAG = "FlutterWebViewClient"; - private final MethodChannel methodChannel; - private boolean hasNavigationDelegate; - boolean hasProgressTracking; - - FlutterWebViewClient(MethodChannel methodChannel) { - this.methodChannel = methodChannel; - } - - static String errorCodeToString(int errorCode) { - switch (errorCode) { - case WebViewClient.ERROR_AUTHENTICATION: - return "authentication"; - case WebViewClient.ERROR_BAD_URL: - return "badUrl"; - case WebViewClient.ERROR_CONNECT: - return "connect"; - case WebViewClient.ERROR_FAILED_SSL_HANDSHAKE: - return "failedSslHandshake"; - case WebViewClient.ERROR_FILE: - return "file"; - case WebViewClient.ERROR_FILE_NOT_FOUND: - return "fileNotFound"; - case WebViewClient.ERROR_HOST_LOOKUP: - return "hostLookup"; - case WebViewClient.ERROR_IO: - return "io"; - case WebViewClient.ERROR_PROXY_AUTHENTICATION: - return "proxyAuthentication"; - case WebViewClient.ERROR_REDIRECT_LOOP: - return "redirectLoop"; - case WebViewClient.ERROR_TIMEOUT: - return "timeout"; - case WebViewClient.ERROR_TOO_MANY_REQUESTS: - return "tooManyRequests"; - case WebViewClient.ERROR_UNKNOWN: - return "unknown"; - case WebViewClient.ERROR_UNSAFE_RESOURCE: - return "unsafeResource"; - case WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME: - return "unsupportedAuthScheme"; - case WebViewClient.ERROR_UNSUPPORTED_SCHEME: - return "unsupportedScheme"; - } - - final String message = - String.format(Locale.getDefault(), "Could not find a string for errorCode: %d", errorCode); - throw new IllegalArgumentException(message); - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - if (!hasNavigationDelegate) { - return false; - } - notifyOnNavigationRequest( - request.getUrl().toString(), request.getRequestHeaders(), view, request.isForMainFrame()); - // We must make a synchronous decision here whether to allow the navigation or not, - // if the Dart code has set a navigation delegate we want that delegate to decide whether - // to navigate or not, and as we cannot get a response from the Dart delegate synchronously we - // return true here to block the navigation, if the Dart delegate decides to allow the - // navigation the plugin will later make an addition loadUrl call for this url. - // - // Since we cannot call loadUrl for a subframe, we currently only allow the delegate to stop - // navigations that target the main frame, if the request is not for the main frame - // we just return false to allow the navigation. - // - // For more details see: https://github.com/flutter/flutter/issues/25329#issuecomment-464863209 - return request.isForMainFrame(); - } - - boolean shouldOverrideUrlLoading(WebView view, String url) { - if (!hasNavigationDelegate) { - return false; - } - // This version of shouldOverrideUrlLoading is only invoked by the webview on devices with - // webview versions earlier than 67(it is also invoked when hasNavigationDelegate is false). - // On these devices we cannot tell whether the navigation is targeted to the main frame or not. - // We proceed assuming that the navigation is targeted to the main frame. If the page had any - // frames they will be loaded in the main frame instead. - Log.w( - TAG, - "Using a navigationDelegate with an old webview implementation, pages with frames or iframes will not work"); - notifyOnNavigationRequest(url, null, view, true); - return true; - } - - /** - * Notifies the Flutter code that a download should start when a navigation delegate is set. - * - * @param view the webView the result of the navigation delegate will be send to. - * @param url the download url - * @return A boolean whether or not the request is forwarded to the Flutter code. - */ - boolean notifyDownload(WebView view, String url) { - if (!hasNavigationDelegate) { - return false; - } - - notifyOnNavigationRequest(url, null, view, true); - return true; - } - - private void onPageStarted(WebView view, String url) { - Map args = new HashMap<>(); - args.put("url", url); - methodChannel.invokeMethod("onPageStarted", args); - } - - private void onPageFinished(WebView view, String url) { - Map args = new HashMap<>(); - args.put("url", url); - methodChannel.invokeMethod("onPageFinished", args); - } - - void onLoadingProgress(int progress) { - if (hasProgressTracking) { - Map args = new HashMap<>(); - args.put("progress", progress); - methodChannel.invokeMethod("onProgress", args); - } - } - - private void onWebResourceError( - final int errorCode, final String description, final String failingUrl) { - final Map args = new HashMap<>(); - args.put("errorCode", errorCode); - args.put("description", description); - args.put("errorType", FlutterWebViewClient.errorCodeToString(errorCode)); - args.put("failingUrl", failingUrl); - methodChannel.invokeMethod("onWebResourceError", args); - } - - private void notifyOnNavigationRequest( - String url, Map headers, WebView webview, boolean isMainFrame) { - HashMap args = new HashMap<>(); - args.put("url", url); - args.put("isForMainFrame", isMainFrame); - if (isMainFrame) { - methodChannel.invokeMethod( - "navigationRequest", args, new OnNavigationRequestResult(url, headers, webview)); - } else { - methodChannel.invokeMethod("navigationRequest", args); - } - } - - // This method attempts to avoid using WebViewClientCompat due to bug - // https://bugs.chromium.org/p/chromium/issues/detail?id=925887. Also, see - // https://github.com/flutter/flutter/issues/29446. - WebViewClient createWebViewClient(boolean hasNavigationDelegate) { - this.hasNavigationDelegate = hasNavigationDelegate; - - if (!hasNavigationDelegate || android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return internalCreateWebViewClient(); - } - - return internalCreateWebViewClientCompat(); - } - - private WebViewClient internalCreateWebViewClient() { - return new WebViewClient() { - @TargetApi(Build.VERSION_CODES.N) - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request); - } - - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - FlutterWebViewClient.this.onPageStarted(view, url); - } - - @Override - public void onPageFinished(WebView view, String url) { - FlutterWebViewClient.this.onPageFinished(view, url); - } - - @TargetApi(Build.VERSION_CODES.M) - @Override - public void onReceivedError( - WebView view, WebResourceRequest request, WebResourceError error) { - if (request.isForMainFrame()) { - FlutterWebViewClient.this.onWebResourceError( - error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); - } - } - - @Override - public void onReceivedError( - WebView view, int errorCode, String description, String failingUrl) { - FlutterWebViewClient.this.onWebResourceError(errorCode, description, failingUrl); - } - - @Override - public void onUnhandledKeyEvent(WebView view, KeyEvent event) { - // Deliberately empty. Occasionally the webview will mark events as having failed to be - // handled even though they were handled. We don't want to propagate those as they're not - // truly lost. - } - }; - } - - private WebViewClientCompat internalCreateWebViewClientCompat() { - return new WebViewClientCompat() { - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request); - } - - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, url); - } - - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - FlutterWebViewClient.this.onPageStarted(view, url); - } - - @Override - public void onPageFinished(WebView view, String url) { - FlutterWebViewClient.this.onPageFinished(view, url); - } - - // This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is - // enabled. The deprecated method is called when a device doesn't support this. - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - @SuppressLint("RequiresFeature") - @Override - public void onReceivedError( - @NonNull WebView view, - @NonNull WebResourceRequest request, - @NonNull WebResourceErrorCompat error) { - if (request.isForMainFrame()) { - FlutterWebViewClient.this.onWebResourceError( - error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); - } - } - - @Override - public void onReceivedError( - WebView view, int errorCode, String description, String failingUrl) { - FlutterWebViewClient.this.onWebResourceError(errorCode, description, failingUrl); - } - - @Override - public void onUnhandledKeyEvent(WebView view, KeyEvent event) { - // Deliberately empty. Occasionally the webview will mark events as having failed to be - // handled even though they were handled. We don't want to propagate those as they're not - // truly lost. - } - }; - } - - private static class OnNavigationRequestResult implements MethodChannel.Result { - private final String url; - private final Map headers; - private final WebView webView; - - private OnNavigationRequestResult(String url, Map headers, WebView webView) { - this.url = url; - this.headers = headers; - this.webView = webView; - } - - @Override - public void success(Object shouldLoad) { - Boolean typedShouldLoad = (Boolean) shouldLoad; - if (typedShouldLoad) { - loadUrl(); - } - } - - @Override - public void error(String errorCode, String s1, Object o) { - throw new IllegalStateException("navigationRequest calls must succeed"); - } - - @Override - public void notImplemented() { - throw new IllegalStateException( - "navigationRequest must be implemented by the webview method channel"); - } - - private void loadUrl() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - webView.loadUrl(url, headers); - } else { - webView.loadUrl(url); - } - } - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java index 8fe58104a0fb..ed3d40177632 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java @@ -5,29 +5,24 @@ package io.flutter.plugins.webviewflutter; import android.content.Context; -import android.view.View; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.StandardMessageCodec; import io.flutter.plugin.platform.PlatformView; import io.flutter.plugin.platform.PlatformViewFactory; -import java.util.Map; public final class FlutterWebViewFactory extends PlatformViewFactory { - private final BinaryMessenger messenger; - private final View containerView; + private final InstanceManager instanceManager; - FlutterWebViewFactory(BinaryMessenger messenger, View containerView) { + FlutterWebViewFactory(InstanceManager instanceManager) { super(StandardMessageCodec.INSTANCE); - this.messenger = messenger; - this.containerView = containerView; + this.instanceManager = instanceManager; } - @SuppressWarnings("unchecked") @Override public PlatformView create(Context context, int id, Object args) { - Map params = (Map) args; - MethodChannel methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id); - return new FlutterWebView(context, methodChannel, params, containerView); + final PlatformView view = (PlatformView) instanceManager.getInstance((Integer) args); + if (view == null) { + throw new IllegalStateException("Unable to find WebView instance: " + args); + } + return view; } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java index ba2b9b1ac481..a94279e6b89e 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v1.0.7), do not edit directly. +// Autogenerated from Pigeon (v1.0.8), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.webviewflutter; @@ -1313,8 +1313,6 @@ private JavaScriptChannelHostApiCodec() {} public interface JavaScriptChannelHostApi { void create(Long instanceId, String channelName); - void dispose(Long instanceId); - /** The codec used by JavaScriptChannelHostApi. */ static MessageCodec getCodec() { return JavaScriptChannelHostApiCodec.INSTANCE; @@ -1354,31 +1352,6 @@ static void setup(BinaryMessenger binaryMessenger, JavaScriptChannelHostApi api) channel.setMessageHandler(null); } } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.JavaScriptChannelHostApi.dispose", getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList) message; - Number instanceIdArg = (Number) args.get(0); - if (instanceIdArg == null) { - throw new NullPointerException("instanceIdArg unexpectedly null."); - } - api.dispose(instanceIdArg.longValue()); - wrapped.put("result", null); - } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } } } @@ -1405,6 +1378,19 @@ static MessageCodec getCodec() { return JavaScriptChannelFlutterApiCodec.INSTANCE; } + public void dispose(Long instanceIdArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.JavaScriptChannelFlutterApi.dispose", + getCodec()); + channel.send( + new ArrayList(Arrays.asList(instanceIdArg)), + channelReply -> { + callback.reply(null); + }); + } + public void postMessage(Long instanceIdArg, String messageArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( @@ -1429,8 +1415,6 @@ private WebViewClientHostApiCodec() {} public interface WebViewClientHostApi { void create(Long instanceId, Boolean shouldOverrideUrlLoading); - void dispose(Long instanceId); - /** The codec used by WebViewClientHostApi. */ static MessageCodec getCodec() { return WebViewClientHostApiCodec.INSTANCE; @@ -1471,31 +1455,6 @@ static void setup(BinaryMessenger binaryMessenger, WebViewClientHostApi api) { channel.setMessageHandler(null); } } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.WebViewClientHostApi.dispose", getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList) message; - Number instanceIdArg = (Number) args.get(0); - if (instanceIdArg == null) { - throw new NullPointerException("instanceIdArg unexpectedly null."); - } - api.dispose(instanceIdArg.longValue()); - wrapped.put("result", null); - } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } } } @@ -1513,9 +1472,6 @@ protected Object readValueOfType(byte type, ByteBuffer buffer) { case (byte) 129: return WebResourceRequestData.fromMap((Map) readValue(buffer)); - case (byte) 130: - return WebResourceRequestData.fromMap((Map) readValue(buffer)); - default: return super.readValueOfType(type, buffer); } @@ -1529,9 +1485,6 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { } else if (value instanceof WebResourceRequestData) { stream.write(129); writeValue(stream, ((WebResourceRequestData) value).toMap()); - } else if (value instanceof WebResourceRequestData) { - stream.write(130); - writeValue(stream, ((WebResourceRequestData) value).toMap()); } else { super.writeValue(stream, value); } @@ -1554,6 +1507,17 @@ static MessageCodec getCodec() { return WebViewClientFlutterApiCodec.INSTANCE; } + public void dispose(Long instanceIdArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebViewClientFlutterApi.dispose", getCodec()); + channel.send( + new ArrayList(Arrays.asList(instanceIdArg)), + channelReply -> { + callback.reply(null); + }); + } + public void onPageStarted( Long instanceIdArg, Long webViewInstanceIdArg, String urlArg, Reply callback) { BasicMessageChannel channel = @@ -1666,8 +1630,6 @@ private DownloadListenerHostApiCodec() {} public interface DownloadListenerHostApi { void create(Long instanceId); - void dispose(Long instanceId); - /** The codec used by DownloadListenerHostApi. */ static MessageCodec getCodec() { return DownloadListenerHostApiCodec.INSTANCE; @@ -1703,31 +1665,6 @@ static void setup(BinaryMessenger binaryMessenger, DownloadListenerHostApi api) channel.setMessageHandler(null); } } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.DownloadListenerHostApi.dispose", getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList) message; - Number instanceIdArg = (Number) args.get(0); - if (instanceIdArg == null) { - throw new NullPointerException("instanceIdArg unexpectedly null."); - } - api.dispose(instanceIdArg.longValue()); - wrapped.put("result", null); - } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } } } @@ -1754,6 +1691,17 @@ static MessageCodec getCodec() { return DownloadListenerFlutterApiCodec.INSTANCE; } + public void dispose(Long instanceIdArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.DownloadListenerFlutterApi.dispose", getCodec()); + channel.send( + new ArrayList(Arrays.asList(instanceIdArg)), + channelReply -> { + callback.reply(null); + }); + } + public void onDownloadStart( Long instanceIdArg, String urlArg, @@ -1792,8 +1740,6 @@ private WebChromeClientHostApiCodec() {} public interface WebChromeClientHostApi { void create(Long instanceId, Long webViewClientInstanceId); - void dispose(Long instanceId); - /** The codec used by WebChromeClientHostApi. */ static MessageCodec getCodec() { return WebChromeClientHostApiCodec.INSTANCE; @@ -1833,31 +1779,6 @@ static void setup(BinaryMessenger binaryMessenger, WebChromeClientHostApi api) { channel.setMessageHandler(null); } } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.WebChromeClientHostApi.dispose", getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList) message; - Number instanceIdArg = (Number) args.get(0); - if (instanceIdArg == null) { - throw new NullPointerException("instanceIdArg unexpectedly null."); - } - api.dispose(instanceIdArg.longValue()); - wrapped.put("result", null); - } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } } } @@ -1884,6 +1805,17 @@ static MessageCodec getCodec() { return WebChromeClientFlutterApiCodec.INSTANCE; } + public void dispose(Long instanceIdArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WebChromeClientFlutterApi.dispose", getCodec()); + channel.send( + new ArrayList(Arrays.asList(instanceIdArg)), + channelReply -> { + callback.reply(null); + }); + } + public void onProgressChanged( Long instanceIdArg, Long webViewInstanceIdArg, Long progressArg, Reply callback) { BasicMessageChannel channel = diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java index bfa7d6f17345..58e8d7990798 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java @@ -19,12 +19,13 @@ void addInstance(Object instance, long instanceId) { } /** Remove the instance from the manager. */ - void removeInstance(long instanceId) { + Object removeInstance(long instanceId) { final Object instance = instanceIdsToInstances.get(instanceId); if (instance != null) { instanceIdsToInstances.remove(instanceId); instancesToInstanceIds.remove(instance); } + return instance; } /** Retrieve the Object paired with instanceId. */ diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java index 2f987c0f86b3..adfddadb3e35 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java @@ -7,8 +7,7 @@ import android.os.Handler; import android.os.Looper; import android.webkit.JavascriptInterface; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelFlutterApi; /** * Added as a JavaScript interface to the WebView for any JavaScript channel that the Dart code sets @@ -17,42 +16,47 @@ *

Exposes a single method named `postMessage` to JavaScript, which sends a message over a method * channel to the Dart code. */ -class JavaScriptChannel { - private final MethodChannel methodChannel; +class JavaScriptChannel implements Releasable { + private final Long instanceId; + private final GeneratedAndroidWebView.JavaScriptChannelFlutterApi javaScriptChannelFlutterApi; final String javaScriptChannelName; private final Handler platformThreadHandler; + private boolean ignoreCallbacks = false; /** - * @param methodChannel the Flutter WebView method channel to which JS messages are sent - * @param javaScriptChannelName the name of the JavaScript channel, this is sent over the method - * channel with each message to let the Dart code know which JavaScript channel the message - * was sent through + * @param instanceId identifier for this object when messages are sent to Dart + * @param javaScriptChannelFlutterApi the Flutter Api to which JS messages are sent + * @param channelName the name of the JavaScript channel, this is sent over the method channel + * with each message to let the Dart code know which JavaScript channel the message was sent + * through + * @param platformThreadHandler handles making callbacks on the desired thread */ JavaScriptChannel( - MethodChannel methodChannel, String javaScriptChannelName, Handler platformThreadHandler) { - this.methodChannel = methodChannel; - this.javaScriptChannelName = javaScriptChannelName; + Long instanceId, + JavaScriptChannelFlutterApi javaScriptChannelFlutterApi, + String channelName, + Handler platformThreadHandler) { + this.instanceId = instanceId; + this.javaScriptChannelFlutterApi = javaScriptChannelFlutterApi; + this.javaScriptChannelName = channelName; this.platformThreadHandler = platformThreadHandler; } - // Suppressing unused warning as this is invoked from JavaScript. - @SuppressWarnings("unused") @JavascriptInterface public void postMessage(final String message) { - Runnable postMessageRunnable = - new Runnable() { - @Override - public void run() { - HashMap arguments = new HashMap<>(); - arguments.put("channel", javaScriptChannelName); - arguments.put("message", message); - methodChannel.invokeMethod("javascriptChannelMessage", arguments); - } - }; - if (platformThreadHandler.getLooper() == Looper.myLooper()) { - postMessageRunnable.run(); - } else { - platformThreadHandler.post(postMessageRunnable); + if (!ignoreCallbacks) { + final Runnable postMessageRunnable = + () -> javaScriptChannelFlutterApi.postMessage(instanceId, message, reply -> {}); + if (platformThreadHandler.getLooper() == Looper.myLooper()) { + postMessageRunnable.run(); + } else { + platformThreadHandler.post(postMessageRunnable); + } } } + + public void release() { + ignoreCallbacks = true; + javaScriptChannelFlutterApi.dispose(instanceId, reply -> {}); + } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java index 2d42d952955c..3a3f3cdbc105 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelHostApiImpl.java @@ -5,44 +5,32 @@ package io.flutter.plugins.webviewflutter; import android.os.Handler; -import android.os.Looper; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelFlutterApi; class JavaScriptChannelHostApiImpl implements GeneratedAndroidWebView.JavaScriptChannelHostApi { private final InstanceManager instanceManager; private final JavaScriptChannelCreator javaScriptChannelCreator; - private final JavaScriptChannelFlutterApi javaScriptChannelFlutterApi; + private final JavaScriptChannelFlutterApi flutterApi; private final Handler platformThreadHandler; static class JavaScriptChannelCreator { JavaScriptChannel createJavaScriptChannel( Long instanceId, - JavaScriptChannelFlutterApi javaScriptChannelFlutterApi, + JavaScriptChannelFlutterApi flutterApi, String channelName, Handler platformThreadHandler) { - return new JavaScriptChannel(null, channelName, platformThreadHandler) { - @Override - public void postMessage(String message) { - final Runnable postMessageRunnable = - () -> javaScriptChannelFlutterApi.postMessage(instanceId, message, reply -> {}); - if (platformThreadHandler.getLooper() == Looper.myLooper()) { - postMessageRunnable.run(); - } else { - platformThreadHandler.post(postMessageRunnable); - } - } - }; + return new JavaScriptChannel(instanceId, flutterApi, channelName, platformThreadHandler); } } JavaScriptChannelHostApiImpl( InstanceManager instanceManager, JavaScriptChannelCreator javaScriptChannelCreator, - JavaScriptChannelFlutterApi javaScriptChannelFlutterApi, + JavaScriptChannelFlutterApi flutterApi, Handler platformThreadHandler) { this.instanceManager = instanceManager; this.javaScriptChannelCreator = javaScriptChannelCreator; - this.javaScriptChannelFlutterApi = javaScriptChannelFlutterApi; + this.flutterApi = flutterApi; this.platformThreadHandler = platformThreadHandler; } @@ -50,12 +38,7 @@ public void postMessage(String message) { public void create(Long instanceId, String channelName) { final JavaScriptChannel javaScriptChannel = javaScriptChannelCreator.createJavaScriptChannel( - instanceId, javaScriptChannelFlutterApi, channelName, platformThreadHandler); + instanceId, flutterApi, channelName, platformThreadHandler); instanceManager.addInstance(javaScriptChannel, instanceId); } - - @Override - public void dispose(Long instanceId) { - instanceManager.removeInstance(instanceId); - } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/Releasable.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/Releasable.java new file mode 100644 index 000000000000..020de09365bb --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/Releasable.java @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +/** + * Represents a resource, or a holder of resources, which may be released once they are no longer + * needed. + */ +interface Releasable { + void release(); +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java index 32f8fcbdeed9..f76cd3136655 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java @@ -17,62 +17,92 @@ class WebChromeClientHostApiImpl implements GeneratedAndroidWebView.WebChromeClientHostApi { private final InstanceManager instanceManager; private final WebChromeClientCreator webChromeClientCreator; - private final WebChromeClientFlutterApi webChromeClientFlutterApi; + private final WebChromeClientFlutterApi flutterApi; + + static class WebChromeClientImpl extends WebChromeClient implements Releasable { + private final Long instanceId; + private final InstanceManager instanceManager; + private final WebChromeClientFlutterApi flutterApi; + private WebViewClient webViewClient; + private boolean ignoreCallbacks = false; + + WebChromeClientImpl( + Long instanceId, + InstanceManager instanceManager, + WebChromeClientFlutterApi flutterApi, + WebViewClient webViewClient) { + this.instanceId = instanceId; + this.instanceManager = instanceManager; + this.flutterApi = flutterApi; + this.webViewClient = webViewClient; + } + + // Verifies that a url opened by `Window.open` has a secure url. + @Override + public boolean onCreateWindow( + final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { + final WebViewClient newWindowWebViewClient = + new WebViewClient() { + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public boolean shouldOverrideUrlLoading( + @NonNull WebView view, @NonNull WebResourceRequest request) { + webViewClient.shouldOverrideUrlLoading(view, request); + return true; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + webViewClient.shouldOverrideUrlLoading(view, url); + return true; + } + }; + + final WebView newWebView = new WebView(view.getContext()); + newWebView.setWebViewClient(newWindowWebViewClient); + + final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; + transport.setWebView(newWebView); + resultMsg.sendToTarget(); + + return true; + } + + @Override + public void onProgressChanged(WebView view, int progress) { + if (!ignoreCallbacks) { + flutterApi.onProgressChanged( + instanceId, instanceManager.getInstanceId(view), (long) progress, reply -> {}); + } + } + + void setWebViewClient(WebViewClient webViewClient) { + this.webViewClient = webViewClient; + } + + public void release() { + ignoreCallbacks = true; + flutterApi.dispose(instanceId, reply -> {}); + } + } static class WebChromeClientCreator { WebChromeClient createWebChromeClient( Long instanceId, InstanceManager instanceManager, - WebViewClient webViewClient, - WebChromeClientFlutterApi webChromeClientFlutterApi) { - return new WebChromeClient() { - // Verifies that a url opened by `Window.open` has a secure url. - @Override - public boolean onCreateWindow( - final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { - final WebViewClient newWindowWebViewClient = - new WebViewClient() { - @RequiresApi(api = Build.VERSION_CODES.N) - @Override - public boolean shouldOverrideUrlLoading( - @NonNull WebView view, @NonNull WebResourceRequest request) { - webViewClient.shouldOverrideUrlLoading(view, request); - return true; - } - - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - webViewClient.shouldOverrideUrlLoading(view, url); - return true; - } - }; - - final WebView newWebView = new WebView(view.getContext()); - newWebView.setWebViewClient(newWindowWebViewClient); - - final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; - transport.setWebView(newWebView); - resultMsg.sendToTarget(); - - return true; - } - - @Override - public void onProgressChanged(WebView view, int progress) { - webChromeClientFlutterApi.onProgressChanged( - instanceId, instanceManager.getInstanceId(view), (long) progress, reply -> {}); - } - }; + WebChromeClientFlutterApi flutterApi, + WebViewClient webViewClient) { + return new WebChromeClientImpl(instanceId, instanceManager, flutterApi, webViewClient); } } WebChromeClientHostApiImpl( InstanceManager instanceManager, WebChromeClientCreator webChromeClientCreator, - WebChromeClientFlutterApi webChromeClientFlutterApi) { + WebChromeClientFlutterApi flutterApi) { this.instanceManager = instanceManager; this.webChromeClientCreator = webChromeClientCreator; - this.webChromeClientFlutterApi = webChromeClientFlutterApi; + this.flutterApi = flutterApi; } @Override @@ -81,12 +111,7 @@ public void create(Long instanceId, Long webViewClientInstanceId) { (WebViewClient) instanceManager.getInstance(webViewClientInstanceId); final WebChromeClient webChromeClient = webChromeClientCreator.createWebChromeClient( - instanceId, instanceManager, webViewClient, webChromeClientFlutterApi); + instanceId, instanceManager, flutterApi, webViewClient); instanceManager.addInstance(webChromeClient, instanceId); } - - @Override - public void dispose(Long instanceId) { - instanceManager.removeInstance(instanceId); - } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java deleted file mode 100644 index e0d5e8815f31..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import android.content.Context; -import android.view.View; -import android.webkit.DownloadListener; -import android.webkit.WebChromeClient; -import android.webkit.WebSettings; -import android.webkit.WebView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -/** Builder used to create {@link android.webkit.WebView} objects. */ -public class WebViewBuilder { - - /** Factory used to create a new {@link android.webkit.WebView} instance. */ - static class WebViewFactory { - - /** - * Creates a new {@link android.webkit.WebView} instance. - * - * @param context an Activity Context to access application assets. This value cannot be null. - * @param usesHybridComposition If {@code false} a {@link InputAwareWebView} instance is - * returned. - * @param containerView must be supplied when the {@code useHybridComposition} parameter is set - * to {@code false}. Used to create an InputConnection on the WebView's dedicated input, or - * IME, thread (see also {@link InputAwareWebView}) - * @return A new instance of the {@link android.webkit.WebView} object. - */ - static WebView create(Context context, boolean usesHybridComposition, View containerView) { - return usesHybridComposition - ? new WebView(context) - : new InputAwareWebView(context, containerView); - } - } - - private final Context context; - private final View containerView; - - private boolean enableDomStorage; - private boolean javaScriptCanOpenWindowsAutomatically; - private boolean supportMultipleWindows; - private boolean usesHybridComposition; - private WebChromeClient webChromeClient; - private DownloadListener downloadListener; - private boolean enableBuiltInZoomControls; - - /** - * Constructs a new {@link WebViewBuilder} object with a custom implementation of the {@link - * WebViewFactory} object. - * - * @param context an Activity Context to access application assets. This value cannot be null. - * @param containerView must be supplied when the {@code useHybridComposition} parameter is set to - * {@code false}. Used to create an InputConnection on the WebView's dedicated input, or IME, - * thread (see also {@link InputAwareWebView}) - */ - WebViewBuilder(@NonNull final Context context, View containerView) { - this.context = context; - this.containerView = containerView; - } - - /** - * Sets whether the DOM storage API is enabled. The default value is {@code false}. - * - * @param flag {@code true} is {@link android.webkit.WebView} should use the DOM storage API. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setDomStorageEnabled(boolean flag) { - this.enableDomStorage = flag; - return this; - } - - /** - * Sets whether JavaScript is allowed to open windows automatically. This applies to the - * JavaScript function {@code window.open()}. The default value is {@code false}. - * - * @param flag {@code true} if JavaScript is allowed to open windows automatically. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setJavaScriptCanOpenWindowsAutomatically(boolean flag) { - this.javaScriptCanOpenWindowsAutomatically = flag; - return this; - } - - /** - * Sets whether the {@link WebView} supports multiple windows. If set to {@code true}, {@link - * WebChromeClient#onCreateWindow} must be implemented by the host application. The default is - * {@code false}. - * - * @param flag {@code true} if multiple windows are supported. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setSupportMultipleWindows(boolean flag) { - this.supportMultipleWindows = flag; - return this; - } - - /** - * Sets whether the hybrid composition should be used. - * - *

If set to {@code true} a standard {@link WebView} is created. If set to {@code false} the - * {@link WebViewBuilder} will create a {@link InputAwareWebView} to workaround issues using the - * {@link WebView} on Android versions below N. - * - * @param flag {@code true} if uses hybrid composition. The default is {@code false}. - * @return This builder. This value cannot be {@code null} - */ - public WebViewBuilder setUsesHybridComposition(boolean flag) { - this.usesHybridComposition = flag; - return this; - } - - /** - * Sets the chrome handler. This is an implementation of WebChromeClient for use in handling - * JavaScript dialogs, favicons, titles, and the progress. This will replace the current handler. - * - * @param webChromeClient an implementation of WebChromeClient This value may be null. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setWebChromeClient(@Nullable WebChromeClient webChromeClient) { - this.webChromeClient = webChromeClient; - return this; - } - - /** - * Registers the interface to be used when content can not be handled by the rendering engine, and - * should be downloaded instead. This will replace the current handler. - * - * @param downloadListener an implementation of DownloadListener This value may be null. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setDownloadListener(@Nullable DownloadListener downloadListener) { - this.downloadListener = downloadListener; - return this; - } - - /** - * Sets whether the {@link WebView} should use its built-in zoom mechanisms. The default value is - * {@code true}. - * - * @param flag {@code true} if built in zoom controls are allowed. - * @return This builder. This value cannot be {@code null}. - */ - public WebViewBuilder setZoomControlsEnabled(boolean flag) { - this.enableBuiltInZoomControls = flag; - return this; - } - - /** - * Build the {@link android.webkit.WebView} using the current settings. - * - * @return The {@link android.webkit.WebView} using the current settings. - */ - public WebView build() { - WebView webView = WebViewFactory.create(context, usesHybridComposition, containerView); - - WebSettings webSettings = webView.getSettings(); - webSettings.setDomStorageEnabled(enableDomStorage); - webSettings.setJavaScriptCanOpenWindowsAutomatically(javaScriptCanOpenWindowsAutomatically); - webSettings.setSupportMultipleWindows(supportMultipleWindows); - webSettings.setLoadWithOverviewMode(true); - webSettings.setUseWideViewPort(true); - webSettings.setDisplayZoomControls(false); - webSettings.setBuiltInZoomControls(enableBuiltInZoomControls); - webView.setWebChromeClient(webChromeClient); - webView.setDownloadListener(downloadListener); - return webView; - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java index 4d17eb129db8..d349da47c133 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java @@ -22,7 +22,7 @@ class WebViewClientHostApiImpl implements GeneratedAndroidWebView.WebViewClientHostApi { private final InstanceManager instanceManager; private final WebViewClientCreator webViewClientCreator; - private final WebViewClientFlutterApi webViewClientFlutterApi; + private final WebViewClientFlutterApi flutterApi; @RequiresApi(api = Build.VERSION_CODES.M) static GeneratedAndroidWebView.WebResourceErrorData createWebResourceErrorData( @@ -63,6 +63,200 @@ static GeneratedAndroidWebView.WebResourceRequestData createWebResourceRequestDa return requestData; } + @RequiresApi(Build.VERSION_CODES.N) + static class WebViewClientImpl extends WebViewClient implements Releasable { + private final Long instanceId; + private final InstanceManager instanceManager; + private final Boolean shouldOverrideUrlLoading; + private final WebViewClientFlutterApi flutterApi; + private boolean ignoreCallbacks = false; + + WebViewClientImpl( + Long instanceId, + InstanceManager instanceManager, + Boolean shouldOverrideUrlLoading, + WebViewClientFlutterApi flutterApi) { + this.instanceId = instanceId; + this.instanceManager = instanceManager; + this.shouldOverrideUrlLoading = shouldOverrideUrlLoading; + this.flutterApi = flutterApi; + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + if (!ignoreCallbacks) { + flutterApi.onPageStarted(instanceId, instanceManager.getInstanceId(view), url, reply -> {}); + } + } + + @Override + public void onPageFinished(WebView view, String url) { + if (!ignoreCallbacks) { + flutterApi.onPageFinished( + instanceId, instanceManager.getInstanceId(view), url, reply -> {}); + } + } + + @Override + public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { + if (!ignoreCallbacks) { + flutterApi.onReceivedRequestError( + instanceId, + instanceManager.getInstanceId(view), + createWebResourceRequestData(request), + createWebResourceErrorData(error), + reply -> {}); + } + } + + @Override + public void onReceivedError( + WebView view, int errorCode, String description, String failingUrl) { + if (!ignoreCallbacks) { + flutterApi.onReceivedError( + instanceId, + instanceManager.getInstanceId(view), + (long) errorCode, + description, + failingUrl, + reply -> {}); + } + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + if (!ignoreCallbacks) { + flutterApi.requestLoading( + instanceId, + instanceManager.getInstanceId(view), + createWebResourceRequestData(request), + reply -> {}); + } + return shouldOverrideUrlLoading; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (!ignoreCallbacks) { + flutterApi.urlLoading(instanceId, instanceManager.getInstanceId(view), url, reply -> {}); + } + return shouldOverrideUrlLoading; + } + + @Override + public void onUnhandledKeyEvent(WebView view, KeyEvent event) { + // Deliberately empty. Occasionally the webview will mark events as having failed to be + // handled even though they were handled. We don't want to propagate those as they're not + // truly lost. + } + + public void release() { + ignoreCallbacks = true; + flutterApi.dispose(instanceId, reply -> {}); + } + } + + static class WebViewClientCompatImpl extends WebViewClientCompat implements Releasable { + private final Long instanceId; + private final InstanceManager instanceManager; + private final Boolean shouldOverrideUrlLoading; + private final WebViewClientFlutterApi flutterApi; + private boolean ignoreCallbacks = false; + + WebViewClientCompatImpl( + Long instanceId, + InstanceManager instanceManager, + Boolean shouldOverrideUrlLoading, + WebViewClientFlutterApi flutterApi) { + this.instanceId = instanceId; + this.instanceManager = instanceManager; + this.shouldOverrideUrlLoading = shouldOverrideUrlLoading; + this.flutterApi = flutterApi; + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + if (!ignoreCallbacks) { + flutterApi.onPageStarted(instanceId, instanceManager.getInstanceId(view), url, reply -> {}); + } + } + + @Override + public void onPageFinished(WebView view, String url) { + if (!ignoreCallbacks) { + flutterApi.onPageFinished( + instanceId, instanceManager.getInstanceId(view), url, reply -> {}); + } + } + + // This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is + // enabled. The deprecated method is called when a device doesn't support this. + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @SuppressLint("RequiresFeature") + @Override + public void onReceivedError( + @NonNull WebView view, + @NonNull WebResourceRequest request, + @NonNull WebResourceErrorCompat error) { + if (!ignoreCallbacks) { + flutterApi.onReceivedRequestError( + instanceId, + instanceManager.getInstanceId(view), + createWebResourceRequestData(request), + createWebResourceErrorData(error), + reply -> {}); + } + } + + @Override + public void onReceivedError( + WebView view, int errorCode, String description, String failingUrl) { + if (!ignoreCallbacks) { + flutterApi.onReceivedError( + instanceId, + instanceManager.getInstanceId(view), + (long) errorCode, + description, + failingUrl, + reply -> {}); + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading( + @NonNull WebView view, @NonNull WebResourceRequest request) { + if (!ignoreCallbacks) { + flutterApi.requestLoading( + instanceId, + instanceManager.getInstanceId(view), + createWebResourceRequestData(request), + reply -> {}); + } + return shouldOverrideUrlLoading; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (!ignoreCallbacks) { + flutterApi.urlLoading(instanceId, instanceManager.getInstanceId(view), url, reply -> {}); + } + return shouldOverrideUrlLoading; + } + + @Override + public void onUnhandledKeyEvent(WebView view, KeyEvent event) { + // Deliberately empty. Occasionally the webview will mark events as having failed to be + // handled even though they were handled. We don't want to propagate those as they're not + // truly lost. + } + + public void release() { + ignoreCallbacks = true; + flutterApi.dispose(instanceId, reply -> {}); + } + } + static class WebViewClientCreator { WebViewClient createWebViewClient( Long instanceId, @@ -78,139 +272,11 @@ WebViewClient createWebViewClient( // to bug https://bugs.chromium.org/p/chromium/issues/detail?id=925887. Also, see // https://github.com/flutter/flutter/issues/29446. if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return new WebViewClient() { - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - webViewClientFlutterApi.onPageStarted( - instanceId, instanceManager.getInstanceId(view), url, reply -> {}); - } - - @Override - public void onPageFinished(WebView view, String url) { - webViewClientFlutterApi.onPageFinished( - instanceId, instanceManager.getInstanceId(view), url, reply -> {}); - } - - @Override - public void onReceivedError( - WebView view, WebResourceRequest request, WebResourceError error) { - webViewClientFlutterApi.onReceivedRequestError( - instanceId, - instanceManager.getInstanceId(view), - createWebResourceRequestData(request), - createWebResourceErrorData(error), - reply -> {}); - } - - @SuppressWarnings("deprecation") - @Override - public void onReceivedError( - WebView view, int errorCode, String description, String failingUrl) { - webViewClientFlutterApi.onReceivedError( - instanceId, - instanceManager.getInstanceId(view), - (long) errorCode, - description, - failingUrl, - reply -> {}); - } - - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - webViewClientFlutterApi.requestLoading( - instanceId, - instanceManager.getInstanceId(view), - createWebResourceRequestData(request), - reply -> {}); - return shouldOverrideUrlLoading; - } - - @SuppressWarnings("deprecation") - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - webViewClientFlutterApi.urlLoading( - instanceId, instanceManager.getInstanceId(view), url, reply -> {}); - return shouldOverrideUrlLoading; - } - - @Override - public void onUnhandledKeyEvent(WebView view, KeyEvent event) { - // Deliberately empty. Occasionally the webview will mark events as having failed to be - // handled even though they were handled. We don't want to propagate those as they're not - // truly lost. - } - }; + return new WebViewClientImpl( + instanceId, instanceManager, shouldOverrideUrlLoading, webViewClientFlutterApi); } else { - return new WebViewClientCompat() { - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - webViewClientFlutterApi.onPageStarted( - instanceId, instanceManager.getInstanceId(view), url, reply -> {}); - } - - @Override - public void onPageFinished(WebView view, String url) { - webViewClientFlutterApi.onPageFinished( - instanceId, instanceManager.getInstanceId(view), url, reply -> {}); - } - - // This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is - // enabled. The deprecated method is called when a device doesn't support this. - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - @SuppressLint("RequiresFeature") - @Override - public void onReceivedError( - @NonNull WebView view, - @NonNull WebResourceRequest request, - @NonNull WebResourceErrorCompat error) { - webViewClientFlutterApi.onReceivedRequestError( - instanceId, - instanceManager.getInstanceId(view), - createWebResourceRequestData(request), - createWebResourceErrorData(error), - reply -> {}); - } - - @SuppressWarnings("deprecation") - @Override - public void onReceivedError( - WebView view, int errorCode, String description, String failingUrl) { - webViewClientFlutterApi.onReceivedError( - instanceId, - instanceManager.getInstanceId(view), - (long) errorCode, - description, - failingUrl, - reply -> {}); - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public boolean shouldOverrideUrlLoading( - @NonNull WebView view, @NonNull WebResourceRequest request) { - webViewClientFlutterApi.requestLoading( - instanceId, - instanceManager.getInstanceId(view), - createWebResourceRequestData(request), - reply -> {}); - return shouldOverrideUrlLoading; - } - - @SuppressWarnings("deprecation") - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - webViewClientFlutterApi.urlLoading( - instanceId, instanceManager.getInstanceId(view), url, reply -> {}); - return shouldOverrideUrlLoading; - } - - @Override - public void onUnhandledKeyEvent(WebView view, KeyEvent event) { - // Deliberately empty. Occasionally the webview will mark events as having failed to be - // handled even though they were handled. We don't want to propagate those as they're not - // truly lost. - } - }; + return new WebViewClientCompatImpl( + instanceId, instanceManager, shouldOverrideUrlLoading, webViewClientFlutterApi); } } } @@ -218,22 +284,17 @@ public void onUnhandledKeyEvent(WebView view, KeyEvent event) { WebViewClientHostApiImpl( InstanceManager instanceManager, WebViewClientCreator webViewClientCreator, - WebViewClientFlutterApi webViewClientFlutterApi) { + WebViewClientFlutterApi flutterApi) { this.instanceManager = instanceManager; this.webViewClientCreator = webViewClientCreator; - this.webViewClientFlutterApi = webViewClientFlutterApi; + this.flutterApi = flutterApi; } @Override public void create(Long instanceId, Boolean shouldOverrideUrlLoading) { final WebViewClient webViewClient = webViewClientCreator.createWebViewClient( - instanceId, instanceManager, shouldOverrideUrlLoading, webViewClientFlutterApi); + instanceId, instanceManager, shouldOverrideUrlLoading, flutterApi); instanceManager.addInstance(webViewClient, instanceId); } - - @Override - public void dispose(Long instanceId) { - instanceManager.removeInstance(instanceId); - } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java index 268d35a1e04c..6acee703e487 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java @@ -4,8 +4,20 @@ package io.flutter.plugins.webviewflutter; +import android.app.Activity; +import android.os.Handler; +import androidx.annotation.NonNull; import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.platform.PlatformViewRegistry; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerHostApi; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelHostApi; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientHostApi; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebSettingsHostApi; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewClientHostApi; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewHostApi; /** * Java platform implementation of the webview_flutter plugin. @@ -15,8 +27,8 @@ *

Call {@link #registerWith(Registrar)} to use the stable {@code io.flutter.plugin.common} * package instead. */ -public class WebViewFlutterPlugin implements FlutterPlugin { - +public class WebViewFlutterPlugin implements FlutterPlugin, ActivityAware { + private FlutterPluginBinding pluginBinding; private FlutterCookieManager flutterCookieManager; /** @@ -42,23 +54,54 @@ public WebViewFlutterPlugin() {} */ @SuppressWarnings("deprecation") public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - registrar - .platformViewRegistry() - .registerViewFactory( - "plugins.flutter.io/webview", - new FlutterWebViewFactory(registrar.messenger(), registrar.view())); + setUp(registrar.messenger(), registrar.platformViewRegistry(), registrar.activity()); new FlutterCookieManager(registrar.messenger()); } + private static void setUp( + BinaryMessenger binaryMessenger, PlatformViewRegistry viewRegistry, Activity activity) { + InstanceManager instanceManager = new InstanceManager(); + viewRegistry.registerViewFactory( + "plugins.flutter.io/webview", new FlutterWebViewFactory(instanceManager)); + WebViewHostApi.setup( + binaryMessenger, + new WebViewHostApiImpl(instanceManager, new WebViewHostApiImpl.WebViewProxy(), activity)); + WebViewClientHostApi.setup( + binaryMessenger, + new WebViewClientHostApiImpl( + instanceManager, + new WebViewClientHostApiImpl.WebViewClientCreator(), + new GeneratedAndroidWebView.WebViewClientFlutterApi(binaryMessenger))); + WebChromeClientHostApi.setup( + binaryMessenger, + new WebChromeClientHostApiImpl( + instanceManager, + new WebChromeClientHostApiImpl.WebChromeClientCreator(), + new GeneratedAndroidWebView.WebChromeClientFlutterApi(binaryMessenger))); + DownloadListenerHostApi.setup( + binaryMessenger, + new DownloadListenerHostApiImpl( + instanceManager, + new DownloadListenerHostApiImpl.DownloadListenerCreator(), + new GeneratedAndroidWebView.DownloadListenerFlutterApi(binaryMessenger))); + JavaScriptChannelHostApi.setup( + binaryMessenger, + new JavaScriptChannelHostApiImpl( + instanceManager, + new JavaScriptChannelHostApiImpl.JavaScriptChannelCreator(), + new GeneratedAndroidWebView.JavaScriptChannelFlutterApi(binaryMessenger), + new Handler(activity.getMainLooper()))); + WebSettingsHostApi.setup( + binaryMessenger, + new WebSettingsHostApiImpl( + instanceManager, new WebSettingsHostApiImpl.WebSettingsCreator())); + new FlutterCookieManager(binaryMessenger); + } + @Override public void onAttachedToEngine(FlutterPluginBinding binding) { - BinaryMessenger messenger = binding.getBinaryMessenger(); - binding - .getPlatformViewRegistry() - .registerViewFactory( - "plugins.flutter.io/webview", - new FlutterWebViewFactory(messenger, /*containerView=*/ null)); - flutterCookieManager = new FlutterCookieManager(messenger); + this.pluginBinding = binding; + flutterCookieManager = new FlutterCookieManager(binding.getBinaryMessenger()); } @Override @@ -70,4 +113,27 @@ public void onDetachedFromEngine(FlutterPluginBinding binding) { flutterCookieManager.dispose(); flutterCookieManager = null; } + + @Override + public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBinding) { + setUp( + pluginBinding.getBinaryMessenger(), + pluginBinding.getPlatformViewRegistry(), + activityPluginBinding.getActivity()); + } + + @Override + public void onDetachedFromActivityForConfigChanges() {} + + @Override + public void onReattachedToActivityForConfigChanges( + @NonNull ActivityPluginBinding activityPluginBinding) { + setUp( + pluginBinding.getBinaryMessenger(), + pluginBinding.getPlatformViewRegistry(), + activityPluginBinding.getActivity()); + } + + @Override + public void onDetachedFromActivity() {} } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java index 35bdc608d6ff..f1aef3e35b21 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java @@ -4,7 +4,9 @@ package io.flutter.plugins.webviewflutter; +import android.annotation.SuppressLint; import android.content.Context; +import android.hardware.display.DisplayManager; import android.view.View; import android.webkit.DownloadListener; import android.webkit.WebChromeClient; @@ -12,9 +14,16 @@ import android.webkit.WebViewClient; import androidx.annotation.NonNull; import io.flutter.plugin.platform.PlatformView; +import io.flutter.plugins.webviewflutter.WebChromeClientHostApiImpl.WebChromeClientImpl; +import java.util.HashMap; import java.util.Map; class WebViewHostApiImpl implements GeneratedAndroidWebView.WebViewHostApi { + // TODO(bparrishMines): This can be removed once pigeon supports null values: https://github.com/flutter/flutter/issues/59118 + // Workaround to represent null Strings since pigeon doesn't support null + // values. + static final String nullStringIdentifier = ""; + private final InstanceManager instanceManager; private final WebViewProxy webViewProxy; private final Context context; @@ -33,7 +42,12 @@ void setWebContentsDebuggingEnabled(boolean enabled) { } } - private static class WebViewPlatformView extends WebView implements PlatformView { + static class WebViewPlatformView extends WebView implements PlatformView, Releasable { + private WebViewClient currentWebViewClient; + private DownloadListener currentDownloadListener; + private WebChromeClient currentWebChromeClient; + private final Map javaScriptInterfaces = new HashMap<>(); + public WebViewPlatformView(Context context) { super(context); } @@ -47,10 +61,82 @@ public View getView() { public void dispose() { destroy(); } + + @Override + public void setWebViewClient(WebViewClient webViewClient) { + super.setWebViewClient(webViewClient); + if (currentWebViewClient instanceof Releasable) { + ((Releasable) currentWebViewClient).release(); + } + currentWebViewClient = (WebViewClient) webViewClient; + } + + @Override + public void setDownloadListener(DownloadListener listener) { + super.setDownloadListener(listener); + if (currentDownloadListener instanceof Releasable) { + ((Releasable) currentDownloadListener).release(); + } + currentDownloadListener = listener; + } + + @Override + public void setWebChromeClient(WebChromeClient client) { + super.setWebChromeClient(client); + if (currentWebChromeClient instanceof Releasable) { + ((Releasable) currentWebChromeClient).release(); + } + currentWebChromeClient = client; + } + + @SuppressLint("JavascriptInterface") + @Override + public void addJavascriptInterface(Object object, String name) { + super.addJavascriptInterface(object, name); + if (object instanceof JavaScriptChannel) { + javaScriptInterfaces.put(name, (JavaScriptChannel) object); + } + } + + @Override + public void removeJavascriptInterface(@NonNull String name) { + super.removeJavascriptInterface(name); + final JavaScriptChannel javaScriptChannel = javaScriptInterfaces.get(name); + if (javaScriptChannel != null) { + javaScriptChannel.release(); + } + javaScriptInterfaces.remove(name); + } + + @Override + public void release() { + if (currentWebViewClient instanceof Releasable) { + ((Releasable) currentWebViewClient).release(); + currentWebViewClient = null; + } + if (currentDownloadListener instanceof Releasable) { + ((Releasable) currentDownloadListener).release(); + currentDownloadListener = null; + } + if (currentWebChromeClient instanceof Releasable) { + ((Releasable) currentWebChromeClient).release(); + currentWebChromeClient = null; + } + for (JavaScriptChannel channel : javaScriptInterfaces.values()) { + channel.release(); + } + javaScriptInterfaces.clear(); + } } - private static class InputAwareWebViewPlatformView extends InputAwareWebView - implements PlatformView { + @SuppressLint("ViewConstructor") + static class InputAwareWebViewPlatformView extends InputAwareWebView + implements PlatformView, Releasable { + private WebViewClient currentWebViewClient; + private DownloadListener currentDownloadListener; + private WebChromeClient currentWebChromeClient; + private final Map javaScriptInterfaces = new HashMap<>(); + InputAwareWebViewPlatformView(Context context, View containerView) { super(context, containerView); } @@ -72,7 +158,7 @@ public void onFlutterViewDetached() { @Override public void dispose() { - dispose(); + super.dispose(); destroy(); } @@ -85,6 +171,76 @@ public void onInputConnectionLocked() { public void onInputConnectionUnlocked() { unlockInputConnection(); } + + @Override + public void setWebViewClient(WebViewClient webViewClient) { + super.setWebViewClient(webViewClient); + if (currentWebViewClient instanceof Releasable) { + ((Releasable) currentWebViewClient).release(); + } + currentWebViewClient = (WebViewClient) webViewClient; + } + + @Override + public void setDownloadListener(DownloadListener listener) { + super.setDownloadListener(listener); + if (currentDownloadListener instanceof Releasable) { + ((Releasable) currentDownloadListener).release(); + } + currentDownloadListener = listener; + } + + @Override + public void setWebChromeClient(WebChromeClient client) { + super.setWebChromeClient(client); + if (currentWebChromeClient instanceof Releasable) { + ((Releasable) currentWebChromeClient).release(); + } + + if (client instanceof WebChromeClientImpl) { + ((WebChromeClientImpl) client).setWebViewClient(currentWebViewClient); + } + currentWebChromeClient = client; + } + + @SuppressLint("JavascriptInterface") + @Override + public void addJavascriptInterface(Object object, String name) { + super.addJavascriptInterface(object, name); + if (object instanceof JavaScriptChannel) { + javaScriptInterfaces.put(name, (JavaScriptChannel) object); + } + } + + @Override + public void removeJavascriptInterface(@NonNull String name) { + super.removeJavascriptInterface(name); + final JavaScriptChannel javaScriptChannel = javaScriptInterfaces.get(name); + if (javaScriptChannel != null) { + javaScriptChannel.release(); + } + javaScriptInterfaces.remove(name); + } + + @Override + public void release() { + if (currentWebViewClient instanceof Releasable) { + ((Releasable) currentWebViewClient).release(); + currentWebViewClient = null; + } + if (currentDownloadListener instanceof Releasable) { + ((Releasable) currentDownloadListener).release(); + currentDownloadListener = null; + } + if (currentWebChromeClient instanceof Releasable) { + ((Releasable) currentWebChromeClient).release(); + currentWebChromeClient = null; + } + for (JavaScriptChannel channel : javaScriptInterfaces.values()) { + channel.release(); + } + javaScriptInterfaces.clear(); + } } WebViewHostApiImpl(InstanceManager instanceManager, WebViewProxy webViewProxy, Context context) { @@ -95,16 +251,24 @@ public void onInputConnectionUnlocked() { @Override public void create(Long instanceId, Boolean useHybridComposition) { + DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy(); + DisplayManager displayManager = + (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); + displayListenerProxy.onPreWebViewInitialization(displayManager); + final WebView webView = useHybridComposition ? webViewProxy.createWebView(context) : webViewProxy.createInputAwareWebView(context); + + displayListenerProxy.onPostWebViewInitialization(displayManager); instanceManager.addInstance(webView, instanceId); } @Override public void dispose(Long instanceId) { - instanceManager.removeInstance(instanceId); + final WebView instance = (WebView) instanceManager.removeInstance(instanceId); + ((Releasable) instance).release(); } @Override @@ -116,7 +280,8 @@ public void loadUrl(Long instanceId, String url, Map headers) { @Override public String getUrl(Long instanceId) { final WebView webView = (WebView) instanceManager.getInstance(instanceId); - return webView.getUrl(); + final String result = webView.getUrl(); + return result != null ? result : nullStringIdentifier; } @Override @@ -165,7 +330,8 @@ public void evaluateJavascript( @Override public String getTitle(Long instanceId) { final WebView webView = (WebView) instanceManager.getInstance(instanceId); - return webView.getTitle(); + final String result = webView.getTitle(); + return result != null ? result : nullStringIdentifier; } @Override @@ -228,6 +394,6 @@ public void setDownloadListener(Long instanceId, Long listenerInstanceId) { @Override public void setWebChromeClient(Long instanceId, Long clientInstanceId) { final WebView webView = (WebView) instanceManager.getInstance(instanceId); - webView.setWebChromeClient((WebChromeClient) instanceManager.getInstance(instanceId)); + webView.setWebChromeClient((WebChromeClient) instanceManager.getInstance(clientInstanceId)); } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java deleted file mode 100644 index 2c918584ba83..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import android.webkit.WebView; -import org.junit.Before; -import org.junit.Test; - -public class FlutterDownloadListenerTest { - private FlutterWebViewClient webViewClient; - private WebView webView; - - @Before - public void before() { - webViewClient = mock(FlutterWebViewClient.class); - webView = mock(WebView.class); - } - - @Test - public void onDownloadStart_should_notify_webViewClient() { - String url = "testurl.com"; - FlutterDownloadListener downloadListener = new FlutterDownloadListener(webViewClient); - downloadListener.onDownloadStart(url, "test", "inline", "data/text", 0); - verify(webViewClient).notifyDownload(nullable(WebView.class), eq(url)); - } - - @Test - public void onDownloadStart_should_pass_webView() { - FlutterDownloadListener downloadListener = new FlutterDownloadListener(webViewClient); - downloadListener.setWebView(webView); - downloadListener.onDownloadStart("testurl.com", "test", "inline", "data/text", 0); - verify(webViewClient).notifyDownload(eq(webView), anyString()); - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java deleted file mode 100644 index 86346ac08f16..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; - -import android.webkit.WebView; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; - -public class FlutterWebViewClientTest { - - MethodChannel mockMethodChannel; - WebView mockWebView; - - @Before - public void before() { - mockMethodChannel = mock(MethodChannel.class); - mockWebView = mock(WebView.class); - } - - @Test - public void notify_download_should_notifyOnNavigationRequest_when_navigationDelegate_is_set() { - final String url = "testurl.com"; - - FlutterWebViewClient client = new FlutterWebViewClient(mockMethodChannel); - client.createWebViewClient(true); - - client.notifyDownload(mockWebView, url); - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Object.class); - verify(mockMethodChannel) - .invokeMethod( - eq("navigationRequest"), argumentCaptor.capture(), any(MethodChannel.Result.class)); - HashMap map = (HashMap) argumentCaptor.getValue(); - assertEquals(map.get("url"), url); - assertEquals(map.get("isForMainFrame"), true); - } - - @Test - public void - notify_download_should_not_notifyOnNavigationRequest_when_navigationDelegate_is_not_set() { - final String url = "testurl.com"; - - FlutterWebViewClient client = new FlutterWebViewClient(mockMethodChannel); - client.createWebViewClient(false); - - client.notifyDownload(mockWebView, url); - verifyNoInteractions(mockMethodChannel); - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java deleted file mode 100644 index fd79bccabbce..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.webkit.DownloadListener; -import android.webkit.WebChromeClient; -import android.webkit.WebView; -import java.util.HashMap; -import java.util.Map; -import org.junit.Before; -import org.junit.Test; - -public class FlutterWebViewTest { - private WebChromeClient mockWebChromeClient; - private DownloadListener mockDownloadListener; - private WebViewBuilder mockWebViewBuilder; - private WebView mockWebView; - - @Before - public void before() { - mockWebChromeClient = mock(WebChromeClient.class); - mockWebViewBuilder = mock(WebViewBuilder.class); - mockWebView = mock(WebView.class); - mockDownloadListener = mock(DownloadListener.class); - - when(mockWebViewBuilder.setDomStorageEnabled(anyBoolean())).thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setJavaScriptCanOpenWindowsAutomatically(anyBoolean())) - .thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setSupportMultipleWindows(anyBoolean())).thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setUsesHybridComposition(anyBoolean())).thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setZoomControlsEnabled(anyBoolean())).thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setWebChromeClient(any(WebChromeClient.class))) - .thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.setDownloadListener(any(DownloadListener.class))) - .thenReturn(mockWebViewBuilder); - - when(mockWebViewBuilder.build()).thenReturn(mockWebView); - } - - @Test - public void createWebView_should_create_webview_with_default_configuration() { - FlutterWebView.createWebView( - mockWebViewBuilder, createParameterMap(false), mockWebChromeClient, mockDownloadListener); - - verify(mockWebViewBuilder, times(1)).setDomStorageEnabled(true); - verify(mockWebViewBuilder, times(1)).setJavaScriptCanOpenWindowsAutomatically(true); - verify(mockWebViewBuilder, times(1)).setSupportMultipleWindows(true); - verify(mockWebViewBuilder, times(1)).setUsesHybridComposition(false); - verify(mockWebViewBuilder, times(1)).setWebChromeClient(mockWebChromeClient); - verify(mockWebViewBuilder, times(1)).setZoomControlsEnabled(true); - } - - private Map createParameterMap(boolean usesHybridComposition) { - Map params = new HashMap<>(); - params.put("usesHybridComposition", usesHybridComposition); - - return params; - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java index 5ab3ab10fe03..ee66c2021108 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java @@ -45,11 +45,11 @@ public void setUp() { WebChromeClient createWebChromeClient( Long instanceId, InstanceManager instanceManager, - WebViewClient webViewClient, - WebChromeClientFlutterApi webChromeClientFlutterApi) { + WebChromeClientFlutterApi webChromeClientFlutterApi, + WebViewClient webViewClient) { testWebChromeClient = super.createWebChromeClient( - instanceId, instanceManager, webViewClient, webChromeClientFlutterApi); + instanceId, instanceManager, webChromeClientFlutterApi, webViewClient); return testWebChromeClient; } }; diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java deleted file mode 100644 index 423cb210c392..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.webviewflutter; - -import static org.junit.Assert.assertNotNull; -import static org.mockito.Mockito.*; - -import android.content.Context; -import android.view.View; -import android.webkit.DownloadListener; -import android.webkit.WebChromeClient; -import android.webkit.WebSettings; -import android.webkit.WebView; -import io.flutter.plugins.webviewflutter.WebViewBuilder.WebViewFactory; -import java.io.IOException; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.mockito.MockedStatic; -import org.mockito.MockedStatic.Verification; - -public class WebViewBuilderTest { - private Context mockContext; - private View mockContainerView; - private WebView mockWebView; - private MockedStatic mockedStaticWebViewFactory; - - @Before - public void before() { - mockContext = mock(Context.class); - mockContainerView = mock(View.class); - mockWebView = mock(WebView.class); - mockedStaticWebViewFactory = mockStatic(WebViewFactory.class); - - mockedStaticWebViewFactory - .when( - new Verification() { - @Override - public void apply() { - WebViewFactory.create(mockContext, false, mockContainerView); - } - }) - .thenReturn(mockWebView); - } - - @After - public void after() { - mockedStaticWebViewFactory.close(); - } - - @Test - public void ctor_test() { - WebViewBuilder builder = new WebViewBuilder(mockContext, mockContainerView); - - assertNotNull(builder); - } - - @Test - public void build_should_set_values() throws IOException { - WebSettings mockWebSettings = mock(WebSettings.class); - WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); - DownloadListener mockDownloadListener = mock(DownloadListener.class); - - when(mockWebView.getSettings()).thenReturn(mockWebSettings); - - WebViewBuilder builder = - new WebViewBuilder(mockContext, mockContainerView) - .setDomStorageEnabled(true) - .setJavaScriptCanOpenWindowsAutomatically(true) - .setSupportMultipleWindows(true) - .setWebChromeClient(mockWebChromeClient) - .setDownloadListener(mockDownloadListener); - - WebView webView = builder.build(); - - assertNotNull(webView); - verify(mockWebSettings).setDomStorageEnabled(true); - verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(true); - verify(mockWebSettings).setSupportMultipleWindows(true); - verify(mockWebView).setWebChromeClient(mockWebChromeClient); - verify(mockWebView).setDownloadListener(mockDownloadListener); - } - - @Test - public void build_should_use_default_values() throws IOException { - WebSettings mockWebSettings = mock(WebSettings.class); - WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); - - when(mockWebView.getSettings()).thenReturn(mockWebSettings); - - WebViewBuilder builder = new WebViewBuilder(mockContext, mockContainerView); - - WebView webView = builder.build(); - - assertNotNull(webView); - verify(mockWebSettings).setDomStorageEnabled(false); - verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(false); - verify(mockWebSettings).setSupportMultipleWindows(false); - verify(mockWebView).setWebChromeClient(null); - verify(mockWebView).setDownloadListener(null); - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java index b914ce913e76..3ba4040e74ef 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java @@ -13,8 +13,15 @@ import android.content.Context; import android.webkit.DownloadListener; import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; import android.webkit.WebView; import android.webkit.WebViewClient; +import io.flutter.plugins.webviewflutter.DownloadListenerHostApiImpl.DownloadListenerImpl; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelFlutterApi; +import io.flutter.plugins.webviewflutter.WebChromeClientHostApiImpl.WebChromeClientImpl; +import io.flutter.plugins.webviewflutter.WebViewClientHostApiImpl.WebViewClientImpl; +import io.flutter.plugins.webviewflutter.WebViewHostApiImpl.InputAwareWebViewPlatformView; +import io.flutter.plugins.webviewflutter.WebViewHostApiImpl.WebViewPlatformView; import java.util.HashMap; import org.junit.Before; import org.junit.Rule; @@ -45,40 +52,99 @@ public void setUp() { } @Test - public void errorCodes() { - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_AUTHENTICATION), - "authentication"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_BAD_URL), "badUrl"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_CONNECT), "connect"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FAILED_SSL_HANDSHAKE), - "failedSslHandshake"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FILE), "file"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FILE_NOT_FOUND), "fileNotFound"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_HOST_LOOKUP), "hostLookup"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_IO), "io"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_PROXY_AUTHENTICATION), - "proxyAuthentication"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_REDIRECT_LOOP), "redirectLoop"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_TIMEOUT), "timeout"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_TOO_MANY_REQUESTS), - "tooManyRequests"); - assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNKNOWN), "unknown"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSAFE_RESOURCE), - "unsafeResource"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME), - "unsupportedAuthScheme"); - assertEquals( - FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSUPPORTED_SCHEME), - "unsupportedScheme"); + public void releaseWebView() { + final WebViewPlatformView webView = new WebViewPlatformView(mockContext); + + final WebViewClientImpl mockWebViewClient = mock(WebViewClientImpl.class); + final WebChromeClientImpl mockWebChromeClient = mock(WebChromeClientImpl.class); + final DownloadListenerImpl mockDownloadListener = mock(DownloadListenerImpl.class); + final JavaScriptChannel mockJavaScriptChannel = mock(JavaScriptChannel.class); + + webView.setWebViewClient(mockWebViewClient); + webView.setWebChromeClient(mockWebChromeClient); + webView.setDownloadListener(mockDownloadListener); + webView.addJavascriptInterface(mockJavaScriptChannel, "jchannel"); + + webView.release(); + + verify(mockWebViewClient).release(); + verify(mockWebChromeClient).release(); + verify(mockDownloadListener).release(); + verify(mockJavaScriptChannel).release(); + } + + @Test + public void releaseWebViewDependents() { + final WebViewPlatformView webView = new WebViewPlatformView(mockContext); + + final WebViewClientImpl mockWebViewClient = mock(WebViewClientImpl.class); + final WebChromeClientImpl mockWebChromeClient = mock(WebChromeClientImpl.class); + final DownloadListenerImpl mockDownloadListener = mock(DownloadListenerImpl.class); + final JavaScriptChannel mockJavaScriptChannel = mock(JavaScriptChannel.class); + + webView.setWebViewClient(mockWebViewClient); + webView.setWebChromeClient(mockWebChromeClient); + webView.setDownloadListener(mockDownloadListener); + webView.addJavascriptInterface(mockJavaScriptChannel, "jchannel"); + + webView.setWebViewClient(null); + webView.setWebChromeClient(null); + webView.setDownloadListener(null); + webView.removeJavascriptInterface("jchannel"); + + verify(mockWebViewClient).release(); + verify(mockWebChromeClient).release(); + verify(mockDownloadListener).release(); + verify(mockJavaScriptChannel).release(); + } + + @Test + public void releaseInputAwareWebView() { + final InputAwareWebViewPlatformView webView = + new InputAwareWebViewPlatformView(mockContext, null); + + final WebViewClientImpl mockWebViewClient = mock(WebViewClientImpl.class); + final WebChromeClientImpl mockWebChromeClient = mock(WebChromeClientImpl.class); + final DownloadListenerImpl mockDownloadListener = mock(DownloadListenerImpl.class); + final JavaScriptChannel mockJavaScriptChannel = mock(JavaScriptChannel.class); + + webView.setWebViewClient(mockWebViewClient); + webView.setWebChromeClient(mockWebChromeClient); + webView.setDownloadListener(mockDownloadListener); + webView.addJavascriptInterface(mockJavaScriptChannel, "jchannel"); + + webView.release(); + + verify(mockWebViewClient).release(); + verify(mockWebChromeClient).release(); + verify(mockDownloadListener).release(); + verify(mockJavaScriptChannel).release(); + } + + @Test + public void releaseInputAwareWebViewDependents() { + final InputAwareWebViewPlatformView webView = + new InputAwareWebViewPlatformView(mockContext, null); + + final WebViewClientImpl mockWebViewClient = mock(WebViewClientImpl.class); + final WebChromeClientImpl mockWebChromeClient = mock(WebChromeClientImpl.class); + final DownloadListenerImpl mockDownloadListener = mock(DownloadListenerImpl.class); + final JavaScriptChannel mockJavaScriptChannel = mock(JavaScriptChannel.class); + + webView.setWebViewClient(mockWebViewClient); + webView.setWebChromeClient(mockWebChromeClient); + webView.setDownloadListener(mockDownloadListener); + webView.addJavascriptInterface(mockJavaScriptChannel, "jchannel"); + + webView.setWebViewClient(null); + webView.setWebChromeClient(null); + webView.setDownloadListener(null); + webView.removeJavascriptInterface("jchannel"); + + verify(mockWebViewClient).release(); + verify(mockWebChromeClient).release(); + verify(mockDownloadListener).release(); + verify(mockJavaScriptChannel).release(); } @Test @@ -195,7 +261,8 @@ public void setWebViewClient() { @Test public void addJavaScriptChannel() { - final JavaScriptChannel javaScriptChannel = new JavaScriptChannel(null, "aName", null); + final JavaScriptChannel javaScriptChannel = + new JavaScriptChannel(0L, mock(JavaScriptChannelFlutterApi.class), "aName", null); testInstanceManager.addInstance(javaScriptChannel, 1L); testHostApiImpl.addJavaScriptChannel(0L, 1L); @@ -204,7 +271,8 @@ public void addJavaScriptChannel() { @Test public void removeJavaScriptChannel() { - final JavaScriptChannel javaScriptChannel = new JavaScriptChannel(null, "aName", null); + final JavaScriptChannel javaScriptChannel = + new JavaScriptChannel(0L, mock(JavaScriptChannelFlutterApi.class), "aName", null); testInstanceManager.addInstance(javaScriptChannel, 1L); testHostApiImpl.removeJavaScriptChannel(0L, 1L); @@ -219,4 +287,13 @@ public void setDownloadListener() { testHostApiImpl.setDownloadListener(0L, 1L); verify(mockWebView).setDownloadListener(mockDownloadListener); } + + @Test + public void setWebChromeClient() { + final WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); + testInstanceManager.addInstance(mockWebChromeClient, 1L); + + testHostApiImpl.setWebChromeClient(0L, 1L); + verify(mockWebView).setWebChromeClient(mockWebChromeClient); + } } diff --git a/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart index c57d2bd55580..5876fa5d2d5c 100644 --- a/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart @@ -168,91 +168,24 @@ void main() { }, skip: _skipDueToIssue86757); testWidgets('resize webview', (WidgetTester tester) async { - final String resizeTest = ''' - - Resize test - - - - - - '''; - final String resizeTestBase64 = - base64Encode(const Utf8Encoder().convert(resizeTest)); - final Completer resizeCompleter = Completer(); - final Completer pageStarted = Completer(); - final Completer pageLoaded = Completer(); - final Completer controllerCompleter = - Completer(); - final GlobalKey key = GlobalKey(); + final Completer pageFinishedCompleter = Completer(); - final WebView webView = WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$resizeTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); + int resizeCallbackCount = 0; + await tester.pumpWidget(ResizableWebView( + onResize: (_) => resizeCallbackCount++, + onPageFinished: () { + pageFinishedCompleter.complete(); }, - javascriptChannels: { - JavascriptChannel( - name: 'Resize', - onMessageReceived: (JavascriptMessage message) { - resizeCompleter.complete(true); - }, - ), - }, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - javascriptMode: JavascriptMode.unrestricted, - ); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Column( - children: [ - SizedBox( - width: 200, - height: 200, - child: webView, - ), - ], - ), - ), - ); + )); + await tester.pump(Duration(seconds: 3)); + await pageFinishedCompleter.future; - await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; + final int oldCount = resizeCallbackCount; - expect(resizeCompleter.isCompleted, false); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Column( - children: [ - SizedBox( - width: 400, - height: 400, - child: webView, - ), - ], - ), - ), - ); + await tester.tap(find.byKey(const ValueKey('resizeButton'))); + await tester.pump(Duration(seconds: 3)); - await resizeCompleter.future; + expect(resizeCallbackCount, greaterThan(oldCount)); }); testWidgets('set custom userAgent', (WidgetTester tester) async { @@ -1416,3 +1349,74 @@ Future _evaluateJavascript( WebViewController controller, String js) async { return jsonDecode(await controller.evaluateJavascript(js)); } + +class ResizableWebView extends StatefulWidget { + ResizableWebView({required this.onResize, required this.onPageFinished}); + + final JavascriptMessageHandler onResize; + final VoidCallback onPageFinished; + + @override + State createState() => ResizableWebViewState(); +} + +class ResizableWebViewState extends State { + double webViewWidth = 200; + double webViewHeight = 200; + + static const String resizePage = ''' + + Resize test + + + + + + '''; + + @override + Widget build(BuildContext context) { + final String resizeTestBase64 = + base64Encode(const Utf8Encoder().convert(resizePage)); + return Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: webViewWidth, + height: webViewHeight, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$resizeTestBase64', + javascriptChannels: { + JavascriptChannel( + name: 'Resize', + onMessageReceived: widget.onResize, + ), + }, + onPageFinished: (_) => widget.onPageFinished(), + javascriptMode: JavascriptMode.unrestricted, + ), + ), + TextButton( + key: Key('resizeButton'), + onPressed: () { + setState(() { + webViewWidth += 100.0; + webViewHeight += 100.0; + }); + }, + child: Text('ResizeButton'), + ), + ], + ), + ); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart index 1ddd6f3d9f0f..3de5a5a3d106 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart' show AndroidViewSurface; +import 'android_webview.pigeon.dart'; import 'android_webview_api_impls.dart'; // TODO(bparrishMines): This can be removed once pigeon supports null values: https://github.com/flutter/flutter/issues/59118 @@ -29,6 +30,8 @@ const String _nullStringIdentifier = ''; /// To learn more about WebView and alternatives for serving web content, read /// the documentation on /// [Web-based content](https://developer.android.com/guide/webapps). +/// +/// When a [WebView] is no longer needed [release] must be called. class WebView { /// Constructs a new WebView. WebView({this.useHybridComposition = false}) { @@ -40,9 +43,6 @@ class WebView { static WebViewHostApiImpl api = WebViewHostApiImpl(); WebViewClient? _currentWebViewClient; - DownloadListener? _currentDownloadListener; - WebChromeClient? _currentWebChromeClient; - Set _javaScriptChannels = {}; /// Whether the [WebView] will be rendered with an [AndroidViewSurface]. /// @@ -187,18 +187,8 @@ class WebView { /// /// This will replace the current handler. Future setWebViewClient(WebViewClient webViewClient) { - final WebViewClient? currentWebViewClient = _currentWebViewClient; - - if (webViewClient == currentWebViewClient) { - return Future.value(); - } - - if (currentWebViewClient != null) { - WebViewClient.api.disposeFromInstance(currentWebViewClient); - } - - WebViewClient.api.createFromInstance(webViewClient); _currentWebViewClient = webViewClient; + WebViewClient.api.createFromInstance(webViewClient); return api.setWebViewClientFromInstance(this, webViewClient); } @@ -227,7 +217,6 @@ class WebView { /// content is ever loaded into the WebView even inside an iframe. Future addJavaScriptChannel(JavaScriptChannel javaScriptChannel) { JavaScriptChannel.api.createFromInstance(javaScriptChannel); - _javaScriptChannels.add(javaScriptChannel); return api.addJavaScriptChannelFromInstance(this, javaScriptChannel); } @@ -236,27 +225,15 @@ class WebView { /// Note that the removal will not be reflected in JavaScript until the page /// is next (re)loaded. See [addJavaScriptChannel]. Future removeJavaScriptChannel(JavaScriptChannel javaScriptChannel) { - _javaScriptChannels.remove(javaScriptChannel); - api.removeJavaScriptChannelFromInstance(this, javaScriptChannel); - return JavaScriptChannel.api.disposeFromInstance(javaScriptChannel); + JavaScriptChannel.api.createFromInstance(javaScriptChannel); + return api.removeJavaScriptChannelFromInstance(this, javaScriptChannel); } /// Registers the interface to be used when content can not be handled by the rendering engine, and should be downloaded instead. /// /// This will replace the current handler. Future setDownloadListener(DownloadListener listener) { - final DownloadListener? currentDownloadListener = _currentDownloadListener; - - if (listener == currentDownloadListener) { - return Future.value(); - } - - if (currentDownloadListener != null) { - DownloadListener.api.disposeFromInstance(currentDownloadListener); - } - DownloadListener.api.createFromInstance(listener); - _currentDownloadListener = listener; return api.setDownloadListenerFromInstance(this, listener); } @@ -266,26 +243,26 @@ class WebView { /// JavaScript dialogs, favicons, titles, and the progress. This will replace /// the current handler. Future setWebChromeClient(WebChromeClient client) { - final WebChromeClient? currentWebChromeClient = _currentWebChromeClient; - - if (client == currentWebChromeClient) { - return Future.value(); - } - - if (currentWebChromeClient != null) { - WebChromeClient.api.disposeFromInstance(currentWebChromeClient); - } - - final WebViewClient? currentWebViewClient = _currentWebViewClient; + // WebView requires a WebViewClient because of a bug fix that makes + // calls to WebViewClient.requestLoading/WebViewClient.urlLoading when a new + // window is opened. This is to make sure a url opened by `Window.open` has + // a secure url. assert( - currentWebViewClient != null, + _currentWebViewClient != null, "Can't set a WebChromeClient without setting a WebViewClient first.", ); - - WebChromeClient.api.createFromInstance(client, currentWebViewClient!); - _currentWebChromeClient = client; + WebChromeClient.api.createFromInstance(client, _currentWebViewClient!); return api.setWebChromeClientFromInstance(this, client); } + + /// Releases all resources used by the [WebView]. + /// + /// Any methods called on the [WebView] instance after [release] will throw + /// an exception. + Future release() { + _currentWebViewClient = null; + return api.disposeFromInstance(this); + } } /// Manages settings state for a [WebView]. @@ -332,7 +309,7 @@ class WebSettings { /// /// The default is false. Future setSupportMultipleWindows(bool support) { - return api.setSupportZoomFromInstance(this, support); + return api.setSupportMultipleWindowsFromInstance(this, support); } /// Tells the WebView to enable JavaScript execution. @@ -423,12 +400,19 @@ class WebSettings { /// See [WebView.addJavaScriptChannel]. abstract class JavaScriptChannel { /// Constructs a [JavaScriptChannel]. - JavaScriptChannel(this.channelName); + JavaScriptChannel(this.channelName) { + if (!_flutterApisHaveBeenSetup) { + JavaScriptChannelFlutterApi.setup(JavaScriptChannelFlutterApiImpl()); + _flutterApisHaveBeenSetup = true; + } + } /// Pigeon Host Api implementation for [JavaScriptChannel]. @visibleForTesting static JavaScriptChannelHostApiImpl api = JavaScriptChannelHostApiImpl(); + static bool _flutterApisHaveBeenSetup = false; + /// Used to identify this object to receive messages from javaScript. final String channelName; @@ -439,55 +423,62 @@ abstract class JavaScriptChannel { /// Receive various notifications and requests for [WebView]. abstract class WebViewClient { /// Constructs a [WebViewClient]. - WebViewClient({this.shouldOverrideUrlLoading = true}); + WebViewClient({this.shouldOverrideUrlLoading = true}) { + if (!_flutterApisHaveBeenSetup) { + WebViewClientFlutterApi.setup(WebViewClientFlutterApiImpl()); + _flutterApisHaveBeenSetup = true; + } + } + + static bool _flutterApisHaveBeenSetup = false; /// User authentication failed on server. - static const int errorAuthentication = 0xfffffffc; + static const int errorAuthentication = -4; /// Malformed URL. - static const int errorBadUrl = 0xfffffff4; + static const int errorBadUrl = -12; /// Failed to connect to the server. - static const int errorConnect = 0xfffffffa; + static const int errorConnect = -6; /// Failed to perform SSL handshake. - static const int errorFailedSslHandshake = 0xfffffff5; + static const int errorFailedSslHandshake = -11; /// Generic file error. - static const int errorFile = 0xfffffff3; + static const int errorFile = -13; /// File not found. - static const int errorFileNotFound = 0xfffffff2; + static const int errorFileNotFound = -14; /// Server or proxy hostname lookup failed. - static const int errorHostLookup = 0xfffffffe; + static const int errorHostLookup = -2; /// Failed to read or write to the server. - static const int errorIO = 0xfffffff9; + static const int errorIO = -7; /// User authentication failed on proxy. - static const int errorProxyAuthentication = 0xfffffffb; + static const int errorProxyAuthentication = -5; /// Too many redirects. - static const int errorRedirectLoop = 0xfffffff7; + static const int errorRedirectLoop = -9; /// Connection timed out. - static const int errorTimeout = 0xfffffff8; + static const int errorTimeout = -8; /// Too many requests during this load. - static const int errorTooManyRequests = 0xfffffff1; + static const int errorTooManyRequests = -15; /// Generic error. - static const int errorUnknown = 0xffffffff; + static const int errorUnknown = -1; /// Resource load was canceled by Safe Browsing. - static const int errorUnsafeResource = 0xfffffff0; + static const int errorUnsafeResource = -16; /// Unsupported authentication scheme (not basic or digest). - static const int errorUnsupportedAuthScheme = 0xfffffffd; + static const int errorUnsupportedAuthScheme = -3; /// Unsupported URI scheme. - static const int errorUnsupportedScheme = 0xfffffff6; + static const int errorUnsupportedScheme = -10; /// Pigeon Host Api implementation for [WebViewClient]. @visibleForTesting @@ -573,6 +564,16 @@ abstract class WebViewClient { /// The interface to be used when content can not be handled by the rendering engine for [WebView], and should be downloaded instead. abstract class DownloadListener { + /// Constructs a [DownloadListener]. + DownloadListener() { + if (!_flutterApisHaveBeenSetup) { + DownloadListenerFlutterApi.setup(DownloadListenerFlutterApiImpl()); + _flutterApisHaveBeenSetup = true; + } + } + + static bool _flutterApisHaveBeenSetup = false; + /// Pigeon Host Api implementation for [DownloadListener]. @visibleForTesting static DownloadListenerHostApiImpl api = DownloadListenerHostApiImpl(); @@ -589,6 +590,16 @@ abstract class DownloadListener { /// Handles JavaScript dialogs, favicons, titles, and the progress for [WebView]. abstract class WebChromeClient { + /// Constructs a [WebChromeClient]. + WebChromeClient() { + if (!_flutterApisHaveBeenSetup) { + WebChromeClientFlutterApi.setup(WebChromeClientFlutterApiImpl()); + _flutterApisHaveBeenSetup = true; + } + } + + static bool _flutterApisHaveBeenSetup = false; + /// Pigeon Host Api implementation for [WebChromeClient]. @visibleForTesting static WebChromeClientHostApiImpl api = WebChromeClientHostApiImpl(); @@ -627,7 +638,7 @@ class WebResourceRequest { final String method; /// Gets the headers associated with the request. - final Map requestHeaders; + final Map? requestHeaders; } /// Encapsulates information about errors occurred during loading of web resources. diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart index 81ec7c7bf80e..2b3f008ed830 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v1.0.7), do not edit directly. +// Autogenerated from Pigeon (v1.0.8), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name // @dart = 2.12 @@ -1039,31 +1039,6 @@ class JavaScriptChannelHostApi { return; } } - - Future dispose(int arg_instanceId) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.JavaScriptChannelHostApi.dispose', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null, - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } } class _JavaScriptChannelFlutterApiCodec extends StandardMessageCodec { @@ -1074,8 +1049,27 @@ abstract class JavaScriptChannelFlutterApi { static const MessageCodec codec = _JavaScriptChannelFlutterApiCodec(); + void dispose(int instanceId); void postMessage(int instanceId, String message); static void setup(JavaScriptChannelFlutterApi? api) { + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.JavaScriptChannelFlutterApi.dispose', codec); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.JavaScriptChannelFlutterApi.dispose was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = args[0] as int?; + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.JavaScriptChannelFlutterApi.dispose was null, expected non-null int.'); + api.dispose(arg_instanceId!); + return; + }); + } + } { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.JavaScriptChannelFlutterApi.postMessage', codec); @@ -1141,31 +1135,6 @@ class WebViewClientHostApi { return; } } - - Future dispose(int arg_instanceId) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewClientHostApi.dispose', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null, - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } } class _WebViewClientFlutterApiCodec extends StandardMessageCodec { @@ -1178,9 +1147,6 @@ class _WebViewClientFlutterApiCodec extends StandardMessageCodec { } else if (value is WebResourceRequestData) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is WebResourceRequestData) { - buffer.putUint8(130); - writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -1195,9 +1161,6 @@ class _WebViewClientFlutterApiCodec extends StandardMessageCodec { case 129: return WebResourceRequestData.decode(readValue(buffer)!); - case 130: - return WebResourceRequestData.decode(readValue(buffer)!); - default: return super.readValueOfType(type, buffer); } @@ -1207,6 +1170,7 @@ class _WebViewClientFlutterApiCodec extends StandardMessageCodec { abstract class WebViewClientFlutterApi { static const MessageCodec codec = _WebViewClientFlutterApiCodec(); + void dispose(int instanceId); void onPageStarted(int instanceId, int webViewInstanceId, String url); void onPageFinished(int instanceId, int webViewInstanceId, String url); void onReceivedRequestError(int instanceId, int webViewInstanceId, @@ -1217,6 +1181,24 @@ abstract class WebViewClientFlutterApi { int instanceId, int webViewInstanceId, WebResourceRequestData request); void urlLoading(int instanceId, int webViewInstanceId, String url); static void setup(WebViewClientFlutterApi? api) { + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewClientFlutterApi.dispose', codec); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.dispose was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = args[0] as int?; + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.dispose was null, expected non-null int.'); + api.dispose(arg_instanceId!); + return; + }); + } + } { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted', codec); @@ -1419,31 +1401,6 @@ class DownloadListenerHostApi { return; } } - - Future dispose(int arg_instanceId) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.DownloadListenerHostApi.dispose', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null, - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } } class _DownloadListenerFlutterApiCodec extends StandardMessageCodec { @@ -1453,9 +1410,28 @@ class _DownloadListenerFlutterApiCodec extends StandardMessageCodec { abstract class DownloadListenerFlutterApi { static const MessageCodec codec = _DownloadListenerFlutterApiCodec(); + void dispose(int instanceId); void onDownloadStart(int instanceId, String url, String userAgent, String contentDisposition, String mimetype, int contentLength); static void setup(DownloadListenerFlutterApi? api) { + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.DownloadListenerFlutterApi.dispose', codec); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.dispose was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = args[0] as int?; + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.dispose was null, expected non-null int.'); + api.dispose(arg_instanceId!); + return; + }); + } + } { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart', @@ -1535,31 +1511,6 @@ class WebChromeClientHostApi { return; } } - - Future dispose(int arg_instanceId) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebChromeClientHostApi.dispose', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null, - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } } class _WebChromeClientFlutterApiCodec extends StandardMessageCodec { @@ -1569,8 +1520,27 @@ class _WebChromeClientFlutterApiCodec extends StandardMessageCodec { abstract class WebChromeClientFlutterApi { static const MessageCodec codec = _WebChromeClientFlutterApiCodec(); + void dispose(int instanceId); void onProgressChanged(int instanceId, int webViewInstanceId, int progress); static void setup(WebChromeClientFlutterApi? api) { + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebChromeClientFlutterApi.dispose', codec); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.dispose was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = args[0] as int?; + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.dispose was null, expected non-null int.'); + api.dispose(arg_instanceId!); + return; + }); + } + } { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged', diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart index f909e49bd802..341b00516d75 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart @@ -239,7 +239,7 @@ class WebSettingsHostApiImpl extends WebSettingsHostApi { WebSettings instance, bool flag, ) { - return setJavaScriptCanOpenWindowsAutomatically( + return setJavaScriptEnabled( instanceManager.getInstanceId(instance)!, flag, ); @@ -337,26 +337,23 @@ class JavaScriptChannelHostApiImpl extends JavaScriptChannelHostApi { return create(instanceId, instance.channelName); } } - - /// Helper method to convert instances ids to objects. - Future disposeFromInstance(JavaScriptChannel instance) async { - final int? instanceId = instanceManager.removeInstance(instance); - if (instanceId != null) { - return dispose(instanceId); - } - } } /// Flutter api implementation for [JavaScriptChannel]. class JavaScriptChannelFlutterApiImpl extends JavaScriptChannelFlutterApi { /// Constructs a [JavaScriptChannelFlutterApiImpl]. - JavaScriptChannelHostApiImpl({InstanceManager? instanceManager}) { + JavaScriptChannelFlutterApiImpl({InstanceManager? instanceManager}) { this.instanceManager = instanceManager ?? InstanceManager.instance; } /// Maintains instances stored to communicate with java objects. late final InstanceManager instanceManager; + @override + void dispose(int instanceId) { + instanceManager.removeInstance(instanceId); + } + @override void postMessage(int instanceId, String message) { final JavaScriptChannel instance = @@ -385,14 +382,6 @@ class WebViewClientHostApiImpl extends WebViewClientHostApi { return create(instanceId, instance.shouldOverrideUrlLoading); } } - - /// Helper method to convert instances ids to objects. - Future disposeFromInstance(WebViewClient instance) async { - final int? instanceId = instanceManager.removeInstance(instance); - if (instanceId != null) { - return dispose(instanceId); - } - } } /// Flutter api implementation for [WebViewClient]. @@ -405,6 +394,11 @@ class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { /// Maintains instances stored to communicate with java objects. late final InstanceManager instanceManager; + @override + void dispose(int instanceId) { + instanceManager.removeInstance(instanceId); + } + @override void onPageFinished(int instanceId, int webViewInstanceId, String url) { final WebViewClient instance = @@ -486,7 +480,7 @@ class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { isRedirect: request.isRedirect, hasGesture: request.hasGesture!, method: request.method!, - requestHeaders: request.requestHeaders!.cast(), + requestHeaders: request.requestHeaders?.cast(), ), ); } @@ -526,14 +520,6 @@ class DownloadListenerHostApiImpl extends DownloadListenerHostApi { return create(instanceId); } } - - /// Helper method to convert instances ids to objects. - Future disposeFromInstance(DownloadListener instance) async { - final int? instanceId = instanceManager.removeInstance(instance); - if (instanceId != null) { - return dispose(instanceId); - } - } } /// Flutter api implementation for [DownloadListener]. @@ -546,6 +532,11 @@ class DownloadListenerFlutterApiImpl extends DownloadListenerFlutterApi { /// Maintains instances stored to communicate with java objects. late final InstanceManager instanceManager; + @override + void dispose(int instanceId) { + instanceManager.removeInstance(instanceId); + } + @override void onDownloadStart( int instanceId, @@ -590,14 +581,6 @@ class WebChromeClientHostApiImpl extends WebChromeClientHostApi { return create(instanceId, instanceManager.getInstanceId(webViewClient)!); } } - - /// Helper method to convert instances ids to objects. - Future disposeFromInstance(WebChromeClient instance) async { - final int? instanceId = instanceManager.removeInstance(instance); - if (instanceId != null) { - return dispose(instanceId); - } - } } /// Flutter api implementation for [DownloadListener]. @@ -610,6 +593,11 @@ class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { /// Maintains instances stored to communicate with java objects. late final InstanceManager instanceManager; + @override + void dispose(int instanceId) { + instanceManager.removeInstance(instanceId); + } + @override void onProgressChanged(int instanceId, int webViewInstanceId, int progress) { final WebChromeClient instance = diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/webview_widget.dart b/packages/webview_flutter/webview_flutter_android/lib/src/webview_widget.dart new file mode 100644 index 000000000000..14b712ff91b8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/src/webview_widget.dart @@ -0,0 +1,500 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/widgets.dart'; + +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'android_webview.dart' as android_webview; + +/// Creates a [Widget] with a [android_webview.WebView]. +class AndroidWebViewWidget extends StatefulWidget { + /// Constructs a [AndroidWebViewWidget]. + AndroidWebViewWidget({ + required this.onBuildWidget, + required this.creationParams, + required this.webViewPlatformCallbacksHandler, + required this.javascriptChannelRegistry, + required this.useHybridComposition, + }); + + /// Callback to build a widget once [android_webview.WebView] has been initialized. + final Widget Function(AndroidWebViewPlatformController platformController) + onBuildWidget; + + /// Initial parameters used to setup the WebView. + final CreationParams creationParams; + + /// Handles callbacks that are made by the created [AndroidWebViewPlatformController]. + final WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler; + + /// Manages named JavaScript channels and forwarding incoming messages on the correct channel. + final JavascriptChannelRegistry javascriptChannelRegistry; + + /// Whether the Widget will be rendered with Hybrid Composition. + final bool useHybridComposition; + + @override + State createState() => _AndroidWebViewWidgetState(); +} + +class _AndroidWebViewWidgetState extends State { + late android_webview.WebView webView; + late AndroidWebViewPlatformController platformController; + + @override + void initState() { + super.initState(); + webView = android_webview.WebView( + useHybridComposition: widget.useHybridComposition, + ); + webView.settings.setDomStorageEnabled(true); + webView.settings.setJavaScriptCanOpenWindowsAutomatically(true); + webView.settings.setSupportMultipleWindows(true); + webView.settings.setLoadWithOverviewMode(true); + webView.settings.setUseWideViewPort(true); + webView.settings.setDisplayZoomControls(false); + webView.settings.setBuiltInZoomControls(true); + + platformController = AndroidWebViewPlatformController( + webView: webView, + callbacksHandler: widget.webViewPlatformCallbacksHandler, + javascriptChannelRegistry: widget.javascriptChannelRegistry, + hasNavigationDelegate: + widget.creationParams.webSettings?.hasNavigationDelegate ?? false, + ); + + setCreationParams(widget.creationParams); + } + + void setCreationParams(CreationParams creationParams) { + final WebSettings? webSettings = creationParams.webSettings; + if (webSettings != null) { + platformController.updateSettings(webSettings); + } + + final String? userAgent = creationParams.userAgent; + if (userAgent != null) { + webView.settings.setUserAgentString(userAgent); + } + + final AutoMediaPlaybackPolicy autoMediaPlaybackPolicy = + creationParams.autoMediaPlaybackPolicy; + switch (autoMediaPlaybackPolicy) { + case AutoMediaPlaybackPolicy.always_allow: + webView.settings.setMediaPlaybackRequiresUserGesture(false); + break; + default: + webView.settings.setMediaPlaybackRequiresUserGesture(true); + } + + platformController.addJavascriptChannels( + creationParams.javascriptChannelNames, + ); + + final String? initialUrl = creationParams.initialUrl; + if (initialUrl != null) { + platformController.loadUrl(initialUrl, {}); + } + } + + @override + void dispose() { + super.dispose(); + platformController.webView.release(); + } + + @override + Widget build(BuildContext context) { + return widget.onBuildWidget(platformController); + } +} + +/// Implementation of [WebViewPlatformController] with the Android WebView api. +class AndroidWebViewPlatformController extends WebViewPlatformController { + /// Construct a [AndroidWebViewPlatformController]. + AndroidWebViewPlatformController({ + required this.webView, + required this.callbacksHandler, + required this.javascriptChannelRegistry, + required bool hasNavigationDelegate, + }) : super(callbacksHandler) { + _webViewClient = _WebViewClientImpl( + callbacksHandler: callbacksHandler, + loadUrl: loadUrl, + hasNavigationDelegate: hasNavigationDelegate, + ); + _downloadListener = _DownloadListenerImpl( + callbacksHandler: callbacksHandler, + loadUrl: loadUrl, + ); + _webChromeClient = _WebChromeClientImpl(callbacksHandler: callbacksHandler); + webView.setWebViewClient(_webViewClient); + webView.setDownloadListener(_downloadListener); + webView.setWebChromeClient(_webChromeClient); + } + + /// Represents the WebView maintained by platform code. + final android_webview.WebView webView; + + /// Handles callbacks that are made by [android_webview.WebViewClient], [android_webview.DownloadListener], and [android_webview.WebChromeClient]. + final WebViewPlatformCallbacksHandler callbacksHandler; + + /// Manages named JavaScript channels and forwarding incoming messages on the correct channel. + final JavascriptChannelRegistry javascriptChannelRegistry; + + final Map _javaScriptChannels = + {}; + late final _DownloadListenerImpl _downloadListener; + late final _WebChromeClientImpl _webChromeClient; + late _WebViewClientImpl _webViewClient; + + @override + Future loadUrl( + String url, + Map? headers, + ) { + return webView.loadUrl(url, headers ?? {}); + } + + @override + Future currentUrl() => webView.getUrl(); + + @override + Future canGoBack() => webView.canGoBack(); + + @override + Future canGoForward() => webView.canGoForward(); + + @override + Future goBack() => webView.goBack(); + + @override + Future goForward() => webView.goForward(); + + @override + Future reload() => webView.reload(); + + @override + Future clearCache() => webView.clearCache(true); + + @override + Future updateSettings(WebSettings settings) { + final bool? hasProgressTracking = settings.hasProgressTracking; + if (hasProgressTracking != null) { + _webChromeClient.hasProgressTracking = hasProgressTracking; + } + + return Future.wait(>[ + _trySetHasNavigationDelegate(settings.hasNavigationDelegate), + _trySetJavaScriptMode(settings.javascriptMode), + _trySetDebuggingEnabled(settings.debuggingEnabled), + _trySetUserAgent(settings.userAgent), + _trySetZoomEnabled(settings.zoomEnabled), + ]); + } + + @override + Future evaluateJavascript(String javascriptString) async { + return await webView.evaluateJavascript(javascriptString) ?? ''; + } + + @override + Future addJavascriptChannels(Set javascriptChannelNames) { + return Future.wait( + javascriptChannelNames.where( + (String channelName) { + return !_javaScriptChannels.containsKey(channelName); + }, + ).map>( + (String channelName) { + final _JavaScriptChannelImpl javaScriptChannel = + _JavaScriptChannelImpl(channelName, javascriptChannelRegistry); + _javaScriptChannels[channelName] = javaScriptChannel; + return webView.addJavaScriptChannel(javaScriptChannel); + }, + ), + ); + } + + @override + Future removeJavascriptChannels( + Set javascriptChannelNames, + ) { + return Future.wait( + javascriptChannelNames.where( + (String channelName) { + return _javaScriptChannels.containsKey(channelName); + }, + ).map>( + (String channelName) { + final _JavaScriptChannelImpl javaScriptChannel = + _javaScriptChannels[channelName]!; + _javaScriptChannels.remove(channelName); + return webView.removeJavaScriptChannel(javaScriptChannel); + }, + ), + ); + } + + @override + Future getTitle() => webView.getTitle(); + + @override + Future scrollTo(int x, int y) => webView.scrollTo(x, y); + + @override + Future scrollBy(int x, int y) => webView.scrollBy(x, y); + + @override + Future getScrollX() => webView.getScrollX(); + + @override + Future getScrollY() => webView.getScrollY(); + + Future _trySetHasNavigationDelegate(bool? hasNavigationDelegate) async { + if (hasNavigationDelegate != null) { + _downloadListener.hasNavigationDelegate = hasNavigationDelegate; + _webViewClient = _WebViewClientImpl( + callbacksHandler: callbacksHandler, + loadUrl: loadUrl, + hasNavigationDelegate: hasNavigationDelegate, + ); + return webView.setWebViewClient(_webViewClient); + } + } + + Future _trySetJavaScriptMode(JavascriptMode? mode) async { + if (mode != null) { + switch (mode) { + case JavascriptMode.disabled: + return webView.settings.setJavaScriptEnabled(false); + case JavascriptMode.unrestricted: + return webView.settings.setJavaScriptEnabled(true); + } + } + } + + Future _trySetDebuggingEnabled(bool? debuggingEnabled) async { + if (debuggingEnabled != null) { + return android_webview.WebView.setWebContentsDebuggingEnabled( + debuggingEnabled, + ); + } + } + + Future _trySetUserAgent(WebSetting userAgent) async { + if (userAgent.isPresent && userAgent.value != null) { + return webView.settings.setUserAgentString(userAgent.value!); + } + + return webView.settings.setUserAgentString(''); + } + + Future _trySetZoomEnabled(bool? zoomEnabled) async { + if (zoomEnabled != null) { + return webView.settings.setSupportZoom(zoomEnabled); + } + } +} + +class _JavaScriptChannelImpl extends android_webview.JavaScriptChannel { + _JavaScriptChannelImpl(String channelName, this.javascriptChannelRegistry) + : super(channelName); + + final JavascriptChannelRegistry javascriptChannelRegistry; + + @override + void postMessage(String message) { + javascriptChannelRegistry.onJavascriptChannelMessage(channelName, message); + } +} + +class _DownloadListenerImpl extends android_webview.DownloadListener { + _DownloadListenerImpl({ + required this.callbacksHandler, + required this.loadUrl, + }); + + final WebViewPlatformCallbacksHandler callbacksHandler; + final Future Function(String url, Map? headers) loadUrl; + bool hasNavigationDelegate = false; + + @override + void onDownloadStart( + String url, + String userAgent, + String contentDisposition, + String mimetype, + int contentLength, + ) { + if (!hasNavigationDelegate) return; + + final FutureOr returnValue = callbacksHandler.onNavigationRequest( + url: url, + isForMainFrame: true, + ); + + if (returnValue is bool && returnValue) { + loadUrl(url, {}); + } else { + (returnValue as Future).then((bool shouldLoadUrl) { + if (shouldLoadUrl) { + loadUrl(url, {}); + } + }); + } + } +} + +class _WebViewClientImpl extends android_webview.WebViewClient { + _WebViewClientImpl({ + required this.callbacksHandler, + required this.loadUrl, + required this.hasNavigationDelegate, + }) : super(shouldOverrideUrlLoading: hasNavigationDelegate); + + final WebViewPlatformCallbacksHandler callbacksHandler; + final Future Function(String url, Map? headers) loadUrl; + final bool hasNavigationDelegate; + + static WebResourceErrorType _errorCodeToErrorType(int errorCode) { + switch (errorCode) { + case android_webview.WebViewClient.errorAuthentication: + return WebResourceErrorType.authentication; + case android_webview.WebViewClient.errorBadUrl: + return WebResourceErrorType.badUrl; + case android_webview.WebViewClient.errorConnect: + return WebResourceErrorType.connect; + case android_webview.WebViewClient.errorFailedSslHandshake: + return WebResourceErrorType.failedSslHandshake; + case android_webview.WebViewClient.errorFile: + return WebResourceErrorType.file; + case android_webview.WebViewClient.errorFileNotFound: + return WebResourceErrorType.fileNotFound; + case android_webview.WebViewClient.errorHostLookup: + return WebResourceErrorType.hostLookup; + case android_webview.WebViewClient.errorIO: + return WebResourceErrorType.io; + case android_webview.WebViewClient.errorProxyAuthentication: + return WebResourceErrorType.proxyAuthentication; + case android_webview.WebViewClient.errorRedirectLoop: + return WebResourceErrorType.redirectLoop; + case android_webview.WebViewClient.errorTimeout: + return WebResourceErrorType.timeout; + case android_webview.WebViewClient.errorTooManyRequests: + return WebResourceErrorType.tooManyRequests; + case android_webview.WebViewClient.errorUnknown: + return WebResourceErrorType.unknown; + case android_webview.WebViewClient.errorUnsafeResource: + return WebResourceErrorType.unsafeResource; + case android_webview.WebViewClient.errorUnsupportedAuthScheme: + return WebResourceErrorType.unsupportedAuthScheme; + case android_webview.WebViewClient.errorUnsupportedScheme: + return WebResourceErrorType.unsupportedScheme; + } + + throw ArgumentError( + 'Could not find a WebResourceErrorType for errorCode: $errorCode', + ); + } + + @override + void onPageStarted(android_webview.WebView webView, String url) { + callbacksHandler.onPageStarted(url); + } + + @override + void onPageFinished(android_webview.WebView webView, String url) { + callbacksHandler.onPageFinished(url); + } + + @override + void onReceivedError( + android_webview.WebView webView, + int errorCode, + String description, + String failingUrl, + ) { + callbacksHandler.onWebResourceError(WebResourceError( + errorCode: errorCode, + description: description, + failingUrl: failingUrl, + errorType: _errorCodeToErrorType(errorCode), + )); + } + + @override + void onReceivedRequestError( + android_webview.WebView webView, + android_webview.WebResourceRequest request, + android_webview.WebResourceError error, + ) { + if (request.isForMainFrame) { + callbacksHandler.onWebResourceError(WebResourceError( + errorCode: error.errorCode, + description: error.description, + failingUrl: request.url, + errorType: _errorCodeToErrorType(error.errorCode), + )); + } + } + + @override + void urlLoading(android_webview.WebView webView, String url) { + if (!hasNavigationDelegate) return; + + final FutureOr returnValue = callbacksHandler.onNavigationRequest( + url: url, + isForMainFrame: true, + ); + + if (returnValue is bool && returnValue) { + loadUrl(url, {}); + } else { + (returnValue as Future).then((bool shouldLoadUrl) { + if (shouldLoadUrl) { + loadUrl(url, {}); + } + }); + } + } + + @override + void requestLoading( + android_webview.WebView webView, + android_webview.WebResourceRequest request, + ) { + if (!hasNavigationDelegate) return; + + final FutureOr returnValue = callbacksHandler.onNavigationRequest( + url: request.url, + isForMainFrame: request.isForMainFrame, + ); + + if (returnValue is bool && returnValue) { + loadUrl(request.url, {}); + } else { + (returnValue as Future).then((bool shouldLoadUrl) { + if (shouldLoadUrl) { + loadUrl(request.url, {}); + } + }); + } + } +} + +class _WebChromeClientImpl extends android_webview.WebChromeClient { + _WebChromeClientImpl({required this.callbacksHandler}); + + final WebViewPlatformCallbacksHandler callbacksHandler; + bool hasProgressTracking = false; + + @override + void onProgressChanged(android_webview.WebView webView, int progress) { + if (hasProgressTracking) callbacksHandler.onProgress(progress); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart index a48e457d55ad..9bbcbb025c12 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart @@ -10,6 +10,9 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'src/webview_widget.dart'; +import 'src/instance_manager.dart'; + /// Builds an Android webview. /// /// This is used as the default implementation for [WebView.platform] on Android. It uses @@ -25,35 +28,38 @@ class AndroidWebView implements WebViewPlatform { WebViewPlatformCreatedCallback? onWebViewPlatformCreated, Set>? gestureRecognizers, }) { - assert(webViewPlatformCallbacksHandler != null); - return GestureDetector( - // We prevent text selection by intercepting the long press event. - // This is a temporary stop gap due to issues with text selection on Android: - // https://github.com/flutter/flutter/issues/24585 - the text selection - // dialog is not responding to touch events. - // https://github.com/flutter/flutter/issues/24584 - the text selection - // handles are not showing. - // TODO(amirh): remove this when the issues above are fixed. - onLongPress: () {}, - excludeFromSemantics: true, - child: AndroidView( - viewType: 'plugins.flutter.io/webview', - onPlatformViewCreated: (int id) { - if (onWebViewPlatformCreated == null) { - return; - } - onWebViewPlatformCreated(MethodChannelWebViewPlatform( - id, - webViewPlatformCallbacksHandler, - javascriptChannelRegistry, - )); - }, - gestureRecognizers: gestureRecognizers, - layoutDirection: Directionality.maybeOf(context) ?? TextDirection.rtl, - creationParams: - MethodChannelWebViewPlatform.creationParamsToMap(creationParams), - creationParamsCodec: const StandardMessageCodec(), - ), + return AndroidWebViewWidget( + onBuildWidget: (AndroidWebViewPlatformController platformController) { + return GestureDetector( + // We prevent text selection by intercepting the long press event. + // This is a temporary stop gap due to issues with text selection on Android: + // https://github.com/flutter/flutter/issues/24585 - the text selection + // dialog is not responding to touch events. + // https://github.com/flutter/flutter/issues/24584 - the text selection + // handles are not showing. + // TODO(amirh): remove this when the issues above are fixed. + onLongPress: () {}, + excludeFromSemantics: true, + child: AndroidView( + viewType: 'plugins.flutter.io/webview', + onPlatformViewCreated: (int id) { + if (onWebViewPlatformCreated != null) { + onWebViewPlatformCreated(platformController); + } + }, + gestureRecognizers: gestureRecognizers, + layoutDirection: + Directionality.maybeOf(context) ?? TextDirection.rtl, + creationParams: InstanceManager.instance + .getInstanceId(platformController.webView), + creationParamsCodec: const StandardMessageCodec(), + ), + ); + }, + creationParams: creationParams, + webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler, + javascriptChannelRegistry: javascriptChannelRegistry, + useHybridComposition: false, ); } diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart index 6beae105e2e5..6bbeae257775 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart @@ -9,6 +9,8 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'src/instance_manager.dart'; +import 'src/webview_widget.dart'; import 'webview_android.dart'; /// Android [WebViewPlatform] that uses [AndroidViewSurface] to build the [WebView] widget. @@ -30,49 +32,47 @@ class SurfaceAndroidWebView extends AndroidWebView { Set>? gestureRecognizers, required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, }) { - assert(webViewPlatformCallbacksHandler != null); - return PlatformViewLink( - viewType: 'plugins.flutter.io/webview', - surfaceFactory: ( - BuildContext context, - PlatformViewController controller, - ) { - return AndroidViewSurface( - controller: controller as AndroidViewController, - gestureRecognizers: gestureRecognizers ?? - const >{}, - hitTestBehavior: PlatformViewHitTestBehavior.opaque, - ); - }, - onCreatePlatformView: (PlatformViewCreationParams params) { - return PlatformViewsService.initSurfaceAndroidView( - id: params.id, + return AndroidWebViewWidget( + onBuildWidget: (AndroidWebViewPlatformController platformController) { + return PlatformViewLink( viewType: 'plugins.flutter.io/webview', - // WebView content is not affected by the Android view's layout direction, - // we explicitly set it here so that the widget doesn't require an ambient - // directionality. - layoutDirection: TextDirection.rtl, - creationParams: MethodChannelWebViewPlatform.creationParamsToMap( - creationParams, - usesHybridComposition: true, - ), - creationParamsCodec: const StandardMessageCodec(), - ) - ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) - ..addOnPlatformViewCreatedListener((int id) { - if (onWebViewPlatformCreated == null) { - return; - } - onWebViewPlatformCreated( - MethodChannelWebViewPlatform( - id, - webViewPlatformCallbacksHandler, - javascriptChannelRegistry, - ), + surfaceFactory: ( + BuildContext context, + PlatformViewController controller, + ) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: gestureRecognizers ?? + const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, ); - }) - ..create(); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + return PlatformViewsService.initSurfaceAndroidView( + id: params.id, + viewType: 'plugins.flutter.io/webview', + // WebView content is not affected by the Android view's layout direction, + // we explicitly set it here so that the widget doesn't require an ambient + // directionality. + layoutDirection: TextDirection.rtl, + creationParams: InstanceManager.instance + .getInstanceId(platformController.webView), + creationParamsCodec: const StandardMessageCodec(), + ) + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) + ..addOnPlatformViewCreatedListener((int id) { + if (onWebViewPlatformCreated != null) { + onWebViewPlatformCreated(platformController); + } + }) + ..create(); + }, + ); }, + creationParams: creationParams, + webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler, + javascriptChannelRegistry: javascriptChannelRegistry, + useHybridComposition: true, ); } } diff --git a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart index 1877c1b91feb..47bc42058ba9 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -105,24 +105,24 @@ abstract class WebSettingsHostApi { @HostApi(dartHostTestHandler: 'TestJavaScriptChannelHostApi') abstract class JavaScriptChannelHostApi { void create(int instanceId, String channelName); - - void dispose(int instanceId); } @FlutterApi() abstract class JavaScriptChannelFlutterApi { + void dispose(int instanceId); + void postMessage(int instanceId, String message); } @HostApi(dartHostTestHandler: 'TestWebViewClientHostApi') abstract class WebViewClientHostApi { void create(int instanceId, bool shouldOverrideUrlLoading); - - void dispose(int instanceId); } @FlutterApi() abstract class WebViewClientFlutterApi { + void dispose(int instanceId); + void onPageStarted(int instanceId, int webViewInstanceId, String url); void onPageFinished(int instanceId, int webViewInstanceId, String url); @@ -154,11 +154,12 @@ abstract class WebViewClientFlutterApi { @HostApi(dartHostTestHandler: 'TestDownloadListenerHostApi') abstract class DownloadListenerHostApi { void create(int instanceId); - void dispose(int instanceId); } @FlutterApi() abstract class DownloadListenerFlutterApi { + void dispose(int instanceId); + void onDownloadStart( int instanceId, String url, @@ -172,10 +173,11 @@ abstract class DownloadListenerFlutterApi { @HostApi(dartHostTestHandler: 'TestWebChromeClientHostApi') abstract class WebChromeClientHostApi { void create(int instanceId, int webViewClientInstanceId); - void dispose(int instanceId); } @FlutterApi() abstract class WebChromeClientFlutterApi { + void dispose(int instanceId); + void onProgressChanged(int instanceId, int webViewInstanceId, int progress); } diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 577d37359af1..f303fcb4f556 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_android description: A Flutter plugin that provides a WebView widget on Android. repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.1.0 +version: 2.1.1 environment: sdk: ">=2.14.0 <3.0.0" @@ -22,10 +22,12 @@ dependencies: webview_flutter_platform_interface: ^1.2.0 dev_dependencies: + build_runner: ^2.1.4 flutter_driver: sdk: flutter flutter_test: sdk: flutter - pigeon: 1.0.7 + mockito: ^5.0.16 + pigeon: 1.0.8 pedantic: ^1.10.0 diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart index 70aa53ca2610..dd549449d851 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v1.0.7), do not edit directly. +// Autogenerated from Pigeon (v1.0.8), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, avoid_relative_lib_imports // @dart = 2.12 @@ -100,7 +100,7 @@ abstract class TestWebViewHostApi { assert(arg_url != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.loadUrl was null, expected non-null String.'); final Map? arg_headers = - args[2] as Map?; + (args[2] as Map).cast(); assert(arg_headers != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.loadUrl was null, expected non-null Map.'); api.loadUrl(arg_instanceId!, arg_url!, arg_headers!); @@ -801,7 +801,6 @@ abstract class TestJavaScriptChannelHostApi { _TestJavaScriptChannelHostApiCodec(); void create(int instanceId, String channelName); - void dispose(int instanceId); static void setup(TestJavaScriptChannelHostApi? api) { { const BasicMessageChannel channel = BasicMessageChannel( @@ -824,24 +823,6 @@ abstract class TestJavaScriptChannelHostApi { }); } } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.JavaScriptChannelHostApi.dispose', codec); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.JavaScriptChannelHostApi.dispose was null.'); - final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; - assert(arg_instanceId != null, - 'Argument for dev.flutter.pigeon.JavaScriptChannelHostApi.dispose was null, expected non-null int.'); - api.dispose(arg_instanceId!); - return {}; - }); - } - } } } @@ -853,7 +834,6 @@ abstract class TestWebViewClientHostApi { static const MessageCodec codec = _TestWebViewClientHostApiCodec(); void create(int instanceId, bool shouldOverrideUrlLoading); - void dispose(int instanceId); static void setup(TestWebViewClientHostApi? api) { { const BasicMessageChannel channel = BasicMessageChannel( @@ -876,24 +856,6 @@ abstract class TestWebViewClientHostApi { }); } } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewClientHostApi.dispose', codec); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewClientHostApi.dispose was null.'); - final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; - assert(arg_instanceId != null, - 'Argument for dev.flutter.pigeon.WebViewClientHostApi.dispose was null, expected non-null int.'); - api.dispose(arg_instanceId!); - return {}; - }); - } - } } } @@ -906,7 +868,6 @@ abstract class TestDownloadListenerHostApi { _TestDownloadListenerHostApiCodec(); void create(int instanceId); - void dispose(int instanceId); static void setup(TestDownloadListenerHostApi? api) { { const BasicMessageChannel channel = BasicMessageChannel( @@ -926,24 +887,6 @@ abstract class TestDownloadListenerHostApi { }); } } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.DownloadListenerHostApi.dispose', codec); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.DownloadListenerHostApi.dispose was null.'); - final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; - assert(arg_instanceId != null, - 'Argument for dev.flutter.pigeon.DownloadListenerHostApi.dispose was null, expected non-null int.'); - api.dispose(arg_instanceId!); - return {}; - }); - } - } } } @@ -955,7 +898,6 @@ abstract class TestWebChromeClientHostApi { static const MessageCodec codec = _TestWebChromeClientHostApiCodec(); void create(int instanceId, int webViewClientInstanceId); - void dispose(int instanceId); static void setup(TestWebChromeClientHostApi? api) { { const BasicMessageChannel channel = BasicMessageChannel( @@ -978,23 +920,5 @@ abstract class TestWebChromeClientHostApi { }); } } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebChromeClientHostApi.dispose', codec); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebChromeClientHostApi.dispose was null.'); - final List args = (message as List?)!; - final int? arg_instanceId = args[0] as int?; - assert(arg_instanceId != null, - 'Argument for dev.flutter.pigeon.WebChromeClientHostApi.dispose was null, expected non-null int.'); - api.dispose(arg_instanceId!); - return {}; - }); - } - } } } diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart index 462670abdb83..cdb93991d0f1 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart @@ -3,19 +3,29 @@ // found in the LICENSE file. import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; import 'package:webview_flutter_android/src/android_webview.dart'; import 'package:webview_flutter_android/src/android_webview_api_impls.dart'; import 'package:webview_flutter_android/src/instance_manager.dart'; import 'android_webview.pigeon.dart'; - +import 'android_webview_test.mocks.dart'; + +@GenerateMocks([ + TestWebViewHostApi, + TestWebSettingsHostApi, + TestWebViewClientHostApi, + TestWebChromeClientHostApi, + TestJavaScriptChannelHostApi, + TestDownloadListenerHostApi +]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('Android WebView', () { group('$WebView', () { setUpAll(() { - TestWebViewHostApi.setup(TestWebViewHostApiImpl()); + TestWebViewHostApi.setup(MockTestWebViewHostApi()); }); setUp(() { @@ -30,8 +40,8 @@ void main() { group('$WebSettings', () { setUpAll(() { - TestWebViewHostApi.setup(TestWebViewHostApiImpl()); - TestWebSettingsHostApi.setup(TestWebSettingsHostApiImpl()); + TestWebViewHostApi.setup(MockTestWebViewHostApi()); + TestWebSettingsHostApi.setup(MockTestWebSettingsHostApi()); }); setUp(() { @@ -54,8 +64,8 @@ void main() { group('$JavaScriptChannel', () { setUpAll(() { - TestWebViewHostApi.setup(TestWebViewHostApiImpl()); - TestJavaScriptChannelHostApi.setup(TestJavaScriptChannelHostApiImpl()); + TestWebViewHostApi.setup(MockTestWebViewHostApi()); + TestJavaScriptChannelHostApi.setup(MockTestJavaScriptChannelHostApi()); }); setUp(() { @@ -75,19 +85,13 @@ void main() { JavaScriptChannel.api.instanceManager.getInstanceId(channel), isNotNull, ); - - webView.removeJavaScriptChannel(channel); - expect( - JavaScriptChannel.api.instanceManager.getInstanceId(channel), - isNull, - ); }); }); group('$WebViewClient', () { setUpAll(() { - TestWebViewHostApi.setup(TestWebViewHostApiImpl()); - TestWebViewClientHostApi.setup(TestWebViewClientHostApiImpl()); + TestWebViewHostApi.setup(MockTestWebViewHostApi()); + TestWebViewClientHostApi.setup(MockTestWebViewClientHostApi()); }); setUp(() { @@ -100,27 +104,20 @@ void main() { test('create', () { final WebView webView = WebView(); - final WebViewClient webViewClient1 = TestWebViewClient(); - final WebViewClient webViewClient2 = TestWebViewClient(); + final WebViewClient webViewClient = TestWebViewClient(); - webView.setWebViewClient(webViewClient1); + webView.setWebViewClient(webViewClient); expect( - WebViewClient.api.instanceManager.getInstanceId(webViewClient1), + WebViewClient.api.instanceManager.getInstanceId(webViewClient), isNotNull, ); - - webView.setWebViewClient(webViewClient2); - expect( - WebViewClient.api.instanceManager.getInstanceId(webViewClient1), - isNull, - ); }); }); group('$DownloadListener', () { setUpAll(() { - TestWebViewHostApi.setup(TestWebViewHostApiImpl()); - TestDownloadListenerHostApi.setup(TestDownloadListenerHostApiImpl()); + TestWebViewHostApi.setup(MockTestWebViewHostApi()); + TestDownloadListenerHostApi.setup(MockTestDownloadListenerHostApi()); }); setUp(() { @@ -133,28 +130,21 @@ void main() { test('create', () { final WebView webView = WebView(); - final DownloadListener downloadListener1 = TestDownloadListener(); - final DownloadListener downloadListener2 = TestDownloadListener(); + final DownloadListener downloadListener = TestDownloadListener(); - webView.setDownloadListener(downloadListener1); + webView.setDownloadListener(downloadListener); expect( - DownloadListener.api.instanceManager.getInstanceId(downloadListener1), + DownloadListener.api.instanceManager.getInstanceId(downloadListener), isNotNull, ); - - webView.setDownloadListener(downloadListener2); - expect( - DownloadListener.api.instanceManager.getInstanceId(downloadListener1), - isNull, - ); }); }); group('$WebChromeClient', () { setUpAll(() { - TestWebViewHostApi.setup(TestWebViewHostApiImpl()); - TestWebViewClientHostApi.setup(TestWebViewClientHostApiImpl()); - TestWebChromeClientHostApi.setup(TestWebChromeClientHostApiImpl()); + TestWebViewHostApi.setup(MockTestWebViewHostApi()); + TestWebViewClientHostApi.setup(MockTestWebViewClientHostApi()); + TestWebChromeClientHostApi.setup(MockTestWebChromeClientHostApi()); }); setUp(() { @@ -172,20 +162,13 @@ void main() { final WebView webView = WebView(); webView.setWebViewClient(TestWebViewClient()); - final WebChromeClient webChromeClient1 = TestWebChromeClient(); - final WebChromeClient webChromeClient2 = TestWebChromeClient(); + final WebChromeClient webChromeClient = TestWebChromeClient(); - webView.setWebChromeClient(webChromeClient1); + webView.setWebChromeClient(webChromeClient); expect( - WebChromeClient.api.instanceManager.getInstanceId(webChromeClient1), + WebChromeClient.api.instanceManager.getInstanceId(webChromeClient), isNotNull, ); - - webView.setWebChromeClient(webChromeClient2); - expect( - WebChromeClient.api.instanceManager.getInstanceId(webChromeClient1), - isNull, - ); }); }); }); @@ -215,159 +198,3 @@ class TestWebChromeClient extends WebChromeClient { @override void onProgressChanged(WebView webView, int progress) {} } - -class TestWebViewHostApiImpl extends TestWebViewHostApi { - @override - void addJavaScriptChannel(int instanceId, int javaScriptChannelInstanceId) {} - - @override - bool canGoBack(int instanceId) { - throw UnimplementedError(); - } - - @override - bool canGoForward(int instanceId) { - throw UnimplementedError(); - } - - @override - void clearCache(int instanceId, bool includeDiskFiles) {} - - @override - void create(int instanceId, bool useHybridComposition) {} - - @override - void dispose(int instanceId) {} - - @override - Future evaluateJavascript(int instanceId, String javascriptString) { - throw UnimplementedError(); - } - - @override - int getScrollX(int instanceId) { - throw UnimplementedError(); - } - - @override - int getScrollY(int instanceId) { - throw UnimplementedError(); - } - - @override - String getTitle(int instanceId) { - throw UnimplementedError(); - } - - @override - String getUrl(int instanceId) { - throw UnimplementedError(); - } - - @override - void goBack(int instanceId) {} - - @override - void goForward(int instanceId) {} - - @override - void loadUrl(int instanceId, String url, Map headers) {} - - @override - void reload(int instanceId) {} - - @override - void removeJavaScriptChannel( - int instanceId, int javaScriptChannelInstanceId) {} - - @override - void scrollBy(int instanceId, int x, int y) {} - - @override - void scrollTo(int instanceId, int x, int y) {} - - @override - void setDownloadListener(int instanceId, int listenerInstanceId) {} - - @override - void setWebContentsDebuggingEnabled(bool enabled) {} - - @override - void setWebViewClient(int instanceId, int webViewClientInstanceId) {} - - @override - void setWebChromeClient(int instanceId, int clientInstanceId) {} -} - -class TestWebSettingsHostApiImpl extends TestWebSettingsHostApi { - @override - void create(int instanceId, int webViewInstanceId) {} - - @override - void dispose(int instanceId) {} - - @override - void setBuiltInZoomControls(int instanceId, bool enabled) {} - - @override - void setDisplayZoomControls(int instanceId, bool enabled) {} - - @override - void setDomStorageEnabled(int instanceId, bool flag) {} - - @override - void setJavaScriptCanOpenWindowsAutomatically(int instanceId, bool flag) {} - - @override - void setJavaScriptEnabled(int instanceId, bool flag) {} - - @override - void setLoadWithOverviewMode(int instanceId, bool overview) {} - - @override - void setMediaPlaybackRequiresUserGesture(int instanceId, bool require) {} - - @override - void setSupportMultipleWindows(int instanceId, bool support) {} - - @override - void setSupportZoom(int instanceId, bool support) {} - - @override - void setUseWideViewPort(int instanceId, bool use) {} - - @override - void setUserAgentString(int instanceId, String userAgentString) {} -} - -class TestJavaScriptChannelHostApiImpl extends TestJavaScriptChannelHostApi { - @override - void create(int instanceId, String channelName) {} - - @override - void dispose(int instanceId) {} -} - -class TestWebViewClientHostApiImpl extends TestWebViewClientHostApi { - @override - void create(int instanceId, bool shouldOverrideUrlLoading) {} - - @override - void dispose(int instanceId) {} -} - -class TestDownloadListenerHostApiImpl extends TestDownloadListenerHostApi { - @override - void create(int instanceId) {} - - @override - void dispose(int instanceId) {} -} - -class TestWebChromeClientHostApiImpl extends TestWebChromeClientHostApi { - @override - void create(int instanceId, int webViewClientInstanceId) {} - - @override - void dispose(int instanceId) {} -} diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart new file mode 100644 index 000000000000..57ab9e82ac1c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart @@ -0,0 +1,285 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Mocks generated by Mockito 5.0.16 from annotations +// in webview_flutter_android/test/android_webview_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i3; + +import 'package:mockito/mockito.dart' as _i1; + +import 'android_webview.pigeon.dart' as _i2; + +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types + +/// A class which mocks [TestWebViewHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWebViewHostApi extends _i1.Mock + implements _i2.TestWebViewHostApi { + MockTestWebViewHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId, bool? useHybridComposition) => + super.noSuchMethod( + Invocation.method(#create, [instanceId, useHybridComposition]), + returnValueForMissingStub: null); + @override + void dispose(int? instanceId) => + super.noSuchMethod(Invocation.method(#dispose, [instanceId]), + returnValueForMissingStub: null); + @override + void loadUrl(int? instanceId, String? url, Map? headers) => + super.noSuchMethod( + Invocation.method(#loadUrl, [instanceId, url, headers]), + returnValueForMissingStub: null); + @override + String getUrl(int? instanceId) => + (super.noSuchMethod(Invocation.method(#getUrl, [instanceId]), + returnValue: '') as String); + @override + bool canGoBack(int? instanceId) => + (super.noSuchMethod(Invocation.method(#canGoBack, [instanceId]), + returnValue: false) as bool); + @override + bool canGoForward(int? instanceId) => + (super.noSuchMethod(Invocation.method(#canGoForward, [instanceId]), + returnValue: false) as bool); + @override + void goBack(int? instanceId) => + super.noSuchMethod(Invocation.method(#goBack, [instanceId]), + returnValueForMissingStub: null); + @override + void goForward(int? instanceId) => + super.noSuchMethod(Invocation.method(#goForward, [instanceId]), + returnValueForMissingStub: null); + @override + void reload(int? instanceId) => + super.noSuchMethod(Invocation.method(#reload, [instanceId]), + returnValueForMissingStub: null); + @override + void clearCache(int? instanceId, bool? includeDiskFiles) => + super.noSuchMethod( + Invocation.method(#clearCache, [instanceId, includeDiskFiles]), + returnValueForMissingStub: null); + @override + _i3.Future evaluateJavascript( + int? instanceId, String? javascriptString) => + (super.noSuchMethod( + Invocation.method( + #evaluateJavascript, [instanceId, javascriptString]), + returnValue: Future.value('')) as _i3.Future); + @override + String getTitle(int? instanceId) => + (super.noSuchMethod(Invocation.method(#getTitle, [instanceId]), + returnValue: '') as String); + @override + void scrollTo(int? instanceId, int? x, int? y) => + super.noSuchMethod(Invocation.method(#scrollTo, [instanceId, x, y]), + returnValueForMissingStub: null); + @override + void scrollBy(int? instanceId, int? x, int? y) => + super.noSuchMethod(Invocation.method(#scrollBy, [instanceId, x, y]), + returnValueForMissingStub: null); + @override + int getScrollX(int? instanceId) => + (super.noSuchMethod(Invocation.method(#getScrollX, [instanceId]), + returnValue: 0) as int); + @override + int getScrollY(int? instanceId) => + (super.noSuchMethod(Invocation.method(#getScrollY, [instanceId]), + returnValue: 0) as int); + @override + void setWebContentsDebuggingEnabled(bool? enabled) => super.noSuchMethod( + Invocation.method(#setWebContentsDebuggingEnabled, [enabled]), + returnValueForMissingStub: null); + @override + void setWebViewClient(int? instanceId, int? webViewClientInstanceId) => + super.noSuchMethod( + Invocation.method( + #setWebViewClient, [instanceId, webViewClientInstanceId]), + returnValueForMissingStub: null); + @override + void addJavaScriptChannel( + int? instanceId, int? javaScriptChannelInstanceId) => + super.noSuchMethod( + Invocation.method( + #addJavaScriptChannel, [instanceId, javaScriptChannelInstanceId]), + returnValueForMissingStub: null); + @override + void removeJavaScriptChannel( + int? instanceId, int? javaScriptChannelInstanceId) => + super.noSuchMethod( + Invocation.method(#removeJavaScriptChannel, + [instanceId, javaScriptChannelInstanceId]), + returnValueForMissingStub: null); + @override + void setDownloadListener(int? instanceId, int? listenerInstanceId) => + super.noSuchMethod( + Invocation.method( + #setDownloadListener, [instanceId, listenerInstanceId]), + returnValueForMissingStub: null); + @override + void setWebChromeClient(int? instanceId, int? clientInstanceId) => + super.noSuchMethod( + Invocation.method( + #setWebChromeClient, [instanceId, clientInstanceId]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [TestWebSettingsHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWebSettingsHostApi extends _i1.Mock + implements _i2.TestWebSettingsHostApi { + MockTestWebSettingsHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId, int? webViewInstanceId) => super.noSuchMethod( + Invocation.method(#create, [instanceId, webViewInstanceId]), + returnValueForMissingStub: null); + @override + void dispose(int? instanceId) => + super.noSuchMethod(Invocation.method(#dispose, [instanceId]), + returnValueForMissingStub: null); + @override + void setDomStorageEnabled(int? instanceId, bool? flag) => super.noSuchMethod( + Invocation.method(#setDomStorageEnabled, [instanceId, flag]), + returnValueForMissingStub: null); + @override + void setJavaScriptCanOpenWindowsAutomatically(int? instanceId, bool? flag) => + super.noSuchMethod( + Invocation.method( + #setJavaScriptCanOpenWindowsAutomatically, [instanceId, flag]), + returnValueForMissingStub: null); + @override + void setSupportMultipleWindows(int? instanceId, bool? support) => + super.noSuchMethod( + Invocation.method(#setSupportMultipleWindows, [instanceId, support]), + returnValueForMissingStub: null); + @override + void setJavaScriptEnabled(int? instanceId, bool? flag) => super.noSuchMethod( + Invocation.method(#setJavaScriptEnabled, [instanceId, flag]), + returnValueForMissingStub: null); + @override + void setUserAgentString(int? instanceId, String? userAgentString) => + super.noSuchMethod( + Invocation.method(#setUserAgentString, [instanceId, userAgentString]), + returnValueForMissingStub: null); + @override + void setMediaPlaybackRequiresUserGesture(int? instanceId, bool? require) => + super.noSuchMethod( + Invocation.method( + #setMediaPlaybackRequiresUserGesture, [instanceId, require]), + returnValueForMissingStub: null); + @override + void setSupportZoom(int? instanceId, bool? support) => super.noSuchMethod( + Invocation.method(#setSupportZoom, [instanceId, support]), + returnValueForMissingStub: null); + @override + void setLoadWithOverviewMode(int? instanceId, bool? overview) => + super.noSuchMethod( + Invocation.method(#setLoadWithOverviewMode, [instanceId, overview]), + returnValueForMissingStub: null); + @override + void setUseWideViewPort(int? instanceId, bool? use) => super.noSuchMethod( + Invocation.method(#setUseWideViewPort, [instanceId, use]), + returnValueForMissingStub: null); + @override + void setDisplayZoomControls(int? instanceId, bool? enabled) => + super.noSuchMethod( + Invocation.method(#setDisplayZoomControls, [instanceId, enabled]), + returnValueForMissingStub: null); + @override + void setBuiltInZoomControls(int? instanceId, bool? enabled) => + super.noSuchMethod( + Invocation.method(#setBuiltInZoomControls, [instanceId, enabled]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [TestWebViewClientHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWebViewClientHostApi extends _i1.Mock + implements _i2.TestWebViewClientHostApi { + MockTestWebViewClientHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId, bool? shouldOverrideUrlLoading) => + super.noSuchMethod( + Invocation.method(#create, [instanceId, shouldOverrideUrlLoading]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [TestWebChromeClientHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWebChromeClientHostApi extends _i1.Mock + implements _i2.TestWebChromeClientHostApi { + MockTestWebChromeClientHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId, int? webViewClientInstanceId) => + super.noSuchMethod( + Invocation.method(#create, [instanceId, webViewClientInstanceId]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [TestJavaScriptChannelHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestJavaScriptChannelHostApi extends _i1.Mock + implements _i2.TestJavaScriptChannelHostApi { + MockTestJavaScriptChannelHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId, String? channelName) => + super.noSuchMethod(Invocation.method(#create, [instanceId, channelName]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [TestDownloadListenerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestDownloadListenerHostApi extends _i1.Mock + implements _i2.TestDownloadListenerHostApi { + MockTestDownloadListenerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId) => + super.noSuchMethod(Invocation.method(#create, [instanceId]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} diff --git a/packages/webview_flutter/webview_flutter_android/test/webview_widget_test.dart b/packages/webview_flutter/webview_flutter_android/test/webview_widget_test.dart new file mode 100644 index 000000000000..a5b2fdd8ade0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/webview_widget_test.dart @@ -0,0 +1,429 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:webview_flutter_android/src/android_webview.dart' + as android_webview; +import 'package:webview_flutter_android/src/android_webview_api_impls.dart'; +import 'package:webview_flutter_android/src/instance_manager.dart'; +import 'package:webview_flutter_android/src/webview_widget.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'android_webview.pigeon.dart'; +import 'webview_widget_test.mocks.dart'; + +@GenerateMocks([ + TestWebViewHostApi, + TestWebSettingsHostApi, + TestWebViewClientHostApi, + TestWebChromeClientHostApi, + TestJavaScriptChannelHostApi, + TestDownloadListenerHostApi, + WebViewPlatformCallbacksHandler, + JavascriptChannelRegistry, +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$AndroidWebViewWidget', () { + late MockTestWebViewHostApi mockWebViewHostApi; + late MockTestWebSettingsHostApi mockWebSettingsHostApi; + late MockTestWebViewClientHostApi mockWebViewClientHostApi; + late MockTestWebChromeClientHostApi mockWebChromeClientHostApi; + late MockTestJavaScriptChannelHostApi mockJavaScriptChannelHostApi; + late MockTestDownloadListenerHostApi mockDownloadListenerHostApi; + + late WebViewPlatformCallbacksHandler mockCallbacksHandler; + late JavascriptChannelRegistry mockJavascriptChannelRegistry; + + setUp(() { + mockWebViewHostApi = MockTestWebViewHostApi(); + mockWebSettingsHostApi = MockTestWebSettingsHostApi(); + mockWebViewClientHostApi = MockTestWebViewClientHostApi(); + mockWebChromeClientHostApi = MockTestWebChromeClientHostApi(); + mockJavaScriptChannelHostApi = MockTestJavaScriptChannelHostApi(); + mockDownloadListenerHostApi = MockTestDownloadListenerHostApi(); + + TestWebViewHostApi.setup(mockWebViewHostApi); + TestWebSettingsHostApi.setup(mockWebSettingsHostApi); + TestWebViewClientHostApi.setup(mockWebViewClientHostApi); + TestWebChromeClientHostApi.setup(mockWebChromeClientHostApi); + TestJavaScriptChannelHostApi.setup(mockJavaScriptChannelHostApi); + TestDownloadListenerHostApi.setup(mockDownloadListenerHostApi); + + mockCallbacksHandler = MockWebViewPlatformCallbacksHandler(); + mockJavascriptChannelRegistry = MockJavascriptChannelRegistry(); + + final InstanceManager instanceManager = InstanceManager(); + android_webview.WebView.api = WebViewHostApiImpl( + instanceManager: instanceManager, + ); + android_webview.WebSettings.api = WebSettingsHostApiImpl( + instanceManager: instanceManager, + ); + android_webview.JavaScriptChannel.api = JavaScriptChannelHostApiImpl( + instanceManager: instanceManager, + ); + android_webview.WebViewClient.api = WebViewClientHostApiImpl( + instanceManager: instanceManager, + ); + android_webview.DownloadListener.api = DownloadListenerHostApiImpl( + instanceManager: instanceManager, + ); + android_webview.WebChromeClient.api = WebChromeClientHostApiImpl( + instanceManager: instanceManager, + ); + }); + + // Builds a AndroidWebViewWidget with default parameters. + Future buildWidget( + WidgetTester tester, { + Widget Function(AndroidWebViewPlatformController platformController)? + onBuildWidget, + CreationParams? creationParams, + WebViewPlatformCallbacksHandler? webViewPlatformCallbacksHandler, + JavascriptChannelRegistry? javascriptChannelRegistry, + bool? useHybridComposition, + }) async { + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + AndroidWebViewWidget( + onBuildWidget: onBuildWidget ?? + (AndroidWebViewPlatformController controller) { + controllerCompleter.complete(controller); + return Container(); + }, + creationParams: creationParams ?? CreationParams(), + webViewPlatformCallbacksHandler: + webViewPlatformCallbacksHandler ?? mockCallbacksHandler, + javascriptChannelRegistry: + javascriptChannelRegistry ?? mockJavascriptChannelRegistry, + useHybridComposition: useHybridComposition ?? false, + ), + ); + + return controllerCompleter.future; + } + + testWidgets('Create Widget', (WidgetTester tester) async { + await buildWidget(tester); + + verify(mockWebSettingsHostApi.setDomStorageEnabled(1, true)); + verify(mockWebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically( + 1, + true, + )); + verify(mockWebSettingsHostApi.setSupportMultipleWindows(1, true)); + verify(mockWebSettingsHostApi.setLoadWithOverviewMode(1, true)); + verify(mockWebSettingsHostApi.setUseWideViewPort(1, true)); + verify(mockWebSettingsHostApi.setDisplayZoomControls(1, false)); + verify(mockWebSettingsHostApi.setBuiltInZoomControls(1, true)); + + verifyInOrder([ + mockWebViewHostApi.create(0, false), + mockWebViewHostApi.setWebViewClient(0, any), + mockWebViewHostApi.setDownloadListener(0, any), + mockWebViewHostApi.setWebChromeClient(0, any), + ]); + }); + + testWidgets( + 'Create Widget with Hybrid Composition', + (WidgetTester tester) async { + await buildWidget(tester, useHybridComposition: true); + verify(mockWebViewHostApi.create(0, true)); + }, + ); + + group('CreationParams', () { + testWidgets('initialUrl', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams(initialUrl: 'https://www.google.com'), + ); + verify(mockWebViewHostApi.loadUrl( + 0, + 'https://www.google.com', + {}, + )); + }); + + testWidgets('userAgent', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams(userAgent: 'MyUserAgent'), + ); + + verify(mockWebSettingsHostApi.setUserAgentString(1, 'MyUserAgent')); + }); + + testWidgets('autoMediaPlaybackPolicy true', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + autoMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ); + + verify( + mockWebSettingsHostApi.setMediaPlaybackRequiresUserGesture(any, true), + ); + }); + + testWidgets('autoMediaPlaybackPolicy false', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + autoMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ); + + verify(mockWebSettingsHostApi.setMediaPlaybackRequiresUserGesture( + any, + false, + )); + }); + + testWidgets('javascriptChannelNames', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + javascriptChannelNames: {'a', 'b'}, + ), + ); + + verify(mockJavaScriptChannelHostApi.create(any, 'a')); + verify(mockJavaScriptChannelHostApi.create(any, 'b')); + verify(mockWebViewHostApi.addJavaScriptChannel(0, any)).called(2); + }); + + group('WebSettings', () { + testWidgets('javascriptMode', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: WebSetting.absent(), + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + + verify(mockWebSettingsHostApi.setJavaScriptEnabled(any, true)); + }); + + testWidgets('hasNavigationDelegate', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: WebSetting.absent(), + hasNavigationDelegate: true, + ), + ), + ); + + verify(mockWebViewClientHostApi.create(any, true)); + }); + + testWidgets('debuggingEnabled', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: WebSetting.absent(), + debuggingEnabled: true, + ), + ), + ); + + verify(mockWebViewHostApi.setWebContentsDebuggingEnabled(true)); + }); + + testWidgets('userAgent', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: WebSetting.of('myUserAgent'), + ), + ), + ); + + verify(mockWebSettingsHostApi.setUserAgentString(any, 'myUserAgent')); + }); + + testWidgets('zoomEnabled', (WidgetTester tester) async { + await buildWidget( + tester, + creationParams: CreationParams( + webSettings: WebSettings( + userAgent: WebSetting.absent(), + zoomEnabled: false, + ), + ), + ); + + verify(mockWebSettingsHostApi.setSupportZoom(any, false)); + }); + }); + }); + + group('$AndroidWebViewPlatformController', () { + testWidgets('loadUrl', (WidgetTester tester) async { + final AndroidWebViewPlatformController controller = + await buildWidget(tester); + + await controller.loadUrl( + 'https://www.google.com', + {'a': 'header'}, + ); + + verify(mockWebViewHostApi.loadUrl( + any, + 'https://www.google.com', + {'a': 'header'}, + )); + }); + + testWidgets('currentUrl', (WidgetTester tester) async { + final AndroidWebViewPlatformController controller = + await buildWidget(tester); + + when(mockWebViewHostApi.getUrl(any)) + .thenReturn('https://www.google.com'); + expect(controller.currentUrl(), completion('https://www.google.com')); + }); + + testWidgets('canGoBack', (WidgetTester tester) async { + final AndroidWebViewPlatformController controller = + await buildWidget(tester); + + when(mockWebViewHostApi.canGoBack(any)).thenReturn(false); + expect(controller.canGoBack(), completion(false)); + }); + + testWidgets('canGoForward', (WidgetTester tester) async { + final AndroidWebViewPlatformController controller = + await buildWidget(tester); + + when(mockWebViewHostApi.canGoForward(any)).thenReturn(true); + expect(controller.canGoForward(), completion(true)); + }); + + testWidgets('goBack', (WidgetTester tester) async { + final AndroidWebViewPlatformController controller = + await buildWidget(tester); + + await controller.goBack(); + verify(mockWebViewHostApi.goBack(any)); + }); + + testWidgets('goForward', (WidgetTester tester) async { + final AndroidWebViewPlatformController controller = + await buildWidget(tester); + + await controller.goForward(); + verify(mockWebViewHostApi.goForward(any)); + }); + + testWidgets('reload', (WidgetTester tester) async { + final AndroidWebViewPlatformController controller = + await buildWidget(tester); + + await controller.reload(); + verify(mockWebViewHostApi.reload(any)); + }); + + testWidgets('clearCache', (WidgetTester tester) async { + final AndroidWebViewPlatformController controller = + await buildWidget(tester); + + await controller.clearCache(); + verify(mockWebViewHostApi.clearCache(any, true)); + }); + + testWidgets('evaluateJavascript', (WidgetTester tester) async { + final AndroidWebViewPlatformController controller = + await buildWidget(tester); + + when(mockWebViewHostApi.evaluateJavascript(any, 'runJavaScript')) + .thenAnswer( + (_) => Future.value('returnString'), + ); + expect( + controller.evaluateJavascript('runJavaScript'), + completion('returnString'), + ); + }); + + testWidgets('addJavascriptChannels', (WidgetTester tester) async { + final AndroidWebViewPlatformController controller = + await buildWidget(tester); + + await controller.addJavascriptChannels({'c', 'd'}); + verify(mockJavaScriptChannelHostApi.create(any, 'c')); + verify(mockJavaScriptChannelHostApi.create(any, 'd')); + verify(mockWebViewHostApi.addJavaScriptChannel(0, any)).called(2); + }); + + testWidgets('removeJavascriptChannels', (WidgetTester tester) async { + final AndroidWebViewPlatformController controller = + await buildWidget(tester); + + await controller.addJavascriptChannels({'c', 'd'}); + await controller.removeJavascriptChannels({'c', 'd'}); + verify(mockWebViewHostApi.removeJavaScriptChannel(0, any)).called(2); + }); + + testWidgets('getTitle', (WidgetTester tester) async { + final AndroidWebViewPlatformController controller = + await buildWidget(tester); + + when(mockWebViewHostApi.getTitle(any)).thenReturn('Web Title'); + expect(controller.getTitle(), completion('Web Title')); + }); + + testWidgets('scrollTo', (WidgetTester tester) async { + final AndroidWebViewPlatformController controller = + await buildWidget(tester); + + await controller.scrollTo(1, 2); + verify(mockWebViewHostApi.scrollTo(any, 1, 2)); + }); + + testWidgets('scrollBy', (WidgetTester tester) async { + final AndroidWebViewPlatformController controller = + await buildWidget(tester); + + await controller.scrollBy(3, 4); + verify(mockWebViewHostApi.scrollBy(any, 3, 4)); + }); + + testWidgets('getScrollX', (WidgetTester tester) async { + final AndroidWebViewPlatformController controller = + await buildWidget(tester); + + when(mockWebViewHostApi.getScrollX(any)).thenReturn(23); + expect(controller.getScrollX(), completion(23)); + }); + + testWidgets('getScrollY', (WidgetTester tester) async { + final AndroidWebViewPlatformController controller = + await buildWidget(tester); + + when(mockWebViewHostApi.getScrollY(any)).thenReturn(25); + expect(controller.getScrollY(), completion(25)); + }); + }); + }); +} diff --git a/packages/webview_flutter/webview_flutter_android/test/webview_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/webview_widget_test.mocks.dart new file mode 100644 index 000000000000..5f4381a580d4 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/webview_widget_test.mocks.dart @@ -0,0 +1,353 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Mocks generated by Mockito 5.0.16 from annotations +// in webview_flutter_android/test/webview_widget_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i3; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_platform_interface/src/platform_interface/javascript_channel_registry.dart' + as _i6; +import 'package:webview_flutter_platform_interface/src/platform_interface/webview_platform_callbacks_handler.dart' + as _i4; +import 'package:webview_flutter_platform_interface/src/types/types.dart' as _i5; + +import 'android_webview.pigeon.dart' as _i2; + +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types + +/// A class which mocks [TestWebViewHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWebViewHostApi extends _i1.Mock + implements _i2.TestWebViewHostApi { + MockTestWebViewHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId, bool? useHybridComposition) => + super.noSuchMethod( + Invocation.method(#create, [instanceId, useHybridComposition]), + returnValueForMissingStub: null); + @override + void dispose(int? instanceId) => + super.noSuchMethod(Invocation.method(#dispose, [instanceId]), + returnValueForMissingStub: null); + @override + void loadUrl(int? instanceId, String? url, Map? headers) => + super.noSuchMethod( + Invocation.method(#loadUrl, [instanceId, url, headers]), + returnValueForMissingStub: null); + @override + String getUrl(int? instanceId) => + (super.noSuchMethod(Invocation.method(#getUrl, [instanceId]), + returnValue: '') as String); + @override + bool canGoBack(int? instanceId) => + (super.noSuchMethod(Invocation.method(#canGoBack, [instanceId]), + returnValue: false) as bool); + @override + bool canGoForward(int? instanceId) => + (super.noSuchMethod(Invocation.method(#canGoForward, [instanceId]), + returnValue: false) as bool); + @override + void goBack(int? instanceId) => + super.noSuchMethod(Invocation.method(#goBack, [instanceId]), + returnValueForMissingStub: null); + @override + void goForward(int? instanceId) => + super.noSuchMethod(Invocation.method(#goForward, [instanceId]), + returnValueForMissingStub: null); + @override + void reload(int? instanceId) => + super.noSuchMethod(Invocation.method(#reload, [instanceId]), + returnValueForMissingStub: null); + @override + void clearCache(int? instanceId, bool? includeDiskFiles) => + super.noSuchMethod( + Invocation.method(#clearCache, [instanceId, includeDiskFiles]), + returnValueForMissingStub: null); + @override + _i3.Future evaluateJavascript( + int? instanceId, String? javascriptString) => + (super.noSuchMethod( + Invocation.method( + #evaluateJavascript, [instanceId, javascriptString]), + returnValue: Future.value('')) as _i3.Future); + @override + String getTitle(int? instanceId) => + (super.noSuchMethod(Invocation.method(#getTitle, [instanceId]), + returnValue: '') as String); + @override + void scrollTo(int? instanceId, int? x, int? y) => + super.noSuchMethod(Invocation.method(#scrollTo, [instanceId, x, y]), + returnValueForMissingStub: null); + @override + void scrollBy(int? instanceId, int? x, int? y) => + super.noSuchMethod(Invocation.method(#scrollBy, [instanceId, x, y]), + returnValueForMissingStub: null); + @override + int getScrollX(int? instanceId) => + (super.noSuchMethod(Invocation.method(#getScrollX, [instanceId]), + returnValue: 0) as int); + @override + int getScrollY(int? instanceId) => + (super.noSuchMethod(Invocation.method(#getScrollY, [instanceId]), + returnValue: 0) as int); + @override + void setWebContentsDebuggingEnabled(bool? enabled) => super.noSuchMethod( + Invocation.method(#setWebContentsDebuggingEnabled, [enabled]), + returnValueForMissingStub: null); + @override + void setWebViewClient(int? instanceId, int? webViewClientInstanceId) => + super.noSuchMethod( + Invocation.method( + #setWebViewClient, [instanceId, webViewClientInstanceId]), + returnValueForMissingStub: null); + @override + void addJavaScriptChannel( + int? instanceId, int? javaScriptChannelInstanceId) => + super.noSuchMethod( + Invocation.method( + #addJavaScriptChannel, [instanceId, javaScriptChannelInstanceId]), + returnValueForMissingStub: null); + @override + void removeJavaScriptChannel( + int? instanceId, int? javaScriptChannelInstanceId) => + super.noSuchMethod( + Invocation.method(#removeJavaScriptChannel, + [instanceId, javaScriptChannelInstanceId]), + returnValueForMissingStub: null); + @override + void setDownloadListener(int? instanceId, int? listenerInstanceId) => + super.noSuchMethod( + Invocation.method( + #setDownloadListener, [instanceId, listenerInstanceId]), + returnValueForMissingStub: null); + @override + void setWebChromeClient(int? instanceId, int? clientInstanceId) => + super.noSuchMethod( + Invocation.method( + #setWebChromeClient, [instanceId, clientInstanceId]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [TestWebSettingsHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWebSettingsHostApi extends _i1.Mock + implements _i2.TestWebSettingsHostApi { + MockTestWebSettingsHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId, int? webViewInstanceId) => super.noSuchMethod( + Invocation.method(#create, [instanceId, webViewInstanceId]), + returnValueForMissingStub: null); + @override + void dispose(int? instanceId) => + super.noSuchMethod(Invocation.method(#dispose, [instanceId]), + returnValueForMissingStub: null); + @override + void setDomStorageEnabled(int? instanceId, bool? flag) => super.noSuchMethod( + Invocation.method(#setDomStorageEnabled, [instanceId, flag]), + returnValueForMissingStub: null); + @override + void setJavaScriptCanOpenWindowsAutomatically(int? instanceId, bool? flag) => + super.noSuchMethod( + Invocation.method( + #setJavaScriptCanOpenWindowsAutomatically, [instanceId, flag]), + returnValueForMissingStub: null); + @override + void setSupportMultipleWindows(int? instanceId, bool? support) => + super.noSuchMethod( + Invocation.method(#setSupportMultipleWindows, [instanceId, support]), + returnValueForMissingStub: null); + @override + void setJavaScriptEnabled(int? instanceId, bool? flag) => super.noSuchMethod( + Invocation.method(#setJavaScriptEnabled, [instanceId, flag]), + returnValueForMissingStub: null); + @override + void setUserAgentString(int? instanceId, String? userAgentString) => + super.noSuchMethod( + Invocation.method(#setUserAgentString, [instanceId, userAgentString]), + returnValueForMissingStub: null); + @override + void setMediaPlaybackRequiresUserGesture(int? instanceId, bool? require) => + super.noSuchMethod( + Invocation.method( + #setMediaPlaybackRequiresUserGesture, [instanceId, require]), + returnValueForMissingStub: null); + @override + void setSupportZoom(int? instanceId, bool? support) => super.noSuchMethod( + Invocation.method(#setSupportZoom, [instanceId, support]), + returnValueForMissingStub: null); + @override + void setLoadWithOverviewMode(int? instanceId, bool? overview) => + super.noSuchMethod( + Invocation.method(#setLoadWithOverviewMode, [instanceId, overview]), + returnValueForMissingStub: null); + @override + void setUseWideViewPort(int? instanceId, bool? use) => super.noSuchMethod( + Invocation.method(#setUseWideViewPort, [instanceId, use]), + returnValueForMissingStub: null); + @override + void setDisplayZoomControls(int? instanceId, bool? enabled) => + super.noSuchMethod( + Invocation.method(#setDisplayZoomControls, [instanceId, enabled]), + returnValueForMissingStub: null); + @override + void setBuiltInZoomControls(int? instanceId, bool? enabled) => + super.noSuchMethod( + Invocation.method(#setBuiltInZoomControls, [instanceId, enabled]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [TestWebViewClientHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWebViewClientHostApi extends _i1.Mock + implements _i2.TestWebViewClientHostApi { + MockTestWebViewClientHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId, bool? shouldOverrideUrlLoading) => + super.noSuchMethod( + Invocation.method(#create, [instanceId, shouldOverrideUrlLoading]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [TestWebChromeClientHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestWebChromeClientHostApi extends _i1.Mock + implements _i2.TestWebChromeClientHostApi { + MockTestWebChromeClientHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId, int? webViewClientInstanceId) => + super.noSuchMethod( + Invocation.method(#create, [instanceId, webViewClientInstanceId]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [TestJavaScriptChannelHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestJavaScriptChannelHostApi extends _i1.Mock + implements _i2.TestJavaScriptChannelHostApi { + MockTestJavaScriptChannelHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId, String? channelName) => + super.noSuchMethod(Invocation.method(#create, [instanceId, channelName]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [TestDownloadListenerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestDownloadListenerHostApi extends _i1.Mock + implements _i2.TestDownloadListenerHostApi { + MockTestDownloadListenerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? instanceId) => + super.noSuchMethod(Invocation.method(#create, [instanceId]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [WebViewPlatformCallbacksHandler]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewPlatformCallbacksHandler extends _i1.Mock + implements _i4.WebViewPlatformCallbacksHandler { + MockWebViewPlatformCallbacksHandler() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.FutureOr onNavigationRequest({String? url, bool? isForMainFrame}) => + (super.noSuchMethod( + Invocation.method(#onNavigationRequest, [], + {#url: url, #isForMainFrame: isForMainFrame}), + returnValue: Future.value(false)) as _i3.FutureOr); + @override + void onPageStarted(String? url) => + super.noSuchMethod(Invocation.method(#onPageStarted, [url]), + returnValueForMissingStub: null); + @override + void onPageFinished(String? url) => + super.noSuchMethod(Invocation.method(#onPageFinished, [url]), + returnValueForMissingStub: null); + @override + void onProgress(int? progress) => + super.noSuchMethod(Invocation.method(#onProgress, [progress]), + returnValueForMissingStub: null); + @override + void onWebResourceError(_i5.WebResourceError? error) => + super.noSuchMethod(Invocation.method(#onWebResourceError, [error]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [JavascriptChannelRegistry]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockJavascriptChannelRegistry extends _i1.Mock + implements _i6.JavascriptChannelRegistry { + MockJavascriptChannelRegistry() { + _i1.throwOnMissingStub(this); + } + + @override + Map get channels => + (super.noSuchMethod(Invocation.getter(#channels), + returnValue: {}) + as Map); + @override + void onJavascriptChannelMessage(String? channel, String? message) => + super.noSuchMethod( + Invocation.method(#onJavascriptChannelMessage, [channel, message]), + returnValueForMissingStub: null); + @override + void updateJavascriptChannelsFromSet(Set<_i5.JavascriptChannel>? channels) => + super.noSuchMethod( + Invocation.method(#updateJavascriptChannelsFromSet, [channels]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +}