Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 40ef139

Browse files
authored
[webview_flutter] Filter onChanged events for invalid displays (#1964)
Works around an Android WebView bug that was causing a crash by filtering some DisplayListener invocations. Older Android WebView versions had assumed that when 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. The 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: flutter/flutter#30420 This change works around the webview bug by unregistering the WebView's DisplayListener, and instead registering our own DisplayListener which delegates the callbacks to the WebView's listener unless it's a onDisplayChanged for an invalid display.
1 parent b48dd36 commit 40ef139

6 files changed

Lines changed: 248 additions & 4 deletions

File tree

packages/webview_flutter/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.3.11+1
2+
3+
* Work around a bug in old Android WebView versions that was causing a crash
4+
when resizing the webview on old devices.
5+
16
## 0.3.11
27

38
* Add an initialAutoMediaPlaybackPolicy setting for controlling how auto media
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package io.flutter.plugins.webviewflutter;
2+
3+
import static android.hardware.display.DisplayManager.DisplayListener;
4+
5+
import android.annotation.TargetApi;
6+
import android.hardware.display.DisplayManager;
7+
import android.os.Build;
8+
import android.util.Log;
9+
import java.lang.reflect.Field;
10+
import java.util.ArrayList;
11+
12+
/**
13+
* Works around an Android WebView bug by filtering some DisplayListener invocations.
14+
*
15+
* <p>Older Android WebView versions had assumed that when {@link DisplayListener#onDisplayChanged}
16+
* is invoked, the display ID it is provided is of a valid display. However it turns out that when a
17+
* display is removed Android may call onDisplayChanged with the ID of the removed display, in this
18+
* case the Android WebView code tries to fetch and use the display with this ID and crashes with an
19+
* NPE.
20+
*
21+
* <p>This issue was fixed in the Android WebView code in
22+
* https://chromium-review.googlesource.com/517913 which is available starting WebView version
23+
* 58.0.3029.125 however older webviews in the wild still have this issue.
24+
*
25+
* <p>Since Flutter removes virtual displays whenever a platform view is resized the webview crash
26+
* is more likely to happen than other apps. And users were reporting this issue see:
27+
* https://github.com/flutter/flutter/issues/30420
28+
*
29+
* <p>This class works around the webview bug by unregistering the WebView's DisplayListener, and
30+
* instead registering its own DisplayListener which delegates the callbacks to the WebView's
31+
* listener unless it's a onDisplayChanged for an invalid display.
32+
*
33+
* <p>I did not find a clean way to get a handle of the WebView's DisplayListener so I'm using
34+
* reflection to fetch all registered listeners before and after initializing a webview. In the
35+
* first initialization of a webview within the process the difference between the lists is the
36+
* webview's display listener.
37+
*/
38+
@TargetApi(Build.VERSION_CODES.KITKAT)
39+
class DisplayListenerProxy {
40+
private static final String TAG = "DisplayListenerProxy";
41+
42+
private ArrayList<DisplayListener> listenersBeforeWebView;
43+
44+
/** Should be called prior to the webview's initialization. */
45+
void onPreWebViewInitialization(DisplayManager displayManager) {
46+
listenersBeforeWebView = yoinkDisplayListeners(displayManager);
47+
}
48+
49+
/** Should be called after the webview's initialization. */
50+
void onPostWebViewInitialization(final DisplayManager displayManager) {
51+
final ArrayList<DisplayListener> webViewListeners = yoinkDisplayListeners(displayManager);
52+
// We recorded the list of listeners prior to initializing webview, any new listeners we see
53+
// after initializing the webview are listeners added by the webview.
54+
webViewListeners.removeAll(listenersBeforeWebView);
55+
56+
if (webViewListeners.isEmpty()) {
57+
// The Android WebView registers a single display listener per process (even if there
58+
// are multiple WebView instances) so this list is expected to be non-empty only the
59+
// first time a webview is initialized.
60+
// Note that in an add2app scenario if the application had instantiated a non Flutter
61+
// WebView prior to instantiating the Flutter WebView we are not able to get a reference
62+
// to the WebView's display listener and can't work around the bug.
63+
//
64+
// This means that webview resizes in add2app Flutter apps with a non Flutter WebView
65+
// running on a system with a webview prior to 58.0.3029.125 may crash (the Android's
66+
// behavior seems to be racy so it doesn't always happen).
67+
return;
68+
}
69+
70+
for (DisplayListener webViewListener : webViewListeners) {
71+
// Note that while DisplayManager.unregisterDisplayListener throws when given an
72+
// unregistered listener, this isn't an issue as the WebView code never calls
73+
// unregisterDisplayListener.
74+
displayManager.unregisterDisplayListener(webViewListener);
75+
76+
// We never explicitly unregister this listener as the webview's listener is never
77+
// unregistered (it's released when the process is terminated).
78+
displayManager.registerDisplayListener(
79+
new DisplayListener() {
80+
@Override
81+
public void onDisplayAdded(int displayId) {
82+
for (DisplayListener webViewListener : webViewListeners) {
83+
webViewListener.onDisplayAdded(displayId);
84+
}
85+
}
86+
87+
@Override
88+
public void onDisplayRemoved(int displayId) {
89+
for (DisplayListener webViewListener : webViewListeners) {
90+
webViewListener.onDisplayRemoved(displayId);
91+
}
92+
}
93+
94+
@Override
95+
public void onDisplayChanged(int displayId) {
96+
if (displayManager.getDisplay(displayId) == null) {
97+
return;
98+
}
99+
for (DisplayListener webViewListener : webViewListeners) {
100+
webViewListener.onDisplayChanged(displayId);
101+
}
102+
}
103+
},
104+
null);
105+
}
106+
}
107+
108+
@SuppressWarnings({"unchecked", "PrivateApi"})
109+
private static ArrayList<DisplayListener> yoinkDisplayListeners(DisplayManager displayManager) {
110+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
111+
// We cannot use reflection on Android O, but it shouldn't matter as it shipped
112+
// with a WebView version that has the bug this code is working around fixed.
113+
return new ArrayList<>();
114+
}
115+
try {
116+
Field displayManagerGlobalField = DisplayManager.class.getDeclaredField("mGlobal");
117+
displayManagerGlobalField.setAccessible(true);
118+
Object displayManagerGlobal = displayManagerGlobalField.get(displayManager);
119+
Field displayListenersField =
120+
displayManagerGlobal.getClass().getDeclaredField("mDisplayListeners");
121+
displayListenersField.setAccessible(true);
122+
ArrayList<Object> delegates =
123+
(ArrayList<Object>) displayListenersField.get(displayManagerGlobal);
124+
125+
Field listenerField = null;
126+
ArrayList<DisplayManager.DisplayListener> listeners = new ArrayList<>();
127+
for (Object delegate : delegates) {
128+
if (listenerField == null) {
129+
listenerField = delegate.getClass().getField("mListener");
130+
listenerField.setAccessible(true);
131+
}
132+
DisplayManager.DisplayListener listener =
133+
(DisplayManager.DisplayListener) listenerField.get(delegate);
134+
listeners.add(listener);
135+
}
136+
return listeners;
137+
} catch (NoSuchFieldException | IllegalAccessException e) {
138+
Log.w(TAG, "Could not extract WebView's display listeners. " + e);
139+
return new ArrayList<>();
140+
}
141+
}
142+
}

packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import android.annotation.TargetApi;
88
import android.content.Context;
9+
import android.hardware.display.DisplayManager;
910
import android.os.Build;
1011
import android.os.Handler;
1112
import android.view.View;
@@ -28,14 +29,21 @@ public class FlutterWebView implements PlatformView, MethodCallHandler {
2829
private final FlutterWebViewClient flutterWebViewClient;
2930
private final Handler platformThreadHandler;
3031

32+
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
3133
@SuppressWarnings("unchecked")
3234
FlutterWebView(
33-
Context context,
35+
final Context context,
3436
BinaryMessenger messenger,
3537
int id,
3638
Map<String, Object> params,
3739
final View containerView) {
40+
41+
DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy();
42+
DisplayManager displayManager =
43+
(DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
44+
displayListenerProxy.onPreWebViewInitialization(displayManager);
3845
webView = new InputAwareWebView(context, containerView);
46+
displayListenerProxy.onPostWebViewInitialization(displayManager);
3947

4048
platformThreadHandler = new Handler(context.getMainLooper());
4149
// Allow local storage.

packages/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
/* End PBXCopyFilesBuildPhase section */
3939

4040
/* Begin PBXFileReference section */
41+
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 = "<group>"; };
4142
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
4243
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
4344
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
@@ -55,6 +56,7 @@
5556
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
5657
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
5758
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
59+
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 = "<group>"; };
5860
/* End PBXFileReference section */
5961

6062
/* Begin PBXFrameworksBuildPhase section */
@@ -138,6 +140,8 @@
138140
C6FFB52F5C2B8A41A7E39DE2 /* Pods */ = {
139141
isa = PBXGroup;
140142
children = (
143+
127772EEA7782174BE0D74B5 /* Pods-Runner.debug.xcconfig */,
144+
C475C484BD510DD9CB2E403C /* Pods-Runner.release.xcconfig */,
141145
);
142146
name = Pods;
143147
sourceTree = "<group>";
@@ -249,7 +253,7 @@
249253
files = (
250254
);
251255
inputPaths = (
252-
"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
256+
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
253257
"${PODS_ROOT}/../.symlinks/flutter/ios/Flutter.framework",
254258
);
255259
name = "[CP] Embed Pods Frameworks";
@@ -258,7 +262,7 @@
258262
);
259263
runOnlyForDeploymentPostprocessing = 0;
260264
shellPath = /bin/sh;
261-
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
265+
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
262266
showEnvVarsInLog = 0;
263267
};
264268
B71376B4FB8384EF9D5F3F84 /* [CP] Check Pods Manifest.lock */ = {

packages/webview_flutter/example/test_driver/webview.dart

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,91 @@ void main() {
137137
expect(messagesReceived, equals(<String>['hello']));
138138
});
139139

140+
test('resize webview', () async {
141+
final String resizeTest = '''
142+
<!DOCTYPE html><html>
143+
<head><title>Resize test</title>
144+
<script type="text/javascript">
145+
function onResize() {
146+
Resize.postMessage("resize");
147+
}
148+
function onLoad() {
149+
window.onresize = onResize;
150+
}
151+
</script>
152+
</head>
153+
<body onload="onLoad();" bgColor="blue">
154+
</body>
155+
</html>
156+
''';
157+
final String resizeTestBase64 =
158+
base64Encode(const Utf8Encoder().convert(resizeTest));
159+
final Completer<void> resizeCompleter = Completer<void>();
160+
final Completer<void> pageLoaded = Completer<void>();
161+
final Completer<WebViewController> controllerCompleter =
162+
Completer<WebViewController>();
163+
final GlobalKey key = GlobalKey();
164+
165+
final WebView webView = WebView(
166+
key: key,
167+
initialUrl: 'data:text/html;charset=utf-8;base64,$resizeTestBase64',
168+
onWebViewCreated: (WebViewController controller) {
169+
controllerCompleter.complete(controller);
170+
},
171+
// TODO(iskakaushik): Remove this when collection literals makes it to stable.
172+
// ignore: prefer_collection_literals
173+
javascriptChannels: <JavascriptChannel>[
174+
JavascriptChannel(
175+
name: 'Resize',
176+
onMessageReceived: (JavascriptMessage message) {
177+
resizeCompleter.complete(true);
178+
},
179+
),
180+
].toSet(),
181+
onPageFinished: (String url) {
182+
pageLoaded.complete(null);
183+
},
184+
javascriptMode: JavascriptMode.unrestricted,
185+
);
186+
187+
await pumpWidget(
188+
Directionality(
189+
textDirection: TextDirection.ltr,
190+
child: Column(
191+
children: <Widget>[
192+
SizedBox(
193+
width: 200,
194+
height: 200,
195+
child: webView,
196+
),
197+
],
198+
),
199+
),
200+
);
201+
202+
await controllerCompleter.future;
203+
await pageLoaded.future;
204+
205+
expect(resizeCompleter.isCompleted, false);
206+
207+
await pumpWidget(
208+
Directionality(
209+
textDirection: TextDirection.ltr,
210+
child: Column(
211+
children: <Widget>[
212+
SizedBox(
213+
width: 400,
214+
height: 400,
215+
child: webView,
216+
),
217+
],
218+
),
219+
),
220+
);
221+
222+
await resizeCompleter.future;
223+
});
224+
140225
group('Media playback policy', () {
141226
String audioTestBase64;
142227
setUpAll(() async {

packages/webview_flutter/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: webview_flutter
22
description: A Flutter plugin that provides a WebView widget on Android and iOS.
3-
version: 0.3.11
3+
version: 0.3.11+1
44
author: Flutter Team <[email protected]>
55
homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter
66

0 commit comments

Comments
 (0)