diff --git a/packages/webview_flutter/analysis_options.yaml b/packages/webview_flutter/analysis_options.yaml deleted file mode 100644 index cda4f6e153e6..000000000000 --- a/packages/webview_flutter/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/webview_flutter/webview_flutter/README.md b/packages/webview_flutter/webview_flutter/README.md deleted file mode 100644 index a1a98901affb..000000000000 --- a/packages/webview_flutter/webview_flutter/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# WebView for Flutter - -[![pub package](https://img.shields.io/pub/v/webview_flutter.svg)](https://pub.dev/packages/webview_flutter) - -A Flutter plugin that provides a WebView widget. - -On iOS the WebView widget is backed by a [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview); -On Android the WebView widget is backed by a [WebView](https://developer.android.com/reference/android/webkit/WebView). - -## Usage -Add `webview_flutter` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). If you are targeting Android, make sure to read the *Android Platform Views* section below to choose the platform view mode that best suits your needs. - -You can now include a WebView widget in your widget tree. See the -[WebView](https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebView-class.html) -widget's Dartdoc for more details on how to use the widget. - -## Android Platform Views -The WebView is relying on -[Platform Views](https://flutter.dev/docs/development/platform-integration/platform-views) to embed -the Android’s webview within the Flutter app. It supports two modes: *Virtual displays* (the current default) and *Hybrid composition*. - -Here are some points to consider when choosing between the two: - -* *Hybrid composition* mode has a built-in keyboard support while *Virtual displays* mode has multiple -[keyboard issues](https://github.com/flutter/flutter/issues?q=is%3Aopen+label%3Avd-only+label%3A%22p%3A+webview-keyboard%22) -* *Hybrid composition* mode requires Android SKD 19+ while *Virtual displays* mode requires Android SDK 20+ -* *Hybrid composition* mode has [performence limitations](https://flutter.dev/docs/development/platform-integration/platform-views#performance) when working on Android versions prior to Android 10 while *Virtual displays* is performant on all supported Android versions - -| | Hybrid composition | Virtual displays | -| --------------------------- | ------------------- | ---------------- | -| **Full keyboard supoport** | yes | no | -| **Android SDK support** | 19+ | 20+ | -| **Full performance** | Android 10+ | always | -| **The default** | no | yes | - -### Using Virtual displays - -The mode is currently enabled by default. You should however make sure to set the correct `minSdkVersion` in `android/app/build.gradle` (if it was previously lower than 20): - -```groovy -android { - defaultConfig { - minSdkVersion 20 - } -} -``` - - -### Using Hybrid Composition - -1. Set the correct `minSdkVersion` in `android/app/build.gradle` (if it was previously lower than 19): - - ```groovy - android { - defaultConfig { - minSdkVersion 19 - } - } - ``` - -2. Set `WebView.platform = SurfaceAndroidWebView();` in `initState()`. - For example: - - ```dart - import 'dart:io'; - - import 'package:webview_flutter/webview_flutter.dart'; - - class WebViewExample extends StatefulWidget { - @override - WebViewExampleState createState() => WebViewExampleState(); - } - - class WebViewExampleState extends State { - @override - void initState() { - super.initState(); - // Enable hybrid composition. - if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView(); - } - - @override - Widget build(BuildContext context) { - return WebView( - initialUrl: 'https://flutter.dev', - ); - } - } - ``` - -### Enable Material Components for Android - -To use Material Components when the user interacts with input elements in the WebView, -follow the steps described in the [Enabling Material Components instructions](https://flutter.dev/docs/deployment/android#enabling-material-components). diff --git a/packages/webview_flutter/webview_flutter/analysis_options.yaml b/packages/webview_flutter/webview_flutter/analysis_options.yaml new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../../analysis_options_legacy.yaml diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md new file mode 100644 index 000000000000..d0bd041d0ff6 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* Initial release. diff --git a/packages/webview_flutter/webview_flutter_android/LICENSE b/packages/webview_flutter/webview_flutter_android/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/webview_flutter/webview_flutter_android/README.md b/packages/webview_flutter/webview_flutter_android/README.md new file mode 100644 index 000000000000..6144596c4293 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/README.md @@ -0,0 +1,11 @@ +# webview\_flutter\_android + +The Android implementation of [`webview_flutter`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `webview_flutter` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/webview_flutter +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/webview_flutter/webview_flutter_android/analysis_options.yaml b/packages/webview_flutter/webview_flutter_android/analysis_options.yaml new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../../analysis_options_legacy.yaml diff --git a/packages/webview_flutter/webview_flutter_android/android/build.gradle b/packages/webview_flutter/webview_flutter_android/android/build.gradle new file mode 100644 index 000000000000..4a164317c60f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/build.gradle @@ -0,0 +1,57 @@ +group 'io.flutter.plugins.webviewflutter' +version '1.0-SNAPSHOT' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.3.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 29 + + defaultConfig { + minSdkVersion 19 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + lintOptions { + disable 'InvalidPackage' + disable 'GradleDependency' + } + + dependencies { + implementation 'androidx.annotation:annotation:1.0.0' + implementation 'androidx.webkit:webkit:1.0.0' + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-inline:3.11.1' + testImplementation 'androidx.test:core:1.3.0' + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/settings.gradle b/packages/webview_flutter/webview_flutter_android/android/settings.gradle new file mode 100644 index 000000000000..5be7a4b4c692 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'webview_flutter' diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/AndroidManifest.xml b/packages/webview_flutter/webview_flutter_android/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..a087f2c75c24 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java new file mode 100644 index 000000000000..31e3fe08c057 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java @@ -0,0 +1,147 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static android.hardware.display.DisplayManager.DisplayListener; + +import android.annotation.TargetApi; +import android.hardware.display.DisplayManager; +import android.os.Build; +import android.util.Log; +import java.lang.reflect.Field; +import java.util.ArrayList; + +/** + * Works around an Android WebView bug by filtering some DisplayListener invocations. + * + *

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. + * + *

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. + * + *

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 + * + *

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. + * + *

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 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 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 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 delegates = + (ArrayList) displayListenersField.get(displayManagerGlobal); + + Field listenerField = null; + ArrayList listeners = new ArrayList<>(); + for (Object delegate : delegates) { + if (listenerField == null) { + listenerField = delegate.getClass().getField("mListener"); + listenerField.setAccessible(true); + } + DisplayManager.DisplayListener listener = + (DisplayManager.DisplayListener) listenerField.get(delegate); + listeners.add(listener); + } + return listeners; + } catch (NoSuchFieldException | IllegalAccessException e) { + Log.w(TAG, "Could not extract WebView's display listeners. " + e); + return new ArrayList<>(); + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java new file mode 100644 index 000000000000..df3f21daadeb --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java @@ -0,0 +1,56 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.os.Build; +import android.os.Build.VERSION_CODES; +import android.webkit.CookieManager; +import android.webkit.ValueCallback; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; + +class FlutterCookieManager implements MethodCallHandler { + private final MethodChannel methodChannel; + + FlutterCookieManager(BinaryMessenger messenger) { + methodChannel = new MethodChannel(messenger, "plugins.flutter.io/cookie_manager"); + methodChannel.setMethodCallHandler(this); + } + + @Override + public void onMethodCall(MethodCall methodCall, Result result) { + switch (methodCall.method) { + case "clearCookies": + clearCookies(result); + break; + default: + result.notImplemented(); + } + } + + void dispose() { + methodChannel.setMethodCallHandler(null); + } + + private static void clearCookies(final Result result) { + CookieManager cookieManager = CookieManager.getInstance(); + final boolean hasCookies = cookieManager.hasCookies(); + if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + cookieManager.removeAllCookies( + new ValueCallback() { + @Override + public void onReceiveValue(Boolean value) { + result.success(hasCookies); + } + }); + } else { + cookieManager.removeAllCookie(); + result.success(hasCookies); + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java new file mode 100644 index 000000000000..a3b681f27980 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java @@ -0,0 +1,485 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.annotation.TargetApi; +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.os.Build; +import android.os.Handler; +import android.os.Message; +import android.view.View; +import android.webkit.WebChromeClient; +import android.webkit.WebResourceRequest; +import android.webkit.WebStorage; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugin.platform.PlatformView; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class FlutterWebView implements PlatformView, MethodCallHandler { + + private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames"; + private final WebView webView; + private final MethodChannel methodChannel; + private final FlutterWebViewClient flutterWebViewClient; + private final Handler platformThreadHandler; + + // Verifies that a url opened by `Window.open` has a secure url. + private class FlutterWebChromeClient extends WebChromeClient { + + @Override + public boolean onCreateWindow( + final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { + final WebViewClient webViewClient = + new WebViewClient() { + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading( + @NonNull WebView view, @NonNull WebResourceRequest request) { + final String url = request.getUrl().toString(); + if (!flutterWebViewClient.shouldOverrideUrlLoading( + FlutterWebView.this.webView, request)) { + webView.loadUrl(url); + } + return true; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (!flutterWebViewClient.shouldOverrideUrlLoading( + FlutterWebView.this.webView, url)) { + webView.loadUrl(url); + } + return true; + } + }; + + final WebView newWebView = new WebView(view.getContext()); + newWebView.setWebViewClient(webViewClient); + + final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; + transport.setWebView(newWebView); + resultMsg.sendToTarget(); + + return true; + } + + @Override + public void onProgressChanged(WebView view, int progress) { + flutterWebViewClient.onLoadingProgress(progress); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + @SuppressWarnings("unchecked") + FlutterWebView( + final Context context, + MethodChannel methodChannel, + Map params, + View containerView) { + + DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy(); + DisplayManager displayManager = + (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); + displayListenerProxy.onPreWebViewInitialization(displayManager); + + webView = + createWebView( + new WebViewBuilder(context, containerView), params, new FlutterWebChromeClient()); + + displayListenerProxy.onPostWebViewInitialization(displayManager); + + platformThreadHandler = new Handler(context.getMainLooper()); + + this.methodChannel = methodChannel; + this.methodChannel.setMethodCallHandler(this); + + flutterWebViewClient = new FlutterWebViewClient(methodChannel); + Map settings = (Map) params.get("settings"); + if (settings != null) { + applySettings(settings); + } + + if (params.containsKey(JS_CHANNEL_NAMES_FIELD)) { + List names = (List) params.get(JS_CHANNEL_NAMES_FIELD); + if (names != null) { + registerJavaScriptChannelNames(names); + } + } + + Integer autoMediaPlaybackPolicy = (Integer) params.get("autoMediaPlaybackPolicy"); + if (autoMediaPlaybackPolicy != null) { + updateAutoMediaPlaybackPolicy(autoMediaPlaybackPolicy); + } + if (params.containsKey("userAgent")) { + String userAgent = (String) params.get("userAgent"); + updateUserAgent(userAgent); + } + if (params.containsKey("initialUrl")) { + String url = (String) params.get("initialUrl"); + webView.loadUrl(url); + } + } + + /** + * Creates a {@link android.webkit.WebView} and configures it according to the supplied + * parameters. + * + *

The {@link WebView} is configured with the following predefined settings: + * + *

    + *
  • always enable the DOM storage API; + *
  • always allow JavaScript to automatically open windows; + *
  • always allow support for multiple windows; + *
  • always use the {@link FlutterWebChromeClient} as web Chrome client. + *
+ * + *

Important: This method is visible for testing purposes only and should + * never be called from outside this class. + * + * @param webViewBuilder a {@link WebViewBuilder} which is responsible for building the {@link + * WebView}. + * @param params creation parameters received over the method channel. + * @param webChromeClient an implementation of WebChromeClient This value may be null. + * @return The new {@link android.webkit.WebView} object. + */ + @VisibleForTesting + static WebView createWebView( + WebViewBuilder webViewBuilder, Map params, WebChromeClient webChromeClient) { + boolean usesHybridComposition = Boolean.TRUE.equals(params.get("usesHybridComposition")); + webViewBuilder + .setUsesHybridComposition(usesHybridComposition) + .setDomStorageEnabled(true) // Always enable DOM storage API. + .setJavaScriptCanOpenWindowsAutomatically( + true) // Always allow automatically opening of windows. + .setSupportMultipleWindows(true) // Always support multiple windows. + .setWebChromeClient( + webChromeClient); // Always use {@link FlutterWebChromeClient} as web Chrome client. + + return webViewBuilder.build(); + } + + @Override + public View getView() { + return webView; + } + + // @Override + // This is overriding a method that hasn't rolled into stable Flutter yet. Including the + // annotation would cause compile time failures in versions of Flutter too old to include the new + // method. However leaving it raw like this means that the method will be ignored in old versions + // of Flutter but used as an override anyway wherever it's actually defined. + // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. + public void onInputConnectionUnlocked() { + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).unlockInputConnection(); + } + } + + // @Override + // This is overriding a method that hasn't rolled into stable Flutter yet. Including the + // annotation would cause compile time failures in versions of Flutter too old to include the new + // method. However leaving it raw like this means that the method will be ignored in old versions + // of Flutter but used as an override anyway wherever it's actually defined. + // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. + public void onInputConnectionLocked() { + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).lockInputConnection(); + } + } + + // @Override + // This is overriding a method that hasn't rolled into stable Flutter yet. Including the + // annotation would cause compile time failures in versions of Flutter too old to include the new + // method. However leaving it raw like this means that the method will be ignored in old versions + // of Flutter but used as an override anyway wherever it's actually defined. + // TODO(mklim): Add the @Override annotation once stable passes v1.10.9. + public void onFlutterViewAttached(View flutterView) { + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).setContainerView(flutterView); + } + } + + // @Override + // This is overriding a method that hasn't rolled into stable Flutter yet. Including the + // annotation would cause compile time failures in versions of Flutter too old to include the new + // method. However leaving it raw like this means that the method will be ignored in old versions + // of Flutter but used as an override anyway wherever it's actually defined. + // TODO(mklim): Add the @Override annotation once stable passes v1.10.9. + public void onFlutterViewDetached() { + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).setContainerView(null); + } + } + + @Override + public void onMethodCall(MethodCall methodCall, Result result) { + switch (methodCall.method) { + case "loadUrl": + loadUrl(methodCall, result); + break; + case "updateSettings": + updateSettings(methodCall, result); + break; + case "canGoBack": + canGoBack(result); + break; + case "canGoForward": + canGoForward(result); + break; + case "goBack": + goBack(result); + break; + case "goForward": + goForward(result); + break; + case "reload": + reload(result); + break; + case "currentUrl": + currentUrl(result); + break; + case "evaluateJavascript": + evaluateJavaScript(methodCall, result); + break; + case "addJavascriptChannels": + addJavaScriptChannels(methodCall, result); + break; + case "removeJavascriptChannels": + removeJavaScriptChannels(methodCall, result); + break; + case "clearCache": + clearCache(result); + break; + case "getTitle": + getTitle(result); + break; + case "scrollTo": + scrollTo(methodCall, result); + break; + case "scrollBy": + scrollBy(methodCall, result); + break; + case "getScrollX": + getScrollX(result); + break; + case "getScrollY": + getScrollY(result); + break; + default: + result.notImplemented(); + } + } + + @SuppressWarnings("unchecked") + private void loadUrl(MethodCall methodCall, Result result) { + Map request = (Map) methodCall.arguments; + String url = (String) request.get("url"); + Map headers = (Map) 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) 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() { + @Override + public void onReceiveValue(String value) { + result.success(value); + } + }); + } + + @SuppressWarnings("unchecked") + private void addJavaScriptChannels(MethodCall methodCall, Result result) { + List channelNames = (List) methodCall.arguments; + registerJavaScriptChannelNames(channelNames); + result.success(null); + } + + @SuppressWarnings("unchecked") + private void removeJavaScriptChannels(MethodCall methodCall, Result result) { + List channelNames = (List) 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 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 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 settings) { + for (String key : settings.keySet()) { + switch (key) { + case "jsMode": + Integer mode = (Integer) settings.get(key); + if (mode != null) { + updateJsMode(mode); + } + break; + case "hasNavigationDelegate": + final boolean hasNavigationDelegate = (boolean) settings.get(key); + + final WebViewClient webViewClient = + flutterWebViewClient.createWebViewClient(hasNavigationDelegate); + + webView.setWebViewClient(webViewClient); + break; + case "debuggingEnabled": + final boolean debuggingEnabled = (boolean) settings.get(key); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + webView.setWebContentsDebuggingEnabled(debuggingEnabled); + } + break; + case "hasProgressTracking": + flutterWebViewClient.hasProgressTracking = (boolean) settings.get(key); + break; + case "gestureNavigationEnabled": + break; + case "userAgent": + updateUserAgent((String) settings.get(key)); + break; + case "allowsInlineMediaPlayback": + // no-op inline media playback is always allowed on Android. + break; + default: + throw new IllegalArgumentException("Unknown WebView setting: " + key); + } + } + } + + private void updateJsMode(int mode) { + switch (mode) { + case 0: // disabled + webView.getSettings().setJavaScriptEnabled(false); + break; + case 1: // unrestricted + webView.getSettings().setJavaScriptEnabled(true); + break; + default: + throw new IllegalArgumentException("Trying to set unknown JavaScript mode: " + mode); + } + } + + private void updateAutoMediaPlaybackPolicy(int mode) { + // This is the index of the AutoMediaPlaybackPolicy enum, index 1 is always_allow, for all + // other values we require a user gesture. + boolean requireUserGesture = mode != 1; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + webView.getSettings().setMediaPlaybackRequiresUserGesture(requireUserGesture); + } + } + + private void registerJavaScriptChannelNames(List channelNames) { + for (String channelName : channelNames) { + webView.addJavascriptInterface( + new JavaScriptChannel(methodChannel, channelName, platformThreadHandler), channelName); + } + } + + private void updateUserAgent(String userAgent) { + webView.getSettings().setUserAgentString(userAgent); + } + + @Override + public void dispose() { + methodChannel.setMethodCallHandler(null); + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).dispose(); + } + webView.destroy(); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java new file mode 100644 index 000000000000..adc84671a701 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java @@ -0,0 +1,307 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.os.Build; +import android.util.Log; +import android.view.KeyEvent; +import android.webkit.WebResourceError; +import android.webkit.WebResourceRequest; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.webkit.WebResourceErrorCompat; +import androidx.webkit.WebViewClientCompat; +import io.flutter.plugin.common.MethodChannel; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +// We need to use WebViewClientCompat to get +// shouldOverrideUrlLoading(WebView view, WebResourceRequest request) +// invoked by the webview on older Android devices, without it pages that use iframes will +// be broken when a navigationDelegate is set on Android version earlier than N. +class FlutterWebViewClient { + private static final String TAG = "FlutterWebViewClient"; + private final MethodChannel methodChannel; + private boolean hasNavigationDelegate; + boolean hasProgressTracking; + + FlutterWebViewClient(MethodChannel methodChannel) { + this.methodChannel = methodChannel; + } + + static String errorCodeToString(int errorCode) { + switch (errorCode) { + case WebViewClient.ERROR_AUTHENTICATION: + return "authentication"; + case WebViewClient.ERROR_BAD_URL: + return "badUrl"; + case WebViewClient.ERROR_CONNECT: + return "connect"; + case WebViewClient.ERROR_FAILED_SSL_HANDSHAKE: + return "failedSslHandshake"; + case WebViewClient.ERROR_FILE: + return "file"; + case WebViewClient.ERROR_FILE_NOT_FOUND: + return "fileNotFound"; + case WebViewClient.ERROR_HOST_LOOKUP: + return "hostLookup"; + case WebViewClient.ERROR_IO: + return "io"; + case WebViewClient.ERROR_PROXY_AUTHENTICATION: + return "proxyAuthentication"; + case WebViewClient.ERROR_REDIRECT_LOOP: + return "redirectLoop"; + case WebViewClient.ERROR_TIMEOUT: + return "timeout"; + case WebViewClient.ERROR_TOO_MANY_REQUESTS: + return "tooManyRequests"; + case WebViewClient.ERROR_UNKNOWN: + return "unknown"; + case WebViewClient.ERROR_UNSAFE_RESOURCE: + return "unsafeResource"; + case WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME: + return "unsupportedAuthScheme"; + case WebViewClient.ERROR_UNSUPPORTED_SCHEME: + return "unsupportedScheme"; + } + + final String message = + String.format(Locale.getDefault(), "Could not find a string for errorCode: %d", errorCode); + throw new IllegalArgumentException(message); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + if (!hasNavigationDelegate) { + return false; + } + notifyOnNavigationRequest( + request.getUrl().toString(), request.getRequestHeaders(), view, request.isForMainFrame()); + // We must make a synchronous decision here whether to allow the navigation or not, + // if the Dart code has set a navigation delegate we want that delegate to decide whether + // to navigate or not, and as we cannot get a response from the Dart delegate synchronously we + // return true here to block the navigation, if the Dart delegate decides to allow the + // navigation the plugin will later make an addition loadUrl call for this url. + // + // Since we cannot call loadUrl for a subframe, we currently only allow the delegate to stop + // navigations that target the main frame, if the request is not for the main frame + // we just return false to allow the navigation. + // + // For more details see: https://github.com/flutter/flutter/issues/25329#issuecomment-464863209 + return request.isForMainFrame(); + } + + boolean shouldOverrideUrlLoading(WebView view, String url) { + if (!hasNavigationDelegate) { + return false; + } + // This version of shouldOverrideUrlLoading is only invoked by the webview on devices with + // webview versions earlier than 67(it is also invoked when hasNavigationDelegate is false). + // On these devices we cannot tell whether the navigation is targeted to the main frame or not. + // We proceed assuming that the navigation is targeted to the main frame. If the page had any + // frames they will be loaded in the main frame instead. + Log.w( + TAG, + "Using a navigationDelegate with an old webview implementation, pages with frames or iframes will not work"); + notifyOnNavigationRequest(url, null, view, true); + return true; + } + + private void onPageStarted(WebView view, String url) { + Map args = new HashMap<>(); + args.put("url", url); + methodChannel.invokeMethod("onPageStarted", args); + } + + private void onPageFinished(WebView view, String url) { + Map args = new HashMap<>(); + args.put("url", url); + methodChannel.invokeMethod("onPageFinished", args); + } + + void onLoadingProgress(int progress) { + if (hasProgressTracking) { + Map args = new HashMap<>(); + args.put("progress", progress); + methodChannel.invokeMethod("onProgress", args); + } + } + + private void onWebResourceError( + final int errorCode, final String description, final String failingUrl) { + final Map 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 headers, WebView webview, boolean isMainFrame) { + HashMap args = new HashMap<>(); + args.put("url", url); + args.put("isForMainFrame", isMainFrame); + if (isMainFrame) { + methodChannel.invokeMethod( + "navigationRequest", args, new OnNavigationRequestResult(url, headers, webview)); + } else { + methodChannel.invokeMethod("navigationRequest", args); + } + } + + // This method attempts to avoid using WebViewClientCompat due to bug + // https://bugs.chromium.org/p/chromium/issues/detail?id=925887. Also, see + // https://github.com/flutter/flutter/issues/29446. + WebViewClient createWebViewClient(boolean hasNavigationDelegate) { + this.hasNavigationDelegate = hasNavigationDelegate; + + if (!hasNavigationDelegate || android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return internalCreateWebViewClient(); + } + + return internalCreateWebViewClientCompat(); + } + + private WebViewClient internalCreateWebViewClient() { + return new WebViewClient() { + @TargetApi(Build.VERSION_CODES.N) + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request); + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + FlutterWebViewClient.this.onPageStarted(view, url); + } + + @Override + public void onPageFinished(WebView view, String url) { + FlutterWebViewClient.this.onPageFinished(view, url); + } + + @TargetApi(Build.VERSION_CODES.M) + @Override + public void onReceivedError( + WebView view, WebResourceRequest request, WebResourceError error) { + if (request.isForMainFrame()) { + FlutterWebViewClient.this.onWebResourceError( + error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); + } + } + + @Override + public void onReceivedError( + WebView view, int errorCode, String description, String failingUrl) { + FlutterWebViewClient.this.onWebResourceError(errorCode, description, failingUrl); + } + + @Override + public void onUnhandledKeyEvent(WebView view, KeyEvent event) { + // Deliberately empty. Occasionally the webview will mark events as having failed to be + // handled even though they were handled. We don't want to propagate those as they're not + // truly lost. + } + }; + } + + private WebViewClientCompat internalCreateWebViewClientCompat() { + return new WebViewClientCompat() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, request); + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + return FlutterWebViewClient.this.shouldOverrideUrlLoading(view, url); + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + FlutterWebViewClient.this.onPageStarted(view, url); + } + + @Override + public void onPageFinished(WebView view, String url) { + FlutterWebViewClient.this.onPageFinished(view, url); + } + + // This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is + // enabled. The deprecated method is called when a device doesn't support this. + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @SuppressLint("RequiresFeature") + @Override + public void onReceivedError( + @NonNull WebView view, + @NonNull WebResourceRequest request, + @NonNull WebResourceErrorCompat error) { + if (request.isForMainFrame()) { + FlutterWebViewClient.this.onWebResourceError( + error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); + } + } + + @Override + public void onReceivedError( + WebView view, int errorCode, String description, String failingUrl) { + FlutterWebViewClient.this.onWebResourceError(errorCode, description, failingUrl); + } + + @Override + public void onUnhandledKeyEvent(WebView view, KeyEvent event) { + // Deliberately empty. Occasionally the webview will mark events as having failed to be + // handled even though they were handled. We don't want to propagate those as they're not + // truly lost. + } + }; + } + + private static class OnNavigationRequestResult implements MethodChannel.Result { + private final String url; + private final Map headers; + private final WebView webView; + + private OnNavigationRequestResult(String url, Map headers, WebView webView) { + this.url = url; + this.headers = headers; + this.webView = webView; + } + + @Override + public void success(Object shouldLoad) { + Boolean typedShouldLoad = (Boolean) shouldLoad; + if (typedShouldLoad) { + loadUrl(); + } + } + + @Override + public void error(String errorCode, String s1, Object o) { + throw new IllegalStateException("navigationRequest calls must succeed"); + } + + @Override + public void notImplemented() { + throw new IllegalStateException( + "navigationRequest must be implemented by the webview method channel"); + } + + private void loadUrl() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + webView.loadUrl(url, headers); + } else { + webView.loadUrl(url); + } + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java new file mode 100644 index 000000000000..8fe58104a0fb --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java @@ -0,0 +1,33 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.content.Context; +import android.view.View; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.StandardMessageCodec; +import io.flutter.plugin.platform.PlatformView; +import io.flutter.plugin.platform.PlatformViewFactory; +import java.util.Map; + +public final class FlutterWebViewFactory extends PlatformViewFactory { + private final BinaryMessenger messenger; + private final View containerView; + + FlutterWebViewFactory(BinaryMessenger messenger, View containerView) { + super(StandardMessageCodec.INSTANCE); + this.messenger = messenger; + this.containerView = containerView; + } + + @SuppressWarnings("unchecked") + @Override + public PlatformView create(Context context, int id, Object args) { + Map params = (Map) args; + MethodChannel methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id); + return new FlutterWebView(context, methodChannel, params, containerView); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java new file mode 100644 index 000000000000..51b2a3809fff --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java @@ -0,0 +1,233 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static android.content.Context.INPUT_METHOD_SERVICE; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Build; +import android.util.Log; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.webkit.WebView; +import android.widget.ListPopupWindow; + +/** + * A WebView subclass that mirrors the same implementation hacks that the system WebView does in + * order to correctly create an InputConnection. + * + *

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}. + * + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

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}. + * + *

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 + * + *

{@code targetView} should have a {@link View#getHandler} method with the thread that future + * InputConnections should be created on. + */ + private void setInputConnectionTarget(final View targetView) { + if (containerView == null) { + Log.e( + TAG, + "Can't set the input connection target because there is no containerView to use as a handler."); + return; + } + + targetView.requestFocus(); + containerView.post( + new Runnable() { + @Override + public void run() { + InputMethodManager imm = + (InputMethodManager) getContext().getSystemService(INPUT_METHOD_SERVICE); + // This is a hack to make InputMethodManager believe that the target view now has focus. + // As a result, InputMethodManager will think that targetView is focused, and will call + // getHandler() of the view when creating input connection. + + // Step 1: Set targetView as InputMethodManager#mNextServedView. This does not affect + // the real window focus. + targetView.onWindowFocusChanged(true); + + // Step 2: Have InputMethodManager focus in on targetView. As a result, IMM will call + // onCreateInputConnection() on targetView on the same thread as + // targetView.getHandler(). It will also call subsequent InputConnection methods on this + // thread. This is the IME thread in cases where targetView is our proxyAdapterView. + imm.isActive(containerView); + } + }); + } + + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + // This works around a crash when old (<67.0.3367.0) Chromium versions are used. + + // Prior to Chromium 67.0.3367 the following sequence happens when a select drop down is shown + // on tablets: + // + // - WebView is calling ListPopupWindow#show + // - buildDropDown is invoked, which sets mDropDownList to a DropDownListView. + // - showAsDropDown is invoked - resulting in mDropDownList being added to the window and is + // also synchronously performing the following sequence: + // - WebView's focus change listener is loosing focus (as mDropDownList got it) + // - WebView is hiding all popups (as it lost focus) + // - WebView's SelectPopupDropDown#hide is invoked. + // - DropDownPopupWindow#dismiss is invoked setting mDropDownList to null. + // - mDropDownList#setSelection is invoked and is throwing a NullPointerException (as we just set mDropDownList to null). + // + // To workaround this, we drop the problematic focus lost call. + // See more details on: https://github.com/flutter/flutter/issues/54164 + // + // We don't do this after Android P as it shipped with a new enough WebView version, and it's + // better to not do this on all future Android versions in case DropDownListView's code changes. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P + && isCalledFromListPopupWindowShow() + && !focused) { + return; + } + super.onFocusChanged(focused, direction, previouslyFocusedRect); + } + + private boolean isCalledFromListPopupWindowShow() { + StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); + for (StackTraceElement stackTraceElement : stackTraceElements) { + if (stackTraceElement.getClassName().equals(ListPopupWindow.class.getCanonicalName()) + && stackTraceElement.getMethodName().equals("show")) { + return true; + } + } + return false; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java new file mode 100644 index 000000000000..4d596351b3d0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java @@ -0,0 +1,58 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.os.Handler; +import android.os.Looper; +import android.webkit.JavascriptInterface; +import io.flutter.plugin.common.MethodChannel; +import java.util.HashMap; + +/** + * Added as a JavaScript interface to the WebView for any JavaScript channel that the Dart code sets + * up. + * + *

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 arguments = new HashMap<>(); + arguments.put("channel", javaScriptChannelName); + arguments.put("message", message); + methodChannel.invokeMethod("javascriptChannelMessage", arguments); + } + }; + if (platformThreadHandler.getLooper() == Looper.myLooper()) { + postMessageRunnable.run(); + } else { + platformThreadHandler.post(postMessageRunnable); + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java new file mode 100644 index 000000000000..1c865c9444e2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java @@ -0,0 +1,112 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.os.Handler; +import android.os.IBinder; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; + +/** + * A fake View only exposed to InputMethodManager. + * + *

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. + * + *

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)}. + * + *

Delegates to ThreadedInputConnectionProxyView to get WebView's input connection. + */ + @Override + public InputConnection onCreateInputConnection(final EditorInfo outAttrs) { + triggerDelayed = false; + InputConnection inputConnection = + (isLocked) ? cachedConnection : targetView.onCreateInputConnection(outAttrs); + triggerDelayed = true; + cachedConnection = inputConnection; + return inputConnection; + } + + @Override + public boolean checkInputConnectionProxy(View view) { + return true; + } + + @Override + public boolean hasWindowFocus() { + // None of our views here correctly report they have window focus because of how we're embedding + // the platform view inside of a virtual display. + return true; + } + + @Override + public View getRootView() { + return rootView; + } + + @Override + public boolean onCheckIsTextEditor() { + return true; + } + + @Override + public boolean isFocused() { + return true; + } + + @Override + public IBinder getWindowToken() { + return windowToken; + } + + @Override + public Handler getHandler() { + return imeHandler; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java new file mode 100644 index 000000000000..6b8cc51febe8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java @@ -0,0 +1,141 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.content.Context; +import android.view.View; +import android.webkit.WebChromeClient; +import android.webkit.WebSettings; +import android.webkit.WebView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** Builder used to create {@link android.webkit.WebView} objects. */ +public class WebViewBuilder { + + /** Factory used to create a new {@link android.webkit.WebView} instance. */ + static class WebViewFactory { + + /** + * Creates a new {@link android.webkit.WebView} instance. + * + * @param context an Activity Context to access application assets. This value cannot be null. + * @param usesHybridComposition If {@code false} a {@link InputAwareWebView} instance is + * returned. + * @param containerView must be supplied when the {@code useHybridComposition} parameter is set + * to {@code false}. Used to create an InputConnection on the WebView's dedicated input, or + * IME, thread (see also {@link InputAwareWebView}) + * @return A new instance of the {@link android.webkit.WebView} object. + */ + static WebView create(Context context, boolean usesHybridComposition, View containerView) { + return usesHybridComposition + ? new WebView(context) + : new InputAwareWebView(context, containerView); + } + } + + private final Context context; + private final View containerView; + + private boolean enableDomStorage; + private boolean javaScriptCanOpenWindowsAutomatically; + private boolean supportMultipleWindows; + private boolean usesHybridComposition; + private WebChromeClient webChromeClient; + + /** + * Constructs a new {@link WebViewBuilder} object with a custom implementation of the {@link + * WebViewFactory} object. + * + * @param context an Activity Context to access application assets. This value cannot be null. + * @param containerView must be supplied when the {@code useHybridComposition} parameter is set to + * {@code false}. Used to create an InputConnection on the WebView's dedicated input, or IME, + * thread (see also {@link InputAwareWebView}) + */ + WebViewBuilder(@NonNull final Context context, View containerView) { + this.context = context; + this.containerView = containerView; + } + + /** + * Sets whether the DOM storage API is enabled. The default value is {@code false}. + * + * @param flag {@code true} is {@link android.webkit.WebView} should use the DOM storage API. + * @return This builder. This value cannot be {@code null}. + */ + public WebViewBuilder setDomStorageEnabled(boolean flag) { + this.enableDomStorage = flag; + return this; + } + + /** + * Sets whether JavaScript is allowed to open windows automatically. This applies to the + * JavaScript function {@code window.open()}. The default value is {@code false}. + * + * @param flag {@code true} if JavaScript is allowed to open windows automatically. + * @return This builder. This value cannot be {@code null}. + */ + public WebViewBuilder setJavaScriptCanOpenWindowsAutomatically(boolean flag) { + this.javaScriptCanOpenWindowsAutomatically = flag; + return this; + } + + /** + * Sets whether the {@link WebView} supports multiple windows. If set to {@code true}, {@link + * WebChromeClient#onCreateWindow} must be implemented by the host application. The default is + * {@code false}. + * + * @param flag {@code true} if multiple windows are supported. + * @return This builder. This value cannot be {@code null}. + */ + public WebViewBuilder setSupportMultipleWindows(boolean flag) { + this.supportMultipleWindows = flag; + return this; + } + + /** + * Sets whether the hybrid composition should be used. + * + *

If set to {@code true} a standard {@link WebView} is created. If set to {@code false} the + * {@link WebViewBuilder} will create a {@link InputAwareWebView} to workaround issues using the + * {@link WebView} on Android versions below N. + * + * @param flag {@code true} if uses hybrid composition. The default is {@code false}. + * @return This builder. This value cannot be {@code null} + */ + public WebViewBuilder setUsesHybridComposition(boolean flag) { + this.usesHybridComposition = flag; + return this; + } + + /** + * Sets the chrome handler. This is an implementation of WebChromeClient for use in handling + * JavaScript dialogs, favicons, titles, and the progress. This will replace the current handler. + * + * @param webChromeClient an implementation of WebChromeClient This value may be null. + * @return This builder. This value cannot be {@code null}. + */ + public WebViewBuilder setWebChromeClient(@Nullable WebChromeClient webChromeClient) { + this.webChromeClient = webChromeClient; + return this; + } + + /** + * Build the {@link android.webkit.WebView} using the current settings. + * + * @return The {@link android.webkit.WebView} using the current settings. + */ + public WebView build() { + WebView webView = WebViewFactory.create(context, usesHybridComposition, containerView); + + WebSettings webSettings = webView.getSettings(); + webSettings.setDomStorageEnabled(enableDomStorage); + webSettings.setJavaScriptCanOpenWindowsAutomatically(javaScriptCanOpenWindowsAutomatically); + webSettings.setSupportMultipleWindows(supportMultipleWindows); + webView.setWebChromeClient(webChromeClient); + + return webView; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java new file mode 100644 index 000000000000..268d35a1e04c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java @@ -0,0 +1,73 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.BinaryMessenger; + +/** + * Java platform implementation of the webview_flutter plugin. + * + *

Register this in an add to app scenario to gracefully handle activity and context changes. + * + *

Call {@link #registerWith(Registrar)} to use the stable {@code io.flutter.plugin.common} + * package instead. + */ +public class WebViewFlutterPlugin implements FlutterPlugin { + + private FlutterCookieManager flutterCookieManager; + + /** + * Add an instance of this to {@link io.flutter.embedding.engine.plugins.PluginRegistry} to + * register it. + * + *

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. + * + *

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. + * + *

Calling this automatically initializes the plugin. However plugins initialized this way + * won't react to changes in activity or context, unlike {@link CameraPlugin}. + */ + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { + registrar + .platformViewRegistry() + .registerViewFactory( + "plugins.flutter.io/webview", + new FlutterWebViewFactory(registrar.messenger(), registrar.view())); + new FlutterCookieManager(registrar.messenger()); + } + + @Override + public void onAttachedToEngine(FlutterPluginBinding binding) { + BinaryMessenger messenger = binding.getBinaryMessenger(); + binding + .getPlatformViewRegistry() + .registerViewFactory( + "plugins.flutter.io/webview", + new FlutterWebViewFactory(messenger, /*containerView=*/ null)); + flutterCookieManager = new FlutterCookieManager(messenger); + } + + @Override + public void onDetachedFromEngine(FlutterPluginBinding binding) { + if (flutterCookieManager == null) { + return; + } + + flutterCookieManager.dispose(); + flutterCookieManager = null; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java new file mode 100644 index 000000000000..96cbdece387c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java @@ -0,0 +1,61 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.webkit.WebChromeClient; +import android.webkit.WebView; +import java.util.HashMap; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; + +public class FlutterWebViewTest { + private WebChromeClient mockWebChromeClient; + private WebViewBuilder mockWebViewBuilder; + private WebView mockWebView; + + @Before + public void before() { + mockWebChromeClient = mock(WebChromeClient.class); + mockWebViewBuilder = mock(WebViewBuilder.class); + mockWebView = mock(WebView.class); + + when(mockWebViewBuilder.setDomStorageEnabled(anyBoolean())).thenReturn(mockWebViewBuilder); + when(mockWebViewBuilder.setJavaScriptCanOpenWindowsAutomatically(anyBoolean())) + .thenReturn(mockWebViewBuilder); + when(mockWebViewBuilder.setSupportMultipleWindows(anyBoolean())).thenReturn(mockWebViewBuilder); + when(mockWebViewBuilder.setUsesHybridComposition(anyBoolean())).thenReturn(mockWebViewBuilder); + when(mockWebViewBuilder.setWebChromeClient(any(WebChromeClient.class))) + .thenReturn(mockWebViewBuilder); + + when(mockWebViewBuilder.build()).thenReturn(mockWebView); + } + + @Test + public void createWebView_should_create_webview_with_default_configuration() { + FlutterWebView.createWebView( + mockWebViewBuilder, createParameterMap(false), mockWebChromeClient); + + verify(mockWebViewBuilder, times(1)).setDomStorageEnabled(true); + verify(mockWebViewBuilder, times(1)).setJavaScriptCanOpenWindowsAutomatically(true); + verify(mockWebViewBuilder, times(1)).setSupportMultipleWindows(true); + verify(mockWebViewBuilder, times(1)).setUsesHybridComposition(false); + verify(mockWebViewBuilder, times(1)).setWebChromeClient(mockWebChromeClient); + } + + private Map createParameterMap(boolean usesHybridComposition) { + Map params = new HashMap<>(); + params.put("usesHybridComposition", usesHybridComposition); + + return params; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java new file mode 100644 index 000000000000..48fbce231ed5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java @@ -0,0 +1,99 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.*; + +import android.content.Context; +import android.view.View; +import android.webkit.WebChromeClient; +import android.webkit.WebSettings; +import android.webkit.WebView; +import io.flutter.plugins.webviewflutter.WebViewBuilder.WebViewFactory; +import java.io.IOException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.MockedStatic.Verification; + +public class WebViewBuilderTest { + private Context mockContext; + private View mockContainerView; + private WebView mockWebView; + private MockedStatic mockedStaticWebViewFactory; + + @Before + public void before() { + mockContext = mock(Context.class); + mockContainerView = mock(View.class); + mockWebView = mock(WebView.class); + mockedStaticWebViewFactory = mockStatic(WebViewFactory.class); + + mockedStaticWebViewFactory + .when( + new Verification() { + @Override + public void apply() { + WebViewFactory.create(mockContext, false, mockContainerView); + } + }) + .thenReturn(mockWebView); + } + + @After + public void after() { + mockedStaticWebViewFactory.close(); + } + + @Test + public void ctor_test() { + WebViewBuilder builder = new WebViewBuilder(mockContext, mockContainerView); + + assertNotNull(builder); + } + + @Test + public void build_should_set_values() throws IOException { + WebSettings mockWebSettings = mock(WebSettings.class); + WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); + + when(mockWebView.getSettings()).thenReturn(mockWebSettings); + + WebViewBuilder builder = + new WebViewBuilder(mockContext, mockContainerView) + .setDomStorageEnabled(true) + .setJavaScriptCanOpenWindowsAutomatically(true) + .setSupportMultipleWindows(true) + .setWebChromeClient(mockWebChromeClient); + + WebView webView = builder.build(); + + assertNotNull(webView); + verify(mockWebSettings).setDomStorageEnabled(true); + verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(true); + verify(mockWebSettings).setSupportMultipleWindows(true); + verify(mockWebView).setWebChromeClient(mockWebChromeClient); + } + + @Test + public void build_should_use_default_values() throws IOException { + WebSettings mockWebSettings = mock(WebSettings.class); + WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); + + when(mockWebView.getSettings()).thenReturn(mockWebSettings); + + WebViewBuilder builder = new WebViewBuilder(mockContext, mockContainerView); + + WebView webView = builder.build(); + + assertNotNull(webView); + verify(mockWebSettings).setDomStorageEnabled(false); + verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(false); + verify(mockWebSettings).setSupportMultipleWindows(false); + verify(mockWebView).setWebChromeClient(null); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java new file mode 100644 index 000000000000..131a5a3eb53a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java @@ -0,0 +1,49 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertEquals; + +import android.webkit.WebViewClient; +import org.junit.Test; + +public class WebViewTest { + @Test + public void errorCodes() { + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_AUTHENTICATION), + "authentication"); + assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_BAD_URL), "badUrl"); + assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_CONNECT), "connect"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FAILED_SSL_HANDSHAKE), + "failedSslHandshake"); + assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FILE), "file"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FILE_NOT_FOUND), "fileNotFound"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_HOST_LOOKUP), "hostLookup"); + assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_IO), "io"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_PROXY_AUTHENTICATION), + "proxyAuthentication"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_REDIRECT_LOOP), "redirectLoop"); + assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_TIMEOUT), "timeout"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_TOO_MANY_REQUESTS), + "tooManyRequests"); + assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNKNOWN), "unknown"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSAFE_RESOURCE), + "unsafeResource"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME), + "unsupportedAuthScheme"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSUPPORTED_SCHEME), + "unsupportedScheme"); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/example/.gitignore b/packages/webview_flutter/webview_flutter_android/example/.gitignore new file mode 100644 index 000000000000..0fa6b675c0a5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/webview_flutter/webview_flutter_android/example/.metadata b/packages/webview_flutter/webview_flutter_android/example/.metadata new file mode 100644 index 000000000000..56bfc2c4d6be --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: f4abaa0735eba4dfd8f33f73363911d63931fe03 + channel: stable + +project_type: app diff --git a/packages/webview_flutter/webview_flutter_android/example/README.md b/packages/webview_flutter/webview_flutter_android/example/README.md new file mode 100644 index 000000000000..a13562602822 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/README.md @@ -0,0 +1,16 @@ +# example + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/build.gradle b/packages/webview_flutter/webview_flutter_android/example/android/app/build.gradle new file mode 100644 index 000000000000..9a43699afb2b --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/build.gradle @@ -0,0 +1,62 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 29 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "io.flutter.plugins.webviewflutterexample" + minSdkVersion 19 + targetSdkVersion 28 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + api 'androidx.test:core:1.2.0' +} diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/webview_flutter/webview_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..9a4163a4f5ee --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java new file mode 100644 index 000000000000..a32aaebb0ecd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutterexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class MainActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java new file mode 100644 index 000000000000..0b3eeef9b6b7 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutterexample; + +import static org.junit.Assert.assertTrue; + +import androidx.test.core.app.ActivityScenario; +import io.flutter.plugins.webviewflutter.WebViewFlutterPlugin; +import org.junit.Test; + +public class WebViewTest { + @Test + public void webViewPluginIsAdded() { + final ActivityScenario scenario = + ActivityScenario.launch(WebViewTestActivity.class); + scenario.onActivity( + activity -> { + assertTrue(activity.engine.getPlugins().has(WebViewFlutterPlugin.class)); + }); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml b/packages/webview_flutter/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..28792201bc36 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/AndroidManifest.xml b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..b8c8d38d45a5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/WebViewTestActivity.java b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/WebViewTestActivity.java new file mode 100644 index 000000000000..cb53a7a0dbf5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/WebViewTestActivity.java @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutterexample; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; + +// Extends FlutterActivity to make the FlutterEngine accessible for testing. +public class WebViewTestActivity extends FlutterActivity { + public FlutterEngine engine; + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + engine = flutterEngine; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/drawable/launch_background.xml b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000000..304732f88420 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000000..db77bb4b7b09 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000000..17987b79bb8a Binary files /dev/null and b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000000..09d4391482be Binary files /dev/null and b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000000..d5f1c8d34e7a Binary files /dev/null and b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000000..4d6372eebdb2 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/values/styles.xml b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000000..00fa4417cfbe --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/packages/webview_flutter/webview_flutter_android/example/android/build.gradle b/packages/webview_flutter/webview_flutter_android/example/android/build.gradle new file mode 100644 index 000000000000..e101ac08df55 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.3.0' + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/packages/webview_flutter/webview_flutter_android/example/android/gradle.properties b/packages/webview_flutter/webview_flutter_android/example/android/gradle.properties new file mode 100644 index 000000000000..a6738207fd15 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true +android.enableR8=true diff --git a/packages/webview_flutter/webview_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/webview_flutter/webview_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..2819f022f1fd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/packages/webview_flutter/webview_flutter_android/example/android/settings.gradle b/packages/webview_flutter/webview_flutter_android/example/android/settings.gradle new file mode 100644 index 000000000000..5a2f14fb18f6 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart new file mode 100644 index 000000000000..80eb2f3a9a16 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -0,0 +1,725 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:webview_flutter_android/webview_surface_android.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +void main() => runApp(MaterialApp(home: WebViewExample())); + +const String kNavigationExamplePage = ''' + +Navigation Delegate Example + +

+The navigation delegate is set to block navigation to the youtube website. +

+ + + +'''; + +class WebViewExample extends StatefulWidget { + final WebViewPlatform platform = SurfaceAndroidWebView(); + + @override + _WebViewExampleState createState() => _WebViewExampleState(); + + void onPageStarted(String url) { + print('Page started loading: $url'); + } + + void onPageFinished(String url) { + print('Page finished loading: $url'); + } + + void onProgress(int progress) { + print("WebView is loading (progress : $progress%)"); + } + + void onWebResourceError(WebResourceError error) { + print("Webview resource error encountered: ${error.description}"); + } +} + +class _WebViewExampleState extends State { + final Completer _controller = + Completer(); + late final JavascriptChannelRegistry _javascriptChannelRegistry; + late final _PlatformCallbacksHandler _platformCallbacksHandler; + + @override + void initState() { + super.initState(); + _platformCallbacksHandler = _PlatformCallbacksHandler(widget); + _javascriptChannelRegistry = JavascriptChannelRegistry(null); + } + + @override + void didUpdateWidget(WebViewExample oldWidget) { + super.didUpdateWidget(oldWidget); + _controller.future.then((WebViewController controller) { + _platformCallbacksHandler._widget = widget; + controller._updateWidget(widget); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Flutter WebView example'), + // This drop down menu demonstrates that Flutter widgets can be shown over the web view. + actions: [ + NavigationControls(_controller.future), + SampleMenu(_controller.future), + ], + ), + // We're using a Builder here so we have a context that is below the Scaffold + // to allow calling Scaffold.of(context) so we can show a snackbar. + body: Builder(builder: (BuildContext context) { + _javascriptChannelRegistry.updateJavascriptChannelsFromSet( + {_toasterJavascriptChannel(context)}); + + return widget.platform.build( + context: context, + onWebViewPlatformCreated: + (WebViewPlatformController? webViewPlatformController) { + WebViewController controller = WebViewController._( + widget, + webViewPlatformController!, + _javascriptChannelRegistry, + ); + _controller.complete(controller); + }, + webViewPlatformCallbacksHandler: _platformCallbacksHandler, + creationParams: CreationParams( + initialUrl: 'https://flutter.dev', + webSettings: _webSettingsFromWidget(widget), + javascriptChannelNames: + _javascriptChannelRegistry.channels.keys.toSet(), + autoMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + javascriptChannelRegistry: _javascriptChannelRegistry, + ); + }), + floatingActionButton: favoriteButton(), + ); + } + + JavascriptChannel _toasterJavascriptChannel(BuildContext context) { + return JavascriptChannel( + name: 'Toaster', + onMessageReceived: (JavascriptMessage message) { + if (context != null) { + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + SnackBar(content: Text(message.message)), + ); + } + }); + } + + Widget favoriteButton() { + return FutureBuilder( + future: _controller.future, + builder: (BuildContext context, + AsyncSnapshot controller) { + if (controller.hasData) { + return FloatingActionButton( + onPressed: () async { + final String url = (await controller.data!.currentUrl())!; + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + SnackBar(content: Text('Favorited $url')), + ); + }, + child: const Icon(Icons.favorite), + ); + } + return Container(); + }); + } +} + +enum MenuOptions { + showUserAgent, + listCookies, + clearCookies, + addToCache, + listCache, + clearCache, + navigationDelegate, +} + +class SampleMenu extends StatelessWidget { + SampleMenu(this.controller); + + final Future controller; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: controller, + builder: + (BuildContext context, AsyncSnapshot controller) { + return PopupMenuButton( + onSelected: (MenuOptions value) { + switch (value) { + case MenuOptions.showUserAgent: + _onShowUserAgent(controller.data!, context); + break; + case MenuOptions.listCookies: + _onListCookies(controller.data!, context); + break; + case MenuOptions.clearCookies: + _onClearCookies(controller.data!, context); + break; + case MenuOptions.addToCache: + _onAddToCache(controller.data!, context); + break; + case MenuOptions.listCache: + _onListCache(controller.data!, context); + break; + case MenuOptions.clearCache: + _onClearCache(controller.data!, context); + break; + case MenuOptions.navigationDelegate: + _onNavigationDelegateExample(controller.data!, context); + break; + } + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: MenuOptions.showUserAgent, + child: const Text('Show user agent'), + enabled: controller.hasData, + ), + const PopupMenuItem( + value: MenuOptions.listCookies, + child: Text('List cookies'), + ), + const PopupMenuItem( + value: MenuOptions.clearCookies, + child: Text('Clear cookies'), + ), + const PopupMenuItem( + value: MenuOptions.addToCache, + child: Text('Add to cache'), + ), + const PopupMenuItem( + value: MenuOptions.listCache, + child: Text('List cache'), + ), + const PopupMenuItem( + value: MenuOptions.clearCache, + child: Text('Clear cache'), + ), + const PopupMenuItem( + value: MenuOptions.navigationDelegate, + child: Text('Navigation Delegate example'), + ), + ], + ); + }, + ); + } + + void _onShowUserAgent( + WebViewController controller, BuildContext context) async { + // Send a message with the user agent string to the Toaster JavaScript channel we registered + // with the WebView. + await controller.evaluateJavascript( + 'Toaster.postMessage("User Agent: " + navigator.userAgent);'); + } + + void _onListCookies( + WebViewController controller, BuildContext context) async { + final String cookies = + await controller.evaluateJavascript('document.cookie'); + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar(SnackBar( + content: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Cookies:'), + _getCookieList(cookies), + ], + ), + )); + } + + void _onAddToCache(WebViewController controller, BuildContext context) async { + await controller.evaluateJavascript( + 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar(const SnackBar( + content: Text('Added a test entry to cache.'), + )); + } + + void _onListCache(WebViewController controller, BuildContext context) async { + await controller.evaluateJavascript('caches.keys()' + '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' + '.then((caches) => Toaster.postMessage(caches))'); + } + + void _onClearCache(WebViewController controller, BuildContext context) async { + await controller.clearCache(); + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar(const SnackBar( + content: Text("Cache cleared."), + )); + } + + void _onClearCookies( + WebViewController controller, BuildContext context) async { + final bool hadCookies = await controller._widget.platform.clearCookies(); + String message = 'There were cookies. Now, they are gone!'; + if (!hadCookies) { + message = 'There are no cookies.'; + } + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar(SnackBar( + content: Text(message), + )); + } + + void _onNavigationDelegateExample( + WebViewController controller, BuildContext context) async { + final String contentBase64 = + base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); + await controller.loadUrl('data:text/html;base64,$contentBase64'); + } + + Widget _getCookieList(String cookies) { + if (cookies == null || cookies == '""') { + return Container(); + } + final List cookieList = cookies.split(';'); + final Iterable cookieWidgets = + cookieList.map((String cookie) => Text(cookie)); + return Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: cookieWidgets.toList(), + ); + } +} + +class NavigationControls extends StatelessWidget { + const NavigationControls(this._webViewControllerFuture) + : assert(_webViewControllerFuture != null); + + final Future _webViewControllerFuture; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _webViewControllerFuture, + builder: + (BuildContext context, AsyncSnapshot snapshot) { + final bool webViewReady = + snapshot.connectionState == ConnectionState.done; + final WebViewController? controller = snapshot.data; + if (controller == null) return Container(); + return Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: !webViewReady + ? null + : () async { + if (await controller.canGoBack()) { + await controller.goBack(); + } else { + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + const SnackBar(content: Text("No back history item")), + ); + return; + } + }, + ), + IconButton( + icon: const Icon(Icons.arrow_forward_ios), + onPressed: !webViewReady + ? null + : () async { + if (await controller.canGoForward()) { + await controller.goForward(); + } else { + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + const SnackBar( + content: Text("No forward history item")), + ); + return; + } + }, + ), + IconButton( + icon: const Icon(Icons.replay), + onPressed: !webViewReady + ? null + : () { + controller.reload(); + }, + ), + ], + ); + }, + ); + } +} + +/// 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._javascriptChannelRegistry, + ) : assert(_webViewPlatformController != null) { + _settings = _webSettingsFromWidget(_widget); + } + + final JavascriptChannelRegistry _javascriptChannelRegistry; + + final WebViewPlatformController _webViewPlatformController; + + late WebSettings _settings; + + WebViewExample _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 loadUrl( + String url, { + Map? 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 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 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 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 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 goForward() { + return _webViewPlatformController.goForward(); + } + + /// Reloads the current URL. + Future 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 clearCache() async { + await _webViewPlatformController.clearCache(); + return reload(); + } + + Future _updateWidget(WebViewExample widget) async { + _widget = widget; + await _updateSettings(_webSettingsFromWidget(widget)); + await _updateJavascriptChannels(_javascriptChannelRegistry.channels.values + .toSet()); // TODO: CHECK WITH MAURITS IF POINTLESS. PROBABLY REMOVE THIS? + } + + Future _updateSettings(WebSettings newSettings) { + final WebSettings update = + _clearUnchangedWebSettings(_settings, newSettings); + _settings = newSettings; + return _webViewPlatformController.updateSettings(update); + } + + Future _updateJavascriptChannels( + Set? newChannels) async { + final Set currentChannels = + _javascriptChannelRegistry.channels.keys.toSet(); + final Set newChannelNames = _extractChannelNames(newChannels); + final Set channelsToAdd = + newChannelNames.difference(currentChannels); + final Set channelsToRemove = + currentChannels.difference(newChannelNames); + if (channelsToRemove.isNotEmpty) { + await _webViewPlatformController + .removeJavascriptChannels(channelsToRemove); + } + if (channelsToAdd.isNotEmpty) { + await _webViewPlatformController.addJavascriptChannels(channelsToAdd); + } + _javascriptChannelRegistry.updateJavascriptChannelsFromSet(newChannels); + } + + /// Evaluates a JavaScript expression in the context of the current page. + /// + /// On Android returns the evaluation result as a JSON formatted string. + /// + /// On iOS depending on the value type the return value would be one of: + /// + /// - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100'). + /// - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.'). + /// - Other non-primitive types are not supported on iOS and will complete the Future with an error. + /// + /// The Future completes with an error if a JavaScript error occurred, or on iOS, if the type of the + /// evaluated expression is not supported as described above. + /// + /// When evaluating Javascript in a [WebView], it is best practice to wait for + /// the [WebView.onPageFinished] callback. This guarantees all the Javascript + /// embedded in the main frame HTML has been loaded. + Future evaluateJavascript(String javascriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + } + // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. + // https://github.com/flutter/flutter/issues/26431 + // ignore: strong_mode_implicit_dynamic_method + return _webViewPlatformController.evaluateJavascript(javascriptString); + } + + /// Returns the title of the currently loaded page. + Future getTitle() { + return _webViewPlatformController.getTitle(); + } + + /// Sets the WebView's content scroll position. + /// + /// The parameters `x` and `y` specify the scroll position in WebView pixels. + Future 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 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 getScrollX() { + return _webViewPlatformController.getScrollX(); + } + + /// Return the vertical scroll position, in WebView pixels, of this view. + /// + /// Scroll position is measured from top. + Future getScrollY() { + return _webViewPlatformController.getScrollY(); + } +} + +WebSettings _webSettingsFromWidget(WebViewExample widget) { + return WebSettings( + javascriptMode: JavascriptMode.unrestricted, + hasNavigationDelegate: true, + hasProgressTracking: widget.onProgress != null, + debuggingEnabled: false, + gestureNavigationEnabled: false, + allowsInlineMediaPlayback: false, + userAgent: WebSetting.of(null), + ); +} + +class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { + _PlatformCallbacksHandler(this._widget); + + WebViewExample _widget; + + @override + FutureOr onNavigationRequest({ + required String url, + required bool isForMainFrame, + }) async { + if (url.startsWith('https://www.youtube.com/')) { + print('blocking navigation to $url'); + return false; + } + print('allowing navigation to $url'); + return true; + } + + @override + void onPageStarted(String url) { + if (_widget.onPageStarted != null) { + _widget.onPageStarted(url); + } + } + + @override + void onPageFinished(String url) { + if (_widget.onPageFinished != null) { + _widget.onPageFinished(url); + } + } + + @override + void onProgress(int progress) { + if (_widget.onProgress != null) { + _widget.onProgress(progress); + } + } + + void onWebResourceError(WebResourceError error) { + if (_widget.onWebResourceError != null) { + _widget.onWebResourceError(error); + } + } +} + +/// Information about a navigation action that is about to be executed. +class NavigationRequest { + NavigationRequest._({required this.url, required this.isForMainFrame}); + + /// The URL that will be loaded if the navigation is executed. + final String url; + + /// Whether the navigation request is to be loaded as the main frame. + final bool isForMainFrame; + + @override + String toString() { + return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)'; + } +} + +// 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); + } +} + +// This method assumes that no fields in `currentValue` are null. +WebSettings _clearUnchangedWebSettings( + WebSettings currentValue, WebSettings newValue) { + assert(currentValue.javascriptMode != null); + assert(currentValue.hasNavigationDelegate != null); + assert(currentValue.hasProgressTracking != null); + assert(currentValue.debuggingEnabled != null); + assert(currentValue.userAgent != null); + assert(newValue.javascriptMode != null); + assert(newValue.hasNavigationDelegate != null); + assert(newValue.debuggingEnabled != null); + assert(newValue.userAgent != null); + + JavascriptMode? javascriptMode; + bool? hasNavigationDelegate; + bool? hasProgressTracking; + bool? debuggingEnabled; + WebSetting userAgent = WebSetting.absent(); + if (currentValue.javascriptMode != newValue.javascriptMode) { + javascriptMode = newValue.javascriptMode; + } + if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) { + hasNavigationDelegate = newValue.hasNavigationDelegate; + } + if (currentValue.hasProgressTracking != newValue.hasProgressTracking) { + hasProgressTracking = newValue.hasProgressTracking; + } + if (currentValue.debuggingEnabled != newValue.debuggingEnabled) { + debuggingEnabled = newValue.debuggingEnabled; + } + if (currentValue.userAgent != newValue.userAgent) { + userAgent = newValue.userAgent; + } + + return WebSettings( + javascriptMode: javascriptMode, + hasNavigationDelegate: hasNavigationDelegate, + hasProgressTracking: hasProgressTracking, + debuggingEnabled: debuggingEnabled, + userAgent: userAgent, + ); +} + +Set _extractChannelNames(Set? channels) { + final Set channelNames = channels == null + ? {} + : channels.map((JavascriptChannel channel) => channel.name).toSet(); + return channelNames; +} + +/// Callback type for handling messages sent from Javascript running in a web view. +typedef void JavascriptMessageHandler(JavascriptMessage message); + +/// A decision on how to handle a navigation request. +enum NavigationDecision { + /// Prevent the navigation from taking place. + prevent, + + /// Allow the navigation to take place. + navigate, +} diff --git a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml new file mode 100644 index 000000000000..a8ec5b6823f3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml @@ -0,0 +1,26 @@ +name: webview_flutter_android_example +description: Demonstrates how to use the webview_flutter_android plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + # TODO (mvanbeusekom): Replace with pub.dev version once published + webview_flutter_platform_interface: + path: ../../webview_flutter_platform_interface + webview_flutter_android: + # When depending on this package from a real application you should use: + # webview_flutter_android: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + +dev_dependencies: + pedantic: ^1.10.0 + +flutter: + uses-material-design: true diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart new file mode 100644 index 000000000000..499b08807b92 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart @@ -0,0 +1,59 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +/// Builds an Android webview. +/// +/// This is used as the default implementation for [WebView.platform] on Android. It uses +/// an [AndroidView] to embed the webview in the widget hierarchy, and uses a method channel to +/// communicate with the platform code. +class AndroidWebView implements WebViewPlatform { + @override + Widget build({ + required BuildContext context, + required CreationParams creationParams, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, + required JavascriptChannelRegistry javascriptChannelRegistry, + }) { + assert(webViewPlatformCallbacksHandler != null); + return GestureDetector( + // We prevent text selection by intercepting the long press event. + // This is a temporary stop gap due to issues with text selection on Android: + // https://github.com/flutter/flutter/issues/24585 - the text selection + // dialog is not responding to touch events. + // https://github.com/flutter/flutter/issues/24584 - the text selection + // handles are not showing. + // TODO(amirh): remove this when the issues above are fixed. + onLongPress: () {}, + excludeFromSemantics: true, + child: AndroidView( + viewType: 'plugins.flutter.io/webview', + onPlatformViewCreated: (int id) { + if (onWebViewPlatformCreated == null) { + return; + } + onWebViewPlatformCreated(MethodChannelWebViewPlatform( + id, webViewPlatformCallbacksHandler, javascriptChannelRegistry)); + }, + gestureRecognizers: gestureRecognizers, + layoutDirection: Directionality.maybeOf(context) ?? TextDirection.rtl, + creationParams: + MethodChannelWebViewPlatform.creationParamsToMap(creationParams), + creationParamsCodec: const StandardMessageCodec(), + ), + ); + } + + @override + Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart new file mode 100644 index 000000000000..7e934e14edfd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart @@ -0,0 +1,77 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:webview_flutter_android/webview_android.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +/// Android [WebViewPlatform] that uses [AndroidViewSurface] to build the [WebView] widget. +/// +/// To use this, set [WebView.platform] to an instance of this class. +/// +/// This implementation uses hybrid composition to render the [WebView] on +/// Android. It solves multiple issues related to accessibility and interaction +/// with the [WebView] at the cost of some performance on Android versions below +/// 10. See https://github.com/flutter/flutter/wiki/Hybrid-Composition for more +/// information. +class SurfaceAndroidWebView extends AndroidWebView { + @override + Widget build({ + required BuildContext context, + required CreationParams creationParams, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + required JavascriptChannelRegistry javascriptChannelRegistry, + }) { + assert(Platform.isAndroid); + assert(webViewPlatformCallbacksHandler != null); + return PlatformViewLink( + viewType: 'plugins.flutter.io/webview', + surfaceFactory: ( + BuildContext context, + PlatformViewController controller, + ) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: gestureRecognizers ?? + const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + return PlatformViewsService.initSurfaceAndroidView( + id: params.id, + viewType: 'plugins.flutter.io/webview', + // WebView content is not affected by the Android view's layout direction, + // we explicitly set it here so that the widget doesn't require an ambient + // directionality. + layoutDirection: TextDirection.rtl, + creationParams: MethodChannelWebViewPlatform.creationParamsToMap( + creationParams, + usesHybridComposition: true, + ), + creationParamsCodec: const StandardMessageCodec(), + ) + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) + ..addOnPlatformViewCreatedListener((int id) { + if (onWebViewPlatformCreated == null) { + return; + } + onWebViewPlatformCreated( + MethodChannelWebViewPlatform(id, webViewPlatformCallbacksHandler, + javascriptChannelRegistry), + ); + }) + ..create(); + }, + ); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml new file mode 100644 index 000000000000..e52f95642bf5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -0,0 +1,35 @@ +name: webview_flutter_android +description: A Flutter plugin that provides a WebView widget on Android. +repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_android +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 +version: 0.0.1 + +# TODO (mvanbeusekom): Remove this line when final version of webview_flutter_platform_interface is published +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +flutter: + plugin: + implements: webview_flutter + platforms: + android: + package: io.flutter.plugins.webviewflutter + pluginClass: WebViewFlutterPlugin + +dependencies: + flutter: + sdk: flutter + + # TODO (mvanbeusekom): Replace with pub.dev version once published + webview_flutter_platform_interface: + path: ../webview_flutter_platform_interface + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + pedantic: ^1.10.0 diff --git a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md new file mode 100644 index 000000000000..2f529b31655d --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +* Initial open-source release. \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter_platform_interface/LICENSE b/packages/webview_flutter/webview_flutter_platform_interface/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/README.md b/packages/webview_flutter/webview_flutter_platform_interface/README.md new file mode 100644 index 000000000000..202b0fcf539f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/README.md @@ -0,0 +1,26 @@ +# webview_flutter_platform_interface + +A common platform interface for the [`webview_flutter`][1] plugin. + +This interface allows platform-specific implementations of the `webview_flutter` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `webview_flutter`, extend +[`WebviewPlatform`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`WebviewPlatform` by calling +`WebviewPlatform.setInstance(MyPlatformWebview())`. + +# Note on breaking changes + +Strongly prefer non-breaking changes (such as adding a method to the interface) +over breaking changes for this package. + +See https://flutter.dev/go/platform-interface-breaking-changes for a discussion +on why a less-clean interface is preferable to a breaking change. + +[1]: ../webview_flutter +[2]: lib/webview_flutter_platform_interface.dart diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart new file mode 100644 index 000000000000..741b3ab748b9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart @@ -0,0 +1,230 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:webview_flutter_platform_interface/src/utils/javascript_channel_registry.dart'; + +import '../platform_interface/webview_platform_callbacks_handler.dart'; +import '../platform_interface/webview_platform_controller.dart'; +import '../types/types.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, + this._javascriptChannelRegistry, + ) : assert(_platformCallbacksHandler != null), + _channel = MethodChannel('plugins.flutter.io/webview_$id') { + _channel.setMethodCallHandler(_onMethodCall); + } + + final JavascriptChannelRegistry _javascriptChannelRegistry; + + final WebViewPlatformCallbacksHandler _platformCallbacksHandler; + + final MethodChannel _channel; + + static const MethodChannel _cookieManagerChannel = + MethodChannel('plugins.flutter.io/cookie_manager'); + + Future _onMethodCall(MethodCall call) async { + switch (call.method) { + case 'javascriptChannelMessage': + final String channel = call.arguments['channel']! as String; + final String message = call.arguments['message']! as String; + _javascriptChannelRegistry.onJavascriptChannelMessage(channel, message); + return true; + case 'navigationRequest': + return await _platformCallbacksHandler.onNavigationRequest( + url: call.arguments['url']! as String, + isForMainFrame: call.arguments['isForMainFrame']! as bool, + ); + case 'onPageFinished': + _platformCallbacksHandler + .onPageFinished(call.arguments['url']! as String); + return null; + case 'onProgress': + _platformCallbacksHandler + .onProgress(call.arguments['progress']! as int); + return null; + case 'onPageStarted': + _platformCallbacksHandler + .onPageStarted(call.arguments['url']! as String); + return null; + case 'onWebResourceError': + _platformCallbacksHandler.onWebResourceError( + WebResourceError( + errorCode: call.arguments['errorCode']! as int, + description: call.arguments['description']! as String, + // iOS doesn't support `failingUrl`. + failingUrl: call.arguments['failingUrl'] as String?, + domain: call.arguments['domain'] as String?, + 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 loadUrl( + String url, + Map? headers, + ) async { + assert(url != null); + return _channel.invokeMethod('loadUrl', { + 'url': url, + 'headers': headers, + }); + } + + @override + Future currentUrl() => _channel.invokeMethod('currentUrl'); + + @override + Future canGoBack() => + _channel.invokeMethod('canGoBack').then((bool? result) => result!); + + @override + Future canGoForward() => _channel + .invokeMethod('canGoForward') + .then((bool? result) => result!); + + @override + Future goBack() => _channel.invokeMethod('goBack'); + + @override + Future goForward() => _channel.invokeMethod('goForward'); + + @override + Future reload() => _channel.invokeMethod('reload'); + + @override + Future clearCache() => _channel.invokeMethod('clearCache'); + + @override + Future updateSettings(WebSettings settings) async { + final Map updatesMap = _webSettingsToMap(settings); + if (updatesMap.isNotEmpty) { + await _channel.invokeMethod('updateSettings', updatesMap); + } + } + + @override + Future evaluateJavascript(String javascriptString) { + return _channel + .invokeMethod('evaluateJavascript', javascriptString) + .then((String? result) { + return result!; + }); + } + + @override + Future addJavascriptChannels(Set javascriptChannelNames) { + return _channel.invokeMethod( + 'addJavascriptChannels', javascriptChannelNames.toList()); + } + + @override + Future removeJavascriptChannels(Set javascriptChannelNames) { + return _channel.invokeMethod( + 'removeJavascriptChannels', javascriptChannelNames.toList()); + } + + @override + Future getTitle() => _channel.invokeMethod('getTitle'); + + @override + Future scrollTo(int x, int y) { + return _channel.invokeMethod('scrollTo', { + 'x': x, + 'y': y, + }); + } + + @override + Future scrollBy(int x, int y) { + return _channel.invokeMethod('scrollBy', { + 'x': x, + 'y': y, + }); + } + + @override + Future getScrollX() => + _channel.invokeMethod('getScrollX').then((int? result) => result!); + + @override + Future getScrollY() => + _channel.invokeMethod('getScrollY').then((int? result) => result!); + + /// Method channel implementation for [WebViewPlatform.clearCookies]. + static Future clearCookies() { + return _cookieManagerChannel + .invokeMethod('clearCookies') + .then((bool? result) => result!); + } + + static Map _webSettingsToMap(WebSettings? settings) { + final Map map = {}; + void _addIfNonNull(String key, dynamic value) { + if (value == null) { + return; + } + map[key] = value; + } + + void _addSettingIfPresent(String key, WebSetting setting) { + if (!setting.isPresent) { + return; + } + map[key] = setting.value; + } + + _addIfNonNull('jsMode', settings!.javascriptMode?.index); + _addIfNonNull('hasNavigationDelegate', settings.hasNavigationDelegate); + _addIfNonNull('hasProgressTracking', settings.hasProgressTracking); + _addIfNonNull('debuggingEnabled', settings.debuggingEnabled); + _addIfNonNull( + 'gestureNavigationEnabled', settings.gestureNavigationEnabled); + _addIfNonNull( + 'allowsInlineMediaPlayback', settings.allowsInlineMediaPlayback); + _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 creationParamsToMap( + CreationParams creationParams, { + bool usesHybridComposition = false, + }) { + return { + 'initialUrl': creationParams.initialUrl, + 'settings': _webSettingsToMap(creationParams.webSettings), + 'javascriptChannelNames': creationParams.javascriptChannelNames.toList(), + 'userAgent': creationParams.userAgent, + 'autoMediaPlaybackPolicy': creationParams.autoMediaPlaybackPolicy.index, + 'usesHybridComposition': usesHybridComposition, + }; + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/platform_interface.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/platform_interface.dart new file mode 100644 index 000000000000..d41c1c8278aa --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/platform_interface.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'webview_platform.dart'; +export 'webview_platform_callbacks_handler.dart'; +export 'webview_platform_controller.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform.dart new file mode 100644 index 000000000000..feab661b9062 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform.dart @@ -0,0 +1,66 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import '../types/types.dart'; +import 'webview_platform_callbacks_handler.dart'; +import 'webview_platform_controller.dart'; + +/// 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({ + required 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. + required CreationParams creationParams, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + required JavascriptChannelRegistry javascriptChannelRegistry, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, + }); + + /// Clears all cookies for all [WebView] instances. + /// + /// Returns true if cookies were present before clearing, else false. + Future clearCookies() { + throw UnimplementedError( + 'WebView clearCookies is not implemented on the current platform'); + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_callbacks_handler.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_callbacks_handler.dart new file mode 100644 index 000000000000..765d3da4462b --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_callbacks_handler.dart @@ -0,0 +1,32 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import '../types/web_resource_error.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 navigation request is pending. + /// + /// If true is returned the navigation is allowed, otherwise it is blocked. + FutureOr onNavigationRequest( + {required String url, required 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); + + /// Invoked by [WebViewPlatformController] when a page is loading. + /// /// Only works when [WebSettings.hasProgressTracking] is set to `true`. + void onProgress(int progress); + + /// Report web resource loading error to the host application. + void onWebResourceError(WebResourceError error); +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart new file mode 100644 index 000000000000..c9c4facd3523 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart @@ -0,0 +1,169 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../types/types.dart'; + +/// 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 { + /// 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 loadUrl( + String url, + Map? 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 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 currentUrl() { + throw UnimplementedError( + 'WebView currentUrl is not implemented on the current platform'); + } + + /// Checks whether there's a back history item. + Future canGoBack() { + throw UnimplementedError( + 'WebView canGoBack is not implemented on the current platform'); + } + + /// Checks whether there's a forward history item. + Future 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 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 goForward() { + throw UnimplementedError( + 'WebView goForward is not implemented on the current platform'); + } + + /// Reloads the current URL. + Future 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 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 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 addJavascriptChannels(Set 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 removeJavascriptChannels(Set javascriptChannelNames) { + throw UnimplementedError( + 'WebView removeJavascriptChannels is not implemented on the current platform'); + } + + /// Returns the title of the currently loaded page. + Future 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 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 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 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 getScrollY() { + throw UnimplementedError( + 'WebView getScrollY is not implemented on the current platform'); + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/auto_media_playback_policy.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/auto_media_playback_policy.dart new file mode 100644 index 000000000000..7d6927ac7957 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/auto_media_playback_policy.dart @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// 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, +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.dart new file mode 100644 index 000000000000..e69f510e32fb --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.dart @@ -0,0 +1,60 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'auto_media_playback_policy.dart'; +import 'web_settings.dart'; + +/// 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 = const {}, + 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 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 'CreationParams(initialUrl: $initialUrl, settings: $webSettings, javascriptChannelNames: $javascriptChannelNames, UserAgent: $userAgent)'; + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.dart new file mode 100644 index 000000000000..4253e782d49f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.dart @@ -0,0 +1,38 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'javascript_message.dart'; + +/// Callback type for handling messages sent from Javascript running in a web view. +typedef JavascriptMessageHandler = void Function(JavascriptMessage message); + +/// Regular expression used to validate Javascript channel name. +final RegExp _validChannelNames = RegExp(r'^[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(_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; +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_message.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_message.dart new file mode 100644 index 000000000000..8d080452c54a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_message.dart @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// 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; +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_mode.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_mode.dart new file mode 100644 index 000000000000..53d049175907 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_mode.dart @@ -0,0 +1,12 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Describes the state of JavaScript support in a given web view. +enum JavascriptMode { + /// JavaScript execution is disabled. + disabled, + + /// JavaScript execution is not restricted. + unrestricted, +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart new file mode 100644 index 000000000000..9bc68e7302e6 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart @@ -0,0 +1,11 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'auto_media_playback_policy.dart'; +export 'creation_params.dart'; +export 'javascript_channel.dart'; +export 'javascript_message.dart'; +export 'javascript_mode.dart'; +export 'web_resource_error.dart'; +export 'web_settings.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error.dart new file mode 100644 index 000000000000..19a93c407cb2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error.dart @@ -0,0 +1,118 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// 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; +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart new file mode 100644 index 000000000000..17a17e601a88 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart @@ -0,0 +1,128 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; + +import 'javascript_mode.dart'; + +/// A single setting for configuring a WebViewPlatform which may be absent. +@immutable +class WebSetting { + /// Constructs an absent setting instance. + /// + /// The [isPresent] field for the instance will be false. + /// + /// Accessing [value] for an absent instance will throw. + const WebSetting.absent() + : _value = null, + isPresent = false; + + /// Constructs a setting of the given `value`. + /// + /// The [isPresent] field for the instance will be true. + const 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); + // The intention of this getter is to return T whether it is nullable or + // not whereas _value is of type T? since _value can be null even when + // T is not nullable (when isPresent == false). + // + // We promote _value to T using `as T` instead of `!` operator to handle + // the case when _value is legitimately null (and T is a nullable type). + // `!` operator would always throw if _value is null. + return _value as T; + } + + /// 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; + } + + return other is WebSetting && + other.isPresent == isPresent && + other._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.hasProgressTracking, + this.debuggingEnabled, + this.gestureNavigationEnabled, + this.allowsInlineMediaPlayback, + 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 the [WebView] should track page loading progress. + /// See also: [WebViewPlatformCallbacksHandler.onProgress] to get the progress. + final bool? hasProgressTracking; + + /// Whether to enable the platform's webview content debugging tools. + /// + /// See also: [WebView.debuggingEnabled]. + final bool? debuggingEnabled; + + /// Whether to play HTML5 videos inline or use the native full-screen controller on iOS. + /// + /// This will have no effect on Android. + final bool? allowsInlineMediaPlayback; + + /// 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 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, hasProgressTracking: $hasProgressTracking, debuggingEnabled: $debuggingEnabled, gestureNavigationEnabled: $gestureNavigationEnabled, userAgent: $userAgent, allowsInlineMediaPlayback: $allowsInlineMediaPlayback)'; + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/utils/javascript_channel_registry.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/utils/javascript_channel_registry.dart new file mode 100644 index 000000000000..142d8eb00950 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/utils/javascript_channel_registry.dart @@ -0,0 +1,42 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../types/javascript_channel.dart'; +import '../types/javascript_message.dart'; + +/// Utility class for managing named JavaScript channels and forwarding incoming +/// messages on the correct channel. +class JavascriptChannelRegistry { + /// Constructs a [JavascriptChannelRegistry] initializing it with the given + /// set of [JavascriptChannel]s. + JavascriptChannelRegistry(Set? channels) { + updateJavascriptChannelsFromSet(channels); + } + + /// Maps a channel name to a channel. + final Map channels = {}; + + /// Invoked when a JavaScript channel message is received. + void onJavascriptChannelMessage(String channel, String message) { + final JavascriptChannel? javascriptChannel = channels[channel]; + + if (javascriptChannel == null) { + throw ArgumentError('No channel registered with name $channel.'); + } + + javascriptChannel.onMessageReceived(JavascriptMessage(message)); + } + + /// Updates the set of [JavascriptChannel]s with the new set. + void updateJavascriptChannelsFromSet(Set? channels) { + this.channels.clear(); + if (channels == null) { + return; + } + + for (final JavascriptChannel channel in channels) { + this.channels[channel.name] = channel; + } + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/utils/utils.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/utils/utils.dart new file mode 100644 index 000000000000..fb7a3b912909 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/utils/utils.dart @@ -0,0 +1,5 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'javascript_channel_registry.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/webview_flutter_platform_interface.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/webview_flutter_platform_interface.dart new file mode 100644 index 000000000000..f15ef418e014 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/webview_flutter_platform_interface.dart @@ -0,0 +1,8 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/method_channel/webview_method_channel.dart'; +export 'src/platform_interface/platform_interface.dart'; +export 'src/types/types.dart'; +export 'src/utils/utils.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml new file mode 100644 index 000000000000..7267e8e10ba9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml @@ -0,0 +1,22 @@ +name: webview_flutter_platform_interface +description: A common platform interface for the webview_flutter plugin. +repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_platform_interface +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview_flutter%22 +# NOTE: We strongly prefer non-breaking changes, even at the expense of a +# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes +version: 1.0.0 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + mockito: ^5.0.0 + pedantic: ^1.10.0 diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart new file mode 100644 index 000000000000..0ffc063a0f0a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart @@ -0,0 +1,458 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import 'package:webview_flutter_platform_interface/src/method_channel/webview_method_channel.dart'; +import 'package:webview_flutter_platform_interface/src/utils/javascript_channel_registry.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$MethodChannelWebViewPlatform', () { + const int channelId = 1; + const MethodChannel channel = + MethodChannel('plugins.flutter.io/webview_$channelId'); + final WebViewPlatformCallbacksHandler callbacksHandler = + MockWebViewPlatformCallbacksHandler(); + final JavascriptChannelRegistry javascriptChannelRegistry = + MockJavascriptChannelRegistry(); + + final List log = []; + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + + switch (methodCall.method) { + case 'currentUrl': + return 'https://test.url'; + case 'canGoBack': + case 'canGoForward': + return true; + case 'evaluateJavascript': + return methodCall.arguments as String; + case 'getScrollX': + return 10; + case 'getScrollY': + return 20; + } + + // Return null explicitly instead of relying on the implicit null + // returned by the method channel if no return statement is specified. + return null; + }); + + final MethodChannelWebViewPlatform webViewPlatform = + MethodChannelWebViewPlatform( + channelId, + callbacksHandler, + javascriptChannelRegistry, + ); + + tearDown(() { + log.clear(); + }); + + test('loadUrl with headers', () async { + await webViewPlatform.loadUrl( + 'https://test.url', + const { + 'Content-Type': 'text/plain', + 'Accept': 'text/html', + }, + ); + + expect( + log, + [ + isMethodCall( + 'loadUrl', + arguments: { + 'url': 'https://test.url', + 'headers': { + 'Content-Type': 'text/plain', + 'Accept': 'text/html', + }, + }, + ), + ], + ); + }); + + test('loadUrl without headers', () async { + await webViewPlatform.loadUrl( + 'https://test.url', + null, + ); + + expect( + log, + [ + isMethodCall( + 'loadUrl', + arguments: { + 'url': 'https://test.url', + 'headers': null, + }, + ), + ], + ); + }); + + test('currentUrl', () async { + final String? currentUrl = await webViewPlatform.currentUrl(); + + expect(currentUrl, 'https://test.url'); + expect( + log, + [ + isMethodCall( + 'currentUrl', + arguments: null, + ), + ], + ); + }); + + test('canGoBack', () async { + final bool canGoBack = await webViewPlatform.canGoBack(); + + expect(canGoBack, true); + expect( + log, + [ + isMethodCall( + 'canGoBack', + arguments: null, + ), + ], + ); + }); + + test('canGoForward', () async { + final bool canGoForward = await webViewPlatform.canGoForward(); + + expect(canGoForward, true); + expect( + log, + [ + isMethodCall( + 'canGoForward', + arguments: null, + ), + ], + ); + }); + + test('goBack', () async { + await webViewPlatform.goBack(); + + expect( + log, + [ + isMethodCall( + 'goBack', + arguments: null, + ), + ], + ); + }); + + test('goForward', () async { + await webViewPlatform.goForward(); + + expect( + log, + [ + isMethodCall( + 'goForward', + arguments: null, + ), + ], + ); + }); + + test('reload', () async { + await webViewPlatform.reload(); + + expect( + log, + [ + isMethodCall( + 'reload', + arguments: null, + ), + ], + ); + }); + + test('clearCache', () async { + await webViewPlatform.clearCache(); + + expect( + log, + [ + isMethodCall( + 'clearCache', + arguments: null, + ), + ], + ); + }); + + test('updateSettings', () async { + final WebSettings settings = + WebSettings(userAgent: const WebSetting.of('Dart Test')); + await webViewPlatform.updateSettings(settings); + + expect( + log, + [ + isMethodCall( + 'updateSettings', + arguments: { + 'userAgent': 'Dart Test', + }, + ), + ], + ); + }); + + test('updateSettings all parameters', () async { + final WebSettings settings = WebSettings( + userAgent: const WebSetting.of('Dart Test'), + javascriptMode: JavascriptMode.disabled, + hasNavigationDelegate: true, + hasProgressTracking: true, + debuggingEnabled: true, + gestureNavigationEnabled: true, + allowsInlineMediaPlayback: true, + ); + await webViewPlatform.updateSettings(settings); + + expect( + log, + [ + isMethodCall( + 'updateSettings', + arguments: { + 'userAgent': 'Dart Test', + 'jsMode': 0, + 'hasNavigationDelegate': true, + 'hasProgressTracking': true, + 'debuggingEnabled': true, + 'gestureNavigationEnabled': true, + 'allowsInlineMediaPlayback': true, + }, + ), + ], + ); + }); + + test('updateSettings without settings', () async { + final WebSettings settings = + WebSettings(userAgent: const WebSetting.absent()); + await webViewPlatform.updateSettings(settings); + + expect( + log.isEmpty, + true, + ); + }); + + test('evaluateJavascript', () async { + final String evaluateJavascript = + await webViewPlatform.evaluateJavascript( + 'This simulates some Javascript code.', + ); + + expect('This simulates some Javascript code.', evaluateJavascript); + expect( + log, + [ + isMethodCall( + 'evaluateJavascript', + arguments: 'This simulates some Javascript code.', + ), + ], + ); + }); + + test('addJavascriptChannels', () async { + final Set channels = {'channel one', 'channel two'}; + await webViewPlatform.addJavascriptChannels(channels); + + expect(log, [ + isMethodCall( + 'addJavascriptChannels', + arguments: [ + 'channel one', + 'channel two', + ], + ), + ]); + }); + + test('addJavascriptChannels without channels', () async { + final Set channels = {}; + await webViewPlatform.addJavascriptChannels(channels); + + expect(log, [ + isMethodCall( + 'addJavascriptChannels', + arguments: [], + ), + ]); + }); + + test('removeJavascriptChannels', () async { + final Set channels = {'channel one', 'channel two'}; + await webViewPlatform.removeJavascriptChannels(channels); + + expect(log, [ + isMethodCall( + 'removeJavascriptChannels', + arguments: [ + 'channel one', + 'channel two', + ], + ), + ]); + }); + + test('removeJavascriptChannels without channels', () async { + final Set channels = {}; + await webViewPlatform.removeJavascriptChannels(channels); + + expect(log, [ + isMethodCall( + 'removeJavascriptChannels', + arguments: [], + ), + ]); + }); + + test('getTitle', () async { + final String? title = await webViewPlatform.getTitle(); + + expect(title, null); + expect( + log, + [ + isMethodCall('getTitle', arguments: null), + ], + ); + }); + + test('scrollTo', () async { + await webViewPlatform.scrollTo(10, 20); + + expect( + log, + [ + isMethodCall( + 'scrollTo', + arguments: { + 'x': 10, + 'y': 20, + }, + ), + ], + ); + }); + + test('scrollBy', () async { + await webViewPlatform.scrollBy(10, 20); + + expect( + log, + [ + isMethodCall( + 'scrollBy', + arguments: { + 'x': 10, + 'y': 20, + }, + ), + ], + ); + }); + + test('getScrollX', () async { + final int x = await webViewPlatform.getScrollX(); + + expect(x, 10); + expect( + log, + [ + isMethodCall( + 'getScrollX', + arguments: null, + ), + ], + ); + }); + + test('getScrollY', () async { + final int y = await webViewPlatform.getScrollY(); + + expect(y, 20); + expect( + log, + [ + isMethodCall( + 'getScrollY', + arguments: null, + ), + ], + ); + }); + }); + + group('$MethodChannelWebViewPlatform cookies', () { + const MethodChannel cookieChannel = + MethodChannel('plugins.flutter.io/cookie_manager'); + + final List log = []; + cookieChannel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + + if (methodCall.method == 'clearCookies') { + return true; + } + + // Return null explicitly instead of relying on the implicit null + // returned by the method channel if no return statement is specified. + return null; + }); + + tearDown(() { + log.clear(); + }); + + test('clearCookies', () async { + final bool clearCookies = + await MethodChannelWebViewPlatform.clearCookies(); + + expect(clearCookies, true); + expect( + log, + [ + isMethodCall( + 'clearCookies', + arguments: null, + ), + ], + ); + }); + }); +} + +class MockWebViewPlatformCallbacksHandler extends Mock + implements WebViewPlatformCallbacksHandler {} + +class MockJavascriptChannelRegistry extends Mock + implements JavascriptChannelRegistry {} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/javascript_channel_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/javascript_channel_test.dart new file mode 100644 index 000000000000..f481edda1edd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/javascript_channel_test.dart @@ -0,0 +1,48 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter_platform_interface/src/types/javascript_channel.dart'; + +void main() { + final List _validChars = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_'.split(''); + final List _commonInvalidChars = + r'`~!@#$%^&*()-=+[]{}\|"' ':;/?<>,. '.split(''); + final List _digits = List.generate(10, (int index) => index++); + + test( + 'ctor should create JavascriptChannel when name starts with a valid character followed by a number.', + () { + for (final String char in _validChars) { + for (final int digit in _digits) { + final JavascriptChannel channel = + JavascriptChannel(name: '$char$digit', onMessageReceived: (_) {}); + + expect(channel.name, '$char$digit'); + } + } + }); + + test('ctor should assert when channel name starts with a number.', () { + for (final int i in _digits) { + expect( + () => JavascriptChannel(name: '$i', onMessageReceived: (_) {}), + throwsAssertionError, + ); + } + }); + + test('ctor should assert when channel contains invalid char.', () { + for (final String validChar in _validChars) { + for (final String invalidChar in _commonInvalidChars) { + expect( + () => JavascriptChannel( + name: validChar + invalidChar, onMessageReceived: (_) {}), + throwsAssertionError, + ); + } + } + }); +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/utils/javascript_channel_registry_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/utils/javascript_channel_registry_test.dart new file mode 100644 index 000000000000..09145bfbe908 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/utils/javascript_channel_registry_test.dart @@ -0,0 +1,119 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter_platform_interface/src/types/javascript_channel.dart'; +import 'package:webview_flutter_platform_interface/src/types/types.dart'; +import 'package:webview_flutter_platform_interface/src/utils/javascript_channel_registry.dart'; + +void main() { + final Map _log = {}; + final Set _channels = { + JavascriptChannel( + name: 'js_channel_1', + onMessageReceived: (JavascriptMessage message) => + _log['js_channel_1'] = message.message, + ), + JavascriptChannel( + name: 'js_channel_2', + onMessageReceived: (JavascriptMessage message) => + _log['js_channel_2'] = message.message, + ), + JavascriptChannel( + name: 'js_channel_3', + onMessageReceived: (JavascriptMessage message) => + _log['js_channel_3'] = message.message, + ), + }; + + tearDown(() { + _log.clear(); + }); + + test('ctor should initialize with channels.', () { + final JavascriptChannelRegistry registry = + JavascriptChannelRegistry(_channels); + + expect(registry.channels.length, 3); + for (final JavascriptChannel channel in _channels) { + expect(registry.channels[channel.name], channel); + } + }); + + test('onJavascriptChannelMessage should forward message on correct channel.', + () { + final JavascriptChannelRegistry registry = + JavascriptChannelRegistry(_channels); + + registry.onJavascriptChannelMessage( + 'js_channel_2', + 'test message on channel 2', + ); + + expect( + _log, + containsPair( + 'js_channel_2', + 'test message on channel 2', + )); + }); + + test( + 'onJavascriptChannelMessage should throw ArgumentError when message arrives on non-existing channel.', + () { + final JavascriptChannelRegistry registry = + JavascriptChannelRegistry(_channels); + + expect( + () => registry.onJavascriptChannelMessage( + 'js_channel_4', + 'test message on channel 2', + ), + throwsA( + isA().having((ArgumentError error) => error.message, + 'message', 'No channel registered with name js_channel_4.'), + )); + }); + + test( + 'updateJavascriptChannelsFromSet should clear all channels when null is supplied.', + () { + final JavascriptChannelRegistry registry = + JavascriptChannelRegistry(_channels); + + expect(registry.channels.length, 3); + + registry.updateJavascriptChannelsFromSet(null); + + expect(registry.channels, isEmpty); + }); + + test('updateJavascriptChannelsFromSet should update registry with new set.', + () { + final JavascriptChannelRegistry registry = + JavascriptChannelRegistry(_channels); + + expect(registry.channels.length, 3); + + final Set newChannels = { + JavascriptChannel( + name: 'new_js_channel_1', + onMessageReceived: (JavascriptMessage message) => + _log['new_js_channel_1'] = message.message, + ), + JavascriptChannel( + name: 'new_js_channel_2', + onMessageReceived: (JavascriptMessage message) => + _log['new_js_channel_2'] = message.message, + ), + }; + + registry.updateJavascriptChannelsFromSet(newChannels); + + expect(registry.channels.length, 2); + for (final JavascriptChannel channel in newChannels) { + expect(registry.channels[channel.name], channel); + } + }); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md new file mode 100644 index 000000000000..d0bd041d0ff6 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* Initial release. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/LICENSE b/packages/webview_flutter/webview_flutter_wkwebview/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/README.md b/packages/webview_flutter/webview_flutter_wkwebview/README.md new file mode 100644 index 000000000000..6448d6e59185 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/README.md @@ -0,0 +1,11 @@ +# webview\_flutter\_wkwebview + +Implementation of [`webview_flutter`][1] based on Apple's WKWebView control. + +## Usage + +This package is [endorsed][2], which means you can simply use `webview_flutter` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/webview_flutter +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/webview_flutter/webview_flutter_wkwebview/analysis_options.yaml b/packages/webview_flutter/webview_flutter_wkwebview/analysis_options.yaml new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../../analysis_options_legacy.yaml diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/.gitignore b/packages/webview_flutter/webview_flutter_wkwebview/example/.gitignore new file mode 100644 index 000000000000..0fa6b675c0a5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/.metadata b/packages/webview_flutter/webview_flutter_wkwebview/example/.metadata new file mode 100644 index 000000000000..56bfc2c4d6be --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: f4abaa0735eba4dfd8f33f73363911d63931fe03 + channel: stable + +project_type: app diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/README.md b/packages/webview_flutter/webview_flutter_wkwebview/example/README.md new file mode 100644 index 000000000000..a13562602822 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/README.md @@ -0,0 +1,16 @@ +# example + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/AppFrameworkInfo.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..9367d483e44e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 8.0 + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/Debug.xcconfig b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..e8efba114687 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/Release.xcconfig b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..399e9340e6f6 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Podfile b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Podfile new file mode 100644 index 000000000000..66509fcae284 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Podfile @@ -0,0 +1,45 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + + target 'RunnerTests' do + inherit! :search_paths + + # Matches test_spec dependency. + pod 'OCMock', '3.5' + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..f75e71d1743a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,722 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 334734012669319100DCC49E /* FLTWebViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */; }; + 334734022669319400DCC49E /* FLTWKNavigationDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 9D26F6F82D91F92CC095EBA9 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */; }; + D9A9D48F1A75E5C682944DDD /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 27CC950C9005575711528C12 /* libPods-RunnerTests.a */; }; + F7151F77266057800028CB91 /* FLTWebViewUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F76266057800028CB91 /* FLTWebViewUITests.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 68BDCAEE23C3F7CB00D9C032 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; + F7151F79266057800028CB91 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 127772EEA7782174BE0D74B5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 27CC950C9005575711528C12 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTWKNavigationDelegateTests.m; sourceTree = ""; }; + 68BDCAE923C3F7CB00D9C032 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 68BDCAED23C3F7CB00D9C032 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTWebViewTests.m; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C475C484BD510DD9CB2E403C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + F7151F74266057800028CB91 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F7151F76266057800028CB91 /* FLTWebViewUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTWebViewUITests.m; sourceTree = ""; }; + F7151F78266057800028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 68BDCAE623C3F7CB00D9C032 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D9A9D48F1A75E5C682944DDD /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9D26F6F82D91F92CC095EBA9 /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F71266057800028CB91 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 68BDCAEA23C3F7CB00D9C032 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */, + 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */, + 68BDCAED23C3F7CB00D9C032 /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 68BDCAEA23C3F7CB00D9C032 /* RunnerTests */, + F7151F75266057800028CB91 /* RunnerUITests */, + 97C146EF1CF9000F007C117D /* Products */, + C6FFB52F5C2B8A41A7E39DE2 /* Pods */, + B6736FC417BDCCDA377E779D /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 68BDCAE923C3F7CB00D9C032 /* RunnerTests.xctest */, + F7151F74266057800028CB91 /* RunnerUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + B6736FC417BDCCDA377E779D /* Frameworks */ = { + isa = PBXGroup; + children = ( + 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */, + 27CC950C9005575711528C12 /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + C6FFB52F5C2B8A41A7E39DE2 /* Pods */ = { + isa = PBXGroup; + children = ( + 127772EEA7782174BE0D74B5 /* Pods-Runner.debug.xcconfig */, + C475C484BD510DD9CB2E403C /* Pods-Runner.release.xcconfig */, + F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */, + E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + F7151F75266057800028CB91 /* RunnerUITests */ = { + isa = PBXGroup; + children = ( + F7151F76266057800028CB91 /* FLTWebViewUITests.m */, + F7151F78266057800028CB91 /* Info.plist */, + ); + path = RunnerUITests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 68BDCAE823C3F7CB00D9C032 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 68BDCAF223C3F7CB00D9C032 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 53FD4CBDD9756D74B5A3B4C1 /* [CP] Check Pods Manifest.lock */, + 68BDCAE523C3F7CB00D9C032 /* Sources */, + 68BDCAE623C3F7CB00D9C032 /* Frameworks */, + 68BDCAE723C3F7CB00D9C032 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 68BDCAEF23C3F7CB00D9C032 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = webview_flutter_exampleTests; + productReference = 68BDCAE923C3F7CB00D9C032 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + B71376B4FB8384EF9D5F3F84 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; + F7151F73266057800028CB91 /* RunnerUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F7151F7B266057800028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */; + buildPhases = ( + F7151F70266057800028CB91 /* Sources */, + F7151F71266057800028CB91 /* Frameworks */, + F7151F72266057800028CB91 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F7151F7A266057800028CB91 /* PBXTargetDependency */, + ); + name = RunnerUITests; + productName = RunnerUITests; + productReference = F7151F74266057800028CB91 /* RunnerUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + DefaultBuildSystemTypeForWorkspace = Original; + LastUpgradeCheck = 1030; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 68BDCAE823C3F7CB00D9C032 = { + ProvisioningStyle = Automatic; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + F7151F73266057800028CB91 = { + CreatedOnToolsVersion = 12.5; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 68BDCAE823C3F7CB00D9C032 /* RunnerTests */, + F7151F73266057800028CB91 /* RunnerUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 68BDCAE723C3F7CB00D9C032 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F72266057800028CB91 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin\n"; + }; + 53FD4CBDD9756D74B5A3B4C1 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; + }; + B71376B4FB8384EF9D5F3F84 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 68BDCAE523C3F7CB00D9C032 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 334734012669319100DCC49E /* FLTWebViewTests.m in Sources */, + 334734022669319400DCC49E /* FLTWKNavigationDelegateTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F70266057800028CB91 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F7151F77266057800028CB91 /* FLTWebViewUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 68BDCAEF23C3F7CB00D9C032 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 68BDCAEE23C3F7CB00D9C032 /* PBXContainerItemProxy */; + }; + F7151F7A266057800028CB91 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F7151F79266057800028CB91 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 68BDCAF023C3F7CB00D9C032 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Debug; + }; + 68BDCAF123C3F7CB00D9C032 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Release; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.webviewFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.webviewFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + F7151F7C266057800028CB91 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_TARGET_NAME = Runner; + }; + name = Debug; + }; + F7151F7D266057800028CB91 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_TARGET_NAME = Runner; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 68BDCAF223C3F7CB00D9C032 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 68BDCAF023C3F7CB00D9C032 /* Debug */, + 68BDCAF123C3F7CB00D9C032 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F7151F7B266057800028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F7151F7C266057800028CB91 /* Debug */, + F7151F7D266057800028CB91 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..d7453a8ce862 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.h b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.h @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +@interface AppDelegate : FlutterAppDelegate + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..30b87969f44a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.m @@ -0,0 +1,17 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "AppDelegate.h" +#include "GeneratedPluginRegistrant.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..d36b1fab2d9d --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 000000000000..3d43d11e66f4 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000000..28c6bf03016f Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000000..f091b6b0bca8 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000000..4cde12118dda Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000000..d0ef06e7edb8 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000000..dcdc2306c285 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000000..c8f9ed8f5cee Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000000..75b2d164a5a9 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000000..c4df70d39da7 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000000..6a84f41e14e2 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000000..d0e1f5853602 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000000..0bedcf2fd467 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000000..89c2725b70f1 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000000..f2e259c7c939 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/Main.storyboard b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..a810c5a172c0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + webview_flutter_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/main.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/main.m new file mode 100644 index 000000000000..f97b9ef5c8a1 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/main.m @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import "AppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m new file mode 100644 index 000000000000..9d3a2aed64eb --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m @@ -0,0 +1,41 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import Flutter; +@import XCTest; +@import webview_flutter; + +// OCMock library doesn't generate a valid modulemap. +#import + +@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 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m new file mode 100644 index 000000000000..f8229935cbe6 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m @@ -0,0 +1,91 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import Flutter; +@import XCTest; +@import webview_flutter; + +// OCMock library doesn't generate a valid modulemap. +#import + +static bool feq(CGFloat a, CGFloat b) { return fabs(b - a) < FLT_EPSILON; } + +@interface FLTWebViewTests : XCTestCase + +@property(strong, nonatomic) NSObject *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 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/Info.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/FLTWebViewUITests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/FLTWebViewUITests.m new file mode 100644 index 000000000000..d193be745972 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/FLTWebViewUITests.m @@ -0,0 +1,101 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import XCTest; +@import os.log; + +@interface FLTWebViewUITests : XCTestCase +@property(nonatomic, strong) XCUIApplication* app; +@end + +@implementation FLTWebViewUITests + +- (void)setUp { + self.continueAfterFailure = NO; + + self.app = [[XCUIApplication alloc] init]; + [self.app launch]; +} + +- (void)testUserAgent { + XCUIApplication* app = self.app; + XCUIElement* menu = app.buttons[@"Show menu"]; + if (![menu waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find menu"); + } + [menu tap]; + + XCUIElement* userAgent = app.buttons[@"Show user agent"]; + if (![userAgent waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find Show user agent"); + } + NSPredicate* userAgentPredicate = + [NSPredicate predicateWithFormat:@"label BEGINSWITH 'User Agent: Mozilla/5.0 (iPhone; '"]; + XCUIElement* userAgentPopUp = [app.otherElements elementMatchingPredicate:userAgentPredicate]; + XCTAssertFalse(userAgentPopUp.exists); + [userAgent tap]; + if (![userAgentPopUp waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find user agent pop up"); + } +} + +- (void)testCache { + XCUIApplication* app = self.app; + XCUIElement* menu = app.buttons[@"Show menu"]; + if (![menu waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find menu"); + } + [menu tap]; + + XCUIElement* clearCache = app.buttons[@"Clear cache"]; + if (![clearCache waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find Clear cache"); + } + [clearCache tap]; + + [menu tap]; + + XCUIElement* listCache = app.buttons[@"List cache"]; + if (![listCache waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find List cache"); + } + [listCache tap]; + + XCUIElement* emptyCachePopup = app.otherElements[@"{\"cacheKeys\":[],\"localStorage\":{}}"]; + if (![emptyCachePopup waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find empty cache pop up"); + } + + [menu tap]; + XCUIElement* addCache = app.buttons[@"Add to cache"]; + if (![addCache waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find Add to cache"); + } + [addCache tap]; + [menu tap]; + + if (![listCache waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find List cache"); + } + [listCache tap]; + + XCUIElement* cachePopup = + app.otherElements[@"{\"cacheKeys\":[\"test_caches_entry\"],\"localStorage\":{\"test_" + @"localStorage\":\"dummy_entry\"}}"]; + if (![cachePopup waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find cache pop up"); + } +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/Info.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart new file mode 100644 index 000000000000..776e561a7b30 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart @@ -0,0 +1,724 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_wkwebview/webview_cupertino.dart'; + +void main() => runApp(MaterialApp(home: WebViewExample())); + +const String kNavigationExamplePage = ''' + +Navigation Delegate Example + +

+The navigation delegate is set to block navigation to the youtube website. +

+ + + +'''; + +class WebViewExample extends StatefulWidget { + final WebViewPlatform platform = CupertinoWebView(); + + @override + _WebViewExampleState createState() => _WebViewExampleState(); + + void onPageStarted(String url) { + print('Page started loading: $url'); + } + + void onPageFinished(String url) { + print('Page finished loading: $url'); + } + + void onProgress(int progress) { + print("WebView is loading (progress : $progress%)"); + } + + void onWebResourceError(WebResourceError error) { + print("Webview resource error encountered: ${error.description}"); + } +} + +class _WebViewExampleState extends State { + final Completer _controller = + Completer(); + late final JavascriptChannelRegistry _javascriptChannelRegistry; + late final _PlatformCallbacksHandler _platformCallbacksHandler; + + @override + void initState() { + super.initState(); + _javascriptChannelRegistry = JavascriptChannelRegistry(null); + _platformCallbacksHandler = _PlatformCallbacksHandler(widget); + } + + @override + void didUpdateWidget(WebViewExample oldWidget) { + super.didUpdateWidget(oldWidget); + _controller.future.then((WebViewController controller) { + _platformCallbacksHandler._widget = widget; + controller._updateWidget(widget); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Flutter WebView example'), + // This drop down menu demonstrates that Flutter widgets can be shown over the web view. + actions: [ + NavigationControls(_controller.future), + SampleMenu(_controller.future), + ], + ), + // We're using a Builder here so we have a context that is below the Scaffold + // to allow calling Scaffold.of(context) so we can show a snackbar. + body: Builder(builder: (BuildContext context) { + _javascriptChannelRegistry.updateJavascriptChannelsFromSet( + {_toasterJavascriptChannel(context)}); + return widget.platform.build( + context: context, + onWebViewPlatformCreated: + (WebViewPlatformController? webViewPlatformController) { + WebViewController controller = WebViewController._( + widget, + webViewPlatformController!, + _javascriptChannelRegistry, + ); + _controller.complete(controller); + }, + webViewPlatformCallbacksHandler: _platformCallbacksHandler, + creationParams: CreationParams( + initialUrl: 'https://flutter.dev', + webSettings: _webSettingsFromWidget(widget), + javascriptChannelNames: + _javascriptChannelRegistry.channels.keys.toSet(), + autoMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + javascriptChannelRegistry: _javascriptChannelRegistry, + ); + }), + floatingActionButton: favoriteButton(), + ); + } + + JavascriptChannel _toasterJavascriptChannel(BuildContext context) { + return JavascriptChannel( + name: 'Toaster', + onMessageReceived: (JavascriptMessage message) { + if (context != null) { + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + SnackBar(content: Text(message.message)), + ); + } + }); + } + + Widget favoriteButton() { + return FutureBuilder( + future: _controller.future, + builder: (BuildContext context, + AsyncSnapshot controller) { + if (controller.hasData) { + return FloatingActionButton( + onPressed: () async { + final String url = (await controller.data!.currentUrl())!; + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + SnackBar(content: Text('Favorited $url')), + ); + }, + child: const Icon(Icons.favorite), + ); + } + return Container(); + }); + } +} + +enum MenuOptions { + showUserAgent, + listCookies, + clearCookies, + addToCache, + listCache, + clearCache, + navigationDelegate, +} + +class SampleMenu extends StatelessWidget { + SampleMenu(this.controller); + + final Future controller; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: controller, + builder: + (BuildContext context, AsyncSnapshot controller) { + return PopupMenuButton( + onSelected: (MenuOptions value) { + switch (value) { + case MenuOptions.showUserAgent: + _onShowUserAgent(controller.data!, context); + break; + case MenuOptions.listCookies: + _onListCookies(controller.data!, context); + break; + case MenuOptions.clearCookies: + _onClearCookies(controller.data!, context); + break; + case MenuOptions.addToCache: + _onAddToCache(controller.data!, context); + break; + case MenuOptions.listCache: + _onListCache(controller.data!, context); + break; + case MenuOptions.clearCache: + _onClearCache(controller.data!, context); + break; + case MenuOptions.navigationDelegate: + _onNavigationDelegateExample(controller.data!, context); + break; + } + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: MenuOptions.showUserAgent, + child: const Text('Show user agent'), + enabled: controller.hasData, + ), + const PopupMenuItem( + value: MenuOptions.listCookies, + child: Text('List cookies'), + ), + const PopupMenuItem( + value: MenuOptions.clearCookies, + child: Text('Clear cookies'), + ), + const PopupMenuItem( + value: MenuOptions.addToCache, + child: Text('Add to cache'), + ), + const PopupMenuItem( + value: MenuOptions.listCache, + child: Text('List cache'), + ), + const PopupMenuItem( + value: MenuOptions.clearCache, + child: Text('Clear cache'), + ), + const PopupMenuItem( + value: MenuOptions.navigationDelegate, + child: Text('Navigation Delegate example'), + ), + ], + ); + }, + ); + } + + void _onShowUserAgent( + WebViewController controller, BuildContext context) async { + // Send a message with the user agent string to the Toaster JavaScript channel we registered + // with the WebView. + await controller.evaluateJavascript( + 'Toaster.postMessage("User Agent: " + navigator.userAgent);'); + } + + void _onListCookies( + WebViewController controller, BuildContext context) async { + final String cookies = + await controller.evaluateJavascript('document.cookie'); + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar(SnackBar( + content: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Cookies:'), + _getCookieList(cookies), + ], + ), + )); + } + + void _onAddToCache(WebViewController controller, BuildContext context) async { + await controller.evaluateJavascript( + 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar(const SnackBar( + content: Text('Added a test entry to cache.'), + )); + } + + void _onListCache(WebViewController controller, BuildContext context) async { + await controller.evaluateJavascript('caches.keys()' + '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' + '.then((caches) => Toaster.postMessage(caches))'); + } + + void _onClearCache(WebViewController controller, BuildContext context) async { + await controller.clearCache(); + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar(const SnackBar( + content: Text("Cache cleared."), + )); + } + + void _onClearCookies( + WebViewController controller, BuildContext context) async { + final bool hadCookies = await controller._widget.platform.clearCookies(); + String message = 'There were cookies. Now, they are gone!'; + if (!hadCookies) { + message = 'There are no cookies.'; + } + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar(SnackBar( + content: Text(message), + )); + } + + void _onNavigationDelegateExample( + WebViewController controller, BuildContext context) async { + final String contentBase64 = + base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); + await controller.loadUrl('data:text/html;base64,$contentBase64'); + } + + Widget _getCookieList(String cookies) { + if (cookies == null || cookies == '""') { + return Container(); + } + final List cookieList = cookies.split(';'); + final Iterable cookieWidgets = + cookieList.map((String cookie) => Text(cookie)); + return Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: cookieWidgets.toList(), + ); + } +} + +class NavigationControls extends StatelessWidget { + const NavigationControls(this._webViewControllerFuture) + : assert(_webViewControllerFuture != null); + + final Future _webViewControllerFuture; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _webViewControllerFuture, + builder: + (BuildContext context, AsyncSnapshot snapshot) { + final bool webViewReady = + snapshot.connectionState == ConnectionState.done; + final WebViewController? controller = snapshot.data; + if (controller == null) return Container(); + return Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: !webViewReady + ? null + : () async { + if (await controller.canGoBack()) { + await controller.goBack(); + } else { + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + const SnackBar(content: Text("No back history item")), + ); + return; + } + }, + ), + IconButton( + icon: const Icon(Icons.arrow_forward_ios), + onPressed: !webViewReady + ? null + : () async { + if (await controller.canGoForward()) { + await controller.goForward(); + } else { + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + const SnackBar( + content: Text("No forward history item")), + ); + return; + } + }, + ), + IconButton( + icon: const Icon(Icons.replay), + onPressed: !webViewReady + ? null + : () { + controller.reload(); + }, + ), + ], + ); + }, + ); + } +} + +/// 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._javascriptChannelRegistry, + ) : assert(_webViewPlatformController != null) { + _settings = _webSettingsFromWidget(_widget); + } + + final JavascriptChannelRegistry _javascriptChannelRegistry; + + final WebViewPlatformController _webViewPlatformController; + + late WebSettings _settings; + + WebViewExample _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 loadUrl( + String url, { + Map? 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 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 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 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 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 goForward() { + return _webViewPlatformController.goForward(); + } + + /// Reloads the current URL. + Future 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 clearCache() async { + await _webViewPlatformController.clearCache(); + return reload(); + } + + Future _updateWidget(WebViewExample widget) async { + _widget = widget; + await _updateSettings(_webSettingsFromWidget(widget)); + await _updateJavascriptChannels(_javascriptChannelRegistry.channels.values + .toSet()); // TODO: CHECK WITH MAURITS IF POINTLESS. PROBABLY REMOVE THIS? + } + + Future _updateSettings(WebSettings newSettings) { + final WebSettings update = + _clearUnchangedWebSettings(_settings, newSettings); + _settings = newSettings; + return _webViewPlatformController.updateSettings(update); + } + + Future _updateJavascriptChannels( + Set? newChannels) async { + final Set currentChannels = + _javascriptChannelRegistry.channels.keys.toSet(); + final Set newChannelNames = _extractChannelNames(newChannels); + final Set channelsToAdd = + newChannelNames.difference(currentChannels); + final Set channelsToRemove = + currentChannels.difference(newChannelNames); + if (channelsToRemove.isNotEmpty) { + await _webViewPlatformController + .removeJavascriptChannels(channelsToRemove); + } + if (channelsToAdd.isNotEmpty) { + await _webViewPlatformController.addJavascriptChannels(channelsToAdd); + } + _javascriptChannelRegistry.updateJavascriptChannelsFromSet(newChannels); + } + + /// Evaluates a JavaScript expression in the context of the current page. + /// + /// On Android returns the evaluation result as a JSON formatted string. + /// + /// On iOS depending on the value type the return value would be one of: + /// + /// - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100'). + /// - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.'). + /// - Other non-primitive types are not supported on iOS and will complete the Future with an error. + /// + /// The Future completes with an error if a JavaScript error occurred, or on iOS, if the type of the + /// evaluated expression is not supported as described above. + /// + /// When evaluating Javascript in a [WebView], it is best practice to wait for + /// the [WebView.onPageFinished] callback. This guarantees all the Javascript + /// embedded in the main frame HTML has been loaded. + Future evaluateJavascript(String javascriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + } + // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. + // https://github.com/flutter/flutter/issues/26431 + // ignore: strong_mode_implicit_dynamic_method + return _webViewPlatformController.evaluateJavascript(javascriptString); + } + + /// Returns the title of the currently loaded page. + Future getTitle() { + return _webViewPlatformController.getTitle(); + } + + /// Sets the WebView's content scroll position. + /// + /// The parameters `x` and `y` specify the scroll position in WebView pixels. + Future 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 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 getScrollX() { + return _webViewPlatformController.getScrollX(); + } + + /// Return the vertical scroll position, in WebView pixels, of this view. + /// + /// Scroll position is measured from top. + Future getScrollY() { + return _webViewPlatformController.getScrollY(); + } +} + +WebSettings _webSettingsFromWidget(WebViewExample widget) { + return WebSettings( + javascriptMode: JavascriptMode.unrestricted, + hasNavigationDelegate: true, + hasProgressTracking: widget.onProgress != null, + debuggingEnabled: false, + gestureNavigationEnabled: true, + allowsInlineMediaPlayback: false, + userAgent: WebSetting.of(null), + ); +} + +class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { + _PlatformCallbacksHandler(this._widget); + + WebViewExample _widget; + + @override + FutureOr onNavigationRequest({ + required String url, + required bool isForMainFrame, + }) async { + if (url.startsWith('https://www.youtube.com/')) { + print('blocking navigation to $url'); + return false; + } + print('allowing navigation to $url'); + return true; + } + + @override + void onPageStarted(String url) { + if (_widget.onPageStarted != null) { + _widget.onPageStarted(url); + } + } + + @override + void onPageFinished(String url) { + if (_widget.onPageFinished != null) { + _widget.onPageFinished(url); + } + } + + @override + void onProgress(int progress) { + if (_widget.onProgress != null) { + _widget.onProgress(progress); + } + } + + void onWebResourceError(WebResourceError error) { + if (_widget.onWebResourceError != null) { + _widget.onWebResourceError(error); + } + } +} + +/// Information about a navigation action that is about to be executed. +class NavigationRequest { + NavigationRequest._({required this.url, required this.isForMainFrame}); + + /// The URL that will be loaded if the navigation is executed. + final String url; + + /// Whether the navigation request is to be loaded as the main frame. + final bool isForMainFrame; + + @override + String toString() { + return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)'; + } +} + +// 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); + } +} + +// This method assumes that no fields in `currentValue` are null. +WebSettings _clearUnchangedWebSettings( + WebSettings currentValue, WebSettings newValue) { + assert(currentValue.javascriptMode != null); + assert(currentValue.hasNavigationDelegate != null); + assert(currentValue.hasProgressTracking != null); + assert(currentValue.debuggingEnabled != null); + assert(currentValue.userAgent != null); + assert(newValue.javascriptMode != null); + assert(newValue.hasNavigationDelegate != null); + assert(newValue.debuggingEnabled != null); + assert(newValue.userAgent != null); + + JavascriptMode? javascriptMode; + bool? hasNavigationDelegate; + bool? hasProgressTracking; + bool? debuggingEnabled; + WebSetting userAgent = WebSetting.absent(); + if (currentValue.javascriptMode != newValue.javascriptMode) { + javascriptMode = newValue.javascriptMode; + } + if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) { + hasNavigationDelegate = newValue.hasNavigationDelegate; + } + if (currentValue.hasProgressTracking != newValue.hasProgressTracking) { + hasProgressTracking = newValue.hasProgressTracking; + } + if (currentValue.debuggingEnabled != newValue.debuggingEnabled) { + debuggingEnabled = newValue.debuggingEnabled; + } + if (currentValue.userAgent != newValue.userAgent) { + userAgent = newValue.userAgent; + } + + return WebSettings( + javascriptMode: javascriptMode, + hasNavigationDelegate: hasNavigationDelegate, + hasProgressTracking: hasProgressTracking, + debuggingEnabled: debuggingEnabled, + userAgent: userAgent, + ); +} + +Set _extractChannelNames(Set? channels) { + final Set channelNames = channels == null + ? {} + : channels.map((JavascriptChannel channel) => channel.name).toSet(); + return channelNames; +} + +/// Callback type for handling messages sent from Javascript running in a web view. +typedef void JavascriptMessageHandler(JavascriptMessage message); + +/// A decision on how to handle a navigation request. +enum NavigationDecision { + /// Prevent the navigation from taking place. + prevent, + + /// Allow the navigation to take place. + navigate, +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml new file mode 100644 index 000000000000..0be80403f35e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml @@ -0,0 +1,26 @@ +name: webview_flutter_wkwebview_example +description: Demonstrates how to use the webview_flutter_wkwebview plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + # TODO (mvanbeusekom): Replace with pub.dev version once published + webview_flutter_platform_interface: + path: ../../webview_flutter_platform_interface + webview_flutter_wkwebview: + # When depending on this package from a real application you should use: + # webview_flutter_wkwebview: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + +dev_dependencies: + pedantic: ^1.10.0 + +flutter: + uses-material-design: true \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Assets/.gitkeep b/packages/webview_flutter/webview_flutter_wkwebview/ios/Assets/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h new file mode 100644 index 000000000000..8fe331875250 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTCookieManager : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m new file mode 100644 index 000000000000..f4783ffb4123 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m @@ -0,0 +1,49 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTCookieManager.h" + +@implementation FLTCookieManager { +} + ++ (void)registerWithRegistrar:(NSObject *)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 *websiteDataTypes = [NSSet setWithObject:WKWebsiteDataTypeCookies]; + WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore]; + + void (^deleteAndNotify)(NSArray *) = + ^(NSArray *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 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.h new file mode 100644 index 000000000000..31edadc8cc05 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.h @@ -0,0 +1,21 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTWKNavigationDelegate : NSObject + +- (instancetype)initWithChannel:(FlutterMethodChannel*)channel; + +/** + * Whether to delegate navigation decisions over the method channel. + */ +@property(nonatomic, assign) BOOL hasDartNavigationDelegate; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.m new file mode 100644 index 000000000000..8b7ee7d0cfb7 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.m @@ -0,0 +1,116 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "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 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.h new file mode 100644 index 000000000000..96af4ef6c578 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.h @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTWKProgressionDelegate : NSObject + +- (instancetype)initWithWebView:(WKWebView *)webView channel:(FlutterMethodChannel *)channel; + +- (void)stopObservingProgress:(WKWebView *)webView; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.m new file mode 100644 index 000000000000..8e7af4649aa0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.m @@ -0,0 +1,41 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTWKProgressionDelegate.h" + +NSString *const FLTWKEstimatedProgressKeyPath = @"estimatedProgress"; + +@implementation FLTWKProgressionDelegate { + FlutterMethodChannel *_methodChannel; +} + +- (instancetype)initWithWebView:(WKWebView *)webView channel:(FlutterMethodChannel *)channel { + self = [super init]; + if (self) { + _methodChannel = channel; + [webView addObserver:self + forKeyPath:FLTWKEstimatedProgressKeyPath + options:NSKeyValueObservingOptionNew + context:nil]; + } + return self; +} + +- (void)stopObservingProgress:(WKWebView *)webView { + [webView removeObserver:self forKeyPath:FLTWKEstimatedProgressKeyPath]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if ([keyPath isEqualToString:FLTWKEstimatedProgressKeyPath]) { + NSNumber *newValue = + change[NSKeyValueChangeNewKey] ?: 0; // newValue is anywhere between 0.0 and 1.0 + int newValueAsInt = [newValue floatValue] * 100; // Anywhere between 0 and 100 + [_methodChannel invokeMethod:@"onProgress" arguments:@{@"progress" : @(newValueAsInt)}]; + } +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.h new file mode 100644 index 000000000000..2a80c7d886f2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.h @@ -0,0 +1,8 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +@interface FLTWebViewFlutterPlugin : NSObject +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m new file mode 100644 index 000000000000..9f01416acc6a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTWebViewFlutterPlugin.h" +#import "FLTCookieManager.h" +#import "FlutterWebView.h" + +@implementation FLTWebViewFlutterPlugin + ++ (void)registerWithRegistrar:(NSObject*)registrar { + FLTWebViewFactory* webviewFactory = + [[FLTWebViewFactory alloc] initWithMessenger:registrar.messenger]; + [registrar registerViewFactory:webviewFactory withId:@"plugins.flutter.io/webview"]; + [FLTCookieManager registerWithRegistrar:registrar]; +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h new file mode 100644 index 000000000000..6e795f7d1528 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h @@ -0,0 +1,32 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTWebViewController : NSObject + +- (instancetype)initWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args + binaryMessenger:(NSObject*)messenger; + +- (UIView*)view; +@end + +@interface FLTWebViewFactory : NSObject +- (instancetype)initWithMessenger:(NSObject*)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 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m new file mode 100644 index 000000000000..c6d926d3cfc2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m @@ -0,0 +1,491 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FlutterWebView.h" +#import "FLTWKNavigationDelegate.h" +#import "FLTWKProgressionDelegate.h" +#import "JavaScriptChannelHandler.h" + +@implementation FLTWebViewFactory { + NSObject* _messenger; +} + +- (instancetype)initWithMessenger:(NSObject*)messenger { + self = [super init]; + if (self) { + _messenger = messenger; + } + return self; +} + +- (NSObject*)createArgsCodec { + return [FlutterStandardMessageCodec sharedInstance]; +} + +- (NSObject*)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; + FLTWKProgressionDelegate* _progressionDelegate; +} + +- (instancetype)initWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args + binaryMessenger:(NSObject*)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* settings = args[@"settings"]; + + WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init]; + [self applyConfigurationSettings:settings toConfiguration:configuration]; + 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; +} + +- (void)dealloc { + if (_progressionDelegate != nil) { + [_progressionDelegate stopObservingProgress:_webView]; + } +} + +- (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(@(canGoBack)); +} + +- (void)onCanGoForward:(FlutterMethodCall*)call result:(FlutterResult)result { + BOOL canGoForward = [_webView canGoForward]; + result(@(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(@(offsetX)); +} + +- (void)getScrollY:(FlutterMethodCall*)call result:(FlutterResult)result { + int offsetY = _webView.scrollView.contentOffset.y; + result(@(offsetY)); +} + +// Returns nil when successful, or an error message when one or more keys are unknown. +- (NSString*)applySettings:(NSDictionary*)settings { + NSMutableArray* 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:@"hasProgressTracking"]) { + NSNumber* hasProgressTrackingValue = settings[key]; + bool hasProgressTracking = [hasProgressTrackingValue boolValue]; + if (hasProgressTracking) { + _progressionDelegate = [[FLTWKProgressionDelegate alloc] initWithWebView:_webView + channel:_channel]; + } + } 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)applyConfigurationSettings:(NSDictionary*)settings + toConfiguration:(WKWebViewConfiguration*)configuration { + NSAssert(configuration != _webView.configuration, + @"configuration needs to be updated before webView.configuration."); + for (NSString* key in settings) { + if ([key isEqualToString:@"allowsInlineMediaPlayback"]) { + NSNumber* allowsInlineMediaPlayback = settings[key]; + configuration.allowsInlineMediaPlayback = [allowsInlineMediaPlayback boolValue]; + } + } +} + +- (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 if (@available(iOS 9.0, *)) { + configuration.requiresUserActionForMediaPlayback = true; + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + configuration.mediaPlaybackRequiresUserAction = true; +#pragma clang diagnostic pop + } + break; + case 1: // always_allow + if (@available(iOS 10.0, *)) { + configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; + } else if (@available(iOS 9.0, *)) { + configuration.requiresUserActionForMediaPlayback = false; + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + configuration.mediaPlaybackRequiresUserAction = false; +#pragma clang diagnostic pop + } + break; + default: + NSLog(@"webview_flutter: unknown auto media playback policy: %@", policy); + } +} + +- (bool)loadRequest:(NSDictionary*)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*)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 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.h new file mode 100644 index 000000000000..a0a5ec657295 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.h @@ -0,0 +1,17 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTJavaScriptChannel : NSObject + +- (instancetype)initWithMethodChannel:(FlutterMethodChannel*)methodChannel + javaScriptChannelName:(NSString*)javaScriptChannelName; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.m new file mode 100644 index 000000000000..ec9a363a4b2e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.m @@ -0,0 +1,36 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "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 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec b/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec new file mode 100644 index 000000000000..86da7729d870 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'webview_flutter_wkwebview' + 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', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } +end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_cupertino.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_cupertino.dart new file mode 100644 index 000000000000..4eb17fd97fa4 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_cupertino.dart @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +/// Builds an 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({ + required BuildContext context, + required CreationParams creationParams, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, + required JavascriptChannelRegistry javascriptChannelRegistry, + }) { + return UiKitView( + viewType: 'plugins.flutter.io/webview', + onPlatformViewCreated: (int id) { + if (onWebViewPlatformCreated == null) { + return; + } + onWebViewPlatformCreated(MethodChannelWebViewPlatform( + id, webViewPlatformCallbacksHandler, javascriptChannelRegistry)); + }, + gestureRecognizers: gestureRecognizers, + creationParams: + MethodChannelWebViewPlatform.creationParamsToMap(creationParams), + creationParamsCodec: const StandardMessageCodec(), + ); + } + + @override + Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml new file mode 100644 index 000000000000..54a76891b31b --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml @@ -0,0 +1,33 @@ +name: webview_flutter_wkwebview +description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control. +repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_wkwebview +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 +version: 0.0.1 +# TODO (mvanbeusekom): Remove this line when final version of webview_flutter_platform_interface is published +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +flutter: + plugin: + implements: webview_flutter + platforms: + ios: + pluginClass: FLTWebViewFlutterPlugin + +dependencies: + flutter: + sdk: flutter + + # TODO (mvanbeusekom): Replace with pub.dev version once published + webview_flutter_platform_interface: + path: ../webview_flutter_platform_interface + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + pedantic: ^1.10.0