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 static android.view.MotionEvent.PointerCoords; 8 import static android.view.MotionEvent.PointerProperties; 9 10 import android.annotation.TargetApi; 11 import android.content.Context; 12 import android.os.Build; 13 import android.support.annotation.UiThread; 14 import android.util.DisplayMetrics; 15 import android.support.annotation.NonNull; 16 import android.util.Log; 17 import android.view.MotionEvent; 18 import android.view.View; 19 20 import io.flutter.embedding.engine.dart.DartExecutor; 21 import io.flutter.embedding.engine.systemchannels.PlatformViewsChannel; 22 import io.flutter.plugin.editing.TextInputPlugin; 23 import io.flutter.view.AccessibilityBridge; 24 import io.flutter.view.TextureRegistry; 25 26 import java.util.ArrayList; 27 import java.util.HashMap; 28 import java.util.List; 29 30 /** 31 * Manages platform views. 32 * <p> 33 * Each {@link io.flutter.app.FlutterPluginRegistry} has a single platform views controller. 34 * A platform views controller can be attached to at most one Flutter view. 35 */ 36 public class PlatformViewsController implements PlatformViewsAccessibilityDelegate { 37 private static final String TAG = "PlatformViewsController"; 38 39 // API level 20 is required for VirtualDisplay#setSurface which we use when resizing a platform view. 40 private static final int MINIMAL_SDK = Build.VERSION_CODES.KITKAT_WATCH; 41 42 private final PlatformViewRegistryImpl registry; 43 44 // The context of the Activity or Fragment hosting the render target for the Flutter engine. 45 private Context context; 46 47 // The texture registry maintaining the textures into which the embedded views will be rendered. 48 private TextureRegistry textureRegistry; 49 50 private TextInputPlugin textInputPlugin; 51 52 // The system channel used to communicate with the framework about platform views. 53 private PlatformViewsChannel platformViewsChannel; 54 55 // The accessibility bridge to which accessibility events form the platform views will be dispatched. 56 private final AccessibilityEventsDelegate accessibilityEventsDelegate; 57 58 private final HashMap<Integer, VirtualDisplayController> vdControllers; 59 60 // Maps a virtual display's context to the platform view hosted in this virtual display. 61 // Since each virtual display has it's unique context this allows associating any view with the platform view that 62 // it is associated with(e.g if a platform view creates other views in the same virtual display. 63 private final HashMap<Context, View> contextToPlatformView; 64 65 private final PlatformViewsChannel.PlatformViewsHandler channelHandler = new PlatformViewsChannel.PlatformViewsHandler() { 66 @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) 67 @Override 68 public long createPlatformView(@NonNull PlatformViewsChannel.PlatformViewCreationRequest request) { 69 ensureValidAndroidVersion(); 70 71 if (!validateDirection(request.direction)) { 72 throw new IllegalStateException("Trying to create a view with unknown direction value: " 73 + request.direction + "(view id: " + request.viewId + ")"); 74 } 75 76 if (vdControllers.containsKey(request.viewId)) { 77 throw new IllegalStateException("Trying to create an already created platform view, view id: " 78 + request.viewId); 79 } 80 81 PlatformViewFactory viewFactory = registry.getFactory(request.viewType); 82 if (viewFactory == null) { 83 throw new IllegalStateException("Trying to create a platform view of unregistered type: " 84 + request.viewType); 85 } 86 87 Object createParams = null; 88 if (request.params != null) { 89 createParams = viewFactory.getCreateArgsCodec().decodeMessage(request.params); 90 } 91 92 int physicalWidth = toPhysicalPixels(request.logicalWidth); 93 int physicalHeight = toPhysicalPixels(request.logicalHeight); 94 validateVirtualDisplayDimensions(physicalWidth, physicalHeight); 95 96 TextureRegistry.SurfaceTextureEntry textureEntry = textureRegistry.createSurfaceTexture(); 97 VirtualDisplayController vdController = VirtualDisplayController.create( 98 context, 99 accessibilityEventsDelegate, 100 viewFactory, 101 textureEntry, 102 physicalWidth, 103 physicalHeight, 104 request.viewId, 105 createParams, 106 (view, hasFocus) -> { 107 if (hasFocus) { 108 platformViewsChannel.invokeViewFocused(request.viewId); 109 } 110 } 111 ); 112 113 if (vdController == null) { 114 throw new IllegalStateException("Failed creating virtual display for a " 115 + request.viewType + " with id: " + request.viewId); 116 } 117 118 vdControllers.put(request.viewId, vdController); 119 View platformView = vdController.getView(); 120 platformView.setLayoutDirection(request.direction); 121 contextToPlatformView.put(platformView.getContext(), platformView); 122 123 // TODO(amirh): copy accessibility nodes to the FlutterView's accessibility tree. 124 125 return textureEntry.id(); 126 } 127 128 @Override 129 public void disposePlatformView(int viewId) { 130 ensureValidAndroidVersion(); 131 132 VirtualDisplayController vdController = vdControllers.get(viewId); 133 if (vdController == null) { 134 throw new IllegalStateException("Trying to dispose a platform view with unknown id: " 135 + viewId); 136 } 137 138 if (textInputPlugin != null) { 139 textInputPlugin.clearPlatformViewClient(viewId); 140 } 141 142 contextToPlatformView.remove(vdController.getView().getContext()); 143 vdController.dispose(); 144 vdControllers.remove(viewId); 145 } 146 147 @Override 148 public void resizePlatformView(@NonNull PlatformViewsChannel.PlatformViewResizeRequest request, @NonNull Runnable onComplete) { 149 ensureValidAndroidVersion(); 150 151 final VirtualDisplayController vdController = vdControllers.get(request.viewId); 152 if (vdController == null) { 153 throw new IllegalStateException("Trying to resize a platform view with unknown id: " 154 + request.viewId); 155 } 156 157 int physicalWidth = toPhysicalPixels(request.newLogicalWidth); 158 int physicalHeight = toPhysicalPixels(request.newLogicalHeight); 159 validateVirtualDisplayDimensions(physicalWidth, physicalHeight); 160 161 // Resizing involved moving the platform view to a new virtual display. Doing so 162 // potentially results in losing an active input connection. To make sure we preserve 163 // the input connection when resizing we lock it here and unlock after the resize is 164 // complete. 165 lockInputConnection(vdController); 166 vdController.resize( 167 physicalWidth, 168 physicalHeight, 169 new Runnable() { 170 @Override 171 public void run() { 172 unlockInputConnection(vdController); 173 onComplete.run(); 174 } 175 } 176 ); 177 } 178 179 @Override 180 public void onTouch(@NonNull PlatformViewsChannel.PlatformViewTouch touch) { 181 ensureValidAndroidVersion(); 182 183 float density = context.getResources().getDisplayMetrics().density; 184 PointerProperties[] pointerProperties = 185 parsePointerPropertiesList(touch.rawPointerPropertiesList) 186 .toArray(new PointerProperties[touch.pointerCount]); 187 PointerCoords[] pointerCoords = 188 parsePointerCoordsList(touch.rawPointerCoords, density) 189 .toArray(new PointerCoords[touch.pointerCount]); 190 191 if (!vdControllers.containsKey(touch.viewId)) { 192 throw new IllegalStateException("Sending touch to an unknown view with id: " + touch.viewId); 193 } 194 View view = vdControllers.get(touch.viewId).getView(); 195 196 MotionEvent event = MotionEvent.obtain( 197 touch.downTime.longValue(), 198 touch.eventTime.longValue(), 199 touch.action, 200 touch.pointerCount, 201 pointerProperties, 202 pointerCoords, 203 touch.metaState, 204 touch.buttonState, 205 touch.xPrecision, 206 touch.yPrecision, 207 touch.deviceId, 208 touch.edgeFlags, 209 touch.source, 210 touch.flags 211 ); 212 213 view.dispatchTouchEvent(event); 214 } 215 216 @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) 217 @Override 218 public void setDirection(int viewId, int direction) { 219 ensureValidAndroidVersion(); 220 221 if (!validateDirection(direction)) { 222 throw new IllegalStateException("Trying to set unknown direction value: " + direction 223 + "(view id: " + viewId + ")"); 224 } 225 226 View view = vdControllers.get(viewId).getView(); 227 if (view == null) { 228 throw new IllegalStateException("Sending touch to an unknown view with id: " 229 + direction); 230 } 231 232 view.setLayoutDirection(direction); 233 } 234 235 @Override 236 public void clearFocus(int viewId) { 237 View view = vdControllers.get(viewId).getView(); 238 view.clearFocus(); 239 } 240 241 private void ensureValidAndroidVersion() { 242 if (Build.VERSION.SDK_INT < MINIMAL_SDK) { 243 Log.e(TAG, "Trying to use platform views with API " + Build.VERSION.SDK_INT 244 + ", required API level is: " + MINIMAL_SDK); 245 throw new IllegalStateException("An attempt was made to use platform views on a" 246 + " version of Android that platform views does not support."); 247 } 248 } 249 }; 250 PlatformViewsController()251 public PlatformViewsController() { 252 registry = new PlatformViewRegistryImpl(); 253 vdControllers = new HashMap<>(); 254 accessibilityEventsDelegate = new AccessibilityEventsDelegate(); 255 contextToPlatformView = new HashMap<>(); 256 } 257 258 /** 259 * Attaches this platform views controller to its input and output channels. 260 * 261 * @param context The base context that will be passed to embedded views created by this controller. 262 * This should be the context of the Activity hosting the Flutter application. 263 * @param textureRegistry The texture registry which provides the output textures into which the embedded views 264 * will be rendered. 265 * @param dartExecutor The dart execution context, which is used to setup a system channel. 266 */ attach(Context context, TextureRegistry textureRegistry, @NonNull DartExecutor dartExecutor)267 public void attach(Context context, TextureRegistry textureRegistry, @NonNull DartExecutor dartExecutor) { 268 if (this.context != null) { 269 throw new AssertionError( 270 "A PlatformViewsController can only be attached to a single output target.\n" + 271 "attach was called while the PlatformViewsController was already attached." 272 ); 273 } 274 this.context = context; 275 this.textureRegistry = textureRegistry; 276 platformViewsChannel = new PlatformViewsChannel(dartExecutor); 277 platformViewsChannel.setPlatformViewsHandler(channelHandler); 278 } 279 280 /** 281 * Detaches this platform views controller. 282 * 283 * This is typically called when a Flutter applications moves to run in the background, or is destroyed. 284 * After calling this the platform views controller will no longer listen to it's previous messenger, and will 285 * not maintain references to the texture registry, context, and messenger passed to the previous attach call. 286 */ 287 @UiThread detach()288 public void detach() { 289 platformViewsChannel.setPlatformViewsHandler(null); 290 platformViewsChannel = null; 291 context = null; 292 textureRegistry = null; 293 } 294 295 @Override attachAccessibilityBridge(AccessibilityBridge accessibilityBridge)296 public void attachAccessibilityBridge(AccessibilityBridge accessibilityBridge) { 297 accessibilityEventsDelegate.setAccessibilityBridge(accessibilityBridge); 298 } 299 300 @Override detachAccessibiltyBridge()301 public void detachAccessibiltyBridge() { 302 accessibilityEventsDelegate.setAccessibilityBridge(null); 303 } 304 305 /** 306 * Attaches this controller to a text input plugin. 307 * 308 * While a text input plugin is available, the platform views controller interacts with it to facilitate 309 * delegation of text input connections to platform views. 310 * 311 * A platform views controller should be attached to a text input plugin whenever it is possible for the Flutter 312 * framework to receive text input. 313 */ attachTextInputPlugin(TextInputPlugin textInputPlugin)314 public void attachTextInputPlugin(TextInputPlugin textInputPlugin) { 315 this.textInputPlugin = textInputPlugin; 316 } 317 318 /** 319 * Detaches this controller from the currently attached text input plugin. 320 */ detachTextInputPlugin()321 public void detachTextInputPlugin() { 322 textInputPlugin = null; 323 } 324 325 /** 326 * Returns true if Flutter should perform input connection proxying for the view. 327 * 328 * If the view is a platform view managed by this platform views controller returns true. 329 * Else if the view was created in a platform view's VD, delegates the decision to the platform view's 330 * {@link View#checkInputConnectionProxy(View)} method. 331 * Else returns false. 332 */ checkInputConnectionProxy(View view)333 public boolean checkInputConnectionProxy(View view) { 334 if(!contextToPlatformView.containsKey(view.getContext())) { 335 return false; 336 } 337 View platformView = contextToPlatformView.get(view.getContext()); 338 if (platformView == view) { 339 return true; 340 } 341 return platformView.checkInputConnectionProxy(view); 342 } 343 getRegistry()344 public PlatformViewRegistry getRegistry() { 345 return registry; 346 } 347 onFlutterViewDestroyed()348 public void onFlutterViewDestroyed() { 349 flushAllViews(); 350 } 351 onPreEngineRestart()352 public void onPreEngineRestart() { 353 flushAllViews(); 354 } 355 356 @Override getPlatformViewById(Integer id)357 public View getPlatformViewById(Integer id) { 358 VirtualDisplayController controller = vdControllers.get(id); 359 if (controller == null) { 360 return null; 361 } 362 return controller.getView(); 363 } 364 lockInputConnection(@onNull VirtualDisplayController controller)365 private void lockInputConnection(@NonNull VirtualDisplayController controller) { 366 if (textInputPlugin == null) { 367 return; 368 } 369 textInputPlugin.lockPlatformViewInputConnection(); 370 controller.onInputConnectionLocked(); 371 } 372 unlockInputConnection(@onNull VirtualDisplayController controller)373 private void unlockInputConnection(@NonNull VirtualDisplayController controller) { 374 if (textInputPlugin == null) { 375 return; 376 } 377 textInputPlugin.unlockPlatformViewInputConnection(); 378 controller.onInputConnectionUnlocked(); 379 } 380 validateDirection(int direction)381 private static boolean validateDirection(int direction) { 382 return direction == View.LAYOUT_DIRECTION_LTR || direction == View.LAYOUT_DIRECTION_RTL; 383 } 384 385 @SuppressWarnings("unchecked") parsePointerPropertiesList(Object rawPropertiesList)386 private static List<PointerProperties> parsePointerPropertiesList(Object rawPropertiesList) { 387 List<Object> rawProperties = (List<Object>) rawPropertiesList; 388 List<PointerProperties> pointerProperties = new ArrayList<>(); 389 for (Object o : rawProperties) { 390 pointerProperties.add(parsePointerProperties(o)); 391 } 392 return pointerProperties; 393 } 394 395 @SuppressWarnings("unchecked") parsePointerProperties(Object rawProperties)396 private static PointerProperties parsePointerProperties(Object rawProperties) { 397 List<Object> propertiesList = (List<Object>) rawProperties; 398 PointerProperties properties = new MotionEvent.PointerProperties(); 399 properties.id = (int) propertiesList.get(0); 400 properties.toolType = (int) propertiesList.get(1); 401 return properties; 402 } 403 404 @SuppressWarnings("unchecked") parsePointerCoordsList(Object rawCoordsList, float density)405 private static List<PointerCoords> parsePointerCoordsList(Object rawCoordsList, float density) { 406 List<Object> rawCoords = (List<Object>) rawCoordsList; 407 List<PointerCoords> pointerCoords = new ArrayList<>(); 408 for (Object o : rawCoords) { 409 pointerCoords.add(parsePointerCoords(o, density)); 410 } 411 return pointerCoords; 412 } 413 414 @SuppressWarnings("unchecked") parsePointerCoords(Object rawCoords, float density)415 private static PointerCoords parsePointerCoords(Object rawCoords, float density) { 416 List<Object> coordsList = (List<Object>) rawCoords; 417 PointerCoords coords = new MotionEvent.PointerCoords(); 418 coords.orientation = (float) (double) coordsList.get(0); 419 coords.pressure = (float) (double) coordsList.get(1); 420 coords.size = (float) (double) coordsList.get(2); 421 coords.toolMajor = (float) (double) coordsList.get(3) * density; 422 coords.toolMinor = (float) (double) coordsList.get(4) * density; 423 coords.touchMajor = (float) (double) coordsList.get(5) * density; 424 coords.touchMinor = (float) (double) coordsList.get(6) * density; 425 coords.x = (float) (double) coordsList.get(7) * density; 426 coords.y = (float) (double) coordsList.get(8) * density; 427 return coords; 428 } 429 430 // Creating a VirtualDisplay larger than the size of the device screen size 431 // could cause the device to restart: https://github.com/flutter/flutter/issues/28978 validateVirtualDisplayDimensions(int width, int height)432 private void validateVirtualDisplayDimensions(int width, int height) { 433 DisplayMetrics metrics = context.getResources().getDisplayMetrics(); 434 if (height > metrics.heightPixels || width > metrics.widthPixels) { 435 String message = "Creating a virtual display of size: " 436 + "[" + width + ", " + height + "] may result in problems" 437 + "(https://github.com/flutter/flutter/issues/2897)." 438 + "It is larger than the device screen size: " 439 + "[" + metrics.widthPixels + ", " + metrics.heightPixels + "]."; 440 Log.w(TAG, message); 441 } 442 } 443 toPhysicalPixels(double logicalPixels)444 private int toPhysicalPixels(double logicalPixels) { 445 float density = context.getResources().getDisplayMetrics().density; 446 return (int) Math.round(logicalPixels * density); 447 } 448 flushAllViews()449 private void flushAllViews() { 450 for (VirtualDisplayController controller : vdControllers.values()) { 451 controller.dispose(); 452 } 453 vdControllers.clear(); 454 } 455 } 456