From 4f2680ffdc424dae73845928e1dc1768539bbedf Mon Sep 17 00:00:00 2001 From: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> Date: Fri, 21 Apr 2023 15:17:49 -0400 Subject: [PATCH 1/2] platform impls --- .../webview_flutter_android/CHANGELOG.md | 4 + .../GeneratedAndroidWebView.java | 130 +++++++++++ .../PermissionRequestFlutterApiImpl.java | 65 ++++++ .../PermissionRequestHostApiImpl.java | 55 +++++ .../WebChromeClientFlutterApiImpl.java | 19 ++ .../WebChromeClientHostApiImpl.java | 8 + .../webviewflutter/WebViewFlutterPlugin.java | 7 + .../webviewflutter/PermissionRequestTest.java | 101 ++++++++ .../webviewflutter/WebChromeClientTest.java | 11 + .../android/app/src/main/AndroidManifest.xml | 3 + .../example/android/build.gradle | 2 +- .../example/lib/main.dart | 8 + .../example/pubspec.yaml | 6 + .../lib/src/android_proxy.dart | 4 + .../lib/src/android_webview.dart | 83 +++++++ .../lib/src/android_webview.g.dart | 131 +++++++++++ .../lib/src/android_webview_api_impls.dart | 98 ++++++++ .../lib/src/android_webview_controller.dart | 134 ++++++++++- .../pigeons/android_webview.dart | 32 +++ .../webview_flutter_android/pubspec.yaml | 8 +- .../android_navigation_delegate_test.dart | 1 + .../test/android_webview_controller_test.dart | 102 +++++++- ...android_webview_controller_test.mocks.dart | 136 +++++++++-- .../android_webview_cookie_manager_test.dart | 2 +- .../test/android_webview_test.dart | 123 ++++++++++ .../test/android_webview_test.mocks.dart | 34 +++ .../test/test_android_webview.g.dart | 71 ++++++ .../webview_flutter_wkwebview/CHANGELOG.md | 4 + .../webview_flutter_wkwebview/README.md | 4 +- .../example/ios/Runner/Info.plist | 2 + .../ios/RunnerTests/FWFDataConvertersTests.m | 62 ++++- .../RunnerTests/FWFUIDelegateHostApiTests.m | 41 ++++ .../example/lib/main.dart | 8 + .../example/pubspec.yaml | 6 + .../ios/Classes/FWFDataConverters.h | 66 ++++-- .../ios/Classes/FWFDataConverters.m | 78 +++++-- .../ios/Classes/FWFGeneratedWebKitApis.h | 83 +++++++ .../ios/Classes/FWFGeneratedWebKitApis.m | 167 +++++++++++++- .../ios/Classes/FWFHTTPCookieStoreHostApi.m | 2 +- .../Classes/FWFNavigationDelegateHostApi.m | 8 +- .../ios/Classes/FWFObjectHostApi.m | 4 +- .../Classes/FWFScriptMessageHandlerHostApi.m | 2 +- .../ios/Classes/FWFUIDelegateHostApi.m | 53 ++++- .../Classes/FWFUserContentControllerHostApi.m | 2 +- .../Classes/FWFWebViewConfigurationHostApi.m | 2 +- .../ios/Classes/FWFWebViewHostApi.m | 4 +- .../ios/Classes/FWFWebsiteDataStoreHostApi.m | 2 +- .../lib/src/common/web_kit.g.dart | 217 +++++++++++++++++- .../lib/src/web_kit/web_kit.dart | 39 +++- .../lib/src/web_kit/web_kit_api_impls.dart | 39 +++- .../lib/src/webkit_proxy.dart | 8 + .../lib/src/webkit_webview_controller.dart | 127 ++++++++-- .../pigeons/web_kit.dart | 80 +++++++ .../webview_flutter_wkwebview/pubspec.yaml | 8 +- .../test/src/common/test_web_kit.g.dart | 37 ++- .../test/src/web_kit/web_kit_test.dart | 68 ++++++ .../test/webkit_navigation_delegate_test.dart | 38 +-- .../test/webkit_webview_controller_test.dart | 93 +++++++- .../test/webkit_webview_widget_test.dart | 50 ++-- .../webkit_webview_widget_test.mocks.dart | 91 +++++++- 60 files changed, 2642 insertions(+), 231 deletions(-) create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/PermissionRequestFlutterApiImpl.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/PermissionRequestHostApiImpl.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PermissionRequestTest.java diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index d2d7c841466..14b0ec9bd9a 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.6.0 + +* Adds support for `PlatformWebViewController.setOnPlatformPermissionRequest`. + ## 3.5.3 * Bumps gradle from 7.2.2 to 8.0.0. 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 31e91a44dab..a9dc42f4010 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 @@ -2501,6 +2501,20 @@ public void onShowFileChooser( callback.reply(output); }); } + /** Callback to Dart function `WebChromeClient.onPermissionRequest`. */ + public void onPermissionRequest( + @NonNull Long instanceIdArg, + @NonNull Long requestInstanceIdArg, + @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.WebChromeClientFlutterApi.onPermissionRequest", + getCodec()); + channel.send( + new ArrayList(Arrays.asList(instanceIdArg, requestInstanceIdArg)), + channelReply -> callback.reply(null)); + } } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface WebStorageHostApi { @@ -2635,4 +2649,120 @@ public void create( channelReply -> callback.reply(null)); } } + /** + * Host API for `PermissionRequest`. + * + *

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/PermissionRequest. + * + *

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

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/PermissionRequest. + * + *

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

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 PermissionRequestFlutterApiImpl { + // To ease adding additional methods, this value is added prematurely. + @SuppressWarnings({"unused", "FieldCanBeLocal"}) + private final BinaryMessenger binaryMessenger; + + private final InstanceManager instanceManager; + private PermissionRequestFlutterApi api; + + /** + * Constructs a {@link PermissionRequestFlutterApiImpl}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public PermissionRequestFlutterApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + api = new PermissionRequestFlutterApi(binaryMessenger); + } + + /** + * Stores the `PermissionRequest` instance and notifies Dart to create and store a new + * `PermissionRequest` instance that is attached to this one. If `instance` has already been + * added, this method does nothing. + */ + public void create( + @NonNull PermissionRequest instance, + @NonNull String[] resources, + @NonNull PermissionRequestFlutterApi.Reply callback) { + if (!instanceManager.containsInstance(instance)) { + api.create( + instanceManager.addHostCreatedInstance(instance), Arrays.asList(resources), callback); + } + } + + /** + * Sets the Flutter API used to send messages to Dart. + * + *

This is only visible for testing. + */ + @VisibleForTesting + void setApi(@NonNull PermissionRequestFlutterApi api) { + this.api = api; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/PermissionRequestHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/PermissionRequestHostApiImpl.java new file mode 100644 index 00000000000..e4faf449adc --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/PermissionRequestHostApiImpl.java @@ -0,0 +1,55 @@ +// 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.os.Build; +import android.webkit.PermissionRequest; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.PermissionRequestHostApi; +import java.util.List; +import java.util.Objects; + +/** + * Host API implementation for `PermissionRequest`. + * + *

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. + */ +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) +public class PermissionRequestHostApiImpl implements PermissionRequestHostApi { + // To ease adding additional methods, this value is added prematurely. + @SuppressWarnings({"unused", "FieldCanBeLocal"}) + private final BinaryMessenger binaryMessenger; + + private final InstanceManager instanceManager; + + /** + * Constructs a {@link PermissionRequestHostApiImpl}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public PermissionRequestHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + } + + @Override + public void grant(@NonNull Long instanceId, @NonNull List resources) { + getPermissionRequestInstance(instanceId).grant(resources.toArray(new String[0])); + } + + @Override + public void deny(@NonNull Long instanceId) { + getPermissionRequestInstance(instanceId).deny(); + } + + private PermissionRequest getPermissionRequestInstance(@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/WebChromeClientFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java index c13e4ae6a46..fab34fc212d 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.webkit.PermissionRequest; import android.webkit.WebChromeClient; import android.webkit.WebView; import androidx.annotation.NonNull; @@ -71,6 +72,24 @@ public void onShowFileChooser( callback); } + /** + * Sends a message to Dart to call `WebChromeClient.onPermissionRequest` on the Dart object + * representing `instance`. + */ + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public void onPermissionRequest( + @NonNull WebChromeClient instance, + @NonNull PermissionRequest request, + @NonNull WebChromeClientFlutterApi.Reply callback) { + new PermissionRequestFlutterApiImpl(binaryMessenger, instanceManager) + .create(request, request.getResources(), reply -> {}); + + super.onPermissionRequest( + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(instance)), + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(request)), + 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 8c35581ddc9..38ebcb8932b 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 @@ -7,6 +7,7 @@ import android.net.Uri; import android.os.Build; import android.os.Message; +import android.webkit.PermissionRequest; import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import android.webkit.WebResourceRequest; @@ -76,6 +77,12 @@ public boolean onShowFileChooser( return currentReturnValueForOnShowFileChooser; } + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public void onPermissionRequest(@NonNull PermissionRequest request) { + flutterApi.onPermissionRequest(this, request, reply -> {}); + } + /** Sets return value for {@link #onShowFileChooser}. */ public void setReturnValueForOnShowFileChooser(boolean value) { returnValueForOnShowFileChooser = value; @@ -136,6 +143,7 @@ public boolean shouldOverrideUrlLoading( return true; } + // Legacy codepath for < N. @Override @SuppressWarnings({"deprecation", "RedundantSuppression"}) public boolean shouldOverrideUrlLoading(WebView windowWebView, String url) { 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 1434e57c080..af34649211a 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 @@ -5,6 +5,7 @@ package io.flutter.plugins.webviewflutter; import android.content.Context; +import android.os.Build; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -19,6 +20,7 @@ import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.InstanceManagerHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaObjectHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelHostApi; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.PermissionRequestHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebSettingsHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebStorageHostApi; @@ -129,6 +131,11 @@ private void setUp( WebStorageHostApi.setup( binaryMessenger, new WebStorageHostApiImpl(instanceManager, new WebStorageHostApiImpl.WebStorageCreator())); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + PermissionRequestHostApi.setup( + binaryMessenger, new PermissionRequestHostApiImpl(binaryMessenger, instanceManager)); + } } @Override diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PermissionRequestTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PermissionRequestTest.java new file mode 100644 index 00000000000..d359d519121 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PermissionRequestTest.java @@ -0,0 +1,101 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import android.webkit.PermissionRequest; +import io.flutter.plugin.common.BinaryMessenger; +import java.util.Collections; +import java.util.List; +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 PermissionRequestTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public PermissionRequest mockPermissionRequest; + + @Mock public BinaryMessenger mockBinaryMessenger; + + @Mock + public io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.PermissionRequestFlutterApi + mockFlutterApi; + + InstanceManager instanceManager; + + @Before + public void setUp() { + instanceManager = InstanceManager.create(identifier -> {}); + } + + @After + public void tearDown() { + instanceManager.stopFinalizationListener(); + } + + // These values MUST equal the constants for the Dart PermissionRequest class. + @Test + public void enums() { + assertEquals(PermissionRequest.RESOURCE_AUDIO_CAPTURE, "android.webkit.resource.AUDIO_CAPTURE"); + assertEquals(PermissionRequest.RESOURCE_VIDEO_CAPTURE, "android.webkit.resource.VIDEO_CAPTURE"); + assertEquals(PermissionRequest.RESOURCE_MIDI_SYSEX, "android.webkit.resource.MIDI_SYSEX"); + } + + @Test + public void grant() { + final List resources = + Collections.singletonList(PermissionRequest.RESOURCE_AUDIO_CAPTURE); + + final long instanceIdentifier = 0; + instanceManager.addDartCreatedInstance(mockPermissionRequest, instanceIdentifier); + + final PermissionRequestHostApiImpl hostApi = + new PermissionRequestHostApiImpl(mockBinaryMessenger, instanceManager); + + hostApi.grant(instanceIdentifier, resources); + + verify(mockPermissionRequest).grant(new String[] {PermissionRequest.RESOURCE_AUDIO_CAPTURE}); + } + + @Test + public void deny() { + final long instanceIdentifier = 0; + instanceManager.addDartCreatedInstance(mockPermissionRequest, instanceIdentifier); + + final PermissionRequestHostApiImpl hostApi = + new PermissionRequestHostApiImpl(mockBinaryMessenger, instanceManager); + + hostApi.deny(instanceIdentifier); + + verify(mockPermissionRequest).deny(); + } + + @Test + public void flutterApiCreate() { + final PermissionRequestFlutterApiImpl flutterApi = + new PermissionRequestFlutterApiImpl(mockBinaryMessenger, instanceManager); + flutterApi.setApi(mockFlutterApi); + + final List resources = + Collections.singletonList(PermissionRequest.RESOURCE_AUDIO_CAPTURE); + + flutterApi.create(mockPermissionRequest, resources.toArray(new String[0]), reply -> {}); + + final long instanceIdentifier = + Objects.requireNonNull( + instanceManager.getIdentifierForStrongReference(mockPermissionRequest)); + verify(mockFlutterApi).create(eq(instanceIdentifier), eq(resources), 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 ccd26f0cdc3..9a97cd6f37a 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,6 +15,7 @@ import android.net.Uri; import android.os.Message; +import android.webkit.PermissionRequest; import android.webkit.WebResourceRequest; import android.webkit.WebView; import android.webkit.WebView.WebViewTransport; @@ -114,4 +115,14 @@ public void onCreateWindow() { mockOnCreateWindowWebView, mockRequest)); verify(mockWebView).loadUrl("https://www.google.com"); } + + @Test + public void onPermissionRequest() { + final PermissionRequest mockRequest = mock(PermissionRequest.class); + instanceManager.addDartCreatedInstance(mockRequest, 10); + + webChromeClient.onPermissionRequest(mockRequest); + + verify(mockFlutterApi).onPermissionRequest(eq(webChromeClient), eq(mockRequest), any()); + } } diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/AndroidManifest.xml b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/AndroidManifest.xml index b8c8d38d45a..b3e1d11fded 100644 --- a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/AndroidManifest.xml +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/AndroidManifest.xml @@ -39,4 +39,7 @@ permission failure prevents tests from running. --> + + + diff --git a/packages/webview_flutter/webview_flutter_android/example/android/build.gradle b/packages/webview_flutter/webview_flutter_android/example/android/build.gradle index ca118fdb964..614573a9fb8 100644 --- a/packages/webview_flutter/webview_flutter_android/example/android/build.gradle +++ b/packages/webview_flutter/webview_flutter_android/example/android/build.gradle @@ -24,7 +24,7 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } 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 b8c9f7bc8cb..a8714d3ae3b 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -136,6 +136,14 @@ Page resource error: ); }, )) + ..setOnPlatformPermissionRequest( + (PlatformWebViewPermissionRequest request) { + debugPrint( + 'requesting permissions for ${request.types.map((WebViewPermissionResourceType type) => type.name)}', + ); + request.grant(); + }, + ) ..loadRequest(LoadRequestParams( uri: Uri.parse('https://flutter.dev'), )); diff --git a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml index 8a190ff22eb..af4ad50821e 100644 --- a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml @@ -35,3 +35,9 @@ flutter: - assets/sample_video.mp4 - assets/www/index.html - assets/www/styles/style.css + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + webview_flutter_platform_interface: + path: ../../../webview_flutter/webview_flutter_platform_interface 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 0d716220b05..8182a42f4b7 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 @@ -35,6 +35,10 @@ class AndroidWebViewProxy { android_webview.WebView webView, android_webview.FileChooserParams params, )? onShowFileChooser, + void Function( + android_webview.WebChromeClient instance, + android_webview.PermissionRequest request, + )? onPermissionRequest, }) createAndroidWebChromeClient; /// Constructs a [android_webview.WebViewClient]. 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 8ab89d4b875..35e1767dd6b 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 @@ -934,6 +934,7 @@ class WebChromeClient extends JavaObject { WebChromeClient({ this.onProgressChanged, this.onShowFileChooser, + this.onPermissionRequest, @visibleForTesting super.binaryMessenger, @visibleForTesting super.instanceManager, }) : super.detached() { @@ -950,6 +951,7 @@ class WebChromeClient extends JavaObject { WebChromeClient.detached({ this.onProgressChanged, this.onShowFileChooser, + this.onPermissionRequest, super.binaryMessenger, super.instanceManager, }) : super.detached(); @@ -974,6 +976,16 @@ class WebChromeClient extends JavaObject { FileChooserParams params, )? onShowFileChooser; + /// Notify the host application that web content is requesting permission to + /// access the specified resources and the permission currently isn't granted + /// or denied. + /// + /// Only invoked on Android versions 21+. + final void Function( + WebChromeClient instance, + PermissionRequest request, + )? onPermissionRequest; + /// Sets the required synchronous return value for the Java method, /// `WebChromeClient.onShowFileChooser(...)`. /// @@ -1014,6 +1026,77 @@ class WebChromeClient extends JavaObject { } } +/// This class defines a permission request and is used when web content +/// requests access to protected resources. +/// +/// Only supported on Android versions >= 21. +/// +/// See https://developer.android.com/reference/android/webkit/PermissionRequest. +class PermissionRequest extends JavaObject { + /// Instantiates a [PermissionRequest] 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 + PermissionRequest.detached({ + required this.resources, + required super.binaryMessenger, + required super.instanceManager, + }) : _permissionRequestApi = PermissionRequestHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super.detached(); + + /// Resource belongs to audio capture device, like microphone. + /// + /// See https://developer.android.com/reference/android/webkit/PermissionRequest#RESOURCE_AUDIO_CAPTURE. + static const String audioCapture = 'android.webkit.resource.AUDIO_CAPTURE'; + + /// Resource will allow sysex messages to be sent to or received from MIDI + /// devices. + /// + /// See https://developer.android.com/reference/android/webkit/PermissionRequest#RESOURCE_MIDI_SYSEX. + static const String midiSysex = 'android.webkit.resource.MIDI_SYSEX'; + + /// Resource belongs to video capture device, like camera. + /// + /// See https://developer.android.com/reference/android/webkit/PermissionRequest#RESOURCE_VIDEO_CAPTURE. + static const String videoCapture = 'android.webkit.resource.VIDEO_CAPTURE'; + + /// Resource belongs to protected media identifier. + /// + /// See https://developer.android.com/reference/android/webkit/PermissionRequest#RESOURCE_VIDEO_CAPTURE. + static const String protectedMediaId = + 'android.webkit.resource.PROTECTED_MEDIA_ID'; + + final PermissionRequestHostApiImpl _permissionRequestApi; + + /// Resources the web page is trying to access. + final List resources; + + /// Call this method to get the resources the web page is trying to access. + Future grant(List resources) { + return _permissionRequestApi.grantFromInstances(this, resources); + } + + /// Call this method to grant origin the permission to access the given + /// resources. + Future deny() { + return _permissionRequestApi.denyFromInstances(this); + } + + @override + PermissionRequest copy() { + return PermissionRequest.detached( + resources: resources, + binaryMessenger: _permissionRequestApi.binaryMessenger, + instanceManager: _permissionRequestApi.instanceManager, + ); + } +} + /// Parameters received when a [WebChromeClient] should show a file chooser. /// /// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams. 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 f5a4564802a..7488cd86337 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 @@ -1929,6 +1929,9 @@ abstract class WebChromeClientFlutterApi { Future> onShowFileChooser( int instanceId, int webViewInstanceId, int paramsInstanceId); + /// Callback to Dart function `WebChromeClient.onPermissionRequest`. + void onPermissionRequest(int instanceId, int requestInstanceId); + static void setup(WebChromeClientFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1985,6 +1988,29 @@ abstract class WebChromeClientFlutterApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebChromeClientFlutterApi.onPermissionRequest', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onPermissionRequest was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onPermissionRequest was null, expected non-null int.'); + final int? arg_requestInstanceId = (args[1] as int?); + assert(arg_requestInstanceId != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onPermissionRequest was null, expected non-null int.'); + api.onPermissionRequest(arg_instanceId!, arg_requestInstanceId!); + return; + }); + } + } } } @@ -2112,3 +2138,108 @@ abstract class FileChooserParamsFlutterApi { } } } + +/// Host API for `PermissionRequest`. +/// +/// 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/PermissionRequest. +class PermissionRequestHostApi { + /// Constructor for [PermissionRequestHostApi]. 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. + PermissionRequestHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + /// Handles Dart method `PermissionRequest.grant`. + Future grant(int arg_instanceId, List arg_resources) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PermissionRequestHostApi.grant', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_instanceId, arg_resources]) 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; + } + } + + /// Handles Dart method `PermissionRequest.deny`. + Future deny(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PermissionRequestHostApi.deny', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_instanceId]) 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 `PermissionRequest`. +/// +/// 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/PermissionRequest. +abstract class PermissionRequestFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + /// Create a new Dart instance and add it to the `InstanceManager`. + void create(int instanceId, List resources); + + static void setup(PermissionRequestFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PermissionRequestFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PermissionRequestFlutterApi.create 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.PermissionRequestFlutterApi.create was null, expected non-null int.'); + final List? arg_resources = + (args[1] as List?)?.cast(); + assert(arg_resources != null, + 'Argument for dev.flutter.pigeon.PermissionRequestFlutterApi.create was null, expected non-null List.'); + api.create(arg_instanceId!, arg_resources!); + return; + }); + } + } + } +} 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 0bc2c84c409..f1010d76f63 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 @@ -46,6 +46,7 @@ class AndroidWebViewFlutterApis { JavaScriptChannelFlutterApiImpl? javaScriptChannelFlutterApi, FileChooserParamsFlutterApiImpl? fileChooserParamsFlutterApi, WebViewFlutterApiImpl? webViewFlutterApi, + PermissionRequestFlutterApiImpl? permissionRequestFlutterApi, }) { this.javaObjectFlutterApi = javaObjectFlutterApi ?? JavaObjectFlutterApiImpl(); @@ -60,6 +61,8 @@ class AndroidWebViewFlutterApis { this.fileChooserParamsFlutterApi = fileChooserParamsFlutterApi ?? FileChooserParamsFlutterApiImpl(); this.webViewFlutterApi = webViewFlutterApi ?? WebViewFlutterApiImpl(); + this.permissionRequestFlutterApi = + permissionRequestFlutterApi ?? PermissionRequestFlutterApiImpl(); } static bool _haveBeenSetUp = false; @@ -90,6 +93,9 @@ class AndroidWebViewFlutterApis { /// Flutter Api for [WebView]. late final WebViewFlutterApiImpl webViewFlutterApi; + /// Flutter Api for [PermissionRequest]. + late final PermissionRequestFlutterApiImpl permissionRequestFlutterApi; + /// Ensures all the Flutter APIs have been setup to receive calls from native code. void ensureSetUp() { if (!_haveBeenSetUp) { @@ -100,6 +106,7 @@ class AndroidWebViewFlutterApis { JavaScriptChannelFlutterApi.setup(javaScriptChannelFlutterApi); FileChooserParamsFlutterApi.setup(fileChooserParamsFlutterApi); WebViewFlutterApi.setup(webViewFlutterApi); + PermissionRequestFlutterApi.setup(permissionRequestFlutterApi); _haveBeenSetUp = true; } } @@ -912,6 +919,28 @@ class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { return Future>.value(const []); } + + @override + void onPermissionRequest( + int instanceId, + int requestInstanceId, + ) { + final WebChromeClient instance = + instanceManager.getInstanceWithWeakReference(instanceId)!; + if (instance.onPermissionRequest != null) { + instance.onPermissionRequest!( + instance, + instanceManager.getInstanceWithWeakReference(requestInstanceId)!, + ); + } else { + // The method requires calling grant or deny if the Java method is + // overridden, so this calls deny by default if `onPermissionRequest` is + // null. + final PermissionRequest request = + instanceManager.getInstanceWithWeakReference(requestInstanceId)!; + request.deny(); + } + } } /// Host api implementation for [WebStorage]. @@ -977,3 +1006,72 @@ class FileChooserParamsFlutterApiImpl extends FileChooserParamsFlutterApi { ); } } + +/// Host api implementation for [PermissionRequest]. +class PermissionRequestHostApiImpl extends PermissionRequestHostApi { + /// Constructs a [PermissionRequestHostApiImpl]. + PermissionRequestHostApiImpl({ + 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 grantFromInstances( + PermissionRequest instance, + List resources, + ) { + return grant(instanceManager.getIdentifier(instance)!, resources); + } + + /// Helper method to convert instance ids to objects. + Future denyFromInstances(PermissionRequest instance) { + return deny(instanceManager.getIdentifier(instance)!); + } +} + +/// Flutter API implementation for [PermissionRequest]. +/// +/// 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 PermissionRequestFlutterApiImpl implements PermissionRequestFlutterApi { + /// Constructs a [PermissionRequestFlutterApiImpl]. + PermissionRequestFlutterApiImpl({ + 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, + List resources, + ) { + instanceManager.addHostCreatedInstance( + PermissionRequest.detached( + resources: resources.cast(), + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + identifier, + ); + } +} 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 88a4bca84ab..c2b1e9a466a 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 @@ -64,6 +64,21 @@ class AndroidWebViewControllerCreationParams final android_webview.WebStorage androidWebStorage; } +/// Android-specific resources that can require permissions. +class AndroidWebViewPermissionResourceType + extends WebViewPermissionResourceType { + const AndroidWebViewPermissionResourceType._(super.name); + + /// A resource that will allow sysex messages to be sent to or received from + /// MIDI devices. + static const AndroidWebViewPermissionResourceType midiSysex = + AndroidWebViewPermissionResourceType._('midiSysex'); + + /// A resource that belongs to a protected media identifier. + static const AndroidWebViewPermissionResourceType protectedMediaId = + AndroidWebViewPermissionResourceType._('protectedMediaId'); +} + /// Implementation of the [PlatformWebViewController] with the Android WebView API. class AndroidWebViewController extends PlatformWebViewController { /// Creates a new [AndroidWebViewCookieManager]. @@ -102,18 +117,63 @@ class AndroidWebViewController extends PlatformWebViewController { } }; }), - onShowFileChooser: withWeakReferenceTo(this, - (WeakReference weakReference) { - return (android_webview.WebView webView, - android_webview.FileChooserParams params) async { - if (weakReference.target?._onShowFileSelectorCallback != null) { - return weakReference.target!._onShowFileSelectorCallback!( - FileSelectorParams._fromFileChooserParams(params), - ); - } - return []; - }; - }), + onShowFileChooser: withWeakReferenceTo( + this, + (WeakReference weakReference) { + return (android_webview.WebView webView, + android_webview.FileChooserParams params) async { + if (weakReference.target?._onShowFileSelectorCallback != null) { + return weakReference.target!._onShowFileSelectorCallback!( + FileSelectorParams._fromFileChooserParams(params), + ); + } + return []; + }; + }, + ), + onPermissionRequest: withWeakReferenceTo( + this, + (WeakReference weakReference) { + return (_, android_webview.PermissionRequest request) async { + final void Function(PlatformWebViewPermissionRequest)? callback = + weakReference.target?._onPermissionRequestCallback; + if (callback == null) { + return request.deny(); + } else { + final Set types = request.resources + .map((String type) { + switch (type) { + case android_webview.PermissionRequest.videoCapture: + return WebViewPermissionResourceType.camera; + case android_webview.PermissionRequest.audioCapture: + return WebViewPermissionResourceType.microphone; + case android_webview.PermissionRequest.midiSysex: + return AndroidWebViewPermissionResourceType.midiSysex; + case android_webview.PermissionRequest.protectedMediaId: + return AndroidWebViewPermissionResourceType + .protectedMediaId; + } + + // Type not supported. + return null; + }) + .whereType() + .toSet(); + + // If the request didn't contain any permissions recognized by the + // implementation, deny by default. + if (types.isEmpty) { + return request.deny(); + } + + callback(AndroidWebViewPermissionRequest._( + types: types, + request: request, + )); + } + }; + }, + ), ); /// The native [android_webview.FlutterAssetManager] allows managing assets. @@ -127,6 +187,7 @@ class AndroidWebViewController extends PlatformWebViewController { Future> Function(FileSelectorParams)? _onShowFileSelectorCallback; + void Function(PlatformWebViewPermissionRequest)? _onPermissionRequestCallback; /// Whether to enable the platform's webview content debugging tools. /// @@ -367,6 +428,55 @@ class AndroidWebViewController extends PlatformWebViewController { onShowFileSelector != null, ); } + + /// Sets a callback that notifies the host application that web content is + /// requesting permission to access the specified resources. + /// + /// Only invoked on Android versions 21+. + @override + Future setOnPlatformPermissionRequest( + void Function( + PlatformWebViewPermissionRequest request, + ) onPermissionRequest, + ) async { + _onPermissionRequestCallback = onPermissionRequest; + } +} + +/// Android implementation of [PlatformWebViewPermissionRequest]. +class AndroidWebViewPermissionRequest extends PlatformWebViewPermissionRequest { + const AndroidWebViewPermissionRequest._({ + required super.types, + required android_webview.PermissionRequest request, + }) : _request = request; + + final android_webview.PermissionRequest _request; + + @override + Future grant() { + return _request + .grant(types.map((WebViewPermissionResourceType type) { + switch (type) { + case WebViewPermissionResourceType.camera: + return android_webview.PermissionRequest.videoCapture; + case WebViewPermissionResourceType.microphone: + return android_webview.PermissionRequest.audioCapture; + case AndroidWebViewPermissionResourceType.midiSysex: + return android_webview.PermissionRequest.midiSysex; + case AndroidWebViewPermissionResourceType.protectedMediaId: + return android_webview.PermissionRequest.protectedMediaId; + } + + throw UnsupportedError( + 'Resource of type `${type.name}` is not supported.', + ); + }).toList()); + } + + @override + Future deny() { + return _request.deny(); + } } /// Mode of how to select files for a file chooser. 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 1d232b24179..62a7aa8a25b 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -345,6 +345,9 @@ abstract class WebChromeClientFlutterApi { int webViewInstanceId, int paramsInstanceId, ); + + /// Callback to Dart function `WebChromeClient.onPermissionRequest`. + void onPermissionRequest(int instanceId, int requestInstanceId); } @HostApi(dartHostTestHandler: 'TestWebStorageHostApi') @@ -367,3 +370,32 @@ abstract class FileChooserParamsFlutterApi { String? filenameHint, ); } + +/// Host API for `PermissionRequest`. +/// +/// 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/PermissionRequest. +@HostApi(dartHostTestHandler: 'TestPermissionRequestHostApi') +abstract class PermissionRequestHostApi { + /// Handles Dart method `PermissionRequest.grant`. + void grant(int instanceId, List resources); + + /// Handles Dart method `PermissionRequest.deny`. + void deny(int instanceId); +} + +/// Flutter API for `PermissionRequest`. +/// +/// 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/PermissionRequest. +@FlutterApi() +abstract class PermissionRequestFlutterApi { + /// Create a new Dart instance and add it to the `InstanceManager`. + void create(int instanceId, List resources); +} diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 1802f7e32d8..c4d13ca813f 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.5.3 +version: 3.6.0 environment: sdk: ">=2.18.0 <4.0.0" @@ -30,3 +30,9 @@ dev_dependencies: sdk: flutter mockito: 5.4.0 pigeon: ^9.2.4 + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + webview_flutter_platform_interface: + path: ../../webview_flutter/webview_flutter_platform_interface 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 1801589f885..7d834ba7466 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 @@ -515,6 +515,7 @@ class CapturingWebChromeClient extends android_webview.WebChromeClient { CapturingWebChromeClient({ super.onProgressChanged, super.onShowFileChooser, + super.onPermissionRequest, super.binaryMessenger, super.instanceManager, }) : super.detached() { 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 f224ff1d1e5..accd4db9ad3 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 @@ -18,7 +18,7 @@ import 'package:webview_flutter_android/src/android_webview_api_impls.dart'; import 'package:webview_flutter_android/src/instance_manager.dart'; import 'package:webview_flutter_android/src/platform_views_service_proxy.dart'; import 'package:webview_flutter_android/webview_flutter_android.dart'; -import 'package:webview_flutter_platform_interface/src/webview_platform.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'android_navigation_delegate_test.dart'; import 'android_webview_controller_test.mocks.dart'; @@ -32,6 +32,7 @@ import 'test_android_webview.g.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), MockSpec(), MockSpec(), MockSpec(), @@ -58,6 +59,10 @@ void main() { android_webview.WebView webView, android_webview.FileChooserParams params, )? onShowFileChooser, + void Function( + android_webview.WebChromeClient instance, + android_webview.PermissionRequest request, + )? onPermissionRequest, })? createWebChromeClient, android_webview.WebView? mockWebView, android_webview.WebViewClient? mockWebViewClient, @@ -79,6 +84,10 @@ void main() { android_webview.WebView webView, android_webview.FileChooserParams params, )? onShowFileChooser, + void Function( + android_webview.WebChromeClient instance, + android_webview.PermissionRequest request, + )? onPermissionRequest, }) => MockWebChromeClient(), createAndroidWebView: () => nonNullMockWebView, @@ -569,6 +578,7 @@ void main() { android_webview.WebView webView, android_webview.FileChooserParams params, )? onShowFileChooser, + dynamic onPermissionRequest, }) { onShowFileChooserCallback = onShowFileChooser!; return mockWebChromeClient; @@ -603,6 +613,96 @@ void main() { expect(fileSelectorParams.mode, FileSelectorMode.open); }); + test('setOnPlatformPermissionRequest', () async { + late final void Function( + android_webview.WebChromeClient instance, + android_webview.PermissionRequest request, + ) onPermissionRequestCallback; + + final MockWebChromeClient mockWebChromeClient = MockWebChromeClient(); + final AndroidWebViewController controller = createControllerWithMocks( + createWebChromeClient: ({ + dynamic onProgressChanged, + dynamic onShowFileChooser, + void Function( + android_webview.WebChromeClient instance, + android_webview.PermissionRequest request, + )? onPermissionRequest, + }) { + onPermissionRequestCallback = onPermissionRequest!; + return mockWebChromeClient; + }, + ); + + late final PlatformWebViewPermissionRequest permissionRequest; + await controller.setOnPlatformPermissionRequest( + (PlatformWebViewPermissionRequest request) async { + permissionRequest = request; + request.grant(); + }, + ); + + final List permissionTypes = [ + android_webview.PermissionRequest.audioCapture, + ]; + + final MockPermissionRequest mockPermissionRequest = + MockPermissionRequest(); + when(mockPermissionRequest.resources).thenReturn(permissionTypes); + + onPermissionRequestCallback( + android_webview.WebChromeClient.detached(), + mockPermissionRequest, + ); + + expect(permissionRequest.types, [ + WebViewPermissionResourceType.microphone, + ]); + verify(mockPermissionRequest.grant(permissionTypes)); + }); + + test( + 'setOnPlatformPermissionRequest callback not invoked when type is not recognized', + () async { + late final void Function( + android_webview.WebChromeClient instance, + android_webview.PermissionRequest request, + ) onPermissionRequestCallback; + + final MockWebChromeClient mockWebChromeClient = MockWebChromeClient(); + final AndroidWebViewController controller = createControllerWithMocks( + createWebChromeClient: ({ + dynamic onProgressChanged, + dynamic onShowFileChooser, + void Function( + android_webview.WebChromeClient instance, + android_webview.PermissionRequest request, + )? onPermissionRequest, + }) { + onPermissionRequestCallback = onPermissionRequest!; + return mockWebChromeClient; + }, + ); + + bool callbackCalled = false; + await controller.setOnPlatformPermissionRequest( + (PlatformWebViewPermissionRequest request) async { + callbackCalled = true; + }, + ); + + final MockPermissionRequest mockPermissionRequest = + MockPermissionRequest(); + when(mockPermissionRequest.resources).thenReturn(['unknownType']); + + onPermissionRequestCallback( + android_webview.WebChromeClient.detached(), + mockPermissionRequest, + ); + + expect(callbackCalled, isFalse); + }); + test('runJavaScript', () async { final MockWebView mockWebView = MockWebView(); final AndroidWebViewController controller = createControllerWithMocks( 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 e0c637efdd1..093312e06e1 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 @@ -184,9 +184,20 @@ class _FakeSize_13 extends _i1.SmartFake implements _i4.Size { ); } -class _FakeExpensiveAndroidViewController_14 extends _i1.SmartFake +class _FakePermissionRequest_14 extends _i1.SmartFake + implements _i2.PermissionRequest { + _FakePermissionRequest_14( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeExpensiveAndroidViewController_15 extends _i1.SmartFake implements _i7.ExpensiveAndroidViewController { - _FakeExpensiveAndroidViewController_14( + _FakeExpensiveAndroidViewController_15( Object parent, Invocation parentInvocation, ) : super( @@ -195,9 +206,9 @@ class _FakeExpensiveAndroidViewController_14 extends _i1.SmartFake ); } -class _FakeSurfaceAndroidViewController_15 extends _i1.SmartFake +class _FakeSurfaceAndroidViewController_16 extends _i1.SmartFake implements _i7.SurfaceAndroidViewController { - _FakeSurfaceAndroidViewController_15( + _FakeSurfaceAndroidViewController_16( Object parent, Invocation parentInvocation, ) : super( @@ -206,8 +217,8 @@ class _FakeSurfaceAndroidViewController_15 extends _i1.SmartFake ); } -class _FakeWebSettings_16 extends _i1.SmartFake implements _i2.WebSettings { - _FakeWebSettings_16( +class _FakeWebSettings_17 extends _i1.SmartFake implements _i2.WebSettings { + _FakeWebSettings_17( Object parent, Invocation parentInvocation, ) : super( @@ -216,8 +227,8 @@ class _FakeWebSettings_16 extends _i1.SmartFake implements _i2.WebSettings { ); } -class _FakeWebStorage_17 extends _i1.SmartFake implements _i2.WebStorage { - _FakeWebStorage_17( +class _FakeWebStorage_18 extends _i1.SmartFake implements _i2.WebStorage { + _FakeWebStorage_18( Object parent, Invocation parentInvocation, ) : super( @@ -353,6 +364,16 @@ class MockAndroidNavigationDelegate extends _i1.Mock returnValue: _i9.Future.value(), returnValueForMissingStub: _i9.Future.value(), ) as _i9.Future); + @override + _i9.Future setOnHttpError(_i3.HttpResponseErrorCallback? onHttpError) => + (super.noSuchMethod( + Invocation.method( + #setOnHttpError, + [onHttpError], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); } /// A class which mocks [AndroidWebViewController]. @@ -686,6 +707,18 @@ class MockAndroidWebViewController extends _i1.Mock returnValue: _i9.Future.value(), returnValueForMissingStub: _i9.Future.value(), ) as _i9.Future); + @override + _i9.Future setOnPlatformPermissionRequest( + void Function(_i3.PlatformWebViewPermissionRequest)? + onPermissionRequest) => + (super.noSuchMethod( + Invocation.method( + #setOnPlatformPermissionRequest, + [onPermissionRequest], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); } /// A class which mocks [AndroidWebViewProxy]. @@ -707,6 +740,10 @@ class MockAndroidWebViewProxy extends _i1.Mock ) as _i2.WebView Function()); @override _i2.WebChromeClient Function({ + void Function( + _i2.WebChromeClient, + _i2.PermissionRequest, + )? onPermissionRequest, void Function( _i2.WebView, int, @@ -718,6 +755,10 @@ class MockAndroidWebViewProxy extends _i1.Mock }) get createAndroidWebChromeClient => (super.noSuchMethod( Invocation.getter(#createAndroidWebChromeClient), returnValue: ({ + void Function( + _i2.WebChromeClient, + _i2.PermissionRequest, + )? onPermissionRequest, void Function( _i2.WebView, int, @@ -732,6 +773,10 @@ class MockAndroidWebViewProxy extends _i1.Mock Invocation.getter(#createAndroidWebChromeClient), ), returnValueForMissingStub: ({ + void Function( + _i2.WebChromeClient, + _i2.PermissionRequest, + )? onPermissionRequest, void Function( _i2.WebView, int, @@ -746,6 +791,10 @@ class MockAndroidWebViewProxy extends _i1.Mock Invocation.getter(#createAndroidWebChromeClient), ), ) as _i2.WebChromeClient Function({ + void Function( + _i2.WebChromeClient, + _i2.PermissionRequest, + )? onPermissionRequest, void Function( _i2.WebView, int, @@ -1297,6 +1346,57 @@ class MockJavaScriptChannel extends _i1.Mock implements _i2.JavaScriptChannel { ) as _i2.JavaScriptChannel); } +/// A class which mocks [PermissionRequest]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPermissionRequest extends _i1.Mock implements _i2.PermissionRequest { + @override + List get resources => (super.noSuchMethod( + Invocation.getter(#resources), + returnValue: [], + returnValueForMissingStub: [], + ) as List); + @override + _i9.Future grant(List? resources) => (super.noSuchMethod( + Invocation.method( + #grant, + [resources], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future deny() => (super.noSuchMethod( + Invocation.method( + #deny, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i2.PermissionRequest copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakePermissionRequest_14( + this, + Invocation.method( + #copy, + [], + ), + ), + returnValueForMissingStub: _FakePermissionRequest_14( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.PermissionRequest); +} + /// A class which mocks [PlatformViewsServiceProxy]. /// /// See the documentation for Mockito's code generation for more information. @@ -1325,7 +1425,7 @@ class MockPlatformViewsServiceProxy extends _i1.Mock #onFocus: onFocus, }, ), - returnValue: _FakeExpensiveAndroidViewController_14( + returnValue: _FakeExpensiveAndroidViewController_15( this, Invocation.method( #initExpensiveAndroidView, @@ -1340,7 +1440,7 @@ class MockPlatformViewsServiceProxy extends _i1.Mock }, ), ), - returnValueForMissingStub: _FakeExpensiveAndroidViewController_14( + returnValueForMissingStub: _FakeExpensiveAndroidViewController_15( this, Invocation.method( #initExpensiveAndroidView, @@ -1378,7 +1478,7 @@ class MockPlatformViewsServiceProxy extends _i1.Mock #onFocus: onFocus, }, ), - returnValue: _FakeSurfaceAndroidViewController_15( + returnValue: _FakeSurfaceAndroidViewController_16( this, Invocation.method( #initSurfaceAndroidView, @@ -1393,7 +1493,7 @@ class MockPlatformViewsServiceProxy extends _i1.Mock }, ), ), - returnValueForMissingStub: _FakeSurfaceAndroidViewController_15( + returnValueForMissingStub: _FakeSurfaceAndroidViewController_16( this, Invocation.method( #initSurfaceAndroidView, @@ -1753,14 +1853,14 @@ class MockWebSettings extends _i1.Mock implements _i2.WebSettings { #copy, [], ), - returnValue: _FakeWebSettings_16( + returnValue: _FakeWebSettings_17( this, Invocation.method( #copy, [], ), ), - returnValueForMissingStub: _FakeWebSettings_16( + returnValueForMissingStub: _FakeWebSettings_17( this, Invocation.method( #copy, @@ -1777,11 +1877,11 @@ class MockWebView extends _i1.Mock implements _i2.WebView { @override _i2.WebSettings get settings => (super.noSuchMethod( Invocation.getter(#settings), - returnValue: _FakeWebSettings_16( + returnValue: _FakeWebSettings_17( this, Invocation.getter(#settings), ), - returnValueForMissingStub: _FakeWebSettings_16( + returnValueForMissingStub: _FakeWebSettings_17( this, Invocation.getter(#settings), ), @@ -2154,14 +2254,14 @@ class MockWebStorage extends _i1.Mock implements _i2.WebStorage { #copy, [], ), - returnValue: _FakeWebStorage_17( + returnValue: _FakeWebStorage_18( this, Invocation.method( #copy, [], ), ), - returnValueForMissingStub: _FakeWebStorage_17( + returnValueForMissingStub: _FakeWebStorage_18( this, Invocation.method( #copy, diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.dart index 9e7422fba88..8e2be2ba48d 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.dart @@ -8,7 +8,7 @@ import 'package:mockito/mockito.dart'; import 'package:webview_flutter_android/src/android_webview.dart' as android_webview; import 'package:webview_flutter_android/webview_flutter_android.dart'; -import 'package:webview_flutter_platform_interface/src/webview_platform.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'android_webview_cookie_manager_test.mocks.dart'; 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 71c30b6e903..73b54caabc1 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 @@ -27,6 +27,7 @@ import 'test_android_webview.g.dart'; TestWebViewClientHostApi, TestWebViewHostApi, TestAssetManagerHostApi, + TestPermissionRequestHostApi, WebChromeClient, WebView, WebViewClient, @@ -970,6 +971,51 @@ void main() { ); }); + test('onPermissionRequest', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + const int instanceIdentifier = 0; + late final List callbackParameters; + final WebChromeClient instance = WebChromeClient.detached( + onPermissionRequest: ( + WebChromeClient instance, + PermissionRequest request, + ) { + callbackParameters = [ + instance, + request, + ]; + }, + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance(instance, instanceIdentifier); + + final WebChromeClientFlutterApiImpl flutterApi = + WebChromeClientFlutterApiImpl( + instanceManager: instanceManager, + ); + + final PermissionRequest request = PermissionRequest.detached( + resources: [], + binaryMessenger: null, + instanceManager: instanceManager, + ); + const int requestIdentifier = 32; + instanceManager.addHostCreatedInstance( + request, + requestIdentifier, + ); + + flutterApi.onPermissionRequest( + instanceIdentifier, + requestIdentifier, + ); + + expect(callbackParameters, [instance, request]); + }); + test('copy', () { expect(WebChromeClient.detached().copy(), isA()); }); @@ -1048,4 +1094,81 @@ void main() { expect(WebStorage.detached().copy(), isA()); }); }); + + group('PermissionRequest', () { + setUp(() {}); + + tearDown(() { + TestPermissionRequestHostApi.setup(null); + }); + + test('grant', () async { + final MockTestPermissionRequestHostApi mockApi = + MockTestPermissionRequestHostApi(); + TestPermissionRequestHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final PermissionRequest instance = PermissionRequest.detached( + resources: [], + binaryMessenger: null, + instanceManager: instanceManager, + ); + const int instanceIdentifier = 0; + instanceManager.addHostCreatedInstance(instance, instanceIdentifier); + + const List resources = [PermissionRequest.audioCapture]; + + await instance.grant(resources); + + verify(mockApi.grant( + instanceIdentifier, + resources, + )); + }); + + test('deny', () async { + final MockTestPermissionRequestHostApi mockApi = + MockTestPermissionRequestHostApi(); + TestPermissionRequestHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final PermissionRequest instance = PermissionRequest.detached( + resources: [], + binaryMessenger: null, + instanceManager: instanceManager, + ); + const int instanceIdentifier = 0; + instanceManager.addHostCreatedInstance(instance, instanceIdentifier); + + await instance.deny(); + + verify(mockApi.deny(instanceIdentifier)); + }); + + test('FlutterAPI create', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final PermissionRequestFlutterApiImpl api = + PermissionRequestFlutterApiImpl( + instanceManager: instanceManager, + ); + + const int instanceIdentifier = 0; + + api.create(instanceIdentifier, []); + + expect( + instanceManager.getInstanceWithWeakReference(instanceIdentifier), + isA(), + ); + }); + }); } 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 1130bc2ea59..58448c063de 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 @@ -986,6 +986,40 @@ class MockTestAssetManagerHostApi extends _i1.Mock ) as String); } +/// A class which mocks [TestPermissionRequestHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestPermissionRequestHostApi extends _i1.Mock + implements _i6.TestPermissionRequestHostApi { + MockTestPermissionRequestHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void grant( + int? instanceId, + List? resources, + ) => + super.noSuchMethod( + Invocation.method( + #grant, + [ + instanceId, + resources, + ], + ), + returnValueForMissingStub: null, + ); + @override + void deny(int? instanceId) => super.noSuchMethod( + Invocation.method( + #deny, + [instanceId], + ), + returnValueForMissingStub: null, + ); +} + /// A class which mocks [WebChromeClient]. /// /// See the documentation for Mockito's code generation for more information. 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 6b0c31ab1c1..fca0b96dd87 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 @@ -1513,3 +1513,74 @@ abstract class TestWebStorageHostApi { } } } + +/// Host API for `PermissionRequest`. +/// +/// 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/PermissionRequest. +abstract class TestPermissionRequestHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = StandardMessageCodec(); + + /// Handles Dart method `PermissionRequest.grant`. + void grant(int instanceId, List resources); + + /// Handles Dart method `PermissionRequest.deny`. + void deny(int instanceId); + + static void setup(TestPermissionRequestHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PermissionRequestHostApi.grant', 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.PermissionRequestHostApi.grant 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.PermissionRequestHostApi.grant was null, expected non-null int.'); + final List? arg_resources = + (args[1] as List?)?.cast(); + assert(arg_resources != null, + 'Argument for dev.flutter.pigeon.PermissionRequestHostApi.grant was null, expected non-null List.'); + api.grant(arg_instanceId!, arg_resources!); + return []; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PermissionRequestHostApi.deny', 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.PermissionRequestHostApi.deny 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.PermissionRequestHostApi.deny was null, expected non-null int.'); + api.deny(arg_instanceId!); + return []; + }); + } + } + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index dc83b548e65..1041b121f48 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.4.0 + +* Adds support for `PlatformWebViewController.setOnPlatformPermissionRequest`. + ## 3.3.0 * Adds support for `PlatformNavigationDelegate.onUrlChange`. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/README.md b/packages/webview_flutter/webview_flutter_wkwebview/README.md index 7eec1637648..3cbec14339e 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/README.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/README.md @@ -34,12 +34,12 @@ Then you will have access to the native class `FWFWebViewFlutterWKWebViewExterna This package uses [pigeon][3] to generate the communication layer between Flutter and the host platform (iOS). The communication interface is defined in the `pigeons/web_kit.dart` file. After editing the communication interface regenerate the communication layer by running -`flutter pub run pigeon --input pigeons/web_kit.dart`. +`dart run pigeon --input pigeons/web_kit.dart`. Besides [pigeon][3] this package also uses [mockito][4] to generate mock objects for testing purposes. To generate the mock objects run the following command: ```bash -flutter pub run build_runner build --delete-conflicting-outputs +dart run build_runner build --delete-conflicting-outputs ``` If you would like to contribute to the plugin, check out our [contribution guide][5]. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist index 6ee44fd0e2f..35368a316f1 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist @@ -45,5 +45,7 @@ UIApplicationSupportsIndirectInputEvents + NSCameraUsageDescription + If you want to use the camera, you have to give permission. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFDataConvertersTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFDataConvertersTests.m index 63e13f9e8ec..82bf99c9a7b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFDataConvertersTests.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFDataConvertersTests.m @@ -13,7 +13,7 @@ @interface FWFDataConvertersTests : XCTestCase @implementation FWFDataConvertersTests - (void)testFWFNSURLRequestFromRequestData { - NSURLRequest *request = FWFNSURLRequestFromRequestData([FWFNSUrlRequestData + NSURLRequest *request = FWFNativeNSURLRequestFromRequestData([FWFNSUrlRequestData makeWithUrl:@"https://flutter.dev" httpMethod:@"post" httpBody:[FlutterStandardTypedData typedDataWithBytes:[NSData data]] @@ -27,16 +27,16 @@ - (void)testFWFNSURLRequestFromRequestData { - (void)testFWFNSURLRequestFromRequestDataDoesNotOverrideDefaultValuesWithNull { NSURLRequest *request = - FWFNSURLRequestFromRequestData([FWFNSUrlRequestData makeWithUrl:@"https://flutter.dev" - httpMethod:nil - httpBody:nil - allHttpHeaderFields:@{}]); + FWFNativeNSURLRequestFromRequestData([FWFNSUrlRequestData makeWithUrl:@"https://flutter.dev" + httpMethod:nil + httpBody:nil + allHttpHeaderFields:@{}]); XCTAssertEqualObjects(request.HTTPMethod, @"GET"); } - (void)testFWFNSHTTPCookieFromCookieData { - NSHTTPCookie *cookie = FWFNSHTTPCookieFromCookieData([FWFNSHttpCookieData + NSHTTPCookie *cookie = FWFNativeNSHTTPCookieFromCookieData([FWFNSHttpCookieData makeWithPropertyKeys:@[ [FWFNSHttpCookiePropertyKeyEnumData makeWithValue:FWFNSHttpCookiePropertyKeyEnumName] ] propertyValues:@[ @"cookieName" ]]); @@ -45,7 +45,7 @@ - (void)testFWFNSHTTPCookieFromCookieData { } - (void)testFWFWKUserScriptFromScriptData { - WKUserScript *userScript = FWFWKUserScriptFromScriptData([FWFWKUserScriptData + WKUserScript *userScript = FWFNativeWKUserScriptFromScriptData([FWFWKUserScriptData makeWithSource:@"mySource" injectionTime:[FWFWKUserScriptInjectionTimeEnumData makeWithValue:FWFWKUserScriptInjectionTimeEnumAtDocumentStart] @@ -70,7 +70,7 @@ - (void)testFWFWKNavigationActionDataFromNavigationAction { OCMStub([mockNavigationAction targetFrame]).andReturn(mockFrameInfo); FWFWKNavigationActionData *data = - FWFWKNavigationActionDataFromNavigationAction(mockNavigationAction); + FWFWKNavigationActionDataFromNativeWKNavigationAction(mockNavigationAction); XCTAssertNotNil(data); XCTAssertEqual(data.navigationType, FWFWKNavigationTypeReload); } @@ -82,7 +82,7 @@ - (void)testFWFNSUrlRequestDataFromNSURLRequest { request.HTTPBody = [@"aString" dataUsingEncoding:NSUTF8StringEncoding]; request.allHTTPHeaderFields = @{@"a" : @"field"}; - FWFNSUrlRequestData *data = FWFNSUrlRequestDataFromNSURLRequest(request); + FWFNSUrlRequestData *data = FWFNSUrlRequestDataFromNativeNSURLRequest(request); XCTAssertEqualObjects(data.url, @"https://www.flutter.dev/"); XCTAssertEqualObjects(data.httpMethod, @"POST"); XCTAssertEqualObjects(data.httpBody.data, [@"aString" dataUsingEncoding:NSUTF8StringEncoding]); @@ -93,7 +93,7 @@ - (void)testFWFWKFrameInfoDataFromWKFrameInfo { WKFrameInfo *mockFrameInfo = OCMClassMock([WKFrameInfo class]); OCMStub([mockFrameInfo isMainFrame]).andReturn(YES); - FWFWKFrameInfoData *targetFrameData = FWFWKFrameInfoDataFromWKFrameInfo(mockFrameInfo); + FWFWKFrameInfoData *targetFrameData = FWFWKFrameInfoDataFromNativeWKFrameInfo(mockFrameInfo); XCTAssertEqualObjects(targetFrameData.isMainFrame, @YES); } @@ -102,7 +102,7 @@ - (void)testFWFNSErrorDataFromNSError { code:23 userInfo:@{NSLocalizedDescriptionKey : @"description"}]; - FWFNSErrorData *data = FWFNSErrorDataFromNSError(error); + FWFNSErrorData *data = FWFNSErrorDataFromNativeNSError(error); XCTAssertEqualObjects(data.code, @23); XCTAssertEqualObjects(data.domain, @"domain"); XCTAssertEqualObjects(data.localizedDescription, @"description"); @@ -113,8 +113,46 @@ - (void)testFWFWKScriptMessageDataFromWKScriptMessage { OCMStub([mockScriptMessage name]).andReturn(@"name"); OCMStub([mockScriptMessage body]).andReturn(@"message"); - FWFWKScriptMessageData *data = FWFWKScriptMessageDataFromWKScriptMessage(mockScriptMessage); + FWFWKScriptMessageData *data = FWFWKScriptMessageDataFromNativeWKScriptMessage(mockScriptMessage); XCTAssertEqualObjects(data.name, @"name"); XCTAssertEqualObjects(data.body, @"message"); } + +- (void)testFWFWKSecurityOriginDataFromWKSecurityOrigin { + WKSecurityOrigin *mockSecurityOrigin = OCMClassMock([WKSecurityOrigin class]); + OCMStub([mockSecurityOrigin host]).andReturn(@"host"); + OCMStub([mockSecurityOrigin port]).andReturn(2); + OCMStub([mockSecurityOrigin protocol]).andReturn(@"protocol"); + + FWFWKSecurityOriginData *data = + FWFWKSecurityOriginDataFromNativeWKSecurityOrigin(mockSecurityOrigin); + XCTAssertEqualObjects(data.host, @"host"); + XCTAssertEqualObjects(data.port, @(2)); + XCTAssertEqualObjects(data.protocol, @"protocol"); +} + +- (void)testFWFWKPermissionDecisionFromData API_AVAILABLE(ios(15.0)) { + XCTAssertEqual(FWFNativeWKPermissionDecisionFromData( + [FWFWKPermissionDecisionData makeWithValue:FWFWKPermissionDecisionDeny]), + WKPermissionDecisionDeny); + XCTAssertEqual(FWFNativeWKPermissionDecisionFromData( + [FWFWKPermissionDecisionData makeWithValue:FWFWKPermissionDecisionGrant]), + WKPermissionDecisionGrant); + XCTAssertEqual(FWFNativeWKPermissionDecisionFromData( + [FWFWKPermissionDecisionData makeWithValue:FWFWKPermissionDecisionPrompt]), + WKPermissionDecisionPrompt); +} + +- (void)testFWFWKMediaCaptureTypeDataFromWKMediaCaptureType API_AVAILABLE(ios(15.0)) { + XCTAssertEqual( + FWFWKMediaCaptureTypeDataFromNativeWKMediaCaptureType(WKMediaCaptureTypeCamera).value, + FWFWKMediaCaptureTypeCamera); + XCTAssertEqual( + FWFWKMediaCaptureTypeDataFromNativeWKMediaCaptureType(WKMediaCaptureTypeMicrophone).value, + FWFWKMediaCaptureTypeMicrophone); + XCTAssertEqual( + FWFWKMediaCaptureTypeDataFromNativeWKMediaCaptureType(WKMediaCaptureTypeCameraAndMicrophone) + .value, + FWFWKMediaCaptureTypeCameraAndMicrophone); +} @end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFUIDelegateHostApiTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFUIDelegateHostApiTests.m index 939c14873fa..72366762b7f 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFUIDelegateHostApiTests.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFUIDelegateHostApiTests.m @@ -97,4 +97,45 @@ - (void)testOnCreateWebViewForDelegateWithIdentifier { isKindOfClass:[FWFWKNavigationActionData class]] completion:OCMOCK_ANY]); } + +- (void)testRequestMediaCapturePermissionForOrigin API_AVAILABLE(ios(15.0)) { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + + FWFUIDelegate *mockDelegate = [self mockDelegateWithManager:instanceManager identifier:0]; + FWFUIDelegateFlutterApiImpl *mockFlutterAPI = [self mockFlutterApiWithManager:instanceManager]; + + OCMStub([mockDelegate UIDelegateAPI]).andReturn(mockFlutterAPI); + + WKWebView *mockWebView = OCMClassMock([WKWebView class]); + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:1]; + + WKSecurityOrigin *mockSecurityOrigin = OCMClassMock([WKSecurityOrigin class]); + OCMStub([mockSecurityOrigin host]).andReturn(@""); + OCMStub([mockSecurityOrigin port]).andReturn(0); + OCMStub([mockSecurityOrigin protocol]).andReturn(@""); + + WKFrameInfo *mockFrameInfo = OCMClassMock([WKFrameInfo class]); + OCMStub([mockFrameInfo isMainFrame]).andReturn(YES); + + [mockDelegate webView:mockWebView + requestMediaCapturePermissionForOrigin:mockSecurityOrigin + initiatedByFrame:mockFrameInfo + type:WKMediaCaptureTypeMicrophone + decisionHandler:^(WKPermissionDecision decision){ + }]; + + OCMVerify([mockFlutterAPI + requestMediaCapturePermissionForDelegateWithIdentifier:@0 + webViewIdentifier:@1 + origin:[OCMArg isKindOfClass: + [FWFWKSecurityOriginData + class]] + frame:[OCMArg + isKindOfClass:[FWFWKFrameInfoData + class]] + type:[OCMArg isKindOfClass: + [FWFWKMediaCaptureTypeData + class]] + completion:OCMOCK_ANY]); +} @end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart index a793c2ca00e..29c42969b47 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart @@ -137,6 +137,14 @@ Page resource error: ); }, )) + ..setOnPlatformPermissionRequest( + (PlatformWebViewPermissionRequest request) { + debugPrint( + 'requesting permissions for ${request.types.map((WebViewPermissionResourceType type) => type.name)}', + ); + request.grant(); + }, + ) ..loadRequest(LoadRequestParams( uri: Uri.parse('https://flutter.dev'), )); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml index b1ee0822395..a26bce6dc7b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml @@ -34,3 +34,9 @@ flutter: - assets/sample_video.mp4 - assets/www/index.html - assets/www/styles/style.css + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + webview_flutter_platform_interface: + path: ../../../webview_flutter/webview_flutter_platform_interface diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h index d2e9bdd71ce..d66767b943a 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h @@ -15,7 +15,7 @@ NS_ASSUME_NONNULL_BEGIN * * @return An NSURLRequest or nil if data could not be converted. */ -extern NSURLRequest *_Nullable FWFNSURLRequestFromRequestData(FWFNSUrlRequestData *data); +extern NSURLRequest *_Nullable FWFNativeNSURLRequestFromRequestData(FWFNSUrlRequestData *data); /** * Converts an FWFNSHttpCookieData to an NSHTTPCookie. @@ -24,7 +24,7 @@ extern NSURLRequest *_Nullable FWFNSURLRequestFromRequestData(FWFNSUrlRequestDat * * @return An NSHTTPCookie or nil if data could not be converted. */ -extern NSHTTPCookie *_Nullable FWFNSHTTPCookieFromCookieData(FWFNSHttpCookieData *data); +extern NSHTTPCookie *_Nullable FWFNativeNSHTTPCookieFromCookieData(FWFNSHttpCookieData *data); /** * Converts an FWFNSKeyValueObservingOptionsEnumData to an NSKeyValueObservingOptions. @@ -33,7 +33,7 @@ extern NSHTTPCookie *_Nullable FWFNSHTTPCookieFromCookieData(FWFNSHttpCookieData * * @return An NSKeyValueObservingOptions or -1 if data could not be converted. */ -extern NSKeyValueObservingOptions FWFNSKeyValueObservingOptionsFromEnumData( +extern NSKeyValueObservingOptions FWFNativeNSKeyValueObservingOptionsFromEnumData( FWFNSKeyValueObservingOptionsEnumData *data); /** @@ -43,7 +43,7 @@ extern NSKeyValueObservingOptions FWFNSKeyValueObservingOptionsFromEnumData( * * @return An NSHttpCookiePropertyKey or nil if data could not be converted. */ -extern NSHTTPCookiePropertyKey _Nullable FWFNSHTTPCookiePropertyKeyFromEnumData( +extern NSHTTPCookiePropertyKey _Nullable FWFNativeNSHTTPCookiePropertyKeyFromEnumData( FWFNSHttpCookiePropertyKeyEnumData *data); /** @@ -53,7 +53,7 @@ extern NSHTTPCookiePropertyKey _Nullable FWFNSHTTPCookiePropertyKeyFromEnumData( * * @return A WKUserScript or nil if data could not be converted. */ -extern WKUserScript *FWFWKUserScriptFromScriptData(FWFWKUserScriptData *data); +extern WKUserScript *FWFNativeWKUserScriptFromScriptData(FWFWKUserScriptData *data); /** * Converts an FWFWKUserScriptInjectionTimeEnumData to a WKUserScriptInjectionTime. @@ -62,7 +62,7 @@ extern WKUserScript *FWFWKUserScriptFromScriptData(FWFWKUserScriptData *data); * * @return A WKUserScriptInjectionTime or -1 if data could not be converted. */ -extern WKUserScriptInjectionTime FWFWKUserScriptInjectionTimeFromEnumData( +extern WKUserScriptInjectionTime FWFNativeWKUserScriptInjectionTimeFromEnumData( FWFWKUserScriptInjectionTimeEnumData *data); /** @@ -72,7 +72,7 @@ extern WKUserScriptInjectionTime FWFWKUserScriptInjectionTimeFromEnumData( * * @return A WKAudiovisualMediaType or -1 if data could not be converted. */ -extern WKAudiovisualMediaTypes FWFWKAudiovisualMediaTypeFromEnumData( +extern WKAudiovisualMediaTypes FWFNativeWKAudiovisualMediaTypeFromEnumData( FWFWKAudiovisualMediaTypeEnumData *data); /** @@ -82,7 +82,8 @@ extern WKAudiovisualMediaTypes FWFWKAudiovisualMediaTypeFromEnumData( * * @return A WKWebsiteDataType or nil if data could not be converted. */ -extern NSString *_Nullable FWFWKWebsiteDataTypeFromEnumData(FWFWKWebsiteDataTypeEnumData *data); +extern NSString *_Nullable FWFNativeWKWebsiteDataTypeFromEnumData( + FWFWKWebsiteDataTypeEnumData *data); /** * Converts a WKNavigationAction to an FWFWKNavigationActionData. @@ -91,7 +92,7 @@ extern NSString *_Nullable FWFWKWebsiteDataTypeFromEnumData(FWFWKWebsiteDataType * * @return A FWFWKNavigationActionData. */ -extern FWFWKNavigationActionData *FWFWKNavigationActionDataFromNavigationAction( +extern FWFWKNavigationActionData *FWFWKNavigationActionDataFromNativeWKNavigationAction( WKNavigationAction *action); /** @@ -101,7 +102,7 @@ extern FWFWKNavigationActionData *FWFWKNavigationActionDataFromNavigationAction( * * @return A FWFNSUrlRequestData. */ -extern FWFNSUrlRequestData *FWFNSUrlRequestDataFromNSURLRequest(NSURLRequest *request); +extern FWFNSUrlRequestData *FWFNSUrlRequestDataFromNativeNSURLRequest(NSURLRequest *request); /** * Converts a WKFrameInfo to an FWFWKFrameInfoData. @@ -110,7 +111,7 @@ extern FWFNSUrlRequestData *FWFNSUrlRequestDataFromNSURLRequest(NSURLRequest *re * * @return A FWFWKFrameInfoData. */ -extern FWFWKFrameInfoData *FWFWKFrameInfoDataFromWKFrameInfo(WKFrameInfo *info); +extern FWFWKFrameInfoData *FWFWKFrameInfoDataFromNativeWKFrameInfo(WKFrameInfo *info); /** * Converts an FWFWKNavigationActionPolicyEnumData to a WKNavigationActionPolicy. @@ -119,7 +120,7 @@ extern FWFWKFrameInfoData *FWFWKFrameInfoDataFromWKFrameInfo(WKFrameInfo *info); * * @return A WKNavigationActionPolicy or -1 if data could not be converted. */ -extern WKNavigationActionPolicy FWFWKNavigationActionPolicyFromEnumData( +extern WKNavigationActionPolicy FWFNativeWKNavigationActionPolicyFromEnumData( FWFWKNavigationActionPolicyEnumData *data); /** @@ -129,7 +130,7 @@ extern WKNavigationActionPolicy FWFWKNavigationActionPolicyFromEnumData( * * @return A FWFNSErrorData. */ -extern FWFNSErrorData *FWFNSErrorDataFromNSError(NSError *error); +extern FWFNSErrorData *FWFNSErrorDataFromNativeNSError(NSError *error); /** * Converts an NSKeyValueChangeKey to a FWFNSKeyValueChangeKeyEnumData. @@ -138,7 +139,7 @@ extern FWFNSErrorData *FWFNSErrorDataFromNSError(NSError *error); * * @return A FWFNSKeyValueChangeKeyEnumData or nil if data could not be converted. */ -extern FWFNSKeyValueChangeKeyEnumData *FWFNSKeyValueChangeKeyEnumDataFromNSKeyValueChangeKey( +extern FWFNSKeyValueChangeKeyEnumData *FWFNSKeyValueChangeKeyEnumDataFromNativeNSKeyValueChangeKey( NSKeyValueChangeKey key); /** @@ -148,7 +149,8 @@ extern FWFNSKeyValueChangeKeyEnumData *FWFNSKeyValueChangeKeyEnumDataFromNSKeyVa * * @return A FWFWKScriptMessageData. */ -extern FWFWKScriptMessageData *FWFWKScriptMessageDataFromWKScriptMessage(WKScriptMessage *message); +extern FWFWKScriptMessageData *FWFWKScriptMessageDataFromNativeWKScriptMessage( + WKScriptMessage *message); /** * Converts a WKNavigationType to an FWFWKNavigationType. @@ -157,6 +159,38 @@ extern FWFWKScriptMessageData *FWFWKScriptMessageDataFromWKScriptMessage(WKScrip * * @return A FWFWKNavigationType. */ -extern FWFWKNavigationType FWFWKNavigationTypeFromWKNavigationType(WKNavigationType type); +extern FWFWKNavigationType FWFWKNavigationTypeFromNativeWKNavigationType(WKNavigationType type); + +/** + * Converts a WKSecurityOrigin to an FWFWKSecurityOriginData. + * + * @param origin The object containing information to create an FWFWKSecurityOriginData. + * + * @return An FWFWKSecurityOriginData. + */ +extern FWFWKSecurityOriginData *FWFWKSecurityOriginDataFromNativeWKSecurityOrigin( + WKSecurityOrigin *origin); + +/** + * Converts an FWFWKPermissionDecisionData to a WKPermissionDecision. + * + * @param data The data object containing information to create a WKPermissionDecision. + * + * @return A WKPermissionDecision or -1 if data could not be converted. + */ +API_AVAILABLE(ios(15.0)) +extern WKPermissionDecision FWFNativeWKPermissionDecisionFromData( + FWFWKPermissionDecisionData *data); + +/** + * Converts an WKMediaCaptureType to a FWFWKMediaCaptureTypeData. + * + * @param type The data object containing information to create a FWFWKMediaCaptureTypeData. + * + * @return A FWFWKMediaCaptureTypeData or nil if data could not be converted. + */ +API_AVAILABLE(ios(15.0)) +extern FWFWKMediaCaptureTypeData *FWFWKMediaCaptureTypeDataFromNativeWKMediaCaptureType( + WKMediaCaptureType type); NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m index 5fbbf2e21a7..8b63d8389fb 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m @@ -6,7 +6,7 @@ #import -NSURLRequest *_Nullable FWFNSURLRequestFromRequestData(FWFNSUrlRequestData *data) { +NSURLRequest *_Nullable FWFNativeNSURLRequestFromRequestData(FWFNSUrlRequestData *data) { NSURL *url = [NSURL URLWithString:data.url]; if (!url) { return nil; @@ -28,11 +28,11 @@ return request; } -extern NSHTTPCookie *_Nullable FWFNSHTTPCookieFromCookieData(FWFNSHttpCookieData *data) { +extern NSHTTPCookie *_Nullable FWFNativeNSHTTPCookieFromCookieData(FWFNSHttpCookieData *data) { NSMutableDictionary *properties = [NSMutableDictionary dictionary]; for (int i = 0; i < data.propertyKeys.count; i++) { NSHTTPCookiePropertyKey cookieKey = - FWFNSHTTPCookiePropertyKeyFromEnumData(data.propertyKeys[i]); + FWFNativeNSHTTPCookiePropertyKeyFromEnumData(data.propertyKeys[i]); if (!cookieKey) { // Some keys aren't supported on all versions, so this ignores keys // that require a higher version or are unsupported. @@ -43,7 +43,7 @@ return [NSHTTPCookie cookieWithProperties:properties]; } -NSKeyValueObservingOptions FWFNSKeyValueObservingOptionsFromEnumData( +NSKeyValueObservingOptions FWFNativeNSKeyValueObservingOptionsFromEnumData( FWFNSKeyValueObservingOptionsEnumData *data) { switch (data.value) { case FWFNSKeyValueObservingOptionsEnumNewValue: @@ -59,7 +59,7 @@ NSKeyValueObservingOptions FWFNSKeyValueObservingOptionsFromEnumData( return -1; } -NSHTTPCookiePropertyKey _Nullable FWFNSHTTPCookiePropertyKeyFromEnumData( +NSHTTPCookiePropertyKey _Nullable FWFNativeNSHTTPCookiePropertyKeyFromEnumData( FWFNSHttpCookiePropertyKeyEnumData *data) { switch (data.value) { case FWFNSHttpCookiePropertyKeyEnumComment: @@ -99,14 +99,14 @@ NSHTTPCookiePropertyKey _Nullable FWFNSHTTPCookiePropertyKeyFromEnumData( return nil; } -extern WKUserScript *FWFWKUserScriptFromScriptData(FWFWKUserScriptData *data) { +extern WKUserScript *FWFNativeWKUserScriptFromScriptData(FWFWKUserScriptData *data) { return [[WKUserScript alloc] initWithSource:data.source - injectionTime:FWFWKUserScriptInjectionTimeFromEnumData(data.injectionTime) + injectionTime:FWFNativeWKUserScriptInjectionTimeFromEnumData(data.injectionTime) forMainFrameOnly:data.isMainFrameOnly.boolValue]; } -WKUserScriptInjectionTime FWFWKUserScriptInjectionTimeFromEnumData( +WKUserScriptInjectionTime FWFNativeWKUserScriptInjectionTimeFromEnumData( FWFWKUserScriptInjectionTimeEnumData *data) { switch (data.value) { case FWFWKUserScriptInjectionTimeEnumAtDocumentStart: @@ -118,7 +118,7 @@ WKUserScriptInjectionTime FWFWKUserScriptInjectionTimeFromEnumData( return -1; } -WKAudiovisualMediaTypes FWFWKAudiovisualMediaTypeFromEnumData( +WKAudiovisualMediaTypes FWFNativeWKAudiovisualMediaTypeFromEnumData( FWFWKAudiovisualMediaTypeEnumData *data) { switch (data.value) { case FWFWKAudiovisualMediaTypeEnumNone: @@ -134,7 +134,7 @@ WKAudiovisualMediaTypes FWFWKAudiovisualMediaTypeFromEnumData( return -1; } -NSString *_Nullable FWFWKWebsiteDataTypeFromEnumData(FWFWKWebsiteDataTypeEnumData *data) { +NSString *_Nullable FWFNativeWKWebsiteDataTypeFromEnumData(FWFWKWebsiteDataTypeEnumData *data) { switch (data.value) { case FWFWKWebsiteDataTypeEnumCookies: return WKWebsiteDataTypeCookies; @@ -157,15 +157,15 @@ WKAudiovisualMediaTypes FWFWKAudiovisualMediaTypeFromEnumData( return nil; } -FWFWKNavigationActionData *FWFWKNavigationActionDataFromNavigationAction( +FWFWKNavigationActionData *FWFWKNavigationActionDataFromNativeWKNavigationAction( WKNavigationAction *action) { return [FWFWKNavigationActionData - makeWithRequest:FWFNSUrlRequestDataFromNSURLRequest(action.request) - targetFrame:FWFWKFrameInfoDataFromWKFrameInfo(action.targetFrame) - navigationType:FWFWKNavigationTypeFromWKNavigationType(action.navigationType)]; + makeWithRequest:FWFNSUrlRequestDataFromNativeNSURLRequest(action.request) + targetFrame:FWFWKFrameInfoDataFromNativeWKFrameInfo(action.targetFrame) + navigationType:FWFWKNavigationTypeFromNativeWKNavigationType(action.navigationType)]; } -FWFNSUrlRequestData *FWFNSUrlRequestDataFromNSURLRequest(NSURLRequest *request) { +FWFNSUrlRequestData *FWFNSUrlRequestDataFromNativeNSURLRequest(NSURLRequest *request) { return [FWFNSUrlRequestData makeWithUrl:request.URL.absoluteString httpMethod:request.HTTPMethod @@ -175,11 +175,11 @@ WKAudiovisualMediaTypes FWFWKAudiovisualMediaTypeFromEnumData( allHttpHeaderFields:request.allHTTPHeaderFields ? request.allHTTPHeaderFields : @{}]; } -FWFWKFrameInfoData *FWFWKFrameInfoDataFromWKFrameInfo(WKFrameInfo *info) { +FWFWKFrameInfoData *FWFWKFrameInfoDataFromNativeWKFrameInfo(WKFrameInfo *info) { return [FWFWKFrameInfoData makeWithIsMainFrame:@(info.isMainFrame)]; } -WKNavigationActionPolicy FWFWKNavigationActionPolicyFromEnumData( +WKNavigationActionPolicy FWFNativeWKNavigationActionPolicyFromEnumData( FWFWKNavigationActionPolicyEnumData *data) { switch (data.value) { case FWFWKNavigationActionPolicyEnumAllow: @@ -191,13 +191,13 @@ WKNavigationActionPolicy FWFWKNavigationActionPolicyFromEnumData( return -1; } -FWFNSErrorData *FWFNSErrorDataFromNSError(NSError *error) { +FWFNSErrorData *FWFNSErrorDataFromNativeNSError(NSError *error) { return [FWFNSErrorData makeWithCode:@(error.code) domain:error.domain localizedDescription:error.localizedDescription]; } -FWFNSKeyValueChangeKeyEnumData *FWFNSKeyValueChangeKeyEnumDataFromNSKeyValueChangeKey( +FWFNSKeyValueChangeKeyEnumData *FWFNSKeyValueChangeKeyEnumDataFromNativeNSKeyValueChangeKey( NSKeyValueChangeKey key) { if ([key isEqualToString:NSKeyValueChangeIndexesKey]) { return [FWFNSKeyValueChangeKeyEnumData makeWithValue:FWFNSKeyValueChangeKeyEnumIndexes]; @@ -215,11 +215,11 @@ WKNavigationActionPolicy FWFWKNavigationActionPolicyFromEnumData( return nil; } -FWFWKScriptMessageData *FWFWKScriptMessageDataFromWKScriptMessage(WKScriptMessage *message) { +FWFWKScriptMessageData *FWFWKScriptMessageDataFromNativeWKScriptMessage(WKScriptMessage *message) { return [FWFWKScriptMessageData makeWithName:message.name body:message.body]; } -FWFWKNavigationType FWFWKNavigationTypeFromWKNavigationType(WKNavigationType type) { +FWFWKNavigationType FWFWKNavigationTypeFromNativeWKNavigationType(WKNavigationType type) { switch (type) { case WKNavigationTypeLinkActivated: return FWFWKNavigationTypeLinkActivated; @@ -235,3 +235,39 @@ FWFWKNavigationType FWFWKNavigationTypeFromWKNavigationType(WKNavigationType typ return FWFWKNavigationTypeOther; } } + +FWFWKSecurityOriginData *FWFWKSecurityOriginDataFromNativeWKSecurityOrigin( + WKSecurityOrigin *origin) { + return [FWFWKSecurityOriginData makeWithHost:origin.host + port:@(origin.port) + protocol:origin.protocol]; +} + +WKPermissionDecision FWFNativeWKPermissionDecisionFromData(FWFWKPermissionDecisionData *data) { + switch (data.value) { + case FWFWKPermissionDecisionDeny: + return WKPermissionDecisionDeny; + case FWFWKPermissionDecisionGrant: + return WKPermissionDecisionGrant; + case FWFWKPermissionDecisionPrompt: + return WKPermissionDecisionPrompt; + } + + return -1; +} + +FWFWKMediaCaptureTypeData *FWFWKMediaCaptureTypeDataFromNativeWKMediaCaptureType( + WKMediaCaptureType type) { + switch (type) { + case WKMediaCaptureTypeCamera: + return [FWFWKMediaCaptureTypeData makeWithValue:FWFWKMediaCaptureTypeCamera]; + case WKMediaCaptureTypeMicrophone: + return [FWFWKMediaCaptureTypeData makeWithValue:FWFWKMediaCaptureTypeMicrophone]; + case WKMediaCaptureTypeCameraAndMicrophone: + return [FWFWKMediaCaptureTypeData makeWithValue:FWFWKMediaCaptureTypeCameraAndMicrophone]; + default: + return [FWFWKMediaCaptureTypeData makeWithValue:FWFWKMediaCaptureTypeUnknown]; + } + + return nil; +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h index 1eb95ff890a..a5c76018a22 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h @@ -145,6 +145,50 @@ typedef NS_ENUM(NSUInteger, FWFWKNavigationType) { FWFWKNavigationTypeOther = 5, }; +/// Possible permission decisions for device resource access. +/// +/// See https://developer.apple.com/documentation/webkit/wkpermissiondecision?language=objc. +typedef NS_ENUM(NSUInteger, FWFWKPermissionDecision) { + /// Deny permission for the requested resource. + /// + /// See + /// https://developer.apple.com/documentation/webkit/wkpermissiondecision/wkpermissiondecisiondeny?language=objc. + FWFWKPermissionDecisionDeny = 0, + /// Deny permission for the requested resource. + /// + /// See + /// https://developer.apple.com/documentation/webkit/wkpermissiondecision/wkpermissiondecisiongrant?language=objc. + FWFWKPermissionDecisionGrant = 1, + /// Prompt the user for permission for the requested resource. + /// + /// See + /// https://developer.apple.com/documentation/webkit/wkpermissiondecision/wkpermissiondecisionprompt?language=objc. + FWFWKPermissionDecisionPrompt = 2, +}; + +/// List of the types of media devices that can capture audio, video, or both. +/// +/// See https://developer.apple.com/documentation/webkit/wkmediacapturetype?language=objc. +typedef NS_ENUM(NSUInteger, FWFWKMediaCaptureType) { + /// A media device that can capture video. + /// + /// See + /// https://developer.apple.com/documentation/webkit/wkmediacapturetype/wkmediacapturetypecamera?language=objc. + FWFWKMediaCaptureTypeCamera = 0, + /// A media device or devices that can capture audio and video. + /// + /// See + /// https://developer.apple.com/documentation/webkit/wkmediacapturetype/wkmediacapturetypecameraandmicrophone?language=objc. + FWFWKMediaCaptureTypeCameraAndMicrophone = 1, + /// A media device that can capture audio. + /// + /// See + /// https://developer.apple.com/documentation/webkit/wkmediacapturetype/wkmediacapturetypemicrophone?language=objc. + FWFWKMediaCaptureTypeMicrophone = 2, + /// An unknown media device. + FWFWKMediaCaptureTypeUnknown = 3, +}; + @class FWFNSKeyValueObservingOptionsEnumData; @class FWFNSKeyValueChangeKeyEnumData; @class FWFWKUserScriptInjectionTimeEnumData; @@ -152,12 +196,15 @@ typedef NS_ENUM(NSUInteger, FWFWKNavigationType) { @class FWFWKWebsiteDataTypeEnumData; @class FWFWKNavigationActionPolicyEnumData; @class FWFNSHttpCookiePropertyKeyEnumData; +@class FWFWKPermissionDecisionData; +@class FWFWKMediaCaptureTypeData; @class FWFNSUrlRequestData; @class FWFWKUserScriptData; @class FWFWKNavigationActionData; @class FWFWKFrameInfoData; @class FWFNSErrorData; @class FWFWKScriptMessageData; +@class FWFWKSecurityOriginData; @class FWFNSHttpCookieData; @class FWFObjectOrIdentifier; @@ -210,6 +257,20 @@ typedef NS_ENUM(NSUInteger, FWFWKNavigationType) { @property(nonatomic, assign) FWFNSHttpCookiePropertyKeyEnum value; @end +@interface FWFWKPermissionDecisionData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithValue:(FWFWKPermissionDecision)value; +@property(nonatomic, assign) FWFWKPermissionDecision value; +@end + +@interface FWFWKMediaCaptureTypeData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithValue:(FWFWKMediaCaptureType)value; +@property(nonatomic, assign) FWFWKMediaCaptureType value; +@end + /// Mirror of NSURLRequest. /// /// See https://developer.apple.com/documentation/foundation/nsurlrequest?language=objc. @@ -289,6 +350,18 @@ typedef NS_ENUM(NSUInteger, FWFWKNavigationType) { @property(nonatomic, strong) id body; @end +/// Mirror of WKSecurityOrigin. +/// +/// See https://developer.apple.com/documentation/webkit/wksecurityorigin?language=objc. +@interface FWFWKSecurityOriginData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithHost:(NSString *)host port:(NSNumber *)port protocol:(NSString *)protocol; +@property(nonatomic, copy) NSString *host; +@property(nonatomic, strong) NSNumber *port; +@property(nonatomic, copy) NSString *protocol; +@end + /// Mirror of NSHttpCookieData. /// /// See https://developer.apple.com/documentation/foundation/nshttpcookie?language=objc. @@ -696,6 +769,16 @@ NSObject *FWFWKUIDelegateFlutterApiGetCodec(void); configurationIdentifier:(NSNumber *)configurationIdentifier navigationAction:(FWFWKNavigationActionData *)navigationAction completion:(void (^)(FlutterError *_Nullable))completion; +/// Callback to Dart function `WKUIDelegate.requestMediaCapturePermission`. +- (void)requestMediaCapturePermissionForDelegateWithIdentifier:(NSNumber *)identifier + webViewIdentifier:(NSNumber *)webViewIdentifier + origin:(FWFWKSecurityOriginData *)origin + frame:(FWFWKFrameInfoData *)frame + type:(FWFWKMediaCaptureTypeData *)type + completion: + (void (^)( + FWFWKPermissionDecisionData *_Nullable, + FlutterError *_Nullable))completion; @end /// The codec used by FWFWKHttpCookieStoreHostApi. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m index 610bc020b32..3a5dff6a5d5 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m @@ -66,6 +66,18 @@ + (nullable FWFNSHttpCookiePropertyKeyEnumData *)nullableFromList:(NSArray *)lis - (NSArray *)toList; @end +@interface FWFWKPermissionDecisionData () ++ (FWFWKPermissionDecisionData *)fromList:(NSArray *)list; ++ (nullable FWFWKPermissionDecisionData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface FWFWKMediaCaptureTypeData () ++ (FWFWKMediaCaptureTypeData *)fromList:(NSArray *)list; ++ (nullable FWFWKMediaCaptureTypeData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + @interface FWFNSUrlRequestData () + (FWFNSUrlRequestData *)fromList:(NSArray *)list; + (nullable FWFNSUrlRequestData *)nullableFromList:(NSArray *)list; @@ -102,6 +114,12 @@ + (nullable FWFWKScriptMessageData *)nullableFromList:(NSArray *)list; - (NSArray *)toList; @end +@interface FWFWKSecurityOriginData () ++ (FWFWKSecurityOriginData *)fromList:(NSArray *)list; ++ (nullable FWFWKSecurityOriginData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + @interface FWFNSHttpCookieData () + (FWFNSHttpCookieData *)fromList:(NSArray *)list; + (nullable FWFNSHttpCookieData *)nullableFromList:(NSArray *)list; @@ -271,6 +289,48 @@ - (NSArray *)toList { } @end +@implementation FWFWKPermissionDecisionData ++ (instancetype)makeWithValue:(FWFWKPermissionDecision)value { + FWFWKPermissionDecisionData *pigeonResult = [[FWFWKPermissionDecisionData alloc] init]; + pigeonResult.value = value; + return pigeonResult; +} ++ (FWFWKPermissionDecisionData *)fromList:(NSArray *)list { + FWFWKPermissionDecisionData *pigeonResult = [[FWFWKPermissionDecisionData alloc] init]; + pigeonResult.value = [GetNullableObjectAtIndex(list, 0) integerValue]; + return pigeonResult; +} ++ (nullable FWFWKPermissionDecisionData *)nullableFromList:(NSArray *)list { + return (list) ? [FWFWKPermissionDecisionData fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.value), + ]; +} +@end + +@implementation FWFWKMediaCaptureTypeData ++ (instancetype)makeWithValue:(FWFWKMediaCaptureType)value { + FWFWKMediaCaptureTypeData *pigeonResult = [[FWFWKMediaCaptureTypeData alloc] init]; + pigeonResult.value = value; + return pigeonResult; +} ++ (FWFWKMediaCaptureTypeData *)fromList:(NSArray *)list { + FWFWKMediaCaptureTypeData *pigeonResult = [[FWFWKMediaCaptureTypeData alloc] init]; + pigeonResult.value = [GetNullableObjectAtIndex(list, 0) integerValue]; + return pigeonResult; +} ++ (nullable FWFWKMediaCaptureTypeData *)nullableFromList:(NSArray *)list { + return (list) ? [FWFWKMediaCaptureTypeData fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.value), + ]; +} +@end + @implementation FWFNSUrlRequestData + (instancetype)makeWithUrl:(NSString *)url httpMethod:(nullable NSString *)httpMethod @@ -449,6 +509,36 @@ - (NSArray *)toList { } @end +@implementation FWFWKSecurityOriginData ++ (instancetype)makeWithHost:(NSString *)host port:(NSNumber *)port protocol:(NSString *)protocol { + FWFWKSecurityOriginData *pigeonResult = [[FWFWKSecurityOriginData alloc] init]; + pigeonResult.host = host; + pigeonResult.port = port; + pigeonResult.protocol = protocol; + return pigeonResult; +} ++ (FWFWKSecurityOriginData *)fromList:(NSArray *)list { + FWFWKSecurityOriginData *pigeonResult = [[FWFWKSecurityOriginData alloc] init]; + pigeonResult.host = GetNullableObjectAtIndex(list, 0); + NSAssert(pigeonResult.host != nil, @""); + pigeonResult.port = GetNullableObjectAtIndex(list, 1); + NSAssert(pigeonResult.port != nil, @""); + pigeonResult.protocol = GetNullableObjectAtIndex(list, 2); + NSAssert(pigeonResult.protocol != nil, @""); + return pigeonResult; +} ++ (nullable FWFWKSecurityOriginData *)nullableFromList:(NSArray *)list { + return (list) ? [FWFWKSecurityOriginData fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.host ?: [NSNull null]), + (self.port ?: [NSNull null]), + (self.protocol ?: [NSNull null]), + ]; +} +@end + @implementation FWFNSHttpCookieData + (instancetype)makeWithPropertyKeys:(NSArray *)propertyKeys propertyValues:(NSArray *)propertyValues { @@ -1828,16 +1918,22 @@ - (nullable id)readValueOfType:(UInt8)type { case 136: return [FWFWKFrameInfoData fromList:[self readValue]]; case 137: - return [FWFWKNavigationActionData fromList:[self readValue]]; + return [FWFWKMediaCaptureTypeData fromList:[self readValue]]; case 138: - return [FWFWKNavigationActionPolicyEnumData fromList:[self readValue]]; + return [FWFWKNavigationActionData fromList:[self readValue]]; case 139: - return [FWFWKScriptMessageData fromList:[self readValue]]; + return [FWFWKNavigationActionPolicyEnumData fromList:[self readValue]]; case 140: - return [FWFWKUserScriptData fromList:[self readValue]]; + return [FWFWKPermissionDecisionData fromList:[self readValue]]; case 141: - return [FWFWKUserScriptInjectionTimeEnumData fromList:[self readValue]]; + return [FWFWKScriptMessageData fromList:[self readValue]]; case 142: + return [FWFWKSecurityOriginData fromList:[self readValue]]; + case 143: + return [FWFWKUserScriptData fromList:[self readValue]]; + case 144: + return [FWFWKUserScriptInjectionTimeEnumData fromList:[self readValue]]; + case 145: return [FWFWKWebsiteDataTypeEnumData fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -1876,24 +1972,33 @@ - (void)writeValue:(id)value { } else if ([value isKindOfClass:[FWFWKFrameInfoData class]]) { [self writeByte:136]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKNavigationActionData class]]) { + } else if ([value isKindOfClass:[FWFWKMediaCaptureTypeData class]]) { [self writeByte:137]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKNavigationActionPolicyEnumData class]]) { + } else if ([value isKindOfClass:[FWFWKNavigationActionData class]]) { [self writeByte:138]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKScriptMessageData class]]) { + } else if ([value isKindOfClass:[FWFWKNavigationActionPolicyEnumData class]]) { [self writeByte:139]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKUserScriptData class]]) { + } else if ([value isKindOfClass:[FWFWKPermissionDecisionData class]]) { [self writeByte:140]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKUserScriptInjectionTimeEnumData class]]) { + } else if ([value isKindOfClass:[FWFWKScriptMessageData class]]) { [self writeByte:141]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKWebsiteDataTypeEnumData class]]) { + } else if ([value isKindOfClass:[FWFWKSecurityOriginData class]]) { [self writeByte:142]; [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FWFWKUserScriptData class]]) { + [self writeByte:143]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FWFWKUserScriptInjectionTimeEnumData class]]) { + [self writeByte:144]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FWFWKWebsiteDataTypeEnumData class]]) { + [self writeByte:145]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -2389,7 +2494,13 @@ - (nullable id)readValueOfType:(UInt8)type { case 129: return [FWFWKFrameInfoData fromList:[self readValue]]; case 130: + return [FWFWKMediaCaptureTypeData fromList:[self readValue]]; + case 131: return [FWFWKNavigationActionData fromList:[self readValue]]; + case 132: + return [FWFWKPermissionDecisionData fromList:[self readValue]]; + case 133: + return [FWFWKSecurityOriginData fromList:[self readValue]]; default: return [super readValueOfType:type]; } @@ -2406,9 +2517,18 @@ - (void)writeValue:(id)value { } else if ([value isKindOfClass:[FWFWKFrameInfoData class]]) { [self writeByte:129]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKNavigationActionData class]]) { + } else if ([value isKindOfClass:[FWFWKMediaCaptureTypeData class]]) { [self writeByte:130]; [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FWFWKNavigationActionData class]]) { + [self writeByte:131]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FWFWKPermissionDecisionData class]]) { + [self writeByte:132]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FWFWKSecurityOriginData class]]) { + [self writeByte:133]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -2467,6 +2587,29 @@ - (void)onCreateWebViewForDelegateWithIdentifier:(NSNumber *)arg_identifier completion(nil); }]; } +- (void)requestMediaCapturePermissionForDelegateWithIdentifier:(NSNumber *)arg_identifier + webViewIdentifier:(NSNumber *)arg_webViewIdentifier + origin:(FWFWKSecurityOriginData *)arg_origin + frame:(FWFWKFrameInfoData *)arg_frame + type:(FWFWKMediaCaptureTypeData *)arg_type + completion: + (void (^)( + FWFWKPermissionDecisionData *_Nullable, + FlutterError *_Nullable))completion { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName: + @"dev.flutter.pigeon.WKUIDelegateFlutterApi.requestMediaCapturePermission" + binaryMessenger:self.binaryMessenger + codec:FWFWKUIDelegateFlutterApiGetCodec()]; + [channel sendMessage:@[ + arg_identifier ?: [NSNull null], arg_webViewIdentifier ?: [NSNull null], + arg_origin ?: [NSNull null], arg_frame ?: [NSNull null], arg_type ?: [NSNull null] + ] + reply:^(id reply) { + FWFWKPermissionDecisionData *output = reply; + completion(output, nil); + }]; +} @end @interface FWFWKHttpCookieStoreHostApiCodecReader : FlutterStandardReader diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFHTTPCookieStoreHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFHTTPCookieStoreHostApi.m index f27b175a211..a5f4e02b9c2 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFHTTPCookieStoreHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFHTTPCookieStoreHostApi.m @@ -37,7 +37,7 @@ - (void)createFromWebsiteDataStoreWithIdentifier:(nonnull NSNumber *)identifier - (void)setCookieForStoreWithIdentifier:(nonnull NSNumber *)identifier cookie:(nonnull FWFNSHttpCookieData *)cookie completion:(nonnull void (^)(FlutterError *_Nullable))completion { - NSHTTPCookie *nsCookie = FWFNSHTTPCookieFromCookieData(cookie); + NSHTTPCookie *nsCookie = FWFNativeNSHTTPCookieFromCookieData(cookie); [[self HTTPCookieStoreForIdentifier:identifier] setCookie:nsCookie completionHandler:^{ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFNavigationDelegateHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFNavigationDelegateHostApi.m index d9cdfd98025..f8b5925f221 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFNavigationDelegateHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFNavigationDelegateHostApi.m @@ -60,7 +60,7 @@ - (void)didStartProvisionalNavigationForDelegate:(FWFNavigationDelegate *)instan NSNumber *webViewIdentifier = @([self.instanceManager identifierWithStrongReferenceForInstance:webView]); FWFWKNavigationActionData *navigationActionData = - FWFWKNavigationActionDataFromNavigationAction(navigationAction); + FWFWKNavigationActionDataFromNativeWKNavigationAction(navigationAction); [self decidePolicyForNavigationActionForDelegateWithIdentifier:@([self identifierForDelegate:instance]) @@ -77,7 +77,7 @@ - (void)didFailNavigationForDelegate:(FWFNavigationDelegate *)instance @([self.instanceManager identifierWithStrongReferenceForInstance:webView]); [self didFailNavigationForDelegateWithIdentifier:@([self identifierForDelegate:instance]) webViewIdentifier:webViewIdentifier - error:FWFNSErrorDataFromNSError(error) + error:FWFNSErrorDataFromNativeNSError(error) completion:completion]; } @@ -90,7 +90,7 @@ - (void)didFailProvisionalNavigationForDelegate:(FWFNavigationDelegate *)instanc [self didFailProvisionalNavigationForDelegateWithIdentifier:@([self identifierForDelegate:instance]) webViewIdentifier:webViewIdentifier - error:FWFNSErrorDataFromNSError(error) + error:FWFNSErrorDataFromNativeNSError(error) completion:completion]; } @@ -148,7 +148,7 @@ - (void)webView:(WKWebView *)webView FlutterError *error) { NSAssert(!error, @"%@", error); decisionHandler( - FWFWKNavigationActionPolicyFromEnumData(policy)); + FWFNativeWKNavigationActionPolicyFromEnumData(policy)); }]; } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFObjectHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFObjectHostApi.m index 4b014a71010..3adf7246a9a 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFObjectHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFObjectHostApi.m @@ -39,7 +39,7 @@ - (void)observeValueForObject:(NSObject *)instance NSMutableArray *changeValues = [NSMutableArray array]; [change enumerateKeysAndObjectsUsingBlock:^(NSKeyValueChangeKey key, id value, BOOL *stop) { - [changeKeys addObject:FWFNSKeyValueChangeKeyEnumDataFromNSKeyValueChangeKey(key)]; + [changeKeys addObject:FWFNSKeyValueChangeKeyEnumDataFromNativeNSKeyValueChangeKey(key)]; BOOL isIdentifier = NO; if ([self.instanceManager containsInstance:value]) { isIdentifier = YES; @@ -124,7 +124,7 @@ - (void)addObserverForObjectWithIdentifier:(nonnull NSNumber *)identifier error:(FlutterError *_Nullable *_Nonnull)error { NSKeyValueObservingOptions optionsInt = 0; for (FWFNSKeyValueObservingOptionsEnumData *data in options) { - optionsInt |= FWFNSKeyValueObservingOptionsFromEnumData(data); + optionsInt |= FWFNativeNSKeyValueObservingOptionsFromEnumData(data); } [[self objectForIdentifier:identifier] addObserver:[self objectForIdentifier:observer] forKeyPath:keyPath diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFScriptMessageHandlerHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFScriptMessageHandlerHostApi.m index fc7c3093c67..f1b83993c4f 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFScriptMessageHandlerHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFScriptMessageHandlerHostApi.m @@ -30,7 +30,7 @@ - (void)didReceiveScriptMessageForHandler:(FWFScriptMessageHandler *)instance completion:(void (^)(FlutterError *_Nullable))completion { NSNumber *userContentControllerIdentifier = @([self.instanceManager identifierWithStrongReferenceForInstance:userContentController]); - FWFWKScriptMessageData *messageData = FWFWKScriptMessageDataFromWKScriptMessage(message); + FWFWKScriptMessageData *messageData = FWFWKScriptMessageDataFromNativeWKScriptMessage(message); [self didReceiveScriptMessageForHandlerWithIdentifier:@([self identifierForHandler:instance]) userContentControllerIdentifier:userContentControllerIdentifier message:messageData diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIDelegateHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIDelegateHostApi.m index a8ae512cc89..9615e7fb6a9 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIDelegateHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIDelegateHostApi.m @@ -46,7 +46,7 @@ - (void)onCreateWebViewForDelegate:(FWFUIDelegate *)instance NSNumber *configurationIdentifier = @([self.instanceManager identifierWithStrongReferenceForInstance:configuration]); FWFWKNavigationActionData *navigationActionData = - FWFWKNavigationActionDataFromNavigationAction(navigationAction); + FWFWKNavigationActionDataFromNativeWKNavigationAction(navigationAction); [self onCreateWebViewForDelegateWithIdentifier:@([self identifierForDelegate:instance]) webViewIdentifier: @@ -56,6 +56,40 @@ - (void)onCreateWebViewForDelegate:(FWFUIDelegate *)instance navigationAction:navigationActionData completion:completion]; } + +- (void)requestMediaCapturePermissionForDelegateWithIdentifier:(FWFUIDelegate *)instance + webView:(WKWebView *)webView + origin:(WKSecurityOrigin *)origin + frame:(WKFrameInfo *)frame + type:(FWFWKMediaCaptureType)type + completion: + (void (^)(WKPermissionDecision))completion + API_AVAILABLE(ios(15.0)) { + [self + requestMediaCapturePermissionForDelegateWithIdentifier:@([self + identifierForDelegate:instance]) + webViewIdentifier: + @([self.instanceManager + identifierWithStrongReferenceForInstance: + webView]) + origin: + FWFWKSecurityOriginDataFromNativeWKSecurityOrigin( + origin) + frame: + FWFWKFrameInfoDataFromNativeWKFrameInfo( + frame) + type: + FWFWKMediaCaptureTypeDataFromNativeWKMediaCaptureType( + type) + completion:^( + FWFWKPermissionDecisionData *decision, + FlutterError *error) { + NSAssert(!error, @"%@", error); + completion( + FWFNativeWKPermissionDecisionFromData( + decision)); + }]; +} @end @implementation FWFUIDelegate @@ -82,6 +116,23 @@ - (WKWebView *)webView:(WKWebView *)webView }]; return nil; } + +- (void)webView:(WKWebView *)webView + requestMediaCapturePermissionForOrigin:(WKSecurityOrigin *)origin + initiatedByFrame:(WKFrameInfo *)frame + type:(WKMediaCaptureType)type + decisionHandler:(void (^)(WKPermissionDecision))decisionHandler + API_AVAILABLE(ios(15.0)) { + [self.UIDelegateAPI + requestMediaCapturePermissionForDelegateWithIdentifier:self + webView:webView + origin:origin + frame:frame + type:type + completion:^(WKPermissionDecision decision) { + decisionHandler(decision); + }]; +} @end @interface FWFUIDelegateHostApiImpl () diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUserContentControllerHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUserContentControllerHostApi.m index 08bbaa68c99..ac314323da3 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUserContentControllerHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUserContentControllerHostApi.m @@ -70,7 +70,7 @@ - (void)addUserScriptForControllerWithIdentifier:(nonnull NSNumber *)identifier userScript:(nonnull FWFWKUserScriptData *)userScript error:(FlutterError *_Nullable *_Nonnull)error { [[self userContentControllerForIdentifier:identifier] - addUserScript:FWFWKUserScriptFromScriptData(userScript)]; + addUserScript:FWFNativeWKUserScriptFromScriptData(userScript)]; } - (void)removeAllUserScriptsForControllerWithIdentifier:(nonnull NSNumber *)identifier diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewConfigurationHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewConfigurationHostApi.m index 8649f3295b1..987d3f45ff2 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewConfigurationHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewConfigurationHostApi.m @@ -119,7 +119,7 @@ - (void)setAllowsInlineMediaPlaybackForConfigurationWithIdentifier:(nonnull NSNu (WKWebViewConfiguration *)[self webViewConfigurationForIdentifier:identifier]; WKAudiovisualMediaTypes typesInt = 0; for (FWFWKAudiovisualMediaTypeEnumData *data in types) { - typesInt |= FWFWKAudiovisualMediaTypeFromEnumData(data); + typesInt |= FWFNativeWKAudiovisualMediaTypeFromEnumData(data); } [configuration setMediaTypesRequiringUserActionForPlayback:typesInt]; } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewHostApi.m index 549657ec8a0..d5746613e90 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewHostApi.m @@ -124,7 +124,7 @@ - (void)loadRequestForWebViewWithIdentifier:(nonnull NSNumber *)identifier request:(nonnull FWFNSUrlRequestData *)request error: (FlutterError *_Nullable __autoreleasing *_Nonnull)error { - NSURLRequest *urlRequest = FWFNSURLRequestFromRequestData(request); + NSURLRequest *urlRequest = FWFNativeNSURLRequestFromRequestData(request); if (!urlRequest) { *error = [FlutterError errorWithCode:@"FWFURLRequestParsingError" message:@"Failed instantiating an NSURLRequest." @@ -190,7 +190,7 @@ - (void)evaluateJavaScriptForWebViewWithIdentifier:(nonnull NSNumber *)identifie } else { flutterError = [FlutterError errorWithCode:@"FWFEvaluateJavaScriptError" message:@"Failed evaluating JavaScript." - details:FWFNSErrorDataFromNSError(error)]; + details:FWFNSErrorDataFromNativeNSError(error)]; } completion(returnValue, flutterError); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebsiteDataStoreHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebsiteDataStoreHostApi.m index 5398d14d4e8..51c7784931b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebsiteDataStoreHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebsiteDataStoreHostApi.m @@ -49,7 +49,7 @@ - (void)createDefaultDataStoreWithIdentifier:(nonnull NSNumber *)identifier FlutterError *_Nullable))completion { NSMutableSet *stringDataTypes = [NSMutableSet set]; for (FWFWKWebsiteDataTypeEnumData *type in dataTypes) { - [stringDataTypes addObject:FWFWKWebsiteDataTypeFromEnumData(type)]; + [stringDataTypes addObject:FWFNativeWKWebsiteDataTypeFromEnumData(type)]; } WKWebsiteDataStore *dataStore = [self websiteDataStoreForIdentifier:identifier]; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart index c44e52a7148..92372f23759 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart @@ -138,6 +138,49 @@ enum WKNavigationType { other, } +/// Possible permission decisions for device resource access. +/// +/// See https://developer.apple.com/documentation/webkit/wkpermissiondecision?language=objc. +enum WKPermissionDecision { + /// Deny permission for the requested resource. + /// + /// See https://developer.apple.com/documentation/webkit/wkpermissiondecision/wkpermissiondecisiondeny?language=objc. + deny, + + /// Deny permission for the requested resource. + /// + /// See https://developer.apple.com/documentation/webkit/wkpermissiondecision/wkpermissiondecisiongrant?language=objc. + grant, + + /// Prompt the user for permission for the requested resource. + /// + /// See https://developer.apple.com/documentation/webkit/wkpermissiondecision/wkpermissiondecisionprompt?language=objc. + prompt, +} + +/// List of the types of media devices that can capture audio, video, or both. +/// +/// See https://developer.apple.com/documentation/webkit/wkmediacapturetype?language=objc. +enum WKMediaCaptureType { + /// A media device that can capture video. + /// + /// See https://developer.apple.com/documentation/webkit/wkmediacapturetype/wkmediacapturetypecamera?language=objc. + camera, + + /// A media device or devices that can capture audio and video. + /// + /// See https://developer.apple.com/documentation/webkit/wkmediacapturetype/wkmediacapturetypecameraandmicrophone?language=objc. + cameraAndMicrophone, + + /// A media device that can capture audio. + /// + /// See https://developer.apple.com/documentation/webkit/wkmediacapturetype/wkmediacapturetypemicrophone?language=objc. + microphone, + + /// An unknown media device. + unknown, +} + class NSKeyValueObservingOptionsEnumData { NSKeyValueObservingOptionsEnumData({ required this.value, @@ -285,6 +328,48 @@ class NSHttpCookiePropertyKeyEnumData { } } +class WKPermissionDecisionData { + WKPermissionDecisionData({ + required this.value, + }); + + WKPermissionDecision value; + + Object encode() { + return [ + value.index, + ]; + } + + static WKPermissionDecisionData decode(Object result) { + result as List; + return WKPermissionDecisionData( + value: WKPermissionDecision.values[result[0]! as int], + ); + } +} + +class WKMediaCaptureTypeData { + WKMediaCaptureTypeData({ + required this.value, + }); + + WKMediaCaptureType value; + + Object encode() { + return [ + value.index, + ]; + } + + static WKMediaCaptureTypeData decode(Object result) { + result as List; + return WKMediaCaptureTypeData( + value: WKMediaCaptureType.values[result[0]! as int], + ); + } +} + /// Mirror of NSURLRequest. /// /// See https://developer.apple.com/documentation/foundation/nsurlrequest?language=objc. @@ -483,6 +568,40 @@ class WKScriptMessageData { } } +/// Mirror of WKSecurityOrigin. +/// +/// See https://developer.apple.com/documentation/webkit/wksecurityorigin?language=objc. +class WKSecurityOriginData { + WKSecurityOriginData({ + required this.host, + required this.port, + required this.protocol, + }); + + String host; + + int port; + + String protocol; + + Object encode() { + return [ + host, + port, + protocol, + ]; + } + + static WKSecurityOriginData decode(Object result) { + result as List; + return WKSecurityOriginData( + host: result[0]! as String, + port: result[1]! as int, + protocol: result[2]! as String, + ); + } +} + /// Mirror of NSHttpCookieData. /// /// See https://developer.apple.com/documentation/foundation/nshttpcookie?language=objc. @@ -1858,24 +1977,33 @@ class _WKWebViewHostApiCodec extends StandardMessageCodec { } else if (value is WKFrameInfoData) { buffer.putUint8(136); writeValue(buffer, value.encode()); - } else if (value is WKNavigationActionData) { + } else if (value is WKMediaCaptureTypeData) { buffer.putUint8(137); writeValue(buffer, value.encode()); - } else if (value is WKNavigationActionPolicyEnumData) { + } else if (value is WKNavigationActionData) { buffer.putUint8(138); writeValue(buffer, value.encode()); - } else if (value is WKScriptMessageData) { + } else if (value is WKNavigationActionPolicyEnumData) { buffer.putUint8(139); writeValue(buffer, value.encode()); - } else if (value is WKUserScriptData) { + } else if (value is WKPermissionDecisionData) { buffer.putUint8(140); writeValue(buffer, value.encode()); - } else if (value is WKUserScriptInjectionTimeEnumData) { + } else if (value is WKScriptMessageData) { buffer.putUint8(141); writeValue(buffer, value.encode()); - } else if (value is WKWebsiteDataTypeEnumData) { + } else if (value is WKSecurityOriginData) { buffer.putUint8(142); writeValue(buffer, value.encode()); + } else if (value is WKUserScriptData) { + buffer.putUint8(143); + writeValue(buffer, value.encode()); + } else if (value is WKUserScriptInjectionTimeEnumData) { + buffer.putUint8(144); + writeValue(buffer, value.encode()); + } else if (value is WKWebsiteDataTypeEnumData) { + buffer.putUint8(145); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -1903,16 +2031,22 @@ class _WKWebViewHostApiCodec extends StandardMessageCodec { case 136: return WKFrameInfoData.decode(readValue(buffer)!); case 137: - return WKNavigationActionData.decode(readValue(buffer)!); + return WKMediaCaptureTypeData.decode(readValue(buffer)!); case 138: - return WKNavigationActionPolicyEnumData.decode(readValue(buffer)!); + return WKNavigationActionData.decode(readValue(buffer)!); case 139: - return WKScriptMessageData.decode(readValue(buffer)!); + return WKNavigationActionPolicyEnumData.decode(readValue(buffer)!); case 140: - return WKUserScriptData.decode(readValue(buffer)!); + return WKPermissionDecisionData.decode(readValue(buffer)!); case 141: - return WKUserScriptInjectionTimeEnumData.decode(readValue(buffer)!); + return WKScriptMessageData.decode(readValue(buffer)!); case 142: + return WKSecurityOriginData.decode(readValue(buffer)!); + case 143: + return WKUserScriptData.decode(readValue(buffer)!); + case 144: + return WKUserScriptInjectionTimeEnumData.decode(readValue(buffer)!); + case 145: return WKWebsiteDataTypeEnumData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -2407,9 +2541,18 @@ class _WKUIDelegateFlutterApiCodec extends StandardMessageCodec { } else if (value is WKFrameInfoData) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is WKNavigationActionData) { + } else if (value is WKMediaCaptureTypeData) { buffer.putUint8(130); writeValue(buffer, value.encode()); + } else if (value is WKNavigationActionData) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is WKPermissionDecisionData) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else if (value is WKSecurityOriginData) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -2423,7 +2566,13 @@ class _WKUIDelegateFlutterApiCodec extends StandardMessageCodec { case 129: return WKFrameInfoData.decode(readValue(buffer)!); case 130: + return WKMediaCaptureTypeData.decode(readValue(buffer)!); + case 131: return WKNavigationActionData.decode(readValue(buffer)!); + case 132: + return WKPermissionDecisionData.decode(readValue(buffer)!); + case 133: + return WKSecurityOriginData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -2439,6 +2588,14 @@ abstract class WKUIDelegateFlutterApi { void onCreateWebView(int identifier, int webViewIdentifier, int configurationIdentifier, WKNavigationActionData navigationAction); + /// Callback to Dart function `WKUIDelegate.requestMediaCapturePermission`. + Future requestMediaCapturePermission( + int identifier, + int webViewIdentifier, + WKSecurityOriginData origin, + WKFrameInfoData frame, + WKMediaCaptureTypeData type); + static void setup(WKUIDelegateFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -2471,6 +2628,42 @@ abstract class WKUIDelegateFlutterApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUIDelegateFlutterApi.requestMediaCapturePermission', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKUIDelegateFlutterApi.requestMediaCapturePermission 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.WKUIDelegateFlutterApi.requestMediaCapturePermission was null, expected non-null int.'); + final int? arg_webViewIdentifier = (args[1] as int?); + assert(arg_webViewIdentifier != null, + 'Argument for dev.flutter.pigeon.WKUIDelegateFlutterApi.requestMediaCapturePermission was null, expected non-null int.'); + final WKSecurityOriginData? arg_origin = + (args[2] as WKSecurityOriginData?); + assert(arg_origin != null, + 'Argument for dev.flutter.pigeon.WKUIDelegateFlutterApi.requestMediaCapturePermission was null, expected non-null WKSecurityOriginData.'); + final WKFrameInfoData? arg_frame = (args[3] as WKFrameInfoData?); + assert(arg_frame != null, + 'Argument for dev.flutter.pigeon.WKUIDelegateFlutterApi.requestMediaCapturePermission was null, expected non-null WKFrameInfoData.'); + final WKMediaCaptureTypeData? arg_type = + (args[4] as WKMediaCaptureTypeData?); + assert(arg_type != null, + 'Argument for dev.flutter.pigeon.WKUIDelegateFlutterApi.requestMediaCapturePermission was null, expected non-null WKMediaCaptureTypeData.'); + final WKPermissionDecisionData output = + await api.requestMediaCapturePermission(arg_identifier!, + arg_webViewIdentifier!, arg_origin!, arg_frame!, arg_type!); + return output; + }); + } + } } } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart index 467fa8735d6..070f554a5d5 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart @@ -10,7 +10,8 @@ import '../foundation/foundation.dart'; import '../ui_kit/ui_kit.dart'; import 'web_kit_api_impls.dart'; -export 'web_kit_api_impls.dart' show WKNavigationType; +export 'web_kit_api_impls.dart' + show WKNavigationType, WKPermissionDecision, WKMediaCaptureType; /// Times at which to inject script content into a webpage. /// @@ -712,6 +713,7 @@ class WKUIDelegate extends NSObject { /// Constructs a [WKUIDelegate]. WKUIDelegate({ this.onCreateWebView, + this.requestMediaCapturePermission, super.observeValue, super.binaryMessenger, super.instanceManager, @@ -732,6 +734,7 @@ class WKUIDelegate extends NSObject { /// create copies. WKUIDelegate.detached({ this.onCreateWebView, + this.requestMediaCapturePermission, super.observeValue, super.binaryMessenger, super.instanceManager, @@ -752,10 +755,22 @@ class WKUIDelegate extends NSObject { WKNavigationAction navigationAction, )? onCreateWebView; + /// Determines whether a web resource, which the security origin object + /// describes, can gain access to the device’s microphone audio and camera + /// video. + final Future Function( + WKUIDelegate instance, + WKWebView webView, + WKSecurityOrigin origin, + WKFrameInfo frame, + WKMediaCaptureType type, + )? requestMediaCapturePermission; + @override WKUIDelegate copy() { return WKUIDelegate.detached( onCreateWebView: onCreateWebView, + requestMediaCapturePermission: requestMediaCapturePermission, observeValue: observeValue, binaryMessenger: _uiDelegateApi.binaryMessenger, instanceManager: _uiDelegateApi.instanceManager, @@ -763,6 +778,28 @@ class WKUIDelegate extends NSObject { } } +/// An object that identifies the origin of a particular resource. +/// +/// Wraps https://developer.apple.com/documentation/webkit/wksecurityorigin?language=objc. +@immutable +class WKSecurityOrigin { + /// Constructs an [WKSecurityOrigin]. + const WKSecurityOrigin({ + required this.host, + required this.port, + required this.protocol, + }); + + /// The security origin’s host. + final String host; + + /// The security origin's port. + final int port; + + /// The security origin's protocol. + final String protocol; +} + /// Methods for handling navigation changes and tracking navigation requests. /// /// Set the methods of the [WKNavigationDelegate] in the object you use to diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart index 7cd29da3e71..07a32aee8d3 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart @@ -10,7 +10,8 @@ import '../common/web_kit.g.dart'; import '../foundation/foundation.dart'; import 'web_kit.dart'; -export '../common/web_kit.g.dart' show WKNavigationType; +export '../common/web_kit.g.dart' + show WKNavigationType, WKPermissionDecision, WKMediaCaptureType; Iterable _toWKWebsiteDataTypeEnumData( Iterable types) { @@ -230,6 +231,12 @@ extension _NSUrlRequestConverter on NSUrlRequest { } } +extension _WKSecurityOriginConverter on WKSecurityOriginData { + WKSecurityOrigin toWKSecurityOrigin() { + return WKSecurityOrigin(host: host, port: port, protocol: protocol); + } +} + /// Handles initialization of Flutter APIs for WebKit. class WebKitFlutterApis { /// Constructs a [WebKitFlutterApis]. @@ -719,6 +726,36 @@ class WKUIDelegateFlutterApiImpl extends WKUIDelegateFlutterApi { navigationAction.toNavigationAction(), ); } + + @override + Future requestMediaCapturePermission( + int identifier, + int webViewIdentifier, + WKSecurityOriginData origin, + WKFrameInfoData frame, + WKMediaCaptureTypeData type, + ) async { + final WKUIDelegate instance = + instanceManager.getInstanceWithWeakReference(identifier)!; + + late final WKPermissionDecision decision; + if (instance.requestMediaCapturePermission != null) { + decision = await instance.requestMediaCapturePermission!( + instance, + instanceManager.getInstanceWithWeakReference(webViewIdentifier)! + as WKWebView, + origin.toWKSecurityOrigin(), + frame.toWKFrameInfo(), + type.value, + ); + } else { + // The default response for iOS is to prompt. See + // https://developer.apple.com/documentation/webkit/wkuidelegate/3763087-webview?language=objc + decision = WKPermissionDecision.prompt; + } + + return WKPermissionDecisionData(value: decision); + } } /// Host api implementation for [WKNavigationDelegate]. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart index e25fdf1e354..68ce9132021 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart @@ -80,5 +80,13 @@ class WebKitProxy { WKWebViewConfiguration configuration, WKNavigationAction navigationAction, )? onCreateWebView, + Future Function( + WKUIDelegate instance, + WKWebView webView, + WKSecurityOrigin origin, + WKFrameInfo frame, + WKMediaCaptureType type, + )? requestMediaCapturePermission, + InstanceManager? instanceManager, }) createUIDelegate; } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart index 9ca4744440e..b7975d5198c 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart @@ -138,6 +138,75 @@ class WebKitWebViewController extends PlatformWebViewController { NSKeyValueObservingOptions.newValue, }, ); + + final WeakReference weakThis = + WeakReference(this); + _uiDelegate = _webKitParams.webKitProxy.createUIDelegate( + instanceManager: _webKitParams._instanceManager, + onCreateWebView: ( + WKWebView webView, + WKWebViewConfiguration configuration, + WKNavigationAction navigationAction, + ) { + if (!navigationAction.targetFrame.isMainFrame) { + webView.loadRequest(navigationAction.request); + } + }, + requestMediaCapturePermission: ( + WKUIDelegate instance, + WKWebView webView, + WKSecurityOrigin origin, + WKFrameInfo frame, + WKMediaCaptureType type, + ) async { + final void Function(PlatformWebViewPermissionRequest)? callback = + weakThis.target?._onPermissionRequestCallback; + + if (callback == null) { + // The default response for iOS is to prompt. See + // https://developer.apple.com/documentation/webkit/wkuidelegate/3763087-webview?language=objc + return WKPermissionDecision.prompt; + } else { + late final Set types; + switch (type) { + case WKMediaCaptureType.camera: + types = { + WebViewPermissionResourceType.camera + }; + break; + case WKMediaCaptureType.cameraAndMicrophone: + types = { + WebViewPermissionResourceType.camera, + WebViewPermissionResourceType.microphone + }; + break; + case WKMediaCaptureType.microphone: + types = { + WebViewPermissionResourceType.microphone + }; + break; + case WKMediaCaptureType.unknown: + // The default response for iOS is to prompt. See + // https://developer.apple.com/documentation/webkit/wkuidelegate/3763087-webview?language=objc + return WKPermissionDecision.prompt; + } + + final Completer decisionCompleter = + Completer(); + + callback( + WebKitWebViewPermissionRequest._( + types: types, + onDecision: decisionCompleter.complete, + ), + ); + + return decisionCompleter.future; + } + }, + ); + + _webView.setUIDelegate(_uiDelegate); } /// The WebKit WebView being controlled. @@ -180,12 +249,16 @@ class WebKitWebViewController extends PlatformWebViewController { instanceManager: _webKitParams._instanceManager, ); + late final WKUIDelegate _uiDelegate; + final Map _javaScriptChannelParams = {}; bool _zoomEnabled = true; WebKitNavigationDelegate? _currentNavigationDelegate; + void Function(PlatformWebViewPermissionRequest)? _onPermissionRequestCallback; + WebKitWebViewControllerCreationParams get _webKitParams => params as WebKitWebViewControllerCreationParams; @@ -410,10 +483,7 @@ class WebKitWebViewController extends PlatformWebViewController { covariant WebKitNavigationDelegate handler, ) { _currentNavigationDelegate = handler; - return Future.wait(>[ - _webView.setUIDelegate(handler._uiDelegate), - _webView.setNavigationDelegate(handler._navigationDelegate) - ]); + return _webView.setNavigationDelegate(handler._navigationDelegate); } Future _disableZoom() { @@ -454,6 +524,13 @@ class WebKitWebViewController extends PlatformWebViewController { if (!_zoomEnabled) _disableZoom(), ]); } + + @override + Future setOnPlatformPermissionRequest( + void Function(PlatformWebViewPermissionRequest request) onPermissionRequest, + ) async { + _onPermissionRequestCallback = onPermissionRequest; + } } /// An implementation of [JavaScriptChannelParams] with the WebKit api. @@ -689,28 +766,11 @@ class WebKitNavigationDelegate extends PlatformNavigationDelegate { } }, ); - - _uiDelegate = (this.params as WebKitNavigationDelegateCreationParams) - .webKitProxy - .createUIDelegate( - onCreateWebView: ( - WKWebView webView, - WKWebViewConfiguration configuration, - WKNavigationAction navigationAction, - ) { - if (!navigationAction.targetFrame.isMainFrame) { - webView.loadRequest(navigationAction.request); - } - }, - ); } // Used to set `WKWebView.setNavigationDelegate` in `WebKitWebViewController`. late final WKNavigationDelegate _navigationDelegate; - // Used to set `WKWebView.setUIDelegate` in `WebKitWebViewController`. - late final WKUIDelegate _uiDelegate; - PageEventCallback? _onPageFinished; PageEventCallback? _onPageStarted; ProgressCallback? _onProgress; @@ -752,3 +812,28 @@ class WebKitNavigationDelegate extends PlatformNavigationDelegate { _onUrlChange = onUrlChange; } } + +/// WebKit implementation of [PlatformWebViewPermissionRequest]. +class WebKitWebViewPermissionRequest extends PlatformWebViewPermissionRequest { + const WebKitWebViewPermissionRequest._({ + required super.types, + required void Function(WKPermissionDecision decision) onDecision, + }) : _onDecision = onDecision; + + final void Function(WKPermissionDecision) _onDecision; + + @override + Future grant() async { + _onDecision(WKPermissionDecision.grant); + } + + @override + Future deny() async { + _onDecision(WKPermissionDecision.deny); + } + + /// Prompt the user for permission for the requested resource. + Future prompt() async { + _onDecision(WKPermissionDecision.prompt); + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart b/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart index 8dcbc2abd00..bb588bd96c9 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart @@ -193,6 +193,64 @@ enum WKNavigationType { other, } +/// Possible permission decisions for device resource access. +/// +/// See https://developer.apple.com/documentation/webkit/wkpermissiondecision?language=objc. +enum WKPermissionDecision { + /// Deny permission for the requested resource. + /// + /// See https://developer.apple.com/documentation/webkit/wkpermissiondecision/wkpermissiondecisiondeny?language=objc. + deny, + + /// Deny permission for the requested resource. + /// + /// See https://developer.apple.com/documentation/webkit/wkpermissiondecision/wkpermissiondecisiongrant?language=objc. + grant, + + /// Prompt the user for permission for the requested resource. + /// + /// See https://developer.apple.com/documentation/webkit/wkpermissiondecision/wkpermissiondecisionprompt?language=objc. + prompt, +} + +// TODO(bparrishMines): Enums need be wrapped in a data class because thay can't +// be used as primitive arguments. See https://github.com/flutter/flutter/issues/87307 +class WKPermissionDecisionData { + late WKPermissionDecision value; +} + +/// List of the types of media devices that can capture audio, video, or both. +/// +/// See https://developer.apple.com/documentation/webkit/wkmediacapturetype?language=objc. +enum WKMediaCaptureType { + /// A media device that can capture video. + /// + /// See https://developer.apple.com/documentation/webkit/wkmediacapturetype/wkmediacapturetypecamera?language=objc. + camera, + + /// A media device or devices that can capture audio and video. + /// + /// See https://developer.apple.com/documentation/webkit/wkmediacapturetype/wkmediacapturetypecameraandmicrophone?language=objc. + cameraAndMicrophone, + + /// A media device that can capture audio. + /// + /// See https://developer.apple.com/documentation/webkit/wkmediacapturetype/wkmediacapturetypemicrophone?language=objc. + microphone, + + /// An unknown media device. + /// + /// This does not represent an actual value provided by the platform and only + /// indicates a value was provided that we don't currently support. + unknown, +} + +// TODO(bparrishMines): Enums need be wrapped in a data class because thay can't +// be used as primitive arguments. See https://github.com/flutter/flutter/issues/87307 +class WKMediaCaptureTypeData { + late WKMediaCaptureType value; +} + /// Mirror of NSURLRequest. /// /// See https://developer.apple.com/documentation/foundation/nsurlrequest?language=objc. @@ -245,6 +303,15 @@ class WKScriptMessageData { late Object? body; } +/// Mirror of WKSecurityOrigin. +/// +/// See https://developer.apple.com/documentation/webkit/wksecurityorigin?language=objc. +class WKSecurityOriginData { + late String host; + late int port; + late String protocol; +} + /// Mirror of NSHttpCookieData. /// /// See https://developer.apple.com/documentation/foundation/nshttpcookie?language=objc. @@ -640,6 +707,19 @@ abstract class WKUIDelegateFlutterApi { int configurationIdentifier, WKNavigationActionData navigationAction, ); + + /// Callback to Dart function `WKUIDelegate.requestMediaCapturePermission`. + @ObjCSelector( + 'requestMediaCapturePermissionForDelegateWithIdentifier:webViewIdentifier:origin:frame:type:', + ) + @async + WKPermissionDecisionData requestMediaCapturePermission( + int identifier, + int webViewIdentifier, + WKSecurityOriginData origin, + WKFrameInfoData frame, + WKMediaCaptureTypeData type, + ); } /// Mirror of WKHttpCookieStore. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 4c5c7484c5c..e742525b508 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_wkwebview description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control. repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 3.3.0 +version: 3.4.0 environment: sdk: ">=2.18.0 <4.0.0" @@ -30,3 +30,9 @@ dev_dependencies: sdk: flutter mockito: 5.4.0 pigeon: ^9.2.4 + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + webview_flutter_platform_interface: + path: ../../webview_flutter/webview_flutter_platform_interface diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart index a9b27a0d731..ff09d4401ee 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart @@ -995,24 +995,33 @@ class _TestWKWebViewHostApiCodec extends StandardMessageCodec { } else if (value is WKFrameInfoData) { buffer.putUint8(136); writeValue(buffer, value.encode()); - } else if (value is WKNavigationActionData) { + } else if (value is WKMediaCaptureTypeData) { buffer.putUint8(137); writeValue(buffer, value.encode()); - } else if (value is WKNavigationActionPolicyEnumData) { + } else if (value is WKNavigationActionData) { buffer.putUint8(138); writeValue(buffer, value.encode()); - } else if (value is WKScriptMessageData) { + } else if (value is WKNavigationActionPolicyEnumData) { buffer.putUint8(139); writeValue(buffer, value.encode()); - } else if (value is WKUserScriptData) { + } else if (value is WKPermissionDecisionData) { buffer.putUint8(140); writeValue(buffer, value.encode()); - } else if (value is WKUserScriptInjectionTimeEnumData) { + } else if (value is WKScriptMessageData) { buffer.putUint8(141); writeValue(buffer, value.encode()); - } else if (value is WKWebsiteDataTypeEnumData) { + } else if (value is WKSecurityOriginData) { buffer.putUint8(142); writeValue(buffer, value.encode()); + } else if (value is WKUserScriptData) { + buffer.putUint8(143); + writeValue(buffer, value.encode()); + } else if (value is WKUserScriptInjectionTimeEnumData) { + buffer.putUint8(144); + writeValue(buffer, value.encode()); + } else if (value is WKWebsiteDataTypeEnumData) { + buffer.putUint8(145); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -1040,16 +1049,22 @@ class _TestWKWebViewHostApiCodec extends StandardMessageCodec { case 136: return WKFrameInfoData.decode(readValue(buffer)!); case 137: - return WKNavigationActionData.decode(readValue(buffer)!); + return WKMediaCaptureTypeData.decode(readValue(buffer)!); case 138: - return WKNavigationActionPolicyEnumData.decode(readValue(buffer)!); + return WKNavigationActionData.decode(readValue(buffer)!); case 139: - return WKScriptMessageData.decode(readValue(buffer)!); + return WKNavigationActionPolicyEnumData.decode(readValue(buffer)!); case 140: - return WKUserScriptData.decode(readValue(buffer)!); + return WKPermissionDecisionData.decode(readValue(buffer)!); case 141: - return WKUserScriptInjectionTimeEnumData.decode(readValue(buffer)!); + return WKScriptMessageData.decode(readValue(buffer)!); case 142: + return WKSecurityOriginData.decode(readValue(buffer)!); + case 143: + return WKUserScriptData.decode(readValue(buffer)!); + case 144: + return WKUserScriptInjectionTimeEnumData.decode(readValue(buffer)!); + case 145: return WKWebsiteDataTypeEnumData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart index dd007869f0e..e2d53bc9fec 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart @@ -938,6 +938,74 @@ void main() { ]), ); }); + + test('requestMediaCapturePermission', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + const int instanceIdentifier = 0; + late final List callbackParameters; + final WKUIDelegate instance = WKUIDelegate.detached( + requestMediaCapturePermission: ( + WKUIDelegate instance, + WKWebView webView, + WKSecurityOrigin origin, + WKFrameInfo frame, + WKMediaCaptureType type, + ) async { + callbackParameters = [ + instance, + webView, + origin, + frame, + type, + ]; + return WKPermissionDecision.grant; + }, + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance(instance, instanceIdentifier); + + final WKUIDelegateFlutterApiImpl flutterApi = + WKUIDelegateFlutterApiImpl( + instanceManager: instanceManager, + ); + + final WKWebView webView = WKWebView.detached( + instanceManager: instanceManager, + ); + const int webViewIdentifier = 42; + instanceManager.addHostCreatedInstance( + webView, + webViewIdentifier, + ); + + const WKSecurityOrigin origin = + WKSecurityOrigin(host: 'host', port: 12, protocol: 'protocol'); + const WKFrameInfo frame = WKFrameInfo(isMainFrame: false); + const WKMediaCaptureType type = WKMediaCaptureType.microphone; + + flutterApi.requestMediaCapturePermission( + instanceIdentifier, + webViewIdentifier, + WKSecurityOriginData( + host: origin.host, + port: origin.port, + protocol: origin.protocol, + ), + WKFrameInfoData(isMainFrame: frame.isMainFrame), + WKMediaCaptureTypeData(value: type), + ); + + expect(callbackParameters, [ + instance, + webView, + isA(), + isA(), + type, + ]); + }); }); }); } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart index 9a654558b6c..0f754cb7f03 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart @@ -6,17 +6,12 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; import 'package:webview_flutter_wkwebview/src/webkit_proxy.dart'; import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; -import 'webkit_navigation_delegate_test.mocks.dart'; - -@GenerateMocks([WKWebView]) void main() { WidgetsFlutterBinding.ensureInitialized(); @@ -209,33 +204,6 @@ void main() { expect(callbackRequest.url, 'https://www.google.com'); expect(callbackRequest.isMainFrame, isFalse); }); - - test('Requests to open a new window loads request in same window', () { - WebKitNavigationDelegate( - const WebKitNavigationDelegateCreationParams( - webKitProxy: WebKitProxy( - createNavigationDelegate: CapturingNavigationDelegate.new, - createUIDelegate: CapturingUIDelegate.new, - ), - ), - ); - - final MockWKWebView mockWebView = MockWKWebView(); - - const NSUrlRequest request = NSUrlRequest(url: 'https://www.google.com'); - - CapturingUIDelegate.lastCreatedDelegate.onCreateWebView!( - mockWebView, - WKWebViewConfiguration.detached(), - const WKNavigationAction( - request: request, - targetFrame: WKFrameInfo(isMainFrame: false), - navigationType: WKNavigationType.linkActivated, - ), - ); - - verify(mockWebView.loadRequest(request)); - }); }); } @@ -257,7 +225,11 @@ class CapturingNavigationDelegate extends WKNavigationDelegate { // Records the last created instance of itself. class CapturingUIDelegate extends WKUIDelegate { - CapturingUIDelegate({super.onCreateWebView}) : super.detached() { + CapturingUIDelegate({ + super.onCreateWebView, + super.requestMediaCapturePermission, + super.instanceManager, + }) : super.detached() { lastCreatedDelegate = this; } static CapturingUIDelegate lastCreatedDelegate = CapturingUIDelegate(); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart index dc7085bc52e..b4a8439e285 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart @@ -39,6 +39,7 @@ void main() { WebKitWebViewController createControllerWithMocks({ MockUIScrollView? mockScrollView, MockWKPreferences? mockPreferences, + WKUIDelegate? uiDelegate, MockWKUserContentController? mockUserContentController, MockWKWebsiteDataStore? mockWebsiteDataStore, MockWKWebView Function( @@ -79,6 +80,27 @@ void main() { ); return nonNullMockWebView; }, + createUIDelegate: ({ + void Function( + WKWebView webView, + WKWebViewConfiguration configuration, + WKNavigationAction navigationAction, + )? onCreateWebView, + Future Function( + WKUIDelegate instance, + WKWebView webView, + WKSecurityOrigin origin, + WKFrameInfo frame, + WKMediaCaptureType type, + )? requestMediaCapturePermission, + InstanceManager? instanceManager, + }) { + return uiDelegate ?? + CapturingUIDelegate( + onCreateWebView: onCreateWebView, + requestMediaCapturePermission: requestMediaCapturePermission, + ); + }, ), instanceManager: instanceManager, ); @@ -811,11 +833,6 @@ void main() { CapturingNavigationDelegate.lastCreatedDelegate, ), ); - verify( - mockWebView.setUIDelegate( - CapturingUIDelegate.lastCreatedDelegate, - ), - ); }); test('setPlatformNavigationDelegate onProgress', () async { @@ -877,8 +894,32 @@ void main() { expect(callbackProgress, 0); }); + test('Requests to open a new window loads request in same window', () { + // Reset last created delegate. + CapturingUIDelegate.lastCreatedDelegate = CapturingUIDelegate(); + + // Create a new WebKitWebViewController that sets + // CapturingUIDelegate.lastCreatedDelegate. + createControllerWithMocks(); + + final MockWKWebView mockWebView = MockWKWebView(); + const NSUrlRequest request = NSUrlRequest(url: 'https://www.google.com'); + + CapturingUIDelegate.lastCreatedDelegate.onCreateWebView!( + mockWebView, + WKWebViewConfiguration.detached(), + const WKNavigationAction( + request: request, + targetFrame: WKFrameInfo(isMainFrame: false), + navigationType: WKNavigationType.linkActivated, + ), + ); + + verify(mockWebView.loadRequest(request)); + }); + test( - 'setPlatformNavigationDelegate onProgress can be changed by the WebKitNavigationDelegage', + 'setPlatformNavigationDelegate onProgress can be changed by the WebKitNavigationDelegate', () async { final MockWKWebView mockWebView = MockWKWebView(); @@ -1013,6 +1054,40 @@ void main() { instanceManager.getIdentifier(mockWebView), ); }); + + test('setOnPermissionRequest', () async { + final WebKitWebViewController controller = createControllerWithMocks(); + + late final PlatformWebViewPermissionRequest permissionRequest; + await controller.setOnPlatformPermissionRequest( + (PlatformWebViewPermissionRequest request) async { + permissionRequest = request; + request.grant(); + }, + ); + + final Future Function( + WKUIDelegate instance, + WKWebView webView, + WKSecurityOrigin origin, + WKFrameInfo frame, + WKMediaCaptureType type, + ) onPermissionRequestCallback = CapturingUIDelegate + .lastCreatedDelegate.requestMediaCapturePermission!; + + final WKPermissionDecision decision = await onPermissionRequestCallback( + CapturingUIDelegate.lastCreatedDelegate, + WKWebView.detached(), + const WKSecurityOrigin(host: '', port: 0, protocol: ''), + const WKFrameInfo(isMainFrame: false), + WKMediaCaptureType.microphone, + ); + + expect(permissionRequest.types, [ + WebViewPermissionResourceType.microphone, + ]); + expect(decision, WKPermissionDecision.grant); + }); }); group('WebKitJavaScriptChannelParams', () { @@ -1070,7 +1145,11 @@ class CapturingNavigationDelegate extends WKNavigationDelegate { // Records the last created instance of itself. class CapturingUIDelegate extends WKUIDelegate { - CapturingUIDelegate({super.onCreateWebView}) : super.detached() { + CapturingUIDelegate({ + super.onCreateWebView, + super.requestMediaCapturePermission, + super.instanceManager, + }) : super.detached() { lastCreatedDelegate = this; } static CapturingUIDelegate lastCreatedDelegate = CapturingUIDelegate(); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart index bc02bea0328..443e3a7163a 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart'; import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; @@ -13,7 +14,7 @@ import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; import 'webkit_webview_widget_test.mocks.dart'; -@GenerateMocks([WKWebViewConfiguration]) +@GenerateMocks([WKUIDelegate, WKWebViewConfiguration]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -25,26 +26,33 @@ void main() { final WebKitWebViewController controller = WebKitWebViewController( WebKitWebViewControllerCreationParams( - webKitProxy: WebKitProxy( - createWebView: ( - WKWebViewConfiguration configuration, { - void Function( - String keyPath, - NSObject object, - Map change, - )? observeValue, - InstanceManager? instanceManager, - }) { - final WKWebView webView = WKWebView.detached( - instanceManager: testInstanceManager, - ); - testInstanceManager.addDartCreatedInstance(webView); - return webView; - }, - createWebViewConfiguration: ({InstanceManager? instanceManager}) { - return MockWKWebViewConfiguration(); - }, - ), + webKitProxy: WebKitProxy(createWebView: ( + WKWebViewConfiguration configuration, { + void Function( + String keyPath, + NSObject object, + Map change, + )? observeValue, + InstanceManager? instanceManager, + }) { + final WKWebView webView = WKWebView.detached( + instanceManager: testInstanceManager, + ); + testInstanceManager.addDartCreatedInstance(webView); + return webView; + }, createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return MockWKWebViewConfiguration(); + }, createUIDelegate: ({ + dynamic onCreateWebView, + dynamic requestMediaCapturePermission, + InstanceManager? instanceManager, + }) { + final MockWKUIDelegate mockWKUIDelegate = MockWKUIDelegate(); + when(mockWKUIDelegate.copy()).thenReturn(MockWKUIDelegate()); + + testInstanceManager.addDartCreatedInstance(mockWKUIDelegate); + return mockWKUIDelegate; + }), ), ); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.mocks.dart index 1b4807eb0cc..fe86de77af0 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.mocks.dart @@ -21,9 +21,19 @@ import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart' as _i2; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeWKUserContentController_0 extends _i1.SmartFake +class _FakeWKUIDelegate_0 extends _i1.SmartFake implements _i2.WKUIDelegate { + _FakeWKUIDelegate_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKUserContentController_1 extends _i1.SmartFake implements _i2.WKUserContentController { - _FakeWKUserContentController_0( + _FakeWKUserContentController_1( Object parent, Invocation parentInvocation, ) : super( @@ -32,8 +42,8 @@ class _FakeWKUserContentController_0 extends _i1.SmartFake ); } -class _FakeWKPreferences_1 extends _i1.SmartFake implements _i2.WKPreferences { - _FakeWKPreferences_1( +class _FakeWKPreferences_2 extends _i1.SmartFake implements _i2.WKPreferences { + _FakeWKPreferences_2( Object parent, Invocation parentInvocation, ) : super( @@ -42,9 +52,9 @@ class _FakeWKPreferences_1 extends _i1.SmartFake implements _i2.WKPreferences { ); } -class _FakeWKWebsiteDataStore_2 extends _i1.SmartFake +class _FakeWKWebsiteDataStore_3 extends _i1.SmartFake implements _i2.WKWebsiteDataStore { - _FakeWKWebsiteDataStore_2( + _FakeWKWebsiteDataStore_3( Object parent, Invocation parentInvocation, ) : super( @@ -53,9 +63,9 @@ class _FakeWKWebsiteDataStore_2 extends _i1.SmartFake ); } -class _FakeWKWebViewConfiguration_3 extends _i1.SmartFake +class _FakeWKWebViewConfiguration_4 extends _i1.SmartFake implements _i2.WKWebViewConfiguration { - _FakeWKWebViewConfiguration_3( + _FakeWKWebViewConfiguration_4( Object parent, Invocation parentInvocation, ) : super( @@ -64,6 +74,63 @@ class _FakeWKWebViewConfiguration_3 extends _i1.SmartFake ); } +/// A class which mocks [WKUIDelegate]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKUIDelegate extends _i1.Mock implements _i2.WKUIDelegate { + MockWKUIDelegate() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.WKUIDelegate copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWKUIDelegate_0( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WKUIDelegate); + @override + _i3.Future addObserver( + _i4.NSObject? observer, { + required String? keyPath, + required Set<_i4.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + @override + _i3.Future removeObserver( + _i4.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); +} + /// A class which mocks [WKWebViewConfiguration]. /// /// See the documentation for Mockito's code generation for more information. @@ -77,7 +144,7 @@ class MockWKWebViewConfiguration extends _i1.Mock @override _i2.WKUserContentController get userContentController => (super.noSuchMethod( Invocation.getter(#userContentController), - returnValue: _FakeWKUserContentController_0( + returnValue: _FakeWKUserContentController_1( this, Invocation.getter(#userContentController), ), @@ -85,7 +152,7 @@ class MockWKWebViewConfiguration extends _i1.Mock @override _i2.WKPreferences get preferences => (super.noSuchMethod( Invocation.getter(#preferences), - returnValue: _FakeWKPreferences_1( + returnValue: _FakeWKPreferences_2( this, Invocation.getter(#preferences), ), @@ -93,7 +160,7 @@ class MockWKWebViewConfiguration extends _i1.Mock @override _i2.WKWebsiteDataStore get websiteDataStore => (super.noSuchMethod( Invocation.getter(#websiteDataStore), - returnValue: _FakeWKWebsiteDataStore_2( + returnValue: _FakeWKWebsiteDataStore_3( this, Invocation.getter(#websiteDataStore), ), @@ -125,7 +192,7 @@ class MockWKWebViewConfiguration extends _i1.Mock #copy, [], ), - returnValue: _FakeWKWebViewConfiguration_3( + returnValue: _FakeWKWebViewConfiguration_4( this, Invocation.method( #copy, From e384ccad927b6c98469273e28c8815bf47acc25e Mon Sep 17 00:00:00 2001 From: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> Date: Fri, 21 Apr 2023 15:37:58 -0400 Subject: [PATCH 2/2] update interface version --- .../plugins/webviewflutter/PermissionRequestTest.java | 3 +++ .../webview_flutter_android/example/pubspec.yaml | 8 +------- .../webview_flutter/webview_flutter_android/pubspec.yaml | 8 +------- .../webview_flutter_wkwebview/example/pubspec.yaml | 8 +------- .../webview_flutter_wkwebview/pubspec.yaml | 8 +------- 5 files changed, 7 insertions(+), 28 deletions(-) diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PermissionRequestTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PermissionRequestTest.java index d359d519121..8e7756936c8 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PermissionRequestTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PermissionRequestTest.java @@ -51,6 +51,9 @@ public void enums() { assertEquals(PermissionRequest.RESOURCE_AUDIO_CAPTURE, "android.webkit.resource.AUDIO_CAPTURE"); assertEquals(PermissionRequest.RESOURCE_VIDEO_CAPTURE, "android.webkit.resource.VIDEO_CAPTURE"); assertEquals(PermissionRequest.RESOURCE_MIDI_SYSEX, "android.webkit.resource.MIDI_SYSEX"); + assertEquals( + PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID, + "android.webkit.resource.PROTECTED_MEDIA_ID"); } @Test diff --git a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml index af4ad50821e..bcaebcba882 100644 --- a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - webview_flutter_platform_interface: ^2.1.0 + webview_flutter_platform_interface: ^2.3.0 dev_dependencies: espresso: ^0.2.0 @@ -35,9 +35,3 @@ flutter: - assets/sample_video.mp4 - assets/www/index.html - assets/www/styles/style.css - - -# FOR TESTING ONLY. DO NOT MERGE. -dependency_overrides: - webview_flutter_platform_interface: - path: ../../../webview_flutter/webview_flutter_platform_interface diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index c4d13ca813f..9accd85746a 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -20,7 +20,7 @@ flutter: dependencies: flutter: sdk: flutter - webview_flutter_platform_interface: ^2.1.0 + webview_flutter_platform_interface: ^2.3.0 dev_dependencies: build_runner: ^2.1.4 @@ -30,9 +30,3 @@ dev_dependencies: sdk: flutter mockito: 5.4.0 pigeon: ^9.2.4 - - -# FOR TESTING ONLY. DO NOT MERGE. -dependency_overrides: - webview_flutter_platform_interface: - path: ../../webview_flutter/webview_flutter_platform_interface diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml index a26bce6dc7b..e67ec2bd6f5 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml @@ -10,7 +10,7 @@ dependencies: flutter: sdk: flutter path_provider: ^2.0.6 - webview_flutter_platform_interface: ^2.1.0 + webview_flutter_platform_interface: ^2.3.0 webview_flutter_wkwebview: # When depending on this package from a real application you should use: # webview_flutter: ^x.y.z @@ -34,9 +34,3 @@ flutter: - assets/sample_video.mp4 - assets/www/index.html - assets/www/styles/style.css - - -# FOR TESTING ONLY. DO NOT MERGE. -dependency_overrides: - webview_flutter_platform_interface: - path: ../../../webview_flutter/webview_flutter_platform_interface diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index e742525b508..915be555f56 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: flutter: sdk: flutter path: ^1.8.0 - webview_flutter_platform_interface: ^2.1.0 + webview_flutter_platform_interface: ^2.3.0 dev_dependencies: build_runner: ^2.1.5 @@ -30,9 +30,3 @@ dev_dependencies: sdk: flutter mockito: 5.4.0 pigeon: ^9.2.4 - - -# FOR TESTING ONLY. DO NOT MERGE. -dependency_overrides: - webview_flutter_platform_interface: - path: ../../webview_flutter/webview_flutter_platform_interface