77import android .annotation .TargetApi ;
88import android .app .Presentation ;
99import android .content .Context ;
10+ import android .content .ContextWrapper ;
11+ import android .graphics .Rect ;
1012import android .os .Build ;
1113import 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 .*;
1516import 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 )
1839class 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}
0 commit comments