diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index eb7b1bc4684f..6e7f498968f0 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.3.11+1 + +* Work around a bug in old Android WebView versions that was causing a crash + when resizing the webview on old devices. + ## 0.3.11 * Add an initialAutoMediaPlaybackPolicy setting for controlling how auto media diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java new file mode 100644 index 000000000000..9335f6f37fcf --- /dev/null +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java @@ -0,0 +1,142 @@ +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.O) { + // We cannot use reflection on Android O, but it shouldn't matter as it shipped + // with a WebView version that has the bug this code is working around fixed. + 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/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java index 80431ad4cd5b..e089f6d28190 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java @@ -6,6 +6,7 @@ import android.annotation.TargetApi; import android.content.Context; +import android.hardware.display.DisplayManager; import android.os.Build; import android.os.Handler; import android.view.View; @@ -28,14 +29,21 @@ public class FlutterWebView implements PlatformView, MethodCallHandler { private final FlutterWebViewClient flutterWebViewClient; private final Handler platformThreadHandler; + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) @SuppressWarnings("unchecked") FlutterWebView( - Context context, + final Context context, BinaryMessenger messenger, int id, Map params, final View containerView) { + + DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy(); + DisplayManager displayManager = + (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); + displayListenerProxy.onPreWebViewInitialization(displayManager); webView = new InputAwareWebView(context, containerView); + displayListenerProxy.onPostWebViewInitialization(displayManager); platformThreadHandler = new Handler(context.getMainLooper()); // Allow local storage. diff --git a/packages/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj index f471b69fb7d1..61ee7d2d4093 100644 --- a/packages/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ /* 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 = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; @@ -55,6 +56,7 @@ 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -138,6 +140,8 @@ C6FFB52F5C2B8A41A7E39DE2 /* Pods */ = { isa = PBXGroup; children = ( + 127772EEA7782174BE0D74B5 /* Pods-Runner.debug.xcconfig */, + C475C484BD510DD9CB2E403C /* Pods-Runner.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -249,7 +253,7 @@ files = ( ); inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", "${PODS_ROOT}/../.symlinks/flutter/ios/Flutter.framework", ); name = "[CP] Embed Pods Frameworks"; @@ -258,7 +262,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; B71376B4FB8384EF9D5F3F84 /* [CP] Check Pods Manifest.lock */ = { diff --git a/packages/webview_flutter/example/test_driver/webview.dart b/packages/webview_flutter/example/test_driver/webview.dart index b6abff94e6b2..fefaf6d49bef 100644 --- a/packages/webview_flutter/example/test_driver/webview.dart +++ b/packages/webview_flutter/example/test_driver/webview.dart @@ -137,6 +137,91 @@ void main() { expect(messagesReceived, equals(['hello'])); }); + test('resize webview', () async { + final String resizeTest = ''' + + Resize test + + + + + + '''; + final String resizeTestBase64 = + base64Encode(const Utf8Encoder().convert(resizeTest)); + final Completer resizeCompleter = Completer(); + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + final GlobalKey key = GlobalKey(); + + final WebView webView = WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$resizeTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + // TODO(iskakaushik): Remove this when collection literals makes it to stable. + // ignore: prefer_collection_literals + javascriptChannels: [ + JavascriptChannel( + name: 'Resize', + onMessageReceived: (JavascriptMessage message) { + resizeCompleter.complete(true); + }, + ), + ].toSet(), + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + javascriptMode: JavascriptMode.unrestricted, + ); + + await pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: 200, + height: 200, + child: webView, + ), + ], + ), + ), + ); + + await controllerCompleter.future; + await pageLoaded.future; + + expect(resizeCompleter.isCompleted, false); + + await pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: 400, + height: 400, + child: webView, + ), + ], + ), + ), + ); + + await resizeCompleter.future; + }); + group('Media playback policy', () { String audioTestBase64; setUpAll(() async { diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index 6cde699ea336..9999ca9519b3 100644 --- a/packages/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. -version: 0.3.11 +version: 0.3.11+1 author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter