• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package io.flutter.plugin.platform;
6 
7 import android.annotation.TargetApi;
8 import android.app.Presentation;
9 import android.content.Context;
10 import android.content.ContextWrapper;
11 import android.graphics.Rect;
12 import android.graphics.drawable.ColorDrawable;
13 import android.os.Build;
14 import android.os.Bundle;
15 import android.util.Log;
16 import android.view.*;
17 import android.view.accessibility.AccessibilityEvent;
18 import android.widget.FrameLayout;
19 
20 import java.lang.reflect.*;
21 
22 import static android.content.Context.WINDOW_SERVICE;
23 import static android.view.View.OnFocusChangeListener;
24 
25 /*
26  * A presentation used for hosting a single Android view in a virtual display.
27  *
28  * This presentation overrides the WindowManager's addView/removeView/updateViewLayout methods, such that views added
29  * directly to the WindowManager are added as part of the presentation's view hierarchy (to fakeWindowViewGroup).
30  *
31  * The view hierarchy for the presentation is as following:
32  *
33  *          rootView
34  *         /         \
35  *        /           \
36  *       /             \
37  *   container       state.fakeWindowViewGroup
38  *      |
39  *   EmbeddedView
40  */
41 @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
42 class SingleViewPresentation extends Presentation {
43 
44     /*
45      * When an embedded view is resized in Flutterverse we move the Android view to a new virtual display
46      * that has the new size. This class keeps the presentation state that moves with the view to the presentation of
47      * the new virtual display.
48      */
49     static class PresentationState {
50         // The Android view we are embedding in the Flutter app.
51         private PlatformView platformView;
52 
53         // The InvocationHandler for a WindowManager proxy. This is essentially the custom window manager for the
54         // presentation.
55         private WindowManagerHandler windowManagerHandler;
56 
57         // Contains views that were added directly to the window manager (e.g android.widget.PopupWindow).
58         private FakeWindowViewGroup fakeWindowViewGroup;
59     }
60 
61     private final PlatformViewFactory viewFactory;
62 
63     // A reference to the current accessibility bridge to which accessibility events will be delegated.
64     private final AccessibilityEventsDelegate accessibilityEventsDelegate;
65 
66     private final OnFocusChangeListener focusChangeListener;
67 
68     // This is the view id assigned by the Flutter framework to the embedded view, we keep it here
69     // so when we create the platform view we can tell it its view id.
70     private int viewId;
71 
72     // This is the creation parameters for the platform view, we keep it here
73     // so when we create the platform view we can tell it its view id.
74     private Object createParams;
75 
76     // The root view for the presentation, it has 2 childs: container which contains the embedded view, and
77     // fakeWindowViewGroup which contains views that were added directly to the presentation's window manager.
78     private AccessibilityDelegatingFrameLayout rootView;
79 
80     // Contains the embedded platform view (platformView.getView()) when it is attached to the presentation.
81     private FrameLayout container;
82 
83     private PresentationState state;
84 
85     private boolean startFocused = false;
86 
87     /**
88      * Creates a presentation that will use the view factory to create a new
89      * platform view in the presentation's onCreate, and attach it.
90      */
SingleViewPresentation( Context outerContext, Display display, PlatformViewFactory viewFactory, AccessibilityEventsDelegate accessibilityEventsDelegate, int viewId, Object createParams, OnFocusChangeListener focusChangeListener )91     public SingleViewPresentation(
92             Context outerContext,
93             Display display,
94             PlatformViewFactory viewFactory,
95             AccessibilityEventsDelegate accessibilityEventsDelegate,
96             int viewId,
97             Object createParams,
98             OnFocusChangeListener focusChangeListener
99     ) {
100         super(outerContext, display);
101         this.viewFactory = viewFactory;
102         this.accessibilityEventsDelegate = accessibilityEventsDelegate;
103         this.viewId = viewId;
104         this.createParams = createParams;
105         this.focusChangeListener = focusChangeListener;
106         state = new PresentationState();
107         getWindow().setFlags(
108                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
109                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
110         );
111     }
112 
113 
114     /**
115      * Creates a presentation that will attach an already existing view as
116      * its root view.
117      *
118      * <p>The display's density must match the density of the context used
119      * when the view was created.
120      */
SingleViewPresentation( Context outerContext, Display display, AccessibilityEventsDelegate accessibilityEventsDelegate, PresentationState state, OnFocusChangeListener focusChangeListener, boolean startFocused )121     public SingleViewPresentation(
122             Context outerContext,
123             Display display,
124             AccessibilityEventsDelegate accessibilityEventsDelegate,
125             PresentationState state,
126             OnFocusChangeListener focusChangeListener,
127             boolean startFocused
128     ) {
129         super(outerContext, display);
130         this.accessibilityEventsDelegate = accessibilityEventsDelegate;
131         viewFactory = null;
132         this.state = state;
133         this.focusChangeListener = focusChangeListener;
134         getWindow().setFlags(
135                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
136                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
137         );
138         this.startFocused = startFocused;
139     }
140 
141     @Override
onCreate(Bundle savedInstanceState)142     protected void onCreate(Bundle savedInstanceState) {
143         super.onCreate(savedInstanceState);
144         // This makes sure we preserve alpha for the VD's content.
145         getWindow().setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT));
146         if (state.fakeWindowViewGroup == null) {
147             state.fakeWindowViewGroup = new FakeWindowViewGroup(getContext());
148         }
149         if (state.windowManagerHandler == null) {
150             WindowManager windowManagerDelegate = (WindowManager) getContext().getSystemService(WINDOW_SERVICE);
151             state.windowManagerHandler = new WindowManagerHandler(windowManagerDelegate, state.fakeWindowViewGroup);
152         }
153 
154         container = new FrameLayout(getContext());
155         PresentationContext context = new PresentationContext(getContext(), state.windowManagerHandler);
156 
157         if (state.platformView == null) {
158             state.platformView = viewFactory.create(context, viewId, createParams);
159         }
160 
161         View embeddedView = state.platformView.getView();
162         container.addView(embeddedView);
163         rootView = new AccessibilityDelegatingFrameLayout(getContext(), accessibilityEventsDelegate, embeddedView);
164         rootView.addView(container);
165         rootView.addView(state.fakeWindowViewGroup);
166 
167         embeddedView.setOnFocusChangeListener(focusChangeListener);
168         rootView.setFocusableInTouchMode(true);
169         if (startFocused) {
170             embeddedView.requestFocus();
171         } else {
172             rootView.requestFocus();
173         }
174         setContentView(rootView);
175     }
176 
detachState()177     public PresentationState detachState() {
178         container.removeAllViews();
179         rootView.removeAllViews();
180         return state;
181     }
182 
getView()183     public PlatformView getView() {
184         if (state.platformView == null)
185             return null;
186         return state.platformView;
187     }
188 
189     /*
190      * A view group that implements the same layout protocol that exist between the WindowManager and its direct
191      * children.
192      *
193      * Currently only a subset of the protocol is supported (gravity, x, and y).
194      */
195     static class FakeWindowViewGroup extends ViewGroup {
196         // Used in onLayout to keep the bounds of the current view.
197         // We keep it as a member to avoid object allocations during onLayout which are discouraged.
198         private final Rect viewBounds;
199 
200         // Used in onLayout to keep the bounds of the child views.
201         // We keep it as a member to avoid object allocations during onLayout which are discouraged.
202         private final Rect childRect;
203 
FakeWindowViewGroup(Context context)204         public FakeWindowViewGroup(Context context) {
205             super(context);
206             viewBounds = new Rect();
207             childRect = new Rect();
208         }
209 
210         @Override
onLayout(boolean changed, int l, int t, int r, int b)211         protected void onLayout(boolean changed, int l, int t, int r, int b) {
212             for(int i = 0; i < getChildCount(); i++) {
213                 View child = getChildAt(i);
214                 WindowManager.LayoutParams params = (WindowManager.LayoutParams) child.getLayoutParams();
215                 viewBounds.set(l, t, r, b);
216                 Gravity.apply(params.gravity, child.getMeasuredWidth(), child.getMeasuredHeight(), viewBounds, params.x,
217                         params.y, childRect);
218                 child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
219             }
220         }
221 
222         @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)223         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
224             for(int i = 0; i < getChildCount(); i++) {
225                 View child = getChildAt(i);
226                 child.measure(atMost(widthMeasureSpec), atMost(heightMeasureSpec));
227             }
228             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
229         }
230 
atMost(int measureSpec)231         private static int atMost(int measureSpec) {
232             return MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(measureSpec), MeasureSpec.AT_MOST);
233         }
234     }
235 
236     /**
237      * Proxies a Context replacing the WindowManager with our custom instance.
238      */
239     static class PresentationContext extends ContextWrapper {
240         private WindowManager windowManager;
241         private final WindowManagerHandler windowManagerHandler;
242 
PresentationContext(Context base, WindowManagerHandler windowManagerHandler)243         PresentationContext(Context base, WindowManagerHandler windowManagerHandler) {
244             super(base);
245             this.windowManagerHandler = windowManagerHandler;
246         }
247 
248         @Override
getSystemService(String name)249         public Object getSystemService(String name) {
250             if (WINDOW_SERVICE.equals(name)) {
251                 return getWindowManager();
252             }
253             return super.getSystemService(name);
254         }
255 
getWindowManager()256         private WindowManager getWindowManager() {
257             if (windowManager == null) {
258                 windowManager = windowManagerHandler.getWindowManager();
259             }
260             return windowManager;
261         }
262     }
263 
264     /*
265      * A dynamic proxy handler for a WindowManager with custom overrides.
266      *
267      * The presentation's window manager delegates all calls to the default window manager.
268      * WindowManager#addView calls triggered by views that are attached to the virtual display are crashing
269      * (see: https://github.com/flutter/flutter/issues/20714). This was triggered when selecting text in an embedded
270      * WebView (as the selection handles are implemented as popup windows).
271      *
272      * This dynamic proxy overrides the addView, removeView, removeViewImmediate, and updateViewLayout methods
273      * to prevent these crashes.
274      *
275      * This will be more efficient as a static proxy that's not using reflection, but as the engine is currently
276      * not being built against the latest Android SDK we cannot override all relevant method.
277      * Tracking issue for upgrading the engine's Android sdk: https://github.com/flutter/flutter/issues/20717
278      */
279     static class WindowManagerHandler implements InvocationHandler {
280         private static final String TAG = "PlatformViewsController";
281 
282         private final WindowManager delegate;
283         FakeWindowViewGroup fakeWindowRootView;
284 
WindowManagerHandler(WindowManager delegate, FakeWindowViewGroup fakeWindowViewGroup)285         WindowManagerHandler(WindowManager delegate, FakeWindowViewGroup fakeWindowViewGroup) {
286             this.delegate = delegate;
287             fakeWindowRootView = fakeWindowViewGroup;
288         }
289 
getWindowManager()290         public WindowManager getWindowManager() {
291             return (WindowManager) Proxy.newProxyInstance(
292                     WindowManager.class.getClassLoader(),
293                     new Class<?>[] { WindowManager.class },
294                     this
295             );
296         }
297 
298         @Override
invoke(Object proxy, Method method, Object[] args)299         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
300             switch (method.getName()) {
301                 case "addView":
302                     addView(args);
303                     return null;
304                 case "removeView":
305                     removeView(args);
306                     return null;
307                 case "removeViewImmediate":
308                     removeViewImmediate(args);
309                     return null;
310                 case "updateViewLayout":
311                     updateViewLayout(args);
312                     return null;
313             }
314             try {
315                 return method.invoke(delegate, args);
316             } catch (InvocationTargetException e) {
317                 throw e.getCause();
318             }
319         }
320 
addView(Object[] args)321         private void addView(Object[] args) {
322             if (fakeWindowRootView == null) {
323                 Log.w(TAG, "Embedded view called addView while detached from presentation");
324                 return;
325             }
326             View view = (View) args[0];
327             WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) args[1];
328             fakeWindowRootView.addView(view, layoutParams);
329         }
330 
removeView(Object[] args)331         private void removeView(Object[] args) {
332             if (fakeWindowRootView == null) {
333                 Log.w(TAG, "Embedded view called removeView while detached from presentation");
334                 return;
335             }
336             View view = (View) args[0];
337             fakeWindowRootView.removeView(view);
338         }
339 
removeViewImmediate(Object[] args)340         private void removeViewImmediate(Object[] args) {
341             if (fakeWindowRootView == null) {
342                 Log.w(TAG, "Embedded view called removeViewImmediate while detached from presentation");
343                 return;
344             }
345             View view = (View) args[0];
346             view.clearAnimation();
347             fakeWindowRootView.removeView(view);
348         }
349 
updateViewLayout(Object[] args)350         private void updateViewLayout(Object[] args) {
351             if (fakeWindowRootView == null) {
352                 Log.w(TAG, "Embedded view called updateViewLayout while detached from presentation");
353                 return;
354             }
355             View view = (View) args[0];
356             WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) args[1];
357             fakeWindowRootView.updateViewLayout(view, layoutParams);
358         }
359     }
360 
361     private static class AccessibilityDelegatingFrameLayout extends FrameLayout {
362         private final AccessibilityEventsDelegate accessibilityEventsDelegate;
363         private final View embeddedView;
364 
AccessibilityDelegatingFrameLayout( Context context, AccessibilityEventsDelegate accessibilityEventsDelegate, View embeddedView )365         public AccessibilityDelegatingFrameLayout(
366                 Context context,
367                 AccessibilityEventsDelegate accessibilityEventsDelegate,
368                 View embeddedView
369         ) {
370             super(context);
371             this.accessibilityEventsDelegate = accessibilityEventsDelegate;
372             this.embeddedView = embeddedView;
373         }
374 
375         @Override
requestSendAccessibilityEvent(View child, AccessibilityEvent event)376         public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) {
377             return accessibilityEventsDelegate.requestSendAccessibilityEvent(embeddedView, child, event);
378         }
379     }
380 }
381