[webview_flutter] Extract Android implementation into a separate package (#4343)

* Setup webview_flutter_android package.

Creates a new `webview_flutter_android` directory and adds
the following meta-data files:
- `AUTHORS`: copied from the `webview_flutter` package and added my name;
- `CHANGELOG.md`: new file adding description for release 0.0.1;
- `LICENSE`: copied from the `webview_flutter` package;
- `README.md`: new file adding the standard platform implementation
  description;
- `pubspec.yaml`: new file adding package meta-data for the
  `webview_flutter_android` package.

* Direct copy of "android" folder.

A one to one copy of the `webview_flutter/android` folder to
`webview_flutter_android/` using the following command:
```
cp -R ./webview_flutter/android ./webview_flutter_android/
```

* Direct copy of Android specific .dart files.

Copied the Android specific .dart files over from the
`./webview_flutter` package. Note that the `SurfaceAndroidWebView` class
in the `./webview_flutter_android/lib/webview_surface_android.dart` file
is copied directly (without modifactions) from the
`./webview_flutter/lib/webview_flutter.dart` file.

* Modify .dart code to work with platform_interface.

Make sure the `AndroidWebView` and `SurfaceAndroidWebView` widgets
extend the `WebViewPlatform` class from the
`webview_flutter_platform_interface` package correctly by accepting an
instance of the `JavascriptChannelRegistry` class.

* Direct copy of the `webview_flutter/example` app.

This commit makes a direct copy of the `webview_flutter/example` app to
the  `webview_flutter_android` package. After the copy the `example/ios`
folder is removed as it doesn't serve a purpose in the Android specific
package. Commands run where:
```
cp -R ./webview_flutter/example ./webview_flutter_android/
rm -rf ./webview_flutter_android/example/ios
```

* Update example to Android specific implementation.

This commit updates the example App so it directly implements an Android
specific implementation of the webview_flutter_platform_interface.

* Update integration tests.

Updated the existing integration tests (copied from webview_flutter
package) so they work correctly with the implementation of the
webview_flutter_android package.

* Update webview_flutter_platform_interface dependency

Updated the pubspec.yaml to depend on version 1.0.0 of the
webview_flutter_platform_interface package instead of using a path
reference (which is now possible since the platform interface package
has now been published).

Co-authored-by: BeMacized <bodhimulders@bemacized.net>

* Use different bundle ID for Android example app.

Make sure the `webview_flutter` and `webview_flutter_android` example
apps use different application identifiers so that the CI doesn't run
into problems.

* Skip flaky integration_tests (issue 86757).

* Exlude platform implementations from build all step.

Make sure the webview_flutter_android and webview_flutter_wkwebview
packages are excluded from the Build All plugins step as they will cause
conflicts with the current implementation which is still part of the
webview_flutter package.

* Split helper classes from main example widget.

Move the `WebView` and related `WebViewController` classes from the
main.dart into a separate web_view.dart file.

Co-authored-by: BeMacized <bodhimulders@bemacized.net>
diff --git a/packages/webview_flutter/webview_flutter_android/AUTHORS b/packages/webview_flutter/webview_flutter_android/AUTHORS
new file mode 100644
index 0000000..4461b60
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/AUTHORS
@@ -0,0 +1,68 @@
+# Below is a list of people and organizations that have contributed
+# to the Flutter project. Names should be added to the list like so:
+#
+#   Name/Organization <email address>
+
+Google Inc.
+The Chromium Authors
+German Saprykin <saprykin.h@gmail.com>
+Benjamin Sauer <sauer.benjamin@gmail.com>
+larsenthomasj@gmail.com
+Ali Bitek <alibitek@protonmail.ch>
+Pol Batlló <pol.batllo@gmail.com>
+Anatoly Pulyaevskiy
+Hayden Flinner <haydenflinner@gmail.com>
+Stefano Rodriguez <hlsroddy@gmail.com>
+Salvatore Giordano <salvatoregiordanoo@gmail.com>
+Brian Armstrong <brian@flutter.institute>
+Paul DeMarco <paulmdemarco@gmail.com>
+Fabricio Nogueira <feufeu@gmail.com>
+Simon Lightfoot <simon@devangels.london>
+Ashton Thomas <ashton@acrinta.com>
+Thomas Danner <thmsdnnr@gmail.com>
+Diego Velásquez <diego.velasquez.lopez@gmail.com>
+Hajime Nakamura <nkmrhj@gmail.com>
+Tuyển Vũ Xuân <netsoft1985@gmail.com>
+Miguel Ruivo <miguel@miguelruivo.com>
+Sarthak Verma <sarthak@artiosys.com>
+Mike Diarmid <mike@invertase.io>
+Invertase <oss@invertase.io>
+Elliot Hesp <elliot@invertase.io>
+Vince Varga <vince.varga@smaho.com>
+Aawaz Gyawali <awazgyawali@gmail.com>
+EUI Limited <ian.evans3@admiralgroup.co.uk>
+Katarina Sheremet <katarina@sheremet.ch>
+Thomas Stockx <thomas@stockxit.com>
+Sarbagya Dhaubanjar <sarbagyastha@gmail.com>
+Ozkan Eksi <ozeksi@gmail.com>
+Rishab Nayak <rishab@bu.edu>
+ko2ic <ko2ic.dev@gmail.com>
+Jonathan Younger <jonathan@daikini.com>
+Jose Sanchez <josesm82@gmail.com>
+Debkanchan Samadder <debu.samadder@gmail.com>
+Audrius Karosevicius <audrius.karosevicius@gmail.com>
+Lukasz Piliszczuk <lukasz@intheloup.io>
+SoundReply Solutions GmbH <ch@soundreply.com>
+Rafal Wachol <rwachol@gmail.com>
+Pau Picas <pau.picas@gmail.com>
+Christian Weder <chrstian.weder@yapeal.ch>
+Alexandru Tuca <salexandru.tuca@outlook.com>
+Christian Weder <chrstian.weder@yapeal.ch>
+Rhodes Davis Jr. <rody.davis.jr@gmail.com>
+Luigi Agosti <luigi@tengio.com>
+Quentin Le Guennec <quentin@tengio.com>
+Koushik Ravikumar <koushik@tengio.com>
+Nissim Dsilva <nissim@tengio.com>
+Giancarlo Rocha <giancarloiff@gmail.com>
+Ryo Miyake <ryo@miyake.id>
+Théo Champion <contact.theochampion@gmail.com>
+Kazuki Yamaguchi <y.kazuki0614n@gmail.com>
+Eitan Schwartz <eshvartz@gmail.com>
+Chris Rutkowski <chrisrutkowski89@gmail.com>
+Juan Alvarez <juan.alvarez@resideo.com>
+Aleksandr Yurkovskiy <sanekyy@gmail.com>
+Anton Borries <mail@antonborri.es>
+Alex Li <google@alexv525.com>
+Rahul Raj <64.rahulraj@gmail.com>
+Maurits van Beusekom <maurits@baseflow.com>
+
diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md
new file mode 100644
index 0000000..d6a10e9
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md
@@ -0,0 +1,4 @@
+## 2.0.13
+
+* Extract Android implementation from `webview_flutter`.
+
diff --git a/packages/webview_flutter/webview_flutter_android/LICENSE b/packages/webview_flutter/webview_flutter_android/LICENSE
new file mode 100644
index 0000000..7713090
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/LICENSE
@@ -0,0 +1,26 @@
+Copyright 2013 The Flutter Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/packages/webview_flutter/webview_flutter_android/README.md b/packages/webview_flutter/webview_flutter_android/README.md
new file mode 100644
index 0000000..3883856
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/README.md
@@ -0,0 +1,12 @@
+# webview\_flutter\_android
+
+The Android implementation of [`webview_flutter`][1].
+
+## Usage
+
+This package is [endorsed][2], which means you can simply use `webview_flutter`
+normally. This package will be automatically included in your app when you do.
+
+[1]: https://pub.dev/packages/webview_flutter
+[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin
+
diff --git a/packages/webview_flutter/webview_flutter_android/android/build.gradle b/packages/webview_flutter/webview_flutter_android/android/build.gradle
new file mode 100644
index 0000000..4a16431
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/build.gradle
@@ -0,0 +1,57 @@
+group 'io.flutter.plugins.webviewflutter'
+version '1.0-SNAPSHOT'
+
+buildscript {
+    repositories {
+        google()
+        mavenCentral()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.3.0'
+    }
+}
+
+rootProject.allprojects {
+    repositories {
+        google()
+        mavenCentral()
+    }
+}
+
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 29
+
+    defaultConfig {
+        minSdkVersion 19
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+
+    lintOptions {
+        disable 'InvalidPackage'
+        disable 'GradleDependency'
+    }
+
+    dependencies {
+        implementation 'androidx.annotation:annotation:1.0.0'
+        implementation 'androidx.webkit:webkit:1.0.0'
+        testImplementation 'junit:junit:4.12'
+        testImplementation 'org.mockito:mockito-inline:3.11.1'
+        testImplementation 'androidx.test:core:1.3.0'
+    }
+
+
+    testOptions {
+        unitTests.includeAndroidResources = true
+        unitTests.returnDefaultValues = true
+        unitTests.all {
+            testLogging {
+               events "passed", "skipped", "failed", "standardOut", "standardError"
+               outputs.upToDateWhen {false}
+               showStandardStreams = true
+            }
+        }
+    }
+}
diff --git a/packages/webview_flutter/webview_flutter_android/android/settings.gradle b/packages/webview_flutter/webview_flutter_android/android/settings.gradle
new file mode 100644
index 0000000..5be7a4b
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'webview_flutter'
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/AndroidManifest.xml b/packages/webview_flutter/webview_flutter_android/android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a087f2c
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+<manifest package="io.flutter.plugins.webviewflutter">
+</manifest>
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java
new file mode 100644
index 0000000..31e3fe0
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java
@@ -0,0 +1,147 @@
+// 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 android.hardware.display.DisplayManager.DisplayListener;
+
+import android.annotation.TargetApi;
+import android.hardware.display.DisplayManager;
+import android.os.Build;
+import android.util.Log;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+
+/**
+ * Works around an Android WebView bug by filtering some DisplayListener invocations.
+ *
+ * <p>Older Android WebView versions had assumed that when {@link DisplayListener#onDisplayChanged}
+ * is invoked, the display ID it is provided is of a valid display. However it turns out that when a
+ * display is removed Android may call onDisplayChanged with the ID of the removed display, in this
+ * case the Android WebView code tries to fetch and use the display with this ID and crashes with an
+ * NPE.
+ *
+ * <p>This issue was fixed in the Android WebView code in
+ * https://chromium-review.googlesource.com/517913 which is available starting WebView version
+ * 58.0.3029.125 however older webviews in the wild still have this issue.
+ *
+ * <p>Since Flutter removes virtual displays whenever a platform view is resized the webview crash
+ * is more likely to happen than other apps. And users were reporting this issue see:
+ * https://github.com/flutter/flutter/issues/30420
+ *
+ * <p>This class works around the webview bug by unregistering the WebView's DisplayListener, and
+ * instead registering its own DisplayListener which delegates the callbacks to the WebView's
+ * listener unless it's a onDisplayChanged for an invalid display.
+ *
+ * <p>I did not find a clean way to get a handle of the WebView's DisplayListener so I'm using
+ * reflection to fetch all registered listeners before and after initializing a webview. In the
+ * first initialization of a webview within the process the difference between the lists is the
+ * webview's display listener.
+ */
+@TargetApi(Build.VERSION_CODES.KITKAT)
+class DisplayListenerProxy {
+  private static final String TAG = "DisplayListenerProxy";
+
+  private ArrayList<DisplayListener> listenersBeforeWebView;
+
+  /** Should be called prior to the webview's initialization. */
+  void onPreWebViewInitialization(DisplayManager displayManager) {
+    listenersBeforeWebView = yoinkDisplayListeners(displayManager);
+  }
+
+  /** Should be called after the webview's initialization. */
+  void onPostWebViewInitialization(final DisplayManager displayManager) {
+    final ArrayList<DisplayListener> webViewListeners = yoinkDisplayListeners(displayManager);
+    // We recorded the list of listeners prior to initializing webview, any new listeners we see
+    // after initializing the webview are listeners added by the webview.
+    webViewListeners.removeAll(listenersBeforeWebView);
+
+    if (webViewListeners.isEmpty()) {
+      // The Android WebView registers a single display listener per process (even if there
+      // are multiple WebView instances) so this list is expected to be non-empty only the
+      // first time a webview is initialized.
+      // Note that in an add2app scenario if the application had instantiated a non Flutter
+      // WebView prior to instantiating the Flutter WebView we are not able to get a reference
+      // to the WebView's display listener and can't work around the bug.
+      //
+      // This means that webview resizes in add2app Flutter apps with a non Flutter WebView
+      // running on a system with a webview prior to 58.0.3029.125 may crash (the Android's
+      // behavior seems to be racy so it doesn't always happen).
+      return;
+    }
+
+    for (DisplayListener webViewListener : webViewListeners) {
+      // Note that while DisplayManager.unregisterDisplayListener throws when given an
+      // unregistered listener, this isn't an issue as the WebView code never calls
+      // unregisterDisplayListener.
+      displayManager.unregisterDisplayListener(webViewListener);
+
+      // We never explicitly unregister this listener as the webview's listener is never
+      // unregistered (it's released when the process is terminated).
+      displayManager.registerDisplayListener(
+          new DisplayListener() {
+            @Override
+            public void onDisplayAdded(int displayId) {
+              for (DisplayListener webViewListener : webViewListeners) {
+                webViewListener.onDisplayAdded(displayId);
+              }
+            }
+
+            @Override
+            public void onDisplayRemoved(int displayId) {
+              for (DisplayListener webViewListener : webViewListeners) {
+                webViewListener.onDisplayRemoved(displayId);
+              }
+            }
+
+            @Override
+            public void onDisplayChanged(int displayId) {
+              if (displayManager.getDisplay(displayId) == null) {
+                return;
+              }
+              for (DisplayListener webViewListener : webViewListeners) {
+                webViewListener.onDisplayChanged(displayId);
+              }
+            }
+          },
+          null);
+    }
+  }
+
+  @SuppressWarnings({"unchecked", "PrivateApi"})
+  private static ArrayList<DisplayListener> yoinkDisplayListeners(DisplayManager displayManager) {
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+      // We cannot use reflection on Android P, but it shouldn't matter as it shipped
+      // with WebView 66.0.3359.158 and the WebView version the bug this code is working around was
+      // fixed in 61.0.3116.0.
+      return new ArrayList<>();
+    }
+    try {
+      Field displayManagerGlobalField = DisplayManager.class.getDeclaredField("mGlobal");
+      displayManagerGlobalField.setAccessible(true);
+      Object displayManagerGlobal = displayManagerGlobalField.get(displayManager);
+      Field displayListenersField =
+          displayManagerGlobal.getClass().getDeclaredField("mDisplayListeners");
+      displayListenersField.setAccessible(true);
+      ArrayList<Object> delegates =
+          (ArrayList<Object>) displayListenersField.get(displayManagerGlobal);
+
+      Field listenerField = null;
+      ArrayList<DisplayManager.DisplayListener> listeners = new ArrayList<>();
+      for (Object delegate : delegates) {
+        if (listenerField == null) {
+          listenerField = delegate.getClass().getField("mListener");
+          listenerField.setAccessible(true);
+        }
+        DisplayManager.DisplayListener listener =
+            (DisplayManager.DisplayListener) listenerField.get(delegate);
+        listeners.add(listener);
+      }
+      return listeners;
+    } catch (NoSuchFieldException | IllegalAccessException e) {
+      Log.w(TAG, "Could not extract WebView's display listeners. " + e);
+      return new ArrayList<>();
+    }
+  }
+}
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
new file mode 100644
index 0000000..df3f21d
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java
@@ -0,0 +1,56 @@
+// 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/FlutterDownloadListener.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java
new file mode 100644
index 0000000..cfad4e3
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java
@@ -0,0 +1,33 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.webviewflutter;
+
+import android.webkit.DownloadListener;
+import android.webkit.WebView;
+
+/** DownloadListener to notify the {@link FlutterWebViewClient} of download starts */
+public class FlutterDownloadListener implements DownloadListener {
+  private final FlutterWebViewClient webViewClient;
+  private WebView webView;
+
+  public FlutterDownloadListener(FlutterWebViewClient webViewClient) {
+    this.webViewClient = webViewClient;
+  }
+
+  /** Sets the {@link WebView} that the result of the navigation delegate will be send to. */
+  public void setWebView(WebView webView) {
+    this.webView = webView;
+  }
+
+  @Override
+  public void onDownloadStart(
+      String url,
+      String userAgent,
+      String contentDisposition,
+      String mimetype,
+      long contentLength) {
+    webViewClient.notifyDownload(webView, url);
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java
new file mode 100644
index 0000000..4651a5f
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java
@@ -0,0 +1,498 @@
+// 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.annotation.TargetApi;
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Message;
+import android.view.View;
+import android.webkit.DownloadListener;
+import android.webkit.WebChromeClient;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebStorage;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+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;
+import io.flutter.plugin.platform.PlatformView;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class FlutterWebView implements PlatformView, MethodCallHandler {
+
+  private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames";
+  private final WebView webView;
+  private final MethodChannel methodChannel;
+  private final FlutterWebViewClient flutterWebViewClient;
+  private final Handler platformThreadHandler;
+
+  // Verifies that a url opened by `Window.open` has a secure url.
+  private class FlutterWebChromeClient extends WebChromeClient {
+
+    @Override
+    public boolean onCreateWindow(
+        final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
+      final WebViewClient webViewClient =
+          new WebViewClient() {
+            @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+            @Override
+            public boolean shouldOverrideUrlLoading(
+                @NonNull WebView view, @NonNull WebResourceRequest request) {
+              final String url = request.getUrl().toString();
+              if (!flutterWebViewClient.shouldOverrideUrlLoading(
+                  FlutterWebView.this.webView, request)) {
+                webView.loadUrl(url);
+              }
+              return true;
+            }
+
+            @Override
+            public boolean shouldOverrideUrlLoading(WebView view, String url) {
+              if (!flutterWebViewClient.shouldOverrideUrlLoading(
+                  FlutterWebView.this.webView, url)) {
+                webView.loadUrl(url);
+              }
+              return true;
+            }
+          };
+
+      final WebView newWebView = new WebView(view.getContext());
+      newWebView.setWebViewClient(webViewClient);
+
+      final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
+      transport.setWebView(newWebView);
+      resultMsg.sendToTarget();
+
+      return true;
+    }
+
+    @Override
+    public void onProgressChanged(WebView view, int progress) {
+      flutterWebViewClient.onLoadingProgress(progress);
+    }
+  }
+
+  @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+  @SuppressWarnings("unchecked")
+  FlutterWebView(
+      final Context context,
+      MethodChannel methodChannel,
+      Map<String, Object> params,
+      View containerView) {
+
+    DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy();
+    DisplayManager displayManager =
+        (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
+    displayListenerProxy.onPreWebViewInitialization(displayManager);
+
+    this.methodChannel = methodChannel;
+    this.methodChannel.setMethodCallHandler(this);
+
+    flutterWebViewClient = new FlutterWebViewClient(methodChannel);
+
+    FlutterDownloadListener flutterDownloadListener =
+        new FlutterDownloadListener(flutterWebViewClient);
+    webView =
+        createWebView(
+            new WebViewBuilder(context, containerView),
+            params,
+            new FlutterWebChromeClient(),
+            flutterDownloadListener);
+    flutterDownloadListener.setWebView(webView);
+
+    displayListenerProxy.onPostWebViewInitialization(displayManager);
+
+    platformThreadHandler = new Handler(context.getMainLooper());
+
+    Map<String, Object> settings = (Map<String, Object>) params.get("settings");
+    if (settings != null) {
+      applySettings(settings);
+    }
+
+    if (params.containsKey(JS_CHANNEL_NAMES_FIELD)) {
+      List<String> names = (List<String>) params.get(JS_CHANNEL_NAMES_FIELD);
+      if (names != null) {
+        registerJavaScriptChannelNames(names);
+      }
+    }
+
+    Integer autoMediaPlaybackPolicy = (Integer) params.get("autoMediaPlaybackPolicy");
+    if (autoMediaPlaybackPolicy != null) {
+      updateAutoMediaPlaybackPolicy(autoMediaPlaybackPolicy);
+    }
+    if (params.containsKey("userAgent")) {
+      String userAgent = (String) params.get("userAgent");
+      updateUserAgent(userAgent);
+    }
+    if (params.containsKey("initialUrl")) {
+      String url = (String) params.get("initialUrl");
+      webView.loadUrl(url);
+    }
+  }
+
+  /**
+   * Creates a {@link android.webkit.WebView} and configures it according to the supplied
+   * parameters.
+   *
+   * <p>The {@link WebView} is configured with the following predefined settings:
+   *
+   * <ul>
+   *   <li>always enable the DOM storage API;
+   *   <li>always allow JavaScript to automatically open windows;
+   *   <li>always allow support for multiple windows;
+   *   <li>always use the {@link FlutterWebChromeClient} as web Chrome client.
+   * </ul>
+   *
+   * <p><strong>Important:</strong> This method is visible for testing purposes only and should
+   * never be called from outside this class.
+   *
+   * @param webViewBuilder a {@link WebViewBuilder} which is responsible for building the {@link
+   *     WebView}.
+   * @param params creation parameters received over the method channel.
+   * @param webChromeClient an implementation of WebChromeClient This value may be null.
+   * @return The new {@link android.webkit.WebView} object.
+   */
+  @VisibleForTesting
+  static WebView createWebView(
+      WebViewBuilder webViewBuilder,
+      Map<String, Object> params,
+      WebChromeClient webChromeClient,
+      @Nullable DownloadListener downloadListener) {
+    boolean usesHybridComposition = Boolean.TRUE.equals(params.get("usesHybridComposition"));
+    webViewBuilder
+        .setUsesHybridComposition(usesHybridComposition)
+        .setDomStorageEnabled(true) // Always enable DOM storage API.
+        .setJavaScriptCanOpenWindowsAutomatically(
+            true) // Always allow automatically opening of windows.
+        .setSupportMultipleWindows(true) // Always support multiple windows.
+        .setWebChromeClient(webChromeClient)
+        .setDownloadListener(
+            downloadListener); // Always use {@link FlutterWebChromeClient} as web Chrome client.
+
+    return webViewBuilder.build();
+  }
+
+  @Override
+  public View getView() {
+    return webView;
+  }
+
+  // @Override
+  // This is overriding a method that hasn't rolled into stable Flutter yet. Including the
+  // annotation would cause compile time failures in versions of Flutter too old to include the new
+  // method. However leaving it raw like this means that the method will be ignored in old versions
+  // of Flutter but used as an override anyway wherever it's actually defined.
+  // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable.
+  public void onInputConnectionUnlocked() {
+    if (webView instanceof InputAwareWebView) {
+      ((InputAwareWebView) webView).unlockInputConnection();
+    }
+  }
+
+  // @Override
+  // This is overriding a method that hasn't rolled into stable Flutter yet. Including the
+  // annotation would cause compile time failures in versions of Flutter too old to include the new
+  // method. However leaving it raw like this means that the method will be ignored in old versions
+  // of Flutter but used as an override anyway wherever it's actually defined.
+  // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable.
+  public void onInputConnectionLocked() {
+    if (webView instanceof InputAwareWebView) {
+      ((InputAwareWebView) webView).lockInputConnection();
+    }
+  }
+
+  // @Override
+  // This is overriding a method that hasn't rolled into stable Flutter yet. Including the
+  // annotation would cause compile time failures in versions of Flutter too old to include the new
+  // method. However leaving it raw like this means that the method will be ignored in old versions
+  // of Flutter but used as an override anyway wherever it's actually defined.
+  // TODO(mklim): Add the @Override annotation once stable passes v1.10.9.
+  public void onFlutterViewAttached(View flutterView) {
+    if (webView instanceof InputAwareWebView) {
+      ((InputAwareWebView) webView).setContainerView(flutterView);
+    }
+  }
+
+  // @Override
+  // This is overriding a method that hasn't rolled into stable Flutter yet. Including the
+  // annotation would cause compile time failures in versions of Flutter too old to include the new
+  // method. However leaving it raw like this means that the method will be ignored in old versions
+  // of Flutter but used as an override anyway wherever it's actually defined.
+  // TODO(mklim): Add the @Override annotation once stable passes v1.10.9.
+  public void onFlutterViewDetached() {
+    if (webView instanceof InputAwareWebView) {
+      ((InputAwareWebView) webView).setContainerView(null);
+    }
+  }
+
+  @Override
+  public void onMethodCall(MethodCall methodCall, Result result) {
+    switch (methodCall.method) {
+      case "loadUrl":
+        loadUrl(methodCall, result);
+        break;
+      case "updateSettings":
+        updateSettings(methodCall, result);
+        break;
+      case "canGoBack":
+        canGoBack(result);
+        break;
+      case "canGoForward":
+        canGoForward(result);
+        break;
+      case "goBack":
+        goBack(result);
+        break;
+      case "goForward":
+        goForward(result);
+        break;
+      case "reload":
+        reload(result);
+        break;
+      case "currentUrl":
+        currentUrl(result);
+        break;
+      case "evaluateJavascript":
+        evaluateJavaScript(methodCall, result);
+        break;
+      case "addJavascriptChannels":
+        addJavaScriptChannels(methodCall, result);
+        break;
+      case "removeJavascriptChannels":
+        removeJavaScriptChannels(methodCall, result);
+        break;
+      case "clearCache":
+        clearCache(result);
+        break;
+      case "getTitle":
+        getTitle(result);
+        break;
+      case "scrollTo":
+        scrollTo(methodCall, result);
+        break;
+      case "scrollBy":
+        scrollBy(methodCall, result);
+        break;
+      case "getScrollX":
+        getScrollX(result);
+        break;
+      case "getScrollY":
+        getScrollY(result);
+        break;
+      default:
+        result.notImplemented();
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private void loadUrl(MethodCall methodCall, Result result) {
+    Map<String, Object> request = (Map<String, Object>) methodCall.arguments;
+    String url = (String) request.get("url");
+    Map<String, String> headers = (Map<String, String>) request.get("headers");
+    if (headers == null) {
+      headers = Collections.emptyMap();
+    }
+    webView.loadUrl(url, headers);
+    result.success(null);
+  }
+
+  private void canGoBack(Result result) {
+    result.success(webView.canGoBack());
+  }
+
+  private void canGoForward(Result result) {
+    result.success(webView.canGoForward());
+  }
+
+  private void goBack(Result result) {
+    if (webView.canGoBack()) {
+      webView.goBack();
+    }
+    result.success(null);
+  }
+
+  private void goForward(Result result) {
+    if (webView.canGoForward()) {
+      webView.goForward();
+    }
+    result.success(null);
+  }
+
+  private void reload(Result result) {
+    webView.reload();
+    result.success(null);
+  }
+
+  private void currentUrl(Result result) {
+    result.success(webView.getUrl());
+  }
+
+  @SuppressWarnings("unchecked")
+  private void updateSettings(MethodCall methodCall, Result result) {
+    applySettings((Map<String, Object>) methodCall.arguments);
+    result.success(null);
+  }
+
+  @TargetApi(Build.VERSION_CODES.KITKAT)
+  private void evaluateJavaScript(MethodCall methodCall, final Result result) {
+    String jsString = (String) methodCall.arguments;
+    if (jsString == null) {
+      throw new UnsupportedOperationException("JavaScript string cannot be null");
+    }
+    webView.evaluateJavascript(
+        jsString,
+        new android.webkit.ValueCallback<String>() {
+          @Override
+          public void onReceiveValue(String value) {
+            result.success(value);
+          }
+        });
+  }
+
+  @SuppressWarnings("unchecked")
+  private void addJavaScriptChannels(MethodCall methodCall, Result result) {
+    List<String> channelNames = (List<String>) methodCall.arguments;
+    registerJavaScriptChannelNames(channelNames);
+    result.success(null);
+  }
+
+  @SuppressWarnings("unchecked")
+  private void removeJavaScriptChannels(MethodCall methodCall, Result result) {
+    List<String> channelNames = (List<String>) methodCall.arguments;
+    for (String channelName : channelNames) {
+      webView.removeJavascriptInterface(channelName);
+    }
+    result.success(null);
+  }
+
+  private void clearCache(Result result) {
+    webView.clearCache(true);
+    WebStorage.getInstance().deleteAllData();
+    result.success(null);
+  }
+
+  private void getTitle(Result result) {
+    result.success(webView.getTitle());
+  }
+
+  private void scrollTo(MethodCall methodCall, Result result) {
+    Map<String, Object> request = methodCall.arguments();
+    int x = (int) request.get("x");
+    int y = (int) request.get("y");
+
+    webView.scrollTo(x, y);
+
+    result.success(null);
+  }
+
+  private void scrollBy(MethodCall methodCall, Result result) {
+    Map<String, Object> request = methodCall.arguments();
+    int x = (int) request.get("x");
+    int y = (int) request.get("y");
+
+    webView.scrollBy(x, y);
+    result.success(null);
+  }
+
+  private void getScrollX(Result result) {
+    result.success(webView.getScrollX());
+  }
+
+  private void getScrollY(Result result) {
+    result.success(webView.getScrollY());
+  }
+
+  private void applySettings(Map<String, Object> settings) {
+    for (String key : settings.keySet()) {
+      switch (key) {
+        case "jsMode":
+          Integer mode = (Integer) settings.get(key);
+          if (mode != null) {
+            updateJsMode(mode);
+          }
+          break;
+        case "hasNavigationDelegate":
+          final boolean hasNavigationDelegate = (boolean) settings.get(key);
+
+          final WebViewClient webViewClient =
+              flutterWebViewClient.createWebViewClient(hasNavigationDelegate);
+
+          webView.setWebViewClient(webViewClient);
+          break;
+        case "debuggingEnabled":
+          final boolean debuggingEnabled = (boolean) settings.get(key);
+
+          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+            webView.setWebContentsDebuggingEnabled(debuggingEnabled);
+          }
+          break;
+        case "hasProgressTracking":
+          flutterWebViewClient.hasProgressTracking = (boolean) settings.get(key);
+          break;
+        case "gestureNavigationEnabled":
+          break;
+        case "userAgent":
+          updateUserAgent((String) settings.get(key));
+          break;
+        case "allowsInlineMediaPlayback":
+          // no-op inline media playback is always allowed on Android.
+          break;
+        default:
+          throw new IllegalArgumentException("Unknown WebView setting: " + key);
+      }
+    }
+  }
+
+  private void updateJsMode(int mode) {
+    switch (mode) {
+      case 0: // disabled
+        webView.getSettings().setJavaScriptEnabled(false);
+        break;
+      case 1: // unrestricted
+        webView.getSettings().setJavaScriptEnabled(true);
+        break;
+      default:
+        throw new IllegalArgumentException("Trying to set unknown JavaScript mode: " + mode);
+    }
+  }
+
+  private void updateAutoMediaPlaybackPolicy(int mode) {
+    // This is the index of the AutoMediaPlaybackPolicy enum, index 1 is always_allow, for all
+    // other values we require a user gesture.
+    boolean requireUserGesture = mode != 1;
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+      webView.getSettings().setMediaPlaybackRequiresUserGesture(requireUserGesture);
+    }
+  }
+
+  private void registerJavaScriptChannelNames(List<String> channelNames) {
+    for (String channelName : channelNames) {
+      webView.addJavascriptInterface(
+          new JavaScriptChannel(methodChannel, channelName, platformThreadHandler), channelName);
+    }
+  }
+
+  private void updateUserAgent(String userAgent) {
+    webView.getSettings().setUserAgentString(userAgent);
+  }
+
+  @Override
+  public void dispose() {
+    methodChannel.setMethodCallHandler(null);
+    if (webView instanceof InputAwareWebView) {
+      ((InputAwareWebView) webView).dispose();
+    }
+    webView.destroy();
+  }
+}
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
new file mode 100644
index 0000000..260ef8e
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java
@@ -0,0 +1,323 @@
+// 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.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.graphics.Bitmap;
+import android.os.Build;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.webkit.WebResourceError;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.webkit.WebResourceErrorCompat;
+import androidx.webkit.WebViewClientCompat;
+import io.flutter.plugin.common.MethodChannel;
+import java.util.HashMap;
+import java.util.Locale;
+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 {
+  private static final String TAG = "FlutterWebViewClient";
+  private final MethodChannel methodChannel;
+  private boolean hasNavigationDelegate;
+  boolean hasProgressTracking;
+
+  FlutterWebViewClient(MethodChannel methodChannel) {
+    this.methodChannel = methodChannel;
+  }
+
+  static String errorCodeToString(int errorCode) {
+    switch (errorCode) {
+      case WebViewClient.ERROR_AUTHENTICATION:
+        return "authentication";
+      case WebViewClient.ERROR_BAD_URL:
+        return "badUrl";
+      case WebViewClient.ERROR_CONNECT:
+        return "connect";
+      case WebViewClient.ERROR_FAILED_SSL_HANDSHAKE:
+        return "failedSslHandshake";
+      case WebViewClient.ERROR_FILE:
+        return "file";
+      case WebViewClient.ERROR_FILE_NOT_FOUND:
+        return "fileNotFound";
+      case WebViewClient.ERROR_HOST_LOOKUP:
+        return "hostLookup";
+      case WebViewClient.ERROR_IO:
+        return "io";
+      case WebViewClient.ERROR_PROXY_AUTHENTICATION:
+        return "proxyAuthentication";
+      case WebViewClient.ERROR_REDIRECT_LOOP:
+        return "redirectLoop";
+      case WebViewClient.ERROR_TIMEOUT:
+        return "timeout";
+      case WebViewClient.ERROR_TOO_MANY_REQUESTS:
+        return "tooManyRequests";
+      case WebViewClient.ERROR_UNKNOWN:
+        return "unknown";
+      case WebViewClient.ERROR_UNSAFE_RESOURCE:
+        return "unsafeResource";
+      case WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME:
+        return "unsupportedAuthScheme";
+      case WebViewClient.ERROR_UNSUPPORTED_SCHEME:
+        return "unsupportedScheme";
+    }
+
+    final String message =
+        String.format(Locale.getDefault(), "Could not find a string for errorCode: %d", errorCode);
+    throw new IllegalArgumentException(message);
+  }
+
+  @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+  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();
+  }
+
+  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;
+  }
+
+  /**
+   * Notifies the Flutter code that a download should start when a navigation delegate is set.
+   *
+   * @param view the webView the result of the navigation delegate will be send to.
+   * @param url the download url
+   * @return A boolean whether or not the request is forwarded to the Flutter code.
+   */
+  boolean notifyDownload(WebView view, String url) {
+    if (!hasNavigationDelegate) {
+      return false;
+    }
+
+    notifyOnNavigationRequest(url, null, view, true);
+    return true;
+  }
+
+  private void onPageStarted(WebView view, String url) {
+    Map<String, Object> args = new HashMap<>();
+    args.put("url", url);
+    methodChannel.invokeMethod("onPageStarted", args);
+  }
+
+  private void onPageFinished(WebView view, String url) {
+    Map<String, Object> args = new HashMap<>();
+    args.put("url", url);
+    methodChannel.invokeMethod("onPageFinished", args);
+  }
+
+  void onLoadingProgress(int progress) {
+    if (hasProgressTracking) {
+      Map<String, Object> args = new HashMap<>();
+      args.put("progress", progress);
+      methodChannel.invokeMethod("onProgress", args);
+    }
+  }
+
+  private void onWebResourceError(
+      final int errorCode, final String description, final String failingUrl) {
+    final Map<String, Object> args = new HashMap<>();
+    args.put("errorCode", errorCode);
+    args.put("description", description);
+    args.put("errorType", FlutterWebViewClient.errorCodeToString(errorCode));
+    args.put("failingUrl", failingUrl);
+    methodChannel.invokeMethod("onWebResourceError", args);
+  }
+
+  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);
+    }
+  }
+
+  // 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.
+  WebViewClient createWebViewClient(boolean hasNavigationDelegate) {
+    this.hasNavigationDelegate = hasNavigationDelegate;
+
+    if (!hasNavigationDelegate || android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+      return internalCreateWebViewClient();
+    }
+
+    return internalCreateWebViewClientCompat();
+  }
+
+  private WebViewClient internalCreateWebViewClient() {
+    return new WebViewClient() {
+      @TargetApi(Build.VERSION_CODES.N)
+      @Override
+      public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
+        return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request);
+      }
+
+      @Override
+      public void onPageStarted(WebView view, String url, Bitmap favicon) {
+        FlutterWebViewClient.this.onPageStarted(view, url);
+      }
+
+      @Override
+      public void onPageFinished(WebView view, String url) {
+        FlutterWebViewClient.this.onPageFinished(view, url);
+      }
+
+      @TargetApi(Build.VERSION_CODES.M)
+      @Override
+      public void onReceivedError(
+          WebView view, WebResourceRequest request, WebResourceError error) {
+        if (request.isForMainFrame()) {
+          FlutterWebViewClient.this.onWebResourceError(
+              error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString());
+        }
+      }
+
+      @Override
+      public void onReceivedError(
+          WebView view, int errorCode, String description, String failingUrl) {
+        FlutterWebViewClient.this.onWebResourceError(errorCode, description, failingUrl);
+      }
+
+      @Override
+      public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
+        // Deliberately empty. Occasionally the webview will mark events as having failed to be
+        // handled even though they were handled. We don't want to propagate those as they're not
+        // truly lost.
+      }
+    };
+  }
+
+  private WebViewClientCompat internalCreateWebViewClientCompat() {
+    return new WebViewClientCompat() {
+      @Override
+      public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
+        return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request);
+      }
+
+      @Override
+      public boolean shouldOverrideUrlLoading(WebView view, String url) {
+        return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, url);
+      }
+
+      @Override
+      public void onPageStarted(WebView view, String url, Bitmap favicon) {
+        FlutterWebViewClient.this.onPageStarted(view, url);
+      }
+
+      @Override
+      public void onPageFinished(WebView view, String url) {
+        FlutterWebViewClient.this.onPageFinished(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)
+      @SuppressLint("RequiresFeature")
+      @Override
+      public void onReceivedError(
+          @NonNull WebView view,
+          @NonNull WebResourceRequest request,
+          @NonNull WebResourceErrorCompat error) {
+        if (request.isForMainFrame()) {
+          FlutterWebViewClient.this.onWebResourceError(
+              error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString());
+        }
+      }
+
+      @Override
+      public void onReceivedError(
+          WebView view, int errorCode, String description, String failingUrl) {
+        FlutterWebViewClient.this.onWebResourceError(errorCode, description, failingUrl);
+      }
+
+      @Override
+      public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
+        // Deliberately empty. Occasionally the webview will mark events as having failed to be
+        // handled even though they were handled. We don't want to propagate those as they're not
+        // truly lost.
+      }
+    };
+  }
+
+  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/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java
new file mode 100644
index 0000000..8fe5810
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java
@@ -0,0 +1,33 @@
+// 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.content.Context;
+import android.view.View;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.StandardMessageCodec;
+import io.flutter.plugin.platform.PlatformView;
+import io.flutter.plugin.platform.PlatformViewFactory;
+import java.util.Map;
+
+public final class FlutterWebViewFactory extends PlatformViewFactory {
+  private final BinaryMessenger messenger;
+  private final View containerView;
+
+  FlutterWebViewFactory(BinaryMessenger messenger, View containerView) {
+    super(StandardMessageCodec.INSTANCE);
+    this.messenger = messenger;
+    this.containerView = containerView;
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public PlatformView create(Context context, int id, Object args) {
+    Map<String, Object> params = (Map<String, Object>) args;
+    MethodChannel methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id);
+    return new FlutterWebView(context, methodChannel, params, containerView);
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java
new file mode 100644
index 0000000..51b2a38
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java
@@ -0,0 +1,233 @@
+// 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 android.content.Context.INPUT_METHOD_SERVICE;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.Log;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.webkit.WebView;
+import android.widget.ListPopupWindow;
+
+/**
+ * A WebView subclass that mirrors the same implementation hacks that the system WebView does in
+ * order to correctly create an InputConnection.
+ *
+ * <p>These hacks are only needed in Android versions below N and exist to create an InputConnection
+ * on the WebView's dedicated input, or IME, thread. The majority of this proxying logic is in
+ * {@link #checkInputConnectionProxy}.
+ *
+ * <p>See also {@link ThreadedInputConnectionProxyAdapterView}.
+ */
+final class InputAwareWebView extends WebView {
+  private static final String TAG = "InputAwareWebView";
+  private View threadedInputConnectionProxyView;
+  private ThreadedInputConnectionProxyAdapterView proxyAdapterView;
+  private View containerView;
+
+  InputAwareWebView(Context context, View containerView) {
+    super(context);
+    this.containerView = containerView;
+  }
+
+  void setContainerView(View containerView) {
+    this.containerView = containerView;
+
+    if (proxyAdapterView == null) {
+      return;
+    }
+
+    Log.w(TAG, "The containerView has changed while the proxyAdapterView exists.");
+    if (containerView != null) {
+      setInputConnectionTarget(proxyAdapterView);
+    }
+  }
+
+  /**
+   * Set our proxy adapter view to use its cached input connection instead of creating new ones.
+   *
+   * <p>This is used to avoid losing our input connection when the virtual display is resized.
+   */
+  void lockInputConnection() {
+    if (proxyAdapterView == null) {
+      return;
+    }
+
+    proxyAdapterView.setLocked(true);
+  }
+
+  /** Sets the proxy adapter view back to its default behavior. */
+  void unlockInputConnection() {
+    if (proxyAdapterView == null) {
+      return;
+    }
+
+    proxyAdapterView.setLocked(false);
+  }
+
+  /** Restore the original InputConnection, if needed. */
+  void dispose() {
+    resetInputConnection();
+  }
+
+  /**
+   * Creates an InputConnection from the IME thread when needed.
+   *
+   * <p>We only need to create a {@link ThreadedInputConnectionProxyAdapterView} and create an
+   * InputConnectionProxy on the IME thread when WebView is doing the same thing. So we rely on the
+   * system calling this method for WebView's proxy view in order to know when we need to create our
+   * own.
+   *
+   * <p>This method would normally be called for any View that used the InputMethodManager. We rely
+   * on flutter/engine filtering the calls we receive down to the ones in our hierarchy and the
+   * system WebView in order to know whether or not the system WebView expects an InputConnection on
+   * the IME thread.
+   */
+  @Override
+  public boolean checkInputConnectionProxy(final View view) {
+    // Check to see if the view param is WebView's ThreadedInputConnectionProxyView.
+    View previousProxy = threadedInputConnectionProxyView;
+    threadedInputConnectionProxyView = view;
+    if (previousProxy == view) {
+      // This isn't a new ThreadedInputConnectionProxyView. Ignore it.
+      return super.checkInputConnectionProxy(view);
+    }
+    if (containerView == null) {
+      Log.e(
+          TAG,
+          "Can't create a proxy view because there's no container view. Text input may not work.");
+      return super.checkInputConnectionProxy(view);
+    }
+
+    // We've never seen this before, so we make the assumption that this is WebView's
+    // ThreadedInputConnectionProxyView. We are making the assumption that the only view that could
+    // possibly be interacting with the IMM here is WebView's ThreadedInputConnectionProxyView.
+    proxyAdapterView =
+        new ThreadedInputConnectionProxyAdapterView(
+            /*containerView=*/ containerView,
+            /*targetView=*/ view,
+            /*imeHandler=*/ view.getHandler());
+    setInputConnectionTarget(/*targetView=*/ proxyAdapterView);
+    return super.checkInputConnectionProxy(view);
+  }
+
+  /**
+   * Ensure that input creation happens back on {@link #containerView}'s thread once this view no
+   * longer has focus.
+   *
+   * <p>The logic in {@link #checkInputConnectionProxy} forces input creation to happen on Webview's
+   * thread for all connections. We undo it here so users will be able to go back to typing in
+   * Flutter UIs as expected.
+   */
+  @Override
+  public void clearFocus() {
+    super.clearFocus();
+    resetInputConnection();
+  }
+
+  /**
+   * Ensure that input creation happens back on {@link #containerView}.
+   *
+   * <p>The logic in {@link #checkInputConnectionProxy} forces input creation to happen on Webview's
+   * thread for all connections. We undo it here so users will be able to go back to typing in
+   * Flutter UIs as expected.
+   */
+  private void resetInputConnection() {
+    if (proxyAdapterView == null) {
+      // No need to reset the InputConnection to the default thread if we've never changed it.
+      return;
+    }
+    if (containerView == null) {
+      Log.e(TAG, "Can't reset the input connection to the container view because there is none.");
+      return;
+    }
+    setInputConnectionTarget(/*targetView=*/ containerView);
+  }
+
+  /**
+   * This is the crucial trick that gets the InputConnection creation to happen on the correct
+   * thread pre Android N.
+   * https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnectionFactory.java?l=169&rcl=f0698ee3e4483fad5b0c34159276f71cfaf81f3a
+   *
+   * <p>{@code targetView} should have a {@link View#getHandler} method with the thread that future
+   * InputConnections should be created on.
+   */
+  private void setInputConnectionTarget(final View targetView) {
+    if (containerView == null) {
+      Log.e(
+          TAG,
+          "Can't set the input connection target because there is no containerView to use as a handler.");
+      return;
+    }
+
+    targetView.requestFocus();
+    containerView.post(
+        new Runnable() {
+          @Override
+          public void run() {
+            InputMethodManager imm =
+                (InputMethodManager) getContext().getSystemService(INPUT_METHOD_SERVICE);
+            // This is a hack to make InputMethodManager believe that the target view now has focus.
+            // As a result, InputMethodManager will think that targetView is focused, and will call
+            // getHandler() of the view when creating input connection.
+
+            // Step 1: Set targetView as InputMethodManager#mNextServedView. This does not affect
+            // the real window focus.
+            targetView.onWindowFocusChanged(true);
+
+            // Step 2: Have InputMethodManager focus in on targetView. As a result, IMM will call
+            // onCreateInputConnection() on targetView on the same thread as
+            // targetView.getHandler(). It will also call subsequent InputConnection methods on this
+            // thread. This is the IME thread in cases where targetView is our proxyAdapterView.
+            imm.isActive(containerView);
+          }
+        });
+  }
+
+  @Override
+  protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+    // This works around a crash when old (<67.0.3367.0) Chromium versions are used.
+
+    // Prior to Chromium 67.0.3367 the following sequence happens when a select drop down is shown
+    // on tablets:
+    //
+    //  - WebView is calling ListPopupWindow#show
+    //  - buildDropDown is invoked, which sets mDropDownList to a DropDownListView.
+    //  - showAsDropDown is invoked - resulting in mDropDownList being added to the window and is
+    //    also synchronously performing the following sequence:
+    //    - WebView's focus change listener is loosing focus (as mDropDownList got it)
+    //    - WebView is hiding all popups (as it lost focus)
+    //    - WebView's SelectPopupDropDown#hide is invoked.
+    //    - DropDownPopupWindow#dismiss is invoked setting mDropDownList to null.
+    //  - mDropDownList#setSelection is invoked and is throwing a NullPointerException (as we just set mDropDownList to null).
+    //
+    // To workaround this, we drop the problematic focus lost call.
+    // See more details on: https://github.com/flutter/flutter/issues/54164
+    //
+    // We don't do this after Android P as it shipped with a new enough WebView version, and it's
+    // better to not do this on all future Android versions in case DropDownListView's code changes.
+    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P
+        && isCalledFromListPopupWindowShow()
+        && !focused) {
+      return;
+    }
+    super.onFocusChanged(focused, direction, previouslyFocusedRect);
+  }
+
+  private boolean isCalledFromListPopupWindowShow() {
+    StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
+    for (StackTraceElement stackTraceElement : stackTraceElements) {
+      if (stackTraceElement.getClassName().equals(ListPopupWindow.class.getCanonicalName())
+          && stackTraceElement.getMethodName().equals("show")) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java
new file mode 100644
index 0000000..4d59635
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java
@@ -0,0 +1,58 @@
+// 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.Handler;
+import android.os.Looper;
+import android.webkit.JavascriptInterface;
+import io.flutter.plugin.common.MethodChannel;
+import java.util.HashMap;
+
+/**
+ * Added as a JavaScript interface to the WebView for any JavaScript channel that the Dart code sets
+ * up.
+ *
+ * <p>Exposes a single method named `postMessage` to JavaScript, which sends a message over a method
+ * channel to the Dart code.
+ */
+class JavaScriptChannel {
+  private final MethodChannel methodChannel;
+  private final String javaScriptChannelName;
+  private final Handler platformThreadHandler;
+
+  /**
+   * @param methodChannel the Flutter WebView method channel to which JS messages are sent
+   * @param javaScriptChannelName the name of the JavaScript channel, this is sent over the method
+   *     channel with each message to let the Dart code know which JavaScript channel the message
+   *     was sent through
+   */
+  JavaScriptChannel(
+      MethodChannel methodChannel, String javaScriptChannelName, Handler platformThreadHandler) {
+    this.methodChannel = methodChannel;
+    this.javaScriptChannelName = javaScriptChannelName;
+    this.platformThreadHandler = platformThreadHandler;
+  }
+
+  // Suppressing unused warning as this is invoked from JavaScript.
+  @SuppressWarnings("unused")
+  @JavascriptInterface
+  public void postMessage(final String message) {
+    Runnable postMessageRunnable =
+        new Runnable() {
+          @Override
+          public void run() {
+            HashMap<String, String> arguments = new HashMap<>();
+            arguments.put("channel", javaScriptChannelName);
+            arguments.put("message", message);
+            methodChannel.invokeMethod("javascriptChannelMessage", arguments);
+          }
+        };
+    if (platformThreadHandler.getLooper() == Looper.myLooper()) {
+      postMessageRunnable.run();
+    } else {
+      platformThreadHandler.post(postMessageRunnable);
+    }
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java
new file mode 100644
index 0000000..1c865c9
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java
@@ -0,0 +1,112 @@
+// 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.Handler;
+import android.os.IBinder;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+/**
+ * A fake View only exposed to InputMethodManager.
+ *
+ * <p>This follows a similar flow to Chromium's WebView (see
+ * https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnectionProxyView.java).
+ * WebView itself bounces its InputConnection around several different threads. We follow its logic
+ * here to get the same working connection.
+ *
+ * <p>This exists solely to forward input creation to WebView's ThreadedInputConnectionProxyView on
+ * the IME thread. The way that this is created in {@link
+ * InputAwareWebView#checkInputConnectionProxy} guarantees that we have a handle to
+ * ThreadedInputConnectionProxyView and {@link #onCreateInputConnection} is always called on the IME
+ * thread. We delegate to ThreadedInputConnectionProxyView there to get WebView's input connection.
+ */
+final class ThreadedInputConnectionProxyAdapterView extends View {
+  final Handler imeHandler;
+  final IBinder windowToken;
+  final View containerView;
+  final View rootView;
+  final View targetView;
+
+  private boolean triggerDelayed = true;
+  private boolean isLocked = false;
+  private InputConnection cachedConnection;
+
+  ThreadedInputConnectionProxyAdapterView(View containerView, View targetView, Handler imeHandler) {
+    super(containerView.getContext());
+    this.imeHandler = imeHandler;
+    this.containerView = containerView;
+    this.targetView = targetView;
+    windowToken = containerView.getWindowToken();
+    rootView = containerView.getRootView();
+    setFocusable(true);
+    setFocusableInTouchMode(true);
+    setVisibility(VISIBLE);
+  }
+
+  /** Returns whether or not this is currently asynchronously acquiring an input connection. */
+  boolean isTriggerDelayed() {
+    return triggerDelayed;
+  }
+
+  /** Sets whether or not this should use its previously cached input connection. */
+  void setLocked(boolean locked) {
+    isLocked = locked;
+  }
+
+  /**
+   * This is expected to be called on the IME thread. See the setup required for this in {@link
+   * InputAwareWebView#checkInputConnectionProxy(View)}.
+   *
+   * <p>Delegates to ThreadedInputConnectionProxyView to get WebView's input connection.
+   */
+  @Override
+  public InputConnection onCreateInputConnection(final EditorInfo outAttrs) {
+    triggerDelayed = false;
+    InputConnection inputConnection =
+        (isLocked) ? cachedConnection : targetView.onCreateInputConnection(outAttrs);
+    triggerDelayed = true;
+    cachedConnection = inputConnection;
+    return inputConnection;
+  }
+
+  @Override
+  public boolean checkInputConnectionProxy(View view) {
+    return true;
+  }
+
+  @Override
+  public boolean hasWindowFocus() {
+    // None of our views here correctly report they have window focus because of how we're embedding
+    // the platform view inside of a virtual display.
+    return true;
+  }
+
+  @Override
+  public View getRootView() {
+    return rootView;
+  }
+
+  @Override
+  public boolean onCheckIsTextEditor() {
+    return true;
+  }
+
+  @Override
+  public boolean isFocused() {
+    return true;
+  }
+
+  @Override
+  public IBinder getWindowToken() {
+    return windowToken;
+  }
+
+  @Override
+  public Handler getHandler() {
+    return imeHandler;
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java
new file mode 100644
index 0000000..d3cd1d5
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java
@@ -0,0 +1,155 @@
+// 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.content.Context;
+import android.view.View;
+import android.webkit.DownloadListener;
+import android.webkit.WebChromeClient;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Builder used to create {@link android.webkit.WebView} objects. */
+public class WebViewBuilder {
+
+  /** Factory used to create a new {@link android.webkit.WebView} instance. */
+  static class WebViewFactory {
+
+    /**
+     * Creates a new {@link android.webkit.WebView} instance.
+     *
+     * @param context an Activity Context to access application assets. This value cannot be null.
+     * @param usesHybridComposition If {@code false} a {@link InputAwareWebView} instance is
+     *     returned.
+     * @param containerView must be supplied when the {@code useHybridComposition} parameter is set
+     *     to {@code false}. Used to create an InputConnection on the WebView's dedicated input, or
+     *     IME, thread (see also {@link InputAwareWebView})
+     * @return A new instance of the {@link android.webkit.WebView} object.
+     */
+    static WebView create(Context context, boolean usesHybridComposition, View containerView) {
+      return usesHybridComposition
+          ? new WebView(context)
+          : new InputAwareWebView(context, containerView);
+    }
+  }
+
+  private final Context context;
+  private final View containerView;
+
+  private boolean enableDomStorage;
+  private boolean javaScriptCanOpenWindowsAutomatically;
+  private boolean supportMultipleWindows;
+  private boolean usesHybridComposition;
+  private WebChromeClient webChromeClient;
+  private DownloadListener downloadListener;
+
+  /**
+   * Constructs a new {@link WebViewBuilder} object with a custom implementation of the {@link
+   * WebViewFactory} object.
+   *
+   * @param context an Activity Context to access application assets. This value cannot be null.
+   * @param containerView must be supplied when the {@code useHybridComposition} parameter is set to
+   *     {@code false}. Used to create an InputConnection on the WebView's dedicated input, or IME,
+   *     thread (see also {@link InputAwareWebView})
+   */
+  WebViewBuilder(@NonNull final Context context, View containerView) {
+    this.context = context;
+    this.containerView = containerView;
+  }
+
+  /**
+   * Sets whether the DOM storage API is enabled. The default value is {@code false}.
+   *
+   * @param flag {@code true} is {@link android.webkit.WebView} should use the DOM storage API.
+   * @return This builder. This value cannot be {@code null}.
+   */
+  public WebViewBuilder setDomStorageEnabled(boolean flag) {
+    this.enableDomStorage = flag;
+    return this;
+  }
+
+  /**
+   * Sets whether JavaScript is allowed to open windows automatically. This applies to the
+   * JavaScript function {@code window.open()}. The default value is {@code false}.
+   *
+   * @param flag {@code true} if JavaScript is allowed to open windows automatically.
+   * @return This builder. This value cannot be {@code null}.
+   */
+  public WebViewBuilder setJavaScriptCanOpenWindowsAutomatically(boolean flag) {
+    this.javaScriptCanOpenWindowsAutomatically = flag;
+    return this;
+  }
+
+  /**
+   * Sets whether the {@link WebView} supports multiple windows. If set to {@code true}, {@link
+   * WebChromeClient#onCreateWindow} must be implemented by the host application. The default is
+   * {@code false}.
+   *
+   * @param flag {@code true} if multiple windows are supported.
+   * @return This builder. This value cannot be {@code null}.
+   */
+  public WebViewBuilder setSupportMultipleWindows(boolean flag) {
+    this.supportMultipleWindows = flag;
+    return this;
+  }
+
+  /**
+   * Sets whether the hybrid composition should be used.
+   *
+   * <p>If set to {@code true} a standard {@link WebView} is created. If set to {@code false} the
+   * {@link WebViewBuilder} will create a {@link InputAwareWebView} to workaround issues using the
+   * {@link WebView} on Android versions below N.
+   *
+   * @param flag {@code true} if uses hybrid composition. The default is {@code false}.
+   * @return This builder. This value cannot be {@code null}
+   */
+  public WebViewBuilder setUsesHybridComposition(boolean flag) {
+    this.usesHybridComposition = flag;
+    return this;
+  }
+
+  /**
+   * Sets the chrome handler. This is an implementation of WebChromeClient for use in handling
+   * JavaScript dialogs, favicons, titles, and the progress. This will replace the current handler.
+   *
+   * @param webChromeClient an implementation of WebChromeClient This value may be null.
+   * @return This builder. This value cannot be {@code null}.
+   */
+  public WebViewBuilder setWebChromeClient(@Nullable WebChromeClient webChromeClient) {
+    this.webChromeClient = webChromeClient;
+    return this;
+  }
+
+  /**
+   * Registers the interface to be used when content can not be handled by the rendering engine, and
+   * should be downloaded instead. This will replace the current handler.
+   *
+   * @param downloadListener an implementation of DownloadListener This value may be null.
+   * @return This builder. This value cannot be {@code null}.
+   */
+  public WebViewBuilder setDownloadListener(@Nullable DownloadListener downloadListener) {
+    this.downloadListener = downloadListener;
+    return this;
+  }
+
+  /**
+   * Build the {@link android.webkit.WebView} using the current settings.
+   *
+   * @return The {@link android.webkit.WebView} using the current settings.
+   */
+  public WebView build() {
+    WebView webView = WebViewFactory.create(context, usesHybridComposition, containerView);
+
+    WebSettings webSettings = webView.getSettings();
+    webSettings.setDomStorageEnabled(enableDomStorage);
+    webSettings.setJavaScriptCanOpenWindowsAutomatically(javaScriptCanOpenWindowsAutomatically);
+    webSettings.setSupportMultipleWindows(supportMultipleWindows);
+    webView.setWebChromeClient(webChromeClient);
+    webView.setDownloadListener(downloadListener);
+    return webView;
+  }
+}
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
new file mode 100644
index 0000000..268d35a
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java
@@ -0,0 +1,73 @@
+// 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 io.flutter.embedding.engine.plugins.FlutterPlugin;
+import io.flutter.plugin.common.BinaryMessenger;
+
+/**
+ * Java platform implementation of the webview_flutter plugin.
+ *
+ * <p>Register this in an add to app scenario to gracefully handle activity and context changes.
+ *
+ * <p>Call {@link #registerWith(Registrar)} to use the stable {@code io.flutter.plugin.common}
+ * package instead.
+ */
+public class WebViewFlutterPlugin implements FlutterPlugin {
+
+  private FlutterCookieManager flutterCookieManager;
+
+  /**
+   * Add an instance of this to {@link io.flutter.embedding.engine.plugins.PluginRegistry} to
+   * register it.
+   *
+   * <p>THIS PLUGIN CODE PATH DEPENDS ON A NEWER VERSION OF FLUTTER THAN THE ONE DEFINED IN THE
+   * PUBSPEC.YAML. Text input will fail on some Android devices unless this is used with at least
+   * flutter/flutter@1d4d63ace1f801a022ea9ec737bf8c15395588b9. Use the V1 embedding with {@link
+   * #registerWith(Registrar)} to use this plugin with older Flutter versions.
+   *
+   * <p>Registration should eventually be handled automatically by v2 of the
+   * GeneratedPluginRegistrant. https://github.com/flutter/flutter/issues/42694
+   */
+  public WebViewFlutterPlugin() {}
+
+  /**
+   * Registers a plugin implementation that uses the stable {@code io.flutter.plugin.common}
+   * package.
+   *
+   * <p>Calling this automatically initializes the plugin. However plugins initialized this way
+   * won't react to changes in activity or context, unlike {@link CameraPlugin}.
+   */
+  @SuppressWarnings("deprecation")
+  public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) {
+    registrar
+        .platformViewRegistry()
+        .registerViewFactory(
+            "plugins.flutter.io/webview",
+            new FlutterWebViewFactory(registrar.messenger(), registrar.view()));
+    new FlutterCookieManager(registrar.messenger());
+  }
+
+  @Override
+  public void onAttachedToEngine(FlutterPluginBinding binding) {
+    BinaryMessenger messenger = binding.getBinaryMessenger();
+    binding
+        .getPlatformViewRegistry()
+        .registerViewFactory(
+            "plugins.flutter.io/webview",
+            new FlutterWebViewFactory(messenger, /*containerView=*/ null));
+    flutterCookieManager = new FlutterCookieManager(messenger);
+  }
+
+  @Override
+  public void onDetachedFromEngine(FlutterPluginBinding binding) {
+    if (flutterCookieManager == null) {
+      return;
+    }
+
+    flutterCookieManager.dispose();
+    flutterCookieManager = null;
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java
new file mode 100644
index 0000000..2c91858
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java
@@ -0,0 +1,42 @@
+// 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.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.webkit.WebView;
+import org.junit.Before;
+import org.junit.Test;
+
+public class FlutterDownloadListenerTest {
+  private FlutterWebViewClient webViewClient;
+  private WebView webView;
+
+  @Before
+  public void before() {
+    webViewClient = mock(FlutterWebViewClient.class);
+    webView = mock(WebView.class);
+  }
+
+  @Test
+  public void onDownloadStart_should_notify_webViewClient() {
+    String url = "testurl.com";
+    FlutterDownloadListener downloadListener = new FlutterDownloadListener(webViewClient);
+    downloadListener.onDownloadStart(url, "test", "inline", "data/text", 0);
+    verify(webViewClient).notifyDownload(nullable(WebView.class), eq(url));
+  }
+
+  @Test
+  public void onDownloadStart_should_pass_webView() {
+    FlutterDownloadListener downloadListener = new FlutterDownloadListener(webViewClient);
+    downloadListener.setWebView(webView);
+    downloadListener.onDownloadStart("testurl.com", "test", "inline", "data/text", 0);
+    verify(webViewClient).notifyDownload(eq(webView), anyString());
+  }
+}
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
new file mode 100644
index 0000000..86346ac
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java
@@ -0,0 +1,60 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.webviewflutter;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+import android.webkit.WebView;
+import io.flutter.plugin.common.MethodChannel;
+import java.util.HashMap;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+public class FlutterWebViewClientTest {
+
+  MethodChannel mockMethodChannel;
+  WebView mockWebView;
+
+  @Before
+  public void before() {
+    mockMethodChannel = mock(MethodChannel.class);
+    mockWebView = mock(WebView.class);
+  }
+
+  @Test
+  public void notify_download_should_notifyOnNavigationRequest_when_navigationDelegate_is_set() {
+    final String url = "testurl.com";
+
+    FlutterWebViewClient client = new FlutterWebViewClient(mockMethodChannel);
+    client.createWebViewClient(true);
+
+    client.notifyDownload(mockWebView, url);
+    ArgumentCaptor<Object> argumentCaptor = ArgumentCaptor.forClass(Object.class);
+    verify(mockMethodChannel)
+        .invokeMethod(
+            eq("navigationRequest"), argumentCaptor.capture(), any(MethodChannel.Result.class));
+    HashMap<String, Object> map = (HashMap<String, Object>) argumentCaptor.getValue();
+    assertEquals(map.get("url"), url);
+    assertEquals(map.get("isForMainFrame"), true);
+  }
+
+  @Test
+  public void
+      notify_download_should_not_notifyOnNavigationRequest_when_navigationDelegate_is_not_set() {
+    final String url = "testurl.com";
+
+    FlutterWebViewClient client = new FlutterWebViewClient(mockMethodChannel);
+    client.createWebViewClient(false);
+
+    client.notifyDownload(mockWebView, url);
+    verifyNoInteractions(mockMethodChannel);
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java
new file mode 100644
index 0000000..56d9db1
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java
@@ -0,0 +1,66 @@
+// 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.anyBoolean;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.webkit.DownloadListener;
+import android.webkit.WebChromeClient;
+import android.webkit.WebView;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+
+public class FlutterWebViewTest {
+  private WebChromeClient mockWebChromeClient;
+  private DownloadListener mockDownloadListener;
+  private WebViewBuilder mockWebViewBuilder;
+  private WebView mockWebView;
+
+  @Before
+  public void before() {
+    mockWebChromeClient = mock(WebChromeClient.class);
+    mockWebViewBuilder = mock(WebViewBuilder.class);
+    mockWebView = mock(WebView.class);
+    mockDownloadListener = mock(DownloadListener.class);
+
+    when(mockWebViewBuilder.setDomStorageEnabled(anyBoolean())).thenReturn(mockWebViewBuilder);
+    when(mockWebViewBuilder.setJavaScriptCanOpenWindowsAutomatically(anyBoolean()))
+        .thenReturn(mockWebViewBuilder);
+    when(mockWebViewBuilder.setSupportMultipleWindows(anyBoolean())).thenReturn(mockWebViewBuilder);
+    when(mockWebViewBuilder.setUsesHybridComposition(anyBoolean())).thenReturn(mockWebViewBuilder);
+    when(mockWebViewBuilder.setWebChromeClient(any(WebChromeClient.class)))
+        .thenReturn(mockWebViewBuilder);
+    when(mockWebViewBuilder.setDownloadListener(any(DownloadListener.class)))
+        .thenReturn(mockWebViewBuilder);
+
+    when(mockWebViewBuilder.build()).thenReturn(mockWebView);
+  }
+
+  @Test
+  public void createWebView_should_create_webview_with_default_configuration() {
+    FlutterWebView.createWebView(
+        mockWebViewBuilder, createParameterMap(false), mockWebChromeClient, mockDownloadListener);
+
+    verify(mockWebViewBuilder, times(1)).setDomStorageEnabled(true);
+    verify(mockWebViewBuilder, times(1)).setJavaScriptCanOpenWindowsAutomatically(true);
+    verify(mockWebViewBuilder, times(1)).setSupportMultipleWindows(true);
+    verify(mockWebViewBuilder, times(1)).setUsesHybridComposition(false);
+    verify(mockWebViewBuilder, times(1)).setWebChromeClient(mockWebChromeClient);
+  }
+
+  private Map<String, Object> createParameterMap(boolean usesHybridComposition) {
+    Map<String, Object> params = new HashMap<>();
+    params.put("usesHybridComposition", usesHybridComposition);
+
+    return params;
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java
new file mode 100644
index 0000000..423cb21
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java
@@ -0,0 +1,104 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.webviewflutter;
+
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.*;
+
+import android.content.Context;
+import android.view.View;
+import android.webkit.DownloadListener;
+import android.webkit.WebChromeClient;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import io.flutter.plugins.webviewflutter.WebViewBuilder.WebViewFactory;
+import java.io.IOException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockedStatic;
+import org.mockito.MockedStatic.Verification;
+
+public class WebViewBuilderTest {
+  private Context mockContext;
+  private View mockContainerView;
+  private WebView mockWebView;
+  private MockedStatic<WebViewFactory> mockedStaticWebViewFactory;
+
+  @Before
+  public void before() {
+    mockContext = mock(Context.class);
+    mockContainerView = mock(View.class);
+    mockWebView = mock(WebView.class);
+    mockedStaticWebViewFactory = mockStatic(WebViewFactory.class);
+
+    mockedStaticWebViewFactory
+        .when(
+            new Verification() {
+              @Override
+              public void apply() {
+                WebViewFactory.create(mockContext, false, mockContainerView);
+              }
+            })
+        .thenReturn(mockWebView);
+  }
+
+  @After
+  public void after() {
+    mockedStaticWebViewFactory.close();
+  }
+
+  @Test
+  public void ctor_test() {
+    WebViewBuilder builder = new WebViewBuilder(mockContext, mockContainerView);
+
+    assertNotNull(builder);
+  }
+
+  @Test
+  public void build_should_set_values() throws IOException {
+    WebSettings mockWebSettings = mock(WebSettings.class);
+    WebChromeClient mockWebChromeClient = mock(WebChromeClient.class);
+    DownloadListener mockDownloadListener = mock(DownloadListener.class);
+
+    when(mockWebView.getSettings()).thenReturn(mockWebSettings);
+
+    WebViewBuilder builder =
+        new WebViewBuilder(mockContext, mockContainerView)
+            .setDomStorageEnabled(true)
+            .setJavaScriptCanOpenWindowsAutomatically(true)
+            .setSupportMultipleWindows(true)
+            .setWebChromeClient(mockWebChromeClient)
+            .setDownloadListener(mockDownloadListener);
+
+    WebView webView = builder.build();
+
+    assertNotNull(webView);
+    verify(mockWebSettings).setDomStorageEnabled(true);
+    verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(true);
+    verify(mockWebSettings).setSupportMultipleWindows(true);
+    verify(mockWebView).setWebChromeClient(mockWebChromeClient);
+    verify(mockWebView).setDownloadListener(mockDownloadListener);
+  }
+
+  @Test
+  public void build_should_use_default_values() throws IOException {
+    WebSettings mockWebSettings = mock(WebSettings.class);
+    WebChromeClient mockWebChromeClient = mock(WebChromeClient.class);
+
+    when(mockWebView.getSettings()).thenReturn(mockWebSettings);
+
+    WebViewBuilder builder = new WebViewBuilder(mockContext, mockContainerView);
+
+    WebView webView = builder.build();
+
+    assertNotNull(webView);
+    verify(mockWebSettings).setDomStorageEnabled(false);
+    verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(false);
+    verify(mockWebSettings).setSupportMultipleWindows(false);
+    verify(mockWebView).setWebChromeClient(null);
+    verify(mockWebView).setDownloadListener(null);
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java
new file mode 100644
index 0000000..131a5a3
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java
@@ -0,0 +1,49 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.webviewflutter;
+
+import static org.junit.Assert.assertEquals;
+
+import android.webkit.WebViewClient;
+import org.junit.Test;
+
+public class WebViewTest {
+  @Test
+  public void errorCodes() {
+    assertEquals(
+        FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_AUTHENTICATION),
+        "authentication");
+    assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_BAD_URL), "badUrl");
+    assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_CONNECT), "connect");
+    assertEquals(
+        FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FAILED_SSL_HANDSHAKE),
+        "failedSslHandshake");
+    assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FILE), "file");
+    assertEquals(
+        FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FILE_NOT_FOUND), "fileNotFound");
+    assertEquals(
+        FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_HOST_LOOKUP), "hostLookup");
+    assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_IO), "io");
+    assertEquals(
+        FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_PROXY_AUTHENTICATION),
+        "proxyAuthentication");
+    assertEquals(
+        FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_REDIRECT_LOOP), "redirectLoop");
+    assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_TIMEOUT), "timeout");
+    assertEquals(
+        FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_TOO_MANY_REQUESTS),
+        "tooManyRequests");
+    assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNKNOWN), "unknown");
+    assertEquals(
+        FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSAFE_RESOURCE),
+        "unsafeResource");
+    assertEquals(
+        FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME),
+        "unsupportedAuthScheme");
+    assertEquals(
+        FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSUPPORTED_SCHEME),
+        "unsupportedScheme");
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_android/example/.metadata b/packages/webview_flutter/webview_flutter_android/example/.metadata
new file mode 100644
index 0000000..da83b1a
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/.metadata
@@ -0,0 +1,8 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: 1e5cb2d87f8542f9fbbd0f22d528823274be0acb
+  channel: master
diff --git a/packages/webview_flutter/webview_flutter_android/example/README.md b/packages/webview_flutter/webview_flutter_android/example/README.md
new file mode 100644
index 0000000..850ee74
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/README.md
@@ -0,0 +1,8 @@
+# webview_flutter_example
+
+Demonstrates how to use the webview_flutter plugin.
+
+## Getting Started
+
+For help getting started with Flutter, view our online
+[documentation](https://flutter.dev/).
diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/build.gradle b/packages/webview_flutter/webview_flutter_android/example/android/app/build.gradle
new file mode 100644
index 0000000..1dcd363
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/android/app/build.gradle
@@ -0,0 +1,62 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+    localPropertiesFile.withReader('UTF-8') { reader ->
+        localProperties.load(reader)
+    }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+    flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+    flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+    compileSdkVersion 29
+
+    lintOptions {
+        disable 'InvalidPackage'
+    }
+
+    defaultConfig {
+        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+        applicationId "io.flutter.plugins.webviewflutterandroidexample"
+        minSdkVersion 19
+        targetSdkVersion 28
+        versionCode flutterVersionCode.toInteger()
+        versionName flutterVersionName
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+
+    buildTypes {
+        release {
+            // TODO: Add your own signing config for the release build.
+            // Signing with the debug keys for now, so `flutter run --release` works.
+            signingConfig signingConfigs.debug
+        }
+    }
+}
+
+flutter {
+    source '../..'
+}
+
+dependencies {
+    testImplementation 'junit:junit:4.12'
+    androidTestImplementation 'androidx.test:runner:1.2.0'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+    api 'androidx.test:core:1.2.0'
+}
diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/webview_flutter/webview_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..9a4163a
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java
new file mode 100644
index 0000000..0f4298d
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java
@@ -0,0 +1,14 @@
+// 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;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface DartIntegrationTest {}
diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java
new file mode 100644
index 0000000..a32aaeb
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java
@@ -0,0 +1,19 @@
+// 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.webviewflutterexample;
+
+import androidx.test.rule.ActivityTestRule;
+import dev.flutter.plugins.integration_test.FlutterTestRunner;
+import io.flutter.embedding.android.FlutterActivity;
+import io.flutter.plugins.DartIntegrationTest;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@DartIntegrationTest
+@RunWith(FlutterTestRunner.class)
+public class MainActivityTest {
+  @Rule
+  public ActivityTestRule<FlutterActivity> rule = new ActivityTestRule<>(FlutterActivity.class);
+}
diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java
new file mode 100644
index 0000000..0b3eeef
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java
@@ -0,0 +1,23 @@
+// 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.webviewflutterexample;
+
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.core.app.ActivityScenario;
+import io.flutter.plugins.webviewflutter.WebViewFlutterPlugin;
+import org.junit.Test;
+
+public class WebViewTest {
+  @Test
+  public void webViewPluginIsAdded() {
+    final ActivityScenario<WebViewTestActivity> scenario =
+        ActivityScenario.launch(WebViewTestActivity.class);
+    scenario.onActivity(
+        activity -> {
+          assertTrue(activity.engine.getPlugins().has(WebViewFlutterPlugin.class));
+        });
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml b/packages/webview_flutter/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..2879220
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="io.flutter.plugins.webviewflutterexample">
+    <!-- Flutter needs it to communicate with the running application
+         to allow setting breakpoints, to provide hot reload, etc.
+    -->
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <application android:usesCleartextTraffic="true">
+        <activity
+            android:name=".WebViewTestActivity"
+            android:launchMode="singleTop"
+            android:theme="@style/LaunchTheme"
+            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+            android:hardwareAccelerated="true"
+            android:windowSoftInputMode="adjustResize">
+        </activity>
+    </application>
+</manifest>
diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/AndroidManifest.xml b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..b8c8d38
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,42 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+  package="io.flutter.plugins.webviewflutterexample">
+
+  <application
+    android:usesCleartextTraffic="true"
+    android:icon="@mipmap/ic_launcher"
+    android:label="webview_flutter_example">
+    <meta-data
+        android:name="flutterEmbedding"
+        android:value="2" />
+    <activity
+      android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+      android:hardwareAccelerated="true"
+      android:launchMode="singleTop"
+      android:name="io.flutter.embedding.android.FlutterActivity"
+      android:theme="@style/LaunchTheme"
+      android:windowSoftInputMode="adjustResize">
+      <!-- This keeps the window background of the activity showing
+           until Flutter renders its first frame. It can be removed if
+           there is no splash screen (such as the default splash screen
+           defined in @style/LaunchTheme). -->
+      <meta-data
+        android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
+        android:value="true"/>
+      <intent-filter>
+        <action android:name="android.intent.action.MAIN"/>
+        <category android:name="android.intent.category.LAUNCHER"/>
+      </intent-filter>
+    </activity>
+  </application>
+
+  <!-- The INTERNET permission is required for development. Specifically,
+       flutter needs it to communicate with the running application
+       to allow setting breakpoints, to provide hot reload, etc.
+  -->
+  <uses-permission android:name="android.permission.INTERNET"/>
+
+  <!-- When tests are ran on Firebase Test Lab, a wake lock
+       permission failure prevents tests from running.
+  -->
+  <uses-permission android:name="android.permission.WAKE_LOCK" />
+</manifest>
diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/WebViewTestActivity.java b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/WebViewTestActivity.java
new file mode 100644
index 0000000..cb53a7a
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/WebViewTestActivity.java
@@ -0,0 +1,20 @@
+// 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.webviewflutterexample;
+
+import androidx.annotation.NonNull;
+import io.flutter.embedding.android.FlutterActivity;
+import io.flutter.embedding.engine.FlutterEngine;
+
+// Extends FlutterActivity to make the FlutterEngine accessible for testing.
+public class WebViewTestActivity extends FlutterActivity {
+  public FlutterEngine engine;
+
+  @Override
+  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
+    super.configureFlutterEngine(flutterEngine);
+    engine = flutterEngine;
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/drawable/launch_background.xml b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@android:color/white" />
+
+    <!-- You can insert your own image assets here -->
+    <!-- <item>
+        <bitmap
+            android:gravity="center"
+            android:src="@mipmap/launch_image" />
+    </item> -->
+</layer-list>
diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/values/styles.xml b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..00fa441
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <!-- Show a splash screen on the activity. Automatically removed when
+             Flutter draws its first frame -->
+        <item name="android:windowBackground">@drawable/launch_background</item>
+    </style>
+</resources>
diff --git a/packages/webview_flutter/webview_flutter_android/example/android/build.gradle b/packages/webview_flutter/webview_flutter_android/example/android/build.gradle
new file mode 100644
index 0000000..e101ac0
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/android/build.gradle
@@ -0,0 +1,29 @@
+buildscript {
+    repositories {
+        google()
+        mavenCentral()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.3.0'
+    }
+}
+
+allprojects {
+    repositories {
+        google()
+        mavenCentral()
+    }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+    project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+    project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}
diff --git a/packages/webview_flutter/webview_flutter_android/example/android/gradle.properties b/packages/webview_flutter/webview_flutter_android/example/android/gradle.properties
new file mode 100644
index 0000000..a673820
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/android/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
+android.enableR8=true
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
new file mode 100644
index 0000000..2819f02
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
diff --git a/packages/webview_flutter/webview_flutter_android/example/android/settings.gradle b/packages/webview_flutter/webview_flutter_android/example/android/settings.gradle
new file mode 100644
index 0000000..5a2f14f
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/android/settings.gradle
@@ -0,0 +1,15 @@
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+    pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
+}
+
+plugins.each { name, path ->
+    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+    include ":$name"
+    project(":$name").projectDir = pluginDirectory
+}
diff --git a/packages/webview_flutter/webview_flutter_android/example/assets/sample_audio.ogg b/packages/webview_flutter/webview_flutter_android/example/assets/sample_audio.ogg
new file mode 100644
index 0000000..27e1710
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/assets/sample_audio.ogg
Binary files differ
diff --git a/packages/webview_flutter/webview_flutter_android/example/assets/sample_video.mp4 b/packages/webview_flutter/webview_flutter_android/example/assets/sample_video.mp4
new file mode 100644
index 0000000..a203d0c
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/assets/sample_video.mp4
Binary files differ
diff --git a/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart
new file mode 100644
index 0000000..e218d90
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart
@@ -0,0 +1,1415 @@
+// 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 'dart:async';
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+import 'package:webview_flutter_android/webview_android.dart';
+import 'package:webview_flutter_android/webview_surface_android.dart';
+import 'package:webview_flutter_android_example/navigation_decision.dart';
+import 'package:webview_flutter_android_example/navigation_request.dart';
+import 'package:webview_flutter_android_example/web_view.dart';
+import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  const bool _skipDueToIssue86757 = true;
+
+  // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757.
+  testWidgets('initialUrl', (WidgetTester tester) async {
+    final Completer<WebViewController> controllerCompleter =
+        Completer<WebViewController>();
+    await tester.pumpWidget(
+      MaterialApp(
+        home: Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: 'https://flutter.dev/',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+          ),
+        ),
+      ),
+    );
+    final WebViewController controller = await controllerCompleter.future;
+    final String? currentUrl = await controller.currentUrl();
+    expect(currentUrl, 'https://flutter.dev/');
+  }, skip: _skipDueToIssue86757);
+
+  // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757.
+  testWidgets('loadUrl', (WidgetTester tester) async {
+    final Completer<WebViewController> controllerCompleter =
+        Completer<WebViewController>();
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          key: GlobalKey(),
+          initialUrl: 'https://flutter.dev/',
+          onWebViewCreated: (WebViewController controller) {
+            controllerCompleter.complete(controller);
+          },
+        ),
+      ),
+    );
+    final WebViewController controller = await controllerCompleter.future;
+    await controller.loadUrl('https://www.google.com/');
+    final String? currentUrl = await controller.currentUrl();
+    expect(currentUrl, 'https://www.google.com/');
+  }, skip: _skipDueToIssue86757);
+
+  // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757.
+  testWidgets('loadUrl with headers', (WidgetTester tester) async {
+    final Completer<WebViewController> controllerCompleter =
+        Completer<WebViewController>();
+    final StreamController<String> pageStarts = StreamController<String>();
+    final StreamController<String> pageLoads = StreamController<String>();
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          key: GlobalKey(),
+          initialUrl: 'https://flutter.dev/',
+          onWebViewCreated: (WebViewController controller) {
+            controllerCompleter.complete(controller);
+          },
+          javascriptMode: JavascriptMode.unrestricted,
+          onPageStarted: (String url) {
+            pageStarts.add(url);
+          },
+          onPageFinished: (String url) {
+            pageLoads.add(url);
+          },
+        ),
+      ),
+    );
+    final WebViewController controller = await controllerCompleter.future;
+    final Map<String, String> headers = <String, String>{
+      'test_header': 'flutter_test_header'
+    };
+    await controller.loadUrl('https://flutter-header-echo.herokuapp.com/',
+        headers: headers);
+    final String? currentUrl = await controller.currentUrl();
+    expect(currentUrl, 'https://flutter-header-echo.herokuapp.com/');
+
+    await pageStarts.stream.firstWhere((String url) => url == currentUrl);
+    await pageLoads.stream.firstWhere((String url) => url == currentUrl);
+
+    final String content = await controller
+        .evaluateJavascript('document.documentElement.innerText');
+    expect(content.contains('flutter_test_header'), isTrue);
+  }, skip: _skipDueToIssue86757);
+
+  // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757.
+  testWidgets('JavaScriptChannel', (WidgetTester tester) async {
+    final Completer<WebViewController> controllerCompleter =
+        Completer<WebViewController>();
+    final Completer<void> pageStarted = Completer<void>();
+    final Completer<void> pageLoaded = Completer<void>();
+    final List<String> messagesReceived = <String>[];
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          key: GlobalKey(),
+          // This is the data URL for: '<!DOCTYPE html>'
+          initialUrl:
+              'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+',
+          onWebViewCreated: (WebViewController controller) {
+            controllerCompleter.complete(controller);
+          },
+          javascriptMode: JavascriptMode.unrestricted,
+          javascriptChannels: <JavascriptChannel>{
+            JavascriptChannel(
+              name: 'Echo',
+              onMessageReceived: (JavascriptMessage message) {
+                messagesReceived.add(message.message);
+              },
+            ),
+          },
+          onPageStarted: (String url) {
+            pageStarted.complete(null);
+          },
+          onPageFinished: (String url) {
+            pageLoaded.complete(null);
+          },
+        ),
+      ),
+    );
+    final WebViewController controller = await controllerCompleter.future;
+    await pageStarted.future;
+    await pageLoaded.future;
+
+    expect(messagesReceived, isEmpty);
+    // Append a return value "1" in the end will prevent an iOS platform exception.
+    // See: https://github.com/flutter/flutter/issues/66318#issuecomment-701105380
+    // TODO(cyanglaz): remove the workaround "1" in the end when the below issue is fixed.
+    // https://github.com/flutter/flutter/issues/66318
+    await controller.evaluateJavascript('Echo.postMessage("hello");1;');
+    expect(messagesReceived, equals(<String>['hello']));
+  }, skip: _skipDueToIssue86757);
+
+  testWidgets('resize webview', (WidgetTester tester) async {
+    final String resizeTest = '''
+        <!DOCTYPE html><html>
+        <head><title>Resize test</title>
+          <script type="text/javascript">
+            function onResize() {
+              Resize.postMessage("resize");
+            }
+            function onLoad() {
+              window.onresize = onResize;
+            }
+          </script>
+        </head>
+        <body onload="onLoad();" bgColor="blue">
+        </body>
+        </html>
+      ''';
+    final String resizeTestBase64 =
+        base64Encode(const Utf8Encoder().convert(resizeTest));
+    final Completer<void> resizeCompleter = Completer<void>();
+    final Completer<void> pageStarted = Completer<void>();
+    final Completer<void> pageLoaded = Completer<void>();
+    final Completer<WebViewController> controllerCompleter =
+        Completer<WebViewController>();
+    final GlobalKey key = GlobalKey();
+
+    final WebView webView = WebView(
+      key: key,
+      initialUrl: 'data:text/html;charset=utf-8;base64,$resizeTestBase64',
+      onWebViewCreated: (WebViewController controller) {
+        controllerCompleter.complete(controller);
+      },
+      javascriptChannels: <JavascriptChannel>{
+        JavascriptChannel(
+          name: 'Resize',
+          onMessageReceived: (JavascriptMessage message) {
+            resizeCompleter.complete(true);
+          },
+        ),
+      },
+      onPageStarted: (String url) {
+        pageStarted.complete(null);
+      },
+      onPageFinished: (String url) {
+        pageLoaded.complete(null);
+      },
+      javascriptMode: JavascriptMode.unrestricted,
+    );
+
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: Column(
+          children: <Widget>[
+            SizedBox(
+              width: 200,
+              height: 200,
+              child: webView,
+            ),
+          ],
+        ),
+      ),
+    );
+
+    await controllerCompleter.future;
+    await pageStarted.future;
+    await pageLoaded.future;
+
+    expect(resizeCompleter.isCompleted, false);
+
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: Column(
+          children: <Widget>[
+            SizedBox(
+              width: 400,
+              height: 400,
+              child: webView,
+            ),
+          ],
+        ),
+      ),
+    );
+
+    await resizeCompleter.future;
+  });
+
+  testWidgets('set custom userAgent', (WidgetTester tester) async {
+    final Completer<WebViewController> controllerCompleter1 =
+        Completer<WebViewController>();
+    final GlobalKey _globalKey = GlobalKey();
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          key: _globalKey,
+          initialUrl: 'about:blank',
+          javascriptMode: JavascriptMode.unrestricted,
+          userAgent: 'Custom_User_Agent1',
+          onWebViewCreated: (WebViewController controller) {
+            controllerCompleter1.complete(controller);
+          },
+        ),
+      ),
+    );
+    final WebViewController controller1 = await controllerCompleter1.future;
+    final String customUserAgent1 = await _getUserAgent(controller1);
+    expect(customUserAgent1, 'Custom_User_Agent1');
+    // rebuild the WebView with a different user agent.
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          key: _globalKey,
+          initialUrl: 'about:blank',
+          javascriptMode: JavascriptMode.unrestricted,
+          userAgent: 'Custom_User_Agent2',
+        ),
+      ),
+    );
+
+    final String customUserAgent2 = await _getUserAgent(controller1);
+    expect(customUserAgent2, 'Custom_User_Agent2');
+  });
+
+  // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757.
+  testWidgets('use default platform userAgent after webView is rebuilt',
+      (WidgetTester tester) async {
+    final Completer<WebViewController> controllerCompleter =
+        Completer<WebViewController>();
+    final GlobalKey _globalKey = GlobalKey();
+    // Build the webView with no user agent to get the default platform user agent.
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          key: _globalKey,
+          initialUrl: 'https://flutter.dev/',
+          javascriptMode: JavascriptMode.unrestricted,
+          onWebViewCreated: (WebViewController controller) {
+            controllerCompleter.complete(controller);
+          },
+        ),
+      ),
+    );
+    final WebViewController controller = await controllerCompleter.future;
+    final String defaultPlatformUserAgent = await _getUserAgent(controller);
+    // rebuild the WebView with a custom user agent.
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          key: _globalKey,
+          initialUrl: 'about:blank',
+          javascriptMode: JavascriptMode.unrestricted,
+          userAgent: 'Custom_User_Agent',
+        ),
+      ),
+    );
+    final String customUserAgent = await _getUserAgent(controller);
+    expect(customUserAgent, 'Custom_User_Agent');
+    // rebuilds the WebView with no user agent.
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          key: _globalKey,
+          initialUrl: 'about:blank',
+          javascriptMode: JavascriptMode.unrestricted,
+        ),
+      ),
+    );
+
+    final String customUserAgent2 = await _getUserAgent(controller);
+    expect(customUserAgent2, defaultPlatformUserAgent);
+  }, skip: _skipDueToIssue86757);
+
+  group('Video playback policy', () {
+    late String videoTestBase64;
+    setUpAll(() async {
+      final ByteData videoData =
+          await rootBundle.load('assets/sample_video.mp4');
+      final String base64VideoData =
+          base64Encode(Uint8List.view(videoData.buffer));
+      final String videoTest = '''
+        <!DOCTYPE html><html>
+        <head><title>Video auto play</title>
+          <script type="text/javascript">
+            function play() {
+              var video = document.getElementById("video");
+              video.play();
+              video.addEventListener('timeupdate', videoTimeUpdateHandler, false);
+            }
+            function videoTimeUpdateHandler(e) {
+              var video = document.getElementById("video");
+              VideoTestTime.postMessage(video.currentTime);
+            }
+            function isPaused() {
+              var video = document.getElementById("video");
+              return video.paused;
+            }
+            function isFullScreen() {
+              var video = document.getElementById("video");
+              return video.webkitDisplayingFullscreen;
+            }
+          </script>
+        </head>
+        <body onload="play();">
+        <video controls playsinline autoplay id="video">
+          <source src="data:video/mp4;charset=utf-8;base64,$base64VideoData">
+        </video>
+        </body>
+        </html>
+      ''';
+      videoTestBase64 = base64Encode(const Utf8Encoder().convert(videoTest));
+    });
+
+    testWidgets('Auto media playback', (WidgetTester tester) async {
+      Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      Completer<void> pageLoaded = Completer<void>();
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+            initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
+          ),
+        ),
+      );
+      WebViewController controller = await controllerCompleter.future;
+      await pageLoaded.future;
+
+      String isPaused = await controller.evaluateJavascript('isPaused();');
+      expect(isPaused, _webviewBool(false));
+
+      controllerCompleter = Completer<WebViewController>();
+      pageLoaded = Completer<void>();
+
+      // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+            initialMediaPlaybackPolicy:
+                AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
+          ),
+        ),
+      );
+
+      controller = await controllerCompleter.future;
+      await pageLoaded.future;
+
+      isPaused = await controller.evaluateJavascript('isPaused();');
+      expect(isPaused, _webviewBool(true));
+    });
+
+    testWidgets('Changes to initialMediaPlaybackPolicy are ignored',
+        (WidgetTester tester) async {
+      final Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      Completer<void> pageLoaded = Completer<void>();
+
+      final GlobalKey key = GlobalKey();
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: key,
+            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+            initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
+          ),
+        ),
+      );
+      final WebViewController controller = await controllerCompleter.future;
+      await pageLoaded.future;
+
+      String isPaused = await controller.evaluateJavascript('isPaused();');
+      expect(isPaused, _webviewBool(false));
+
+      pageLoaded = Completer<void>();
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: key,
+            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+            initialMediaPlaybackPolicy:
+                AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
+          ),
+        ),
+      );
+
+      await controller.reload();
+
+      await pageLoaded.future;
+
+      isPaused = await controller.evaluateJavascript('isPaused();');
+      expect(isPaused, _webviewBool(false));
+    });
+
+    testWidgets('Video plays inline when allowsInlineMediaPlayback is true',
+        (WidgetTester tester) async {
+      Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      Completer<void> pageLoaded = Completer<void>();
+      Completer<void> videoPlaying = Completer<void>();
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            javascriptChannels: <JavascriptChannel>{
+              JavascriptChannel(
+                name: 'VideoTestTime',
+                onMessageReceived: (JavascriptMessage message) {
+                  final double currentTime = double.parse(message.message);
+                  // Let it play for at least 1 second to make sure the related video's properties are set.
+                  if (currentTime > 1) {
+                    videoPlaying.complete(null);
+                  }
+                },
+              ),
+            },
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+            initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
+            allowsInlineMediaPlayback: true,
+          ),
+        ),
+      );
+      WebViewController controller = await controllerCompleter.future;
+      await pageLoaded.future;
+
+      // Pump once to trigger the video play.
+      await tester.pump();
+
+      // Makes sure we get the correct event that indicates the video is actually playing.
+      await videoPlaying.future;
+
+      String fullScreen =
+          await controller.evaluateJavascript('isFullScreen();');
+      expect(fullScreen, _webviewBool(false));
+    });
+  });
+
+  group('Audio playback policy', () {
+    late String audioTestBase64;
+    setUpAll(() async {
+      final ByteData audioData =
+          await rootBundle.load('assets/sample_audio.ogg');
+      final String base64AudioData =
+          base64Encode(Uint8List.view(audioData.buffer));
+      final String audioTest = '''
+        <!DOCTYPE html><html>
+        <head><title>Audio auto play</title>
+          <script type="text/javascript">
+            function play() {
+              var audio = document.getElementById("audio");
+              audio.play();
+            }
+            function isPaused() {
+              var audio = document.getElementById("audio");
+              return audio.paused;
+            }
+          </script>
+        </head>
+        <body onload="play();">
+        <audio controls id="audio">
+          <source src="data:audio/ogg;charset=utf-8;base64,$base64AudioData">
+        </audio>
+        </body>
+        </html>
+      ''';
+      audioTestBase64 = base64Encode(const Utf8Encoder().convert(audioTest));
+    });
+
+    testWidgets('Auto media playback', (WidgetTester tester) async {
+      Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      Completer<void> pageStarted = Completer<void>();
+      Completer<void> pageLoaded = Completer<void>();
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageStarted: (String url) {
+              pageStarted.complete(null);
+            },
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+            initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
+          ),
+        ),
+      );
+      WebViewController controller = await controllerCompleter.future;
+      await pageStarted.future;
+      await pageLoaded.future;
+
+      String isPaused = await controller.evaluateJavascript('isPaused();');
+      expect(isPaused, _webviewBool(false));
+
+      controllerCompleter = Completer<WebViewController>();
+      pageStarted = Completer<void>();
+      pageLoaded = Completer<void>();
+
+      // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageStarted: (String url) {
+              pageStarted.complete(null);
+            },
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+            initialMediaPlaybackPolicy:
+                AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
+          ),
+        ),
+      );
+
+      controller = await controllerCompleter.future;
+      await pageStarted.future;
+      await pageLoaded.future;
+
+      isPaused = await controller.evaluateJavascript('isPaused();');
+      expect(isPaused, _webviewBool(true));
+    });
+
+    testWidgets('Changes to initialMediaPlaybackPolocy are ignored',
+        (WidgetTester tester) async {
+      final Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      Completer<void> pageStarted = Completer<void>();
+      Completer<void> pageLoaded = Completer<void>();
+
+      final GlobalKey key = GlobalKey();
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: key,
+            initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageStarted: (String url) {
+              pageStarted.complete(null);
+            },
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+            initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
+          ),
+        ),
+      );
+      final WebViewController controller = await controllerCompleter.future;
+      await pageStarted.future;
+      await pageLoaded.future;
+
+      String isPaused = await controller.evaluateJavascript('isPaused();');
+      expect(isPaused, _webviewBool(false));
+
+      pageStarted = Completer<void>();
+      pageLoaded = Completer<void>();
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: key,
+            initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageStarted: (String url) {
+              pageStarted.complete(null);
+            },
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+            initialMediaPlaybackPolicy:
+                AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
+          ),
+        ),
+      );
+
+      await controller.reload();
+
+      await pageStarted.future;
+      await pageLoaded.future;
+
+      isPaused = await controller.evaluateJavascript('isPaused();');
+      expect(isPaused, _webviewBool(false));
+    });
+  });
+
+  testWidgets('getTitle', (WidgetTester tester) async {
+    final String getTitleTest = '''
+        <!DOCTYPE html><html>
+        <head><title>Some title</title>
+        </head>
+        <body>
+        </body>
+        </html>
+      ''';
+    final String getTitleTestBase64 =
+        base64Encode(const Utf8Encoder().convert(getTitleTest));
+    final Completer<void> pageStarted = Completer<void>();
+    final Completer<void> pageLoaded = Completer<void>();
+    final Completer<WebViewController> controllerCompleter =
+        Completer<WebViewController>();
+
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64',
+          onWebViewCreated: (WebViewController controller) {
+            controllerCompleter.complete(controller);
+          },
+          onPageStarted: (String url) {
+            pageStarted.complete(null);
+          },
+          onPageFinished: (String url) {
+            pageLoaded.complete(null);
+          },
+        ),
+      ),
+    );
+
+    final WebViewController controller = await controllerCompleter.future;
+    await pageStarted.future;
+    await pageLoaded.future;
+
+    final String? title = await controller.getTitle();
+    expect(title, 'Some title');
+  });
+
+  group('Programmatic Scroll', () {
+    // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757.
+    testWidgets('setAndGetScrollPosition', (WidgetTester tester) async {
+      final String scrollTestPage = '''
+        <!DOCTYPE html>
+        <html>
+          <head>
+            <style>
+              body {
+                height: 100%;
+                width: 100%;
+              }
+              #container{
+                width:5000px;
+                height:5000px;
+            }
+            </style>
+          </head>
+          <body>
+            <div id="container"/>
+          </body>
+        </html>
+      ''';
+
+      final String scrollTestPageBase64 =
+          base64Encode(const Utf8Encoder().convert(scrollTestPage));
+
+      final Completer<void> pageLoaded = Completer<void>();
+      final Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            initialUrl:
+                'data:text/html;charset=utf-8;base64,$scrollTestPageBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+          ),
+        ),
+      );
+
+      final WebViewController controller = await controllerCompleter.future;
+      await pageLoaded.future;
+
+      await tester.pumpAndSettle(Duration(seconds: 3));
+
+      int scrollPosX = await controller.getScrollX();
+      int scrollPosY = await controller.getScrollY();
+
+      // Check scrollTo()
+      const int X_SCROLL = 123;
+      const int Y_SCROLL = 321;
+      // Get the initial position; this ensures that scrollTo is actually
+      // changing something, but also gives the native view's scroll position
+      // time to settle.
+      expect(scrollPosX, isNot(X_SCROLL));
+      expect(scrollPosX, isNot(Y_SCROLL));
+
+      await controller.scrollTo(X_SCROLL, Y_SCROLL);
+      scrollPosX = await controller.getScrollX();
+      scrollPosY = await controller.getScrollY();
+      expect(scrollPosX, X_SCROLL);
+      expect(scrollPosY, Y_SCROLL);
+
+      // Check scrollBy() (on top of scrollTo())
+      await controller.scrollBy(X_SCROLL, Y_SCROLL);
+      scrollPosX = await controller.getScrollX();
+      scrollPosY = await controller.getScrollY();
+      expect(scrollPosX, X_SCROLL * 2);
+      expect(scrollPosY, Y_SCROLL * 2);
+    }, skip: _skipDueToIssue86757);
+  });
+
+  group('SurfaceAndroidWebView', () {
+    setUpAll(() {
+      WebView.platform = SurfaceAndroidWebView();
+    });
+
+    tearDownAll(() {
+      WebView.platform = AndroidWebView();
+    });
+
+    // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757.
+    testWidgets('setAndGetScrollPosition', (WidgetTester tester) async {
+      final String scrollTestPage = '''
+        <!DOCTYPE html>
+        <html>
+          <head>
+            <style>
+              body {
+                height: 100%;
+                width: 100%;
+              }
+              #container{
+                width:5000px;
+                height:5000px;
+            }
+            </style>
+          </head>
+          <body>
+            <div id="container"/>
+          </body>
+        </html>
+      ''';
+
+      final String scrollTestPageBase64 =
+          base64Encode(const Utf8Encoder().convert(scrollTestPage));
+
+      final Completer<void> pageLoaded = Completer<void>();
+      final Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            initialUrl:
+                'data:text/html;charset=utf-8;base64,$scrollTestPageBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+          ),
+        ),
+      );
+
+      final WebViewController controller = await controllerCompleter.future;
+      await pageLoaded.future;
+
+      await tester.pumpAndSettle(Duration(seconds: 3));
+
+      // Check scrollTo()
+      const int X_SCROLL = 123;
+      const int Y_SCROLL = 321;
+
+      await controller.scrollTo(X_SCROLL, Y_SCROLL);
+      int scrollPosX = await controller.getScrollX();
+      int scrollPosY = await controller.getScrollY();
+      expect(X_SCROLL, scrollPosX);
+      expect(Y_SCROLL, scrollPosY);
+
+      // Check scrollBy() (on top of scrollTo())
+      await controller.scrollBy(X_SCROLL, Y_SCROLL);
+      scrollPosX = await controller.getScrollX();
+      scrollPosY = await controller.getScrollY();
+      expect(X_SCROLL * 2, scrollPosX);
+      expect(Y_SCROLL * 2, scrollPosY);
+    }, skip: _skipDueToIssue86757);
+
+    // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757.
+    testWidgets('inputs are scrolled into view when focused',
+        (WidgetTester tester) async {
+      final String scrollTestPage = '''
+        <!DOCTYPE html>
+        <html>
+          <head>
+            <style>
+              input {
+                margin: 10000px 0;
+              }
+              #viewport {
+                position: fixed;
+                top:0;
+                bottom:0;
+                left:0;
+                right:0;
+              }
+            </style>
+          </head>
+          <body>
+            <div id="viewport"></div>
+            <input type="text" id="inputEl">
+          </body>
+        </html>
+      ''';
+
+      final String scrollTestPageBase64 =
+          base64Encode(const Utf8Encoder().convert(scrollTestPage));
+
+      final Completer<void> pageLoaded = Completer<void>();
+      final Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+
+      await tester.runAsync(() async {
+        await tester.pumpWidget(
+          Directionality(
+            textDirection: TextDirection.ltr,
+            child: SizedBox(
+              width: 200,
+              height: 200,
+              child: WebView(
+                initialUrl:
+                    'data:text/html;charset=utf-8;base64,$scrollTestPageBase64',
+                onWebViewCreated: (WebViewController controller) {
+                  controllerCompleter.complete(controller);
+                },
+                onPageFinished: (String url) {
+                  pageLoaded.complete(null);
+                },
+                javascriptMode: JavascriptMode.unrestricted,
+              ),
+            ),
+          ),
+        );
+        await Future.delayed(Duration(milliseconds: 20));
+        await tester.pump();
+      });
+
+      final WebViewController controller = await controllerCompleter.future;
+      await pageLoaded.future;
+      final String viewportRectJSON = await _evaluateJavascript(
+          controller, 'JSON.stringify(viewport.getBoundingClientRect())');
+      final Map<String, dynamic> viewportRectRelativeToViewport =
+          jsonDecode(viewportRectJSON);
+
+      // Check that the input is originally outside of the viewport.
+
+      final String initialInputClientRectJSON = await _evaluateJavascript(
+          controller, 'JSON.stringify(inputEl.getBoundingClientRect())');
+      final Map<String, dynamic> initialInputClientRectRelativeToViewport =
+          jsonDecode(initialInputClientRectJSON);
+
+      expect(
+          initialInputClientRectRelativeToViewport['bottom'] <=
+              viewportRectRelativeToViewport['bottom'],
+          isFalse);
+
+      await controller.evaluateJavascript('inputEl.focus()');
+
+      // Check that focusing the input brought it into view.
+
+      final String lastInputClientRectJSON = await _evaluateJavascript(
+          controller, 'JSON.stringify(inputEl.getBoundingClientRect())');
+      final Map<String, dynamic> lastInputClientRectRelativeToViewport =
+          jsonDecode(lastInputClientRectJSON);
+
+      expect(
+          lastInputClientRectRelativeToViewport['top'] >=
+              viewportRectRelativeToViewport['top'],
+          isTrue);
+      expect(
+          lastInputClientRectRelativeToViewport['bottom'] <=
+              viewportRectRelativeToViewport['bottom'],
+          isTrue);
+
+      expect(
+          lastInputClientRectRelativeToViewport['left'] >=
+              viewportRectRelativeToViewport['left'],
+          isTrue);
+      expect(
+          lastInputClientRectRelativeToViewport['right'] <=
+              viewportRectRelativeToViewport['right'],
+          isTrue);
+    }, skip: _skipDueToIssue86757);
+  });
+
+  group('NavigationDelegate', () {
+    final String blankPage = "<!DOCTYPE html><head></head><body></body></html>";
+    final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' +
+        base64Encode(const Utf8Encoder().convert(blankPage));
+
+    testWidgets('can allow requests', (WidgetTester tester) async {
+      final Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      final StreamController<String> pageLoads =
+          StreamController<String>.broadcast();
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: blankPageEncoded,
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            navigationDelegate: (NavigationRequest request) {
+              return (request.url.contains('youtube.com'))
+                  ? NavigationDecision.prevent
+                  : NavigationDecision.navigate;
+            },
+            onPageFinished: (String url) => pageLoads.add(url),
+          ),
+        ),
+      );
+
+      await pageLoads.stream.first; // Wait for initial page load.
+      final WebViewController controller = await controllerCompleter.future;
+      await controller
+          .evaluateJavascript('location.href = "https://www.google.com/"');
+
+      await pageLoads.stream.first; // Wait for the next page load.
+      final String? currentUrl = await controller.currentUrl();
+      expect(currentUrl, 'https://www.google.com/');
+    });
+
+    testWidgets('onWebResourceError', (WidgetTester tester) async {
+      final Completer<WebResourceError> errorCompleter =
+          Completer<WebResourceError>();
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: 'https://www.notawebsite..com',
+            onWebResourceError: (WebResourceError error) {
+              errorCompleter.complete(error);
+            },
+          ),
+        ),
+      );
+
+      final WebResourceError error = await errorCompleter.future;
+      expect(error, isNotNull);
+
+      expect(error.errorType, isNotNull);
+      expect(
+          error.failingUrl?.startsWith('https://www.notawebsite..com'), isTrue);
+    });
+
+    testWidgets('onWebResourceError is not called with valid url',
+        (WidgetTester tester) async {
+      final Completer<WebResourceError> errorCompleter =
+          Completer<WebResourceError>();
+      final Completer<void> pageFinishCompleter = Completer<void>();
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl:
+                'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+',
+            onWebResourceError: (WebResourceError error) {
+              errorCompleter.complete(error);
+            },
+            onPageFinished: (_) => pageFinishCompleter.complete(),
+          ),
+        ),
+      );
+
+      expect(errorCompleter.future, doesNotComplete);
+      await pageFinishCompleter.future;
+    });
+
+    testWidgets(
+      'onWebResourceError only called for main frame',
+      (WidgetTester tester) async {
+        final String iframeTest = '''
+        <!DOCTYPE html>
+        <html>
+        <head>
+          <title>WebResourceError test</title>
+        </head>
+        <body>
+          <iframe src="https://notawebsite..com"></iframe>
+        </body>
+        </html>
+       ''';
+        final String iframeTestBase64 =
+            base64Encode(const Utf8Encoder().convert(iframeTest));
+
+        final Completer<WebResourceError> errorCompleter =
+            Completer<WebResourceError>();
+        final Completer<void> pageFinishCompleter = Completer<void>();
+
+        await tester.pumpWidget(
+          Directionality(
+            textDirection: TextDirection.ltr,
+            child: WebView(
+              key: GlobalKey(),
+              initialUrl:
+                  'data:text/html;charset=utf-8;base64,$iframeTestBase64',
+              onWebResourceError: (WebResourceError error) {
+                errorCompleter.complete(error);
+              },
+              onPageFinished: (_) => pageFinishCompleter.complete(),
+            ),
+          ),
+        );
+
+        expect(errorCompleter.future, doesNotComplete);
+        await pageFinishCompleter.future;
+      },
+    );
+
+    testWidgets('can block requests', (WidgetTester tester) async {
+      final Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      final StreamController<String> pageLoads =
+          StreamController<String>.broadcast();
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: blankPageEncoded,
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            navigationDelegate: (NavigationRequest request) {
+              return (request.url.contains('youtube.com'))
+                  ? NavigationDecision.prevent
+                  : NavigationDecision.navigate;
+            },
+            onPageFinished: (String url) => pageLoads.add(url),
+          ),
+        ),
+      );
+
+      await pageLoads.stream.first; // Wait for initial page load.
+      final WebViewController controller = await controllerCompleter.future;
+      await controller
+          .evaluateJavascript('location.href = "https://www.youtube.com/"');
+
+      // There should never be any second page load, since our new URL is
+      // blocked. Still wait for a potential page change for some time in order
+      // to give the test a chance to fail.
+      await pageLoads.stream.first
+          .timeout(const Duration(milliseconds: 500), onTimeout: () => '');
+      final String? currentUrl = await controller.currentUrl();
+      expect(currentUrl, isNot(contains('youtube.com')));
+    });
+
+    testWidgets('supports asynchronous decisions', (WidgetTester tester) async {
+      final Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      final StreamController<String> pageLoads =
+          StreamController<String>.broadcast();
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: blankPageEncoded,
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            navigationDelegate: (NavigationRequest request) async {
+              NavigationDecision decision = NavigationDecision.prevent;
+              decision = await Future<NavigationDecision>.delayed(
+                  const Duration(milliseconds: 10),
+                  () => NavigationDecision.navigate);
+              return decision;
+            },
+            onPageFinished: (String url) => pageLoads.add(url),
+          ),
+        ),
+      );
+
+      await pageLoads.stream.first; // Wait for initial page load.
+      final WebViewController controller = await controllerCompleter.future;
+      await controller
+          .evaluateJavascript('location.href = "https://www.google.com"');
+
+      await pageLoads.stream.first; // Wait for second page to load.
+      final String? currentUrl = await controller.currentUrl();
+      expect(currentUrl, 'https://www.google.com/');
+    });
+  });
+
+  testWidgets('launches with gestureNavigationEnabled on iOS',
+      (WidgetTester tester) async {
+    final Completer<WebViewController> controllerCompleter =
+        Completer<WebViewController>();
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: SizedBox(
+          width: 400,
+          height: 300,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: 'https://flutter.dev/',
+            gestureNavigationEnabled: true,
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+          ),
+        ),
+      ),
+    );
+    final WebViewController controller = await controllerCompleter.future;
+    final String? currentUrl = await controller.currentUrl();
+    expect(currentUrl, 'https://flutter.dev/');
+  });
+
+  testWidgets('target _blank opens in same window',
+      (WidgetTester tester) async {
+    final Completer<WebViewController> controllerCompleter =
+        Completer<WebViewController>();
+    final Completer<void> pageLoaded = Completer<void>();
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          key: GlobalKey(),
+          onWebViewCreated: (WebViewController controller) {
+            controllerCompleter.complete(controller);
+          },
+          javascriptMode: JavascriptMode.unrestricted,
+          onPageFinished: (String url) {
+            pageLoaded.complete(null);
+          },
+        ),
+      ),
+    );
+    final WebViewController controller = await controllerCompleter.future;
+    await controller
+        .evaluateJavascript('window.open("https://flutter.dev/", "_blank")');
+    await pageLoaded.future;
+    final String? currentUrl = await controller.currentUrl();
+    expect(currentUrl, 'https://flutter.dev/');
+  },
+      // Flaky on Android: https://github.com/flutter/flutter/issues/86757
+      skip: _skipDueToIssue86757);
+
+  // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757.
+  testWidgets(
+    'can open new window and go back',
+    (WidgetTester tester) async {
+      final Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      Completer<void> pageLoaded = Completer<void>();
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageFinished: (String url) {
+              pageLoaded.complete();
+            },
+            initialUrl: 'https://flutter.dev',
+          ),
+        ),
+      );
+      final WebViewController controller = await controllerCompleter.future;
+      expect(controller.currentUrl(), completion('https://flutter.dev/'));
+      await pageLoaded.future;
+      pageLoaded = Completer<void>();
+
+      await controller
+          .evaluateJavascript('window.open("https://www.google.com/")');
+      await pageLoaded.future;
+      pageLoaded = Completer<void>();
+      expect(controller.currentUrl(), completion('https://www.google.com/'));
+
+      expect(controller.canGoBack(), completion(true));
+      await controller.goBack();
+      await pageLoaded.future;
+      expect(controller.currentUrl(), completion('https://flutter.dev/'));
+    },
+    skip: _skipDueToIssue86757,
+  );
+
+  testWidgets(
+    'javascript does not run in parent window',
+    (WidgetTester tester) async {
+      final String iframe = '''
+        <!DOCTYPE html>
+        <script>
+          window.onload = () => {
+            window.open(`javascript:
+              var elem = document.createElement("p");
+              elem.innerHTML = "<b>Executed JS in parent origin: " + window.location.origin + "</b>";
+              document.body.append(elem);
+            `);
+          };
+        </script>
+      ''';
+      final String iframeTestBase64 =
+          base64Encode(const Utf8Encoder().convert(iframe));
+
+      final String openWindowTest = '''
+        <!DOCTYPE html>
+        <html>
+        <head>
+          <title>XSS test</title>
+        </head>
+        <body>
+          <iframe
+            onload="window.iframeLoaded = true;"
+            src="data:text/html;charset=utf-8;base64,$iframeTestBase64"></iframe>
+        </body>
+        </html>
+      ''';
+      final String openWindowTestBase64 =
+          base64Encode(const Utf8Encoder().convert(openWindowTest));
+      final Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      final Completer<void> pageLoadCompleter = Completer<void>();
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            initialUrl:
+                'data:text/html;charset=utf-8;base64,$openWindowTestBase64',
+            onPageFinished: (String url) {
+              pageLoadCompleter.complete();
+            },
+          ),
+        ),
+      );
+
+      final WebViewController controller = await controllerCompleter.future;
+      await pageLoadCompleter.future;
+
+      expect(controller.evaluateJavascript('iframeLoaded'), completion('true'));
+      expect(
+        controller.evaluateJavascript(
+            'document.querySelector("p") && document.querySelector("p").textContent'),
+        completion('null'),
+      );
+    },
+  );
+}
+
+// JavaScript booleans evaluate to different string values on Android and iOS.
+// This utility method returns the string boolean value of the current platform.
+String _webviewBool(bool value) {
+  if (defaultTargetPlatform == TargetPlatform.iOS) {
+    return value ? '1' : '0';
+  }
+  return value ? 'true' : 'false';
+}
+
+/// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests.
+Future<String> _getUserAgent(WebViewController controller) async {
+  return _evaluateJavascript(controller, 'navigator.userAgent;');
+}
+
+Future<String> _evaluateJavascript(
+    WebViewController controller, String js) async {
+  return jsonDecode(await controller.evaluateJavascript(js));
+}
diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart
new file mode 100644
index 0000000..6176ce2
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart
@@ -0,0 +1,344 @@
+// 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.
+
+// ignore_for_file: public_member_api_docs
+
+import 'dart:async';
+import 'dart:convert';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:webview_flutter_android/webview_surface_android.dart';
+import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
+
+import 'web_view.dart';
+
+void main() {
+  // Configure the [WebView] to use the [SurfaceAndroidWebView]
+  // implementation instead of the default [AndroidWebView].
+  WebView.platform = SurfaceAndroidWebView();
+
+  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 StatefulWidget {
+  const _WebViewExample({Key? key}) : super(key: key);
+
+  @override
+  _WebViewExampleState createState() => _WebViewExampleState();
+}
+
+class _WebViewExampleState extends State<_WebViewExample> {
+  final Completer<WebViewController> _controller =
+      Completer<WebViewController>();
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('Flutter WebView example'),
+        // This drop down menu demonstrates that Flutter widgets can be shown over the web view.
+        actions: <Widget>[
+          _NavigationControls(_controller.future),
+          _SampleMenu(_controller.future),
+        ],
+      ),
+      // We're using a Builder here so we have a context that is below the Scaffold
+      // to allow calling Scaffold.of(context) so we can show a snackbar.
+      body: Builder(builder: (context) {
+        return WebView(
+          initialUrl: 'https://flutter.dev',
+          onWebViewCreated: (WebViewController controller) {
+            _controller.complete(controller);
+          },
+          javascriptChannels: _createJavascriptChannels(context),
+          javascriptMode: JavascriptMode.unrestricted,
+          userAgent: 'Custom_User_Agent',
+        );
+      }),
+      floatingActionButton: favoriteButton(),
+    );
+  }
+
+  Widget favoriteButton() {
+    return FutureBuilder<WebViewController>(
+        future: _controller.future,
+        builder: (BuildContext context,
+            AsyncSnapshot<WebViewController> controller) {
+          if (controller.hasData) {
+            return FloatingActionButton(
+              onPressed: () async {
+                final String url = (await controller.data!.currentUrl())!;
+                // ignore: deprecated_member_use
+                Scaffold.of(context).showSnackBar(
+                  SnackBar(content: Text('Favorited $url')),
+                );
+              },
+              child: const Icon(Icons.favorite),
+            );
+          }
+          return Container();
+        });
+  }
+}
+
+Set<JavascriptChannel> _createJavascriptChannels(BuildContext context) {
+  return {
+    JavascriptChannel(
+        name: 'Snackbar',
+        onMessageReceived: (JavascriptMessage message) {
+          ScaffoldMessenger.of(context)
+              .showSnackBar(SnackBar(content: Text(message.message)));
+        }),
+  };
+}
+
+enum _MenuOptions {
+  showUserAgent,
+  listCookies,
+  clearCookies,
+  addToCache,
+  listCache,
+  clearCache,
+  navigationDelegate,
+}
+
+class _SampleMenu extends StatelessWidget {
+  _SampleMenu(this.controller);
+
+  final Future<WebViewController> controller;
+
+  @override
+  Widget build(BuildContext context) {
+    return FutureBuilder<WebViewController>(
+      future: controller,
+      builder:
+          (BuildContext context, AsyncSnapshot<WebViewController> controller) {
+        return PopupMenuButton<_MenuOptions>(
+          onSelected: (_MenuOptions value) {
+            switch (value) {
+              case _MenuOptions.showUserAgent:
+                _onShowUserAgent(controller.data!, context);
+                break;
+              case _MenuOptions.listCookies:
+                _onListCookies(controller.data!, context);
+                break;
+              case _MenuOptions.clearCookies:
+                _onClearCookies(controller.data!, context);
+                break;
+              case _MenuOptions.addToCache:
+                _onAddToCache(controller.data!, context);
+                break;
+              case _MenuOptions.listCache:
+                _onListCache(controller.data!, context);
+                break;
+              case _MenuOptions.clearCache:
+                _onClearCache(controller.data!, context);
+                break;
+              case _MenuOptions.navigationDelegate:
+                _onNavigationDelegateExample(controller.data!, context);
+                break;
+            }
+          },
+          itemBuilder: (BuildContext context) => <PopupMenuItem<_MenuOptions>>[
+            PopupMenuItem<_MenuOptions>(
+              value: _MenuOptions.showUserAgent,
+              child: const Text('Show user agent'),
+              enabled: controller.hasData,
+            ),
+            const PopupMenuItem<_MenuOptions>(
+              value: _MenuOptions.listCookies,
+              child: Text('List cookies'),
+            ),
+            const PopupMenuItem<_MenuOptions>(
+              value: _MenuOptions.clearCookies,
+              child: Text('Clear cookies'),
+            ),
+            const PopupMenuItem<_MenuOptions>(
+              value: _MenuOptions.addToCache,
+              child: Text('Add to cache'),
+            ),
+            const PopupMenuItem<_MenuOptions>(
+              value: _MenuOptions.listCache,
+              child: Text('List cache'),
+            ),
+            const PopupMenuItem<_MenuOptions>(
+              value: _MenuOptions.clearCache,
+              child: Text('Clear cache'),
+            ),
+            const PopupMenuItem<_MenuOptions>(
+              value: _MenuOptions.navigationDelegate,
+              child: Text('Navigation Delegate example'),
+            ),
+          ],
+        );
+      },
+    );
+  }
+
+  void _onShowUserAgent(
+      WebViewController controller, BuildContext context) async {
+    // Send a message with the user agent string to the Snackbar JavaScript channel we registered
+    // with the WebView.
+    await controller.evaluateJavascript(
+        'Snackbar.postMessage("User Agent: " + navigator.userAgent);');
+  }
+
+  void _onListCookies(
+      WebViewController controller, BuildContext context) async {
+    final String cookies =
+        await controller.evaluateJavascript('document.cookie');
+    // ignore: deprecated_member_use
+    Scaffold.of(context).showSnackBar(SnackBar(
+      content: Column(
+        mainAxisAlignment: MainAxisAlignment.end,
+        mainAxisSize: MainAxisSize.min,
+        children: <Widget>[
+          const Text('Cookies:'),
+          _getCookieList(cookies),
+        ],
+      ),
+    ));
+  }
+
+  void _onAddToCache(WebViewController controller, BuildContext context) async {
+    await controller.evaluateJavascript(
+        'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";');
+    // ignore: deprecated_member_use
+    Scaffold.of(context).showSnackBar(const SnackBar(
+      content: Text('Added a test entry to cache.'),
+    ));
+  }
+
+  void _onListCache(WebViewController controller, BuildContext context) async {
+    await controller.evaluateJavascript('caches.keys()'
+        '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))'
+        '.then((caches) => Snackbar.postMessage(caches))');
+  }
+
+  void _onClearCache(WebViewController controller, BuildContext context) async {
+    await controller.clearCache();
+    // ignore: deprecated_member_use
+    Scaffold.of(context).showSnackBar(const SnackBar(
+      content: Text("Cache cleared."),
+    ));
+  }
+
+  void _onClearCookies(
+      WebViewController controller, BuildContext context) async {
+    final bool hadCookies = await WebView.platform.clearCookies();
+    String message = 'There were cookies. Now, they are gone!';
+    if (!hadCookies) {
+      message = 'There are no cookies.';
+    }
+    // ignore: deprecated_member_use
+    Scaffold.of(context).showSnackBar(SnackBar(
+      content: Text(message),
+    ));
+  }
+
+  void _onNavigationDelegateExample(
+      WebViewController controller, BuildContext context) async {
+    final String contentBase64 =
+        base64Encode(const Utf8Encoder().convert(kNavigationExamplePage));
+    await controller.loadUrl('data:text/html;base64,$contentBase64');
+  }
+
+  Widget _getCookieList(String cookies) {
+    if (cookies == null || cookies == '""') {
+      return Container();
+    }
+    final List<String> cookieList = cookies.split(';');
+    final Iterable<Text> cookieWidgets =
+        cookieList.map((String cookie) => Text(cookie));
+    return Column(
+      mainAxisAlignment: MainAxisAlignment.end,
+      mainAxisSize: MainAxisSize.min,
+      children: cookieWidgets.toList(),
+    );
+  }
+}
+
+class _NavigationControls extends StatelessWidget {
+  const _NavigationControls(this._webViewControllerFuture)
+      : assert(_webViewControllerFuture != null);
+
+  final Future<WebViewController> _webViewControllerFuture;
+
+  @override
+  Widget build(BuildContext context) {
+    return FutureBuilder<WebViewController>(
+      future: _webViewControllerFuture,
+      builder:
+          (BuildContext context, AsyncSnapshot<WebViewController> snapshot) {
+        final bool webViewReady =
+            snapshot.connectionState == ConnectionState.done;
+        final WebViewController? controller = snapshot.data;
+        if (controller == null) return Container();
+        return Row(
+          children: <Widget>[
+            IconButton(
+              icon: const Icon(Icons.arrow_back_ios),
+              onPressed: !webViewReady
+                  ? null
+                  : () async {
+                      if (await controller.canGoBack()) {
+                        await controller.goBack();
+                      } else {
+                        // ignore: deprecated_member_use
+                        Scaffold.of(context).showSnackBar(
+                          const SnackBar(content: Text("No back history item")),
+                        );
+                        return;
+                      }
+                    },
+            ),
+            IconButton(
+              icon: const Icon(Icons.arrow_forward_ios),
+              onPressed: !webViewReady
+                  ? null
+                  : () async {
+                      if (await controller.canGoForward()) {
+                        await controller.goForward();
+                      } else {
+                        // ignore: deprecated_member_use
+                        Scaffold.of(context).showSnackBar(
+                          const SnackBar(
+                              content: Text("No forward history item")),
+                        );
+                        return;
+                      }
+                    },
+            ),
+            IconButton(
+              icon: const Icon(Icons.replay),
+              onPressed: !webViewReady
+                  ? null
+                  : () {
+                      controller.reload();
+                    },
+            ),
+          ],
+        );
+      },
+    );
+  }
+}
+
+/// Callback type for handling messages sent from Javascript running in a web view.
+typedef void JavascriptMessageHandler(JavascriptMessage message);
diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/navigation_decision.dart b/packages/webview_flutter/webview_flutter_android/example/lib/navigation_decision.dart
new file mode 100644
index 0000000..d8178ac
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/lib/navigation_decision.dart
@@ -0,0 +1,12 @@
+// 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.
+
+/// 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,
+}
diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/navigation_request.dart b/packages/webview_flutter/webview_flutter_android/example/lib/navigation_request.dart
new file mode 100644
index 0000000..c1ff8dc
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/lib/navigation_request.dart
@@ -0,0 +1,19 @@
+// 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.
+
+/// Information about a navigation action that is about to be executed.
+class NavigationRequest {
+  NavigationRequest._({required this.url, required 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)';
+  }
+}
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
new file mode 100644
index 0000000..33773f9
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart
@@ -0,0 +1,617 @@
+// 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 'dart:async';
+
+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_platform_interface/webview_flutter_platform_interface.dart';
+
+import 'navigation_decision.dart';
+import 'navigation_request.dart';
+
+/// Optional callback invoked when a web view is first created. [controller] is
+/// the [WebViewController] for the created web view.
+typedef void WebViewCreatedCallback(WebViewController controller);
+
+/// 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 FutureOr<NavigationDecision> NavigationDelegate(
+    NavigationRequest navigation);
+
+/// Signature for when a [WebView] has started loading a page.
+typedef void PageStartedCallback(String url);
+
+/// Signature for when a [WebView] has finished loading a page.
+typedef void PageFinishedCallback(String url);
+
+/// Signature for when a [WebView] is loading a page.
+typedef void PageLoadingCallback(int progress);
+
+/// Signature for when a [WebView] has failed to load a resource.
+typedef void WebResourceErrorCallback(WebResourceError error);
+
+/// A web view widget for showing html content.
+///
+/// The [WebView] widget wraps around the [AndroidWebView] or
+/// [SurfaceAndroidWebView] classes and acts like a facade which makes it easier
+/// to inject a [AndroidWebView] or [SurfaceAndroidWebView] control into the
+/// widget tree.
+///
+/// The [WebView] widget is controlled using the [WebViewController] which is
+/// provided through the `onWebViewCreated` callback.
+///
+/// In this example project it's main purpose is to facilitate integration
+/// testing of the `webview_flutter_android` package.
+class WebView extends StatefulWidget {
+  /// Creates a new web view.
+  ///
+  /// The web view can be controlled using a `WebViewController` that is passed to the
+  /// `onWebViewCreated` callback once the web view is created.
+  ///
+  /// The `javascriptMode` and `autoMediaPlaybackPolicy` parameters must not be null.
+  const WebView({
+    Key? key,
+    this.onWebViewCreated,
+    this.initialUrl,
+    this.javascriptMode = JavascriptMode.disabled,
+    this.javascriptChannels,
+    this.navigationDelegate,
+    this.gestureRecognizers,
+    this.onPageStarted,
+    this.onPageFinished,
+    this.onProgress,
+    this.onWebResourceError,
+    this.debuggingEnabled = false,
+    this.gestureNavigationEnabled = false,
+    this.userAgent,
+    this.initialMediaPlaybackPolicy =
+        AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
+    this.allowsInlineMediaPlayback = false,
+  })  : assert(javascriptMode != null),
+        assert(initialMediaPlaybackPolicy != null),
+        assert(allowsInlineMediaPlayback != null),
+        super(key: key);
+
+  static WebViewPlatform _platform = AndroidWebView();
+
+  /// The WebView platform that's used by this WebView.
+  ///
+  /// The default value is [AndroidWebView].
+  static WebViewPlatform get platform => _platform;
+
+  /// Sets a custom [WebViewPlatform].
+  ///
+  /// This property can be set to use a custom platform implementation for WebViews.
+  ///
+  /// Setting `platform` doesn't affect [WebView]s that were already created.
+  ///
+  /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS.
+  static set platform(WebViewPlatform platform) {
+    _platform = platform;
+  }
+
+  /// If not null invoked once the web view is created.
+  final WebViewCreatedCallback? onWebViewCreated;
+
+  /// Which gestures should be consumed by the web view.
+  ///
+  /// It is possible for other gesture recognizers to be competing with the web view on pointer
+  /// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle
+  /// vertical drags. The web view will claim gestures that are recognized by any of the
+  /// recognizers on this list.
+  ///
+  /// When this set is empty or null, the web view will only handle pointer events for gestures that
+  /// were not claimed by any other gesture recognizer.
+  final Set<Factory<OneSequenceGestureRecognizer>>? gestureRecognizers;
+
+  /// The initial URL to load.
+  final String? initialUrl;
+
+  /// Whether Javascript execution is enabled.
+  final JavascriptMode javascriptMode;
+
+  /// The set of [JavascriptChannel]s available to JavaScript code running in the web view.
+  ///
+  /// For each [JavascriptChannel] in the set, a channel object is made available for the
+  /// JavaScript code in a window property named [JavascriptChannel.name].
+  /// The JavaScript code can then call `postMessage` on that object to send a message that will be
+  /// passed to [JavascriptChannel.onMessageReceived].
+  ///
+  /// For example for the following JavascriptChannel:
+  ///
+  /// ```dart
+  /// JavascriptChannel(name: 'Print', onMessageReceived: (JavascriptMessage message) { print(message.message); });
+  /// ```
+  ///
+  /// JavaScript code can call:
+  ///
+  /// ```javascript
+  /// Print.postMessage('Hello');
+  /// ```
+  ///
+  /// To asynchronously invoke the message handler which will print the message to standard output.
+  ///
+  /// Adding a new JavaScript channel only takes affect after the next page is loaded.
+  ///
+  /// Set values must not be null. A [JavascriptChannel.name] cannot be the same for multiple
+  /// channels in the list.
+  ///
+  /// 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;
+
+  /// Controls whether inline playback of HTML5 videos is allowed on iOS.
+  ///
+  /// This field is ignored on Android because Android allows it by default.
+  ///
+  /// By default `allowsInlineMediaPlayback` is false.
+  final bool allowsInlineMediaPlayback;
+
+  /// Invoked when a page starts loading.
+  final PageStartedCallback? onPageStarted;
+
+  /// Invoked when a page has finished loading.
+  ///
+  /// This is invoked only for the main frame.
+  ///
+  /// When [onPageFinished] is invoked on Android, the page being rendered may
+  /// not be updated yet.
+  ///
+  /// When invoked on iOS or Android, any Javascript code that is embedded
+  /// directly in the HTML has been loaded and code injected with
+  /// [WebViewController.evaluateJavascript] can assume this.
+  final PageFinishedCallback? onPageFinished;
+
+  /// Invoked when a page is loading.
+  final PageLoadingCallback? onProgress;
+
+  /// Invoked when a web resource has failed to load.
+  ///
+  /// This callback is only called for the main page.
+  final WebResourceErrorCallback? onWebResourceError;
+
+  /// 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/).
+  ///
+  /// WebView debugging is enabled by default in dev builds on iOS.
+  ///
+  /// To debug WebViews on iOS:
+  /// - Enable developer options (Open Safari, go to Preferences -> Advanced and make sure "Show Develop Menu in Menubar" is on.)
+  /// - From the Menu-bar (of Safari) select Develop -> iPhone Simulator -> <your webview page>
+  ///
+  /// By default `debuggingEnabled` is false.
+  final bool debuggingEnabled;
+
+  /// A Boolean value indicating whether horizontal swipe gestures will trigger back-forward list navigations.
+  ///
+  /// This only works on iOS.
+  ///
+  /// By default `gestureNavigationEnabled` is false.
+  final bool gestureNavigationEnabled;
+
+  /// The value used for the HTTP User-Agent: request header.
+  ///
+  /// When null the platform's webview default is used for the User-Agent header.
+  ///
+  /// When the [WebView] is rebuilt with a different `userAgent`, the page reloads and the request uses the new User Agent.
+  ///
+  /// When [WebViewController.goBack] is called after changing `userAgent` the previous `userAgent` value is used until the page is reloaded.
+  ///
+  /// This field is ignored on iOS versions prior to 9 as the platform does not support a custom
+  /// user agent.
+  ///
+  /// By default `userAgent` is null.
+  final String? userAgent;
+
+  /// Which restrictions apply on automatic media playback.
+  ///
+  /// This initial value is applied to the platform's webview upon creation. Any following
+  /// changes to this parameter are ignored (as long as the state of the [WebView] is preserved).
+  ///
+  /// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all_media_types].
+  final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy;
+
+  @override
+  _WebViewState createState() => _WebViewState();
+}
+
+class _WebViewState extends State<WebView> {
+  final Completer<WebViewController> _controller =
+      Completer<WebViewController>();
+  late final JavascriptChannelRegistry _javascriptChannelRegistry;
+  late final _PlatformCallbacksHandler _platformCallbacksHandler;
+
+  @override
+  void initState() {
+    super.initState();
+    _platformCallbacksHandler = _PlatformCallbacksHandler(widget);
+    _javascriptChannelRegistry =
+        JavascriptChannelRegistry(widget.javascriptChannels);
+  }
+
+  @override
+  void didUpdateWidget(WebView oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    _controller.future.then((WebViewController controller) {
+      controller.updateWidget(widget);
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return WebView.platform.build(
+      context: context,
+      onWebViewPlatformCreated:
+          (WebViewPlatformController? webViewPlatformController) {
+        WebViewController controller = WebViewController(
+          widget,
+          webViewPlatformController!,
+          _javascriptChannelRegistry,
+        );
+        _controller.complete(controller);
+
+        if (widget.onWebViewCreated != null) {
+          widget.onWebViewCreated!(controller);
+        }
+      },
+      webViewPlatformCallbacksHandler: _platformCallbacksHandler,
+      creationParams: CreationParams(
+        initialUrl: widget.initialUrl,
+        webSettings: _webSettingsFromWidget(widget),
+        javascriptChannelNames:
+            _javascriptChannelRegistry.channels.keys.toSet(),
+        autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy,
+        userAgent: widget.userAgent,
+      ),
+      javascriptChannelRegistry: _javascriptChannelRegistry,
+    );
+  }
+}
+
+class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler {
+  _PlatformCallbacksHandler(this._webView);
+
+  final WebView _webView;
+
+  @override
+  FutureOr<bool> onNavigationRequest({
+    required String url,
+    required bool isForMainFrame,
+  }) async {
+    if (url.startsWith('https://www.youtube.com/')) {
+      print('blocking navigation to $url');
+      return false;
+    }
+    print('allowing navigation to $url');
+    return true;
+  }
+
+  @override
+  void onPageStarted(String url) {
+    if (_webView.onPageStarted != null) {
+      _webView.onPageStarted!(url);
+    }
+  }
+
+  @override
+  void onPageFinished(String url) {
+    if (_webView.onPageFinished != null) {
+      _webView.onPageFinished!(url);
+    }
+  }
+
+  @override
+  void onProgress(int progress) {
+    if (_webView.onProgress != null) {
+      _webView.onProgress!(progress);
+    }
+  }
+
+  void onWebResourceError(WebResourceError error) {
+    if (_webView.onWebResourceError != null) {
+      _webView.onWebResourceError!(error);
+    }
+  }
+}
+
+/// Controls a [WebView].
+///
+/// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated]
+/// callback for a [WebView] widget.
+class WebViewController {
+  /// Creates a [WebViewController] which can be used to control the provided
+  /// [WebView] widget.
+  WebViewController(
+    this._widget,
+    this._webViewPlatformController,
+    this._javascriptChannelRegistry,
+  ) : assert(_webViewPlatformController != null) {
+    _settings = _webSettingsFromWidget(_widget);
+  }
+
+  final JavascriptChannelRegistry _javascriptChannelRegistry;
+
+  final WebViewPlatformController _webViewPlatformController;
+
+  late WebSettings _settings;
+
+  WebView _widget;
+
+  /// Loads the specified URL.
+  ///
+  /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will
+  /// be added as key value pairs of HTTP headers for the request.
+  ///
+  /// `url` must not be null.
+  ///
+  /// Throws an ArgumentError if `url` is not a valid URL string.
+  Future<void> loadUrl(
+    String url, {
+    Map<String, String>? headers,
+  }) async {
+    assert(url != null);
+    _validateUrlString(url);
+    return _webViewPlatformController.loadUrl(url, headers);
+  }
+
+  /// Accessor to the current URL that the WebView is displaying.
+  ///
+  /// If [WebView.initialUrl] was never specified, returns `null`.
+  /// Note that this operation is asynchronous, and it is possible that the
+  /// current URL changes again by the time this function returns (in other
+  /// words, by the time this future completes, the WebView may be displaying a
+  /// different URL).
+  Future<String?> currentUrl() {
+    return _webViewPlatformController.currentUrl();
+  }
+
+  /// Checks whether there's a back history item.
+  ///
+  /// Note that this operation is asynchronous, and it is possible that the "canGoBack" state has
+  /// changed by the time the future completed.
+  Future<bool> canGoBack() {
+    return _webViewPlatformController.canGoBack();
+  }
+
+  /// Checks whether there's a forward history item.
+  ///
+  /// Note that this operation is asynchronous, and it is possible that the "canGoForward" state has
+  /// changed by the time the future completed.
+  Future<bool> canGoForward() {
+    return _webViewPlatformController.canGoForward();
+  }
+
+  /// Goes back in the history of this WebView.
+  ///
+  /// If there is no back history item this is a no-op.
+  Future<void> goBack() {
+    return _webViewPlatformController.goBack();
+  }
+
+  /// Goes forward in the history of this WebView.
+  ///
+  /// If there is no forward history item this is a no-op.
+  Future<void> goForward() {
+    return _webViewPlatformController.goForward();
+  }
+
+  /// Reloads the current URL.
+  Future<void> reload() {
+    return _webViewPlatformController.reload();
+  }
+
+  /// Clears all caches used by the [WebView].
+  ///
+  /// The following caches are cleared:
+  ///	1. Browser HTTP Cache.
+  ///	2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches.
+  ///    These are not yet supported in iOS WkWebView. Service workers tend to use this cache.
+  ///	3. Application cache.
+  ///	4. Local Storage.
+  ///
+  /// Note: Calling this method also triggers a reload.
+  Future<void> clearCache() async {
+    await _webViewPlatformController.clearCache();
+    return reload();
+  }
+
+  /// Update the widget managed by the [WebViewController].
+  Future<void> updateWidget(WebView widget) async {
+    _widget = widget;
+    await _updateSettings(_webSettingsFromWidget(widget));
+    await _updateJavascriptChannels(
+        _javascriptChannelRegistry.channels.values.toSet());
+  }
+
+  Future<void> _updateSettings(WebSettings newSettings) {
+    final WebSettings update =
+        _clearUnchangedWebSettings(_settings, newSettings);
+    _settings = newSettings;
+    return _webViewPlatformController.updateSettings(update);
+  }
+
+  Future<void> _updateJavascriptChannels(
+      Set<JavascriptChannel>? newChannels) async {
+    final Set<String> currentChannels =
+        _javascriptChannelRegistry.channels.keys.toSet();
+    final Set<String> newChannelNames = _extractChannelNames(newChannels);
+    final Set<String> channelsToAdd =
+        newChannelNames.difference(currentChannels);
+    final Set<String> channelsToRemove =
+        currentChannels.difference(newChannelNames);
+    if (channelsToRemove.isNotEmpty) {
+      await _webViewPlatformController
+          .removeJavascriptChannels(channelsToRemove);
+    }
+    if (channelsToAdd.isNotEmpty) {
+      await _webViewPlatformController.addJavascriptChannels(channelsToAdd);
+    }
+    _javascriptChannelRegistry.updateJavascriptChannelsFromSet(newChannels);
+  }
+
+  /// Evaluates a JavaScript expression in the context of the current page.
+  ///
+  /// On Android returns the evaluation result as a JSON formatted string.
+  ///
+  /// On iOS depending on the value type the return value would be one of:
+  ///
+  ///  - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100').
+  ///  - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.').
+  ///  - Other non-primitive types are not supported on iOS and will complete the Future with an error.
+  ///
+  /// The Future completes with an error if a JavaScript error occurred, or on iOS, if the type of the
+  /// evaluated expression is not supported as described above.
+  ///
+  /// When evaluating Javascript in a [WebView], it is best practice to wait for
+  /// the [WebView.onPageFinished] callback. This guarantees all the Javascript
+  /// embedded in the main frame HTML has been loaded.
+  Future<String> evaluateJavascript(String javascriptString) {
+    if (_settings.javascriptMode == JavascriptMode.disabled) {
+      return Future<String>.error(FlutterError(
+          'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.'));
+    }
+    // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter.
+    // https://github.com/flutter/flutter/issues/26431
+    // ignore: strong_mode_implicit_dynamic_method
+    return _webViewPlatformController.evaluateJavascript(javascriptString);
+  }
+
+  /// Returns the title of the currently loaded page.
+  Future<String?> getTitle() {
+    return _webViewPlatformController.getTitle();
+  }
+
+  /// Sets the WebView's content scroll position.
+  ///
+  /// The parameters `x` and `y` specify the scroll position in WebView pixels.
+  Future<void> scrollTo(int x, int y) {
+    return _webViewPlatformController.scrollTo(x, y);
+  }
+
+  /// Move the scrolled position of this view.
+  ///
+  /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by horizontally and vertically respectively.
+  Future<void> scrollBy(int x, int y) {
+    return _webViewPlatformController.scrollBy(x, y);
+  }
+
+  /// Return the horizontal scroll position, in WebView pixels, of this view.
+  ///
+  /// Scroll position is measured from left.
+  Future<int> getScrollX() {
+    return _webViewPlatformController.getScrollX();
+  }
+
+  /// Return the vertical scroll position, in WebView pixels, of this view.
+  ///
+  /// Scroll position is measured from top.
+  Future<int> getScrollY() {
+    return _webViewPlatformController.getScrollY();
+  }
+
+  // This method assumes that no fields in `currentValue` are null.
+  WebSettings _clearUnchangedWebSettings(
+      WebSettings currentValue, WebSettings newValue) {
+    assert(currentValue.javascriptMode != null);
+    assert(currentValue.hasNavigationDelegate != null);
+    assert(currentValue.hasProgressTracking != null);
+    assert(currentValue.debuggingEnabled != null);
+    assert(currentValue.userAgent != null);
+    assert(newValue.javascriptMode != null);
+    assert(newValue.hasNavigationDelegate != null);
+    assert(newValue.debuggingEnabled != null);
+    assert(newValue.userAgent != null);
+
+    JavascriptMode? javascriptMode;
+    bool? hasNavigationDelegate;
+    bool? hasProgressTracking;
+    bool? debuggingEnabled;
+    WebSetting<String?> userAgent = WebSetting.absent();
+    if (currentValue.javascriptMode != newValue.javascriptMode) {
+      javascriptMode = newValue.javascriptMode;
+    }
+    if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) {
+      hasNavigationDelegate = newValue.hasNavigationDelegate;
+    }
+    if (currentValue.hasProgressTracking != newValue.hasProgressTracking) {
+      hasProgressTracking = newValue.hasProgressTracking;
+    }
+    if (currentValue.debuggingEnabled != newValue.debuggingEnabled) {
+      debuggingEnabled = newValue.debuggingEnabled;
+    }
+    if (currentValue.userAgent != newValue.userAgent) {
+      userAgent = newValue.userAgent;
+    }
+
+    return WebSettings(
+      javascriptMode: javascriptMode,
+      hasNavigationDelegate: hasNavigationDelegate,
+      hasProgressTracking: hasProgressTracking,
+      debuggingEnabled: debuggingEnabled,
+      userAgent: userAgent,
+    );
+  }
+
+  Set<String> _extractChannelNames(Set<JavascriptChannel>? channels) {
+    final Set<String> channelNames = channels == null
+        ? <String>{}
+        : channels.map((JavascriptChannel channel) => channel.name).toSet();
+    return channelNames;
+  }
+
+  // Throws an ArgumentError if `url` is not a valid URL string.
+  void _validateUrlString(String url) {
+    try {
+      final Uri uri = Uri.parse(url);
+      if (uri.scheme.isEmpty) {
+        throw ArgumentError('Missing scheme in URL string: "$url"');
+      }
+    } on FormatException catch (e) {
+      throw ArgumentError(e);
+    }
+  }
+}
+
+WebSettings _webSettingsFromWidget(WebView widget) {
+  return WebSettings(
+    javascriptMode: widget.javascriptMode,
+    hasNavigationDelegate: widget.navigationDelegate != null,
+    hasProgressTracking: widget.onProgress != null,
+    debuggingEnabled: widget.debuggingEnabled,
+    gestureNavigationEnabled: widget.gestureNavigationEnabled,
+    allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback,
+    userAgent: WebSetting<String?>.of(widget.userAgent),
+  );
+}
diff --git a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml
new file mode 100644
index 0000000..1e065a6
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml
@@ -0,0 +1,33 @@
+name: webview_flutter_android_example
+description: Demonstrates how to use the webview_flutter_android plugin.
+publish_to: none
+
+environment:
+  sdk: ">=2.14.0 <3.0.0"
+
+dependencies:
+  flutter:
+    sdk: flutter
+  webview_flutter_android:
+    # When depending on this package from a real application you should use:
+    #   webview_flutter: ^x.y.z
+    # See https://dart.dev/tools/pub/dependencies#version-constraints
+    # The example app is bundled with the plugin so we use a path dependency on
+    # the parent directory to use the current plugin's version.
+    path: ../
+
+dev_dependencies:
+  espresso: ^0.1.0+2
+  flutter_test:
+    sdk: flutter
+  flutter_driver:
+    sdk: flutter
+  integration_test:
+    sdk: flutter
+  pedantic: ^1.10.0
+
+flutter:
+  uses-material-design: true
+  assets:
+    - assets/sample_audio.ogg
+    - assets/sample_video.mp4
diff --git a/packages/webview_flutter/webview_flutter_android/example/test_driver/integration_test.dart b/packages/webview_flutter/webview_flutter_android/example/test_driver/integration_test.dart
new file mode 100644
index 0000000..4f10f2a
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/example/test_driver/integration_test.dart
@@ -0,0 +1,7 @@
+// 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:integration_test/integration_test_driver.dart';
+
+Future<void> main() => integrationDriver();
diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart
new file mode 100644
index 0000000..a48e457
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart
@@ -0,0 +1,62 @@
+// 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 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
+
+/// Builds an Android webview.
+///
+/// This is used as the default implementation for [WebView.platform] on Android. It uses
+/// an [AndroidView] to embed the webview in the widget hierarchy, and uses a method channel to
+/// communicate with the platform code.
+class AndroidWebView implements WebViewPlatform {
+  @override
+  Widget build({
+    required BuildContext context,
+    required CreationParams creationParams,
+    required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler,
+    required JavascriptChannelRegistry javascriptChannelRegistry,
+    WebViewPlatformCreatedCallback? onWebViewPlatformCreated,
+    Set<Factory<OneSequenceGestureRecognizer>>? gestureRecognizers,
+  }) {
+    assert(webViewPlatformCallbacksHandler != null);
+    return GestureDetector(
+      // We prevent text selection by intercepting the long press event.
+      // This is a temporary stop gap due to issues with text selection on Android:
+      // https://github.com/flutter/flutter/issues/24585 - the text selection
+      // dialog is not responding to touch events.
+      // https://github.com/flutter/flutter/issues/24584 - the text selection
+      // handles are not showing.
+      // TODO(amirh): remove this when the issues above are fixed.
+      onLongPress: () {},
+      excludeFromSemantics: true,
+      child: AndroidView(
+        viewType: 'plugins.flutter.io/webview',
+        onPlatformViewCreated: (int id) {
+          if (onWebViewPlatformCreated == null) {
+            return;
+          }
+          onWebViewPlatformCreated(MethodChannelWebViewPlatform(
+            id,
+            webViewPlatformCallbacksHandler,
+            javascriptChannelRegistry,
+          ));
+        },
+        gestureRecognizers: gestureRecognizers,
+        layoutDirection: Directionality.maybeOf(context) ?? TextDirection.rtl,
+        creationParams:
+            MethodChannelWebViewPlatform.creationParamsToMap(creationParams),
+        creationParamsCodec: const StandardMessageCodec(),
+      ),
+    );
+  }
+
+  @override
+  Future<bool> clearCookies() => MethodChannelWebViewPlatform.clearCookies();
+}
diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart
new file mode 100644
index 0000000..6beae10
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart
@@ -0,0 +1,78 @@
+// 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/foundation.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
+
+import 'webview_android.dart';
+
+/// Android [WebViewPlatform] that uses [AndroidViewSurface] to build the [WebView] widget.
+///
+/// To use this, set [WebView.platform] to an instance of this class.
+///
+/// This implementation uses hybrid composition to render the [WebView] on
+/// Android. It solves multiple issues related to accessibility and interaction
+/// with the [WebView] at the cost of some performance on Android versions below
+/// 10. See https://github.com/flutter/flutter/wiki/Hybrid-Composition for more
+/// information.
+class SurfaceAndroidWebView extends AndroidWebView {
+  @override
+  Widget build({
+    required BuildContext context,
+    required CreationParams creationParams,
+    required JavascriptChannelRegistry javascriptChannelRegistry,
+    WebViewPlatformCreatedCallback? onWebViewPlatformCreated,
+    Set<Factory<OneSequenceGestureRecognizer>>? gestureRecognizers,
+    required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler,
+  }) {
+    assert(webViewPlatformCallbacksHandler != null);
+    return PlatformViewLink(
+      viewType: 'plugins.flutter.io/webview',
+      surfaceFactory: (
+        BuildContext context,
+        PlatformViewController controller,
+      ) {
+        return AndroidViewSurface(
+          controller: controller as AndroidViewController,
+          gestureRecognizers: gestureRecognizers ??
+              const <Factory<OneSequenceGestureRecognizer>>{},
+          hitTestBehavior: PlatformViewHitTestBehavior.opaque,
+        );
+      },
+      onCreatePlatformView: (PlatformViewCreationParams params) {
+        return PlatformViewsService.initSurfaceAndroidView(
+          id: params.id,
+          viewType: 'plugins.flutter.io/webview',
+          // WebView content is not affected by the Android view's layout direction,
+          // we explicitly set it here so that the widget doesn't require an ambient
+          // directionality.
+          layoutDirection: TextDirection.rtl,
+          creationParams: MethodChannelWebViewPlatform.creationParamsToMap(
+            creationParams,
+            usesHybridComposition: true,
+          ),
+          creationParamsCodec: const StandardMessageCodec(),
+        )
+          ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
+          ..addOnPlatformViewCreatedListener((int id) {
+            if (onWebViewPlatformCreated == null) {
+              return;
+            }
+            onWebViewPlatformCreated(
+              MethodChannelWebViewPlatform(
+                id,
+                webViewPlatformCallbacksHandler,
+                javascriptChannelRegistry,
+              ),
+            );
+          })
+          ..create();
+      },
+    );
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml
new file mode 100644
index 0000000..f7db4c6
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml
@@ -0,0 +1,31 @@
+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.0.13
+
+environment:
+  sdk: ">=2.14.0 <3.0.0"
+  flutter: ">=2.5.0"
+
+flutter:
+  plugin:
+    implements: webview_flutter
+    platforms:
+      android:
+        package: io.flutter.plugins.webviewflutter
+        pluginClass: WebViewFlutterPlugin
+
+dependencies:
+  flutter:
+    sdk: flutter
+
+  webview_flutter_platform_interface: ^1.0.0
+
+dev_dependencies:
+  flutter_driver:
+    sdk: flutter
+  flutter_test:
+    sdk: flutter
+  pedantic: ^1.10.0
+
diff --git a/script/configs/exclude_all_plugins_app.yaml b/script/configs/exclude_all_plugins_app.yaml
index 8dd0fde..f6246aa 100644
--- a/script/configs/exclude_all_plugins_app.yaml
+++ b/script/configs/exclude_all_plugins_app.yaml
@@ -8,3 +8,9 @@
 
 # This is a permament entry, as it should never be a direct app dependency.
 - plugin_platform_interface
+# TODO(mvanbeusekom): Remove the exclusion of the webview_flutter_android and 
+#                     webview_flutter_wkwebview packages once the native 
+#                     implementation is removed from the webview_flutter 
+#                     package (see https://github.com/flutter/flutter/issues/86286).
+- webview_flutter_android
+- webview_flutter_wkwebview