From fed99ec0e94148c7200a355eb3357ebf507fd7de Mon Sep 17 00:00:00 2001 From: Lorenzo Pichilli Date: Tue, 29 Oct 2019 17:51:55 +0100 Subject: [PATCH] added debuggingEnabled option, fixed InputConnection error on Android --- .idea/workspace.xml | 204 +++++++++--------- CHANGELOG.md | 4 +- .../flutter_inappbrowser/FlutterWebView.java | 28 ++- .../FlutterWebViewFactory.java | 7 +- .../InAppBrowserFlutterPlugin.java | 2 +- .../InAppWebView/DisplayListenerProxy.java | 122 +++++++++++ .../InAppWebView/InAppWebView.java | 11 +- .../InAppWebView/InAppWebViewClient.java | 57 ++++- .../InAppWebView/InAppWebViewOptions.java | 1 + .../InAppWebView/InputAwareWebView.java | 169 +++++++++++++++ ...readedInputConnectionProxyAdapterView.java | 99 +++++++++ example/lib/inline_example.screen.dart | 1 + ios/Classes/InAppWebView.swift | 66 +++++- ios/Classes/InAppWebViewOptions.swift | 3 +- lib/src/in_app_browser.dart | 9 + lib/src/in_app_webview.dart | 16 ++ lib/src/types.dart | 31 ++- lib/src/webview_options.dart | 3 +- 18 files changed, 704 insertions(+), 129 deletions(-) create mode 100644 android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/DisplayListenerProxy.java create mode 100644 android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/InputAwareWebView.java create mode 100644 android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/ThreadedInputConnectionProxyAdapterView.java diff --git a/.idea/workspace.xml b/.idea/workspace.xml index e655e257..1e9fd305 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -15,13 +15,20 @@ + + + + - - + + + + - + + @@ -45,11 +52,11 @@ - + - - + + @@ -57,8 +64,8 @@ - - + + @@ -78,20 +85,11 @@ - - - - - - - - - - - + + @@ -102,8 +100,8 @@ - - + + @@ -114,8 +112,8 @@ - - + + @@ -132,11 +130,11 @@ - + - - + + @@ -156,7 +154,6 @@ - minimumFontSize defaultWebpagePreferences contentBlockers preferredContentMode @@ -183,9 +180,10 @@ ConsoleMessageLevel ConsoleMessage appCa - iOSInAppWebViewUserPreferredContentMode - SafeBrowsingResponse onSafeBrowsingHit + SafeBrowsingResponse + onReceivedHttpAuthRequest + iOSInAppWebViewUserPreferredContentMode activity.getPreferences(0) @@ -244,19 +242,19 @@ @@ -502,8 +500,8 @@ - - + + @@ -513,7 +511,7 @@ - + @@ -521,7 +519,7 @@ - + @@ -757,13 +755,6 @@ - - - - - - - @@ -802,10 +793,37 @@ - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -816,10 +834,37 @@ - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -833,67 +878,20 @@ - - - - - - - - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 255120d8..f937ebed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,17 +13,19 @@ - Added `onLoadResourceCustomScheme` event and `resourceCustomSchemes` option to set custom schemes that WebView must handle to load resources - Added `onTargetBlank` event and `useOnTargetBlank` option to manage links with `target="_blank"` - Added `ContentBlocker`, `ContentBlockerTrigger` and `ContentBlockerAction` classes and the `contentBlockers` option that allows to define a set of rules to use to block content in the WebView -- Added new WebView option `minimumFontSize` +- Added new WebView options: `minimumFontSize`, `debuggingEnabled` - Added new Android WebView options: `allowContentAccess`, `allowFileAccess`, `allowFileAccessFromFileURLs`, `allowUniversalAccessFromFileURLs`, `appCacheEnabled`, `appCachePath`, `blockNetworkImage`, `blockNetworkLoads`, `cacheMode`, `cursiveFontFamily`, `defaultFixedFontSize`, `defaultFontSize`, `defaultTextEncodingName`, `disabledActionModeMenuItems`, `fantasyFontFamily`, `fixedFontFamily`, `forceDark`, `geolocationEnabled`, `layoutAlgorithm`, `loadWithOverviewMode`, `loadsImagesAutomatically`, `minimumLogicalFontSize`, `needInitialFocus`, `offscreenPreRaster`, `sansSerifFontFamily`, `serifFontFamily`, `standardFontFamily` - Added new iOS WebView options: `applicationNameForUserAgent`, `isFraudulentWebsiteWarningEnabled`, `selectionGranularity`, `dataDetectorTypes`, `preferredContentMode` - Added `onGeolocationPermissionsShowPrompt` event and `GeolocationPermissionShowPromptResponse` class (available only for Android) - Added `startSafeBrowsing`, `setSafeBrowsingWhitelist` and `getSafeBrowsingPrivacyPolicyUrl` methods (available only for Android) - Added `onSafeBrowsingHit` event (available only for Android) - Added `onJsAlert`, `onJsConfirm` and `onJsPrompt` events to manage javascript popup dialogs +- Fixed `InputConnection` error on Android ### BREAKING CHANGES - Deleted `WebResourceRequest` class - Updated `WebResourceResponse` class +- Updated `ConsoleMessageLevel` class - Updated `onLoadResource` event - WebView options are now available with the new corresponding classes: `InAppWebViewOptions`, `AndroidInAppWebViewOptions`, `iOSInAppWebViewOptions`, `InAppBrowserOptions`, `AndroidInAppBrowserOptions`, `iOSInAppBrowserOptions`, `AndroidChromeCustomTabsOptions` and `iOSChromeCustomTabsOptions` diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/FlutterWebView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/FlutterWebView.java index 8a3b7c0b..de5c6720 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/FlutterWebView.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/FlutterWebView.java @@ -3,21 +3,26 @@ package com.pichillilorenzo.flutter_inappbrowser; import android.Manifest; import android.app.Activity; import android.content.Context; +import android.hardware.display.DisplayManager; import android.os.Build; +import android.os.Handler; import android.util.Log; import android.view.View; import android.webkit.WebChromeClient; import android.webkit.WebView; import android.webkit.WebViewClient; +import com.pichillilorenzo.flutter_inappbrowser.InAppWebView.DisplayListenerProxy; import com.pichillilorenzo.flutter_inappbrowser.InAppWebView.InAppWebView; import com.pichillilorenzo.flutter_inappbrowser.InAppWebView.InAppWebViewOptions; +import com.pichillilorenzo.flutter_inappbrowser.InAppWebView.InputAwareWebView; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; +import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import static io.flutter.plugin.common.MethodChannel.MethodCallHandler; @@ -31,14 +36,19 @@ public class FlutterWebView implements PlatformView, MethodCallHandler { public final Activity activity; public InAppWebView webView; - public MethodChannel channel; + public final MethodChannel channel; public final Registrar registrar; - public FlutterWebView(Registrar registrar, int id, HashMap params) { + public FlutterWebView(Registrar registrar, int id, HashMap params, View containerView) { this.registrar = registrar; this.activity = registrar.activity(); + DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy(); + DisplayManager displayManager = + (DisplayManager) this.registrar.context().getSystemService(Context.DISPLAY_SERVICE); + displayListenerProxy.onPreWebViewInitialization(displayManager); + String initialUrl = (String) params.get("initialUrl"); String initialFile = (String) params.get("initialFile"); Map initialData = (Map) params.get("initialData"); @@ -48,7 +58,9 @@ public class FlutterWebView implements PlatformView, MethodCallHandler { InAppWebViewOptions options = new InAppWebViewOptions(); options.parse(initialOptions); - webView = new InAppWebView(registrar, this, id, options); + webView = new InAppWebView(registrar, this, id, options, containerView); + displayListenerProxy.onPostWebViewInitialization(displayManager); + webView.prepare(); channel = new MethodChannel(registrar.messenger(), "com.pichillilorenzo/flutter_inappwebview_" + id); @@ -257,9 +269,15 @@ public class FlutterWebView implements PlatformView, MethodCallHandler { } @Override - public void onInputConnectionLocked() {} + public void onInputConnectionLocked() { + if (webView.inAppBrowserActivity == null) + webView.lockInputConnection(); + } @Override - public void onInputConnectionUnlocked() {} + public void onInputConnectionUnlocked() { + if (webView.inAppBrowserActivity == null) + webView.unlockInputConnection(); + } } \ No newline at end of file diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/FlutterWebViewFactory.java b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/FlutterWebViewFactory.java index abd42684..a365e89f 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/FlutterWebViewFactory.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/FlutterWebViewFactory.java @@ -1,6 +1,7 @@ package com.pichillilorenzo.flutter_inappbrowser; import android.content.Context; +import android.view.View; import java.util.HashMap; @@ -11,16 +12,18 @@ import io.flutter.plugin.platform.PlatformViewFactory; public class FlutterWebViewFactory extends PlatformViewFactory { private final Registrar registrar; + private final View containerView; - public FlutterWebViewFactory(Registrar registrar) { + public FlutterWebViewFactory(Registrar registrar, View containerView) { super(StandardMessageCodec.INSTANCE); this.registrar = registrar; + this.containerView = containerView; } @Override public PlatformView create(Context context, int id, Object args) { HashMap params = (HashMap) args; - return new FlutterWebView(registrar, id, params); + return new FlutterWebView(registrar, id, params, containerView); } } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppBrowserFlutterPlugin.java b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppBrowserFlutterPlugin.java index ea08456c..0cc98117 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppBrowserFlutterPlugin.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppBrowserFlutterPlugin.java @@ -86,7 +86,7 @@ public class InAppBrowserFlutterPlugin implements MethodCallHandler { registrar .platformViewRegistry() .registerViewFactory( - "com.pichillilorenzo/flutter_inappwebview", new FlutterWebViewFactory(registrar)); + "com.pichillilorenzo/flutter_inappwebview", new FlutterWebViewFactory(registrar, registrar.view())); } } diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/DisplayListenerProxy.java b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/DisplayListenerProxy.java new file mode 100644 index 00000000..25f4e29d --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/DisplayListenerProxy.java @@ -0,0 +1,122 @@ +package com.pichillilorenzo.flutter_inappbrowser.InAppWebView; + +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. + * https://github.com/flutter/plugins/blob/master/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java + */ +@TargetApi(Build.VERSION_CODES.KITKAT) +public +class DisplayListenerProxy { + private static final String TAG = "DisplayListenerProxy"; + + private ArrayList listenersBeforeWebView; + + /** Should be called prior to the webview's initialization. */ + public void onPreWebViewInitialization(DisplayManager displayManager) { + listenersBeforeWebView = yoinkDisplayListeners(displayManager); + } + + /** Should be called after the webview's initialization. */ + public 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<>(); + } + } +} \ No newline at end of file diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/InAppWebView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/InAppWebView.java index a3d4216a..a4722154 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/InAppWebView.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/InAppWebView.java @@ -10,9 +10,9 @@ import android.util.AttributeSet; import android.util.JsonReader; import android.util.JsonToken; import android.util.Log; +import android.view.View; import android.webkit.CookieManager; import android.webkit.DownloadListener; -import android.webkit.JsResult; import android.webkit.ValueCallback; import android.webkit.WebBackForwardList; import android.webkit.WebHistoryItem; @@ -41,7 +41,7 @@ import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.PluginRegistry; import okhttp3.OkHttpClient; -public class InAppWebView extends WebView { +final public class InAppWebView extends InputAwareWebView { static final String LOG_TAG = "InAppWebView"; @@ -107,8 +107,8 @@ public class InAppWebView extends WebView { super(context, attrs, defaultStyle); } - public InAppWebView(PluginRegistry.Registrar registrar, Object obj, int id, InAppWebViewOptions options) { - super(registrar.activeContext()); + public InAppWebView(PluginRegistry.Registrar registrar, Object obj, int id, InAppWebViewOptions options, View containerView) { + super(registrar.activeContext(), containerView); this.registrar = registrar; if (obj instanceof InAppBrowserActivity) this.inAppBrowserActivity = (InAppBrowserActivity) obj; @@ -146,6 +146,9 @@ public class InAppWebView extends WebView { WebSettings settings = getSettings(); settings.setJavaScriptEnabled(options.javaScriptEnabled); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + setWebContentsDebuggingEnabled(options.debuggingEnabled); + } settings.setJavaScriptCanOpenWindowsAutomatically(options.javaScriptCanOpenWindowsAutomatically); settings.setBuiltInZoomControls(options.builtInZoomControls); settings.setDisplayZoomControls(options.displayZoomControls); diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/InAppWebViewClient.java b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/InAppWebViewClient.java index fc31fd3f..4541816d 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/InAppWebViewClient.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/InAppWebViewClient.java @@ -17,6 +17,7 @@ import android.webkit.WebResourceRequest; import android.webkit.WebResourceResponse; import android.webkit.WebView; import android.webkit.WebViewClient; +import android.webkit.WebViewDatabase; import androidx.annotation.RequiresApi; @@ -296,9 +297,56 @@ public class InAppWebViewClient extends WebViewClient { * On received http auth request. */ @Override - public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) { - // By default handle 401 like we'd normally do! - super.onReceivedHttpAuthRequest(view, handler, host, realm); + public void onReceivedHttpAuthRequest(final WebView view, final HttpAuthHandler handler, final String host, final String realm) { + Map obj = new HashMap<>(); + if (inAppBrowserActivity != null) + obj.put("uuid", inAppBrowserActivity.uuid); + obj.put("host", host); + obj.put("realm", realm); + + getChannel().invokeMethod("onReceivedHttpAuthRequest", obj, new MethodChannel.Result() { + @Override + public void success(Object response) { + if (response != null) { + Map responseMap = (Map) response; + Integer action = (Integer) responseMap.get("action"); + + Log.d(LOG_TAG, "\n\naction: " + action); + + if (action != null) { + switch (action) { + case 0: + handler.cancel(); + return; + case 1: + String username = (String) responseMap.get("username"); + String password = (String) responseMap.get("password"); + Boolean permanentPersistence = (Boolean) responseMap.get("permanentPersistence"); + if (permanentPersistence != null && permanentPersistence && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + WebViewDatabase.getInstance(view.getContext()).setHttpAuthUsernamePassword(host, realm, username, password); + } + handler.proceed(username, password); + return; + case 2: + handler.useHttpAuthUsernamePassword(); + return; + } + } + } + + handler.cancel(); + } + + @Override + public void error(String s, String s1, Object o) { + Log.e(LOG_TAG, s + ", " + s1); + } + + @Override + public void notImplemented() { + handler.cancel(); + } + }); } @Override @@ -324,9 +372,6 @@ public class InAppWebViewClient extends WebViewClient { Boolean report = (Boolean) responseMap.get("report"); Integer action = (Integer) responseMap.get("action"); - Log.d(LOG_TAG, "\n\nreport: " + report); - Log.d(LOG_TAG, "\n\naction: " + action); - report = report != null ? report : true; if (action != null) { diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/InAppWebViewOptions.java b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/InAppWebViewOptions.java index e240cedf..6bbfdf7d 100644 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/InAppWebViewOptions.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/InAppWebViewOptions.java @@ -19,6 +19,7 @@ public class InAppWebViewOptions extends Options { public boolean clearCache = false; public String userAgent = ""; public boolean javaScriptEnabled = true; + public boolean debuggingEnabled = false; public boolean javaScriptCanOpenWindowsAutomatically = false; public boolean mediaPlaybackRequiresUserGesture = true; public Integer textZoom = 100; diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/InputAwareWebView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/InputAwareWebView.java new file mode 100644 index 00000000..7c6754d7 --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/InputAwareWebView.java @@ -0,0 +1,169 @@ +package com.pichillilorenzo.flutter_inappbrowser.InAppWebView; + +import static android.content.Context.INPUT_METHOD_SERVICE; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.webkit.WebView; + +/** + * A WebView subclass that mirrors the same implementation hacks that the system WebView does in + * order to correctly create an InputConnection. + * + * https://github.com/flutter/plugins/blob/master/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java + */ +public class InputAwareWebView extends WebView { + public View containerView; + + private View threadedInputConnectionProxyView; + private ThreadedInputConnectionProxyAdapterView proxyAdapterView; + + public InputAwareWebView(Context context, View containerView) { + super(context); + this.containerView = containerView; + } + + public InputAwareWebView(Context context, AttributeSet attrs) { + super(context, attrs); + this.containerView = null; + } + + public InputAwareWebView(Context context) { + super(context); + this.containerView = null; + } + + public InputAwareWebView(Context context, AttributeSet attrs, int defaultStyle) { + super(context, attrs, defaultStyle); + this.containerView = null; + } + + /** + * 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. + */ + public void lockInputConnection() { + if (proxyAdapterView == null) { + return; + } + + proxyAdapterView.setLocked(true); + } + + /** Sets the proxy adapter view back to its default behavior. */ + public 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) { + if (containerView == null) + return super.checkInputConnectionProxy(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); + } + + // 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(); + if (containerView != null) + 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; + } + 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) { + 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); + } + }); + } +} \ No newline at end of file diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/ThreadedInputConnectionProxyAdapterView.java b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/ThreadedInputConnectionProxyAdapterView.java new file mode 100644 index 00000000..b3bb970d --- /dev/null +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappbrowser/InAppWebView/ThreadedInputConnectionProxyAdapterView.java @@ -0,0 +1,99 @@ +package com.pichillilorenzo.flutter_inappbrowser.InAppWebView; + +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. + * + * https://github.com/flutter/plugins/blob/master/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java + */ +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; + } +} \ No newline at end of file diff --git a/example/lib/inline_example.screen.dart b/example/lib/inline_example.screen.dart index 02b1d444..a7d32a2f 100644 --- a/example/lib/inline_example.screen.dart +++ b/example/lib/inline_example.screen.dart @@ -70,6 +70,7 @@ class _InlineExampleScreenState extends State { initialHeaders: {}, initialOptions: [ InAppWebViewOptions( + clearCache: true, useShouldOverrideUrlLoading: true, useOnTargetBlank: true, //useOnLoadResource: true, diff --git a/ios/Classes/InAppWebView.swift b/ios/Classes/InAppWebView.swift index 39aa13b7..ea489830 100755 --- a/ios/Classes/InAppWebView.swift +++ b/ios/Classes/InAppWebView.swift @@ -202,7 +202,10 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi scrollView.showsVerticalScrollIndicator = (options?.verticalScrollBarEnabled)! scrollView.showsHorizontalScrollIndicator = (options?.horizontalScrollBarEnabled)! - + + + // options.debuggingEnabled is always enabled for iOS. + if (options?.clearCache)! { clearCache() } @@ -745,6 +748,54 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi } } + public func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + let host = challenge.protectionSpace.host + let realm = challenge.protectionSpace.realm + onReceivedHttpAuthRequest(host: host, realm: realm, result: {(result) -> Void in + if result is FlutterError { + print((result as! FlutterError).message) + } + else if (result as? NSObject) == FlutterMethodNotImplemented { + completionHandler(.performDefaultHandling, nil) + } + else { + //WKWebsiteDataStore.default() + //URLCredentialStorage() + var response: [String: Any] + if let r = result { + response = r as! [String: Any] + var action = response["action"] as? Int + action = action != nil ? action : 0; + switch action { + case 0: + completionHandler(.cancelAuthenticationChallenge, nil) + break + case 1: + let username = response["username"] as! String + let password = response["password"] as! String + let permanentPersistence = response["permanentPersistence"] as? Bool ?? false + let persistence = (permanentPersistence) ? URLCredential.Persistence.permanent : URLCredential.Persistence.forSession + let credential = URLCredential(user: username, password: password, persistence: persistence) + completionHandler(.useCredential, credential) + break + case 2: + if let credential = challenge.proposedCredential { + completionHandler(.useCredential, credential) + } + else { + completionHandler(.performDefaultHandling, nil) + } + break + default: + completionHandler(.performDefaultHandling, nil) + } + return; + } + completionHandler(.performDefaultHandling, nil) + } + }) + } + fileprivate func createAlertDialog(message: String?, responseMessage: String?, confirmButtonTitle: String?, completionHandler: @escaping () -> Void) { let title = responseMessage != nil && !responseMessage!.isEmpty ? responseMessage : message let okButton = confirmButtonTitle != nil && !confirmButtonTitle!.isEmpty ? confirmButtonTitle : NSLocalizedString("Ok", comment: "") @@ -765,7 +816,6 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi onJsAlert(message: message, result: {(result) -> Void in if result is FlutterError { print((result as! FlutterError).message) - completionHandler() } else if (result as? NSObject) == FlutterMethodNotImplemented { self.createAlertDialog(message: message, responseMessage: nil, confirmButtonTitle: nil, completionHandler: completionHandler) @@ -824,7 +874,6 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi onJsConfirm(message: message, result: {(result) -> Void in if result is FlutterError { print((result as! FlutterError).message) - completionHandler(false) } else if (result as? NSObject) == FlutterMethodNotImplemented { self.createConfirmDialog(message: message, responseMessage: nil, confirmButtonTitle: nil, cancelButtonTitle: nil, completionHandler: completionHandler) @@ -898,7 +947,6 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi onJsPrompt(message: message, defaultValue: defaultValue, result: {(result) -> Void in if result is FlutterError { print((result as! FlutterError).message) - completionHandler(nil) } else if (result as? NSObject) == FlutterMethodNotImplemented { self.createPromptDialog(message: message, defaultValue: defaultValue, responseMessage: nil, confirmButtonTitle: nil, cancelButtonTitle: nil, value: nil, completionHandler: completionHandler) @@ -1053,6 +1101,16 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi } } + public func onReceivedHttpAuthRequest(host: String, realm: String?, result: FlutterResult?) { + var arguments: [String: Any] = ["host": host, "realm": realm as Any] + if IABController != nil { + arguments["uuid"] = IABController!.uuid + } + if let channel = getChannel() { + channel.invokeMethod("onReceivedHttpAuthRequest", arguments: arguments, result: result) + } + } + public func onJsAlert(message: String, result: FlutterResult?) { var arguments: [String: Any] = ["message": message] if IABController != nil { diff --git a/ios/Classes/InAppWebViewOptions.swift b/ios/Classes/InAppWebViewOptions.swift index 37110d23..7db8c7c5 100755 --- a/ios/Classes/InAppWebViewOptions.swift +++ b/ios/Classes/InAppWebViewOptions.swift @@ -18,6 +18,7 @@ public class InAppWebViewOptions: Options { var clearCache = false var userAgent = "" var javaScriptEnabled = true + var debuggingEnabled = true var javaScriptCanOpenWindowsAutomatically = false var mediaPlaybackRequiresUserGesture = true var verticalScrollBarEnabled = true @@ -25,7 +26,7 @@ public class InAppWebViewOptions: Options { var resourceCustomSchemes: [String] = [] var contentBlockers: [[String: [String : Any]]] = [] var minimumFontSize = 0; - + var disallowOverScroll = false var enableViewportScale = false //var keyboardDisplayRequiresUserAction = true diff --git a/lib/src/in_app_browser.dart b/lib/src/in_app_browser.dart index 6118b062..da329dd6 100644 --- a/lib/src/in_app_browser.dart +++ b/lib/src/in_app_browser.dart @@ -406,6 +406,15 @@ class InAppBrowser { } + ///Event fires when a WebView received an HTTP authentication request. The default behavior is to cancel the request. + /// + ///[host] represents the host requiring authentication. + /// + ///[realm] represents the realm for which authentication is required + Future onReceivedHttpAuthRequest(String url, String realm) { + + } + void throwIsAlreadyOpened({String message = ''}) { if (this.isOpened()) { throw Exception(['Error: ${ (message.isEmpty) ? '' : message + ' '}The browser is already opened.']); diff --git a/lib/src/in_app_webview.dart b/lib/src/in_app_webview.dart index d9f47e4f..db804f2b 100644 --- a/lib/src/in_app_webview.dart +++ b/lib/src/in_app_webview.dart @@ -176,6 +176,13 @@ class InAppWebView extends StatefulWidget { ///**NOTE**: available only for Android. final onSafeBrowsingHitCallback onSafeBrowsingHit; + ///Event fires when a WebView received an HTTP authentication request. The default behavior is to cancel the request. + /// + ///[host] represents the host requiring authentication. + /// + ///[realm] represents the realm for which authentication is required + final onReceivedHttpAuthRequestCallback onReceivedHttpAuthRequest; + ///Initial url that will be loaded. final String initialUrl; ///Initial asset file that will be loaded. See [InAppWebView.loadFile()] for explanation. @@ -219,6 +226,7 @@ class InAppWebView extends StatefulWidget { this.onJsConfirm, this.onJsPrompt, this.onSafeBrowsingHit, + this.onReceivedHttpAuthRequest, this.gestureRecognizers, }) : super(key: key); @@ -468,6 +476,14 @@ class InAppWebViewController { else if (_inAppBrowser != null) return (await _inAppBrowser.onSafeBrowsingHit(url, threatType))?.toMap(); break; + case "onReceivedHttpAuthRequest": + String host = call.arguments["host"]; + String realm = call.arguments["realm"]; + if (_widget != null && _widget.onReceivedHttpAuthRequest != null) + return (await _widget.onReceivedHttpAuthRequest(this, host, realm))?.toMap(); + else if (_inAppBrowser != null) + return (await _inAppBrowser.onReceivedHttpAuthRequest(host, realm))?.toMap(); + break; case "onCallJsHandler": String handlerName = call.arguments["handlerName"]; // decode args to json diff --git a/lib/src/types.dart b/lib/src/types.dart index 707c9591..fef73fac 100644 --- a/lib/src/types.dart +++ b/lib/src/types.dart @@ -270,6 +270,34 @@ class SafeBrowsingResponse { } } +class HttpAuthResponseAction { + final int _value; + const HttpAuthResponseAction._internal(this._value); + toValue() => _value; + + static const CANCEL = const HttpAuthResponseAction._internal(0); + static const PROCEED = const HttpAuthResponseAction._internal(1); + static const USE_HTTP_AUTH_USERNAME_PASSWORD = const HttpAuthResponseAction._internal(2); +} + +class HttpAuthResponse { + String username; + String password; + bool permanentPersistence; + HttpAuthResponseAction action; + + HttpAuthResponse({this.username = "", this.password = "", this.permanentPersistence = false, this.action = HttpAuthResponseAction.CANCEL}); + + Map toMap() { + return { + "username": username, + "password": password, + "permanentPersistence": permanentPersistence, + "action": action?.toValue() + }; + } +} + typedef onWebViewCreatedCallback = void Function(InAppWebViewController controller); typedef onWebViewLoadStartCallback = void Function(InAppWebViewController controller, String url); typedef onWebViewLoadStopCallback = void Function(InAppWebViewController controller, String url); @@ -286,4 +314,5 @@ typedef onGeolocationPermissionsShowPromptCallback = Future Function(InAppWebViewController controller, String message); typedef onJsConfirmCallback = Future Function(InAppWebViewController controller, String message); typedef onJsPromptCallback = Future Function(InAppWebViewController controller, String message, String defaultValue); -typedef onSafeBrowsingHitCallback = Future Function(InAppWebViewController controller, String url, SafeBrowsingThreat threatType); \ No newline at end of file +typedef onSafeBrowsingHitCallback = Future Function(InAppWebViewController controller, String url, SafeBrowsingThreat threatType); +typedef onReceivedHttpAuthRequestCallback = Future Function(InAppWebViewController controller, String url, String realm); \ No newline at end of file diff --git a/lib/src/webview_options.dart b/lib/src/webview_options.dart index e81cfebc..01175647 100644 --- a/lib/src/webview_options.dart +++ b/lib/src/webview_options.dart @@ -22,6 +22,7 @@ class InAppWebViewOptions implements WebViewOptions, BrowserOptions { bool clearCache; String userAgent; bool javaScriptEnabled; + bool debuggingEnabled; bool javaScriptCanOpenWindowsAutomatically; bool mediaPlaybackRequiresUserGesture; int textZoom; @@ -32,7 +33,7 @@ class InAppWebViewOptions implements WebViewOptions, BrowserOptions { List contentBlockers; InAppWebViewOptions({this.useShouldOverrideUrlLoading = false, this.useOnLoadResource = false, this.useOnDownloadStart = false, this.useOnTargetBlank = false, - this.clearCache = false, this.userAgent = "", this.javaScriptEnabled = true, this.javaScriptCanOpenWindowsAutomatically = false, + this.clearCache = false, this.userAgent = "", this.javaScriptEnabled = true, this.debuggingEnabled = false, this.javaScriptCanOpenWindowsAutomatically = false, this.mediaPlaybackRequiresUserGesture = true, this.textZoom = 100, this.minimumFontSize, this.verticalScrollBarEnabled = true, this.horizontalScrollBarEnabled = true, this.resourceCustomSchemes = const [], this.contentBlockers = const []}) { if (this.minimumFontSize == null)