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