From 505f1a25649736f9e8b18ae57711dde009c438e9 Mon Sep 17 00:00:00 2001 From: Ben Bieker Date: Mon, 9 Dec 2019 11:15:25 +0100 Subject: [PATCH] [webview_flutter] Send history events Send URL change events when the user navigates in the Browser. This also works for JavaScript Navigation that does not actually make any request. --- packages/webview_flutter/CHANGELOG.md | 4 ++ .../webviewflutter/FlutterWebViewClient.java | 18 +++++++ .../webview_flutter_test.dart | 54 +++++++++++++++++++ .../webview_flutter/example/lib/main.dart | 3 ++ .../ios/Classes/FLTWKHistoryDelegate.h | 16 ++++++ .../ios/Classes/FLTWKHistoryDelegate.m | 41 ++++++++++++++ .../ios/Classes/FlutterWebView.m | 7 +++ .../lib/platform_interface.dart | 3 ++ .../lib/src/webview_method_channel.dart | 3 ++ .../webview_flutter/lib/webview_flutter.dart | 17 ++++++ packages/webview_flutter/pubspec.yaml | 2 +- .../test/webview_flutter_test.dart | 34 ++++++++++++ 12 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 packages/webview_flutter/ios/Classes/FLTWKHistoryDelegate.h create mode 100644 packages/webview_flutter/ios/Classes/FLTWKHistoryDelegate.m diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index 4270e11ba090..f9469e2283c2 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.3 + +* Add support for Navigation History observation. + ## 1.0.2 * Android Code Inspection and Clean up. diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java index 24926bfc4117..f0b1a4b8036e 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java @@ -148,6 +148,12 @@ private void notifyOnNavigationRequest( } } + private void onUpdateVisitedHistory(String url) { + HashMap args = new HashMap<>(); + args.put("url", url); + methodChannel.invokeMethod("onUpdateVisitedHistory", args); + } + // This method attempts to avoid using WebViewClientCompat due to bug // https://bugs.chromium.org/p/chromium/issues/detail?id=925887. Also, see // https://github.com/flutter/flutter/issues/29446. @@ -163,6 +169,12 @@ WebViewClient createWebViewClient(boolean hasNavigationDelegate) { private WebViewClient internalCreateWebViewClient() { return new WebViewClient() { + @Override + public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) { + FlutterWebViewClient.this.onUpdateVisitedHistory(url); + super.doUpdateVisitedHistory(view, url, isReload); + } + @TargetApi(Build.VERSION_CODES.N) @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { @@ -204,6 +216,12 @@ public void onUnhandledKeyEvent(WebView view, KeyEvent event) { private WebViewClientCompat internalCreateWebViewClientCompat() { return new WebViewClientCompat() { + @Override + public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) { + FlutterWebViewClient.this.onUpdateVisitedHistory(url); + super.doUpdateVisitedHistory(view, url, isReload); + } + @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request); diff --git a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart index 2a17c53a4c3f..76dc871e36a9 100644 --- a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart @@ -1008,6 +1008,60 @@ void main() { }, skip: !Platform.isAndroid, ); + + testWidgets('onUpdateVisitedHistory triggers when the URL changes', + (WidgetTester tester) async { + final String onUpdateVisitedHistoryTest = ''' + + onUpdateVisitedHistory test + + + + + + '''; + final String onUpdateVisitedHistoryTestBase64 = + base64Encode(const Utf8Encoder().convert(onUpdateVisitedHistoryTest)); + final Completer onUpdateVisitedHistoryCompleter = + Completer(); + final GlobalKey key = GlobalKey(); + + final String initialUrl = + 'data:text/html;charset=utf-8;base64,$onUpdateVisitedHistoryTestBase64'; + + final WebView webView = WebView( + key: key, + initialUrl: initialUrl, + javascriptMode: JavascriptMode.unrestricted, + onUpdateVisitedHistory: (String newUrl) { + if (newUrl == initialUrl) { + return; + } + onUpdateVisitedHistoryCompleter.complete(newUrl); + }); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: 200, + height: 200, + child: webView, + ), + ], + ), + ), + ); + + final String nextUrl = await onUpdateVisitedHistoryCompleter.future; + expect(nextUrl, 'https://www.google.com/'); + }); } // JavaScript booleans evaluate to different string values on Android and iOS. diff --git a/packages/webview_flutter/example/lib/main.dart b/packages/webview_flutter/example/lib/main.dart index ff25e8a16c9b..9789ff3e31d9 100644 --- a/packages/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/example/lib/main.dart @@ -82,6 +82,9 @@ class _WebViewExampleState extends State { print('Page finished loading: $url'); }, gestureNavigationEnabled: true, + onUpdateVisitedHistory: (String url) { + print('Update visited history: $url'); + }, ); }), floatingActionButton: favoriteButton(), diff --git a/packages/webview_flutter/ios/Classes/FLTWKHistoryDelegate.h b/packages/webview_flutter/ios/Classes/FLTWKHistoryDelegate.h new file mode 100644 index 000000000000..92d2117f2a57 --- /dev/null +++ b/packages/webview_flutter/ios/Classes/FLTWKHistoryDelegate.h @@ -0,0 +1,16 @@ +// Copyright 2019 The Chromium 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 +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTWKHistoryDelegate : NSObject +- (instancetype)initWithWebView:(WKWebView *)webView channel:(FlutterMethodChannel *)channel; +- (void)stopObserving:(WKWebView *)webView; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/ios/Classes/FLTWKHistoryDelegate.m b/packages/webview_flutter/ios/Classes/FLTWKHistoryDelegate.m new file mode 100644 index 000000000000..29a60f98f22e --- /dev/null +++ b/packages/webview_flutter/ios/Classes/FLTWKHistoryDelegate.m @@ -0,0 +1,41 @@ +// Copyright 2019 The Chromium 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 "FLTWKHistoryDelegate.h" + +NSString *const keyPath = @"URL"; + +@implementation FLTWKHistoryDelegate { + FlutterMethodChannel *_methodChannel; +} + +- (instancetype)initWithWebView:(id)webView channel:(FlutterMethodChannel *)channel { + self = [super init]; + if (self) { + _methodChannel = channel; + [webView addObserver:self + forKeyPath:keyPath + options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld + context:nil]; + } + + return self; +} + +- (void)stopObserving:(WKWebView *)webView { + [webView removeObserver:self forKeyPath:keyPath]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if ([change[NSKeyValueChangeNewKey] isKindOfClass:[NSURL class]]) { + NSURL *newUrl = change[NSKeyValueChangeNewKey]; + [_methodChannel invokeMethod:@"onUpdateVisitedHistory" + arguments:@{@"url" : [newUrl absoluteString]}]; + } +} + +@end diff --git a/packages/webview_flutter/ios/Classes/FlutterWebView.m b/packages/webview_flutter/ios/Classes/FlutterWebView.m index 969e010913f3..e494e0c46f68 100644 --- a/packages/webview_flutter/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/ios/Classes/FlutterWebView.m @@ -3,6 +3,7 @@ // found in the LICENSE file. #import "FlutterWebView.h" +#import "FLTWKHistoryDelegate.h" #import "FLTWKNavigationDelegate.h" #import "JavaScriptChannelHandler.h" @@ -64,6 +65,7 @@ @implementation FLTWebViewController { // The set of registered JavaScript channel names. NSMutableSet* _javaScriptChannelNames; FLTWKNavigationDelegate* _navigationDelegate; + FLTWKHistoryDelegate* _historyDelegate; } - (instancetype)initWithFrame:(CGRect)frame @@ -95,6 +97,7 @@ - (instancetype)initWithFrame:(CGRect)frame _navigationDelegate = [[FLTWKNavigationDelegate alloc] initWithChannel:_channel]; _webView.UIDelegate = self; _webView.navigationDelegate = _navigationDelegate; + _historyDelegate = [[FLTWKHistoryDelegate alloc] initWithWebView:_webView channel:_channel]; __weak __typeof__(self) weakSelf = self; [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { [weakSelf onMethodCall:call result:result]; @@ -119,6 +122,10 @@ - (instancetype)initWithFrame:(CGRect)frame return self; } +- (void)dealloc { + [_historyDelegate stopObserving:_webView]; +} + - (UIView*)view { return _webView; } diff --git a/packages/webview_flutter/lib/platform_interface.dart b/packages/webview_flutter/lib/platform_interface.dart index 6c991b14a76e..0197c8eb4571 100644 --- a/packages/webview_flutter/lib/platform_interface.dart +++ b/packages/webview_flutter/lib/platform_interface.dart @@ -31,6 +31,9 @@ abstract class WebViewPlatformCallbacksHandler { /// Report web resource loading error to the host application. void onWebResourceError(WebResourceError error); + + /// Invoked by [WebViewPlatformController] when the URL has changed. + void onUpdateVisitedHistory(String url); } /// Possible error type categorizations used by [WebResourceError]. diff --git a/packages/webview_flutter/lib/src/webview_method_channel.dart b/packages/webview_flutter/lib/src/webview_method_channel.dart index 348b225bb257..f93065d31e5d 100644 --- a/packages/webview_flutter/lib/src/webview_method_channel.dart +++ b/packages/webview_flutter/lib/src/webview_method_channel.dart @@ -61,6 +61,9 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { ), ); return null; + case 'onUpdateVisitedHistory': + _platformCallbacksHandler.onUpdateVisitedHistory(call.arguments['url']); + return null; } throw MissingPluginException( diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index 5e2bffd6539d..282158b9ca77 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -145,6 +145,9 @@ typedef void PageFinishedCallback(String url); /// Signature for when a [WebView] has failed to load a resource. typedef void WebResourceErrorCallback(WebResourceError error); +/// Signature for when a [WebView] had a history update. +typedef void PageUpdateVisitedHistoryCallback(String url); + /// Specifies possible restrictions on automatic media playback. /// /// This is typically used in [WebView.initialMediaPlaybackPolicy]. @@ -214,6 +217,7 @@ class WebView extends StatefulWidget { this.onPageStarted, this.onPageFinished, this.onWebResourceError, + this.onUpdateVisitedHistory, this.debuggingEnabled = false, this.gestureNavigationEnabled = false, this.userAgent, @@ -350,6 +354,12 @@ class WebView extends StatefulWidget { /// the main page. final WebResourceErrorCallback onWebResourceError; + /// Invoked when the URL in the browser changed. + /// + /// This is always triggered when the URL changes. Also when this is done via + /// JavaScript and no [onPageStarted] or [onPageFinished] events would be fired. + final PageUpdateVisitedHistoryCallback onUpdateVisitedHistory; + /// Controls whether WebView debugging is enabled. /// /// Setting this to true enables [WebView debugging on Android](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/). @@ -560,6 +570,13 @@ class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { } } + @override + void onUpdateVisitedHistory(String url) { + if (_widget.onUpdateVisitedHistory != null) { + _widget.onUpdateVisitedHistory(url); + } + } + void _updateJavascriptChannelsFromSet(Set channels) { _javascriptChannels.clear(); if (channels == null) { diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index ad1c356a67df..14b11120329b 100644 --- a/packages/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. -version: 1.0.2 +version: 1.0.3 homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter environment: diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart index c7cf46a080d7..a5dd3b6560a6 100644 --- a/packages/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/test/webview_flutter_test.dart @@ -889,6 +889,22 @@ void main() { expect(platformWebView.userAgent, 'UA'); }); + testWidgets('onUpdateVisitedHistory', (WidgetTester tester) async { + String visitedUrl; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onUpdateVisitedHistory: (String url) { + visitedUrl = url; + })); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + platformWebView.fakeOnUpdateVisitedHistoryCallback('https://google.com'); + + expect(visitedUrl, 'https://google.com'); + }); } class FakePlatformWebView { @@ -1052,6 +1068,24 @@ class FakePlatformWebView { ); } + void fakeOnUpdateVisitedHistoryCallback(String nextUrl) { + final StandardMethodCodec codec = const StandardMethodCodec(); + + final ByteData data = codec.encodeMethodCall(MethodCall( + 'onUpdateVisitedHistory', + {'url': nextUrl}, + )); + + // TODO(hterkelsen): Remove this when defaultBinaryMessages is in stable. + // https://github.com/flutter/flutter/issues/33446 + // ignore: deprecated_member_use + BinaryMessages.handlePlatformMessage( + channel.name, + data, + (ByteData data) {}, + ); + } + void _loadUrl(String url) { history = history.sublist(0, currentPosition + 1); history.add(url);