[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