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