Commit 3695a070 by zhang

添加项目

parent e58b8acd
## 1.0.7
* Minor documentation update to indicate known issue on iOS 13.4 and 13.5.
* See: https://github.com/flutter/flutter/issues/53490
## 1.0.6
* Invoke the WebView.onWebResourceError on iOS when the webview content process crashes.
## 1.0.5
* Fix example in the readme.
## 1.0.4
* Suppress the `deprecated_member_use` warning in the example app for `ScaffoldMessenger.showSnackBar`.
## 1.0.3
* Update android compileSdkVersion to 29.
## 1.0.2
* Android Code Inspection and Clean up.
## 1.0.1
* Add documentation for `WebViewPlatformCreatedCallback`.
## 1.0.0 - Out of developer preview 🎉.
* Bumped the minimal Flutter SDK to 1.22 where platform views are out of developer preview, and
performing better on iOS. Flutter 1.22 no longer requires adding the
`io.flutter.embedded_views_preview` flag to `Info.plist`.
* Added support for Hybrid Composition on Android (see opt-in instructions in [README](https://github.com/flutter/plugins/blob/master/packages/webview_flutter/README.md#android))
* Lowered the required Android API to 19 (was previously 20): [#23728](https://github.com/flutter/flutter/issues/23728).
* Fixed the following issues:
* 🎹 Keyboard: [#41089](https://github.com/flutter/flutter/issues/41089), [#36478](https://github.com/flutter/flutter/issues/36478), [#51254](https://github.com/flutter/flutter/issues/51254), [#50716](https://github.com/flutter/flutter/issues/50716), [#55724](https://github.com/flutter/flutter/issues/55724), [#56513](https://github.com/flutter/flutter/issues/56513), [#56515](https://github.com/flutter/flutter/issues/56515), [#61085](https://github.com/flutter/flutter/issues/61085), [#62205](https://github.com/flutter/flutter/issues/62205), [#62547](https://github.com/flutter/flutter/issues/62547), [#58943](https://github.com/flutter/flutter/issues/58943), [#56361](https://github.com/flutter/flutter/issues/56361), [#56361](https://github.com/flutter/flutter/issues/42902), [#40716](https://github.com/flutter/flutter/issues/40716), [#37989](https://github.com/flutter/flutter/issues/37989), [#27924](https://github.com/flutter/flutter/issues/27924).
* ♿️ Accessibility: [#50716](https://github.com/flutter/flutter/issues/50716).
* ⚡️ Performance: [#61280](https://github.com/flutter/flutter/issues/61280), [#31243](https://github.com/flutter/flutter/issues/31243), [#52211](https://github.com/flutter/flutter/issues/52211).
* 📹 Video: [#5191](https://github.com/flutter/flutter/issues/5191).
## 0.3.24
* Keep handling deprecated Android v1 classes for backward compatibility.
## 0.3.23
* Handle WebView multi-window support.
## 0.3.22+2
* Update package:e2e reference to use the local version in the flutter/plugins
repository.
## 0.3.22+1
* Update the `setAndGetScrollPosition` to use hard coded values and add a `pumpAndSettle` call.
## 0.3.22
* Add support for passing a failing url.
## 0.3.21
* Enable programmatic scrolling using Android's WebView.scrollTo & iOS WKWebView.scrollView.contentOffset.
## 0.3.20+2
* Fix CocoaPods podspec lint warnings.
## 0.3.20+1
* OCMock module import -> #import, unit tests compile generated as library.
* Fix select drop down crash on old Android tablets (https://github.com/flutter/flutter/issues/54164).
## 0.3.20
* Added support for receiving web resource loading errors. See `WebView.onWebResourceError`.
## 0.3.19+10
* Replace deprecated `getFlutterEngine` call on Android.
## 0.3.19+9
* Remove example app's iOS workspace settings.
## 0.3.19+8
* Make the pedantic dev_dependency explicit.
## 0.3.19+7
* Remove the Flutter SDK constraint upper bound.
## 0.3.19+6
* Enable opening links that target the "_blank" window (links open in same window).
## 0.3.19+5
* On iOS, always keep contentInsets of the WebView to be 0.
* Fix XCTest case to follow XCTest naming convention.
## 0.3.19+4
* On iOS, fix the scroll view content inset is automatically adjusted. After the fix, the content position of the WebView is customizable by Flutter.
* Fix an iOS 13 bug where the scroll indicator shows at random location.
## 0.3.19+3
* Setup XCTests.
## 0.3.19+2
* Migrate from deprecated BinaryMessages to ServicesBinding.instance.defaultBinaryMessenger.
## 0.3.19+1
* Raise min Flutter SDK requirement to the latest stable. v2 embedding apps no
longer need to special case their Flutter SDK requirement like they have
since v0.3.15+3.
## 0.3.19
* Add setting for iOS to allow gesture based navigation.
## 0.3.18+1
* Be explicit that keyboard is not ready for production in README.md.
## 0.3.18
* Add support for onPageStarted event.
* Remove the deprecated `author:` field from pubspec.yaml
* Migrate to the new pubspec platforms manifest.
* Require Flutter SDK 1.10.0 or greater.
## 0.3.17
* Fix pedantic lint errors. Added missing documentation and awaited some futures
in tests and the example app.
## 0.3.16
* Add support for async NavigationDelegates. Synchronous NavigationDelegates
should still continue to function without any change in behavior.
## 0.3.15+3
* Re-land support for the v2 Android embedding. This correctly sets the minimum
SDK to the latest stable and avoid any compile errors. *WARNING:* the V2
embedding itself still requires the current Flutter master channel
(flutter/flutter@1d4d63a) for text input to work properly on all Android
versions.
## 0.3.15+2
* Remove AndroidX warnings.
## 0.3.15+1
* Revert the prior embedding support add since it requires an API that hasn't
rolled to stable.
## 0.3.15
* Add support for the v2 Android embedding. This shouldn't affect existing
functionality. Plugin authors who use the V2 embedding can now register the
plugin and expect that it correctly responds to app lifecycle changes.
## 0.3.14+2
* Define clang module for iOS.
## 0.3.14+1
* Allow underscores anywhere for Javascript Channel name.
## 0.3.14
* Added a getTitle getter to WebViewController.
## 0.3.13
* Add an optional `userAgent` property to set a custom User Agent.
## 0.3.12+1
* Temporarily revert getTitle (doing this as a patch bump shortly after publishing).
## 0.3.12
* Added a getTitle getter to WebViewController.
## 0.3.11+6
* Calling destroy on Android webview when flutter webview is getting disposed.
## 0.3.11+5
* Reduce compiler warnings regarding iOS9 compatibility by moving a single
method back into a `@available` block.
## 0.3.11+4
* Removed noisy log messages on iOS.
## 0.3.11+3
* Apply the display listeners workaround that was shipped in 0.3.11+1 on
all Android versions prior to P.
## 0.3.11+2
* Add fix for input connection being dropped after a screen resize on certain
Android devices.
## 0.3.11+1
* Work around a bug in old Android WebView versions that was causing a crash
when resizing the webview on old devices.
## 0.3.11
* Add an initialAutoMediaPlaybackPolicy setting for controlling how auto media
playback is restricted.
## 0.3.10+5
* Add dependency on `androidx.annotation:annotation:1.0.0`.
## 0.3.10+4
* Add keyboard text to README.
## 0.3.10+3
* Don't log an unknown setting key error for 'debuggingEnabled' on iOS.
## 0.3.10+2
* Fix InputConnection being lost when combined with route transitions.
## 0.3.10+1
* Add support for simultaenous Flutter `TextInput` and WebView text fields.
## 0.3.10
* Add partial WebView keyboard support for Android versions prior to N. Support
for UIs that also have Flutter `TextInput` fields is still pending. This basic
support currently only works with Flutter `master`. The keyboard will still
appear when it previously did not when run with older versions of Flutter. But
if the WebView is resized while showing the keyboard the text field will need
to be focused multiple times for any input to be registered.
## 0.3.9+2
* Update Dart code to conform to current Dart formatter.
## 0.3.9+1
* Add missing template type parameter to `invokeMethod` calls.
* Bump minimum Flutter version to 1.5.0.
* Replace invokeMethod with invokeMapMethod wherever necessary.
## 0.3.9
* Allow external packages to provide webview implementations for new platforms.
## 0.3.8+1
* Suppress deprecation warning for BinaryMessages. See: https://github.com/flutter/flutter/issues/33446
## 0.3.8
* Add `debuggingEnabled` property.
## 0.3.7+1
* Fix an issue where JavaScriptChannel messages weren't sent from the platform thread on Android.
## 0.3.7
* Fix loadUrlWithHeaders flaky test.
## 0.3.6+1
* Remove un-used method params in webview\_flutter
## 0.3.6
* Add an optional `headers` field to the controller.
## 0.3.5+5
* Fixed error in documentation of `javascriptChannels`.
## 0.3.5+4
* Fix bugs in the example app by updating it to use a `StatefulWidget`.
## 0.3.5+3
* Make sure to post javascript channel messages from the platform thread.
## 0.3.5+2
* Fix crash from `NavigationDelegate` on later versions of Android.
## 0.3.5+1
* Fix a bug where updates to onPageFinished were ignored.
## 0.3.5
* Added an onPageFinished callback.
## 0.3.4
* Support specifying navigation delegates that can prevent navigations from being executed.
## 0.3.3+2
* Exclude LongPress handler from semantics tree since it does nothing.
## 0.3.3+1
* Fixed a memory leak on Android - the WebView was not properly disposed.
## 0.3.3
* Add clearCache method to WebView controller.
## 0.3.2+1
* Log a more detailed warning at build time about the previous AndroidX
migration.
## 0.3.2
* Added CookieManager to interface with WebView cookies. Currently has the ability to clear cookies.
## 0.3.1
* Added JavaScript channels to facilitate message passing from JavaScript code running inside
the WebView to the Flutter app's Dart code.
## 0.3.0
* **Breaking change**. Migrate from the deprecated original Android Support
Library to AndroidX. This shouldn't result in any functional changes, but it
requires any Android apps using this plugin to [also
migrate](https://developer.android.com/jetpack/androidx/migrate) if they're
using the original support library.
## 0.2.0
* Added a evaluateJavascript method to WebView controller.
* (BREAKING CHANGE) Renamed the `JavaScriptMode` enum to `JavascriptMode`, and the WebView `javasScriptMode` parameter to `javascriptMode`.
## 0.1.2
* Added a reload method to the WebView controller.
## 0.1.1
* Added a `currentUrl` accessor for the WebView controller to look up what URL
is being displayed.
## 0.1.0+1
* Fix null crash when initialUrl is unset on iOS.
## 0.1.0
* Add goBack, goForward, canGoBack, and canGoForward methods to the WebView controller.
## 0.0.1+1
* Fix case for "FLTWebViewFlutterPlugin" (iOS was failing to buld on case-sensitive file systems).
## 0.0.1
* Initial release.
Copyright 2018 The Chromium 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.
group 'io.flutter.plugins.webviewflutter'
version '1.0-SNAPSHOT'
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.0'
}
}
rootProject.allprojects {
repositories {
google()
jcenter()
}
}
apply plugin: 'com.android.library'
android {
compileSdkVersion 29
defaultConfig {
minSdkVersion 16
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
lintOptions {
disable 'InvalidPackage'
}
dependencies {
implementation 'androidx.annotation:annotation:1.0.0'
implementation 'androidx.webkit:webkit:1.0.0'
api 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.4@aar'
api 'io.reactivex.rxjava2:rxandroid:2.1.1'
}
}
rootProject.name = 'webview_flutter'
<manifest package="io.flutter.plugins.webviewflutter">
</manifest>
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<>();
}
}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.plugins.webviewflutter;
import android.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);
}
}
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.plugins.webviewflutter;
import android.Manifest;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.hardware.display.DisplayManager;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
import android.webkit.WebStorage;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;
import com.tbruyelle.rxpermissions2.RxPermissions;
import androidx.annotation.NonNull;
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;
import io.flutter.plugin.platform.PlatformView;
import io.reactivex.Observer;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
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 InputAwareWebView webView;
private final MethodChannel methodChannel;
private final FlutterWebViewClient flutterWebViewClient;
private WebViewUploadFileHelper helper;
private final Handler platformThreadHandler;
private Activity mActivity;
public void setActivity(Activity activity) {
mActivity = activity;
helper = new WebViewUploadFileHelper(activity);
}
// 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;
}
// For Android >= 5.0
@Override
public boolean onShowFileChooser(WebView webView,
ValueCallback<Uri[]> filePathCallback,
final FileChooserParams fileChooserParams) {
if (helper != null) {
helper.setUploadMessageAboveL(filePathCallback);
permission(new CallBack() {
@Override
public void onSuccess() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
helper.openImageActivity(fileChooserParams.getAcceptTypes(), fileChooserParams.isCaptureEnabled());
}
}
});
}
return true;
}
@SuppressLint("CheckResult")
public void permission(final CallBack callBack){
// 权限支持
if (mActivity != null){
RxPermissions rxPermissions = new RxPermissions( mActivity );
rxPermissions.request(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
.subscribe(new Consumer<Boolean>() {
@Override
public void accept(Boolean grant) throws Exception {
if (grant) {
//全部通过
try {
if (callBack!=null){
callBack.onSuccess();
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
} else {
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(mActivity, "请同意权限", Toast.LENGTH_LONG).show();
}
});
}
}
});
}
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
@SuppressWarnings("unchecked")
FlutterWebView(
final Context context,
BinaryMessenger messenger,
int id,
Map<String, Object> params,
View containerView,
Activity activity) {
mActivity = activity;
DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy();
DisplayManager displayManager =
(DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
displayListenerProxy.onPreWebViewInitialization(displayManager);
webView = new InputAwareWebView(context, containerView);
displayListenerProxy.onPostWebViewInitialization(displayManager);
platformThreadHandler = new Handler(context.getMainLooper());
// Allow local storage.
webView.getSettings().setDomStorageEnabled(true);
webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);
// Multi windows is set with FlutterWebChromeClient by default to handle internal bug: b/159892679.
webView.getSettings().setSupportMultipleWindows(true);
webView.setWebChromeClient(new FlutterWebChromeClient());
methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id);
methodChannel.setMethodCallHandler(this);
flutterWebViewClient = new FlutterWebViewClient(methodChannel);
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);
}
helper = new WebViewUploadFileHelper(activity);
if (params.containsKey("initialUrl")) {
String url = (String) params.get("initialUrl");
webView.loadUrl(url);
}
}
@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() {
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() {
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) {
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() {
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 "gestureNavigationEnabled":
break;
case "userAgent":
updateUserAgent((String) settings.get(key));
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);
webView.dispose();
webView.destroy();
}
public interface CallBack{
void onSuccess();
}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.plugins.webviewflutter;
import android.annotation.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.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;
FlutterWebViewClient(MethodChannel methodChannel) {
this.methodChannel = methodChannel;
}
private 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;
}
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);
}
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) {
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(
WebView view, WebResourceRequest request, WebResourceErrorCompat error) {
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);
}
}
}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.plugins.webviewflutter;
import 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;
}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.plugins.webviewflutter;
import android.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);
}
}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.plugins.webviewflutter;
import android.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;
}
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.plugins.webviewflutter;
import android.app.Activity;
import android.content.Context;
import android.view.View;
import io.flutter.plugin.common.BinaryMessenger;
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 WebViewFactory extends PlatformViewFactory {
private final BinaryMessenger messenger;
private final View containerView;
private Activity mActivity;
private FlutterWebView flutterWebView;
WebViewFactory(BinaryMessenger messenger, View containerView, Activity activity) {
super(StandardMessageCodec.INSTANCE);
this.messenger = messenger;
this.containerView = containerView;
this.mActivity = activity;
}
@SuppressWarnings("unchecked")
@Override
public PlatformView create(Context context, int id, Object args) {
Map<String, Object> params = (Map<String, Object>) args;
flutterWebView = new FlutterWebView(context, messenger, id, params, containerView, mActivity);
return flutterWebView;
}
public void setActivity(Activity activity) {
this.mActivity = activity;
flutterWebView.setActivity(activity);
}
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.plugins.webviewflutter;
import android.app.Activity;
import androidx.annotation.NonNull;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
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, ActivityAware {
private FlutterCookieManager flutterCookieManager;
private WebViewFactory webViewFactory;
private Activity activity;
/**
* 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 WebViewFactory(registrar.messenger(), registrar.view(), registrar.activity()));
new FlutterCookieManager(registrar.messenger());
}
@Override
public void onAttachedToEngine(FlutterPluginBinding binding) {
BinaryMessenger messenger = binding.getBinaryMessenger();
webViewFactory = new WebViewFactory(messenger, /*containerView=*/ null, activity);
binding
.getPlatformViewRegistry()
.registerViewFactory(
"plugins.flutter.io/webview", webViewFactory);
flutterCookieManager = new FlutterCookieManager(messenger);
}
@Override
public void onDetachedFromEngine(FlutterPluginBinding binding) {
if (flutterCookieManager == null) {
return;
}
flutterCookieManager.dispose();
flutterCookieManager = null;
}
@Override
public void onAttachedToActivity(ActivityPluginBinding binding) {
activity = binding.getActivity();
if (webViewFactory != null){
webViewFactory.setActivity(activity);
}
}
@Override
public void onDetachedFromActivityForConfigChanges() {
}
@Override
public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) {
onAttachedToActivity(binding);
}
@Override
public void onDetachedFromActivity() {
}
}
package io.flutter.plugins.webviewflutter;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.webkit.ValueCallback;
import java.io.File;
import java.util.Calendar;
import java.util.Locale;
import androidx.core.content.FileProvider;
public class WebViewUploadFileHelper {
private static final int RESULT_OK = 200;
private ValueCallback<Uri> uploadMessage;
private ValueCallback<Uri[]> uploadMessageAboveL;
private final static int FILE_CHOOSER_RESULT_CODE = 10011;//文件选择
private Uri imageUri;
public Activity activity;
private WebViewUploadFileHelper() {
}
public WebViewUploadFileHelper(Activity activity) {
this.activity = activity;
}
public void setUploadMessage(ValueCallback<Uri> uploadMessage) {
this.uploadMessage = uploadMessage;
}
public void setUploadMessageAboveL(ValueCallback<Uri[]> uploadMessageAboveL) {
this.uploadMessageAboveL = uploadMessageAboveL;
}
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode != FILE_CHOOSER_RESULT_CODE) return;
// 经过上边(1)、(2)两个赋值操作,此处即可根据其值是否为空来决定采用哪种处理方法
if (uploadMessage != null) {
chooseBelow( resultCode, data );
} else if (uploadMessageAboveL != null) {
chooseAbove( resultCode, data );
}
}
public void openImageActivity() {
chooseImage( "image/*" );
}
public void openImageActivity(String acceptType) {
chooseImage( acceptType );
}
public void openImageActivity(String acceptType, String capture) {
if (capture != null && capture.equals("camera")){
takePhoto();
} else {
chooseImage( acceptType );
}
}
public void openImageActivity(String[] acceptType, boolean isCaptureEnabled) {
if (isCaptureEnabled) {
takePhoto();
} else {
chooseImage( acceptType );
}
}
private void chooseBelow(int resultCode, Intent data) {
if (RESULT_OK == resultCode) {
updatePhotos();
if (data != null) {
// 这里是针对文件路径处理
Uri uri = data.getData();
if (uri != null) {
uploadMessage.onReceiveValue( uri );
} else {
uploadMessage.onReceiveValue( null );
}
} else {
// 以指定图像存储路径的方式调起相机,成功后返回data为空
uploadMessage.onReceiveValue( imageUri );
}
} else {
uploadMessage.onReceiveValue( null );
}
uploadMessage = null;
}
private void chooseAbove(int resultCode, Intent data) {
if (RESULT_OK == resultCode) {
updatePhotos();
if (data != null) {
// 这里是针对从文件中选图片的处理
Uri[] results;
Uri uriData = data.getData();
if (uriData != null) {
results = new Uri[]{uriData};
uploadMessageAboveL.onReceiveValue( results );
} else {
uploadMessageAboveL.onReceiveValue( null );
}
} else {
uploadMessageAboveL.onReceiveValue( new Uri[]{imageUri} );
}
} else {
uploadMessageAboveL.onReceiveValue( null );
}
uploadMessageAboveL = null;
}
private void updatePhotos() {
// 该广播即使多发(即选取照片成功时也发送)也没有关系,只是唤醒系统刷新媒体文件
Intent intent = new Intent( Intent.ACTION_MEDIA_SCANNER_SCAN_FILE );
intent.setData( imageUri );
activity.sendBroadcast( intent );
}
//调用相机
private void takePhoto() {
String fileName = "IMG_" + DateFormat.format( "yyyyMMdd_hhmmss", Calendar.getInstance( Locale.CHINA ) ) + ".jpg";
// 步骤一:创建存储照片的文件
String imagePath = activity.getFilesDir() + File.separator + "images" + File.separator + fileName;
File file = new File( imagePath );
//创建文件夹
if (!file.getParentFile().exists())
file.getParentFile().mkdirs();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//步骤二:Android 7.0及以上获取文件 Uri
imageUri = FileProvider.getUriForFile( activity, activity.getPackageName() + ".fileProvider", file );
} else {
//步骤三:获取文件Uri
imageUri = Uri.fromFile( file );
}
Intent intent = new Intent();
intent.addFlags( Intent.FLAG_GRANT_READ_URI_PERMISSION );
intent.setAction( MediaStore.ACTION_IMAGE_CAPTURE );//设置Action为拍照
intent.putExtra( MediaStore.EXTRA_OUTPUT, imageUri );//将拍取的照片保存到指定URI
activity.startActivityForResult( intent, FILE_CHOOSER_RESULT_CODE );
}
//图片选择器
private void chooseImage(String[] acceptType) {
Intent i = new Intent( Intent.ACTION_GET_CONTENT );
i.addCategory( Intent.CATEGORY_OPENABLE );
i.setType( "*/*" );
i.putExtra( Intent.EXTRA_MIME_TYPES, acceptType );
activity.startActivityForResult( i, FILE_CHOOSER_RESULT_CODE );
}
//图片选择器
private void chooseImage(String acceptType) {
Intent i = new Intent( Intent.ACTION_GET_CONTENT );
i.addCategory( Intent.CATEGORY_OPENABLE );
if (TextUtils.isEmpty( acceptType )) {
i.setType( "*/*" );
} else {
i.setType( acceptType );
}
activity.startActivityForResult( Intent.createChooser( i, "Image Chooser" ), FILE_CHOOSER_RESULT_CODE );
}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <Flutter/Flutter.h>
#import <WebKit/WebKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface FLTCookieManager : NSObject <FlutterPlugin>
@end
NS_ASSUME_NONNULL_END
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "FLTCookieManager.h"
@implementation FLTCookieManager {
}
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
FLTCookieManager *instance = [[FLTCookieManager alloc] init];
FlutterMethodChannel *channel =
[FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/cookie_manager"
binaryMessenger:[registrar messenger]];
[registrar addMethodCallDelegate:instance channel:channel];
}
- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
if ([[call method] isEqualToString:@"clearCookies"]) {
[self clearCookies:result];
} else {
result(FlutterMethodNotImplemented);
}
}
- (void)clearCookies:(FlutterResult)result {
if (@available(iOS 9.0, *)) {
NSSet<NSString *> *websiteDataTypes = [NSSet setWithObject:WKWebsiteDataTypeCookies];
WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore];
void (^deleteAndNotify)(NSArray<WKWebsiteDataRecord *> *) =
^(NSArray<WKWebsiteDataRecord *> *cookies) {
BOOL hasCookies = cookies.count > 0;
[dataStore removeDataOfTypes:websiteDataTypes
forDataRecords:cookies
completionHandler:^{
result(@(hasCookies));
}];
};
[dataStore fetchDataRecordsOfTypes:websiteDataTypes completionHandler:deleteAndNotify];
} else {
// support for iOS8 tracked in https://github.com/flutter/flutter/issues/27624.
NSLog(@"Clearing cookies is not supported for Flutter WebViews prior to iOS 9.");
}
}
@end
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <Flutter/Flutter.h>
#import <WebKit/WebKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface FLTWKNavigationDelegate : NSObject <WKNavigationDelegate>
- (instancetype)initWithChannel:(FlutterMethodChannel*)channel;
/**
* Whether to delegate navigation decisions over the method channel.
*/
@property(nonatomic, assign) BOOL hasDartNavigationDelegate;
@end
NS_ASSUME_NONNULL_END
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "FLTWKNavigationDelegate.h"
@implementation FLTWKNavigationDelegate {
FlutterMethodChannel *_methodChannel;
}
- (instancetype)initWithChannel:(FlutterMethodChannel *)channel {
self = [super init];
if (self) {
_methodChannel = channel;
}
return self;
}
#pragma mark - WKNavigationDelegate conformance
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
[_methodChannel invokeMethod:@"onPageStarted" arguments:@{@"url" : webView.URL.absoluteString}];
}
- (void)webView:(WKWebView *)webView
decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
if (!self.hasDartNavigationDelegate) {
decisionHandler(WKNavigationActionPolicyAllow);
return;
}
NSDictionary *arguments = @{
@"url" : navigationAction.request.URL.absoluteString,
@"isForMainFrame" : @(navigationAction.targetFrame.isMainFrame)
};
[_methodChannel invokeMethod:@"navigationRequest"
arguments:arguments
result:^(id _Nullable result) {
if ([result isKindOfClass:[FlutterError class]]) {
NSLog(@"navigationRequest has unexpectedly completed with an error, "
@"allowing navigation.");
decisionHandler(WKNavigationActionPolicyAllow);
return;
}
if (result == FlutterMethodNotImplemented) {
NSLog(@"navigationRequest was unexepectedly not implemented: %@, "
@"allowing navigation.",
result);
decisionHandler(WKNavigationActionPolicyAllow);
return;
}
if (![result isKindOfClass:[NSNumber class]]) {
NSLog(@"navigationRequest unexpectedly returned a non boolean value: "
@"%@, allowing navigation.",
result);
decisionHandler(WKNavigationActionPolicyAllow);
return;
}
NSNumber *typedResult = result;
decisionHandler([typedResult boolValue] ? WKNavigationActionPolicyAllow
: WKNavigationActionPolicyCancel);
}];
}
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
[_methodChannel invokeMethod:@"onPageFinished" arguments:@{@"url" : webView.URL.absoluteString}];
}
+ (id)errorCodeToString:(NSUInteger)code {
switch (code) {
case WKErrorUnknown:
return @"unknown";
case WKErrorWebContentProcessTerminated:
return @"webContentProcessTerminated";
case WKErrorWebViewInvalidated:
return @"webViewInvalidated";
case WKErrorJavaScriptExceptionOccurred:
return @"javaScriptExceptionOccurred";
case WKErrorJavaScriptResultTypeIsUnsupported:
return @"javaScriptResultTypeIsUnsupported";
}
return [NSNull null];
}
- (void)onWebResourceError:(NSError *)error {
[_methodChannel invokeMethod:@"onWebResourceError"
arguments:@{
@"errorCode" : @(error.code),
@"domain" : error.domain,
@"description" : error.description,
@"errorType" : [FLTWKNavigationDelegate errorCodeToString:error.code],
}];
}
- (void)webView:(WKWebView *)webView
didFailNavigation:(WKNavigation *)navigation
withError:(NSError *)error {
[self onWebResourceError:error];
}
- (void)webView:(WKWebView *)webView
didFailProvisionalNavigation:(WKNavigation *)navigation
withError:(NSError *)error {
[self onWebResourceError:error];
}
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView {
NSError *contentProcessTerminatedError =
[[NSError alloc] initWithDomain:WKErrorDomain
code:WKErrorWebContentProcessTerminated
userInfo:nil];
[self onWebResourceError:contentProcessTerminatedError];
}
@end
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <Flutter/Flutter.h>
@interface FLTWebViewFlutterPlugin : NSObject <FlutterPlugin>
@end
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "FLTWebViewFlutterPlugin.h"
#import "FLTCookieManager.h"
#import "FlutterWebView.h"
@implementation FLTWebViewFlutterPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
FLTWebViewFactory* webviewFactory =
[[FLTWebViewFactory alloc] initWithMessenger:registrar.messenger];
[registrar registerViewFactory:webviewFactory withId:@"plugins.flutter.io/webview"];
[FLTCookieManager registerWithRegistrar:registrar];
}
@end
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <Flutter/Flutter.h>
#import <WebKit/WebKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface FLTWebViewController : NSObject <FlutterPlatformView, WKUIDelegate>
- (instancetype)initWithFrame:(CGRect)frame
viewIdentifier:(int64_t)viewId
arguments:(id _Nullable)args
binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger;
- (UIView*)view;
@end
@interface FLTWebViewFactory : NSObject <FlutterPlatformViewFactory>
- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger>*)messenger;
@end
/**
* The WkWebView used for the plugin.
*
* This class overrides some methods in `WKWebView` to serve the needs for the plugin.
*/
@interface FLTWKWebView : WKWebView
@end
NS_ASSUME_NONNULL_END
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "FlutterWebView.h"
#import "FLTWKNavigationDelegate.h"
#import "JavaScriptChannelHandler.h"
@implementation FLTWebViewFactory {
NSObject<FlutterBinaryMessenger>* _messenger;
}
- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger>*)messenger {
self = [super init];
if (self) {
_messenger = messenger;
}
return self;
}
- (NSObject<FlutterMessageCodec>*)createArgsCodec {
return [FlutterStandardMessageCodec sharedInstance];
}
- (NSObject<FlutterPlatformView>*)createWithFrame:(CGRect)frame
viewIdentifier:(int64_t)viewId
arguments:(id _Nullable)args {
FLTWebViewController* webviewController = [[FLTWebViewController alloc] initWithFrame:frame
viewIdentifier:viewId
arguments:args
binaryMessenger:_messenger];
return webviewController;
}
@end
@implementation FLTWKWebView
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
self.scrollView.contentInset = UIEdgeInsetsZero;
// We don't want the contentInsets to be adjusted by iOS, flutter should always take control of
// webview's contentInsets.
// self.scrollView.contentInset = UIEdgeInsetsZero;
if (@available(iOS 11, *)) {
// Above iOS 11, adjust contentInset to compensate the adjustedContentInset so the sum will
// always be 0.
if (UIEdgeInsetsEqualToEdgeInsets(self.scrollView.adjustedContentInset, UIEdgeInsetsZero)) {
return;
}
UIEdgeInsets insetToAdjust = self.scrollView.adjustedContentInset;
self.scrollView.contentInset = UIEdgeInsetsMake(-insetToAdjust.top, -insetToAdjust.left,
-insetToAdjust.bottom, -insetToAdjust.right);
}
}
@end
@implementation FLTWebViewController {
FLTWKWebView* _webView;
int64_t _viewId;
FlutterMethodChannel* _channel;
NSString* _currentUrl;
// The set of registered JavaScript channel names.
NSMutableSet* _javaScriptChannelNames;
FLTWKNavigationDelegate* _navigationDelegate;
}
- (instancetype)initWithFrame:(CGRect)frame
viewIdentifier:(int64_t)viewId
arguments:(id _Nullable)args
binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger {
if (self = [super init]) {
_viewId = viewId;
NSString* channelName = [NSString stringWithFormat:@"plugins.flutter.io/webview_%lld", viewId];
_channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:messenger];
_javaScriptChannelNames = [[NSMutableSet alloc] init];
WKUserContentController* userContentController = [[WKUserContentController alloc] init];
if ([args[@"javascriptChannelNames"] isKindOfClass:[NSArray class]]) {
NSArray* javaScriptChannelNames = args[@"javascriptChannelNames"];
[_javaScriptChannelNames addObjectsFromArray:javaScriptChannelNames];
[self registerJavaScriptChannels:_javaScriptChannelNames controller:userContentController];
}
NSDictionary<NSString*, id>* settings = args[@"settings"];
WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init];
configuration.userContentController = userContentController;
[self updateAutoMediaPlaybackPolicy:args[@"autoMediaPlaybackPolicy"]
inConfiguration:configuration];
_webView = [[FLTWKWebView alloc] initWithFrame:frame configuration:configuration];
_navigationDelegate = [[FLTWKNavigationDelegate alloc] initWithChannel:_channel];
_webView.UIDelegate = self;
_webView.navigationDelegate = _navigationDelegate;
__weak __typeof__(self) weakSelf = self;
[_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
[weakSelf onMethodCall:call result:result];
}];
if (@available(iOS 11.0, *)) {
_webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
if (@available(iOS 13.0, *)) {
_webView.scrollView.automaticallyAdjustsScrollIndicatorInsets = NO;
}
}
[self applySettings:settings];
// TODO(amirh): return an error if apply settings failed once it's possible to do so.
// https://github.com/flutter/flutter/issues/36228
NSString* initialUrl = args[@"initialUrl"];
if ([initialUrl isKindOfClass:[NSString class]]) {
[self loadUrl:initialUrl];
}
}
return self;
}
- (UIView*)view {
return _webView;
}
- (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if ([[call method] isEqualToString:@"updateSettings"]) {
[self onUpdateSettings:call result:result];
} else if ([[call method] isEqualToString:@"loadUrl"]) {
[self onLoadUrl:call result:result];
} else if ([[call method] isEqualToString:@"canGoBack"]) {
[self onCanGoBack:call result:result];
} else if ([[call method] isEqualToString:@"canGoForward"]) {
[self onCanGoForward:call result:result];
} else if ([[call method] isEqualToString:@"goBack"]) {
[self onGoBack:call result:result];
} else if ([[call method] isEqualToString:@"goForward"]) {
[self onGoForward:call result:result];
} else if ([[call method] isEqualToString:@"reload"]) {
[self onReload:call result:result];
} else if ([[call method] isEqualToString:@"currentUrl"]) {
[self onCurrentUrl:call result:result];
} else if ([[call method] isEqualToString:@"evaluateJavascript"]) {
[self onEvaluateJavaScript:call result:result];
} else if ([[call method] isEqualToString:@"addJavascriptChannels"]) {
[self onAddJavaScriptChannels:call result:result];
} else if ([[call method] isEqualToString:@"removeJavascriptChannels"]) {
[self onRemoveJavaScriptChannels:call result:result];
} else if ([[call method] isEqualToString:@"clearCache"]) {
[self clearCache:result];
} else if ([[call method] isEqualToString:@"getTitle"]) {
[self onGetTitle:result];
} else if ([[call method] isEqualToString:@"scrollTo"]) {
[self onScrollTo:call result:result];
} else if ([[call method] isEqualToString:@"scrollBy"]) {
[self onScrollBy:call result:result];
} else if ([[call method] isEqualToString:@"getScrollX"]) {
[self getScrollX:call result:result];
} else if ([[call method] isEqualToString:@"getScrollY"]) {
[self getScrollY:call result:result];
} else {
result(FlutterMethodNotImplemented);
}
}
- (void)onUpdateSettings:(FlutterMethodCall*)call result:(FlutterResult)result {
NSString* error = [self applySettings:[call arguments]];
if (error == nil) {
result(nil);
return;
}
result([FlutterError errorWithCode:@"updateSettings_failed" message:error details:nil]);
}
- (void)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result {
if (![self loadRequest:[call arguments]]) {
result([FlutterError
errorWithCode:@"loadUrl_failed"
message:@"Failed parsing the URL"
details:[NSString stringWithFormat:@"Request was: '%@'", [call arguments]]]);
} else {
result(nil);
}
}
- (void)onCanGoBack:(FlutterMethodCall*)call result:(FlutterResult)result {
BOOL canGoBack = [_webView canGoBack];
result([NSNumber numberWithBool:canGoBack]);
}
- (void)onCanGoForward:(FlutterMethodCall*)call result:(FlutterResult)result {
BOOL canGoForward = [_webView canGoForward];
result([NSNumber numberWithBool:canGoForward]);
}
- (void)onGoBack:(FlutterMethodCall*)call result:(FlutterResult)result {
[_webView goBack];
result(nil);
}
- (void)onGoForward:(FlutterMethodCall*)call result:(FlutterResult)result {
[_webView goForward];
result(nil);
}
- (void)onReload:(FlutterMethodCall*)call result:(FlutterResult)result {
[_webView reload];
result(nil);
}
- (void)onCurrentUrl:(FlutterMethodCall*)call result:(FlutterResult)result {
_currentUrl = [[_webView URL] absoluteString];
result(_currentUrl);
}
- (void)onEvaluateJavaScript:(FlutterMethodCall*)call result:(FlutterResult)result {
NSString* jsString = [call arguments];
if (!jsString) {
result([FlutterError errorWithCode:@"evaluateJavaScript_failed"
message:@"JavaScript String cannot be null"
details:nil]);
return;
}
[_webView evaluateJavaScript:jsString
completionHandler:^(_Nullable id evaluateResult, NSError* _Nullable error) {
if (error) {
result([FlutterError
errorWithCode:@"evaluateJavaScript_failed"
message:@"Failed evaluating JavaScript"
details:[NSString stringWithFormat:@"JavaScript string was: '%@'\n%@",
jsString, error]]);
} else {
result([NSString stringWithFormat:@"%@", evaluateResult]);
}
}];
}
- (void)onAddJavaScriptChannels:(FlutterMethodCall*)call result:(FlutterResult)result {
NSArray* channelNames = [call arguments];
NSSet* channelNamesSet = [[NSSet alloc] initWithArray:channelNames];
[_javaScriptChannelNames addObjectsFromArray:channelNames];
[self registerJavaScriptChannels:channelNamesSet
controller:_webView.configuration.userContentController];
result(nil);
}
- (void)onRemoveJavaScriptChannels:(FlutterMethodCall*)call result:(FlutterResult)result {
// WkWebView does not support removing a single user script, so instead we remove all
// user scripts, all message handlers. And re-register channels that shouldn't be removed.
[_webView.configuration.userContentController removeAllUserScripts];
for (NSString* channelName in _javaScriptChannelNames) {
[_webView.configuration.userContentController removeScriptMessageHandlerForName:channelName];
}
NSArray* channelNamesToRemove = [call arguments];
for (NSString* channelName in channelNamesToRemove) {
[_javaScriptChannelNames removeObject:channelName];
}
[self registerJavaScriptChannels:_javaScriptChannelNames
controller:_webView.configuration.userContentController];
result(nil);
}
- (void)clearCache:(FlutterResult)result {
if (@available(iOS 9.0, *)) {
NSSet* cacheDataTypes = [WKWebsiteDataStore allWebsiteDataTypes];
WKWebsiteDataStore* dataStore = [WKWebsiteDataStore defaultDataStore];
NSDate* dateFrom = [NSDate dateWithTimeIntervalSince1970:0];
[dataStore removeDataOfTypes:cacheDataTypes
modifiedSince:dateFrom
completionHandler:^{
result(nil);
}];
} else {
// support for iOS8 tracked in https://github.com/flutter/flutter/issues/27624.
NSLog(@"Clearing cache is not supported for Flutter WebViews prior to iOS 9.");
}
}
- (void)onGetTitle:(FlutterResult)result {
NSString* title = _webView.title;
result(title);
}
- (void)onScrollTo:(FlutterMethodCall*)call result:(FlutterResult)result {
NSDictionary* arguments = [call arguments];
int x = [arguments[@"x"] intValue];
int y = [arguments[@"y"] intValue];
_webView.scrollView.contentOffset = CGPointMake(x, y);
result(nil);
}
- (void)onScrollBy:(FlutterMethodCall*)call result:(FlutterResult)result {
CGPoint contentOffset = _webView.scrollView.contentOffset;
NSDictionary* arguments = [call arguments];
int x = [arguments[@"x"] intValue] + contentOffset.x;
int y = [arguments[@"y"] intValue] + contentOffset.y;
_webView.scrollView.contentOffset = CGPointMake(x, y);
result(nil);
}
- (void)getScrollX:(FlutterMethodCall*)call result:(FlutterResult)result {
int offsetX = _webView.scrollView.contentOffset.x;
result([NSNumber numberWithInt:offsetX]);
}
- (void)getScrollY:(FlutterMethodCall*)call result:(FlutterResult)result {
int offsetY = _webView.scrollView.contentOffset.y;
result([NSNumber numberWithInt:offsetY]);
}
// Returns nil when successful, or an error message when one or more keys are unknown.
- (NSString*)applySettings:(NSDictionary<NSString*, id>*)settings {
NSMutableArray<NSString*>* unknownKeys = [[NSMutableArray alloc] init];
for (NSString* key in settings) {
if ([key isEqualToString:@"jsMode"]) {
NSNumber* mode = settings[key];
[self updateJsMode:mode];
} else if ([key isEqualToString:@"hasNavigationDelegate"]) {
NSNumber* hasDartNavigationDelegate = settings[key];
_navigationDelegate.hasDartNavigationDelegate = [hasDartNavigationDelegate boolValue];
} else if ([key isEqualToString:@"debuggingEnabled"]) {
// no-op debugging is always enabled on iOS.
} else if ([key isEqualToString:@"gestureNavigationEnabled"]) {
NSNumber* allowsBackForwardNavigationGestures = settings[key];
_webView.allowsBackForwardNavigationGestures =
[allowsBackForwardNavigationGestures boolValue];
} else if ([key isEqualToString:@"userAgent"]) {
NSString* userAgent = settings[key];
[self updateUserAgent:[userAgent isEqual:[NSNull null]] ? nil : userAgent];
} else {
[unknownKeys addObject:key];
}
}
if ([unknownKeys count] == 0) {
return nil;
}
return [NSString stringWithFormat:@"webview_flutter: unknown setting keys: {%@}",
[unknownKeys componentsJoinedByString:@", "]];
}
- (void)updateJsMode:(NSNumber*)mode {
WKPreferences* preferences = [[_webView configuration] preferences];
switch ([mode integerValue]) {
case 0: // disabled
[preferences setJavaScriptEnabled:NO];
break;
case 1: // unrestricted
[preferences setJavaScriptEnabled:YES];
break;
default:
NSLog(@"webview_flutter: unknown JavaScript mode: %@", mode);
}
}
- (void)updateAutoMediaPlaybackPolicy:(NSNumber*)policy
inConfiguration:(WKWebViewConfiguration*)configuration {
switch ([policy integerValue]) {
case 0: // require_user_action_for_all_media_types
if (@available(iOS 10.0, *)) {
configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll;
} else {
configuration.mediaPlaybackRequiresUserAction = true;
}
break;
case 1: // always_allow
if (@available(iOS 10.0, *)) {
configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone;
} else {
configuration.mediaPlaybackRequiresUserAction = false;
}
break;
default:
NSLog(@"webview_flutter: unknown auto media playback policy: %@", policy);
}
}
- (bool)loadRequest:(NSDictionary<NSString*, id>*)request {
if (!request) {
return false;
}
NSString* url = request[@"url"];
if ([url isKindOfClass:[NSString class]]) {
id headers = request[@"headers"];
if ([headers isKindOfClass:[NSDictionary class]]) {
return [self loadUrl:url withHeaders:headers];
} else {
return [self loadUrl:url];
}
}
return false;
}
- (bool)loadUrl:(NSString*)url {
return [self loadUrl:url withHeaders:[NSMutableDictionary dictionary]];
}
- (bool)loadUrl:(NSString*)url withHeaders:(NSDictionary<NSString*, NSString*>*)headers {
NSURL* nsUrl = [NSURL URLWithString:url];
if (!nsUrl) {
return false;
}
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:nsUrl];
[request setAllHTTPHeaderFields:headers];
[_webView loadRequest:request];
return true;
}
- (void)registerJavaScriptChannels:(NSSet*)channelNames
controller:(WKUserContentController*)userContentController {
for (NSString* channelName in channelNames) {
FLTJavaScriptChannel* channel =
[[FLTJavaScriptChannel alloc] initWithMethodChannel:_channel
javaScriptChannelName:channelName];
[userContentController addScriptMessageHandler:channel name:channelName];
NSString* wrapperSource = [NSString
stringWithFormat:@"window.%@ = webkit.messageHandlers.%@;", channelName, channelName];
WKUserScript* wrapperScript =
[[WKUserScript alloc] initWithSource:wrapperSource
injectionTime:WKUserScriptInjectionTimeAtDocumentStart
forMainFrameOnly:NO];
[userContentController addUserScript:wrapperScript];
}
}
- (void)updateUserAgent:(NSString*)userAgent {
if (@available(iOS 9.0, *)) {
[_webView setCustomUserAgent:userAgent];
} else {
NSLog(@"Updating UserAgent is not supported for Flutter WebViews prior to iOS 9.");
}
}
#pragma mark WKUIDelegate
- (WKWebView*)webView:(WKWebView*)webView
createWebViewWithConfiguration:(WKWebViewConfiguration*)configuration
forNavigationAction:(WKNavigationAction*)navigationAction
windowFeatures:(WKWindowFeatures*)windowFeatures {
if (!navigationAction.targetFrame.isMainFrame) {
[webView loadRequest:navigationAction.request];
}
return nil;
}
@end
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <Flutter/Flutter.h>
#import <WebKit/WebKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface FLTJavaScriptChannel : NSObject <WKScriptMessageHandler>
- (instancetype)initWithMethodChannel:(FlutterMethodChannel*)methodChannel
javaScriptChannelName:(NSString*)javaScriptChannelName;
@end
NS_ASSUME_NONNULL_END
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "JavaScriptChannelHandler.h"
@implementation FLTJavaScriptChannel {
FlutterMethodChannel* _methodChannel;
NSString* _javaScriptChannelName;
}
- (instancetype)initWithMethodChannel:(FlutterMethodChannel*)methodChannel
javaScriptChannelName:(NSString*)javaScriptChannelName {
self = [super init];
NSAssert(methodChannel != nil, @"methodChannel must not be null.");
NSAssert(javaScriptChannelName != nil, @"javaScriptChannelName must not be null.");
if (self) {
_methodChannel = methodChannel;
_javaScriptChannelName = javaScriptChannelName;
}
return self;
}
- (void)userContentController:(WKUserContentController*)userContentController
didReceiveScriptMessage:(WKScriptMessage*)message {
NSAssert(_methodChannel != nil, @"Can't send a message to an unitialized JavaScript channel.");
NSAssert(_javaScriptChannelName != nil,
@"Can't send a message to an unitialized JavaScript channel.");
NSDictionary* arguments = @{
@"channel" : _javaScriptChannelName,
@"message" : [NSString stringWithFormat:@"%@", message.body]
};
[_methodChannel invokeMethod:@"javascriptChannelMessage" arguments:arguments];
}
@end
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
@import Flutter;
@import XCTest;
@import webview_flutter;
// OCMock library doesn't generate a valid modulemap.
#import <OCMock/OCMock.h>
@interface FLTWKNavigationDelegateTests : XCTestCase
@property(strong, nonatomic) FlutterMethodChannel *mockMethodChannel;
@property(strong, nonatomic) FLTWKNavigationDelegate *navigationDelegate;
@end
@implementation FLTWKNavigationDelegateTests
- (void)setUp {
self.mockMethodChannel = OCMClassMock(FlutterMethodChannel.class);
self.navigationDelegate =
[[FLTWKNavigationDelegate alloc] initWithChannel:self.mockMethodChannel];
}
- (void)testWebViewWebContentProcessDidTerminateCallsRecourseErrorChannel {
if (@available(iOS 9.0, *)) {
// `webViewWebContentProcessDidTerminate` is only available on iOS 9.0 and above.
WKWebView *webview = OCMClassMock(WKWebView.class);
[self.navigationDelegate webViewWebContentProcessDidTerminate:webview];
OCMVerify([self.mockMethodChannel
invokeMethod:@"onWebResourceError"
arguments:[OCMArg checkWithBlock:^BOOL(NSDictionary *args) {
XCTAssertEqualObjects(args[@"errorType"], @"webContentProcessTerminated");
return true;
}]]);
}
}
@end
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
@import Flutter;
@import XCTest;
@import webview_flutter;
// OCMock library doesn't generate a valid modulemap.
#import <OCMock/OCMock.h>
static bool feq(CGFloat a, CGFloat b) { return fabs(b - a) < FLT_EPSILON; }
@interface FLTWebViewTests : XCTestCase
@property(strong, nonatomic) NSObject<FlutterBinaryMessenger> *mockBinaryMessenger;
@end
@implementation FLTWebViewTests
- (void)setUp {
[super setUp];
self.mockBinaryMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
}
- (void)testCanInitFLTWebViewController {
FLTWebViewController *controller =
[[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400)
viewIdentifier:1
arguments:nil
binaryMessenger:self.mockBinaryMessenger];
XCTAssertNotNil(controller);
}
- (void)testCanInitFLTWebViewFactory {
FLTWebViewFactory *factory =
[[FLTWebViewFactory alloc] initWithMessenger:self.mockBinaryMessenger];
XCTAssertNotNil(factory);
}
- (void)webViewContentInsetBehaviorShouldBeNeverOnIOS11 {
if (@available(iOS 11, *)) {
FLTWebViewController *controller =
[[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400)
viewIdentifier:1
arguments:nil
binaryMessenger:self.mockBinaryMessenger];
UIView *view = controller.view;
XCTAssertTrue([view isKindOfClass:WKWebView.class]);
WKWebView *webView = (WKWebView *)view;
XCTAssertEqual(webView.scrollView.contentInsetAdjustmentBehavior,
UIScrollViewContentInsetAdjustmentNever);
}
}
- (void)testWebViewScrollIndicatorAticautomaticallyAdjustsScrollIndicatorInsetsShouldbeNoOnIOS13 {
if (@available(iOS 13, *)) {
FLTWebViewController *controller =
[[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400)
viewIdentifier:1
arguments:nil
binaryMessenger:self.mockBinaryMessenger];
UIView *view = controller.view;
XCTAssertTrue([view isKindOfClass:WKWebView.class]);
WKWebView *webView = (WKWebView *)view;
XCTAssertFalse(webView.scrollView.automaticallyAdjustsScrollIndicatorInsets);
}
}
- (void)testContentInsetsSumAlwaysZeroAfterSetFrame {
FLTWKWebView *webView = [[FLTWKWebView alloc] initWithFrame:CGRectMake(0, 0, 300, 400)];
webView.scrollView.contentInset = UIEdgeInsetsMake(0, 0, 300, 0);
XCTAssertFalse(UIEdgeInsetsEqualToEdgeInsets(webView.scrollView.contentInset, UIEdgeInsetsZero));
webView.frame = CGRectMake(0, 0, 300, 200);
XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(webView.scrollView.contentInset, UIEdgeInsetsZero));
XCTAssertTrue(CGRectEqualToRect(webView.frame, CGRectMake(0, 0, 300, 200)));
if (@available(iOS 11, *)) {
// After iOS 11, we need to make sure the contentInset compensates the adjustedContentInset.
UIScrollView *partialMockScrollView = OCMPartialMock(webView.scrollView);
UIEdgeInsets insetToAdjust = UIEdgeInsetsMake(0, 0, 300, 0);
OCMStub(partialMockScrollView.adjustedContentInset).andReturn(insetToAdjust);
XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(webView.scrollView.contentInset, UIEdgeInsetsZero));
webView.frame = CGRectMake(0, 0, 300, 100);
XCTAssertTrue(feq(webView.scrollView.contentInset.bottom, -insetToAdjust.bottom));
XCTAssertTrue(CGRectEqualToRect(webView.frame, CGRectMake(0, 0, 300, 100)));
}
}
@end
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
#
Pod::Spec.new do |s|
s.name = 'webview_flutter'
s.version = '0.0.1'
s.summary = 'A WebView Plugin for Flutter.'
s.description = <<-DESC
A Flutter plugin that provides a WebView widget.
Downloaded by pub (not CocoaPods).
DESC
s.homepage = 'https://github.com/flutter/plugins'
s.license = { :type => 'BSD', :file => '../LICENSE' }
s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/webview_flutter' }
s.documentation_url = 'https://pub.dev/packages/webview_flutter'
s.source_files = 'Classes/**/*'
s.public_header_files = 'Classes/**/*.h'
s.dependency 'Flutter'
s.platform = :ios, '8.0'
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' }
s.test_spec 'Tests' do |test_spec|
test_spec.source_files = 'Tests/**/*'
test_spec.dependency 'OCMock','3.5'
end
end
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
import 'webview_flutter.dart';
/// Interface for callbacks made by [WebViewPlatformController].
///
/// The webview plugin implements this class, and passes an instance to the [WebViewPlatformController].
/// [WebViewPlatformController] is notifying this handler on events that happened on the platform's webview.
abstract class WebViewPlatformCallbacksHandler {
/// Invoked by [WebViewPlatformController] when a JavaScript channel message is received.
void onJavaScriptChannelMessage(String channel, String message);
/// Invoked by [WebViewPlatformController] when a navigation request is pending.
///
/// If true is returned the navigation is allowed, otherwise it is blocked.
FutureOr<bool> onNavigationRequest({String url, bool isForMainFrame});
/// Invoked by [WebViewPlatformController] when a page has started loading.
void onPageStarted(String url);
/// Invoked by [WebViewPlatformController] when a page has finished loading.
void onPageFinished(String url);
/// Report web resource loading error to the host application.
void onWebResourceError(WebResourceError error);
}
/// Possible error type categorizations used by [WebResourceError].
enum WebResourceErrorType {
/// User authentication failed on server.
authentication,
/// Malformed URL.
badUrl,
/// Failed to connect to the server.
connect,
/// Failed to perform SSL handshake.
failedSslHandshake,
/// Generic file error.
file,
/// File not found.
fileNotFound,
/// Server or proxy hostname lookup failed.
hostLookup,
/// Failed to read or write to the server.
io,
/// User authentication failed on proxy.
proxyAuthentication,
/// Too many redirects.
redirectLoop,
/// Connection timed out.
timeout,
/// Too many requests during this load.
tooManyRequests,
/// Generic error.
unknown,
/// Resource load was canceled by Safe Browsing.
unsafeResource,
/// Unsupported authentication scheme (not basic or digest).
unsupportedAuthScheme,
/// Unsupported URI scheme.
unsupportedScheme,
/// The web content process was terminated.
webContentProcessTerminated,
/// The web view was invalidated.
webViewInvalidated,
/// A JavaScript exception occurred.
javaScriptExceptionOccurred,
/// The result of JavaScript execution could not be returned.
javaScriptResultTypeIsUnsupported,
}
/// Error returned in `WebView.onWebResourceError` when a web resource loading error has occurred.
class WebResourceError {
/// Creates a new [WebResourceError]
///
/// A user should not need to instantiate this class, but will receive one in
/// [WebResourceErrorCallback].
WebResourceError({
@required this.errorCode,
@required this.description,
this.domain,
this.errorType,
this.failingUrl,
}) : assert(errorCode != null),
assert(description != null);
/// Raw code of the error from the respective platform.
///
/// On Android, the error code will be a constant from a
/// [WebViewClient](https://developer.android.com/reference/android/webkit/WebViewClient#summary) and
/// will have a corresponding [errorType].
///
/// On iOS, the error code will be a constant from `NSError.code` in
/// Objective-C. See
/// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html
/// for more information on error handling on iOS. Some possible error codes
/// can be found at https://developer.apple.com/documentation/webkit/wkerrorcode?language=objc.
final int errorCode;
/// The domain of where to find the error code.
///
/// This field is only available on iOS and represents a "domain" from where
/// the [errorCode] is from. This value is taken directly from an `NSError`
/// in Objective-C. See
/// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html
/// for more information on error handling on iOS.
final String domain;
/// Description of the error that can be used to communicate the problem to the user.
final String description;
/// The type this error can be categorized as.
///
/// This will never be `null` on Android, but can be `null` on iOS.
final WebResourceErrorType errorType;
/// Gets the URL for which the resource request was made.
///
/// This value is not provided on iOS. Alternatively, you can keep track of
/// the last values provided to [WebViewPlatformController.loadUrl].
final String failingUrl;
}
/// Interface for talking to the webview's platform implementation.
///
/// An instance implementing this interface is passed to the `onWebViewPlatformCreated` callback that is
/// passed to [WebViewPlatformBuilder#onWebViewPlatformCreated].
///
/// Platform implementations that live in a separate package should extend this class rather than
/// implement it as webview_flutter does not consider newly added methods to be breaking changes.
/// Extending this class (using `extends`) ensures that the subclass will get the default
/// implementation, while platform implementations that `implements` this interface will be broken
/// by newly added [WebViewPlatformController] methods.
abstract class WebViewPlatformController {
/// Creates a new WebViewPlatform.
///
/// Callbacks made by the WebView will be delegated to `handler`.
///
/// The `handler` parameter must not be null.
WebViewPlatformController(WebViewPlatformCallbacksHandler handler);
/// 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,
) {
throw UnimplementedError(
"WebView loadUrl is not implemented on the current platform");
}
/// Updates the webview settings.
///
/// Any non null field in `settings` will be set as the new setting value.
/// All null fields in `settings` are ignored.
Future<void> updateSettings(WebSettings setting) {
throw UnimplementedError(
"WebView updateSettings is not implemented on the current platform");
}
/// Accessor to the current URL that the WebView is displaying.
///
/// If no URL was ever loaded, returns `null`.
Future<String> currentUrl() {
throw UnimplementedError(
"WebView currentUrl is not implemented on the current platform");
}
/// Checks whether there's a back history item.
Future<bool> canGoBack() {
throw UnimplementedError(
"WebView canGoBack is not implemented on the current platform");
}
/// Checks whether there's a forward history item.
Future<bool> canGoForward() {
throw UnimplementedError(
"WebView canGoForward is not implemented on the current platform");
}
/// Goes back in the history of this WebView.
///
/// If there is no back history item this is a no-op.
Future<void> goBack() {
throw UnimplementedError(
"WebView goBack is not implemented on the current platform");
}
/// Goes forward in the history of this WebView.
///
/// If there is no forward history item this is a no-op.
Future<void> goForward() {
throw UnimplementedError(
"WebView goForward is not implemented on the current platform");
}
/// Reloads the current URL.
Future<void> reload() {
throw UnimplementedError(
"WebView reload is not implemented on the current platform");
}
/// 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.
Future<void> clearCache() {
throw UnimplementedError(
"WebView clearCache is not implemented on the current platform");
}
/// Evaluates a JavaScript expression in the context of the current page.
///
/// The Future completes with an error if a JavaScript error occurred, or if the type of the
/// evaluated expression is not supported(e.g on iOS not all non primitive type can be evaluated).
Future<String> evaluateJavascript(String javascriptString) {
throw UnimplementedError(
"WebView evaluateJavascript is not implemented on the current platform");
}
/// Adds new JavaScript channels to the set of enabled channels.
///
/// For each value in this list the platform's webview should make sure that a corresponding
/// property with a postMessage method is set on `window`. For example for a JavaScript channel
/// named `Foo` it should be possible for JavaScript code executing in the webview to do
///
/// ```javascript
/// Foo.postMessage('hello');
/// ```
///
/// See also: [CreationParams.javascriptChannelNames].
Future<void> addJavascriptChannels(Set<String> javascriptChannelNames) {
throw UnimplementedError(
"WebView addJavascriptChannels is not implemented on the current platform");
}
/// Removes JavaScript channel names from the set of enabled channels.
///
/// This disables channels that were previously enabled by [addJavaScriptChannels] or through
/// [CreationParams.javascriptChannelNames].
Future<void> removeJavascriptChannels(Set<String> javascriptChannelNames) {
throw UnimplementedError(
"WebView removeJavascriptChannels is not implemented on the current platform");
}
/// Returns the title of the currently loaded page.
Future<String> getTitle() {
throw UnimplementedError(
"WebView getTitle is not implemented on the current platform");
}
/// Set the scrolled position of this view.
///
/// The parameters `x` and `y` specify the position to scroll to in WebView pixels.
Future<void> scrollTo(int x, int y) {
throw UnimplementedError(
"WebView scrollTo is not implemented on the current platform");
}
/// Move the scrolled position of this view.
///
/// The parameters `x` and `y` specify the amount of WebView pixels to scroll by.
Future<void> scrollBy(int x, int y) {
throw UnimplementedError(
"WebView scrollBy is not implemented on the current platform");
}
/// Return the horizontal scroll position of this view.
///
/// Scroll position is measured from left.
Future<int> getScrollX() {
throw UnimplementedError(
"WebView getScrollX is not implemented on the current platform");
}
/// Return the vertical scroll position of this view.
///
/// Scroll position is measured from top.
Future<int> getScrollY() {
throw UnimplementedError(
"WebView getScrollY is not implemented on the current platform");
}
}
/// A single setting for configuring a WebViewPlatform which may be absent.
class WebSetting<T> {
/// Constructs an absent setting instance.
///
/// The [isPresent] field for the instance will be false.
///
/// Accessing [value] for an absent instance will throw.
WebSetting.absent()
: _value = null,
isPresent = false;
/// Constructs a setting of the given `value`.
///
/// The [isPresent] field for the instance will be true.
WebSetting.of(T value)
: _value = value,
isPresent = true;
final T _value;
/// The setting's value.
///
/// Throws if [WebSetting.isPresent] is false.
T get value {
if (!isPresent) {
throw StateError('Cannot access a value of an absent WebSetting');
}
assert(isPresent);
return _value;
}
/// True when this web setting instance contains a value.
///
/// When false the [WebSetting.value] getter throws.
final bool isPresent;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
final WebSetting<T> typedOther = other;
return typedOther.isPresent == isPresent && typedOther._value == _value;
}
@override
int get hashCode => hashValues(_value, isPresent);
}
/// Settings for configuring a WebViewPlatform.
///
/// Initial settings are passed as part of [CreationParams], settings updates are sent with
/// [WebViewPlatform#updateSettings].
///
/// The `userAgent` parameter must not be null.
class WebSettings {
/// Construct an instance with initial settings. Future setting changes can be
/// sent with [WebviewPlatform#updateSettings].
///
/// The `userAgent` parameter must not be null.
WebSettings({
this.javascriptMode,
this.hasNavigationDelegate,
this.debuggingEnabled,
this.gestureNavigationEnabled,
@required this.userAgent,
}) : assert(userAgent != null);
/// The JavaScript execution mode to be used by the webview.
final JavascriptMode javascriptMode;
/// Whether the [WebView] has a [NavigationDelegate] set.
final bool hasNavigationDelegate;
/// Whether to enable the platform's webview content debugging tools.
///
/// See also: [WebView.debuggingEnabled].
final bool debuggingEnabled;
/// The value used for the HTTP `User-Agent:` request header.
///
/// If [userAgent.value] is null the platform's default user agent should be used.
///
/// An absent value ([userAgent.isPresent] is false) represents no change to this setting from the
/// last time it was set.
///
/// See also [WebView.userAgent].
final WebSetting<String> userAgent;
/// Whether to allow swipe based navigation in iOS.
///
/// See also: [WebView.gestureNavigationEnabled]
final bool gestureNavigationEnabled;
@override
String toString() {
return 'WebSettings(javascriptMode: $javascriptMode, hasNavigationDelegate: $hasNavigationDelegate, debuggingEnabled: $debuggingEnabled, gestureNavigationEnabled: $gestureNavigationEnabled, userAgent: $userAgent)';
}
}
/// Configuration to use when creating a new [WebViewPlatformController].
///
/// The `autoMediaPlaybackPolicy` parameter must not be null.
class CreationParams {
/// Constructs an instance to use when creating a new
/// [WebViewPlatformController].
///
/// The `autoMediaPlaybackPolicy` parameter must not be null.
CreationParams({
this.initialUrl,
this.webSettings,
this.javascriptChannelNames,
this.userAgent,
this.autoMediaPlaybackPolicy =
AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
}) : assert(autoMediaPlaybackPolicy != null);
/// The initialUrl to load in the webview.
///
/// When null the webview will be created without loading any page.
final String initialUrl;
/// The initial [WebSettings] for the new webview.
///
/// This can later be updated with [WebViewPlatformController.updateSettings].
final WebSettings webSettings;
/// The initial set of JavaScript channels that are configured for this webview.
///
/// For each value in this set the platform's webview should make sure that a corresponding
/// property with a postMessage method is set on `window`. For example for a JavaScript channel
/// named `Foo` it should be possible for JavaScript code executing in the webview to do
///
/// ```javascript
/// Foo.postMessage('hello');
/// ```
// TODO(amirh): describe what should happen when postMessage is called once that code is migrated
// to PlatformWebView.
final Set<String> javascriptChannelNames;
/// The value used for the HTTP User-Agent: request header.
///
/// When null the platform's webview default is used for the User-Agent header.
final String userAgent;
/// Which restrictions apply on automatic media playback.
final AutoMediaPlaybackPolicy autoMediaPlaybackPolicy;
@override
String toString() {
return '$runtimeType(initialUrl: $initialUrl, settings: $webSettings, javascriptChannelNames: $javascriptChannelNames, UserAgent: $userAgent)';
}
}
/// Signature for callbacks reporting that a [WebViewPlatformController] was created.
///
/// See also the `onWebViewPlatformCreated` argument for [WebViewPlatform.build].
typedef WebViewPlatformCreatedCallback = void Function(
WebViewPlatformController webViewPlatformController);
/// Interface for a platform implementation of a WebView.
///
/// [WebView.platform] controls the builder that is used by [WebView].
/// [AndroidWebViewPlatform] and [CupertinoWebViewPlatform] are the default implementations
/// for Android and iOS respectively.
abstract class WebViewPlatform {
/// Builds a new WebView.
///
/// Returns a Widget tree that embeds the created webview.
///
/// `creationParams` are the initial parameters used to setup the webview.
///
/// `webViewPlatformHandler` will be used for handling callbacks that are made by the created
/// [WebViewPlatformController].
///
/// `onWebViewPlatformCreated` will be invoked after the platform specific [WebViewPlatformController]
/// implementation is created with the [WebViewPlatformController] instance as a parameter.
///
/// `gestureRecognizers` specifies 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 `gestureRecognizers` is empty or null, the web view will only handle pointer events for gestures that
/// were not claimed by any other gesture recognizer.
///
/// `webViewPlatformHandler` must not be null.
Widget build({
BuildContext context,
// TODO(amirh): convert this to be the actual parameters.
// I'm starting without it as the PR is starting to become pretty big.
// I'll followup with the conversion PR.
CreationParams creationParams,
@required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler,
WebViewPlatformCreatedCallback onWebViewPlatformCreated,
Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers,
});
/// Clears all cookies for all [WebView] instances.
///
/// Returns true if cookies were present before clearing, else false.
Future<bool> clearCookies() {
throw UnimplementedError(
"WebView clearCookies is not implemented on the current platform");
}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import '../platform_interface.dart';
import 'webview_method_channel.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({
BuildContext context,
CreationParams creationParams,
@required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler,
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));
},
gestureRecognizers: gestureRecognizers,
// 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),
creationParamsCodec: const StandardMessageCodec(),
),
);
}
@override
Future<bool> clearCookies() => MethodChannelWebViewPlatform.clearCookies();
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import '../platform_interface.dart';
import 'webview_method_channel.dart';
/// Builds an iOS webview.
///
/// This is used as the default implementation for [WebView.platform] on iOS. It uses
/// a [UiKitView] to embed the webview in the widget hierarchy, and uses a method channel to
/// communicate with the platform code.
class CupertinoWebView implements WebViewPlatform {
@override
Widget build({
BuildContext context,
CreationParams creationParams,
@required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler,
WebViewPlatformCreatedCallback onWebViewPlatformCreated,
Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers,
}) {
return UiKitView(
viewType: 'plugins.flutter.io/webview',
onPlatformViewCreated: (int id) {
if (onWebViewPlatformCreated == null) {
return;
}
onWebViewPlatformCreated(
MethodChannelWebViewPlatform(id, webViewPlatformCallbacksHandler));
},
gestureRecognizers: gestureRecognizers,
creationParams:
MethodChannelWebViewPlatform.creationParamsToMap(creationParams),
creationParamsCodec: const StandardMessageCodec(),
);
}
@override
Future<bool> clearCookies() => MethodChannelWebViewPlatform.clearCookies();
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/services.dart';
import '../platform_interface.dart';
/// A [WebViewPlatformController] that uses a method channel to control the webview.
class MethodChannelWebViewPlatform implements WebViewPlatformController {
/// Constructs an instance that will listen for webviews broadcasting to the
/// given [id], using the given [WebViewPlatformCallbacksHandler].
MethodChannelWebViewPlatform(int id, this._platformCallbacksHandler)
: assert(_platformCallbacksHandler != null),
_channel = MethodChannel('plugins.flutter.io/webview_$id') {
_channel.setMethodCallHandler(_onMethodCall);
}
final WebViewPlatformCallbacksHandler _platformCallbacksHandler;
final MethodChannel _channel;
static const MethodChannel _cookieManagerChannel =
MethodChannel('plugins.flutter.io/cookie_manager');
Future<bool> _onMethodCall(MethodCall call) async {
switch (call.method) {
case 'javascriptChannelMessage':
final String channel = call.arguments['channel'];
final String message = call.arguments['message'];
_platformCallbacksHandler.onJavaScriptChannelMessage(channel, message);
return true;
case 'navigationRequest':
return await _platformCallbacksHandler.onNavigationRequest(
url: call.arguments['url'],
isForMainFrame: call.arguments['isForMainFrame'],
);
case 'onPageFinished':
_platformCallbacksHandler.onPageFinished(call.arguments['url']);
return null;
case 'onPageStarted':
_platformCallbacksHandler.onPageStarted(call.arguments['url']);
return null;
case 'onWebResourceError':
_platformCallbacksHandler.onWebResourceError(
WebResourceError(
errorCode: call.arguments['errorCode'],
description: call.arguments['description'],
domain: call.arguments['domain'],
failingUrl: call.arguments['failingUrl'],
errorType: call.arguments['errorType'] == null
? null
: WebResourceErrorType.values.firstWhere(
(WebResourceErrorType type) {
return type.toString() ==
'$WebResourceErrorType.${call.arguments['errorType']}';
},
),
),
);
return null;
}
throw MissingPluginException(
'${call.method} was invoked but has no handler',
);
}
@override
Future<void> loadUrl(
String url,
Map<String, String> headers,
) async {
assert(url != null);
return _channel.invokeMethod<void>('loadUrl', <String, dynamic>{
'url': url,
'headers': headers,
});
}
@override
Future<String> currentUrl() => _channel.invokeMethod<String>('currentUrl');
@override
Future<bool> canGoBack() => _channel.invokeMethod<bool>("canGoBack");
@override
Future<bool> canGoForward() => _channel.invokeMethod<bool>("canGoForward");
@override
Future<void> goBack() => _channel.invokeMethod<void>("goBack");
@override
Future<void> goForward() => _channel.invokeMethod<void>("goForward");
@override
Future<void> reload() => _channel.invokeMethod<void>("reload");
@override
Future<void> clearCache() => _channel.invokeMethod<void>("clearCache");
@override
Future<void> updateSettings(WebSettings settings) {
final Map<String, dynamic> updatesMap = _webSettingsToMap(settings);
if (updatesMap.isEmpty) {
return null;
}
return _channel.invokeMethod<void>('updateSettings', updatesMap);
}
@override
Future<String> evaluateJavascript(String javascriptString) {
return _channel.invokeMethod<String>(
'evaluateJavascript', javascriptString);
}
@override
Future<void> addJavascriptChannels(Set<String> javascriptChannelNames) {
return _channel.invokeMethod<void>(
'addJavascriptChannels', javascriptChannelNames.toList());
}
@override
Future<void> removeJavascriptChannels(Set<String> javascriptChannelNames) {
return _channel.invokeMethod<void>(
'removeJavascriptChannels', javascriptChannelNames.toList());
}
@override
Future<String> getTitle() => _channel.invokeMethod<String>("getTitle");
@override
Future<void> scrollTo(int x, int y) {
return _channel.invokeMethod<void>('scrollTo', <String, int>{
'x': x,
'y': y,
});
}
@override
Future<void> scrollBy(int x, int y) {
return _channel.invokeMethod<void>('scrollBy', <String, int>{
'x': x,
'y': y,
});
}
@override
Future<int> getScrollX() => _channel.invokeMethod<int>("getScrollX");
@override
Future<int> getScrollY() => _channel.invokeMethod<int>("getScrollY");
/// Method channel implementation for [WebViewPlatform.clearCookies].
static Future<bool> clearCookies() {
return _cookieManagerChannel
.invokeMethod<bool>('clearCookies')
.then<bool>((dynamic result) => result);
}
static Map<String, dynamic> _webSettingsToMap(WebSettings settings) {
final Map<String, dynamic> map = <String, dynamic>{};
void _addIfNonNull(String key, dynamic value) {
if (value == null) {
return;
}
map[key] = value;
}
void _addSettingIfPresent<T>(String key, WebSetting<T> setting) {
if (!setting.isPresent) {
return;
}
map[key] = setting.value;
}
_addIfNonNull('jsMode', settings.javascriptMode?.index);
_addIfNonNull('hasNavigationDelegate', settings.hasNavigationDelegate);
_addIfNonNull('debuggingEnabled', settings.debuggingEnabled);
_addIfNonNull(
'gestureNavigationEnabled', settings.gestureNavigationEnabled);
_addSettingIfPresent('userAgent', settings.userAgent);
return map;
}
/// Converts a [CreationParams] object to a map as expected by `platform_views` channel.
///
/// This is used for the `creationParams` argument of the platform views created by
/// [AndroidWebViewBuilder] and [CupertinoWebViewBuilder].
static Map<String, dynamic> creationParamsToMap(
CreationParams creationParams) {
return <String, dynamic>{
'initialUrl': creationParams.initialUrl,
'settings': _webSettingsToMap(creationParams.webSettings),
'javascriptChannelNames': creationParams.javascriptChannelNames.toList(),
'userAgent': creationParams.userAgent,
'autoMediaPlaybackPolicy': creationParams.autoMediaPlaybackPolicy.index,
};
}
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
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 'platform_interface.dart';
import 'src/webview_android.dart';
import 'src/webview_cupertino.dart';
import 'src/webview_method_channel.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);
/// Describes the state of JavaScript support in a given web view.
enum JavascriptMode {
/// JavaScript execution is disabled.
disabled,
/// JavaScript execution is not restricted.
unrestricted,
}
/// A message that was sent by JavaScript code running in a [WebView].
class JavascriptMessage {
/// Constructs a JavaScript message object.
///
/// The `message` parameter must not be null.
const JavascriptMessage(this.message) : assert(message != null);
/// The contents of the message that was sent by the JavaScript code.
final String message;
}
/// Callback type for handling messages sent from Javascript running in a web view.
typedef void JavascriptMessageHandler(JavascriptMessage message);
/// Information about a navigation action that is about to be executed.
class NavigationRequest {
NavigationRequest._({this.url, this.isForMainFrame});
/// The URL that will be loaded if the navigation is executed.
final String url;
/// Whether the navigation request is to be loaded as the main frame.
final bool isForMainFrame;
@override
String toString() {
return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)';
}
}
/// A decision on how to handle a navigation request.
enum NavigationDecision {
/// Prevent the navigation from taking place.
prevent,
/// Allow the navigation to take place.
navigate,
}
/// 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({
BuildContext context,
CreationParams creationParams,
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,
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,
),
creationParamsCodec: const StandardMessageCodec(),
)
..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
..addOnPlatformViewCreatedListener((int id) {
if (onWebViewPlatformCreated == null) {
return;
}
onWebViewPlatformCreated(
MethodChannelWebViewPlatform(id, webViewPlatformCallbacksHandler),
);
})
..create();
},
);
}
}
/// 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] has failed to load a resource.
typedef void WebResourceErrorCallback(WebResourceError error);
/// Specifies possible restrictions on automatic media playback.
///
/// This is typically used in [WebView.initialMediaPlaybackPolicy].
// The method channel implementation is marshalling this enum to the value's index, so the order
// is important.
enum AutoMediaPlaybackPolicy {
/// Starting any kind of media playback requires a user action.
///
/// For example: JavaScript code cannot start playing media unless the code was executed
/// as a result of a user action (like a touch event).
require_user_action_for_all_media_types,
/// Starting any kind of media playback is always allowed.
///
/// For example: JavaScript code that's triggered when the page is loaded can start playing
/// video or audio.
always_allow,
}
final RegExp _validChannelNames = RegExp('^[a-zA-Z_][a-zA-Z0-9_]*\$');
/// A named channel for receiving messaged from JavaScript code running inside a web view.
class JavascriptChannel {
/// Constructs a Javascript channel.
///
/// The parameters `name` and `onMessageReceived` must not be null.
JavascriptChannel({
@required this.name,
@required this.onMessageReceived,
}) : assert(name != null),
assert(onMessageReceived != null),
assert(_validChannelNames.hasMatch(name));
/// The channel's name.
///
/// Passing this channel object as part of a [WebView.javascriptChannels] adds a channel object to
/// the Javascript window object's property named `name`.
///
/// The name must start with a letter or underscore(_), followed by any combination of those
/// characters plus digits.
///
/// Note that any JavaScript existing `window` property with this name will be overriden.
///
/// See also [WebView.javascriptChannels] for more details on the channel registration mechanism.
final String name;
/// A callback that's invoked when a message is received through the channel.
final JavascriptMessageHandler onMessageReceived;
}
/// A web view widget for showing html content.
///
/// There is a known issue that on iOS 13.4 and 13.5, other flutter widgets covering
/// the `WebView` is not able to block the `WebView` from receiving touch events.
/// See https://github.com/flutter/flutter/issues/53490.
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.onWebResourceError,
this.debuggingEnabled = false,
this.gestureNavigationEnabled = false,
this.userAgent,
this.initialMediaPlaybackPolicy =
AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
}) : assert(javascriptMode != null),
assert(initialMediaPlaybackPolicy != null),
super(key: key);
static WebViewPlatform _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;
}
/// The WebView platform that's used by this WebView.
///
/// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS.
static WebViewPlatform get platform {
if (_platform == null) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
_platform = AndroidWebView();
break;
case TargetPlatform.iOS:
_platform = CupertinoWebView();
break;
default:
throw UnsupportedError(
"Trying to use the default webview implementation for $defaultTargetPlatform but there isn't a default one");
}
}
return _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;
/// 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 web resource has failed to load.
///
/// This can be called for any resource (iframe, image, etc.), not just 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
State<StatefulWidget> createState() => _WebViewState();
}
class _WebViewState extends State<WebView> {
final Completer<WebViewController> _controller =
Completer<WebViewController>();
_PlatformCallbacksHandler _platformCallbacksHandler;
@override
Widget build(BuildContext context) {
return WebView.platform.build(
context: context,
onWebViewPlatformCreated: _onWebViewPlatformCreated,
webViewPlatformCallbacksHandler: _platformCallbacksHandler,
gestureRecognizers: widget.gestureRecognizers,
creationParams: _creationParamsfromWidget(widget),
);
}
@override
void initState() {
super.initState();
_assertJavascriptChannelNamesAreUnique();
_platformCallbacksHandler = _PlatformCallbacksHandler(widget);
}
@override
void didUpdateWidget(WebView oldWidget) {
super.didUpdateWidget(oldWidget);
_assertJavascriptChannelNamesAreUnique();
_controller.future.then((WebViewController controller) {
_platformCallbacksHandler._widget = widget;
controller._updateWidget(widget);
});
}
void _onWebViewPlatformCreated(WebViewPlatformController webViewPlatform) {
final WebViewController controller =
WebViewController._(widget, webViewPlatform, _platformCallbacksHandler);
_controller.complete(controller);
if (widget.onWebViewCreated != null) {
widget.onWebViewCreated(controller);
}
}
void _assertJavascriptChannelNamesAreUnique() {
if (widget.javascriptChannels == null ||
widget.javascriptChannels.isEmpty) {
return;
}
assert(_extractChannelNames(widget.javascriptChannels).length ==
widget.javascriptChannels.length);
}
}
CreationParams _creationParamsfromWidget(WebView widget) {
return CreationParams(
initialUrl: widget.initialUrl,
webSettings: _webSettingsFromWidget(widget),
javascriptChannelNames: _extractChannelNames(widget.javascriptChannels),
userAgent: widget.userAgent,
autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy,
);
}
WebSettings _webSettingsFromWidget(WebView widget) {
return WebSettings(
javascriptMode: widget.javascriptMode,
hasNavigationDelegate: widget.navigationDelegate != null,
debuggingEnabled: widget.debuggingEnabled,
gestureNavigationEnabled: widget.gestureNavigationEnabled,
userAgent: WebSetting<String>.of(widget.userAgent),
);
}
// 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.debuggingEnabled != null);
assert(currentValue.userAgent.isPresent);
assert(newValue.javascriptMode != null);
assert(newValue.hasNavigationDelegate != null);
assert(newValue.debuggingEnabled != null);
assert(newValue.userAgent.isPresent);
JavascriptMode javascriptMode;
bool hasNavigationDelegate;
bool debuggingEnabled;
WebSetting<String> userAgent = WebSetting<String>.absent();
if (currentValue.javascriptMode != newValue.javascriptMode) {
javascriptMode = newValue.javascriptMode;
}
if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) {
hasNavigationDelegate = newValue.hasNavigationDelegate;
}
if (currentValue.debuggingEnabled != newValue.debuggingEnabled) {
debuggingEnabled = newValue.debuggingEnabled;
}
if (currentValue.userAgent != newValue.userAgent) {
userAgent = newValue.userAgent;
}
return WebSettings(
javascriptMode: javascriptMode,
hasNavigationDelegate: hasNavigationDelegate,
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;
}
class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler {
_PlatformCallbacksHandler(this._widget) {
_updateJavascriptChannelsFromSet(_widget.javascriptChannels);
}
WebView _widget;
// Maps a channel name to a channel.
final Map<String, JavascriptChannel> _javascriptChannels =
<String, JavascriptChannel>{};
@override
void onJavaScriptChannelMessage(String channel, String message) {
_javascriptChannels[channel].onMessageReceived(JavascriptMessage(message));
}
@override
FutureOr<bool> onNavigationRequest({String url, bool isForMainFrame}) async {
final NavigationRequest request =
NavigationRequest._(url: url, isForMainFrame: isForMainFrame);
final bool allowNavigation = _widget.navigationDelegate == null ||
await _widget.navigationDelegate(request) ==
NavigationDecision.navigate;
return allowNavigation;
}
@override
void onPageStarted(String url) {
if (_widget.onPageStarted != null) {
_widget.onPageStarted(url);
}
}
@override
void onPageFinished(String url) {
if (_widget.onPageFinished != null) {
_widget.onPageFinished(url);
}
}
@override
void onWebResourceError(WebResourceError error) {
if (_widget.onWebResourceError != null) {
_widget.onWebResourceError(error);
}
}
void _updateJavascriptChannelsFromSet(Set<JavascriptChannel> channels) {
_javascriptChannels.clear();
if (channels == null) {
return;
}
for (JavascriptChannel channel in channels) {
_javascriptChannels[channel.name] = channel;
}
}
}
/// Controls a [WebView].
///
/// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated]
/// callback for a [WebView] widget.
class WebViewController {
WebViewController._(
this._widget,
this._webViewPlatformController,
this._platformCallbacksHandler,
) : assert(_webViewPlatformController != null) {
_settings = _webSettingsFromWidget(_widget);
}
final WebViewPlatformController _webViewPlatformController;
final _PlatformCallbacksHandler _platformCallbacksHandler;
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();
}
Future<void> _updateWidget(WebView widget) async {
_widget = widget;
await _updateSettings(_webSettingsFromWidget(widget));
await _updateJavascriptChannels(widget.javascriptChannels);
}
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 =
_platformCallbacksHandler._javascriptChannels.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);
}
_platformCallbacksHandler._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.'));
}
if (javascriptString == null) {
return Future<String>.error(
ArgumentError('The argument javascriptString must not be null.'));
}
// 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();
}
}
/// Manages cookies pertaining to all [WebView]s.
class CookieManager {
/// Creates a [CookieManager] -- returns the instance if it's already been called.
factory CookieManager() {
return _instance ??= CookieManager._();
}
CookieManager._();
static CookieManager _instance;
/// Clears all cookies for all [WebView] instances.
///
/// This is a no op on iOS version smaller than 9.
///
/// Returns true if cookies were present before clearing, else false.
Future<bool> clearCookies() => WebView.platform.clearCookies();
}
// 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);
}
}
name: webview_flutter
description: A Flutter plugin that provides a WebView widget on Android and iOS.
version: 1.0.7
homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter
environment:
sdk: ">=2.7.0 <3.0.0"
flutter: ">=1.22.0 <2.0.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter_driver:
sdk: flutter
pedantic: ^1.8.0
flutter:
plugin:
platforms:
android:
package: io.flutter.plugins.webviewflutter
pluginClass: WebViewFlutterPlugin
ios:
pluginClass: FLTWebViewFlutterPlugin
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:flutter/src/foundation/basic_types.dart';
import 'package:flutter/src/gestures/recognizer.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:webview_flutter/platform_interface.dart';
import 'package:webview_flutter/webview_flutter.dart';
typedef void VoidCallback();
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final _FakePlatformViewsController fakePlatformViewsController =
_FakePlatformViewsController();
final _FakeCookieManager _fakeCookieManager = _FakeCookieManager();
setUpAll(() {
SystemChannels.platform_views.setMockMethodCallHandler(
fakePlatformViewsController.fakePlatformViewsMethodHandler);
SystemChannels.platform
.setMockMethodCallHandler(_fakeCookieManager.onMethodCall);
});
setUp(() {
fakePlatformViewsController.reset();
_fakeCookieManager.reset();
});
testWidgets('Create WebView', (WidgetTester tester) async {
await tester.pumpWidget(const WebView());
});
testWidgets('Initial url', (WidgetTester tester) async {
WebViewController controller;
await tester.pumpWidget(
WebView(
initialUrl: 'https://youtube.com',
onWebViewCreated: (WebViewController webViewController) {
controller = webViewController;
},
),
);
expect(await controller.currentUrl(), 'https://youtube.com');
});
testWidgets('Javascript mode', (WidgetTester tester) async {
await tester.pumpWidget(const WebView(
initialUrl: 'https://youtube.com',
javascriptMode: JavascriptMode.unrestricted,
));
final FakePlatformWebView platformWebView =
fakePlatformViewsController.lastCreatedView;
expect(platformWebView.javascriptMode, JavascriptMode.unrestricted);
await tester.pumpWidget(const WebView(
initialUrl: 'https://youtube.com',
javascriptMode: JavascriptMode.disabled,
));
expect(platformWebView.javascriptMode, JavascriptMode.disabled);
});
testWidgets('Load url', (WidgetTester tester) async {
WebViewController controller;
await tester.pumpWidget(
WebView(
onWebViewCreated: (WebViewController webViewController) {
controller = webViewController;
},
),
);
expect(controller, isNotNull);
await controller.loadUrl('https://flutter.io');
expect(await controller.currentUrl(), 'https://flutter.io');
});
testWidgets('Invalid urls', (WidgetTester tester) async {
WebViewController controller;
await tester.pumpWidget(
WebView(
onWebViewCreated: (WebViewController webViewController) {
controller = webViewController;
},
),
);
expect(controller, isNotNull);
expect(() => controller.loadUrl(null), throwsA(anything));
expect(await controller.currentUrl(), isNull);
expect(() => controller.loadUrl(''), throwsA(anything));
expect(await controller.currentUrl(), isNull);
// Missing schema.
expect(() => controller.loadUrl('flutter.io'), throwsA(anything));
expect(await controller.currentUrl(), isNull);
});
testWidgets('Headers in loadUrl', (WidgetTester tester) async {
WebViewController controller;
await tester.pumpWidget(
WebView(
onWebViewCreated: (WebViewController webViewController) {
controller = webViewController;
},
),
);
expect(controller, isNotNull);
final Map<String, String> headers = <String, String>{
'CACHE-CONTROL': 'ABC'
};
await controller.loadUrl('https://flutter.io', headers: headers);
expect(await controller.currentUrl(), equals('https://flutter.io'));
});
testWidgets("Can't go back before loading a page",
(WidgetTester tester) async {
WebViewController controller;
await tester.pumpWidget(
WebView(
onWebViewCreated: (WebViewController webViewController) {
controller = webViewController;
},
),
);
expect(controller, isNotNull);
final bool canGoBackNoPageLoaded = await controller.canGoBack();
expect(canGoBackNoPageLoaded, false);
});
testWidgets("Clear Cache", (WidgetTester tester) async {
WebViewController controller;
await tester.pumpWidget(
WebView(
onWebViewCreated: (WebViewController webViewController) {
controller = webViewController;
},
),
);
expect(controller, isNotNull);
expect(fakePlatformViewsController.lastCreatedView.hasCache, true);
await controller.clearCache();
expect(fakePlatformViewsController.lastCreatedView.hasCache, false);
});
testWidgets("Can't go back with no history", (WidgetTester tester) async {
WebViewController controller;
await tester.pumpWidget(
WebView(
initialUrl: 'https://flutter.io',
onWebViewCreated: (WebViewController webViewController) {
controller = webViewController;
},
),
);
expect(controller, isNotNull);
final bool canGoBackFirstPageLoaded = await controller.canGoBack();
expect(canGoBackFirstPageLoaded, false);
});
testWidgets('Can go back', (WidgetTester tester) async {
WebViewController controller;
await tester.pumpWidget(
WebView(
initialUrl: 'https://flutter.io',
onWebViewCreated: (WebViewController webViewController) {
controller = webViewController;
},
),
);
expect(controller, isNotNull);
await controller.loadUrl('https://www.google.com');
final bool canGoBackSecondPageLoaded = await controller.canGoBack();
expect(canGoBackSecondPageLoaded, true);
});
testWidgets("Can't go forward before loading a page",
(WidgetTester tester) async {
WebViewController controller;
await tester.pumpWidget(
WebView(
onWebViewCreated: (WebViewController webViewController) {
controller = webViewController;
},
),
);
expect(controller, isNotNull);
final bool canGoForwardNoPageLoaded = await controller.canGoForward();
expect(canGoForwardNoPageLoaded, false);
});
testWidgets("Can't go forward with no history", (WidgetTester tester) async {
WebViewController controller;
await tester.pumpWidget(
WebView(
initialUrl: 'https://flutter.io',
onWebViewCreated: (WebViewController webViewController) {
controller = webViewController;
},
),
);
expect(controller, isNotNull);
final bool canGoForwardFirstPageLoaded = await controller.canGoForward();
expect(canGoForwardFirstPageLoaded, false);
});
testWidgets('Can go forward', (WidgetTester tester) async {
WebViewController controller;
await tester.pumpWidget(
WebView(
initialUrl: 'https://flutter.io',
onWebViewCreated: (WebViewController webViewController) {
controller = webViewController;
},
),
);
expect(controller, isNotNull);
await controller.loadUrl('https://youtube.com');
await controller.goBack();
final bool canGoForwardFirstPageBacked = await controller.canGoForward();
expect(canGoForwardFirstPageBacked, true);
});
testWidgets('Go back', (WidgetTester tester) async {
WebViewController controller;
await tester.pumpWidget(
WebView(
initialUrl: 'https://youtube.com',
onWebViewCreated: (WebViewController webViewController) {
controller = webViewController;
},
),
);
expect(controller, isNotNull);
expect(await controller.currentUrl(), 'https://youtube.com');
await controller.loadUrl('https://flutter.io');
expect(await controller.currentUrl(), 'https://flutter.io');
await controller.goBack();
expect(await controller.currentUrl(), 'https://youtube.com');
});
testWidgets('Go forward', (WidgetTester tester) async {
WebViewController controller;
await tester.pumpWidget(
WebView(
initialUrl: 'https://youtube.com',
onWebViewCreated: (WebViewController webViewController) {
controller = webViewController;
},
),
);
expect(controller, isNotNull);
expect(await controller.currentUrl(), 'https://youtube.com');
await controller.loadUrl('https://flutter.io');
expect(await controller.currentUrl(), 'https://flutter.io');
await controller.goBack();
expect(await controller.currentUrl(), 'https://youtube.com');
await controller.goForward();
expect(await controller.currentUrl(), 'https://flutter.io');
});
testWidgets('Current URL', (WidgetTester tester) async {
WebViewController controller;
await tester.pumpWidget(
WebView(
onWebViewCreated: (WebViewController webViewController) {
controller = webViewController;
},
),
);
expect(controller, isNotNull);
// Test a WebView without an explicitly set first URL.
expect(await controller.currentUrl(), isNull);
await controller.loadUrl('https://youtube.com');
expect(await controller.currentUrl(), 'https://youtube.com');
await controller.loadUrl('https://flutter.io');
expect(await controller.currentUrl(), 'https://flutter.io');
await controller.goBack();
expect(await controller.currentUrl(), 'https://youtube.com');
});
testWidgets('Reload url', (WidgetTester tester) async {
WebViewController controller;
await tester.pumpWidget(
WebView(
initialUrl: 'https://flutter.io',
onWebViewCreated: (WebViewController webViewController) {
controller = webViewController;
},
),
);
final FakePlatformWebView platformWebView =
fakePlatformViewsController.lastCreatedView;
expect(platformWebView.currentUrl, 'https://flutter.io');
expect(platformWebView.amountOfReloadsOnCurrentUrl, 0);
await controller.reload();
expect(platformWebView.currentUrl, 'https://flutter.io');
expect(platformWebView.amountOfReloadsOnCurrentUrl, 1);
await controller.loadUrl('https://youtube.com');
expect(platformWebView.amountOfReloadsOnCurrentUrl, 0);
});
testWidgets('evaluate Javascript', (WidgetTester tester) async {
WebViewController controller;
await tester.pumpWidget(
WebView(
initialUrl: 'https://flutter.io',
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (WebViewController webViewController) {
controller = webViewController;
},
),
);
expect(
await controller.evaluateJavascript("fake js string"), "fake js string",
reason: 'should get the argument');
expect(
() => controller.evaluateJavascript(null),
throwsA(anything),
);
});
testWidgets('evaluate Javascript with JavascriptMode disabled',
(WidgetTester tester) async {
WebViewController controller;
await tester.pumpWidget(
WebView(
initialUrl: 'https://flutter.io',
javascriptMode: JavascriptMode.disabled,
onWebViewCreated: (WebViewController webViewController) {
controller = webViewController;
},
),
);
expect(
() => controller.evaluateJavascript('fake js string'),
throwsA(anything),
);
expect(
() => controller.evaluateJavascript(null),
throwsA(anything),
);
});
testWidgets('Cookies can be cleared once', (WidgetTester tester) async {
await tester.pumpWidget(
const WebView(
initialUrl: 'https://flutter.io',
),
);
final CookieManager cookieManager = CookieManager();
final bool hasCookies = await cookieManager.clearCookies();
expect(hasCookies, true);
});
testWidgets('Second cookie clear does not have cookies',
(WidgetTester tester) async {
await tester.pumpWidget(
const WebView(
initialUrl: 'https://flutter.io',
),
);
final CookieManager cookieManager = CookieManager();
final bool hasCookies = await cookieManager.clearCookies();
expect(hasCookies, true);
final bool hasCookiesSecond = await cookieManager.clearCookies();
expect(hasCookiesSecond, false);
});
testWidgets('Initial JavaScript channels', (WidgetTester tester) async {
await tester.pumpWidget(
WebView(
initialUrl: 'https://youtube.com',
// TODO(iskakaushik): Remove this when collection literals makes it to stable.
// ignore: prefer_collection_literals
javascriptChannels: <JavascriptChannel>[
JavascriptChannel(
name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
JavascriptChannel(
name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}),
].toSet(),
),
);
final FakePlatformWebView platformWebView =
fakePlatformViewsController.lastCreatedView;
expect(platformWebView.javascriptChannelNames,
unorderedEquals(<String>['Tts', 'Alarm']));
});
test('Only valid JavaScript channel names are allowed', () {
final JavascriptMessageHandler noOp = (JavascriptMessage msg) {};
JavascriptChannel(name: 'Tts1', onMessageReceived: noOp);
JavascriptChannel(name: '_Alarm', onMessageReceived: noOp);
JavascriptChannel(name: 'foo_bar_', onMessageReceived: noOp);
VoidCallback createChannel(String name) {
return () {
JavascriptChannel(name: name, onMessageReceived: noOp);
};
}
expect(createChannel('1Alarm'), throwsAssertionError);
expect(createChannel('foo.bar'), throwsAssertionError);
expect(createChannel(''), throwsAssertionError);
});
testWidgets('Unique JavaScript channel names are required',
(WidgetTester tester) async {
await tester.pumpWidget(
WebView(
initialUrl: 'https://youtube.com',
// TODO(iskakaushik): Remove this when collection literals makes it to stable.
// ignore: prefer_collection_literals
javascriptChannels: <JavascriptChannel>[
JavascriptChannel(
name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}),
JavascriptChannel(
name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}),
].toSet(),
),
);
expect(tester.takeException(), isNot(null));
});
testWidgets('JavaScript channels update', (WidgetTester tester) async {
await tester.pumpWidget(
WebView(
initialUrl: 'https://youtube.com',
// TODO(iskakaushik): Remove this when collection literals makes it to stable.
// ignore: prefer_collection_literals
javascriptChannels: <JavascriptChannel>[
JavascriptChannel(
name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
JavascriptChannel(
name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}),
].toSet(),
),
);
await tester.pumpWidget(
WebView(
initialUrl: 'https://youtube.com',
// TODO(iskakaushik): Remove this when collection literals makes it to stable.
// ignore: prefer_collection_literals
javascriptChannels: <JavascriptChannel>[
JavascriptChannel(
name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
JavascriptChannel(
name: 'Alarm2', onMessageReceived: (JavascriptMessage msg) {}),
JavascriptChannel(
name: 'Alarm3', onMessageReceived: (JavascriptMessage msg) {}),
].toSet(),
),
);
final FakePlatformWebView platformWebView =
fakePlatformViewsController.lastCreatedView;
expect(platformWebView.javascriptChannelNames,
unorderedEquals(<String>['Tts', 'Alarm2', 'Alarm3']));
});
testWidgets('Remove all JavaScript channels and then add',
(WidgetTester tester) async {
// This covers a specific bug we had where after updating javascriptChannels to null,
// updating it again with a subset of the previously registered channels fails as the
// widget's cache of current channel wasn't properly updated when updating javascriptChannels to
// null.
await tester.pumpWidget(
WebView(
initialUrl: 'https://youtube.com',
// TODO(iskakaushik): Remove this when collection literals makes it to stable.
// ignore: prefer_collection_literals
javascriptChannels: <JavascriptChannel>[
JavascriptChannel(
name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
].toSet(),
),
);
await tester.pumpWidget(
const WebView(
initialUrl: 'https://youtube.com',
),
);
await tester.pumpWidget(
WebView(
initialUrl: 'https://youtube.com',
// TODO(iskakaushik): Remove this when collection literals makes it to stable.
// ignore: prefer_collection_literals
javascriptChannels: <JavascriptChannel>[
JavascriptChannel(
name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
].toSet(),
),
);
final FakePlatformWebView platformWebView =
fakePlatformViewsController.lastCreatedView;
expect(platformWebView.javascriptChannelNames,
unorderedEquals(<String>['Tts']));
});
testWidgets('JavaScript channel messages', (WidgetTester tester) async {
final List<String> ttsMessagesReceived = <String>[];
final List<String> alarmMessagesReceived = <String>[];
await tester.pumpWidget(
WebView(
initialUrl: 'https://youtube.com',
// TODO(iskakaushik): Remove this when collection literals makes it to stable.
// ignore: prefer_collection_literals
javascriptChannels: <JavascriptChannel>[
JavascriptChannel(
name: 'Tts',
onMessageReceived: (JavascriptMessage msg) {
ttsMessagesReceived.add(msg.message);
}),
JavascriptChannel(
name: 'Alarm',
onMessageReceived: (JavascriptMessage msg) {
alarmMessagesReceived.add(msg.message);
}),
].toSet(),
),
);
final FakePlatformWebView platformWebView =
fakePlatformViewsController.lastCreatedView;
expect(ttsMessagesReceived, isEmpty);
expect(alarmMessagesReceived, isEmpty);
platformWebView.fakeJavascriptPostMessage('Tts', 'Hello');
platformWebView.fakeJavascriptPostMessage('Tts', 'World');
expect(ttsMessagesReceived, <String>['Hello', 'World']);
});
group('$PageStartedCallback', () {
testWidgets('onPageStarted is not null', (WidgetTester tester) async {
String returnedUrl;
await tester.pumpWidget(WebView(
initialUrl: 'https://youtube.com',
onPageStarted: (String url) {
returnedUrl = url;
},
));
final FakePlatformWebView platformWebView =
fakePlatformViewsController.lastCreatedView;
platformWebView.fakeOnPageStartedCallback();
expect(platformWebView.currentUrl, returnedUrl);
});
testWidgets('onPageStarted is null', (WidgetTester tester) async {
await tester.pumpWidget(const WebView(
initialUrl: 'https://youtube.com',
onPageStarted: null,
));
final FakePlatformWebView platformWebView =
fakePlatformViewsController.lastCreatedView;
// The platform side will always invoke a call for onPageStarted. This is
// to test that it does not crash on a null callback.
platformWebView.fakeOnPageStartedCallback();
});
testWidgets('onPageStarted changed', (WidgetTester tester) async {
String returnedUrl;
await tester.pumpWidget(WebView(
initialUrl: 'https://youtube.com',
onPageStarted: (String url) {},
));
await tester.pumpWidget(WebView(
initialUrl: 'https://youtube.com',
onPageStarted: (String url) {
returnedUrl = url;
},
));
final FakePlatformWebView platformWebView =
fakePlatformViewsController.lastCreatedView;
platformWebView.fakeOnPageStartedCallback();
expect(platformWebView.currentUrl, returnedUrl);
});
});
group('$PageFinishedCallback', () {
testWidgets('onPageFinished is not null', (WidgetTester tester) async {
String returnedUrl;
await tester.pumpWidget(WebView(
initialUrl: 'https://youtube.com',
onPageFinished: (String url) {
returnedUrl = url;
},
));
final FakePlatformWebView platformWebView =
fakePlatformViewsController.lastCreatedView;
platformWebView.fakeOnPageFinishedCallback();
expect(platformWebView.currentUrl, returnedUrl);
});
testWidgets('onPageFinished is null', (WidgetTester tester) async {
await tester.pumpWidget(const WebView(
initialUrl: 'https://youtube.com',
onPageFinished: null,
));
final FakePlatformWebView platformWebView =
fakePlatformViewsController.lastCreatedView;
// The platform side will always invoke a call for onPageFinished. This is
// to test that it does not crash on a null callback.
platformWebView.fakeOnPageFinishedCallback();
});
testWidgets('onPageFinished changed', (WidgetTester tester) async {
String returnedUrl;
await tester.pumpWidget(WebView(
initialUrl: 'https://youtube.com',
onPageFinished: (String url) {},
));
await tester.pumpWidget(WebView(
initialUrl: 'https://youtube.com',
onPageFinished: (String url) {
returnedUrl = url;
},
));
final FakePlatformWebView platformWebView =
fakePlatformViewsController.lastCreatedView;
platformWebView.fakeOnPageFinishedCallback();
expect(platformWebView.currentUrl, returnedUrl);
});
});
group('navigationDelegate', () {
testWidgets('hasNavigationDelegate', (WidgetTester tester) async {
await tester.pumpWidget(const WebView(
initialUrl: 'https://youtube.com',
));
final FakePlatformWebView platformWebView =
fakePlatformViewsController.lastCreatedView;
expect(platformWebView.hasNavigationDelegate, false);
await tester.pumpWidget(WebView(
initialUrl: 'https://youtube.com',
navigationDelegate: (NavigationRequest r) => null,
));
expect(platformWebView.hasNavigationDelegate, true);
});
testWidgets('Block navigation', (WidgetTester tester) async {
final List<NavigationRequest> navigationRequests = <NavigationRequest>[];
await tester.pumpWidget(WebView(
initialUrl: 'https://youtube.com',
navigationDelegate: (NavigationRequest request) {
navigationRequests.add(request);
// Only allow navigating to https://flutter.dev
return request.url == 'https://flutter.dev'
? NavigationDecision.navigate
: NavigationDecision.prevent;
}));
final FakePlatformWebView platformWebView =
fakePlatformViewsController.lastCreatedView;
expect(platformWebView.hasNavigationDelegate, true);
platformWebView.fakeNavigate('https://www.google.com');
// The navigation delegate only allows navigation to https://flutter.dev
// so we should still be in https://youtube.com.
expect(platformWebView.currentUrl, 'https://youtube.com');
expect(navigationRequests.length, 1);
expect(navigationRequests[0].url, 'https://www.google.com');
expect(navigationRequests[0].isForMainFrame, true);
platformWebView.fakeNavigate('https://flutter.dev');
await tester.pump();
expect(platformWebView.currentUrl, 'https://flutter.dev');
});
});
group('debuggingEnabled', () {
testWidgets('enable debugging', (WidgetTester tester) async {
await tester.pumpWidget(const WebView(
debuggingEnabled: true,
));
final FakePlatformWebView platformWebView =
fakePlatformViewsController.lastCreatedView;
expect(platformWebView.debuggingEnabled, true);
});
testWidgets('defaults to false', (WidgetTester tester) async {
await tester.pumpWidget(const WebView());
final FakePlatformWebView platformWebView =
fakePlatformViewsController.lastCreatedView;
expect(platformWebView.debuggingEnabled, false);
});
testWidgets('can be changed', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(WebView(key: key));
final FakePlatformWebView platformWebView =
fakePlatformViewsController.lastCreatedView;
await tester.pumpWidget(WebView(
key: key,
debuggingEnabled: true,
));
expect(platformWebView.debuggingEnabled, true);
await tester.pumpWidget(WebView(
key: key,
debuggingEnabled: false,
));
expect(platformWebView.debuggingEnabled, false);
});
});
group('Custom platform implementation', () {
setUpAll(() {
WebView.platform = MyWebViewPlatform();
});
tearDownAll(() {
WebView.platform = null;
});
testWidgets('creation', (WidgetTester tester) async {
await tester.pumpWidget(
const WebView(
initialUrl: 'https://youtube.com',
gestureNavigationEnabled: true,
),
);
final MyWebViewPlatform builder = WebView.platform;
final MyWebViewPlatformController platform = builder.lastPlatformBuilt;
expect(
platform.creationParams,
MatchesCreationParams(CreationParams(
initialUrl: 'https://youtube.com',
webSettings: WebSettings(
javascriptMode: JavascriptMode.disabled,
hasNavigationDelegate: false,
debuggingEnabled: false,
userAgent: WebSetting<String>.of(null),
gestureNavigationEnabled: true,
),
// TODO(iskakaushik): Remove this when collection literals makes it to stable.
// ignore: prefer_collection_literals
javascriptChannelNames: Set<String>(),
)));
});
testWidgets('loadUrl', (WidgetTester tester) async {
WebViewController controller;
await tester.pumpWidget(
WebView(
initialUrl: 'https://youtube.com',
onWebViewCreated: (WebViewController webViewController) {
controller = webViewController;
},
),
);
final MyWebViewPlatform builder = WebView.platform;
final MyWebViewPlatformController platform = builder.lastPlatformBuilt;
final Map<String, String> headers = <String, String>{
'header': 'value',
};
await controller.loadUrl('https://google.com', headers: headers);
expect(platform.lastUrlLoaded, 'https://google.com');
expect(platform.lastRequestHeaders, headers);
});
});
testWidgets('Set UserAgent', (WidgetTester tester) async {
await tester.pumpWidget(const WebView(
initialUrl: 'https://youtube.com',
javascriptMode: JavascriptMode.unrestricted,
));
final FakePlatformWebView platformWebView =
fakePlatformViewsController.lastCreatedView;
expect(platformWebView.userAgent, isNull);
await tester.pumpWidget(const WebView(
initialUrl: 'https://youtube.com',
javascriptMode: JavascriptMode.unrestricted,
userAgent: 'UA',
));
expect(platformWebView.userAgent, 'UA');
});
}
class FakePlatformWebView {
FakePlatformWebView(int id, Map<dynamic, dynamic> params) {
if (params.containsKey('initialUrl')) {
final String initialUrl = params['initialUrl'];
if (initialUrl != null) {
history.add(initialUrl);
currentPosition++;
}
}
if (params.containsKey('javascriptChannelNames')) {
javascriptChannelNames =
List<String>.from(params['javascriptChannelNames']);
}
javascriptMode = JavascriptMode.values[params['settings']['jsMode']];
hasNavigationDelegate =
params['settings']['hasNavigationDelegate'] ?? false;
debuggingEnabled = params['settings']['debuggingEnabled'];
userAgent = params['settings']['userAgent'];
channel = MethodChannel(
'plugins.flutter.io/webview_$id', const StandardMethodCodec());
channel.setMockMethodCallHandler(onMethodCall);
}
MethodChannel channel;
List<String> history = <String>[];
int currentPosition = -1;
int amountOfReloadsOnCurrentUrl = 0;
bool hasCache = true;
String get currentUrl => history.isEmpty ? null : history[currentPosition];
JavascriptMode javascriptMode;
List<String> javascriptChannelNames;
bool hasNavigationDelegate;
bool debuggingEnabled;
String userAgent;
Future<dynamic> onMethodCall(MethodCall call) {
switch (call.method) {
case 'loadUrl':
final Map<dynamic, dynamic> request = call.arguments;
_loadUrl(request['url']);
return Future<void>.sync(() {});
case 'updateSettings':
if (call.arguments['jsMode'] != null) {
javascriptMode = JavascriptMode.values[call.arguments['jsMode']];
}
if (call.arguments['hasNavigationDelegate'] != null) {
hasNavigationDelegate = call.arguments['hasNavigationDelegate'];
}
if (call.arguments['debuggingEnabled'] != null) {
debuggingEnabled = call.arguments['debuggingEnabled'];
}
userAgent = call.arguments['userAgent'];
break;
case 'canGoBack':
return Future<bool>.sync(() => currentPosition > 0);
break;
case 'canGoForward':
return Future<bool>.sync(() => currentPosition < history.length - 1);
break;
case 'goBack':
currentPosition = max(-1, currentPosition - 1);
return Future<void>.sync(() {});
break;
case 'goForward':
currentPosition = min(history.length - 1, currentPosition + 1);
return Future<void>.sync(() {});
case 'reload':
amountOfReloadsOnCurrentUrl++;
return Future<void>.sync(() {});
break;
case 'currentUrl':
return Future<String>.value(currentUrl);
break;
case 'evaluateJavascript':
return Future<dynamic>.value(call.arguments);
break;
case 'addJavascriptChannels':
final List<String> channelNames = List<String>.from(call.arguments);
javascriptChannelNames.addAll(channelNames);
break;
case 'removeJavascriptChannels':
final List<String> channelNames = List<String>.from(call.arguments);
javascriptChannelNames
.removeWhere((String channel) => channelNames.contains(channel));
break;
case 'clearCache':
hasCache = false;
return Future<void>.sync(() {});
}
return Future<void>.sync(() {});
}
void fakeJavascriptPostMessage(String jsChannel, String message) {
final StandardMethodCodec codec = const StandardMethodCodec();
final Map<String, dynamic> arguments = <String, dynamic>{
'channel': jsChannel,
'message': message
};
final ByteData data = codec
.encodeMethodCall(MethodCall('javascriptChannelMessage', arguments));
ServicesBinding.instance.defaultBinaryMessenger
.handlePlatformMessage(channel.name, data, (ByteData data) {});
}
// Fakes a main frame navigation that was initiated by the webview, e.g when
// the user clicks a link in the currently loaded page.
void fakeNavigate(String url) {
if (!hasNavigationDelegate) {
print('no navigation delegate');
_loadUrl(url);
return;
}
final StandardMethodCodec codec = const StandardMethodCodec();
final Map<String, dynamic> arguments = <String, dynamic>{
'url': url,
'isForMainFrame': true
};
final ByteData data =
codec.encodeMethodCall(MethodCall('navigationRequest', arguments));
ServicesBinding.instance.defaultBinaryMessenger
.handlePlatformMessage(channel.name, data, (ByteData data) {
final bool allow = codec.decodeEnvelope(data);
if (allow) {
_loadUrl(url);
}
});
}
void fakeOnPageStartedCallback() {
final StandardMethodCodec codec = const StandardMethodCodec();
final ByteData data = codec.encodeMethodCall(MethodCall(
'onPageStarted',
<dynamic, dynamic>{'url': currentUrl},
));
ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
channel.name,
data,
(ByteData data) {},
);
}
void fakeOnPageFinishedCallback() {
final StandardMethodCodec codec = const StandardMethodCodec();
final ByteData data = codec.encodeMethodCall(MethodCall(
'onPageFinished',
<dynamic, dynamic>{'url': currentUrl},
));
ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
channel.name,
data,
(ByteData data) {},
);
}
void _loadUrl(String url) {
history = history.sublist(0, currentPosition + 1);
history.add(url);
currentPosition++;
amountOfReloadsOnCurrentUrl = 0;
}
}
class _FakePlatformViewsController {
FakePlatformWebView lastCreatedView;
Future<dynamic> fakePlatformViewsMethodHandler(MethodCall call) {
switch (call.method) {
case 'create':
final Map<dynamic, dynamic> args = call.arguments;
final Map<dynamic, dynamic> params = _decodeParams(args['params']);
lastCreatedView = FakePlatformWebView(
args['id'],
params,
);
return Future<int>.sync(() => 1);
default:
return Future<void>.sync(() {});
}
}
void reset() {
lastCreatedView = null;
}
}
Map<dynamic, dynamic> _decodeParams(Uint8List paramsMessage) {
final ByteBuffer buffer = paramsMessage.buffer;
final ByteData messageBytes = buffer.asByteData(
paramsMessage.offsetInBytes,
paramsMessage.lengthInBytes,
);
return const StandardMessageCodec().decodeMessage(messageBytes);
}
class _FakeCookieManager {
_FakeCookieManager() {
final MethodChannel channel = const MethodChannel(
'plugins.flutter.io/cookie_manager',
StandardMethodCodec(),
);
channel.setMockMethodCallHandler(onMethodCall);
}
bool hasCookies = true;
Future<bool> onMethodCall(MethodCall call) {
switch (call.method) {
case 'clearCookies':
bool hadCookies = false;
if (hasCookies) {
hadCookies = true;
hasCookies = false;
}
return Future<bool>.sync(() {
return hadCookies;
});
break;
}
return Future<bool>.sync(() => null);
}
void reset() {
hasCookies = true;
}
}
class MyWebViewPlatform implements WebViewPlatform {
MyWebViewPlatformController lastPlatformBuilt;
@override
Widget build({
BuildContext context,
CreationParams creationParams,
@required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler,
@required WebViewPlatformCreatedCallback onWebViewPlatformCreated,
Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers,
}) {
assert(onWebViewPlatformCreated != null);
lastPlatformBuilt = MyWebViewPlatformController(
creationParams, gestureRecognizers, webViewPlatformCallbacksHandler);
onWebViewPlatformCreated(lastPlatformBuilt);
return Container();
}
@override
Future<bool> clearCookies() {
return Future<bool>.sync(() => null);
}
}
class MyWebViewPlatformController extends WebViewPlatformController {
MyWebViewPlatformController(this.creationParams, this.gestureRecognizers,
WebViewPlatformCallbacksHandler platformHandler)
: super(platformHandler);
CreationParams creationParams;
Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers;
String lastUrlLoaded;
Map<String, String> lastRequestHeaders;
@override
Future<void> loadUrl(String url, Map<String, String> headers) {
equals(1, 1);
lastUrlLoaded = url;
lastRequestHeaders = headers;
return null;
}
}
class MatchesWebSettings extends Matcher {
MatchesWebSettings(this._webSettings);
final WebSettings _webSettings;
@override
Description describe(Description description) =>
description.add('$_webSettings');
@override
bool matches(
covariant WebSettings webSettings, Map<dynamic, dynamic> matchState) {
return _webSettings.javascriptMode == webSettings.javascriptMode &&
_webSettings.hasNavigationDelegate ==
webSettings.hasNavigationDelegate &&
_webSettings.debuggingEnabled == webSettings.debuggingEnabled &&
_webSettings.gestureNavigationEnabled ==
webSettings.gestureNavigationEnabled &&
_webSettings.userAgent == webSettings.userAgent;
}
}
class MatchesCreationParams extends Matcher {
MatchesCreationParams(this._creationParams);
final CreationParams _creationParams;
@override
Description describe(Description description) =>
description.add('$_creationParams');
@override
bool matches(covariant CreationParams creationParams,
Map<dynamic, dynamic> matchState) {
return _creationParams.initialUrl == creationParams.initialUrl &&
MatchesWebSettings(_creationParams.webSettings)
.matches(creationParams.webSettings, matchState) &&
orderedEquals(_creationParams.javascriptChannelNames)
.matches(creationParams.javascriptChannelNames, matchState);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment