[webview_flutter] Add Android implementations for new cookie manager, to allow setting cookies directly and on webview creation. (#4557)
diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md
index ce73ab2..7bd3387 100644
--- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md
+++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.8.0
+
+* Implements new cookie manager for setting cookies and providing initial cookies.
+
## 2.7.0
* Adds support for the `loadRequest` method from the platform interface.
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImpl.java
new file mode 100644
index 0000000..3e38ce9
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImpl.java
@@ -0,0 +1,29 @@
+// 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.CookieManager;
+
+class CookieManagerHostApiImpl implements GeneratedAndroidWebView.CookieManagerHostApi {
+ @Override
+ public void clearCookies(GeneratedAndroidWebView.Result<Boolean> result) {
+ CookieManager cookieManager = CookieManager.getInstance();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ cookieManager.removeAllCookies(result::success);
+ } else {
+ final boolean hasCookies = cookieManager.hasCookies();
+ if (hasCookies) {
+ cookieManager.removeAllCookie();
+ }
+ result.success(hasCookies);
+ }
+ }
+
+ @Override
+ public void setCookie(String url, String value) {
+ CookieManager.getInstance().setCookie(url, value);
+ }
+}
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java
deleted file mode 100644
index df3f21d..0000000
--- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-package io.flutter.plugins.webviewflutter;
-
-import android.os.Build;
-import android.os.Build.VERSION_CODES;
-import android.webkit.CookieManager;
-import android.webkit.ValueCallback;
-import io.flutter.plugin.common.BinaryMessenger;
-import io.flutter.plugin.common.MethodCall;
-import io.flutter.plugin.common.MethodChannel;
-import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
-import io.flutter.plugin.common.MethodChannel.Result;
-
-class FlutterCookieManager implements MethodCallHandler {
- private final MethodChannel methodChannel;
-
- FlutterCookieManager(BinaryMessenger messenger) {
- methodChannel = new MethodChannel(messenger, "plugins.flutter.io/cookie_manager");
- methodChannel.setMethodCallHandler(this);
- }
-
- @Override
- public void onMethodCall(MethodCall methodCall, Result result) {
- switch (methodCall.method) {
- case "clearCookies":
- clearCookies(result);
- break;
- default:
- result.notImplemented();
- }
- }
-
- void dispose() {
- methodChannel.setMethodCallHandler(null);
- }
-
- private static void clearCookies(final Result result) {
- CookieManager cookieManager = CookieManager.getInstance();
- final boolean hasCookies = cookieManager.hasCookies();
- if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
- cookieManager.removeAllCookies(
- new ValueCallback<Boolean>() {
- @Override
- public void onReceiveValue(Boolean value) {
- result.success(hasCookies);
- }
- });
- } else {
- cookieManager.removeAllCookie();
- result.success(hasCookies);
- }
- }
-}
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 0e6776e..8ef0b8d 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
@@ -162,6 +162,94 @@
void error(Throwable error);
}
+ private static class CookieManagerHostApiCodec extends StandardMessageCodec {
+ public static final CookieManagerHostApiCodec INSTANCE = new CookieManagerHostApiCodec();
+
+ private CookieManagerHostApiCodec() {}
+ }
+
+ /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
+ public interface CookieManagerHostApi {
+ void clearCookies(Result<Boolean> result);
+
+ void setCookie(String url, String value);
+
+ /** The codec used by CookieManagerHostApi. */
+ static MessageCodec<Object> getCodec() {
+ return CookieManagerHostApiCodec.INSTANCE;
+ }
+
+ /**
+ * Sets up an instance of `CookieManagerHostApi` to handle messages through the
+ * `binaryMessenger`.
+ */
+ static void setup(BinaryMessenger binaryMessenger, CookieManagerHostApi api) {
+ {
+ BasicMessageChannel<Object> channel =
+ new BasicMessageChannel<>(
+ binaryMessenger,
+ "dev.flutter.pigeon.CookieManagerHostApi.clearCookies",
+ getCodec());
+ if (api != null) {
+ channel.setMessageHandler(
+ (message, reply) -> {
+ Map<String, Object> wrapped = new HashMap<>();
+ try {
+ Result<Boolean> resultCallback =
+ new Result<Boolean>() {
+ public void success(Boolean result) {
+ wrapped.put("result", result);
+ reply.reply(wrapped);
+ }
+
+ public void error(Throwable error) {
+ wrapped.put("error", wrapError(error));
+ reply.reply(wrapped);
+ }
+ };
+
+ api.clearCookies(resultCallback);
+ } catch (Error | RuntimeException exception) {
+ wrapped.put("error", wrapError(exception));
+ reply.reply(wrapped);
+ }
+ });
+ } else {
+ channel.setMessageHandler(null);
+ }
+ }
+ {
+ BasicMessageChannel<Object> channel =
+ new BasicMessageChannel<>(
+ binaryMessenger, "dev.flutter.pigeon.CookieManagerHostApi.setCookie", getCodec());
+ if (api != null) {
+ channel.setMessageHandler(
+ (message, reply) -> {
+ Map<String, Object> wrapped = new HashMap<>();
+ try {
+ ArrayList<Object> args = (ArrayList<Object>) message;
+ String urlArg = (String) args.get(0);
+ if (urlArg == null) {
+ throw new NullPointerException("urlArg unexpectedly null.");
+ }
+ String valueArg = (String) args.get(1);
+ if (valueArg == null) {
+ throw new NullPointerException("valueArg unexpectedly null.");
+ }
+ api.setCookie(urlArg, valueArg);
+ wrapped.put("result", null);
+ } catch (Error | RuntimeException exception) {
+ wrapped.put("error", wrapError(exception));
+ }
+ reply.reply(wrapped);
+ });
+ } else {
+ channel.setMessageHandler(null);
+ }
+ }
+ }
+ }
+
private static class WebViewHostApiCodec extends StandardMessageCodec {
public static final WebViewHostApiCodec INSTANCE = new WebViewHostApiCodec();
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 cbeda8d..4ef622f 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
@@ -13,6 +13,7 @@
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.platform.PlatformViewRegistry;
+import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.CookieManagerHostApi;
import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerHostApi;
import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.FlutterAssetManagerHostApi;
import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelHostApi;
@@ -30,7 +31,6 @@
*/
public class WebViewFlutterPlugin implements FlutterPlugin, ActivityAware {
private FlutterPluginBinding pluginBinding;
- private FlutterCookieManager flutterCookieManager;
private WebViewHostApiImpl webViewHostApi;
private JavaScriptChannelHostApiImpl javaScriptChannelHostApi;
@@ -65,7 +65,6 @@
registrar.view(),
new FlutterAssetManager.RegistrarFlutterAssetManager(
registrar.context().getAssets(), registrar));
- new FlutterCookieManager(registrar.messenger());
}
private void setUp(
@@ -74,7 +73,6 @@
Context context,
View containerView,
FlutterAssetManager flutterAssetManager) {
- new FlutterCookieManager(binaryMessenger);
InstanceManager instanceManager = new InstanceManager();
@@ -117,6 +115,7 @@
instanceManager, new WebSettingsHostApiImpl.WebSettingsCreator()));
FlutterAssetManagerHostApi.setup(
binaryMessenger, new FlutterAssetManagerHostApiImpl(flutterAssetManager));
+ CookieManagerHostApi.setup(binaryMessenger, new CookieManagerHostApiImpl());
}
@Override
@@ -132,14 +131,7 @@
}
@Override
- public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
- if (flutterCookieManager == null) {
- return;
- }
-
- flutterCookieManager.dispose();
- flutterCookieManager = null;
- }
+ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {}
@Override
public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBinding) {
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImplTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImplTest.java
new file mode 100644
index 0000000..6daeb1b
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImplTest.java
@@ -0,0 +1,83 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.webviewflutter;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.Build;
+import android.webkit.CookieManager;
+import android.webkit.ValueCallback;
+import io.flutter.plugins.webviewflutter.utils.TestUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockedStatic;
+
+public class CookieManagerHostApiImplTest {
+
+ private CookieManager cookieManager;
+ private MockedStatic<CookieManager> staticMockCookieManager;
+
+ @Before
+ public void setup() {
+ staticMockCookieManager = mockStatic(CookieManager.class);
+ cookieManager = mock(CookieManager.class);
+ when(CookieManager.getInstance()).thenReturn(cookieManager);
+ when(cookieManager.hasCookies()).thenReturn(true);
+ doAnswer(
+ answer -> {
+ ((ValueCallback<Boolean>) answer.getArgument(0)).onReceiveValue(true);
+ return null;
+ })
+ .when(cookieManager)
+ .removeAllCookies(any());
+ }
+
+ @After
+ public void tearDown() {
+ staticMockCookieManager.close();
+ }
+
+ @Test
+ public void setCookieShouldCallSetCookie() {
+ // Setup
+ CookieManagerHostApiImpl impl = new CookieManagerHostApiImpl();
+ // Run
+ impl.setCookie("flutter.dev", "foo=bar; path=/");
+ // Verify
+ verify(cookieManager).setCookie("flutter.dev", "foo=bar; path=/");
+ }
+
+ @Test
+ public void clearCookiesShouldCallRemoveAllCookiesOnAndroidLAbove() {
+ // Setup
+ TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.LOLLIPOP);
+ GeneratedAndroidWebView.Result<Boolean> result = mock(GeneratedAndroidWebView.Result.class);
+ CookieManagerHostApiImpl impl = new CookieManagerHostApiImpl();
+ // Run
+ impl.clearCookies(result);
+ // Verify
+ verify(cookieManager).removeAllCookies(any());
+ verify(result).success(true);
+ }
+
+ @Test
+ public void clearCookiesShouldCallRemoveAllCookieBelowAndroidL() {
+ // Setup
+ TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.KITKAT_WATCH);
+ GeneratedAndroidWebView.Result<Boolean> result = mock(GeneratedAndroidWebView.Result.class);
+ CookieManagerHostApiImpl impl = new CookieManagerHostApiImpl();
+ // Run
+ impl.clearCookies(result);
+ // Verify
+ verify(cookieManager).removeAllCookie();
+ verify(result).success(true);
+ }
+}
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/utils/TestUtils.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/utils/TestUtils.java
new file mode 100644
index 0000000..31e7d58
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/utils/TestUtils.java
@@ -0,0 +1,47 @@
+// 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.utils;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import org.junit.Assert;
+
+public class TestUtils {
+ public static <T> void setFinalStatic(Class<T> classToModify, String fieldName, Object newValue) {
+ try {
+ Field field = classToModify.getField(fieldName);
+ field.setAccessible(true);
+
+ Field modifiersField = Field.class.getDeclaredField("modifiers");
+ modifiersField.setAccessible(true);
+ modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
+
+ field.set(null, newValue);
+ } catch (Exception e) {
+ Assert.fail("Unable to mock static field: " + fieldName);
+ }
+ }
+
+ public static <T> void setPrivateField(T instance, String fieldName, Object newValue) {
+ try {
+ Field field = instance.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ field.set(instance, newValue);
+ } catch (Exception e) {
+ Assert.fail("Unable to mock private field: " + fieldName);
+ }
+ }
+
+ public static <T> Object getPrivateField(T instance, String fieldName) {
+ try {
+ Field field = instance.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ return field.get(instance);
+ } catch (Exception e) {
+ Assert.fail("Unable to mock private field: " + fieldName);
+ return null;
+ }
+ }
+}
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 eb3162a..0d0cd59 100644
--- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart
+++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart
@@ -191,6 +191,7 @@
loadHtmlString,
transparentBackground,
doPostRequest,
+ setCookie,
}
class _SampleMenu extends StatelessWidget {
@@ -244,6 +245,9 @@
case _MenuOptions.doPostRequest:
_onDoPostRequest(controller.data!, context);
break;
+ case _MenuOptions.setCookie:
+ _onSetCookie(controller.data!, context);
+ break;
}
},
itemBuilder: (BuildContext context) => <PopupMenuItem<_MenuOptions>>[
@@ -297,6 +301,10 @@
value: _MenuOptions.doPostRequest,
child: Text('Post Request'),
),
+ const PopupMenuItem<_MenuOptions>(
+ value: _MenuOptions.setCookie,
+ child: Text('Set Cookie'),
+ ),
],
);
},
@@ -356,7 +364,7 @@
Future<void> _onClearCookies(
WebViewController controller, BuildContext context) async {
- final bool hadCookies = await WebView.platform.clearCookies();
+ final bool hadCookies = await WebViewCookieManager.instance.clearCookies();
String message = 'There were cookies. Now, they are gone!';
if (!hadCookies) {
message = 'There are no cookies.';
@@ -367,6 +375,15 @@
));
}
+ Future<void> _onSetCookie(
+ WebViewController controller, BuildContext context) async {
+ await WebViewCookieManager.instance.setCookie(
+ const WebViewCookie(
+ name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything'),
+ );
+ await controller.loadUrl('https://httpbin.org/anything');
+ }
+
Future<void> _onNavigationDelegateExample(
WebViewController controller, BuildContext context) async {
final String contentBase64 =
diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart
index adaf7fb..91ea663 100644
--- a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart
+++ b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart
@@ -3,11 +3,13 @@
// found in the LICENSE file.
import 'dart:async';
+import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:webview_flutter_android/webview_android.dart';
+import 'package:webview_flutter_android/webview_android_cookie_manager.dart';
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
import 'navigation_decision.dart';
@@ -61,6 +63,7 @@
Key? key,
this.onWebViewCreated,
this.initialUrl,
+ this.initialCookies = const <WebViewCookie>[],
this.javascriptMode = JavascriptMode.disabled,
this.javascriptChannels,
this.navigationDelegate,
@@ -104,6 +107,9 @@
/// The initial URL to load.
final String? initialUrl;
+ /// The initial cookies to set.
+ final List<WebViewCookie> initialCookies;
+
/// Whether JavaScript execution is enabled.
final JavascriptMode javascriptMode;
@@ -295,6 +301,7 @@
autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy,
userAgent: widget.userAgent,
backgroundColor: widget.backgroundColor,
+ cookies: widget.initialCookies,
),
javascriptChannelRegistry: _javascriptChannelRegistry,
);
@@ -674,3 +681,21 @@
zoomEnabled: widget.zoomEnabled,
);
}
+
+/// App-facing cookie manager that exposes the correct platform implementation.
+class WebViewCookieManager extends WebViewCookieManagerPlatform {
+ WebViewCookieManager._();
+
+ /// Returns an instance of the cookie manager for the current platform.
+ static WebViewCookieManagerPlatform get instance {
+ if (WebViewCookieManagerPlatform.instance == null) {
+ if (Platform.isAndroid) {
+ WebViewCookieManagerPlatform.instance = WebViewAndroidCookieManager();
+ } else {
+ throw AssertionError(
+ 'This platform is currently unsupported for webview_flutter_android.');
+ }
+ }
+ return WebViewCookieManagerPlatform.instance!;
+ }
+}
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 d027016..dfa05cd 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
@@ -375,6 +375,49 @@
}
}
+/// Manages cookies globally for all webviews.
+class CookieManager {
+ CookieManager._();
+
+ static CookieManager? _instance;
+
+ /// Gets the globally set CookieManager instance.
+ static CookieManager get instance => _instance ??= CookieManager._();
+
+ /// Setter for the singleton value, for testing purposes only.
+ @visibleForTesting
+ static set instance(CookieManager value) => _instance = value;
+
+ /// Pigeon Host Api implementation for [CookieManager].
+ @visibleForTesting
+ static CookieManagerHostApi api = CookieManagerHostApi();
+
+ /// Sets a single cookie (key-value pair) for the given URL. Any existing
+ /// cookie with the same host, path and name will be replaced with the new
+ /// cookie. The cookie being set will be ignored if it is expired. To set
+ /// multiple cookies, your application should invoke this method multiple
+ /// times.
+ ///
+ /// The value parameter must follow the format of the Set-Cookie HTTP
+ /// response header defined by RFC6265bis. This is a key-value pair of the
+ /// form "key=value", optionally followed by a list of cookie attributes
+ /// delimited with semicolons (ex. "key=value; Max-Age=123"). Please consult
+ /// the RFC specification for a list of valid attributes.
+ ///
+ /// Note: if specifying a value containing the "Secure" attribute, url must
+ /// use the "https://" scheme.
+ ///
+ /// Params:
+ /// url – the URL for which the cookie is to be set
+ /// value – the cookie as a string, using the format of the 'Set-Cookie' HTTP response header
+ Future<void> setCookie(String url, String value) => api.setCookie(url, value);
+
+ /// Removes all cookies.
+ ///
+ /// The returned future resolves to true if any cookies were removed.
+ Future<bool> clearCookies() => api.clearCookies();
+}
+
/// Manages settings state for a [WebView].
///
/// When a WebView is first created, it obtains a set of default settings. These
diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart
index e0a1db3..810a717 100644
--- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart
+++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart
@@ -63,6 +63,72 @@
}
}
+class _CookieManagerHostApiCodec extends StandardMessageCodec {
+ const _CookieManagerHostApiCodec();
+}
+
+class CookieManagerHostApi {
+ /// Constructor for [CookieManagerHostApi]. 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.
+ CookieManagerHostApi({BinaryMessenger? binaryMessenger})
+ : _binaryMessenger = binaryMessenger;
+
+ final BinaryMessenger? _binaryMessenger;
+
+ static const MessageCodec<Object?> codec = _CookieManagerHostApiCodec();
+
+ Future<bool> clearCookies() async {
+ final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+ 'dev.flutter.pigeon.CookieManagerHostApi.clearCookies', codec,
+ binaryMessenger: _binaryMessenger);
+ final Map<Object?, Object?>? replyMap =
+ await channel.send(null) as Map<Object?, Object?>?;
+ if (replyMap == null) {
+ throw PlatformException(
+ code: 'channel-error',
+ message: 'Unable to establish connection on channel.',
+ details: null,
+ );
+ } else if (replyMap['error'] != null) {
+ final Map<Object?, Object?> error =
+ (replyMap['error'] as Map<Object?, Object?>?)!;
+ throw PlatformException(
+ code: (error['code'] as String?)!,
+ message: error['message'] as String?,
+ details: error['details'],
+ );
+ } else {
+ return (replyMap['result'] as bool?)!;
+ }
+ }
+
+ Future<void> setCookie(String arg_url, String arg_value) async {
+ final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+ 'dev.flutter.pigeon.CookieManagerHostApi.setCookie', codec,
+ binaryMessenger: _binaryMessenger);
+ final Map<Object?, Object?>? replyMap = await channel
+ .send(<Object>[arg_url, arg_value]) as Map<Object?, Object?>?;
+ if (replyMap == null) {
+ throw PlatformException(
+ code: 'channel-error',
+ message: 'Unable to establish connection on channel.',
+ details: null,
+ );
+ } else if (replyMap['error'] != null) {
+ final Map<Object?, Object?> error =
+ (replyMap['error'] as Map<Object?, Object?>?)!;
+ throw PlatformException(
+ code: (error['code'] as String?)!,
+ message: error['message'] as String?,
+ details: error['details'],
+ );
+ } else {
+ return;
+ }
+ }
+}
+
class _WebViewHostApiCodec extends StandardMessageCodec {
const _WebViewHostApiCodec();
}
diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart
index ee474b9..1f0eb7b 100644
--- a/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart
+++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart
@@ -65,5 +65,11 @@
}
@override
- Future<bool> clearCookies() => MethodChannelWebViewPlatform.clearCookies();
+ Future<bool> clearCookies() {
+ if (WebViewCookieManagerPlatform.instance == null) {
+ throw Exception(
+ 'Could not clear cookies as no implementation for WebViewCookieManagerPlatform has been registered.');
+ }
+ return WebViewCookieManagerPlatform.instance!.clearCookies();
+ }
}
diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android_cookie_manager.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_android_cookie_manager.dart
new file mode 100644
index 0000000..bba75ff
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android_cookie_manager.dart
@@ -0,0 +1,36 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:webview_flutter_android/src/android_webview.dart'
+ as android_webview;
+import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
+
+/// Handles all cookie operations for the current platform.
+class WebViewAndroidCookieManager extends WebViewCookieManagerPlatform {
+ @override
+ Future<bool> clearCookies() =>
+ android_webview.CookieManager.instance.clearCookies();
+
+ @override
+ Future<void> setCookie(WebViewCookie cookie) {
+ if (!_isValidPath(cookie.path)) {
+ throw ArgumentError(
+ 'The path property for the provided cookie was not given a legal value.');
+ }
+ return android_webview.CookieManager.instance.setCookie(
+ cookie.domain,
+ '${Uri.encodeComponent(cookie.name)}=${Uri.encodeComponent(cookie.value)}; path=${cookie.path}',
+ );
+ }
+
+ bool _isValidPath(String path) {
+ // Permitted ranges based on RFC6265bis: https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-02#section-4.1.1
+ for (final int char in path.codeUnits) {
+ if ((char < 0x20 || char > 0x3A) && (char < 0x3C || char > 0x7E)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart
index fc295be..1dec9c1 100644
--- a/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart
+++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart
@@ -6,6 +6,7 @@
import 'dart:typed_data';
import 'package:flutter/widgets.dart';
+import 'package:webview_flutter_android/webview_android_cookie_manager.dart';
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
import 'src/android_webview.dart' as android_webview;
@@ -363,6 +364,14 @@
}
addJavascriptChannels(creationParams.javascriptChannelNames);
+
+ // TODO(BeMacized): Remove once platform implementations
+ // are able to register themselves (Flutter >=2.8),
+ // https://github.com/flutter/flutter/issues/94224
+ WebViewCookieManagerPlatform.instance ??= WebViewAndroidCookieManager();
+
+ creationParams.cookies
+ .forEach(WebViewCookieManagerPlatform.instance!.setCookie);
}
Future<void> _setHasProgressTracking(bool hasProgressTracking) async {
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 4907dad..36862f7 100644
--- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart
+++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart
@@ -18,6 +18,14 @@
String? description;
}
+@HostApi()
+abstract class CookieManagerHostApi {
+ @async
+ bool clearCookies();
+
+ void setCookie(String url, String value);
+}
+
@HostApi(dartHostTestHandler: 'TestWebViewHostApi')
abstract class WebViewHostApi {
void create(int instanceId, bool useHybridComposition);
diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml
index 08bcbb5..34bea57 100644
--- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml
+++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml
@@ -2,7 +2,7 @@
description: A Flutter plugin that provides a WebView widget on Android.
repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_android
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22
-version: 2.7.0
+version: 2.8.0
environment:
sdk: ">=2.14.0 <3.0.0"
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 b903678..cc29fc7 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
@@ -14,6 +14,7 @@
import 'android_webview_test.mocks.dart';
@GenerateMocks(<Type>[
+ CookieManagerHostApi,
DownloadListener,
JavaScriptChannel,
TestDownloadListenerHostApi,
@@ -647,4 +648,20 @@
});
});
});
+
+ group('CookieManager', () {
+ test('setCookie calls setCookie on CookieManagerHostApi', () {
+ CookieManager.api = MockCookieManagerHostApi();
+ CookieManager.instance.setCookie('foo', 'bar');
+ verify(CookieManager.api.setCookie('foo', 'bar'));
+ });
+
+ test('clearCookies calls clearCookies on CookieManagerHostApi', () {
+ CookieManager.api = MockCookieManagerHostApi();
+ when(CookieManager.api.clearCookies())
+ .thenAnswer((_) => Future<bool>.value(true));
+ CookieManager.instance.clearCookies();
+ verify(CookieManager.api.clearCookies());
+ });
+ });
}
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 8bb5694..d25d233 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
@@ -6,14 +6,15 @@
// in webview_flutter_android/test/android_webview_test.dart.
// Do not manually edit this file.
-import 'dart:async' as _i5;
-import 'dart:typed_data' as _i4;
-import 'dart:ui' as _i6;
+import 'dart:async' as _i4;
+import 'dart:typed_data' as _i6;
+import 'dart:ui' as _i7;
import 'package:mockito/mockito.dart' as _i1;
import 'package:webview_flutter_android/src/android_webview.dart' as _i2;
+import 'package:webview_flutter_android/src/android_webview.pigeon.dart' as _i3;
-import 'android_webview.pigeon.dart' as _i3;
+import 'android_webview.pigeon.dart' as _i5;
// ignore_for_file: avoid_redundant_argument_values
// ignore_for_file: avoid_setters_without_getters
@@ -26,6 +27,28 @@
class _FakeWebSettings_0 extends _i1.Fake implements _i2.WebSettings {}
+/// A class which mocks [CookieManagerHostApi].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockCookieManagerHostApi extends _i1.Mock
+ implements _i3.CookieManagerHostApi {
+ MockCookieManagerHostApi() {
+ _i1.throwOnMissingStub(this);
+ }
+
+ @override
+ _i4.Future<bool> clearCookies() =>
+ (super.noSuchMethod(Invocation.method(#clearCookies, []),
+ returnValue: Future<bool>.value(false)) as _i4.Future<bool>);
+ @override
+ _i4.Future<void> setCookie(String? arg_url, String? arg_value) =>
+ (super.noSuchMethod(Invocation.method(#setCookie, [arg_url, arg_value]),
+ returnValue: Future<void>.value(),
+ returnValueForMissingStub: Future<void>.value()) as _i4.Future<void>);
+ @override
+ String toString() => super.toString();
+}
+
/// A class which mocks [DownloadListener].
///
/// See the documentation for Mockito's code generation for more information.
@@ -69,7 +92,7 @@
///
/// See the documentation for Mockito's code generation for more information.
class MockTestDownloadListenerHostApi extends _i1.Mock
- implements _i3.TestDownloadListenerHostApi {
+ implements _i5.TestDownloadListenerHostApi {
MockTestDownloadListenerHostApi() {
_i1.throwOnMissingStub(this);
}
@@ -86,7 +109,7 @@
///
/// See the documentation for Mockito's code generation for more information.
class MockTestJavaScriptChannelHostApi extends _i1.Mock
- implements _i3.TestJavaScriptChannelHostApi {
+ implements _i5.TestJavaScriptChannelHostApi {
MockTestJavaScriptChannelHostApi() {
_i1.throwOnMissingStub(this);
}
@@ -103,7 +126,7 @@
///
/// See the documentation for Mockito's code generation for more information.
class MockTestWebChromeClientHostApi extends _i1.Mock
- implements _i3.TestWebChromeClientHostApi {
+ implements _i5.TestWebChromeClientHostApi {
MockTestWebChromeClientHostApi() {
_i1.throwOnMissingStub(this);
}
@@ -121,7 +144,7 @@
///
/// See the documentation for Mockito's code generation for more information.
class MockTestWebSettingsHostApi extends _i1.Mock
- implements _i3.TestWebSettingsHostApi {
+ implements _i5.TestWebSettingsHostApi {
MockTestWebSettingsHostApi() {
_i1.throwOnMissingStub(this);
}
@@ -195,7 +218,7 @@
///
/// See the documentation for Mockito's code generation for more information.
class MockTestWebViewClientHostApi extends _i1.Mock
- implements _i3.TestWebViewClientHostApi {
+ implements _i5.TestWebViewClientHostApi {
MockTestWebViewClientHostApi() {
_i1.throwOnMissingStub(this);
}
@@ -213,7 +236,7 @@
///
/// See the documentation for Mockito's code generation for more information.
class MockTestWebViewHostApi extends _i1.Mock
- implements _i3.TestWebViewHostApi {
+ implements _i5.TestWebViewHostApi {
MockTestWebViewHostApi() {
_i1.throwOnMissingStub(this);
}
@@ -246,7 +269,7 @@
Invocation.method(#loadUrl, [instanceId, url, headers]),
returnValueForMissingStub: null);
@override
- void postUrl(int? instanceId, String? url, _i4.Uint8List? data) =>
+ void postUrl(int? instanceId, String? url, _i6.Uint8List? data) =>
super.noSuchMethod(Invocation.method(#postUrl, [instanceId, url, data]),
returnValueForMissingStub: null);
@override
@@ -279,12 +302,12 @@
Invocation.method(#clearCache, [instanceId, includeDiskFiles]),
returnValueForMissingStub: null);
@override
- _i5.Future<String> evaluateJavascript(
+ _i4.Future<String> evaluateJavascript(
int? instanceId, String? javascriptString) =>
(super.noSuchMethod(
Invocation.method(
#evaluateJavascript, [instanceId, javascriptString]),
- returnValue: Future<String>.value('')) as _i5.Future<String>);
+ returnValue: Future<String>.value('')) as _i4.Future<String>);
@override
String getTitle(int? instanceId) =>
(super.noSuchMethod(Invocation.method(#getTitle, [instanceId]),
@@ -353,7 +376,7 @@
///
/// See the documentation for Mockito's code generation for more information.
class MockTestAssetManagerHostApi extends _i1.Mock
- implements _i3.TestAssetManagerHostApi {
+ implements _i5.TestAssetManagerHostApi {
MockTestAssetManagerHostApi() {
_i1.throwOnMissingStub(this);
}
@@ -403,15 +426,15 @@
(super.noSuchMethod(Invocation.getter(#settings),
returnValue: _FakeWebSettings_0()) as _i2.WebSettings);
@override
- _i5.Future<void> loadData(
+ _i4.Future<void> loadData(
{String? data, String? mimeType, String? encoding}) =>
(super.noSuchMethod(
Invocation.method(#loadData, [],
{#data: data, #mimeType: mimeType, #encoding: encoding}),
returnValue: Future<void>.value(),
- returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+ returnValueForMissingStub: Future<void>.value()) as _i4.Future<void>);
@override
- _i5.Future<void> loadDataWithBaseUrl(
+ _i4.Future<void> loadDataWithBaseUrl(
{String? baseUrl,
String? data,
String? mimeType,
@@ -426,114 +449,114 @@
#historyUrl: historyUrl
}),
returnValue: Future<void>.value(),
- returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+ returnValueForMissingStub: Future<void>.value()) as _i4.Future<void>);
@override
- _i5.Future<void> loadUrl(String? url, Map<String, String>? headers) =>
+ _i4.Future<void> loadUrl(String? url, Map<String, String>? headers) =>
(super.noSuchMethod(Invocation.method(#loadUrl, [url, headers]),
returnValue: Future<void>.value(),
- returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+ returnValueForMissingStub: Future<void>.value()) as _i4.Future<void>);
@override
- _i5.Future<void> postUrl(String? url, _i4.Uint8List? data) =>
+ _i4.Future<void> postUrl(String? url, _i6.Uint8List? data) =>
(super.noSuchMethod(Invocation.method(#postUrl, [url, data]),
returnValue: Future<void>.value(),
- returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+ returnValueForMissingStub: Future<void>.value()) as _i4.Future<void>);
@override
- _i5.Future<String?> getUrl() =>
+ _i4.Future<String?> getUrl() =>
(super.noSuchMethod(Invocation.method(#getUrl, []),
- returnValue: Future<String?>.value()) as _i5.Future<String?>);
+ returnValue: Future<String?>.value()) as _i4.Future<String?>);
@override
- _i5.Future<bool> canGoBack() =>
+ _i4.Future<bool> canGoBack() =>
(super.noSuchMethod(Invocation.method(#canGoBack, []),
- returnValue: Future<bool>.value(false)) as _i5.Future<bool>);
+ returnValue: Future<bool>.value(false)) as _i4.Future<bool>);
@override
- _i5.Future<bool> canGoForward() =>
+ _i4.Future<bool> canGoForward() =>
(super.noSuchMethod(Invocation.method(#canGoForward, []),
- returnValue: Future<bool>.value(false)) as _i5.Future<bool>);
+ returnValue: Future<bool>.value(false)) as _i4.Future<bool>);
@override
- _i5.Future<void> goBack() =>
+ _i4.Future<void> goBack() =>
(super.noSuchMethod(Invocation.method(#goBack, []),
returnValue: Future<void>.value(),
- returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+ returnValueForMissingStub: Future<void>.value()) as _i4.Future<void>);
@override
- _i5.Future<void> goForward() =>
+ _i4.Future<void> goForward() =>
(super.noSuchMethod(Invocation.method(#goForward, []),
returnValue: Future<void>.value(),
- returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+ returnValueForMissingStub: Future<void>.value()) as _i4.Future<void>);
@override
- _i5.Future<void> reload() =>
+ _i4.Future<void> reload() =>
(super.noSuchMethod(Invocation.method(#reload, []),
returnValue: Future<void>.value(),
- returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+ returnValueForMissingStub: Future<void>.value()) as _i4.Future<void>);
@override
- _i5.Future<void> clearCache(bool? includeDiskFiles) =>
+ _i4.Future<void> clearCache(bool? includeDiskFiles) =>
(super.noSuchMethod(Invocation.method(#clearCache, [includeDiskFiles]),
returnValue: Future<void>.value(),
- returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+ returnValueForMissingStub: Future<void>.value()) as _i4.Future<void>);
@override
- _i5.Future<String?> evaluateJavascript(String? javascriptString) => (super
+ _i4.Future<String?> evaluateJavascript(String? javascriptString) => (super
.noSuchMethod(Invocation.method(#evaluateJavascript, [javascriptString]),
- returnValue: Future<String?>.value()) as _i5.Future<String?>);
+ returnValue: Future<String?>.value()) as _i4.Future<String?>);
@override
- _i5.Future<String?> getTitle() =>
+ _i4.Future<String?> getTitle() =>
(super.noSuchMethod(Invocation.method(#getTitle, []),
- returnValue: Future<String?>.value()) as _i5.Future<String?>);
+ returnValue: Future<String?>.value()) as _i4.Future<String?>);
@override
- _i5.Future<void> scrollTo(int? x, int? y) =>
+ _i4.Future<void> scrollTo(int? x, int? y) =>
(super.noSuchMethod(Invocation.method(#scrollTo, [x, y]),
returnValue: Future<void>.value(),
- returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+ returnValueForMissingStub: Future<void>.value()) as _i4.Future<void>);
@override
- _i5.Future<void> scrollBy(int? x, int? y) =>
+ _i4.Future<void> scrollBy(int? x, int? y) =>
(super.noSuchMethod(Invocation.method(#scrollBy, [x, y]),
returnValue: Future<void>.value(),
- returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+ returnValueForMissingStub: Future<void>.value()) as _i4.Future<void>);
@override
- _i5.Future<int> getScrollX() =>
+ _i4.Future<int> getScrollX() =>
(super.noSuchMethod(Invocation.method(#getScrollX, []),
- returnValue: Future<int>.value(0)) as _i5.Future<int>);
+ returnValue: Future<int>.value(0)) as _i4.Future<int>);
@override
- _i5.Future<int> getScrollY() =>
+ _i4.Future<int> getScrollY() =>
(super.noSuchMethod(Invocation.method(#getScrollY, []),
- returnValue: Future<int>.value(0)) as _i5.Future<int>);
+ returnValue: Future<int>.value(0)) as _i4.Future<int>);
@override
- _i5.Future<void> setWebViewClient(_i2.WebViewClient? webViewClient) =>
+ _i4.Future<void> setWebViewClient(_i2.WebViewClient? webViewClient) =>
(super.noSuchMethod(Invocation.method(#setWebViewClient, [webViewClient]),
returnValue: Future<void>.value(),
- returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+ returnValueForMissingStub: Future<void>.value()) as _i4.Future<void>);
@override
- _i5.Future<void> addJavaScriptChannel(
+ _i4.Future<void> addJavaScriptChannel(
_i2.JavaScriptChannel? javaScriptChannel) =>
(super.noSuchMethod(
Invocation.method(#addJavaScriptChannel, [javaScriptChannel]),
returnValue: Future<void>.value(),
- returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+ returnValueForMissingStub: Future<void>.value()) as _i4.Future<void>);
@override
- _i5.Future<void> removeJavaScriptChannel(
+ _i4.Future<void> removeJavaScriptChannel(
_i2.JavaScriptChannel? javaScriptChannel) =>
(super.noSuchMethod(
Invocation.method(#removeJavaScriptChannel, [javaScriptChannel]),
returnValue: Future<void>.value(),
- returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+ returnValueForMissingStub: Future<void>.value()) as _i4.Future<void>);
@override
- _i5.Future<void> setDownloadListener(_i2.DownloadListener? listener) =>
+ _i4.Future<void> setDownloadListener(_i2.DownloadListener? listener) =>
(super.noSuchMethod(Invocation.method(#setDownloadListener, [listener]),
returnValue: Future<void>.value(),
- returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+ returnValueForMissingStub: Future<void>.value()) as _i4.Future<void>);
@override
- _i5.Future<void> setWebChromeClient(_i2.WebChromeClient? client) =>
+ _i4.Future<void> setWebChromeClient(_i2.WebChromeClient? client) =>
(super.noSuchMethod(Invocation.method(#setWebChromeClient, [client]),
returnValue: Future<void>.value(),
- returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+ returnValueForMissingStub: Future<void>.value()) as _i4.Future<void>);
@override
- _i5.Future<void> setBackgroundColor(_i6.Color? color) =>
+ _i4.Future<void> setBackgroundColor(_i7.Color? color) =>
(super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]),
returnValue: Future<void>.value(),
- returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+ returnValueForMissingStub: Future<void>.value()) as _i4.Future<void>);
@override
- _i5.Future<void> release() =>
+ _i4.Future<void> release() =>
(super.noSuchMethod(Invocation.method(#release, []),
returnValue: Future<void>.value(),
- returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+ returnValueForMissingStub: Future<void>.value()) as _i4.Future<void>);
@override
String toString() => super.toString();
}
diff --git a/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.dart b/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.dart
new file mode 100644
index 0000000..4f274ff
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.dart
@@ -0,0 +1,53 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+import 'package:webview_flutter_android/src/android_webview.dart'
+ as android_webview;
+import 'package:webview_flutter_android/webview_android_cookie_manager.dart';
+import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
+
+import 'webview_android_cookie_manager_test.mocks.dart';
+
+@GenerateMocks(<Type>[android_webview.CookieManager])
+void main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+
+ setUp(() {
+ android_webview.CookieManager.instance = MockCookieManager();
+ });
+
+ test('clearCookies should call android_webview.clearCookies', () {
+ when(android_webview.CookieManager.instance.clearCookies())
+ .thenAnswer((_) => Future<bool>.value(true));
+ WebViewAndroidCookieManager().clearCookies();
+ verify(android_webview.CookieManager.instance.clearCookies());
+ });
+
+ test('setCookie should throw ArgumentError for cookie with invalid path', () {
+ expect(
+ () => WebViewAndroidCookieManager().setCookie(const WebViewCookie(
+ name: 'foo',
+ value: 'bar',
+ domain: 'flutter.dev',
+ path: 'invalid;path',
+ )),
+ throwsA(const TypeMatcher<ArgumentError>()),
+ );
+ });
+
+ test(
+ 'setCookie should call android_webview.csetCookie with properly formatted cookie value',
+ () {
+ WebViewAndroidCookieManager().setCookie(const WebViewCookie(
+ name: 'foo&',
+ value: 'bar@',
+ domain: 'flutter.dev',
+ ));
+ verify(android_webview.CookieManager.instance
+ .setCookie('flutter.dev', 'foo%26=bar%40; path=/'));
+ });
+}
diff --git a/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.mocks.dart
new file mode 100644
index 0000000..131977b
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.mocks.dart
@@ -0,0 +1,38 @@
+// Mocks generated by Mockito 5.0.16 from annotations
+// in webview_flutter_android/test/webview_android_cookie_manager_test.dart.
+// Do not manually edit this file.
+
+import 'dart:async' as _i3;
+
+import 'package:mockito/mockito.dart' as _i1;
+import 'package:webview_flutter_android/src/android_webview.dart' as _i2;
+
+// ignore_for_file: avoid_redundant_argument_values
+// ignore_for_file: avoid_setters_without_getters
+// ignore_for_file: comment_references
+// ignore_for_file: implementation_imports
+// ignore_for_file: invalid_use_of_visible_for_testing_member
+// ignore_for_file: prefer_const_constructors
+// ignore_for_file: unnecessary_parenthesis
+// ignore_for_file: camel_case_types
+
+/// A class which mocks [CookieManager].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockCookieManager extends _i1.Mock implements _i2.CookieManager {
+ MockCookieManager() {
+ _i1.throwOnMissingStub(this);
+ }
+
+ @override
+ _i3.Future<void> setCookie(String? url, String? value) =>
+ (super.noSuchMethod(Invocation.method(#setCookie, [url, value]),
+ returnValue: Future<void>.value(),
+ returnValueForMissingStub: Future<void>.value()) as _i3.Future<void>);
+ @override
+ _i3.Future<bool> clearCookies() =>
+ (super.noSuchMethod(Invocation.method(#clearCookies, []),
+ returnValue: Future<bool>.value(false)) as _i3.Future<bool>);
+ @override
+ String toString() => super.toString();
+}