[webview_flutter]Allow specifying a navigation delegate(Android and Dart). (#1236)

This allows the app to prevent specific navigations(e.g prevent
navigating to specific URLs).

flutter/flutter#25329

iOS implementation in #1323
diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md
index 889a6a1..7a3256a 100644
--- a/packages/webview_flutter/CHANGELOG.md
+++ b/packages/webview_flutter/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.3.4
+
+* Support specifying navigation delegates that can prevent navigations from being executed.
+
 ## 0.3.3+2
 
 * Exclude LongPress handler from semantics tree since it does nothing.
diff --git a/packages/webview_flutter/android/build.gradle b/packages/webview_flutter/android/build.gradle
index 098c2f4..45ab74d 100644
--- a/packages/webview_flutter/android/build.gradle
+++ b/packages/webview_flutter/android/build.gradle
@@ -44,4 +44,8 @@
     lintOptions {
         disable 'InvalidPackage'
     }
+
+    dependencies {
+        implementation 'androidx.webkit:webkit:1.0.0'
+    }
 }
diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java
index 44fe31f..070ba74 100644
--- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java
+++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java
@@ -4,7 +4,9 @@
 
 package io.flutter.plugins.webviewflutter;
 
+import android.annotation.TargetApi;
 import android.content.Context;
+import android.os.Build;
 import android.view.View;
 import android.webkit.WebStorage;
 import android.webkit.WebView;
@@ -21,6 +23,7 @@
   private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames";
   private final WebView webView;
   private final MethodChannel methodChannel;
+  private final FlutterWebViewClient flutterWebViewClient;
 
   @SuppressWarnings("unchecked")
   FlutterWebView(Context context, BinaryMessenger messenger, int id, Map<String, Object> params) {
@@ -31,12 +34,15 @@
     methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id);
     methodChannel.setMethodCallHandler(this);
 
+    flutterWebViewClient = new FlutterWebViewClient(methodChannel);
     applySettings((Map<String, Object>) params.get("settings"));
 
     if (params.containsKey(JS_CHANNEL_NAMES_FIELD)) {
       registerJavaScriptChannelNames((List<String>) params.get(JS_CHANNEL_NAMES_FIELD));
     }
 
+    webView.setWebViewClient(flutterWebViewClient);
+
     if (params.containsKey("initialUrl")) {
       String url = (String) params.get("initialUrl");
       webView.loadUrl(url);
@@ -135,6 +141,7 @@
     result.success(null);
   }
 
+  @TargetApi(Build.VERSION_CODES.KITKAT)
   private void evaluateJavaScript(MethodCall methodCall, final Result result) {
     String jsString = (String) methodCall.arguments;
     if (jsString == null) {
@@ -178,6 +185,9 @@
         case "jsMode":
           updateJsMode((Integer) settings.get(key));
           break;
+        case "hasNavigationDelegate":
+          flutterWebViewClient.setHasNavigationDelegate((boolean) settings.get(key));
+          break;
         default:
           throw new IllegalArgumentException("Unknown WebView setting: " + key);
       }
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
new file mode 100644
index 0000000..fe4482c
--- /dev/null
+++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java
@@ -0,0 +1,124 @@
+// 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.
+
+package io.flutter.plugins.webviewflutter;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.util.Log;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebView;
+import androidx.webkit.WebViewClientCompat;
+import io.flutter.plugin.common.MethodChannel;
+import java.util.HashMap;
+import java.util.Map;
+
+// We need to use WebViewClientCompat to get
+// shouldOverrideUrlLoading(WebView view, WebResourceRequest request)
+// invoked by the webview on older Android devices, without it pages that use iframes will
+// be broken when a navigationDelegate is set on Android version earlier than N.
+class FlutterWebViewClient extends WebViewClientCompat {
+  private static final String TAG = "FlutterWebViewClient";
+  private final MethodChannel methodChannel;
+  private boolean hasNavigationDelegate;
+
+  FlutterWebViewClient(MethodChannel methodChannel) {
+    this.methodChannel = methodChannel;
+  }
+
+  void setHasNavigationDelegate(boolean hasNavigationDelegate) {
+    this.hasNavigationDelegate = hasNavigationDelegate;
+  }
+
+  @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+  @Override
+  public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
+    if (!hasNavigationDelegate) {
+      return false;
+    }
+    notifyOnNavigationRequest(
+        request.getUrl().toString(), request.getRequestHeaders(), view, request.isForMainFrame());
+    // We must make a synchronous decision here whether to allow the navigation or not,
+    // if the Dart code has set a navigation delegate we want that delegate to decide whether
+    // to navigate or not, and as we cannot get a response from the Dart delegate synchronously we
+    // return true here to block the navigation, if the Dart delegate decides to allow the
+    // navigation the plugin will later make an addition loadUrl call for this url.
+    //
+    // Since we cannot call loadUrl for a subframe, we currently only allow the delegate to stop
+    // navigations that target the main frame, if the request is not for the main frame
+    // we just return false to allow the navigation.
+    //
+    // For more details see: https://github.com/flutter/flutter/issues/25329#issuecomment-464863209
+    return request.isForMainFrame();
+  }
+
+  @Override
+  public boolean shouldOverrideUrlLoading(WebView view, String url) {
+    if (!hasNavigationDelegate) {
+      return false;
+    }
+    // This version of shouldOverrideUrlLoading is only invoked by the webview on devices with
+    // webview versions  earlier than 67(it is also invoked when hasNavigationDelegate is false).
+    // On these devices we cannot tell whether the navigation is targeted to the main frame or not.
+    // We proceed assuming that the navigation is targeted to the main frame. If the page had any
+    // frames they will be loaded in the main frame instead.
+    Log.w(
+        TAG,
+        "Using a navigationDelegate with an old webview implementation, pages with frames or iframes will not work");
+    notifyOnNavigationRequest(url, null, view, true);
+    return true;
+  }
+
+  private void notifyOnNavigationRequest(
+      String url, Map<String, String> headers, WebView webview, boolean isMainFrame) {
+    HashMap<String, Object> args = new HashMap<>();
+    args.put("url", url);
+    args.put("isForMainFrame", isMainFrame);
+    if (isMainFrame) {
+      methodChannel.invokeMethod(
+          "navigationRequest", args, new OnNavigationRequestResult(url, headers, webview));
+    } else {
+      methodChannel.invokeMethod("navigationRequest", args);
+    }
+  }
+
+  private static class OnNavigationRequestResult implements MethodChannel.Result {
+    private final String url;
+    private final Map<String, String> headers;
+    private final WebView webView;
+
+    private OnNavigationRequestResult(String url, Map<String, String> headers, WebView webView) {
+      this.url = url;
+      this.headers = headers;
+      this.webView = webView;
+    }
+
+    @Override
+    public void success(Object shouldLoad) {
+      Boolean typedShouldLoad = (Boolean) shouldLoad;
+      if (typedShouldLoad) {
+        loadUrl();
+      }
+    }
+
+    @Override
+    public void error(String errorCode, String s1, Object o) {
+      throw new IllegalStateException("navigationRequest calls must succeed");
+    }
+
+    @Override
+    public void notImplemented() {
+      throw new IllegalStateException(
+          "navigationRequest must be implemented by the webview method channel");
+    }
+
+    private void loadUrl() {
+      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+        webView.loadUrl(url, headers);
+      } else {
+        webView.loadUrl(url);
+      }
+    }
+  }
+}
diff --git a/packages/webview_flutter/example/android/gradle.properties b/packages/webview_flutter/example/android/gradle.properties
index 8bd86f6..ad8917e 100644
--- a/packages/webview_flutter/example/android/gradle.properties
+++ b/packages/webview_flutter/example/android/gradle.properties
@@ -1 +1,2 @@
 org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
\ No newline at end of file
diff --git a/packages/webview_flutter/example/lib/main.dart b/packages/webview_flutter/example/lib/main.dart
index f688b60..7d6ce10 100644
--- a/packages/webview_flutter/example/lib/main.dart
+++ b/packages/webview_flutter/example/lib/main.dart
@@ -3,11 +3,27 @@
 // found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:convert';
 import 'package:flutter/material.dart';
 import 'package:webview_flutter/webview_flutter.dart';
 
 void main() => runApp(MaterialApp(home: WebViewExample()));
 
+const String kNavigationExamplePage = '''
+<!DOCTYPE html><html>
+<head><title>Navigation Delegate Example</title></head>
+<body>
+<p>
+The navigation delegate is set to block navigation to the youtube website.
+</p>
+<ul>
+<ul><a href="https://www.youtube.com/">https://www.youtube.com/</a></ul>
+<ul><a href="https://www.google.com/">https://www.google.com/</a></ul>
+</ul>
+</body>
+</html>
+''';
+
 class WebViewExample extends StatelessWidget {
   final Completer<WebViewController> _controller =
       Completer<WebViewController>();
@@ -37,6 +53,14 @@
           javascriptChannels: <JavascriptChannel>[
             _toasterJavascriptChannel(context),
           ].toSet(),
+          navigationDelegate: (NavigationRequest request) {
+            if (request.url.startsWith('https://www.youtube.com/')) {
+              print('blocking navigation to $request}');
+              return NavigationDecision.prevent;
+            }
+            print('allowing navigation to $request');
+            return NavigationDecision.navigate;
+          },
         );
       }),
       floatingActionButton: favoriteButton(),
@@ -76,12 +100,12 @@
 
 enum MenuOptions {
   showUserAgent,
-  toast,
   listCookies,
   clearCookies,
   addToCache,
   listCache,
   clearCache,
+  navigationDelegate,
 }
 
 class SampleMenu extends StatelessWidget {
@@ -102,13 +126,6 @@
               case MenuOptions.showUserAgent:
                 _onShowUserAgent(controller.data, context);
                 break;
-              case MenuOptions.toast:
-                Scaffold.of(context).showSnackBar(
-                  SnackBar(
-                    content: Text('You selected: $value'),
-                  ),
-                );
-                break;
               case MenuOptions.listCookies:
                 _onListCookies(controller.data, context);
                 break;
@@ -124,6 +141,9 @@
               case MenuOptions.clearCache:
                 _onClearCache(controller.data, context);
                 break;
+              case MenuOptions.navigationDelegate:
+                _onNavigationDelegateExample(controller.data, context);
+                break;
             }
           },
           itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
@@ -133,10 +153,6 @@
                   enabled: controller.hasData,
                 ),
                 const PopupMenuItem<MenuOptions>(
-                  value: MenuOptions.toast,
-                  child: Text('Make a toast'),
-                ),
-                const PopupMenuItem<MenuOptions>(
                   value: MenuOptions.listCookies,
                   child: Text('List cookies'),
                 ),
@@ -156,6 +172,10 @@
                   value: MenuOptions.clearCache,
                   child: Text('Clear cache'),
                 ),
+                const PopupMenuItem<MenuOptions>(
+                  value: MenuOptions.navigationDelegate,
+                  child: Text('Navigation Delegate example'),
+                ),
               ],
         );
       },
@@ -218,6 +238,13 @@
     ));
   }
 
+  void _onNavigationDelegateExample(
+      WebViewController controller, BuildContext context) async {
+    final String contentBase64 =
+        base64Encode(const Utf8Encoder().convert(kNavigationExamplePage));
+    controller.loadUrl('data:text/html;base64,$contentBase64');
+  }
+
   Widget _getCookieList(String cookies) {
     if (cookies == null || cookies == '""') {
       return Container();
diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart
index 4f02852..8833cb4 100644
--- a/packages/webview_flutter/lib/webview_flutter.dart
+++ b/packages/webview_flutter/lib/webview_flutter.dart
@@ -33,6 +33,39 @@
 /// Callback type for handling messages sent from Javascript running in a web view.
 typedef void JavascriptMessageHandler(JavascriptMessage message);
 
+/// Information about a navigation action that is about to be executed.
+class NavigationRequest {
+  NavigationRequest._({this.url, this.isForMainFrame});
+
+  /// The URL that will be loaded if the navigation is executed.
+  final String url;
+
+  /// Whether the navigation request is to be loaded as the main frame.
+  final bool isForMainFrame;
+
+  @override
+  String toString() {
+    return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)';
+  }
+}
+
+/// A decision on how to handle a navigation request.
+enum NavigationDecision {
+  /// Prevent the navigation from taking place.
+  prevent,
+
+  /// Allow the navigation to take place.
+  navigate,
+}
+
+/// Decides how to handle a specific navigation request.
+///
+/// The returned [NavigationDecision] determines how the navigation described by
+/// `navigation` should be handled.
+///
+/// See also: [WebView.navigationDelegate].
+typedef NavigationDecision NavigationDelegate(NavigationRequest navigation);
+
 final RegExp _validChannelNames = RegExp('^[a-zA-Z_][a-zA-Z0-9]*\$');
 
 /// A named channel for receiving messaged from JavaScript code running inside a web view.
@@ -78,6 +111,7 @@
     this.initialUrl,
     this.javascriptMode = JavascriptMode.disabled,
     this.javascriptChannels,
+    this.navigationDelegate,
     this.gestureRecognizers,
   })  : assert(javascriptMode != null),
         super(key: key);
@@ -131,6 +165,30 @@
   /// A null value is equivalent to an empty set.
   final Set<JavascriptChannel> javascriptChannels;
 
+  /// A delegate function that decides how to handle navigation actions.
+  ///
+  /// When a navigation is initiated by the WebView (e.g when a user clicks a link)
+  /// this delegate is called and has to decide how to proceed with the navigation.
+  ///
+  /// See [NavigationDecision] for possible decisions the delegate can take.
+  ///
+  /// When null all navigation actions are allowed.
+  ///
+  /// Caveats on Android:
+  ///
+  ///   * Navigation actions targeted to the main frame can be intercepted,
+  ///     navigation actions targeted to subframes are allowed regardless of the value
+  ///     returned by this delegate.
+  ///   * Setting a navigationDelegate makes the WebView treat all navigations as if they were
+  ///     triggered by a user gesture, this disables some of Chromium's security mechanisms.
+  ///     A navigationDelegate should only be set when loading trusted content.
+  ///   * On Android WebView versions earlier than 67(most devices running at least Android L+ should have
+  ///     a later version):
+  ///     * When a navigationDelegate is set pages with frames are not properly handled by the
+  ///       webview, and frames will be opened in the main frame.
+  ///     * When a navigationDelegate is set HTTP requests do not include the HTTP referer header.
+  final NavigationDelegate navigationDelegate;
+
   @override
   State<StatefulWidget> createState() => _WebViewState();
 }
@@ -197,6 +255,7 @@
     final WebViewController controller = await _controller.future;
     controller._updateSettings(settings);
     controller._updateJavascriptChannels(widget.javascriptChannels);
+    controller._navigationDelegate = widget.navigationDelegate;
   }
 
   void _onPlatformViewCreated(int id) {
@@ -204,6 +263,7 @@
       id,
       _WebSettings.fromWidget(widget),
       widget.javascriptChannels,
+      widget.navigationDelegate,
     );
     _controller.complete(controller);
     if (widget.onWebViewCreated != null) {
@@ -261,27 +321,35 @@
 class _WebSettings {
   _WebSettings({
     this.javascriptMode,
+    this.hasNavigationDelegate,
   });
 
   static _WebSettings fromWidget(WebView widget) {
-    return _WebSettings(javascriptMode: widget.javascriptMode);
+    return _WebSettings(
+      javascriptMode: widget.javascriptMode,
+      hasNavigationDelegate: widget.navigationDelegate != null,
+    );
   }
 
   final JavascriptMode javascriptMode;
+  final bool hasNavigationDelegate;
 
   Map<String, dynamic> toMap() {
     return <String, dynamic>{
       'jsMode': javascriptMode.index,
+      'hasNavigationDelegate': hasNavigationDelegate,
     };
   }
 
   Map<String, dynamic> updatesMap(_WebSettings newSettings) {
-    if (javascriptMode == newSettings.javascriptMode) {
-      return null;
+    final Map<String, dynamic> updates = <String, dynamic>{};
+    if (javascriptMode != newSettings.javascriptMode) {
+      updates['jsMode'] = newSettings.javascriptMode.index;
     }
-    return <String, dynamic>{
-      'jsMode': newSettings.javascriptMode.index,
-    };
+    if (hasNavigationDelegate != newSettings.hasNavigationDelegate) {
+      updates['hasNavigationDelegate'] = newSettings.hasNavigationDelegate;
+    }
+    return updates;
   }
 }
 
@@ -291,29 +359,48 @@
 /// callback for a [WebView] widget.
 class WebViewController {
   WebViewController._(
-      int id, this._settings, Set<JavascriptChannel> javascriptChannels)
-      : _channel = MethodChannel('plugins.flutter.io/webview_$id') {
+    int id,
+    this._settings,
+    Set<JavascriptChannel> javascriptChannels,
+    this._navigationDelegate,
+  ) : _channel = MethodChannel('plugins.flutter.io/webview_$id') {
     _updateJavascriptChannelsFromSet(javascriptChannels);
     _channel.setMethodCallHandler(_onMethodCall);
   }
 
   final MethodChannel _channel;
 
+  NavigationDelegate _navigationDelegate;
+
   _WebSettings _settings;
 
   // Maps a channel name to a channel.
   Map<String, JavascriptChannel> _javascriptChannels =
       <String, JavascriptChannel>{};
 
-  Future<void> _onMethodCall(MethodCall call) async {
+  Future<bool> _onMethodCall(MethodCall call) async {
     switch (call.method) {
       case 'javascriptChannelMessage':
         final String channel = call.arguments['channel'];
         final String message = call.arguments['message'];
         _javascriptChannels[channel]
             .onMessageReceived(JavascriptMessage(message));
-        break;
+        return true;
+      case 'navigationRequest':
+        final NavigationRequest request = NavigationRequest._(
+          url: call.arguments['url'],
+          isForMainFrame: call.arguments['isForMainFrame'],
+        );
+
+        // _navigationDelegate can be null if the widget was rebuilt with no
+        // navigation delegate after a navigation happened and just before we
+        // got the navigationRequest message.
+        final bool allowNavigation = _navigationDelegate == null ||
+            _navigationDelegate(request) == NavigationDecision.navigate;
+        return allowNavigation;
     }
+    throw MissingPluginException(
+        '${call.method} was invoked but has no handler');
   }
 
   /// Loads the specified URL.
@@ -417,7 +504,7 @@
 
   Future<void> _updateSettings(_WebSettings setting) async {
     final Map<String, dynamic> updateMap = _settings.updatesMap(setting);
-    if (updateMap == null) {
+    if (updateMap == null || updateMap.isEmpty) {
       return null;
     }
     _settings = setting;
diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml
index 8307e1f..84484a2 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: 0.3.3+2
+version: 0.3.4
 author: Flutter Team <flutter-dev@googlegroups.com>
 homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter
 
diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart
index 4fcdc7f..8635875 100644
--- a/packages/webview_flutter/test/webview_flutter_test.dart
+++ b/packages/webview_flutter/test/webview_flutter_test.dart
@@ -575,6 +575,57 @@
 
     expect(ttsMessagesReceived, <String>['Hello', 'World']);
   });
+
+  group('navigationDelegate', () {
+    testWidgets('hasNavigationDelegate', (WidgetTester tester) async {
+      await tester.pumpWidget(const WebView(
+        initialUrl: 'https://youtube.com',
+      ));
+
+      final FakePlatformWebView platformWebView =
+          fakePlatformViewsController.lastCreatedView;
+
+      expect(platformWebView.hasNavigationDelegate, false);
+
+      await tester.pumpWidget(WebView(
+        initialUrl: 'https://youtube.com',
+        navigationDelegate: (NavigationRequest r) => null,
+      ));
+
+      expect(platformWebView.hasNavigationDelegate, true);
+    });
+
+    testWidgets('Block navigation', (WidgetTester tester) async {
+      final List<NavigationRequest> navigationRequests = <NavigationRequest>[];
+
+      await tester.pumpWidget(WebView(
+          initialUrl: 'https://youtube.com',
+          navigationDelegate: (NavigationRequest request) {
+            navigationRequests.add(request);
+            // Only allow navigating to https://flutter.dev
+            return request.url == 'https://flutter.dev'
+                ? NavigationDecision.navigate
+                : NavigationDecision.prevent;
+          }));
+
+      final FakePlatformWebView platformWebView =
+          fakePlatformViewsController.lastCreatedView;
+
+      expect(platformWebView.hasNavigationDelegate, true);
+
+      platformWebView.fakeNavigate('https://www.google.com');
+      // The navigation delegate only allows navigation to https://flutter.dev
+      // so we should still be in https://youtube.com.
+      expect(platformWebView.currentUrl, 'https://youtube.com');
+      expect(navigationRequests.length, 1);
+      expect(navigationRequests[0].url, 'https://www.google.com');
+      expect(navigationRequests[0].isForMainFrame, true);
+
+      platformWebView.fakeNavigate('https://flutter.dev');
+      await tester.pump();
+      expect(platformWebView.currentUrl, 'https://flutter.dev');
+    });
+  });
 }
 
 class FakePlatformWebView {
@@ -585,12 +636,14 @@
         history.add(initialUrl);
         currentPosition++;
       }
-      javascriptMode = JavascriptMode.values[params['settings']['jsMode']];
     }
     if (params.containsKey('javascriptChannelNames')) {
       javascriptChannelNames =
           List<String>.from(params['javascriptChannelNames']);
     }
+    javascriptMode = JavascriptMode.values[params['settings']['jsMode']];
+    hasNavigationDelegate =
+        params['settings']['hasNavigationDelegate'] ?? false;
     channel = MethodChannel(
         'plugins.flutter.io/webview_$id', const StandardMethodCodec());
     channel.setMockMethodCallHandler(onMethodCall);
@@ -607,20 +660,21 @@
   JavascriptMode javascriptMode;
   List<String> javascriptChannelNames;
 
+  bool hasNavigationDelegate;
+
   Future<dynamic> onMethodCall(MethodCall call) {
     switch (call.method) {
       case 'loadUrl':
         final String url = call.arguments;
-        history = history.sublist(0, currentPosition + 1);
-        history.add(url);
-        currentPosition++;
-        amountOfReloadsOnCurrentUrl = 0;
+        _loadUrl(url);
         return Future<void>.sync(() {});
       case 'updateSettings':
-        if (call.arguments['jsMode'] == null) {
-          break;
+        if (call.arguments['jsMode'] != null) {
+          javascriptMode = JavascriptMode.values[call.arguments['jsMode']];
         }
-        javascriptMode = JavascriptMode.values[call.arguments['jsMode']];
+        if (call.arguments['hasNavigationDelegate'] != null) {
+          hasNavigationDelegate = call.arguments['hasNavigationDelegate'];
+        }
         break;
       case 'canGoBack':
         return Future<bool>.sync(() => currentPosition > 0);
@@ -672,6 +726,36 @@
     BinaryMessages.handlePlatformMessage(
         channel.name, data, (ByteData data) {});
   }
+
+  // Fakes a main frame navigation that was initiated by the webview, e.g when
+  // the user clicks a link in the currently loaded page.
+  void fakeNavigate(String url) {
+    if (!hasNavigationDelegate) {
+      print('no navigation delegate');
+      _loadUrl(url);
+      return;
+    }
+    final StandardMethodCodec codec = const StandardMethodCodec();
+    final Map<String, dynamic> arguments = <String, dynamic>{
+      'url': url,
+      'isForMainFrame': true
+    };
+    final ByteData data =
+        codec.encodeMethodCall(MethodCall('navigationRequest', arguments));
+    BinaryMessages.handlePlatformMessage(channel.name, data, (ByteData data) {
+      final bool allow = codec.decodeEnvelope(data);
+      if (allow) {
+        _loadUrl(url);
+      }
+    });
+  }
+
+  void _loadUrl(String url) {
+    history = history.sublist(0, currentPosition + 1);
+    history.add(url);
+    currentPosition++;
+    amountOfReloadsOnCurrentUrl = 0;
+  }
 }
 
 class _FakePlatformViewsController {