diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index b241be63248c..41c40188a2e3 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.3.0 + +* Support new `onUrlChanged` event in platform interface. + ## 2.2.1 * Fix `NullPointerException` from a race condition when changing focus. This only affects `WebView` diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java index a86d3e8a4b63..9fda7d2e1261 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java @@ -151,6 +151,12 @@ void onLoadingProgress(int progress) { } } + private void onUrlChanged(WebView view, String url) { + Map args = new HashMap<>(); + args.put("url", url); + methodChannel.invokeMethod("onUrlChanged", args); + } + private void onWebResourceError( final int errorCode, final String description, final String failingUrl) { final Map args = new HashMap<>(); @@ -205,6 +211,11 @@ public void onPageFinished(WebView view, String url) { FlutterWebViewClient.this.onPageFinished(view, url); } + @Override + public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) { + FlutterWebViewClient.this.onUrlChanged(view, url); + } + @TargetApi(Build.VERSION_CODES.M) @Override public void onReceivedError( @@ -252,6 +263,11 @@ public void onPageFinished(WebView view, String url) { FlutterWebViewClient.this.onPageFinished(view, url); } + @Override + public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) { + FlutterWebViewClient.this.onUrlChanged(view, url); + } + // This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is // enabled. The deprecated method is called when a device doesn't support this. @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 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 a34274893117..89a2d41e83f2 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 @@ -1,8 +1,4 @@ -// 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. - -// Autogenerated from Pigeon (v1.0.9), do not edit directly. +// Autogenerated from Pigeon (v1.0.10), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.webviewflutter; @@ -1546,6 +1542,20 @@ public void onPageFinished( }); } + public void onUrlChanged( + Long instanceIdArg, Long webViewInstanceIdArg, String urlArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.WebViewClientFlutterApi.onUrlChanged", + getCodec()); + channel.send( + new ArrayList(Arrays.asList(instanceIdArg, webViewInstanceIdArg, urlArg)), + channelReply -> { + callback.reply(null); + }); + } + public void onReceivedRequestError( Long instanceIdArg, Long webViewInstanceIdArg, diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientFlutterApiImpl.java index 9e462faa58a7..27f45568c926 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientFlutterApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientFlutterApiImpl.java @@ -94,6 +94,16 @@ public void onPageFinished( callback); } + /** Passes arguments from {@link WebViewClient#onUrlChanged} to Dart. */ + public void onUrlChanged( + WebViewClient webViewClient, WebView webView, String urlArg, Reply callback) { + onUrlChanged( + instanceManager.getInstanceId(webViewClient), + instanceManager.getInstanceId(webView), + urlArg, + callback); + } + /** * Passes arguments from {@link WebViewClient#onReceivedError(WebView, WebResourceRequest, * WebResourceError)} to Dart. diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java index 6b659fae9c0f..8fdde72c1a41 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java @@ -66,6 +66,13 @@ public void onPageFinished(WebView view, String url) { } } + @Override + public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) { + if (flutterApi != null) { + flutterApi.onUrlChanged(this, view, url, reply -> {}); + } + } + @Override public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { if (flutterApi != null) { @@ -142,6 +149,13 @@ public void onPageFinished(WebView view, String url) { } } + @Override + public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) { + if (flutterApi != null) { + flutterApi.onUrlChanged(this, view, url, reply -> {}); + } + } + // This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is // enabled. The deprecated method is called when a device doesn't support this. @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java index 86346ac08f16..56ada60efbdf 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java @@ -11,9 +11,15 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; +import android.os.Build; import android.webkit.WebView; +import android.webkit.WebViewClient; +import androidx.webkit.WebViewClientCompat; import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.webviewflutter.utils.TestUtils; import java.util.HashMap; +import java.util.Map; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -30,7 +36,7 @@ public void before() { } @Test - public void notify_download_should_notifyOnNavigationRequest_when_navigationDelegate_is_set() { + public void notifyDownload_shouldNotifyOnNavigationRequestWhenNavigationDelegateIsSet() { final String url = "testurl.com"; FlutterWebViewClient client = new FlutterWebViewClient(mockMethodChannel); @@ -47,8 +53,7 @@ public void notify_download_should_notifyOnNavigationRequest_when_navigationDele } @Test - public void - notify_download_should_not_notifyOnNavigationRequest_when_navigationDelegate_is_not_set() { + public void notifyDownload_shouldNotNotifyOnNavigationRequestWhenNavigationDelegateIsNotSet() { final String url = "testurl.com"; FlutterWebViewClient client = new FlutterWebViewClient(mockMethodChannel); @@ -57,4 +62,44 @@ public void notify_download_should_notifyOnNavigationRequest_when_navigationDele client.notifyDownload(mockWebView, url); verifyNoInteractions(mockMethodChannel); } + + @Test + public void WebViewClient_doUpdateVisitedHistory_shouldCallOnUrlChangedEvent() { + // Setup + FlutterWebViewClient fltClient = new FlutterWebViewClient(mockMethodChannel); + WebViewClient client = + fltClient.createWebViewClient( + false // Force creation of internal WebViewClient. + ); + WebView mockView = mock(WebView.class); + Map methodChannelData = new HashMap<>(); + methodChannelData.put("url", "https://flutter.dev/"); + + //Run + client.doUpdateVisitedHistory(mockView, "https://flutter.dev/", false); + + // Verify + Assert.assertFalse(client instanceof WebViewClientCompat); + verify(mockMethodChannel).invokeMethod(eq("onUrlChanged"), eq(methodChannelData)); + } + + @Test + public void WebViewClientCompat_doUpdateVisitedHistory_shouldCallOnUrlChangedEvent() { + // Setup + FlutterWebViewClient fltClient = new FlutterWebViewClient(mockMethodChannel); + // Force creation of internal WebViewClientCompat (< Android N). + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.M); + WebViewClient client = fltClient.createWebViewClient(true); + + WebView mockView = mock(WebView.class); + Map methodChannelData = new HashMap<>(); + methodChannelData.put("url", "https://flutter.dev/"); + + //Run + client.doUpdateVisitedHistory(mockView, "https://flutter.dev/", false); + + // Verify + Assert.assertTrue(client instanceof WebViewClientCompat); + verify(mockMethodChannel).invokeMethod(eq("onUrlChanged"), eq(methodChannelData)); + } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImplTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImplTest.java new file mode 100644 index 000000000000..f2d34e21052d --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImplTest.java @@ -0,0 +1,39 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import org.junit.Test; + +public class WebViewClientHostApiImplTest { + + @Test + public void WebViewClientImpl_doUpdateVisitedHistory_shouldCallOnUrlChangedEvent() { + WebViewClientFlutterApiImpl mockFlutterApi = mock(WebViewClientFlutterApiImpl.class); + WebViewClientHostApiImpl.WebViewClientImpl webViewClient = + new WebViewClientHostApiImpl.WebViewClientImpl(mockFlutterApi, false); + + webViewClient.doUpdateVisitedHistory(null, "https://flutter.dev/", false); + + verify(mockFlutterApi) + .onUrlChanged(eq(webViewClient), any(), eq("https://flutter.dev/"), any()); + } + + @Test + public void WebViewClientCompatImpl_doUpdateVisitedHistory_shouldCallOnUrlChangedEvent() { + WebViewClientFlutterApiImpl mockFlutterApi = mock(WebViewClientFlutterApiImpl.class); + WebViewClientHostApiImpl.WebViewClientCompatImpl webViewClient = + new WebViewClientHostApiImpl.WebViewClientCompatImpl(mockFlutterApi, false); + + webViewClient.doUpdateVisitedHistory(null, "https://flutter.dev/", false); + + verify(mockFlutterApi) + .onUrlChanged(eq(webViewClient), any(), eq("https://flutter.dev/"), any()); + } +} 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 000000000000..31e7d58ee13f --- /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 void setFinalStatic(Class 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 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 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/android/gradle/wrapper/gradle-wrapper.properties b/packages/webview_flutter/webview_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties index 2819f022f1fd..772dacfed959 100644 --- a/packages/webview_flutter/webview_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/webview_flutter/webview_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -#Fri Jun 23 08:50:38 CEST 2017 +#Mon Nov 15 16:20:35 CET 2021 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME 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 b43507447462..d79a2d99f6bd 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 @@ -35,6 +35,9 @@ typedef void PageFinishedCallback(String url); /// Signature for when a [WebView] is loading a page. typedef void PageLoadingCallback(int progress); +/// Signature for when a [WebView] changed its current URL. +typedef void UrlChangedCallback(String url); + /// Signature for when a [WebView] has failed to load a resource. typedef void WebResourceErrorCallback(WebResourceError error); @@ -68,6 +71,7 @@ class WebView extends StatefulWidget { this.onPageStarted, this.onPageFinished, this.onProgress, + this.onUrlChanged, this.onWebResourceError, this.debuggingEnabled = false, this.gestureNavigationEnabled = false, @@ -197,6 +201,9 @@ class WebView extends StatefulWidget { /// Invoked when a page is loading. final PageLoadingCallback? onProgress; + /// Invoked when a webview's URL has changed. + final UrlChangedCallback? onUrlChanged; + /// Invoked when a web resource has failed to load. /// /// This callback is only called for the main page. @@ -345,6 +352,13 @@ class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { } } + @override + void onUrlChanged(String url) { + if (_webView.onUrlChanged != null) { + _webView.onUrlChanged!(url); + } + } + void onWebResourceError(WebResourceError error) { if (_webView.onWebResourceError != null) { _webView.onWebResourceError!(error); diff --git a/packages/webview_flutter/webview_flutter_android/generateMocks.sh b/packages/webview_flutter/webview_flutter_android/generateMocks.sh new file mode 100755 index 000000000000..7b8937e421ce --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/generateMocks.sh @@ -0,0 +1,5 @@ +# 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. + +flutter pub run build_runner build 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 f6faa8e74331..c8b653bd4eb8 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 @@ -537,6 +537,13 @@ abstract class WebViewClient { /// reflect the state of the DOM at this point. void onPageFinished(WebView webView, String url) {} + /// Notify the host application that a webview's URL has changed. + /// + /// Unlike [onPageStarted], [onProgress], and [onPageFinished], + /// [onUrlChanged] also fires when navigating without a full page load + /// e.g. when using a single page application. + void onUrlChanged(WebView webView, String url) {} + /// Report web resource loading error to the host application. /// /// These errors usually indicate inability to connect to the server. Note 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 7fb95f1dd942..01cc66a09fd4 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 @@ -1,8 +1,4 @@ -// 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. - -// Autogenerated from Pigeon (v1.0.9), do not edit directly. +// Autogenerated from Pigeon (v1.0.10), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name // @dart = 2.12 @@ -1176,6 +1172,7 @@ abstract class WebViewClientFlutterApi { void dispose(int instanceId); void onPageStarted(int instanceId, int webViewInstanceId, String url); void onPageFinished(int instanceId, int webViewInstanceId, String url); + void onUrlChanged(int instanceId, int webViewInstanceId, String url); void onReceivedRequestError(int instanceId, int webViewInstanceId, WebResourceRequestData request, WebResourceErrorData error); void onReceivedError(int instanceId, int webViewInstanceId, int errorCode, @@ -1254,6 +1251,31 @@ abstract class WebViewClientFlutterApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewClientFlutterApi.onUrlChanged', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onUrlChanged was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onUrlChanged was null, expected non-null int.'); + final int? arg_webViewInstanceId = (args[1] as int?); + assert(arg_webViewInstanceId != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onUrlChanged was null, expected non-null int.'); + final String? arg_url = (args[2] as String?); + assert(arg_url != null, + 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.onUrlChanged was null, expected non-null String.'); + api.onUrlChanged(arg_instanceId!, arg_webViewInstanceId!, arg_url!); + return; + }); + } + } { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewClientFlutterApi.onReceivedRequestError', 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 fba0dea0a421..ee1402a4aca4 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 @@ -491,6 +491,16 @@ class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { ); } + @override + void onUrlChanged(int instanceId, int webViewInstanceId, String url) { + final WebViewClient instance = + instanceManager.getInstance(instanceId) as WebViewClient; + instance.onUrlChanged( + instanceManager.getInstance(webViewInstanceId) as WebView, + url, + ); + } + @override void onReceivedError( int instanceId, 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 47bc42058ba9..e56fefaaa939 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -127,6 +127,8 @@ abstract class WebViewClientFlutterApi { void onPageFinished(int instanceId, int webViewInstanceId, String url); + void onUrlChanged(int instanceId, int webViewInstanceId, String url); + void onReceivedRequestError( int instanceId, int webViewInstanceId, diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 2fe03c888d6b..3562ab3a96f2 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_android description: A Flutter plugin that provides a WebView widget on Android. repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.2.1 +version: 2.3.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -19,15 +19,17 @@ flutter: dependencies: flutter: sdk: flutter - webview_flutter_platform_interface: ^1.2.0 + #TODO (BeMacized): CHANGE TO VERSION DEPENDENCY ONCE UPDATED PLATFORM INTERFACE HAS BEEN PUBLISHED. + webview_flutter_platform_interface: + path: ../webview_flutter_platform_interface dev_dependencies: - build_runner: ^2.1.4 flutter_driver: sdk: flutter flutter_test: sdk: flutter - mockito: ^5.0.16 - pigeon: 1.0.9 + pigeon: 1.0.10 pedantic: ^1.10.0 + mockito: ^5.0.16 + build_runner: ^2.1.5 diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart index 4173663b6248..87a15e92f212 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart @@ -1,10 +1,6 @@ -// 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. - -// Autogenerated from Pigeon (v1.0.9), do not edit directly. +// Autogenerated from Pigeon (v1.0.10), do not edit directly. // See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, avoid_relative_lib_imports +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import // @dart = 2.12 import 'dart:async'; import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_api_impls_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_api_impls_test.dart new file mode 100644 index 000000000000..7bc3d7f936d7 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_api_impls_test.dart @@ -0,0 +1,34 @@ +// 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'; +import 'package:webview_flutter_android/src/android_webview_api_impls.dart'; +import 'package:webview_flutter_android/src/instance_manager.dart'; +import 'android_webview_api_impls_test.mocks.dart'; + +@GenerateMocks([InstanceManager, WebViewClient, WebView]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('WebViewClientFlutterApiImpl', () { + test('onUrlChanged should call WebViewClient#onUrlChanged', () { + // Setup + var mockInstanceManager = MockInstanceManager(); + var mockWebViewClient = MockWebViewClient(); + var mockWebView = MockWebView(); + when(mockInstanceManager.getInstance(1)).thenReturn(mockWebViewClient); + when(mockInstanceManager.getInstance(2)).thenReturn(mockWebView); + var impl = + WebViewClientFlutterApiImpl(instanceManager: mockInstanceManager); + // Run + impl.onUrlChanged(1, 2, 'https://flutter.dev/'); + // Verify + verify( + mockWebViewClient.onUrlChanged(mockWebView, 'https://flutter.dev/')); + }); + }); +} diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_api_impls_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_api_impls_test.mocks.dart new file mode 100644 index 000000000000..6f5b75afd3fc --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_api_impls_test.mocks.dart @@ -0,0 +1,213 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Mocks generated by Mockito 5.0.16 from annotations +// in webview_flutter_android/test/android_webview_api_impls_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i4; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_android/src/android_webview.dart' as _i2; +import 'package:webview_flutter_android/src/instance_manager.dart' as _i3; + +// 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 + +class _FakeWebSettings_0 extends _i1.Fake implements _i2.WebSettings {} + +/// A class which mocks [InstanceManager]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockInstanceManager extends _i1.Mock implements _i3.InstanceManager { + MockInstanceManager() { + _i1.throwOnMissingStub(this); + } + + @override + int? tryAddInstance(Object? instance) => + (super.noSuchMethod(Invocation.method(#tryAddInstance, [instance])) + as int?); + @override + int? removeInstance(Object? instance) => + (super.noSuchMethod(Invocation.method(#removeInstance, [instance])) + as int?); + @override + Object? getInstance(int? instanceId) => + (super.noSuchMethod(Invocation.method(#getInstance, [instanceId])) + as Object?); + @override + int? getInstanceId(Object? instance) => + (super.noSuchMethod(Invocation.method(#getInstanceId, [instance])) + as int?); + @override + String toString() => super.toString(); +} + +/// A class which mocks [WebViewClient]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewClient extends _i1.Mock implements _i2.WebViewClient { + MockWebViewClient() { + _i1.throwOnMissingStub(this); + } + + @override + bool get shouldOverrideUrlLoading => + (super.noSuchMethod(Invocation.getter(#shouldOverrideUrlLoading), + returnValue: false) as bool); + @override + void onPageStarted(_i2.WebView? webView, String? url) => + super.noSuchMethod(Invocation.method(#onPageStarted, [webView, url]), + returnValueForMissingStub: null); + @override + void onPageFinished(_i2.WebView? webView, String? url) => + super.noSuchMethod(Invocation.method(#onPageFinished, [webView, url]), + returnValueForMissingStub: null); + @override + void onUrlChanged(_i2.WebView? webView, String? url) => + super.noSuchMethod(Invocation.method(#onUrlChanged, [webView, url]), + returnValueForMissingStub: null); + @override + void onReceivedRequestError(_i2.WebView? webView, + _i2.WebResourceRequest? request, _i2.WebResourceError? error) => + super.noSuchMethod( + Invocation.method(#onReceivedRequestError, [webView, request, error]), + returnValueForMissingStub: null); + @override + void onReceivedError(_i2.WebView? webView, int? errorCode, + String? description, String? failingUrl) => + super.noSuchMethod( + Invocation.method( + #onReceivedError, [webView, errorCode, description, failingUrl]), + returnValueForMissingStub: null); + @override + void requestLoading(_i2.WebView? webView, _i2.WebResourceRequest? request) => + super.noSuchMethod(Invocation.method(#requestLoading, [webView, request]), + returnValueForMissingStub: null); + @override + void urlLoading(_i2.WebView? webView, String? url) => + super.noSuchMethod(Invocation.method(#urlLoading, [webView, url]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// A class which mocks [WebView]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebView extends _i1.Mock implements _i2.WebView { + MockWebView() { + _i1.throwOnMissingStub(this); + } + + @override + bool get useHybridComposition => + (super.noSuchMethod(Invocation.getter(#useHybridComposition), + returnValue: false) as bool); + @override + _i2.WebSettings get settings => + (super.noSuchMethod(Invocation.getter(#settings), + returnValue: _FakeWebSettings_0()) as _i2.WebSettings); + @override + _i4.Future loadUrl(String? url, Map? headers) => + (super.noSuchMethod(Invocation.method(#loadUrl, [url, headers]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future getUrl() => + (super.noSuchMethod(Invocation.method(#getUrl, []), + returnValue: Future.value()) as _i4.Future); + @override + _i4.Future canGoBack() => + (super.noSuchMethod(Invocation.method(#canGoBack, []), + returnValue: Future.value(false)) as _i4.Future); + @override + _i4.Future canGoForward() => + (super.noSuchMethod(Invocation.method(#canGoForward, []), + returnValue: Future.value(false)) as _i4.Future); + @override + _i4.Future goBack() => + (super.noSuchMethod(Invocation.method(#goBack, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future goForward() => + (super.noSuchMethod(Invocation.method(#goForward, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future reload() => + (super.noSuchMethod(Invocation.method(#reload, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future clearCache(bool? includeDiskFiles) => + (super.noSuchMethod(Invocation.method(#clearCache, [includeDiskFiles]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future evaluateJavascript(String? javascriptString) => (super + .noSuchMethod(Invocation.method(#evaluateJavascript, [javascriptString]), + returnValue: Future.value()) as _i4.Future); + @override + _i4.Future getTitle() => + (super.noSuchMethod(Invocation.method(#getTitle, []), + returnValue: Future.value()) as _i4.Future); + @override + _i4.Future scrollTo(int? x, int? y) => + (super.noSuchMethod(Invocation.method(#scrollTo, [x, y]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future scrollBy(int? x, int? y) => + (super.noSuchMethod(Invocation.method(#scrollBy, [x, y]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future getScrollX() => + (super.noSuchMethod(Invocation.method(#getScrollX, []), + returnValue: Future.value(0)) as _i4.Future); + @override + _i4.Future getScrollY() => + (super.noSuchMethod(Invocation.method(#getScrollY, []), + returnValue: Future.value(0)) as _i4.Future); + @override + _i4.Future setWebViewClient(_i2.WebViewClient? webViewClient) => + (super.noSuchMethod(Invocation.method(#setWebViewClient, [webViewClient]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future addJavaScriptChannel( + _i2.JavaScriptChannel? javaScriptChannel) => + (super.noSuchMethod( + Invocation.method(#addJavaScriptChannel, [javaScriptChannel]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future removeJavaScriptChannel( + _i2.JavaScriptChannel? javaScriptChannel) => + (super.noSuchMethod( + Invocation.method(#removeJavaScriptChannel, [javaScriptChannel]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future setDownloadListener(_i2.DownloadListener? listener) => + (super.noSuchMethod(Invocation.method(#setDownloadListener, [listener]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + _i4.Future setWebChromeClient(_i2.WebChromeClient? client) => + (super.noSuchMethod(Invocation.method(#setWebChromeClient, [client]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + String toString() => super.toString(); +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md index 4c7434a86b41..212f1c4c4784 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.5.0 + +* Added `onUrlChanged` callback to platform callback handler. + ## 1.4.0 * Added `loadFile` and `loadHtml` interface methods. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart index 8df9f4c62b33..3a6047d9d5d0 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart @@ -53,6 +53,9 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { case 'onPageStarted': _platformCallbacksHandler.onPageStarted(call.arguments['url']!); return null; + case 'onUrlChanged': + _platformCallbacksHandler.onUrlChanged(call.arguments['url']!); + return null; case 'onWebResourceError': _platformCallbacksHandler.onWebResourceError( WebResourceError( diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_callbacks_handler.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_callbacks_handler.dart index 44dae2ece434..fd2df5fdde64 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_callbacks_handler.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_callbacks_handler.dart @@ -24,9 +24,16 @@ abstract class WebViewPlatformCallbacksHandler { void onPageFinished(String url); /// Invoked by [WebViewPlatformController] when a page is loading. - /// /// Only works when [WebSettings.hasProgressTracking] is set to `true`. + /// Only works when [WebSettings.hasProgressTracking] is set to `true`. void onProgress(int progress); + /// Invoked by [WebViewPlatformController] when the webview's URL has changed. + /// + /// Unlike [onPageStarted], [onProgress], and [onPageFinished], + /// [onUrlChanged] also fires when navigating without a full page load + /// e.g. when using a single page application. + void onUrlChanged(String url); + /// Report web resource loading error to the host application. void onWebResourceError(WebResourceError error); } diff --git a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml index 4a4746d8ab68..57ae9a06f056 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/webview_flut issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview_flutter%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 1.4.0 +version: 1.5.0 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart index 396013535aa9..f827d8fcf6b0 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart @@ -13,7 +13,9 @@ import 'package:webview_flutter_platform_interface/webview_flutter_platform_inte void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('Tests on `plugin.flutter.io/webview_` channel', () { + group( + 'Tests on `plugin.flutter.io/webview_` channel dart->native', + () { const int channelId = 1; const MethodChannel channel = MethodChannel('plugins.flutter.io/webview_$channelId'); @@ -554,6 +556,40 @@ void main() { }); }); + group( + 'Tests on `plugin.flutter.io/webview_` channel native->dart', + () { + const int channelId = 1; + final WebViewPlatformCallbacksHandler callbacksHandler = + MockWebViewPlatformCallbacksHandler(); + final JavascriptChannelRegistry javascriptChannelRegistry = + MockJavascriptChannelRegistry(); + + MethodChannelWebViewPlatform( + channelId, + callbacksHandler, + javascriptChannelRegistry, + ); + + tearDown(() { + reset(callbacksHandler); + }); + + test('onUrlChanged', () async { + // Run + await ServicesBinding.instance!.defaultBinaryMessenger + .handlePlatformMessage( + 'plugins.flutter.io/webview_$channelId', + StandardMethodCodec() + .encodeMethodCall(MethodCall('onUrlChanged', {'url': 'Test Url'})), + null, + ); + + // Verify + verify(callbacksHandler.onUrlChanged('Test Url')); + }); + }); + group('Tests on `plugins.flutter.io/cookie_manager` channel', () { const MethodChannel cookieChannel = MethodChannel('plugins.flutter.io/cookie_manager');