Skip to content

Commit be0c314

Browse files
authored
Add views added to the WindowManager into the presentation view tree. (flutter#6043)
The default WindowManager implementation in Android's Presentation is delegating addView/removeView/updateViewLayout calls to the global WindowManager. This can result in a crash when an embedded view is trying to e.g show a PopupWindow. This change adds a custom WindowManager that overrides addView (and removeView/updateViewLayout) and adds the view to the presentation's view tree. Note that views might keep a reference to the window manager which might be an issue when we move a view from one virtual display to another (due to a resize). For this reason when re-sizing we are not creating a new window manager for the new presentation, but updating the window manager's references to be relevant for the new presentation and re-use it.
1 parent 5bd9620 commit be0c314

File tree

2 files changed

+244
-22
lines changed

2 files changed

+244
-22
lines changed

shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java

Lines changed: 237 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,69 @@
77
import android.annotation.TargetApi;
88
import android.app.Presentation;
99
import android.content.Context;
10+
import android.content.ContextWrapper;
11+
import android.graphics.Rect;
1012
import android.os.Build;
1113
import android.os.Bundle;
12-
import android.view.Display;
13-
import android.view.View;
14-
import android.view.WindowManager;
14+
import android.util.Log;
15+
import android.view.*;
1516
import android.widget.FrameLayout;
1617

18+
import java.lang.reflect.*;
19+
20+
import static android.content.Context.WINDOW_SERVICE;
21+
22+
/*
23+
* A presentation used for hosting a single Android view in a virtual display.
24+
*
25+
* This presentation overrides the WindowManager's addView/removeView/updateViewLayout methods, such that views added
26+
* directly to the WindowManager are added as part of the presentation's view hierarchy (to mFakeWindowRootView).
27+
*
28+
* The view hierarchy for the presentation is as following:
29+
*
30+
* mRootView
31+
* / \
32+
* / \
33+
* / \
34+
* mContainer mState.mFakeWindowRootView
35+
* |
36+
* EmbeddedView
37+
*/
1738
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
1839
class SingleViewPresentation extends Presentation {
40+
41+
/*
42+
* When an embedded view is resized in Flutterverse we move the Android view to a new virtual display
43+
* that has the new size. This class keeps the presentation state that moves with the view to the presentation of
44+
* the new virtual display.
45+
*/
46+
static class PresentationState {
47+
// The Android view we are embedding in the Flutter app.
48+
private PlatformView mView;
49+
50+
// The InvocationHandler for a WindowManager proxy. This is essentially the custom window manager for the
51+
// presentation.
52+
private WindowManagerHandler mWindowManagerHandler;
53+
54+
// Contains views that were added directly to the window manager (e.g android.widget.PopupWindow).
55+
private FakeWindowViewGroup mFakeWindowRootView;
56+
}
57+
1958
private final PlatformViewFactory mViewFactory;
2059

21-
private PlatformView mView;
60+
// This is the view id assigned by the Flutter framework to the embedded view, we keep it here
61+
// so when we create the platform we can tell it its view id.
2262
private int mViewId;
2363

24-
// As the root view of a display cannot be detached, we use this mContainer
25-
// as the root, and attach mView to it. This allows us to detach mView.
64+
// The root view for the presentation, it has 2 childs: mContainer which contains the embedded view, and
65+
// mFakeWindowRootView which contains views that were added directly to the presentation's window manager.
66+
private FrameLayout mRootView;
67+
68+
// Contains the embedded platform view (mView.getView()) when it is attached to the presentation.
2669
private FrameLayout mContainer;
2770

71+
private PresentationState mState;
72+
2873
/**
2974
* Creates a presentation that will use the view factory to create a new
3075
* platform view in the presentation's onCreate, and attach it.
@@ -33,6 +78,7 @@ public SingleViewPresentation(Context outerContext, Display display, PlatformVie
3378
super(outerContext, display);
3479
mViewFactory = viewFactory;
3580
mViewId = viewId;
81+
mState = new PresentationState();
3682
getWindow().setFlags(
3783
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
3884
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
@@ -46,10 +92,10 @@ public SingleViewPresentation(Context outerContext, Display display, PlatformVie
4692
* <p>The display's density must match the density of the context used
4793
* when the view was created.
4894
*/
49-
public SingleViewPresentation(Context outerContext, Display display, PlatformView view) {
95+
public SingleViewPresentation(Context outerContext, Display display, PresentationState state) {
5096
super(outerContext, display);
5197
mViewFactory = null;
52-
mView = view;
98+
mState = state;
5399
getWindow().setFlags(
54100
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
55101
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
@@ -59,22 +105,195 @@ public SingleViewPresentation(Context outerContext, Display display, PlatformVie
59105
@Override
60106
protected void onCreate(Bundle savedInstanceState) {
61107
super.onCreate(savedInstanceState);
62-
if (mView == null) {
63-
mView = mViewFactory.create(getContext(), mViewId);
108+
if (mState.mFakeWindowRootView == null) {
109+
mState.mFakeWindowRootView = new FakeWindowViewGroup(getContext());
64110
}
111+
if (mState.mWindowManagerHandler == null) {
112+
WindowManager windowManagerDelegate = (WindowManager) getContext().getSystemService(WINDOW_SERVICE);
113+
mState.mWindowManagerHandler = new WindowManagerHandler(windowManagerDelegate, mState.mFakeWindowRootView);
114+
}
115+
65116
mContainer = new FrameLayout(getContext());
66-
mContainer.addView(mView.getView());
67-
setContentView(mContainer);
117+
PresentationContext context = new PresentationContext(getContext(), mState.mWindowManagerHandler);
118+
119+
if (mState.mView == null) {
120+
mState.mView = mViewFactory.create(context, mViewId);
121+
}
122+
123+
mContainer.addView(mState.mView.getView());
124+
mRootView = new FrameLayout(getContext());
125+
mRootView.addView(mContainer);
126+
mRootView.addView(mState.mFakeWindowRootView);
127+
setContentView(mRootView);
68128
}
69129

70-
public PlatformView detachView() {
71-
mContainer.removeView(mView.getView());
72-
return mView;
130+
public PresentationState detachState() {
131+
mContainer.removeAllViews();
132+
mRootView.removeAllViews();
133+
return mState;
73134
}
74135

75-
public View getView() {
76-
if (mView == null)
136+
public PlatformView getView() {
137+
if (mState.mView == null)
77138
return null;
78-
return mView.getView();
139+
return mState.mView;
140+
}
141+
142+
/*
143+
* A view group that implements the same layout protocol that exist between the WindowManager and its direct
144+
* children.
145+
*
146+
* Currently only a subset of the protocol is supported (gravity, x, and y).
147+
*/
148+
static class FakeWindowViewGroup extends ViewGroup {
149+
// Used in onLayout to keep the bounds of the current view.
150+
// We keep it as a member to avoid object allocations during onLayout which are discouraged.
151+
private final Rect mViewBounds;
152+
153+
// Used in onLayout to keep the bounds of the child views.
154+
// We keep it as a member to avoid object allocations during onLayout which are discouraged.
155+
private final Rect mChildRect;
156+
157+
public FakeWindowViewGroup(Context context) {
158+
super(context);
159+
mViewBounds = new Rect();
160+
mChildRect = new Rect();
161+
}
162+
163+
@Override
164+
protected void onLayout(boolean changed, int l, int t, int r, int b) {
165+
for(int i = 0; i < getChildCount(); i++) {
166+
View child = getChildAt(i);
167+
WindowManager.LayoutParams params = (WindowManager.LayoutParams) child.getLayoutParams();
168+
mViewBounds.set(l, t, r, b);
169+
Gravity.apply(params.gravity, child.getMeasuredWidth(), child.getMeasuredHeight(), mViewBounds, params.x,
170+
params.y, mChildRect);
171+
child.layout(mChildRect.left, mChildRect.top, mChildRect.right, mChildRect.bottom);
172+
}
173+
}
174+
175+
@Override
176+
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
177+
for(int i = 0; i < getChildCount(); i++) {
178+
View child = getChildAt(i);
179+
child.measure(atMost(widthMeasureSpec), atMost(heightMeasureSpec));
180+
}
181+
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
182+
}
183+
184+
private static int atMost(int measureSpec) {
185+
return MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(measureSpec), MeasureSpec.AT_MOST);
186+
}
187+
}
188+
189+
/**
190+
* Proxies a Context replacing the WindowManager with our custom instance.
191+
*/
192+
static class PresentationContext extends ContextWrapper {
193+
private WindowManager mWindowManager;
194+
private final WindowManagerHandler mWindowManagerHandler;
195+
196+
PresentationContext(Context base, WindowManagerHandler windowManagerHandler) {
197+
super(base);
198+
mWindowManagerHandler = windowManagerHandler;
199+
}
200+
201+
@Override
202+
public Object getSystemService(String name) {
203+
if (WINDOW_SERVICE.equals(name)) {
204+
return getWindowManager();
205+
}
206+
return super.getSystemService(name);
207+
}
208+
209+
private WindowManager getWindowManager() {
210+
if (mWindowManager == null) {
211+
mWindowManager = mWindowManagerHandler.getWindowManager();
212+
}
213+
return mWindowManager;
214+
}
215+
}
216+
217+
/*
218+
* A dynamic proxy handler for a WindowManager with custom overrides.
219+
*
220+
* The presentation's window manager delegates all calls to the default window manager.
221+
* WindowManager#addView calls triggered by views that are attached to the virtual display are crashing
222+
* (see: https://github.com/flutter/flutter/issues/20714). This was triggered when selecting text in an embedded
223+
* WebView (as the selection handles are implemented as popup windows).
224+
*
225+
* This dynamic proxy overrides the addView, removeView, and updateViewLayout methods to prevent these crashes.
226+
*
227+
* This will be more efficient as a static proxy that's not using reflection, but as the engine is currently
228+
* not being built against the latest Android SDK we cannot override all relevant method.
229+
* Tracking issue for upgrading the engine's Android sdk: https://github.com/flutter/flutter/issues/20717
230+
*/
231+
static class WindowManagerHandler implements InvocationHandler {
232+
private static final String TAG = "PlatformViewsController";
233+
234+
private final WindowManager mDelegate;
235+
FakeWindowViewGroup mFakeWindowRootView;
236+
237+
WindowManagerHandler(WindowManager delegate, FakeWindowViewGroup fakeWindowViewGroup) {
238+
mDelegate = delegate;
239+
mFakeWindowRootView = fakeWindowViewGroup;
240+
}
241+
242+
public WindowManager getWindowManager() {
243+
return (WindowManager) Proxy.newProxyInstance(
244+
WindowManager.class.getClassLoader(),
245+
new Class[] { WindowManager.class },
246+
this
247+
);
248+
}
249+
250+
@Override
251+
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
252+
switch (method.getName()) {
253+
case "addView":
254+
addView(args);
255+
return null;
256+
case "removeView":
257+
removeView(args);
258+
return null;
259+
case "updateViewLayout":
260+
updateViewLayout(args);
261+
return null;
262+
}
263+
try {
264+
return method.invoke(mDelegate, args);
265+
} catch (InvocationTargetException e) {
266+
throw e.getCause();
267+
}
268+
}
269+
270+
private void addView(Object[] args) {
271+
if (mFakeWindowRootView == null) {
272+
Log.w(TAG, "Embedded view called addView while detached from presentation");
273+
return;
274+
}
275+
View view = (View) args[0];
276+
WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) args[1];
277+
mFakeWindowRootView.addView(view, layoutParams);
278+
}
279+
280+
private void removeView(Object[] args) {
281+
if (mFakeWindowRootView == null) {
282+
Log.w(TAG, "Embedded view called removeView while detached from presentation");
283+
return;
284+
}
285+
View view = (View) args[0];
286+
mFakeWindowRootView.removeView(view);
287+
}
288+
289+
private void updateViewLayout(Object[] args) {
290+
if (mFakeWindowRootView == null) {
291+
Log.w(TAG, "Embedded view called updateViewLayout while detached from presentation");
292+
return;
293+
}
294+
View view = (View) args[0];
295+
WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) args[1];
296+
mFakeWindowRootView.updateViewLayout(view, layoutParams);
297+
}
79298
}
80299
}

shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ private VirtualDisplayController(
7272
}
7373

7474
public void resize(final int width, final int height, final Runnable onNewSizeFrameAvailable) {
75-
final PlatformView view = mPresentation.detachView();
75+
final SingleViewPresentation.PresentationState presentationState = mPresentation.detachState();
7676
// We detach the surface to prevent it being destroyed when releasing the vd.
7777
//
7878
// setSurface is only available starting API 20. We could support API 19 by re-creating a new
@@ -118,18 +118,21 @@ public void onDraw() {
118118
public void onViewDetachedFromWindow(View v) {}
119119
});
120120

121-
mPresentation = new SingleViewPresentation(mContext, mVirtualDisplay.getDisplay(), view);
121+
mPresentation = new SingleViewPresentation(mContext, mVirtualDisplay.getDisplay(), presentationState);
122122
mPresentation.show();
123123
}
124124

125125
public void dispose() {
126-
mPresentation.detachView().dispose();
126+
PlatformView view = mPresentation.getView();
127+
mPresentation.detachState();
128+
view.dispose();
127129
mVirtualDisplay.release();
128130
}
129131

130132
public View getView() {
131133
if (mPresentation == null)
132134
return null;
133-
return mPresentation.getView();
135+
PlatformView platformView = mPresentation.getView();
136+
return platformView.getView();
134137
}
135138
}

0 commit comments

Comments
 (0)