blob: bc36210e4c9b780649618767d3d46c205f8cf53c [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.plugin.platform;
import static android.view.MotionEvent.PointerCoords;
import static android.view.MotionEvent.PointerProperties;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.systemchannels.PlatformViewsChannel;
import io.flutter.plugin.editing.TextInputPlugin;
import io.flutter.view.AccessibilityBridge;
import io.flutter.view.TextureRegistry;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* Manages platform views.
*
* <p>Each {@link io.flutter.app.FlutterPluginRegistry} has a single platform views controller. A
* platform views controller can be attached to at most one Flutter view.
*/
public class PlatformViewsController implements PlatformViewsAccessibilityDelegate {
private static final String TAG = "PlatformViewsController";
// API level 20 is required for VirtualDisplay#setSurface which we use when resizing a platform
// view.
private static final int MINIMAL_SDK = Build.VERSION_CODES.KITKAT_WATCH;
private final PlatformViewRegistryImpl registry;
// The context of the Activity or Fragment hosting the render target for the Flutter engine.
private Context context;
// The View currently rendering the Flutter UI associated with these platform views.
private View flutterView;
// The texture registry maintaining the textures into which the embedded views will be rendered.
private TextureRegistry textureRegistry;
private TextInputPlugin textInputPlugin;
// The system channel used to communicate with the framework about platform views.
private PlatformViewsChannel platformViewsChannel;
// The accessibility bridge to which accessibility events form the platform views will be
// dispatched.
private final AccessibilityEventsDelegate accessibilityEventsDelegate;
// TODO(mattcarroll): Refactor overall platform views to facilitate testing and then make
// this private. This is visible as a hack to facilitate testing. This was deemed the least
// bad option at the time of writing.
@VisibleForTesting /* package */ final HashMap<Integer, VirtualDisplayController> vdControllers;
// Maps a virtual display's context to the platform view hosted in this virtual display.
// Since each virtual display has it's unique context this allows associating any view with the
// platform view that
// it is associated with(e.g if a platform view creates other views in the same virtual display.
private final HashMap<Context, View> contextToPlatformView;
private final PlatformViewsChannel.PlatformViewsHandler channelHandler =
new PlatformViewsChannel.PlatformViewsHandler() {
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
@Override
public long createPlatformView(
@NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
ensureValidAndroidVersion();
if (!validateDirection(request.direction)) {
throw new IllegalStateException(
"Trying to create a view with unknown direction value: "
+ request.direction
+ "(view id: "
+ request.viewId
+ ")");
}
if (vdControllers.containsKey(request.viewId)) {
throw new IllegalStateException(
"Trying to create an already created platform view, view id: " + request.viewId);
}
PlatformViewFactory viewFactory = registry.getFactory(request.viewType);
if (viewFactory == null) {
throw new IllegalStateException(
"Trying to create a platform view of unregistered type: " + request.viewType);
}
Object createParams = null;
if (request.params != null) {
createParams = viewFactory.getCreateArgsCodec().decodeMessage(request.params);
}
int physicalWidth = toPhysicalPixels(request.logicalWidth);
int physicalHeight = toPhysicalPixels(request.logicalHeight);
validateVirtualDisplayDimensions(physicalWidth, physicalHeight);
TextureRegistry.SurfaceTextureEntry textureEntry = textureRegistry.createSurfaceTexture();
VirtualDisplayController vdController =
VirtualDisplayController.create(
context,
accessibilityEventsDelegate,
viewFactory,
textureEntry,
physicalWidth,
physicalHeight,
request.viewId,
createParams,
(view, hasFocus) -> {
if (hasFocus) {
platformViewsChannel.invokeViewFocused(request.viewId);
}
});
if (vdController == null) {
throw new IllegalStateException(
"Failed creating virtual display for a "
+ request.viewType
+ " with id: "
+ request.viewId);
}
// If our FlutterEngine is already attached to a Flutter UI, provide that Android
// View to this new platform view.
if (flutterView != null) {
vdController.onFlutterViewAttached(flutterView);
}
vdControllers.put(request.viewId, vdController);
View platformView = vdController.getView();
platformView.setLayoutDirection(request.direction);
contextToPlatformView.put(platformView.getContext(), platformView);
// TODO(amirh): copy accessibility nodes to the FlutterView's accessibility tree.
return textureEntry.id();
}
@Override
public void disposePlatformView(int viewId) {
ensureValidAndroidVersion();
VirtualDisplayController vdController = vdControllers.get(viewId);
if (vdController == null) {
throw new IllegalStateException(
"Trying to dispose a platform view with unknown id: " + viewId);
}
if (textInputPlugin != null) {
textInputPlugin.clearPlatformViewClient(viewId);
}
contextToPlatformView.remove(vdController.getView().getContext());
vdController.dispose();
vdControllers.remove(viewId);
}
@Override
public void resizePlatformView(
@NonNull PlatformViewsChannel.PlatformViewResizeRequest request,
@NonNull Runnable onComplete) {
ensureValidAndroidVersion();
final VirtualDisplayController vdController = vdControllers.get(request.viewId);
if (vdController == null) {
throw new IllegalStateException(
"Trying to resize a platform view with unknown id: " + request.viewId);
}
int physicalWidth = toPhysicalPixels(request.newLogicalWidth);
int physicalHeight = toPhysicalPixels(request.newLogicalHeight);
validateVirtualDisplayDimensions(physicalWidth, physicalHeight);
// Resizing involved moving the platform view to a new virtual display. Doing so
// potentially results in losing an active input connection. To make sure we preserve
// the input connection when resizing we lock it here and unlock after the resize is
// complete.
lockInputConnection(vdController);
vdController.resize(
physicalWidth,
physicalHeight,
new Runnable() {
@Override
public void run() {
unlockInputConnection(vdController);
onComplete.run();
}
});
}
@Override
public void onTouch(@NonNull PlatformViewsChannel.PlatformViewTouch touch) {
ensureValidAndroidVersion();
float density = context.getResources().getDisplayMetrics().density;
PointerProperties[] pointerProperties =
parsePointerPropertiesList(touch.rawPointerPropertiesList)
.toArray(new PointerProperties[touch.pointerCount]);
PointerCoords[] pointerCoords =
parsePointerCoordsList(touch.rawPointerCoords, density)
.toArray(new PointerCoords[touch.pointerCount]);
if (!vdControllers.containsKey(touch.viewId)) {
throw new IllegalStateException(
"Sending touch to an unknown view with id: " + touch.viewId);
}
MotionEvent event =
MotionEvent.obtain(
touch.downTime.longValue(),
touch.eventTime.longValue(),
touch.action,
touch.pointerCount,
pointerProperties,
pointerCoords,
touch.metaState,
touch.buttonState,
touch.xPrecision,
touch.yPrecision,
touch.deviceId,
touch.edgeFlags,
touch.source,
touch.flags);
vdControllers.get(touch.viewId).dispatchTouchEvent(event);
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
@Override
public void setDirection(int viewId, int direction) {
ensureValidAndroidVersion();
if (!validateDirection(direction)) {
throw new IllegalStateException(
"Trying to set unknown direction value: "
+ direction
+ "(view id: "
+ viewId
+ ")");
}
View view = vdControllers.get(viewId).getView();
if (view == null) {
throw new IllegalStateException(
"Sending touch to an unknown view with id: " + direction);
}
view.setLayoutDirection(direction);
}
@Override
public void clearFocus(int viewId) {
View view = vdControllers.get(viewId).getView();
view.clearFocus();
}
private void ensureValidAndroidVersion() {
if (Build.VERSION.SDK_INT < MINIMAL_SDK) {
throw new IllegalStateException(
"Trying to use platform views with API "
+ Build.VERSION.SDK_INT
+ ", required API level is: "
+ MINIMAL_SDK);
}
}
};
public PlatformViewsController() {
registry = new PlatformViewRegistryImpl();
vdControllers = new HashMap<>();
accessibilityEventsDelegate = new AccessibilityEventsDelegate();
contextToPlatformView = new HashMap<>();
}
/**
* Attaches this platform views controller to its input and output channels.
*
* @param context The base context that will be passed to embedded views created by this
* controller. This should be the context of the Activity hosting the Flutter application.
* @param textureRegistry The texture registry which provides the output textures into which the
* embedded views will be rendered.
* @param dartExecutor The dart execution context, which is used to setup a system channel.
*/
public void attach(
Context context, TextureRegistry textureRegistry, @NonNull DartExecutor dartExecutor) {
if (this.context != null) {
throw new AssertionError(
"A PlatformViewsController can only be attached to a single output target.\n"
+ "attach was called while the PlatformViewsController was already attached.");
}
this.context = context;
this.textureRegistry = textureRegistry;
platformViewsChannel = new PlatformViewsChannel(dartExecutor);
platformViewsChannel.setPlatformViewsHandler(channelHandler);
}
/**
* Detaches this platform views controller.
*
* <p>This is typically called when a Flutter applications moves to run in the background, or is
* destroyed. After calling this the platform views controller will no longer listen to it's
* previous messenger, and will not maintain references to the texture registry, context, and
* messenger passed to the previous attach call.
*/
@UiThread
public void detach() {
platformViewsChannel.setPlatformViewsHandler(null);
platformViewsChannel = null;
context = null;
textureRegistry = null;
}
/**
* This {@code PlatformViewsController} and its {@code FlutterEngine} is now attached to an
* Android {@code View} that renders a Flutter UI.
*/
public void attachToView(@NonNull View flutterView) {
this.flutterView = flutterView;
// Inform all existing platform views that they are now associated with
// a Flutter View.
for (VirtualDisplayController controller : vdControllers.values()) {
controller.onFlutterViewAttached(flutterView);
}
}
/**
* This {@code PlatformViewController} and its {@code FlutterEngine} are no longer attached to an
* Android {@code View} that renders a Flutter UI.
*
* <p>All platform views controlled by this {@code PlatformViewController} will be detached from
* the previously attached {@code View}.
*/
public void detachFromView() {
this.flutterView = null;
// Inform all existing platform views that they are no longer associated with
// a Flutter View.
for (VirtualDisplayController controller : vdControllers.values()) {
controller.onFlutterViewDetached();
}
}
@Override
public void attachAccessibilityBridge(AccessibilityBridge accessibilityBridge) {
accessibilityEventsDelegate.setAccessibilityBridge(accessibilityBridge);
}
@Override
public void detachAccessibiltyBridge() {
accessibilityEventsDelegate.setAccessibilityBridge(null);
}
/**
* Attaches this controller to a text input plugin.
*
* <p>While a text input plugin is available, the platform views controller interacts with it to
* facilitate delegation of text input connections to platform views.
*
* <p>A platform views controller should be attached to a text input plugin whenever it is
* possible for the Flutter framework to receive text input.
*/
public void attachTextInputPlugin(TextInputPlugin textInputPlugin) {
this.textInputPlugin = textInputPlugin;
}
/** Detaches this controller from the currently attached text input plugin. */
public void detachTextInputPlugin() {
textInputPlugin = null;
}
/**
* Returns true if Flutter should perform input connection proxying for the view.
*
* <p>If the view is a platform view managed by this platform views controller returns true. Else
* if the view was created in a platform view's VD, delegates the decision to the platform view's
* {@link View#checkInputConnectionProxy(View)} method. Else returns false.
*/
public boolean checkInputConnectionProxy(View view) {
if (!contextToPlatformView.containsKey(view.getContext())) {
return false;
}
View platformView = contextToPlatformView.get(view.getContext());
if (platformView == view) {
return true;
}
return platformView.checkInputConnectionProxy(view);
}
public PlatformViewRegistry getRegistry() {
return registry;
}
/**
* Invoked when the {@link io.flutter.embedding.engine.FlutterEngine} that owns this {@link
* PlatformViewsController} attaches to JNI.
*/
public void onAttachedToJNI() {
// Currently no action needs to be taken after JNI attachment.
}
/**
* Invoked when the {@link io.flutter.embedding.engine.FlutterEngine} that owns this {@link
* PlatformViewsController} detaches from JNI.
*/
public void onDetachedFromJNI() {
// Dispose all virtual displays so that any future updates to textures will not be
// propagated to the native peer.
flushAllViews();
}
public void onPreEngineRestart() {
flushAllViews();
}
@Override
public View getPlatformViewById(Integer id) {
VirtualDisplayController controller = vdControllers.get(id);
if (controller == null) {
return null;
}
return controller.getView();
}
private void lockInputConnection(@NonNull VirtualDisplayController controller) {
if (textInputPlugin == null) {
return;
}
textInputPlugin.lockPlatformViewInputConnection();
controller.onInputConnectionLocked();
}
private void unlockInputConnection(@NonNull VirtualDisplayController controller) {
if (textInputPlugin == null) {
return;
}
textInputPlugin.unlockPlatformViewInputConnection();
controller.onInputConnectionUnlocked();
}
private static boolean validateDirection(int direction) {
return direction == View.LAYOUT_DIRECTION_LTR || direction == View.LAYOUT_DIRECTION_RTL;
}
@SuppressWarnings("unchecked")
private static List<PointerProperties> parsePointerPropertiesList(Object rawPropertiesList) {
List<Object> rawProperties = (List<Object>) rawPropertiesList;
List<PointerProperties> pointerProperties = new ArrayList<>();
for (Object o : rawProperties) {
pointerProperties.add(parsePointerProperties(o));
}
return pointerProperties;
}
@SuppressWarnings("unchecked")
private static PointerProperties parsePointerProperties(Object rawProperties) {
List<Object> propertiesList = (List<Object>) rawProperties;
PointerProperties properties = new MotionEvent.PointerProperties();
properties.id = (int) propertiesList.get(0);
properties.toolType = (int) propertiesList.get(1);
return properties;
}
@SuppressWarnings("unchecked")
private static List<PointerCoords> parsePointerCoordsList(Object rawCoordsList, float density) {
List<Object> rawCoords = (List<Object>) rawCoordsList;
List<PointerCoords> pointerCoords = new ArrayList<>();
for (Object o : rawCoords) {
pointerCoords.add(parsePointerCoords(o, density));
}
return pointerCoords;
}
@SuppressWarnings("unchecked")
private static PointerCoords parsePointerCoords(Object rawCoords, float density) {
List<Object> coordsList = (List<Object>) rawCoords;
PointerCoords coords = new MotionEvent.PointerCoords();
coords.orientation = (float) (double) coordsList.get(0);
coords.pressure = (float) (double) coordsList.get(1);
coords.size = (float) (double) coordsList.get(2);
coords.toolMajor = (float) (double) coordsList.get(3) * density;
coords.toolMinor = (float) (double) coordsList.get(4) * density;
coords.touchMajor = (float) (double) coordsList.get(5) * density;
coords.touchMinor = (float) (double) coordsList.get(6) * density;
coords.x = (float) (double) coordsList.get(7) * density;
coords.y = (float) (double) coordsList.get(8) * density;
return coords;
}
// Creating a VirtualDisplay larger than the size of the device screen size
// could cause the device to restart: https://github.com/flutter/flutter/issues/28978
private void validateVirtualDisplayDimensions(int width, int height) {
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
if (height > metrics.heightPixels || width > metrics.widthPixels) {
String message =
"Creating a virtual display of size: "
+ "["
+ width
+ ", "
+ height
+ "] may result in problems"
+ "(https://github.com/flutter/flutter/issues/2897)."
+ "It is larger than the device screen size: "
+ "["
+ metrics.widthPixels
+ ", "
+ metrics.heightPixels
+ "].";
Log.w(TAG, message);
}
}
private int toPhysicalPixels(double logicalPixels) {
float density = context.getResources().getDisplayMetrics().density;
return (int) Math.round(logicalPixels * density);
}
private void flushAllViews() {
for (VirtualDisplayController controller : vdControllers.values()) {
controller.dispose();
}
vdControllers.clear();
}
}