Commit 3695a070 by zhang

添加项目

parent e58b8acd
This diff is collapsed. Click to expand it.
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 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 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/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,
};
}
}
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
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