From 3800f6db08c4281b449ac3f2d3cb3d36190f98e0 Mon Sep 17 00:00:00 2001 From: paulppn <48349434+paulppn@users.noreply.github.com> Date: Fri, 8 Sep 2023 23:16:53 +0200 Subject: [PATCH] [webview_flutter_android] Added the functionality to fullscreen html5 video (#3879) At this moment on android the fullscreen html5 video does not work. This PR solves that issues, adding the functionality that the video is played fullscreen when you click on the fullscreen-button in the video player. Fixes https://github.com/flutter/flutter/issues/27101 - [ x I read and followed the [relevant style guides] and ran the auto-formatter. (Unlike the flutter/flutter repo, the flutter/packages repo does use `dart format`.) --- .../webview_flutter_android/CHANGELOG.md | 5 + .../webview_flutter_android/README.md | 23 +- .../CustomViewCallbackFlutterApiImpl.java | 62 ++++ .../CustomViewCallbackHostApiImpl.java | 46 +++ ...ewFactory.java => FlutterViewFactory.java} | 28 +- .../GeneratedAndroidWebView.java | 158 +++++++++++ .../webviewflutter/ViewFlutterApiImpl.java | 59 ++++ .../WebChromeClientFlutterApiImpl.java | 32 +++ .../WebChromeClientHostApiImpl.java | 12 + .../webviewflutter/WebViewFlutterPlugin.java | 5 +- .../CustomViewCallbackTest.java | 71 +++++ .../plugins/webviewflutter/ViewTest.java | 57 ++++ .../webviewflutter/WebChromeClientTest.java | 21 ++ .../webview_flutter_test.dart | 90 +++++- .../example/lib/main.dart | 34 ++- .../lib/src/android_proxy.dart | 39 +-- .../lib/src/android_webview.dart | 94 ++++++- .../lib/src/android_webview.g.dart | 171 +++++++++++ .../lib/src/android_webview_api_impls.dart | 131 +++++++++ .../lib/src/android_webview_controller.dart | 232 +++++++++++++-- .../pigeons/android_webview.dart | 49 ++++ .../webview_flutter_android/pubspec.yaml | 2 +- .../android_navigation_delegate_test.dart | 2 + ...ndroid_navigation_delegate_test.mocks.dart | 4 +- .../test/android_webview_controller_test.dart | 265 ++++++++++++++++-- ...android_webview_controller_test.mocks.dart | 58 +++- ...oid_webview_cookie_manager_test.mocks.dart | 21 +- .../test/android_webview_test.dart | 150 ++++++++++ .../test/android_webview_test.mocks.dart | 23 +- .../test/instance_manager_test.mocks.dart | 4 +- ...iew_android_cookie_manager_test.mocks.dart | 4 +- .../webview_android_widget_test.mocks.dart | 4 +- .../test/test_android_webview.g.dart | 43 +++ 33 files changed, 1911 insertions(+), 88 deletions(-) create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CustomViewCallbackFlutterApiImpl.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CustomViewCallbackHostApiImpl.java rename packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/{FlutterWebViewFactory.java => FlutterViewFactory.java} (56%) create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/ViewFlutterApiImpl.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CustomViewCallbackTest.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/ViewTest.java diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index 86685a2666bc..73d025347971 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.10.0 + +* Adds support for playing video in fullscreen. See + `AndroidWebViewController.setCustomWidgetCallbacks`. + ## 3.9.5 * Updates pigeon to 11 and removes unneeded enum wrappers. diff --git a/packages/webview_flutter/webview_flutter_android/README.md b/packages/webview_flutter/webview_flutter_android/README.md index 06ec1a7bad49..1db03b3afdda 100644 --- a/packages/webview_flutter/webview_flutter_android/README.md +++ b/packages/webview_flutter/webview_flutter_android/README.md @@ -36,7 +36,7 @@ This can be configured for versions >=23 with `AndroidWebViewWidgetCreationParams.displayWithHybridComposition`. See https://pub.dev/packages/webview_flutter#platform-specific-features for more details on setting platform-specific features in the main plugin. -### External Native API +## External Native API The plugin also provides a native API accessible by the native code of Android applications or packages. This API follows the convention of breaking changes of the Dart API, which means that any @@ -52,6 +52,27 @@ Java: import io.flutter.plugins.webviewflutter.WebViewFlutterAndroidExternalApi; ``` +## Fullscreen Video + +To display a video as fullscreen, an app must manually handle the notification that the current page +has entered fullscreen mode. This can be done by calling +`AndroidWebViewController.setCustomWidgetCallbacks`. Below is an example implementation. + + +```dart +androidController.setCustomWidgetCallbacks( + onShowCustomWidget: (Widget widget, OnHideCustomWidgetCallback callback) { + Navigator.of(context).push(MaterialPageRoute( + builder: (BuildContext context) => widget, + fullscreenDialog: true, + )); + }, + onHideCustomWidget: () { + Navigator.of(context).pop(); + }, +); +``` + ## Contributing This package uses [pigeon][3] to generate the communication layer between Flutter and the host diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CustomViewCallbackFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CustomViewCallbackFlutterApiImpl.java new file mode 100644 index 000000000000..ffe70f694164 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CustomViewCallbackFlutterApiImpl.java @@ -0,0 +1,62 @@ +// 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.WebChromeClient.CustomViewCallback; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.CustomViewCallbackFlutterApi; + +/** + * Flutter API implementation for `CustomViewCallback`. + * + *

This class may handle adding native instances that are attached to a Dart instance or passing + * arguments of callbacks methods to a Dart instance. + */ +public class CustomViewCallbackFlutterApiImpl { + // To ease adding additional methods, this value is added prematurely. + @SuppressWarnings({"unused", "FieldCanBeLocal"}) + private final BinaryMessenger binaryMessenger; + + private final InstanceManager instanceManager; + private CustomViewCallbackFlutterApi api; + + /** + * Constructs a {@link CustomViewCallbackFlutterApiImpl}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public CustomViewCallbackFlutterApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + api = new CustomViewCallbackFlutterApi(binaryMessenger); + } + + /** + * Stores the `CustomViewCallback` instance and notifies Dart to create and store a new + * `CustomViewCallback` instance that is attached to this one. If `instance` has already been + * added, this method does nothing. + */ + public void create( + @NonNull CustomViewCallback instance, + @NonNull CustomViewCallbackFlutterApi.Reply callback) { + if (!instanceManager.containsInstance(instance)) { + api.create(instanceManager.addHostCreatedInstance(instance), callback); + } + } + + /** + * Sets the Flutter API used to send messages to Dart. + * + *

This is only visible for testing. + */ + @VisibleForTesting + void setApi(@NonNull CustomViewCallbackFlutterApi api) { + this.api = api; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CustomViewCallbackHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CustomViewCallbackHostApiImpl.java new file mode 100644 index 000000000000..44b8c454cc65 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CustomViewCallbackHostApiImpl.java @@ -0,0 +1,46 @@ +// 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.WebChromeClient.CustomViewCallback; +import androidx.annotation.NonNull; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.CustomViewCallbackHostApi; +import java.util.Objects; + +/** + * Host API implementation for `CustomViewCallback`. + * + *

This class may handle instantiating and adding native object instances that are attached to a + * Dart instance or handle method calls on the associated native class or an instance of the class. + */ +public class CustomViewCallbackHostApiImpl implements CustomViewCallbackHostApi { + // To ease adding additional methods, this value is added prematurely. + @SuppressWarnings({"unused", "FieldCanBeLocal"}) + private final BinaryMessenger binaryMessenger; + + private final InstanceManager instanceManager; + + /** + * Constructs a {@link CustomViewCallbackHostApiImpl}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public CustomViewCallbackHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + } + + @Override + public void onCustomViewHidden(@NonNull Long identifier) { + getCustomViewCallbackInstance(identifier).onCustomViewHidden(); + } + + private CustomViewCallback getCustomViewCallbackInstance(@NonNull Long identifier) { + return Objects.requireNonNull(instanceManager.getInstance(identifier)); + } +} 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/FlutterViewFactory.java similarity index 56% rename from packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java rename to packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterViewFactory.java index ec99e996ae06..f77bf8d95bdd 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/FlutterViewFactory.java @@ -5,16 +5,17 @@ package io.flutter.plugins.webviewflutter; import android.content.Context; +import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.flutter.plugin.common.StandardMessageCodec; import io.flutter.plugin.platform.PlatformView; import io.flutter.plugin.platform.PlatformViewFactory; -class FlutterWebViewFactory extends PlatformViewFactory { +class FlutterViewFactory extends PlatformViewFactory { private final InstanceManager instanceManager; - FlutterWebViewFactory(InstanceManager instanceManager) { + FlutterViewFactory(InstanceManager instanceManager) { super(StandardMessageCodec.INSTANCE); this.instanceManager = instanceManager; } @@ -24,13 +25,26 @@ class FlutterWebViewFactory extends PlatformViewFactory { public PlatformView create(Context context, int viewId, @Nullable Object args) { final Integer identifier = (Integer) args; if (identifier == null) { - throw new IllegalStateException("An identifier is required to retrieve WebView instance."); + throw new IllegalStateException("An identifier is required to retrieve a View instance."); } - final PlatformView view = instanceManager.getInstance(identifier); - if (view == null) { - throw new IllegalStateException("Unable to find WebView instance: " + args); + final Object instance = instanceManager.getInstance(identifier); + + if (instance instanceof PlatformView) { + return (PlatformView) instance; + } else if (instance instanceof View) { + return new PlatformView() { + @Override + public View getView() { + return (View) instance; + } + + @Override + public void dispose() {} + }; } - return view; + + throw new IllegalStateException( + "Unable to find a PlatformView or View instance: " + args + ", " + instance); } } 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 71c01f171725..567e201b859c 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 @@ -2602,6 +2602,33 @@ public void onPermissionRequest( new ArrayList(Arrays.asList(instanceIdArg, requestInstanceIdArg)), channelReply -> callback.reply(null)); } + /** Callback to Dart function `WebChromeClient.onShowCustomView`. */ + public void onShowCustomView( + @NonNull Long instanceIdArg, + @NonNull Long viewIdentifierArg, + @NonNull Long callbackIdentifierArg, + @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onShowCustomView", + getCodec()); + channel.send( + new ArrayList( + Arrays.asList(instanceIdArg, viewIdentifierArg, callbackIdentifierArg)), + channelReply -> callback.reply(null)); + } + /** Callback to Dart function `WebChromeClient.onHideCustomView`. */ + public void onHideCustomView(@NonNull Long instanceIdArg, @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onHideCustomView", + getCodec()); + channel.send( + new ArrayList(Collections.singletonList(instanceIdArg)), + channelReply -> callback.reply(null)); + } /** Callback to Dart function `WebChromeClient.onGeolocationPermissionsShowPrompt`. */ public void onGeolocationPermissionsShowPrompt( @NonNull Long instanceIdArg, @@ -2867,6 +2894,137 @@ public void create( channelReply -> callback.reply(null)); } } + /** + * Host API for `CustomViewCallback`. + * + *

This class may handle instantiating and adding native object instances that are attached to + * a Dart instance or handle method calls on the associated native class or an instance of the + * class. + * + *

See + * https://developer.android.com/reference/android/webkit/WebChromeClient.CustomViewCallback. + * + *

Generated interface from Pigeon that represents a handler of messages from Flutter. + */ + public interface CustomViewCallbackHostApi { + /** Handles Dart method `CustomViewCallback.onCustomViewHidden`. */ + void onCustomViewHidden(@NonNull Long identifier); + + /** The codec used by CustomViewCallbackHostApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** + * Sets up an instance of `CustomViewCallbackHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup( + @NonNull BinaryMessenger binaryMessenger, @Nullable CustomViewCallbackHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.webview_flutter_android.CustomViewCallbackHostApi.onCustomViewHidden", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + api.onCustomViewHidden( + (identifierArg == null) ? null : identifierArg.longValue()); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + /** + * Flutter API for `CustomViewCallback`. + * + *

This class may handle instantiating and adding Dart instances that are attached to a native + * instance or receiving callback methods from an overridden native class. + * + *

See + * https://developer.android.com/reference/android/webkit/WebChromeClient.CustomViewCallback. + * + *

Generated class from Pigeon that represents Flutter messages that can be called from Java. + */ + public static class CustomViewCallbackFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public CustomViewCallbackFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by CustomViewCallbackFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** Create a new Dart instance and add it to the `InstanceManager`. */ + public void create(@NonNull Long identifierArg, @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.webview_flutter_android.CustomViewCallbackFlutterApi.create", + getCodec()); + channel.send( + new ArrayList(Collections.singletonList(identifierArg)), + channelReply -> callback.reply(null)); + } + } + /** + * Flutter API for `View`. + * + *

This class may handle instantiating and adding Dart instances that are attached to a native + * instance or receiving callback methods from an overridden native class. + * + *

See https://developer.android.com/reference/android/view/View. + * + *

Generated class from Pigeon that represents Flutter messages that can be called from Java. + */ + public static class ViewFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public ViewFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by ViewFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** Create a new Dart instance and add it to the `InstanceManager`. */ + public void create(@NonNull Long identifierArg, @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.webview_flutter_android.ViewFlutterApi.create", + getCodec()); + channel.send( + new ArrayList(Collections.singletonList(identifierArg)), + channelReply -> callback.reply(null)); + } + } /** * Host API for `GeolocationPermissionsCallback`. * diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/ViewFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/ViewFlutterApiImpl.java new file mode 100644 index 000000000000..653bfa9faf3d --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/ViewFlutterApiImpl.java @@ -0,0 +1,59 @@ +// 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.view.View; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.ViewFlutterApi; + +/** + * Flutter API implementation for `View`. + * + *

This class may handle adding native instances that are attached to a Dart instance or passing + * arguments of callbacks methods to a Dart instance. + */ +public class ViewFlutterApiImpl { + // To ease adding additional methods, this value is added prematurely. + @SuppressWarnings({"unused", "FieldCanBeLocal"}) + private final BinaryMessenger binaryMessenger; + + private final InstanceManager instanceManager; + private ViewFlutterApi api; + + /** + * Constructs a {@link ViewFlutterApiImpl}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public ViewFlutterApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + api = new ViewFlutterApi(binaryMessenger); + } + + /** + * Stores the `View` instance and notifies Dart to create and store a new `View` instance that is + * attached to this one. If `instance` has already been added, this method does nothing. + */ + public void create(@NonNull View instance, @NonNull ViewFlutterApi.Reply callback) { + if (!instanceManager.containsInstance(instance)) { + api.create(instanceManager.addHostCreatedInstance(instance), callback); + } + } + + /** + * Sets the Flutter API used to send messages to Dart. + * + *

This is only visible for testing. + */ + @VisibleForTesting + void setApi(@NonNull ViewFlutterApi api) { + this.api = api; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java index ad5168fa110a..f5097b6393cf 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java @@ -5,6 +5,7 @@ package io.flutter.plugins.webviewflutter; import android.os.Build; +import android.view.View; import android.webkit.GeolocationPermissions; import android.webkit.PermissionRequest; import android.webkit.WebChromeClient; @@ -117,6 +118,37 @@ public void onPermissionRequest( callback); } + /** + * Sends a message to Dart to call `WebChromeClient.onShowCustomView` on the Dart object + * representing `instance`. + */ + public void onShowCustomView( + @NonNull WebChromeClient instance, + @NonNull View view, + @NonNull WebChromeClient.CustomViewCallback customViewCallback, + @NonNull WebChromeClientFlutterApi.Reply callback) { + new ViewFlutterApiImpl(binaryMessenger, instanceManager).create(view, reply -> {}); + new CustomViewCallbackFlutterApiImpl(binaryMessenger, instanceManager) + .create(customViewCallback, reply -> {}); + + onShowCustomView( + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(instance)), + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(view)), + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(customViewCallback)), + callback); + } + + /** + * Sends a message to Dart to call `WebChromeClient.onHideCustomView` on the Dart object + * representing `instance`. + */ + public void onHideCustomView( + @NonNull WebChromeClient instance, @NonNull WebChromeClientFlutterApi.Reply callback) { + super.onHideCustomView( + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(instance)), + callback); + } + private long getIdentifierForClient(WebChromeClient webChromeClient) { final Long identifier = instanceManager.getIdentifierForStrongReference(webChromeClient); if (identifier == null) { 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 74ea45e5359a..635c6c30ee93 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 @@ -4,9 +4,11 @@ package io.flutter.plugins.webviewflutter; +import android.content.Context; import android.net.Uri; import android.os.Build; import android.os.Message; +import android.view.View; import android.webkit.GeolocationPermissions; import android.webkit.PermissionRequest; import android.webkit.ValueCallback; @@ -30,6 +32,7 @@ public class WebChromeClientHostApiImpl implements WebChromeClientHostApi { private final InstanceManager instanceManager; private final WebChromeClientCreator webChromeClientCreator; private final WebChromeClientFlutterApiImpl flutterApi; + private Context context; /** * Implementation of {@link WebChromeClient} that passes arguments of callback methods to Dart. @@ -53,6 +56,15 @@ public void onProgressChanged(@NonNull WebView view, int progress) { } @Override + public void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) { + flutterApi.onShowCustomView(this, view, callback, reply -> {}); + } + + @Override + public void onHideCustomView() { + flutterApi.onHideCustomView(this, reply -> {}); + } + public void onGeolocationPermissionsShowPrompt( @NonNull String origin, @NonNull GeolocationPermissions.Callback callback) { flutterApi.onGeolocationPermissionsShowPrompt(this, origin, callback, reply -> {}); 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 305821943e3c..e763c919e021 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 @@ -15,6 +15,7 @@ import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.platform.PlatformViewRegistry; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.CookieManagerHostApi; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.CustomViewCallbackHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.FlutterAssetManagerHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.GeolocationPermissionsCallbackHostApi; @@ -89,7 +90,7 @@ private void setUp( InstanceManagerHostApi.setup(binaryMessenger, () -> instanceManager.clear()); viewRegistry.registerViewFactory( - "plugins.flutter.io/webview", new FlutterWebViewFactory(instanceManager)); + "plugins.flutter.io/webview", new FlutterViewFactory(instanceManager)); webViewHostApi = new WebViewHostApiImpl( @@ -141,6 +142,8 @@ private void setUp( GeolocationPermissionsCallbackHostApi.setup( binaryMessenger, new GeolocationPermissionsCallbackHostApiImpl(binaryMessenger, instanceManager)); + CustomViewCallbackHostApi.setup( + binaryMessenger, new CustomViewCallbackHostApiImpl(binaryMessenger, instanceManager)); } @Override diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CustomViewCallbackTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CustomViewCallbackTest.java new file mode 100644 index 000000000000..3a633999532d --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CustomViewCallbackTest.java @@ -0,0 +1,71 @@ +// 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.eq; +import static org.mockito.Mockito.verify; + +import android.webkit.WebChromeClient.CustomViewCallback; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.CustomViewCallbackFlutterApi; +import java.util.Objects; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class CustomViewCallbackTest { + + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public CustomViewCallback mockCustomViewCallback; + + @Mock public BinaryMessenger mockBinaryMessenger; + + @Mock public CustomViewCallbackFlutterApi mockFlutterApi; + + InstanceManager instanceManager; + + @Before + public void setUp() { + instanceManager = InstanceManager.create(identifier -> {}); + } + + @After + public void tearDown() { + instanceManager.stopFinalizationListener(); + } + + @Test + public void onCustomViewHidden() { + final long instanceIdentifier = 0; + instanceManager.addDartCreatedInstance(mockCustomViewCallback, instanceIdentifier); + + final CustomViewCallbackHostApiImpl hostApi = + new CustomViewCallbackHostApiImpl(mockBinaryMessenger, instanceManager); + + hostApi.onCustomViewHidden(instanceIdentifier); + + verify(mockCustomViewCallback).onCustomViewHidden(); + } + + @Test + public void flutterApiCreate() { + final CustomViewCallbackFlutterApiImpl flutterApi = + new CustomViewCallbackFlutterApiImpl(mockBinaryMessenger, instanceManager); + flutterApi.setApi(mockFlutterApi); + + flutterApi.create(mockCustomViewCallback, reply -> {}); + + final long instanceIdentifier = + Objects.requireNonNull( + instanceManager.getIdentifierForStrongReference(mockCustomViewCallback)); + verify(mockFlutterApi).create(eq(instanceIdentifier), any()); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/ViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/ViewTest.java new file mode 100644 index 000000000000..3019900d6618 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/ViewTest.java @@ -0,0 +1,57 @@ +// 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.eq; +import static org.mockito.Mockito.verify; + +import android.view.View; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.ViewFlutterApi; +import java.util.Objects; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class ViewTest { + + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public View mockView; + + @Mock public BinaryMessenger mockBinaryMessenger; + + @Mock public ViewFlutterApi mockFlutterApi; + + InstanceManager instanceManager; + + @Before + public void setUp() { + instanceManager = InstanceManager.create(identifier -> {}); + } + + @After + public void tearDown() { + instanceManager.stopFinalizationListener(); + } + + @Test + public void flutterApiCreate() { + final ViewFlutterApiImpl flutterApi = + new ViewFlutterApiImpl(mockBinaryMessenger, instanceManager); + flutterApi.setApi(mockFlutterApi); + + flutterApi.create(mockView, reply -> {}); + + final long instanceIdentifier = + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(mockView)); + verify(mockFlutterApi).create(eq(instanceIdentifier), any()); + } +} 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 e2d5a444b716..4e09b2e4b452 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 @@ -15,8 +15,10 @@ import android.net.Uri; import android.os.Message; +import android.view.View; import android.webkit.GeolocationPermissions; import android.webkit.PermissionRequest; +import android.webkit.WebChromeClient; import android.webkit.WebResourceRequest; import android.webkit.WebView; import android.webkit.WebView.WebViewTransport; @@ -126,6 +128,25 @@ public void onPermissionRequest() { } @Test + public void onShowCustomView() { + final View mockView = mock(View.class); + instanceManager.addDartCreatedInstance(mockView, 10); + + final WebChromeClient.CustomViewCallback mockCustomViewCallback = + mock(WebChromeClient.CustomViewCallback.class); + instanceManager.addDartCreatedInstance(mockView, 12); + + webChromeClient.onShowCustomView(mockView, mockCustomViewCallback); + verify(mockFlutterApi) + .onShowCustomView(eq(webChromeClient), eq(mockView), eq(mockCustomViewCallback), any()); + } + + @Test + public void onHideCustomView() { + webChromeClient.onHideCustomView(); + verify(mockFlutterApi).onHideCustomView(eq(webChromeClient), any()); + } + public void onGeolocationPermissionsShowPrompt() { final GeolocationPermissions.Callback mockCallback = mock(GeolocationPermissions.Callback.class); 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 53b8a0078fd0..0e805dd2aef4 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 @@ -356,6 +356,19 @@ Future main() async { final String videoTest = ''' Video auto play + - +

+
+ +
'''; @@ -509,6 +534,65 @@ Future main() async { .runJavaScriptReturningResult('isFullScreen();') as bool; expect(fullScreen, false); }); + + testWidgets('Video plays fullscreen', (WidgetTester tester) async { + final Completer fullscreenEntered = Completer(); + final Completer fullscreenExited = Completer(); + final Completer pageLoaded = Completer(); + + final AndroidWebViewController controller = AndroidWebViewController( + const PlatformWebViewControllerCreationParams(), + ); + unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted)); + unawaited(controller.setMediaPlaybackRequiresUserGesture(false)); + final AndroidNavigationDelegate delegate = AndroidNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + ); + unawaited(delegate.setOnPageFinished((_) => pageLoaded.complete())); + unawaited(controller.setPlatformNavigationDelegate(delegate)); + unawaited(controller.setCustomWidgetCallbacks(onHideCustomWidget: () { + fullscreenExited.complete(); + }, onShowCustomWidget: + (Widget webView, void Function() onHideCustomView) { + fullscreenEntered.complete(); + onHideCustomView(); + })); + + await controller.loadRequest( + LoadRequestParams( + uri: Uri.parse( + 'data:text/html;charset=utf-8;base64,$videoTestBase64', + ), + ), + ); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams( + key: const Key('webview_widget'), + controller: controller, + ), + ).build(context); + }, + )); + + await pageLoaded.future; + + await tester.pumpAndSettle(); + + // Due to security reasons, Chrome doesn't allow to programmatically + // toggle a video to fullscreen unless the call is directly coming from + // a user triggered event. + // The top half of the loaded web content contains a clickable div, which + // is tapped using the code below, triggering a user event. + // + // The offset of 20 x 20 is chosen at random. + await tester.tapAt(const Offset(20, 20)); + + await expectLater(fullscreenEntered.future, completes); + await expectLater(fullscreenExited.future, completes); + }); }); group('Audio playback policy', () { diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart index 0c3e3411bd09..781a98f87f7b 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -118,7 +118,7 @@ Page resource error: '''); }) ..setOnNavigationRequest((NavigationRequest request) { - if (request.url.startsWith('https://www.youtube.com/')) { + if (request.url.contains('pub.dev')) { debugPrint('blocking navigation to ${request.url}'); return NavigationDecision.prevent; } @@ -201,6 +201,7 @@ enum MenuOptions { loadHtmlString, transparentBackground, setCookie, + videoExample, } class SampleMenu extends StatelessWidget { @@ -261,6 +262,9 @@ class SampleMenu extends StatelessWidget { case MenuOptions.setCookie: _onSetCookie(); break; + case MenuOptions.videoExample: + _onVideoExample(context); + break; } }, itemBuilder: (BuildContext context) => >[ @@ -317,6 +321,10 @@ class SampleMenu extends StatelessWidget { value: MenuOptions.transparentBackground, child: Text('Transparent background example'), ), + const PopupMenuItem( + value: MenuOptions.videoExample, + child: Text('Video example'), + ), ], ); } @@ -412,6 +420,30 @@ class SampleMenu extends StatelessWidget { )); } + Future _onVideoExample(BuildContext context) { + final AndroidWebViewController androidController = + webViewController as AndroidWebViewController; + // #docregion fullscreen_example + androidController.setCustomWidgetCallbacks( + onShowCustomWidget: (Widget widget, OnHideCustomWidgetCallback callback) { + Navigator.of(context).push(MaterialPageRoute( + builder: (BuildContext context) => widget, + fullscreenDialog: true, + )); + }, + onHideCustomWidget: () { + Navigator.of(context).pop(); + }, + ); + // #enddocregion fullscreen_example + + return androidController.loadRequest( + LoadRequestParams( + uri: Uri.parse('https://www.youtube.com/watch?v=4AoFA19gbLo'), + ), + ); + } + Future _onDoPostRequest() { return webViewController.loadRequest(LoadRequestParams( uri: Uri.parse('https://httpbin.org/post'), diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart index e95de8cfa6f1..508a31bbe5fc 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart @@ -28,22 +28,29 @@ class AndroidWebViewProxy { final android_webview.WebView Function() createAndroidWebView; /// Constructs a [android_webview.WebChromeClient]. - final android_webview.WebChromeClient Function( - {void Function(android_webview.WebView webView, int progress)? - onProgressChanged, - Future> Function( - android_webview.WebView webView, - android_webview.FileChooserParams params, - )? onShowFileChooser, - void Function( - android_webview.WebChromeClient instance, - android_webview.PermissionRequest request, - )? onPermissionRequest, - Future Function(String origin, - android_webview.GeolocationPermissionsCallback callback)? - onGeolocationPermissionsShowPrompt, - void Function(android_webview.WebChromeClient instance)? - onGeolocationPermissionsHidePrompt}) createAndroidWebChromeClient; + final android_webview.WebChromeClient Function({ + void Function(android_webview.WebView webView, int progress)? + onProgressChanged, + Future> Function( + android_webview.WebView webView, + android_webview.FileChooserParams params, + )? onShowFileChooser, + void Function( + android_webview.WebChromeClient instance, + android_webview.PermissionRequest request, + )? onPermissionRequest, + Future Function(String origin, + android_webview.GeolocationPermissionsCallback callback)? + onGeolocationPermissionsShowPrompt, + void Function(android_webview.WebChromeClient instance)? + onGeolocationPermissionsHidePrompt, + void Function( + android_webview.WebChromeClient instance, + android_webview.View view, + android_webview.CustomViewCallback callback)? + onShowCustomView, + void Function(android_webview.WebChromeClient instance)? onHideCustomView, + }) createAndroidWebChromeClient; /// Constructs a [android_webview.WebViewClient]. final android_webview.WebViewClient Function({ 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 85ec6b902c57..5ed2fc0351ef 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 @@ -129,7 +129,7 @@ class GeolocationPermissionsCallback extends JavaObject { /// [Web-based content](https://developer.android.com/guide/webapps). /// /// When a [WebView] is no longer needed [release] must be called. -class WebView extends JavaObject { +class WebView extends View { /// Constructs a new WebView. /// /// Due to changes in Flutter 3.0 the [useHybridComposition] doesn't have @@ -1024,6 +1024,18 @@ typedef GeolocationPermissionsHidePrompt = void Function( WebChromeClient instance, ); +/// Signature for the callback that is responsible for showing a custom view. +typedef ShowCustomViewCallback = void Function( + WebChromeClient instance, + View view, + CustomViewCallback callback, +); + +/// Signature for the callback that is responsible for hiding a custom view. +typedef HideCustomViewCallback = void Function( + WebChromeClient instance, +); + /// Handles JavaScript dialogs, favicons, titles, and the progress for [WebView]. class WebChromeClient extends JavaObject { /// Constructs a [WebChromeClient]. @@ -1033,6 +1045,8 @@ class WebChromeClient extends JavaObject { this.onPermissionRequest, this.onGeolocationPermissionsShowPrompt, this.onGeolocationPermissionsHidePrompt, + this.onShowCustomView, + this.onHideCustomView, @visibleForTesting super.binaryMessenger, @visibleForTesting super.instanceManager, }) : super.detached() { @@ -1052,6 +1066,8 @@ class WebChromeClient extends JavaObject { this.onPermissionRequest, this.onGeolocationPermissionsShowPrompt, this.onGeolocationPermissionsHidePrompt, + this.onShowCustomView, + this.onHideCustomView, super.binaryMessenger, super.instanceManager, }) : super.detached(); @@ -1092,9 +1108,18 @@ class WebChromeClient extends JavaObject { /// Notify the host application that a request for Geolocation permissions, /// made with a previous call to [onGeolocationPermissionsShowPrompt] has been /// canceled. - final void Function( - WebChromeClient instance, - )? onGeolocationPermissionsHidePrompt; + final GeolocationPermissionsHidePrompt? onGeolocationPermissionsHidePrompt; + + /// Notify the host application that the current page has entered full screen + /// mode. + /// + /// After this call, web content will no longer be rendered in the WebView, + /// but will instead be rendered in `view`. + final ShowCustomViewCallback? onShowCustomView; + + /// Notify the host application that the current page has exited full screen + /// mode. + final HideCustomViewCallback? onHideCustomView; /// Sets the required synchronous return value for the Java method, /// `WebChromeClient.onShowFileChooser(...)`. @@ -1130,8 +1155,11 @@ class WebChromeClient extends JavaObject { return WebChromeClient.detached( onProgressChanged: onProgressChanged, onShowFileChooser: onShowFileChooser, + onPermissionRequest: onPermissionRequest, onGeolocationPermissionsShowPrompt: onGeolocationPermissionsShowPrompt, onGeolocationPermissionsHidePrompt: onGeolocationPermissionsHidePrompt, + onShowCustomView: onShowCustomView, + onHideCustomView: onHideCustomView, binaryMessenger: _api.binaryMessenger, instanceManager: _api.instanceManager, ); @@ -1370,3 +1398,61 @@ class WebStorage extends JavaObject { ); } } + +/// The basic building block for user interface components. +/// +/// See https://developer.android.com/reference/android/view/View. +class View extends JavaObject { + /// Instantiates a [View] without creating and attaching to an + /// instance of the associated native class. + /// + /// This should only be used outside of tests by subclasses created by this + /// library or to create a copy for an [InstanceManager]. + @protected + View.detached({super.binaryMessenger, super.instanceManager}) + : super.detached(); + + @override + View copy() { + return View.detached( + binaryMessenger: _api.binaryMessenger, + instanceManager: _api.instanceManager, + ); + } +} + +/// A callback interface used by the host application to notify the current page +/// that its custom view has been dismissed. +/// +/// See https://developer.android.com/reference/android/webkit/WebChromeClient.CustomViewCallback. +class CustomViewCallback extends JavaObject { + /// Instantiates a [CustomViewCallback] without creating and attaching to an + /// instance of the associated native class. + /// + /// This should only be used outside of tests by subclasses created by this + /// library or to create a copy for an [InstanceManager]. + @protected + CustomViewCallback.detached({ + super.binaryMessenger, + super.instanceManager, + }) : _customViewCallbackApi = CustomViewCallbackHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super.detached(); + + final CustomViewCallbackHostApiImpl _customViewCallbackApi; + + /// Invoked when the host application dismisses the custom view. + Future onCustomViewHidden() { + return _customViewCallbackApi.onCustomViewHiddenFromInstances(this); + } + + @override + CustomViewCallback copy() { + return CustomViewCallback.detached( + binaryMessenger: _customViewCallbackApi.binaryMessenger, + instanceManager: _customViewCallbackApi.instanceManager, + ); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart index b2a82fdf1ab8..99e1e7f4e104 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart @@ -2023,6 +2023,13 @@ abstract class WebChromeClientFlutterApi { /// Callback to Dart function `WebChromeClient.onPermissionRequest`. void onPermissionRequest(int instanceId, int requestInstanceId); + /// Callback to Dart function `WebChromeClient.onShowCustomView`. + void onShowCustomView( + int instanceId, int viewIdentifier, int callbackIdentifier); + + /// Callback to Dart function `WebChromeClient.onHideCustomView`. + void onHideCustomView(int instanceId); + /// Callback to Dart function `WebChromeClient.onGeolocationPermissionsShowPrompt`. void onGeolocationPermissionsShowPrompt( int instanceId, int paramsInstanceId, String origin); @@ -2109,6 +2116,53 @@ abstract class WebChromeClientFlutterApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onShowCustomView', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onShowCustomView 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.webview_flutter_android.WebChromeClientFlutterApi.onShowCustomView was null, expected non-null int.'); + final int? arg_viewIdentifier = (args[1] as int?); + assert(arg_viewIdentifier != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onShowCustomView was null, expected non-null int.'); + final int? arg_callbackIdentifier = (args[2] as int?); + assert(arg_callbackIdentifier != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onShowCustomView was null, expected non-null int.'); + api.onShowCustomView( + arg_instanceId!, arg_viewIdentifier!, arg_callbackIdentifier!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onHideCustomView', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onHideCustomView 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.webview_flutter_android.WebChromeClientFlutterApi.onHideCustomView was null, expected non-null int.'); + api.onHideCustomView(arg_instanceId!); + return; + }); + } + } { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onGeolocationPermissionsShowPrompt', @@ -2371,6 +2425,123 @@ abstract class PermissionRequestFlutterApi { } } +/// Host API for `CustomViewCallback`. +/// +/// This class may handle instantiating and adding native object instances that +/// are attached to a Dart instance or handle method calls on the associated +/// native class or an instance of the class. +/// +/// See https://developer.android.com/reference/android/webkit/WebChromeClient.CustomViewCallback. +class CustomViewCallbackHostApi { + /// Constructor for [CustomViewCallbackHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + CustomViewCallbackHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + /// Handles Dart method `CustomViewCallback.onCustomViewHidden`. + Future onCustomViewHidden(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_android.CustomViewCallbackHostApi.onCustomViewHidden', + codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } +} + +/// Flutter API for `CustomViewCallback`. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +/// +/// See https://developer.android.com/reference/android/webkit/WebChromeClient.CustomViewCallback. +abstract class CustomViewCallbackFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + /// Create a new Dart instance and add it to the `InstanceManager`. + void create(int identifier); + + static void setup(CustomViewCallbackFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_android.CustomViewCallbackFlutterApi.create', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.CustomViewCallbackFlutterApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.CustomViewCallbackFlutterApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return; + }); + } + } + } +} + +/// Flutter API for `View`. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +/// +/// See https://developer.android.com/reference/android/view/View. +abstract class ViewFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + /// Create a new Dart instance and add it to the `InstanceManager`. + void create(int identifier); + + static void setup(ViewFlutterApi? api, {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_android.ViewFlutterApi.create', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.ViewFlutterApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.ViewFlutterApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return; + }); + } + } + } +} + /// Host API for `GeolocationPermissionsCallback`. /// /// This class may handle instantiating and adding native object instances that 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 a2fa1da3477e..2c773fd9e190 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 @@ -47,6 +47,8 @@ class AndroidWebViewFlutterApis { geolocationPermissionsCallbackFlutterApi, WebViewFlutterApiImpl? webViewFlutterApi, PermissionRequestFlutterApiImpl? permissionRequestFlutterApi, + CustomViewCallbackFlutterApiImpl? customViewCallbackFlutterApi, + ViewFlutterApiImpl? viewFlutterApi, }) { this.javaObjectFlutterApi = javaObjectFlutterApi ?? JavaObjectFlutterApiImpl(); @@ -66,6 +68,9 @@ class AndroidWebViewFlutterApis { this.webViewFlutterApi = webViewFlutterApi ?? WebViewFlutterApiImpl(); this.permissionRequestFlutterApi = permissionRequestFlutterApi ?? PermissionRequestFlutterApiImpl(); + this.customViewCallbackFlutterApi = + customViewCallbackFlutterApi ?? CustomViewCallbackFlutterApiImpl(); + this.viewFlutterApi = viewFlutterApi ?? ViewFlutterApiImpl(); } static bool _haveBeenSetUp = false; @@ -103,6 +108,12 @@ class AndroidWebViewFlutterApis { /// Flutter Api for [PermissionRequest]. late final PermissionRequestFlutterApiImpl permissionRequestFlutterApi; + /// Flutter Api for [CustomViewCallback]. + late final CustomViewCallbackFlutterApiImpl customViewCallbackFlutterApi; + + /// Flutter Api for [View]. + late final ViewFlutterApiImpl viewFlutterApi; + /// Ensures all the Flutter APIs have been setup to receive calls from native code. void ensureSetUp() { if (!_haveBeenSetUp) { @@ -116,6 +127,8 @@ class AndroidWebViewFlutterApis { geolocationPermissionsCallbackFlutterApi); WebViewFlutterApi.setup(webViewFlutterApi); PermissionRequestFlutterApi.setup(permissionRequestFlutterApi); + CustomViewCallbackFlutterApi.setup(customViewCallbackFlutterApi); + ViewFlutterApi.setup(viewFlutterApi); _haveBeenSetUp = true; } } @@ -976,6 +989,34 @@ class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { request.deny(); } } + + @override + void onShowCustomView( + int instanceId, + int viewIdentifier, + int callbackIdentifier, + ) { + final WebChromeClient instance = + instanceManager.getInstanceWithWeakReference(instanceId)!; + if (instance.onShowCustomView != null) { + return instance.onShowCustomView!( + instance, + instanceManager.getInstanceWithWeakReference(viewIdentifier)!, + instanceManager.getInstanceWithWeakReference(callbackIdentifier)!, + ); + } + } + + @override + void onHideCustomView(int instanceId) { + final WebChromeClient instance = + instanceManager.getInstanceWithWeakReference(instanceId)!; + if (instance.onHideCustomView != null) { + return instance.onHideCustomView!( + instance, + ); + } + } } /// Host api implementation for [WebStorage]. @@ -1180,6 +1221,96 @@ class PermissionRequestFlutterApiImpl implements PermissionRequestFlutterApi { } } +/// Host api implementation for [CustomViewCallback]. +class CustomViewCallbackHostApiImpl extends CustomViewCallbackHostApi { + /// Constructs a [CustomViewCallbackHostApiImpl]. + CustomViewCallbackHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + /// Sends binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + /// Helper method to convert instance ids to objects. + Future onCustomViewHiddenFromInstances(CustomViewCallback instance) { + return onCustomViewHidden(instanceManager.getIdentifier(instance)!); + } +} + +/// Flutter API implementation for [CustomViewCallback]. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +class CustomViewCallbackFlutterApiImpl implements CustomViewCallbackFlutterApi { + /// Constructs a [CustomViewCallbackFlutterApiImpl]. + CustomViewCallbackFlutterApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + @override + void create(int identifier) { + instanceManager.addHostCreatedInstance( + CustomViewCallback.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + identifier, + ); + } +} + +/// Flutter API implementation for [View]. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +class ViewFlutterApiImpl implements ViewFlutterApi { + /// Constructs a [ViewFlutterApiImpl]. + ViewFlutterApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + @override + void create(int identifier) { + instanceManager.addHostCreatedInstance( + View.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + identifier, + ); + } +} + /// Host api implementation for [CookieManager]. class CookieManagerHostApiImpl extends CookieManagerHostApi { /// Constructs a [CookieManagerHostApiImpl]. diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart index fe4aebf8d0ce..bc1da74bf952 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart @@ -4,6 +4,8 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; @@ -137,6 +139,41 @@ class AndroidWebViewController extends PlatformWebViewController { } }; }), + onShowCustomView: withWeakReferenceTo(this, + (WeakReference weakReference) { + return (_, android_webview.View view, + android_webview.CustomViewCallback callback) { + final AndroidWebViewController? webViewController = + weakReference.target; + if (webViewController == null) { + callback.onCustomViewHidden(); + return; + } + final OnShowCustomWidgetCallback? onShowCallback = + webViewController._onShowCustomWidgetCallback; + if (onShowCallback == null) { + callback.onCustomViewHidden(); + return; + } + onShowCallback( + AndroidCustomViewWidget.private( + controller: webViewController, + customView: view, + ), + () => callback.onCustomViewHidden(), + ); + }; + }), + onHideCustomView: withWeakReferenceTo(this, + (WeakReference weakReference) { + return (android_webview.WebChromeClient instance) { + final OnHideCustomWidgetCallback? onHideCustomViewCallback = + weakReference.target?._onHideCustomWidgetCallback; + if (onHideCustomViewCallback != null) { + onHideCustomViewCallback(); + } + }; + }), onShowFileChooser: withWeakReferenceTo( this, (WeakReference weakReference) { @@ -212,6 +249,10 @@ class AndroidWebViewController extends PlatformWebViewController { OnGeolocationPermissionsHidePrompt? _onGeolocationPermissionsHidePrompt; + OnShowCustomWidgetCallback? _onShowCustomWidgetCallback; + + OnHideCustomWidgetCallback? _onHideCustomWidgetCallback; + void Function(PlatformWebViewPermissionRequest)? _onPermissionRequestCallback; /// Whether to enable the platform's webview content debugging tools. @@ -495,6 +536,36 @@ class AndroidWebViewController extends PlatformWebViewController { _onGeolocationPermissionsShowPrompt = onShowPrompt; _onGeolocationPermissionsHidePrompt = onHidePrompt; } + + /// Sets the callbacks that are invoked when the host application wants to + /// show or hide a custom widget. + /// + /// The most common use case these methods are invoked a video element wants + /// to be displayed in fullscreen. + /// + /// The [onShowCustomWidget] notifies the host application that web content + /// from the specified origin wants to be displayed in a custom widget. After + /// this call, web content will no longer be rendered in the `WebViewWidget`, + /// but will instead be rendered in the custom widget. The application may + /// explicitly exit fullscreen mode by invoking `onCustomWidgetHidden` in the + /// [onShowCustomWidget] callback (ex. when the user presses the back + /// button). However, this is generally not necessary as the web page will + /// often show its own UI to close out of fullscreen. Regardless of how the + /// WebView exits fullscreen mode, WebView will invoke [onHideCustomWidget], + /// signaling for the application to remove the custom widget. If this value + /// is `null` when passed to an `AndroidWebViewWidget`, a default handler + /// will be set. + /// + /// The [onHideCustomWidget] notifies the host application that the custom + /// widget must be hidden. After this call, web content will render in the + /// original `WebViewWidget` again. + Future setCustomWidgetCallbacks({ + required OnShowCustomWidgetCallback? onShowCustomWidget, + required OnHideCustomWidgetCallback? onHideCustomWidget, + }) async { + _onShowCustomWidgetCallback = onShowCustomWidget; + _onHideCustomWidgetCallback = onHideCustomWidget; + } } /// Android implementation of [PlatformWebViewPermissionRequest]. @@ -541,6 +612,13 @@ typedef OnGeolocationPermissionsShowPrompt /// Signature for the `setGeolocationPermissionsPromptCallbacks` callback responsible for request the Geolocation API is cancel. typedef OnGeolocationPermissionsHidePrompt = void Function(); +/// Signature for the `setCustomWidgetCallbacks` callback responsible for showing the custom view. +typedef OnShowCustomWidgetCallback = void Function( + Widget widget, void Function() onCustomWidgetHidden); + +/// Signature for the `setCustomWidgetCallbacks` callback responsible for hiding the custom view. +typedef OnHideCustomWidgetCallback = void Function(); + /// A request params used by the host application to set the Geolocation permission state for an origin. @immutable class GeolocationPermissionsRequestParams { @@ -781,6 +859,7 @@ class AndroidWebViewWidget extends PlatformWebViewWidget { @override Widget build(BuildContext context) { + _trySetDefaultOnShowCustomWidgetCallbacks(context); return PlatformViewLink( // Setting a default key using `params` ensures the `PlatformViewLink` // recreates the PlatformView when changes are made. @@ -803,6 +882,11 @@ class AndroidWebViewWidget extends PlatformWebViewWidget { params, displayWithHybridComposition: _androidParams.displayWithHybridComposition, + platformViewsServiceProxy: _androidParams.platformViewsServiceProxy, + view: + (_androidParams.controller as AndroidWebViewController)._webView, + instanceManager: _androidParams.instanceManager, + layoutDirection: _androidParams.layoutDirection, ) ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) ..create(); @@ -810,32 +894,138 @@ class AndroidWebViewWidget extends PlatformWebViewWidget { ); } - AndroidViewController _initAndroidView( - PlatformViewCreationParams params, { - required bool displayWithHybridComposition, - }) { - if (displayWithHybridComposition) { - return _androidParams.platformViewsServiceProxy.initExpensiveAndroidView( - id: params.id, - viewType: 'plugins.flutter.io/webview', - layoutDirection: _androidParams.layoutDirection, - creationParams: _androidParams.instanceManager.getIdentifier( - (_androidParams.controller as AndroidWebViewController)._webView), - creationParamsCodec: const StandardMessageCodec(), - ); - } else { - return _androidParams.platformViewsServiceProxy.initSurfaceAndroidView( - id: params.id, - viewType: 'plugins.flutter.io/webview', - layoutDirection: _androidParams.layoutDirection, - creationParams: _androidParams.instanceManager.getIdentifier( - (_androidParams.controller as AndroidWebViewController)._webView), - creationParamsCodec: const StandardMessageCodec(), + // Attempt to handle custom views with a default implementation if it has not + // been set. + void _trySetDefaultOnShowCustomWidgetCallbacks(BuildContext context) { + final AndroidWebViewController controller = + _androidParams.controller as AndroidWebViewController; + + if (controller._onShowCustomWidgetCallback == null) { + controller.setCustomWidgetCallbacks( + onShowCustomWidget: + (Widget widget, OnHideCustomWidgetCallback callback) { + Navigator.of(context).push(MaterialPageRoute( + builder: (BuildContext context) => widget, + fullscreenDialog: true, + )); + }, + onHideCustomWidget: () { + Navigator.of(context).pop(); + }, ); } } } +/// Represents a Flutter implementation of the Android [View](https://developer.android.com/reference/android/view/View) +/// that is created by the host platform when web content needs to be displayed +/// in fullscreen mode. +/// +/// The [AndroidCustomViewWidget] cannot be manually instantiated and is +/// provided to the host application through the callbacks specified using the +/// [AndroidWebViewController.setCustomWidgetCallbacks] method. +/// +/// The [AndroidCustomViewWidget] is initialized internally and should only be +/// exposed as a [Widget] externally. The type [AndroidCustomViewWidget] is +/// visible for testing purposes only and should never be called externally. +@visibleForTesting +class AndroidCustomViewWidget extends StatelessWidget { + /// Creates a [AndroidCustomViewWidget]. + /// + /// The [AndroidCustomViewWidget] should only be instantiated internally. + /// This constructor is visible for testing purposes only and should + /// never be called externally. + @visibleForTesting + AndroidCustomViewWidget.private({ + super.key, + required this.controller, + required this.customView, + @visibleForTesting InstanceManager? instanceManager, + @visibleForTesting + this.platformViewsServiceProxy = const PlatformViewsServiceProxy(), + }) : instanceManager = + instanceManager ?? android_webview.JavaObject.globalInstanceManager; + + /// The reference to the Android native view that should be shown. + final android_webview.View customView; + + /// The [PlatformWebViewController] that allows controlling the native web + /// view. + final PlatformWebViewController controller; + + /// Maintains instances used to communicate with the native objects they + /// represent. + /// + /// This field is exposed for testing purposes only and should not be used + /// outside of tests. + @visibleForTesting + final InstanceManager instanceManager; + + /// Proxy that provides access to the platform views service. + /// + /// This service allows creating and controlling platform-specific views. + @visibleForTesting + final PlatformViewsServiceProxy platformViewsServiceProxy; + + @override + Widget build(BuildContext context) { + return PlatformViewLink( + key: key, + viewType: 'plugins.flutter.io/webview', + surfaceFactory: ( + BuildContext context, + PlatformViewController controller, + ) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + gestureRecognizers: const >{}, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + return _initAndroidView( + params, + displayWithHybridComposition: false, + platformViewsServiceProxy: platformViewsServiceProxy, + view: customView, + instanceManager: instanceManager, + ) + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) + ..create(); + }, + ); + } +} + +AndroidViewController _initAndroidView( + PlatformViewCreationParams params, { + required bool displayWithHybridComposition, + required PlatformViewsServiceProxy platformViewsServiceProxy, + required android_webview.View view, + required InstanceManager instanceManager, + TextDirection layoutDirection = TextDirection.ltr, +}) { + final int? instanceId = instanceManager.getIdentifier(view); + + if (displayWithHybridComposition) { + return platformViewsServiceProxy.initExpensiveAndroidView( + id: params.id, + viewType: 'plugins.flutter.io/webview', + layoutDirection: layoutDirection, + creationParams: instanceId, + creationParamsCodec: const StandardMessageCodec(), + ); + } else { + return platformViewsServiceProxy.initSurfaceAndroidView( + id: params.id, + viewType: 'plugins.flutter.io/webview', + layoutDirection: layoutDirection, + creationParams: instanceId, + creationParamsCodec: const StandardMessageCodec(), + ); + } +} + /// Signature for the `loadRequest` callback responsible for loading the [url] /// after a navigation request has been approved. typedef LoadRequestCallback = Future Function(LoadRequestParams params); 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 0a43f89e5eac..c19a2b226b0d 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -360,6 +360,16 @@ abstract class WebChromeClientFlutterApi { /// Callback to Dart function `WebChromeClient.onPermissionRequest`. void onPermissionRequest(int instanceId, int requestInstanceId); + /// Callback to Dart function `WebChromeClient.onShowCustomView`. + void onShowCustomView( + int instanceId, + int viewIdentifier, + int callbackIdentifier, + ); + + /// Callback to Dart function `WebChromeClient.onHideCustomView`. + void onHideCustomView(int instanceId); + /// Callback to Dart function `WebChromeClient.onGeolocationPermissionsShowPrompt`. void onGeolocationPermissionsShowPrompt( int instanceId, @@ -421,6 +431,45 @@ abstract class PermissionRequestFlutterApi { void create(int instanceId, List resources); } +/// Host API for `CustomViewCallback`. +/// +/// This class may handle instantiating and adding native object instances that +/// are attached to a Dart instance or handle method calls on the associated +/// native class or an instance of the class. +/// +/// See https://developer.android.com/reference/android/webkit/WebChromeClient.CustomViewCallback. +@HostApi(dartHostTestHandler: 'TestCustomViewCallbackHostApi') +abstract class CustomViewCallbackHostApi { + /// Handles Dart method `CustomViewCallback.onCustomViewHidden`. + void onCustomViewHidden(int identifier); +} + +/// Flutter API for `CustomViewCallback`. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +/// +/// See https://developer.android.com/reference/android/webkit/WebChromeClient.CustomViewCallback. +@FlutterApi() +abstract class CustomViewCallbackFlutterApi { + /// Create a new Dart instance and add it to the `InstanceManager`. + void create(int identifier); +} + +/// Flutter API for `View`. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +/// +/// See https://developer.android.com/reference/android/view/View. +@FlutterApi() +abstract class ViewFlutterApi { + /// Create a new Dart instance and add it to the `InstanceManager`. + void create(int identifier); +} + /// Host API for `GeolocationPermissionsCallback`. /// /// This class may handle instantiating and adding native object instances that diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 894448491c5a..6b4942ae204d 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/packages/tree/main/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: 3.9.5 +version: 3.10.0 environment: sdk: ">=2.19.0 <4.0.0" diff --git a/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart index 3f93d46b7705..0693ef5d4ce9 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart @@ -517,6 +517,8 @@ class CapturingWebChromeClient extends android_webview.WebChromeClient { super.onShowFileChooser, super.onGeolocationPermissionsShowPrompt, super.onGeolocationPermissionsHidePrompt, + super.onShowCustomView, + super.onHideCustomView, super.onPermissionRequest, super.binaryMessenger, super.instanceManager, diff --git a/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.mocks.dart index aa0bf6569d8d..37eaec813c02 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_android/test/android_navigation_delegate_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:mockito/mockito.dart' as _i1; diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart index 2809a40d885c..c350be64f2cf 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart @@ -19,7 +19,9 @@ import 'package:webview_flutter_platform_interface/webview_flutter_platform_inte import 'android_navigation_delegate_test.dart'; import 'android_webview_controller_test.mocks.dart'; import 'android_webview_test.mocks.dart' - show MockTestGeolocationPermissionsCallbackHostApi; + show + MockTestGeolocationPermissionsCallbackHostApi, + MockTestCustomViewCallbackHostApi; import 'test_android_webview.g.dart'; @GenerateNiceMocks(>[ @@ -65,6 +67,12 @@ void main() { android_webview.WebChromeClient instance, android_webview.PermissionRequest request, )? onPermissionRequest, + void Function( + android_webview.WebChromeClient instance, + android_webview.View view, + android_webview.CustomViewCallback callback)? + onShowCustomView, + void Function(android_webview.WebChromeClient instance)? onHideCustomView, })? createWebChromeClient, android_webview.WebView? mockWebView, android_webview.WebViewClient? mockWebViewClient, @@ -79,25 +87,31 @@ void main() { androidWebStorage: mockWebStorage ?? MockWebStorage(), androidWebViewProxy: AndroidWebViewProxy( createAndroidWebChromeClient: createWebChromeClient ?? - ( - {void Function(android_webview.WebView, int)? - onProgressChanged, - Future> Function( - android_webview.WebView webView, - android_webview.FileChooserParams params, - )? onShowFileChooser, - void Function( + ({ + void Function(android_webview.WebView, int)? + onProgressChanged, + Future> Function( + android_webview.WebView webView, + android_webview.FileChooserParams params, + )? onShowFileChooser, + void Function( + android_webview.WebChromeClient instance, + android_webview.PermissionRequest request, + )? onPermissionRequest, + Future Function( + String origin, + android_webview.GeolocationPermissionsCallback callback, + )? onGeolocationPermissionsShowPrompt, + void Function(android_webview.WebChromeClient instance)? + onGeolocationPermissionsHidePrompt, + void Function( android_webview.WebChromeClient instance, - android_webview.PermissionRequest request, - )? onPermissionRequest, - Future Function( - String origin, - android_webview.GeolocationPermissionsCallback - callback, - )? onGeolocationPermissionsShowPrompt, - void Function( - android_webview.WebChromeClient instance)? - onGeolocationPermissionsHidePrompt}) => + android_webview.View view, + android_webview.CustomViewCallback callback)? + onShowCustomView, + void Function(android_webview.WebChromeClient instance)? + onHideCustomView, + }) => MockWebChromeClient(), createAndroidWebView: () => nonNullMockWebView, createAndroidWebViewClient: ({ @@ -590,6 +604,8 @@ void main() { dynamic onGeolocationPermissionsShowPrompt, dynamic onGeolocationPermissionsHidePrompt, dynamic onPermissionRequest, + dynamic onShowCustomView, + dynamic onHideCustomView, }) { onShowFileChooserCallback = onShowFileChooser!; return mockWebChromeClient; @@ -658,6 +674,8 @@ void main() { void Function(android_webview.WebChromeClient instance)? onGeolocationPermissionsHidePrompt, dynamic onPermissionRequest, + dynamic onShowCustomView, + dynamic onHideCustomView, }) { onGeoPermissionHandle = onGeolocationPermissionsShowPrompt!; onGeoPermissionHidePromptHandle = onGeolocationPermissionsHidePrompt!; @@ -693,6 +711,78 @@ void main() { expect(testValue, 'changed'); }); + test('setCustomViewCallbacks', () async { + final MockTestCustomViewCallbackHostApi mockApi = + MockTestCustomViewCallbackHostApi(); + TestCustomViewCallbackHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final android_webview.CustomViewCallback testCallback = + android_webview.CustomViewCallback.detached( + instanceManager: instanceManager, + ); + + const int instanceIdentifier = 0; + instanceManager.addHostCreatedInstance(testCallback, instanceIdentifier); + + late final void Function( + android_webview.WebChromeClient instance, + android_webview.View view, + android_webview.CustomViewCallback callback) onShowCustomViewHandle; + late final void Function(android_webview.WebChromeClient instance) + onHideCustomViewHandle; + + final MockWebChromeClient mockWebChromeClient = MockWebChromeClient(); + final AndroidWebViewController controller = createControllerWithMocks( + createWebChromeClient: ({ + dynamic onProgressChanged, + dynamic onShowFileChooser, + dynamic onGeolocationPermissionsShowPrompt, + dynamic onGeolocationPermissionsHidePrompt, + dynamic onPermissionRequest, + void Function( + android_webview.WebChromeClient instance, + android_webview.View view, + android_webview.CustomViewCallback callback)? + onShowCustomView, + void Function(android_webview.WebChromeClient instance)? + onHideCustomView, + }) { + onShowCustomViewHandle = onShowCustomView!; + onHideCustomViewHandle = onHideCustomView!; + return mockWebChromeClient; + }, + ); + + final android_webview.View testView = android_webview.View.detached(); + bool showCustomViewCalled = false; + bool hideCustomViewCalled = false; + + await controller.setCustomWidgetCallbacks( + onShowCustomWidget: + (Widget widget, OnHideCustomWidgetCallback callback) async { + showCustomViewCalled = true; + }, + onHideCustomWidget: () { + hideCustomViewCalled = true; + }, + ); + + onShowCustomViewHandle( + mockWebChromeClient, + testView, + android_webview.CustomViewCallback.detached(), + ); + + expect(showCustomViewCalled, true); + + onHideCustomViewHandle(mockWebChromeClient); + expect(hideCustomViewCalled, true); + }); + test('setOnPlatformPermissionRequest', () async { late final void Function( android_webview.WebChromeClient instance, @@ -710,6 +800,8 @@ void main() { android_webview.WebChromeClient instance, android_webview.PermissionRequest request, )? onPermissionRequest, + dynamic onShowCustomView, + dynamic onHideCustomView, }) { onPermissionRequestCallback = onPermissionRequest!; return mockWebChromeClient; @@ -762,6 +854,8 @@ void main() { android_webview.WebChromeClient instance, android_webview.PermissionRequest request, )? onPermissionRequest, + dynamic onShowCustomView, + dynamic onHideCustomView, }) { onPermissionRequestCallback = onPermissionRequest!; return mockWebChromeClient; @@ -1217,6 +1311,76 @@ void main() { ); }); + testWidgets('default handling of custom views', + (WidgetTester tester) async { + final MockWebChromeClient mockWebChromeClient = MockWebChromeClient(); + + void Function( + android_webview.WebChromeClient instance, + android_webview.View view, + android_webview.CustomViewCallback callback)? + onShowCustomViewCallback; + + final AndroidWebViewController controller = createControllerWithMocks( + createWebChromeClient: ({ + dynamic onProgressChanged, + dynamic onShowFileChooser, + dynamic onGeolocationPermissionsShowPrompt, + dynamic onGeolocationPermissionsHidePrompt, + dynamic onPermissionRequest, + void Function( + android_webview.WebChromeClient instance, + android_webview.View view, + android_webview.CustomViewCallback callback)? + onShowCustomView, + dynamic onHideCustomView, + }) { + onShowCustomViewCallback = onShowCustomView; + return mockWebChromeClient; + }, + ); + + final MockPlatformViewsServiceProxy mockPlatformViewsService = + MockPlatformViewsServiceProxy(); + + when( + mockPlatformViewsService.initSurfaceAndroidView( + id: anyNamed('id'), + viewType: anyNamed('viewType'), + layoutDirection: anyNamed('layoutDirection'), + creationParams: anyNamed('creationParams'), + creationParamsCodec: anyNamed('creationParamsCodec'), + onFocus: anyNamed('onFocus'), + ), + ).thenReturn(MockSurfaceAndroidViewController()); + + final AndroidWebViewWidget webViewWidget = AndroidWebViewWidget( + AndroidWebViewWidgetCreationParams( + key: const Key('test_web_view'), + controller: controller, + platformViewsServiceProxy: mockPlatformViewsService, + ), + ); + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (BuildContext context) => webViewWidget.build(context), + ), + ), + ); + await tester.pumpAndSettle(); + + onShowCustomViewCallback!( + MockWebChromeClient(), + android_webview.WebView.detached(), + android_webview.CustomViewCallback.detached(), + ); + await tester.pumpAndSettle(); + + expect(find.byType(AndroidCustomViewWidget), findsOneWidget); + }); + testWidgets('PlatformView is recreated when the controller changes', (WidgetTester tester) async { final MockPlatformViewsServiceProxy mockPlatformViewsService = @@ -1346,4 +1510,67 @@ void main() { ); }); }); + + group('AndroidCustomViewWidget', () { + testWidgets('Builds Android custom view using supplied parameters', + (WidgetTester tester) async { + final AndroidWebViewController controller = createControllerWithMocks(); + + final AndroidCustomViewWidget customViewWidget = + AndroidCustomViewWidget.private( + key: const Key('test_custom_view'), + customView: android_webview.View.detached(), + controller: controller, + ); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) => customViewWidget.build(context), + )); + + expect(find.byType(PlatformViewLink), findsOneWidget); + expect(find.byKey(const Key('test_custom_view')), findsOneWidget); + }); + + testWidgets('displayWithHybridComposition should be false', + (WidgetTester tester) async { + final AndroidWebViewController controller = createControllerWithMocks(); + + final MockPlatformViewsServiceProxy mockPlatformViewsService = + MockPlatformViewsServiceProxy(); + + when( + mockPlatformViewsService.initSurfaceAndroidView( + id: anyNamed('id'), + viewType: anyNamed('viewType'), + layoutDirection: anyNamed('layoutDirection'), + creationParams: anyNamed('creationParams'), + creationParamsCodec: anyNamed('creationParamsCodec'), + onFocus: anyNamed('onFocus'), + ), + ).thenReturn(MockSurfaceAndroidViewController()); + + final AndroidCustomViewWidget customViewWidget = + AndroidCustomViewWidget.private( + controller: controller, + customView: android_webview.View.detached(), + platformViewsServiceProxy: mockPlatformViewsService, + ); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) => customViewWidget.build(context), + )); + await tester.pumpAndSettle(); + + verify( + mockPlatformViewsService.initSurfaceAndroidView( + id: anyNamed('id'), + viewType: anyNamed('viewType'), + layoutDirection: anyNamed('layoutDirection'), + creationParams: anyNamed('creationParams'), + creationParamsCodec: anyNamed('creationParamsCodec'), + onFocus: anyNamed('onFocus'), + ), + ); + }); + }); } diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart index f6e7124c4d95..b6d6e2c574a8 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart @@ -1,15 +1,16 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_android/test/android_webview_controller_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i9; -import 'dart:typed_data' as _i14; +import 'dart:typed_data' as _i13; import 'dart:ui' as _i4; import 'package:flutter/foundation.dart' as _i11; import 'package:flutter/gestures.dart' as _i12; -import 'package:flutter/material.dart' as _i13; import 'package:flutter/services.dart' as _i7; import 'package:mockito/mockito.dart' as _i1; import 'package:webview_flutter_android/src/android_proxy.dart' as _i10; @@ -22,7 +23,7 @@ import 'package:webview_flutter_android/src/platform_views_service_proxy.dart' import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart' as _i3; -import 'test_android_webview.g.dart' as _i15; +import 'test_android_webview.g.dart' as _i14; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -736,6 +737,23 @@ class MockAndroidWebViewController extends _i1.Mock returnValue: _i9.Future.value(), returnValueForMissingStub: _i9.Future.value(), ) as _i9.Future); + @override + _i9.Future setCustomWidgetCallbacks({ + _i8.OnShowCustomWidgetCallback? onShowCustomWidget, + _i8.OnHideCustomWidgetCallback? onHideCustomWidget, + }) => + (super.noSuchMethod( + Invocation.method( + #setCustomWidgetCallbacks, + [], + { + #onShowCustomWidget: onShowCustomWidget, + #onHideCustomWidget: onHideCustomWidget, + }, + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); } /// A class which mocks [AndroidWebViewProxy]. @@ -762,6 +780,7 @@ class MockAndroidWebViewProxy extends _i1.Mock String, _i2.GeolocationPermissionsCallback, )? onGeolocationPermissionsShowPrompt, + void Function(_i2.WebChromeClient)? onHideCustomView, void Function( _i2.WebChromeClient, _i2.PermissionRequest, @@ -770,6 +789,11 @@ class MockAndroidWebViewProxy extends _i1.Mock _i2.WebView, int, )? onProgressChanged, + void Function( + _i2.WebChromeClient, + _i2.View, + _i2.CustomViewCallback, + )? onShowCustomView, _i9.Future> Function( _i2.WebView, _i2.FileChooserParams, @@ -783,6 +807,7 @@ class MockAndroidWebViewProxy extends _i1.Mock String, _i2.GeolocationPermissionsCallback, )? onGeolocationPermissionsShowPrompt, + void Function(_i2.WebChromeClient)? onHideCustomView, void Function( _i2.WebChromeClient, _i2.PermissionRequest, @@ -791,6 +816,11 @@ class MockAndroidWebViewProxy extends _i1.Mock _i2.WebView, int, )? onProgressChanged, + void Function( + _i2.WebChromeClient, + _i2.View, + _i2.CustomViewCallback, + )? onShowCustomView, _i9.Future> Function( _i2.WebView, _i2.FileChooserParams, @@ -807,6 +837,7 @@ class MockAndroidWebViewProxy extends _i1.Mock String, _i2.GeolocationPermissionsCallback, )? onGeolocationPermissionsShowPrompt, + void Function(_i2.WebChromeClient)? onHideCustomView, void Function( _i2.WebChromeClient, _i2.PermissionRequest, @@ -815,6 +846,11 @@ class MockAndroidWebViewProxy extends _i1.Mock _i2.WebView, int, )? onProgressChanged, + void Function( + _i2.WebChromeClient, + _i2.View, + _i2.CustomViewCallback, + )? onShowCustomView, _i9.Future> Function( _i2.WebView, _i2.FileChooserParams, @@ -830,6 +866,7 @@ class MockAndroidWebViewProxy extends _i1.Mock String, _i2.GeolocationPermissionsCallback, )? onGeolocationPermissionsShowPrompt, + void Function(_i2.WebChromeClient)? onHideCustomView, void Function( _i2.WebChromeClient, _i2.PermissionRequest, @@ -838,6 +875,11 @@ class MockAndroidWebViewProxy extends _i1.Mock _i2.WebView, int, )? onProgressChanged, + void Function( + _i2.WebChromeClient, + _i2.View, + _i2.CustomViewCallback, + )? onShowCustomView, _i9.Future> Function( _i2.WebView, _i2.FileChooserParams, @@ -1291,7 +1333,7 @@ class MockExpensiveAndroidViewController extends _i1.Mock returnValueForMissingStub: _i9.Future.value(), ) as _i9.Future); @override - _i9.Future dispatchPointerEvent(_i13.PointerEvent? event) => + _i9.Future dispatchPointerEvent(_i12.PointerEvent? event) => (super.noSuchMethod( Invocation.method( #dispatchPointerEvent, @@ -1694,7 +1736,7 @@ class MockSurfaceAndroidViewController extends _i1.Mock returnValueForMissingStub: _i9.Future.value(), ) as _i9.Future); @override - _i9.Future dispatchPointerEvent(_i13.PointerEvent? event) => + _i9.Future dispatchPointerEvent(_i12.PointerEvent? event) => (super.noSuchMethod( Invocation.method( #dispatchPointerEvent, @@ -1986,7 +2028,7 @@ class MockWebView extends _i1.Mock implements _i2.WebView { @override _i9.Future postUrl( String? url, - _i14.Uint8List? data, + _i13.Uint8List? data, ) => (super.noSuchMethod( Invocation.method( @@ -2401,7 +2443,7 @@ class MockInstanceManager extends _i1.Mock implements _i5.InstanceManager { /// /// See the documentation for Mockito's code generation for more information. class MockTestInstanceManagerHostApi extends _i1.Mock - implements _i15.TestInstanceManagerHostApi { + implements _i14.TestInstanceManagerHostApi { @override void clear() => super.noSuchMethod( Invocation.method( diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart index ee0b48188ef6..b23bcaa24a2a 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_android/test/android_webview_cookie_manager_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; import 'dart:ui' as _i4; @@ -471,6 +473,23 @@ class MockAndroidWebViewController extends _i1.Mock returnValue: _i5.Future.value(), returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); + @override + _i5.Future setCustomWidgetCallbacks({ + _i6.OnShowCustomWidgetCallback? onShowCustomWidget, + _i6.OnHideCustomWidgetCallback? onHideCustomWidget, + }) => + (super.noSuchMethod( + Invocation.method( + #setCustomWidgetCallbacks, + [], + { + #onShowCustomWidget: onShowCustomWidget, + #onHideCustomWidget: onHideCustomWidget, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [TestInstanceManagerHostApi]. 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 194f1b17e587..da03052c7f22 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 @@ -18,6 +18,7 @@ import 'test_android_webview.g.dart'; DownloadListener, JavaScriptChannel, TestCookieManagerHostApi, + TestCustomViewCallbackHostApi, TestDownloadListenerHostApi, TestGeolocationPermissionsCallbackHostApi, TestInstanceManagerHostApi, @@ -1037,6 +1038,88 @@ void main() { expect(callbackParameters, [instance, request]); }); + test('onShowCustomView', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + const int instanceIdentifier = 0; + late final List callbackParameters; + final WebChromeClient instance = WebChromeClient.detached( + onShowCustomView: ( + WebChromeClient instance, + View view, + CustomViewCallback callback, + ) { + callbackParameters = [ + instance, + view, + callback, + ]; + }, + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance(instance, instanceIdentifier); + + final WebChromeClientFlutterApiImpl flutterApi = + WebChromeClientFlutterApiImpl( + instanceManager: instanceManager, + ); + + final View view = View.detached( + instanceManager: instanceManager, + ); + const int viewIdentifier = 50; + instanceManager.addHostCreatedInstance(view, viewIdentifier); + + final CustomViewCallback callback = CustomViewCallback.detached( + instanceManager: instanceManager, + ); + const int callbackIdentifier = 51; + instanceManager.addHostCreatedInstance(callback, callbackIdentifier); + + flutterApi.onShowCustomView( + instanceIdentifier, + viewIdentifier, + callbackIdentifier, + ); + + expect(callbackParameters, [ + instance, + view, + callback, + ]); + }); + + test('onHideCustomView', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + const int instanceIdentifier = 0; + late final List callbackParameters; + final WebChromeClient instance = WebChromeClient.detached( + onHideCustomView: ( + WebChromeClient instance, + ) { + callbackParameters = [ + instance, + ]; + }, + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance(instance, instanceIdentifier); + + final WebChromeClientFlutterApiImpl flutterApi = + WebChromeClientFlutterApiImpl( + instanceManager: instanceManager, + ); + + flutterApi.onHideCustomView(instanceIdentifier); + + expect(callbackParameters, [instance]); + }); + test('copy', () { expect(WebChromeClient.detached().copy(), isA()); }); @@ -1096,6 +1179,73 @@ void main() { expect(instance.mode, FileChooserMode.openMultiple); expect(instance.filenameHint, 'filenameHint'); }); + + group('CustomViewCallback', () { + tearDown(() { + TestCustomViewCallbackHostApi.setup(null); + }); + + test('onCustomViewHidden', () async { + final MockTestCustomViewCallbackHostApi mockApi = + MockTestCustomViewCallbackHostApi(); + TestCustomViewCallbackHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final CustomViewCallback instance = CustomViewCallback.detached( + instanceManager: instanceManager, + ); + const int instanceIdentifier = 0; + instanceManager.addHostCreatedInstance(instance, instanceIdentifier); + + await instance.onCustomViewHidden(); + + verify(mockApi.onCustomViewHidden(instanceIdentifier)); + }); + + test('FlutterAPI create', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final CustomViewCallbackFlutterApiImpl api = + CustomViewCallbackFlutterApiImpl( + instanceManager: instanceManager, + ); + + const int instanceIdentifier = 0; + + api.create(instanceIdentifier); + + expect( + instanceManager.getInstanceWithWeakReference(instanceIdentifier), + isA(), + ); + }); + }); + + group('View', () { + test('FlutterAPI create', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final ViewFlutterApiImpl api = ViewFlutterApiImpl( + instanceManager: instanceManager, + ); + + const int instanceIdentifier = 0; + + api.create(instanceIdentifier); + + expect( + instanceManager.getInstanceWithWeakReference(instanceIdentifier), + isA(), + ); + }); + }); }); group('CookieManager', () { 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 index a7f825fa16c8..fecf8140a114 100644 --- 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 @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_android/test/android_webview_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; import 'dart:typed_data' as _i7; @@ -314,6 +316,25 @@ class MockTestCookieManagerHostApi extends _i1.Mock ); } +/// A class which mocks [TestCustomViewCallbackHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestCustomViewCallbackHostApi extends _i1.Mock + implements _i6.TestCustomViewCallbackHostApi { + MockTestCustomViewCallbackHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void onCustomViewHidden(int? identifier) => super.noSuchMethod( + Invocation.method( + #onCustomViewHidden, + [identifier], + ), + returnValueForMissingStub: null, + ); +} + /// A class which mocks [TestDownloadListenerHostApi]. /// /// See the documentation for Mockito's code generation for more information. diff --git a/packages/webview_flutter/webview_flutter_android/test/instance_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/instance_manager_test.mocks.dart index c9e6e4bfadac..d071991ab0c4 100644 --- a/packages/webview_flutter/webview_flutter_android/test/instance_manager_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/instance_manager_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_android/test/instance_manager_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:mockito/mockito.dart' as _i1; diff --git a/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.mocks.dart index 864dbaa04aa0..df06f6252274 100644 --- a/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_android/test/legacy/webview_android_cookie_manager_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i3; diff --git a/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.mocks.dart index b82f8dea91e6..c52703812fe6 100644 --- a/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_android/test/legacy/webview_android_widget_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; import 'dart:typed_data' as _i6; diff --git a/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart b/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart index a018fccdcbe7..b88ea8a8c2dd 100644 --- a/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart +++ b/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart @@ -1764,6 +1764,49 @@ abstract class TestPermissionRequestHostApi { } } +/// Host API for `CustomViewCallback`. +/// +/// This class may handle instantiating and adding native object instances that +/// are attached to a Dart instance or handle method calls on the associated +/// native class or an instance of the class. +/// +/// See https://developer.android.com/reference/android/webkit/WebChromeClient.CustomViewCallback. +abstract class TestCustomViewCallbackHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = StandardMessageCodec(); + + /// Handles Dart method `CustomViewCallback.onCustomViewHidden`. + void onCustomViewHidden(int identifier); + + static void setup(TestCustomViewCallbackHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_android.CustomViewCallbackHostApi.onCustomViewHidden', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.CustomViewCallbackHostApi.onCustomViewHidden was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.CustomViewCallbackHostApi.onCustomViewHidden was null, expected non-null int.'); + api.onCustomViewHidden(arg_identifier!); + return []; + }); + } + } + } +} + /// Host API for `GeolocationPermissionsCallback`. /// /// This class may handle instantiating and adding native object instances that