blob: 4b61cdc10d8d818fdc1b729e8f0d616d62f1d3cf [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.content.MutableContextWrapper;
import android.os.Build;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import io.flutter.Log;
import io.flutter.embedding.android.AndroidTouchProcessor;
import io.flutter.embedding.android.FlutterView;
import io.flutter.embedding.android.MotionEventTracker;
import io.flutter.embedding.engine.FlutterOverlaySurface;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.mutatorsstack.*;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.systemchannels.PlatformViewsChannel;
import io.flutter.plugin.editing.TextInputPlugin;
import io.flutter.util.ViewUtils;
import io.flutter.view.AccessibilityBridge;
import io.flutter.view.TextureRegistry;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
/**
* Manages platform views.
*
* <p>Each {@link io.flutter.embedding.engine.FlutterEngine} or {@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";
// These view types allow out-of-band drawing commands that don't notify the Android view
// hierarchy.
// To support these cases, Flutter hosts the embedded view in a VirtualDisplay,
// and binds the VirtualDisplay to a GL texture that is then composed by the engine.
// However, there are a few issues with Virtual Displays. For example, they don't fully support
// accessibility due to https://github.com/flutter/flutter/issues/29717,
// and keyboard interactions may have non-deterministic behavior.
// Views that issue out-of-band drawing commands that aren't included in this array are
// required to call `View#invalidate()` to notify Flutter about the update.
// This isn't ideal, but given all the other limitations it's a reasonable tradeoff.
// Related issue: https://github.com/flutter/flutter/issues/103630
private static Class[] VIEW_TYPES_REQUIRE_VIRTUAL_DISPLAY = {SurfaceView.class};
private final PlatformViewRegistryImpl registry;
private AndroidTouchProcessor androidTouchProcessor;
// 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 FlutterView flutterView;
// The texture registry maintaining the textures into which the embedded views will be rendered.
@Nullable private TextureRegistry textureRegistry;
@Nullable 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 embedded 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.
@VisibleForTesting /* package */ final HashMap<Context, View> contextToEmbeddedView;
// The platform views.
private final SparseArray<PlatformView> platformViews;
// The platform view wrappers that are appended to FlutterView.
//
// These platform views use a PlatformViewLayer in the framework. This is different than
// the platform views that use a TextureLayer.
//
// This distinction is necessary because a PlatformViewLayer allows to embed Android's
// SurfaceViews in a Flutter app whereas the texture layer is unable to support such native views.
//
// If an entry in `platformViews` doesn't have an entry in this array, the platform view isn't
// in the view hierarchy.
//
// This view provides a wrapper that applies scene builder operations to the platform view.
// For example, a transform matrix, or setting opacity on the platform view layer.
private final SparseArray<FlutterMutatorView> platformViewParent;
// Map of unique IDs to views that render overlay layers.
private final SparseArray<PlatformOverlayView> overlayLayerViews;
// The platform view wrappers that are appended to FlutterView.
//
// These platform views use a TextureLayer in the framework. This is different than
// the platform views that use a PlatformViewLayer.
//
// This is the default mode, and recommended for better performance.
private final SparseArray<PlatformViewWrapper> viewWrappers;
// Next available unique ID for use in overlayLayerViews.
private int nextOverlayLayerId = 0;
// Tracks whether the flutterView has been converted to use a FlutterImageView.
private boolean flutterViewConvertedToImageView = false;
// When adding platform views using Hybrid Composition, the engine converts the render surface
// to a FlutterImageView to help improve animation synchronization on Android. This flag allows
// disabling this conversion through the PlatformView platform channel.
private boolean synchronizeToNativeViewHierarchy = true;
// Overlay layer IDs that were displayed since the start of the current frame.
private final HashSet<Integer> currentFrameUsedOverlayLayerIds;
// Platform view IDs that were displayed since the start of the current frame.
private final HashSet<Integer> currentFrameUsedPlatformViewIds;
// Used to acquire the original motion events using the motionEventIds.
private final MotionEventTracker motionEventTracker;
// Whether software rendering is used.
private boolean usesSoftwareRendering = false;
private final PlatformViewsChannel.PlatformViewsHandler channelHandler =
new PlatformViewsChannel.PlatformViewsHandler() {
@TargetApi(19)
@Override
// TODO(egarciad): Remove the need for this.
// https://github.com/flutter/flutter/issues/96679
public void createForPlatformViewLayer(
@NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
// API level 19 is required for `android.graphics.ImageReader`.
ensureValidAndroidVersion(19);
ensureValidRequest(request);
final PlatformView platformView = createPlatformView(request, false);
configureForHybridComposition(platformView, request);
// New code should be added to configureForHybridComposition, not here, unless it is
// not applicable to fallback from TLHC to HC.
}
@TargetApi(20)
@Override
public long createForTextureLayer(
@NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
ensureValidRequest(request);
final int viewId = request.viewId;
if (viewWrappers.get(viewId) != null) {
throw new IllegalStateException(
"Trying to create an already created platform view, view id: " + viewId);
}
if (textureRegistry == null) {
throw new IllegalStateException(
"Texture registry is null. This means that platform views controller was detached, view id: "
+ viewId);
}
if (flutterView == null) {
throw new IllegalStateException(
"Flutter view is null. This means the platform views controller doesn't have an attached view, view id: "
+ viewId);
}
final PlatformView platformView = createPlatformView(request, true);
final View embeddedView = platformView.getView();
if (embeddedView.getParent() != null) {
throw new IllegalStateException(
"The Android view returned from PlatformView#getView() was already added to a parent view.");
}
// The newer Texture Layer Hybrid Composition mode isn't suppported if any of the
// following are true:
// - The embedded view contains any of the VIEW_TYPES_REQUIRE_VIRTUAL_DISPLAY view types.
// These views allow out-of-band graphics operations that aren't notified to the Android
// view hierarchy via callbacks such as ViewParent#onDescendantInvalidated().
// - The API level is <23, due to TLHC implementation API requirements.
final boolean supportsTextureLayerMode =
Build.VERSION.SDK_INT >= 23
&& !ViewUtils.hasChildViewOfType(
embeddedView, VIEW_TYPES_REQUIRE_VIRTUAL_DISPLAY);
// Fall back to Hybrid Composition or Virtual Display when necessary, depending on which
// fallback mode is requested.
if (!supportsTextureLayerMode) {
if (request.displayMode
== PlatformViewsChannel.PlatformViewCreationRequest.RequestedDisplayMode
.TEXTURE_WITH_HYBRID_FALLBACK) {
configureForHybridComposition(platformView, request);
return PlatformViewsChannel.PlatformViewsHandler.NON_TEXTURE_FALLBACK;
} else if (!usesSoftwareRendering) { // Virtual Display doesn't support software mode.
return configureForVirtualDisplay(platformView, request);
}
// TODO(stuartmorgan): Consider throwing a specific exception here as a breaking change.
// For now, preserve the 3.0 behavior of falling through to Texture Layer mode even
// though it won't work correctly.
}
return configureForTextureLayerComposition(platformView, request);
}
@Override
public void dispose(int viewId) {
final PlatformView platformView = platformViews.get(viewId);
if (platformView == null) {
Log.e(TAG, "Disposing unknown platform view with id: " + viewId);
return;
}
platformViews.remove(viewId);
try {
platformView.dispose();
} catch (RuntimeException exception) {
Log.e(TAG, "Disposing platform view threw an exception", exception);
}
if (usesVirtualDisplay(viewId)) {
final VirtualDisplayController vdController = vdControllers.get(viewId);
final View embeddedView = vdController.getView();
if (embeddedView != null) {
contextToEmbeddedView.remove(embeddedView.getContext());
}
vdControllers.remove(viewId);
return;
}
// The platform view is displayed using a TextureLayer and is inserted in the view
// hierarchy.
final PlatformViewWrapper viewWrapper = viewWrappers.get(viewId);
if (viewWrapper != null) {
viewWrapper.removeAllViews();
viewWrapper.release();
viewWrapper.unsetOnDescendantFocusChangeListener();
final ViewGroup wrapperParent = (ViewGroup) viewWrapper.getParent();
if (wrapperParent != null) {
wrapperParent.removeView(viewWrapper);
}
viewWrappers.remove(viewId);
return;
}
// The platform view is displayed using a PlatformViewLayer.
final FlutterMutatorView parentView = platformViewParent.get(viewId);
if (parentView != null) {
parentView.removeAllViews();
parentView.unsetOnDescendantFocusChangeListener();
final ViewGroup mutatorViewParent = (ViewGroup) parentView.getParent();
if (mutatorViewParent != null) {
mutatorViewParent.removeView(parentView);
}
platformViewParent.remove(viewId);
}
}
@Override
public void offset(int viewId, double top, double left) {
if (usesVirtualDisplay(viewId)) {
// Virtual displays don't need an accessibility offset.
return;
}
// For platform views that use TextureView and are in the view hierarchy, set
// an offset to the wrapper view.
// This ensures that the accessibility highlights are drawn in the expected position on
// screen.
// This offset doesn't affect the position of the embeded view by itself since the GL
// texture is positioned by the Flutter engine, which knows where to position different
// types of layers.
final PlatformViewWrapper viewWrapper = viewWrappers.get(viewId);
if (viewWrapper == null) {
Log.e(TAG, "Setting offset for unknown platform view with id: " + viewId);
return;
}
final int physicalTop = toPhysicalPixels(top);
final int physicalLeft = toPhysicalPixels(left);
final FrameLayout.LayoutParams layoutParams =
(FrameLayout.LayoutParams) viewWrapper.getLayoutParams();
layoutParams.topMargin = physicalTop;
layoutParams.leftMargin = physicalLeft;
viewWrapper.setLayoutParams(layoutParams);
}
@Override
public void resize(
@NonNull PlatformViewsChannel.PlatformViewResizeRequest request,
@NonNull PlatformViewsChannel.PlatformViewBufferResized onComplete) {
final int physicalWidth = toPhysicalPixels(request.newLogicalWidth);
final int physicalHeight = toPhysicalPixels(request.newLogicalHeight);
final int viewId = request.viewId;
if (usesVirtualDisplay(viewId)) {
final float originalDisplayDensity = getDisplayDensity();
final VirtualDisplayController vdController = vdControllers.get(viewId);
// 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,
() -> {
unlockInputConnection(vdController);
// Converting back to logic pixels requires a context, which may no longer be
// available. If that happens, assume the same logic/physical relationship as
// was present when the request arrived.
final float displayDensity =
context == null ? originalDisplayDensity : getDisplayDensity();
onComplete.run(
new PlatformViewsChannel.PlatformViewBufferSize(
toLogicalPixels(vdController.getBufferWidth(), displayDensity),
toLogicalPixels(vdController.getBufferHeight(), displayDensity)));
});
return;
}
final PlatformView platformView = platformViews.get(viewId);
final PlatformViewWrapper viewWrapper = viewWrappers.get(viewId);
if (platformView == null || viewWrapper == null) {
Log.e(TAG, "Resizing unknown platform view with id: " + viewId);
return;
}
// Resize the buffer only when the current buffer size is smaller than the new size.
// This is required to prevent a situation when smooth keyboard animation
// resizes the texture too often, such that the GPU and the platform thread don't agree on
// the
// timing of the new size.
// Resizing the texture causes pixel stretching since the size of the GL texture used in
// the engine
// is set by the framework, but the texture buffer size is set by the platform down below.
if (physicalWidth > viewWrapper.getBufferWidth()
|| physicalHeight > viewWrapper.getBufferHeight()) {
viewWrapper.setBufferSize(physicalWidth, physicalHeight);
}
final ViewGroup.LayoutParams viewWrapperLayoutParams = viewWrapper.getLayoutParams();
viewWrapperLayoutParams.width = physicalWidth;
viewWrapperLayoutParams.height = physicalHeight;
viewWrapper.setLayoutParams(viewWrapperLayoutParams);
final View embeddedView = platformView.getView();
if (embeddedView != null) {
final ViewGroup.LayoutParams embeddedViewLayoutParams = embeddedView.getLayoutParams();
embeddedViewLayoutParams.width = physicalWidth;
embeddedViewLayoutParams.height = physicalHeight;
embeddedView.setLayoutParams(embeddedViewLayoutParams);
}
onComplete.run(
new PlatformViewsChannel.PlatformViewBufferSize(
toLogicalPixels(viewWrapper.getBufferWidth()),
toLogicalPixels(viewWrapper.getBufferHeight())));
}
@Override
public void onTouch(@NonNull PlatformViewsChannel.PlatformViewTouch touch) {
final int viewId = touch.viewId;
final float density = context.getResources().getDisplayMetrics().density;
if (usesVirtualDisplay(viewId)) {
final VirtualDisplayController vdController = vdControllers.get(viewId);
final MotionEvent event = toMotionEvent(density, touch, true);
vdController.dispatchTouchEvent(event);
return;
}
final PlatformView platformView = platformViews.get(viewId);
if (platformView == null) {
Log.e(TAG, "Sending touch to an unknown view with id: " + viewId);
return;
}
final View view = platformView.getView();
if (view == null) {
Log.e(TAG, "Sending touch to a null view with id: " + viewId);
return;
}
final MotionEvent event = toMotionEvent(density, touch, false);
view.dispatchTouchEvent(event);
}
@TargetApi(17)
@Override
public void setDirection(int viewId, int direction) {
if (!validateDirection(direction)) {
throw new IllegalStateException(
"Trying to set unknown direction value: "
+ direction
+ "(view id: "
+ viewId
+ ")");
}
View embeddedView;
if (usesVirtualDisplay(viewId)) {
final VirtualDisplayController controller = vdControllers.get(viewId);
embeddedView = controller.getView();
} else {
final PlatformView platformView = platformViews.get(viewId);
if (platformView == null) {
Log.e(TAG, "Setting direction to an unknown view with id: " + viewId);
return;
}
embeddedView = platformView.getView();
}
if (embeddedView == null) {
Log.e(TAG, "Setting direction to a null view with id: " + viewId);
return;
}
embeddedView.setLayoutDirection(direction);
}
@Override
public void clearFocus(int viewId) {
View embeddedView;
if (usesVirtualDisplay(viewId)) {
final VirtualDisplayController controller = vdControllers.get(viewId);
embeddedView = controller.getView();
} else {
final PlatformView platformView = platformViews.get(viewId);
if (platformView == null) {
Log.e(TAG, "Clearing focus on an unknown view with id: " + viewId);
return;
}
embeddedView = platformView.getView();
}
if (embeddedView == null) {
Log.e(TAG, "Clearing focus on a null view with id: " + viewId);
return;
}
embeddedView.clearFocus();
}
private void ensureValidAndroidVersion(int minSdkVersion) {
if (Build.VERSION.SDK_INT < minSdkVersion) {
throw new IllegalStateException(
"Trying to use platform views with API "
+ Build.VERSION.SDK_INT
+ ", required API level is: "
+ minSdkVersion);
}
}
private void ensureValidRequest(
@NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
if (!validateDirection(request.direction)) {
throw new IllegalStateException(
"Trying to create a view with unknown direction value: "
+ request.direction
+ "(view id: "
+ request.viewId
+ ")");
}
}
// Creates a platform view based on `request`, performs configuration that's common to
// all display modes, and adds it to `platformViews`.
@TargetApi(19)
private PlatformView createPlatformView(
@NonNull PlatformViewsChannel.PlatformViewCreationRequest request,
boolean wrapContext) {
final 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);
}
// In some display modes, the context needs to be modified during display.
// TODO(stuartmorgan): Make this wrapping unconditional if possible; for context see
// https://github.com/flutter/flutter/issues/113449
final Context mutableContext = wrapContext ? new MutableContextWrapper(context) : context;
final PlatformView platformView =
viewFactory.create(mutableContext, request.viewId, createParams);
// Configure the view to match the requested layout direction.
final View embeddedView = platformView.getView();
if (embeddedView == null) {
throw new IllegalStateException(
"PlatformView#getView() returned null, but an Android view reference was expected.");
}
embeddedView.setLayoutDirection(request.direction);
platformViews.put(request.viewId, platformView);
return platformView;
}
// Configures the view for Hybrid Composition mode.
private void configureForHybridComposition(
@NonNull PlatformView platformView,
@NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
Log.i(TAG, "Using hybrid composition for platform view: " + request.viewId);
}
// Configures the view for Virtual Display mode, returning the associated texture ID.
private long configureForVirtualDisplay(
@NonNull PlatformView platformView,
@NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
// This mode adds the view to a virtual display, which is wired up to a GL texture that
// is composed by the Flutter engine.
// API level 20 is required to use VirtualDisplay#setSurface.
ensureValidAndroidVersion(20);
Log.i(TAG, "Hosting view in a virtual display for platform view: " + request.viewId);
final TextureRegistry.SurfaceTextureEntry textureEntry =
textureRegistry.createSurfaceTexture();
final int physicalWidth = toPhysicalPixels(request.logicalWidth);
final int physicalHeight = toPhysicalPixels(request.logicalHeight);
final VirtualDisplayController vdController =
VirtualDisplayController.create(
context,
accessibilityEventsDelegate,
platformView,
textureEntry,
physicalWidth,
physicalHeight,
request.viewId,
null,
(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);
}
// The embedded view doesn't need to be sized in Virtual Display mode because the
// virtual display itself is sized.
vdControllers.put(request.viewId, vdController);
final View embeddedView = platformView.getView();
contextToEmbeddedView.put(embeddedView.getContext(), embeddedView);
return textureEntry.id();
}
// Configures the view for Texture Layer Hybrid Composition mode, returning the associated
// texture ID.
@TargetApi(23)
private long configureForTextureLayerComposition(
@NonNull PlatformView platformView,
@NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
// This mode attaches the view to the Android view hierarchy and record its drawing
// operations, so they can be forwarded to a GL texture that is composed by the
// Flutter engine.
// API level 23 is required to use Surface#lockHardwareCanvas().
ensureValidAndroidVersion(23);
Log.i(TAG, "Hosting view in view hierarchy for platform view: " + request.viewId);
final int physicalWidth = toPhysicalPixels(request.logicalWidth);
final int physicalHeight = toPhysicalPixels(request.logicalHeight);
PlatformViewWrapper viewWrapper;
long textureId;
if (usesSoftwareRendering) {
viewWrapper = new PlatformViewWrapper(context);
textureId = -1;
} else {
final TextureRegistry.SurfaceTextureEntry textureEntry =
textureRegistry.createSurfaceTexture();
viewWrapper = new PlatformViewWrapper(context, textureEntry);
textureId = textureEntry.id();
}
viewWrapper.setTouchProcessor(androidTouchProcessor);
viewWrapper.setBufferSize(physicalWidth, physicalHeight);
final FrameLayout.LayoutParams viewWrapperLayoutParams =
new FrameLayout.LayoutParams(physicalWidth, physicalHeight);
// Size and position the view wrapper.
final int physicalTop = toPhysicalPixels(request.logicalTop);
final int physicalLeft = toPhysicalPixels(request.logicalLeft);
viewWrapperLayoutParams.topMargin = physicalTop;
viewWrapperLayoutParams.leftMargin = physicalLeft;
viewWrapper.setLayoutParams(viewWrapperLayoutParams);
// Size the embedded view.
final View embeddedView = platformView.getView();
embeddedView.setLayoutParams(new FrameLayout.LayoutParams(physicalWidth, physicalHeight));
// Accessibility in the embedded view is initially disabled because if a Flutter app
// disabled accessibility in the first frame, the embedding won't receive an update to
// disable accessibility since the embedding never received an update to enable it.
// The AccessibilityBridge keeps track of the accessibility nodes, and handles the deltas
// when the framework sends a new a11y tree to the embedding.
// To prevent races, the framework populate the SemanticsNode after the platform view has
// been created.
embeddedView.setImportantForAccessibility(
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
// Add the embedded view to the wrapper.
viewWrapper.addView(embeddedView);
// Listen for focus changed in any subview, so the framework is notified when the platform
// view is focused.
viewWrapper.setOnDescendantFocusChangeListener(
(v, hasFocus) -> {
if (hasFocus) {
platformViewsChannel.invokeViewFocused(request.viewId);
} else if (textInputPlugin != null) {
textInputPlugin.clearPlatformViewClient(request.viewId);
}
});
flutterView.addView(viewWrapper);
viewWrappers.append(request.viewId, viewWrapper);
return textureId;
}
@Override
public void synchronizeToNativeViewHierarchy(boolean yes) {
synchronizeToNativeViewHierarchy = yes;
}
};
@VisibleForTesting
public MotionEvent toMotionEvent(
float density, PlatformViewsChannel.PlatformViewTouch touch, boolean usingVirtualDiplay) {
MotionEventTracker.MotionEventId motionEventId =
MotionEventTracker.MotionEventId.from(touch.motionEventId);
MotionEvent trackedEvent = motionEventTracker.pop(motionEventId);
// Pointer coordinates in the tracked events are global to FlutterView
// framework converts them to be local to a widget, given that
// motion events operate on local coords, we need to replace these in the tracked
// event with their local counterparts.
PointerProperties[] pointerProperties =
parsePointerPropertiesList(touch.rawPointerPropertiesList)
.toArray(new PointerProperties[touch.pointerCount]);
PointerCoords[] pointerCoords =
parsePointerCoordsList(touch.rawPointerCoords, density)
.toArray(new PointerCoords[touch.pointerCount]);
if (!usingVirtualDiplay && trackedEvent != null) {
return MotionEvent.obtain(
trackedEvent.getDownTime(),
trackedEvent.getEventTime(),
touch.action,
touch.pointerCount,
pointerProperties,
pointerCoords,
trackedEvent.getMetaState(),
trackedEvent.getButtonState(),
trackedEvent.getXPrecision(),
trackedEvent.getYPrecision(),
trackedEvent.getDeviceId(),
trackedEvent.getEdgeFlags(),
trackedEvent.getSource(),
trackedEvent.getFlags());
}
// TODO (kaushikiska) : warn that we are potentially using an untracked
// event in the platform views.
return 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);
}
public PlatformViewsController() {
registry = new PlatformViewRegistryImpl();
vdControllers = new HashMap<>();
accessibilityEventsDelegate = new AccessibilityEventsDelegate();
contextToEmbeddedView = new HashMap<>();
overlayLayerViews = new SparseArray<>();
currentFrameUsedOverlayLayerIds = new HashSet<>();
currentFrameUsedPlatformViewIds = new HashSet<>();
viewWrappers = new SparseArray<>();
platformViews = new SparseArray<>();
platformViewParent = new SparseArray<>();
motionEventTracker = MotionEventTracker.getInstance();
}
/**
* 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 set up a system channel.
*/
public void attach(
@Nullable Context context,
@NonNull 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);
}
/**
* Sets whether Flutter uses software rendering.
*
* <p>When software rendering is used, no GL context is available on the raster thread. When this
* is set to true, there's no Flutter composition of Android views and Flutter widgets since GL
* textures cannot be used.
*
* <p>Software rendering is only used for testing in emulators, and it should never be set to true
* in a production environment.
*
* @param useSoftwareRendering Whether software rendering is used.
*/
public void setSoftwareRendering(boolean useSoftwareRendering) {
usesSoftwareRendering = useSoftwareRendering;
}
/**
* 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() {
if (platformViewsChannel != null) {
platformViewsChannel.setPlatformViewsHandler(null);
}
destroyOverlaySurfaces();
platformViewsChannel = null;
context = null;
textureRegistry = null;
}
/**
* Attaches the controller to a {@link FlutterView}.
*
* <p>When {@link io.flutter.embedding.android.FlutterFragment} is used, this method is called
* after the device rotates since the FlutterView is recreated after a rotation.
*/
public void attachToView(@NonNull FlutterView newFlutterView) {
flutterView = newFlutterView;
// Add wrapper for platform views that use GL texture.
for (int index = 0; index < viewWrappers.size(); index++) {
final PlatformViewWrapper view = viewWrappers.valueAt(index);
flutterView.addView(view);
}
// Add wrapper for platform views that are composed at the view hierarchy level.
for (int index = 0; index < platformViewParent.size(); index++) {
final FlutterMutatorView view = platformViewParent.valueAt(index);
flutterView.addView(view);
}
// Notify platform views that they are now attached to a FlutterView.
for (int index = 0; index < platformViews.size(); index++) {
final PlatformView view = platformViews.valueAt(index);
view.onFlutterViewAttached(flutterView);
}
}
/**
* Detaches the controller from {@link FlutterView}.
*
* <p>When {@link io.flutter.embedding.android.FlutterFragment} is used, this method is called
* when the device rotates since the FlutterView is detached from the fragment. The next time the
* fragment needs to be displayed, a new Flutter view is created, so attachToView is called again.
*/
public void detachFromView() {
// Remove wrapper for platform views that use GL texture.
for (int index = 0; index < viewWrappers.size(); index++) {
final PlatformViewWrapper view = viewWrappers.valueAt(index);
flutterView.removeView(view);
}
// Remove wrapper for platform views that are composed at the view hierarchy level.
for (int index = 0; index < platformViewParent.size(); index++) {
final FlutterMutatorView view = platformViewParent.valueAt(index);
flutterView.removeView(view);
}
destroyOverlaySurfaces();
removeOverlaySurfaces();
flutterView = null;
flutterViewConvertedToImageView = false;
// Notify that the platform view have been detached from FlutterView.
for (int index = 0; index < platformViews.size(); index++) {
final PlatformView view = platformViews.valueAt(index);
view.onFlutterViewDetached();
}
}
@Override
public void attachAccessibilityBridge(@NonNull AccessibilityBridge accessibilityBridge) {
accessibilityEventsDelegate.setAccessibilityBridge(accessibilityBridge);
}
@Override
public void detachAccessibilityBridge() {
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(@NonNull 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(@Nullable View view) {
// View can be null on some devices
// See: https://github.com/flutter/flutter/issues/36517
if (view == null) {
return false;
}
if (!contextToEmbeddedView.containsKey(view.getContext())) {
return false;
}
View platformView = contextToEmbeddedView.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() {
diposeAllViews();
}
public void onPreEngineRestart() {
diposeAllViews();
}
@Override
@Nullable
public View getPlatformViewById(int viewId) {
if (usesVirtualDisplay(viewId)) {
final VirtualDisplayController controller = vdControllers.get(viewId);
return controller.getView();
}
final PlatformView platformView = platformViews.get(viewId);
if (platformView == null) {
return null;
}
return platformView.getView();
}
@Override
public boolean usesVirtualDisplay(int id) {
return vdControllers.containsKey(id);
}
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;
}
private float getDisplayDensity() {
return context.getResources().getDisplayMetrics().density;
}
private int toPhysicalPixels(double logicalPixels) {
return (int) Math.round(logicalPixels * getDisplayDensity());
}
private int toLogicalPixels(double physicalPixels, float displayDensity) {
return (int) Math.round(physicalPixels / displayDensity);
}
private int toLogicalPixels(double physicalPixels) {
return toLogicalPixels(physicalPixels, getDisplayDensity());
}
private void diposeAllViews() {
while (platformViews.size() > 0) {
final int viewId = platformViews.keyAt(0);
// Dispose deletes the entry from platformViews and clears associated resources.
channelHandler.dispose(viewId);
}
}
private void initializeRootImageViewIfNeeded() {
if (synchronizeToNativeViewHierarchy && !flutterViewConvertedToImageView) {
flutterView.convertToImageView();
flutterViewConvertedToImageView = true;
}
}
/**
* Initializes a platform view and adds it to the view hierarchy.
*
* @param viewId The view ID. This member is not intended for public use, and is only visible for
* testing.
*/
@VisibleForTesting
@TargetApi(Build.VERSION_CODES.KITKAT)
void initializePlatformViewIfNeeded(int viewId) {
final PlatformView platformView = platformViews.get(viewId);
if (platformView == null) {
throw new IllegalStateException(
"Platform view hasn't been initialized from the platform view channel.");
}
if (platformViewParent.get(viewId) != null) {
return;
}
final View embeddedView = platformView.getView();
if (embeddedView == null) {
throw new IllegalStateException(
"PlatformView#getView() returned null, but an Android view reference was expected.");
}
if (embeddedView.getParent() != null) {
throw new IllegalStateException(
"The Android view returned from PlatformView#getView() was already added to a parent view.");
}
final FlutterMutatorView parentView =
new FlutterMutatorView(
context, context.getResources().getDisplayMetrics().density, androidTouchProcessor);
parentView.setOnDescendantFocusChangeListener(
(view, hasFocus) -> {
if (hasFocus) {
platformViewsChannel.invokeViewFocused(viewId);
} else if (textInputPlugin != null) {
textInputPlugin.clearPlatformViewClient(viewId);
}
});
platformViewParent.put(viewId, parentView);
// Accessibility in the embedded view is initially disabled because if a Flutter app disabled
// accessibility in the first frame, the embedding won't receive an update to disable
// accessibility since the embedding never received an update to enable it.
// The AccessibilityBridge keeps track of the accessibility nodes, and handles the deltas when
// the framework sends a new a11y tree to the embedding.
// To prevent races, the framework populate the SemanticsNode after the platform view has been
// created.
embeddedView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
parentView.addView(embeddedView);
flutterView.addView(parentView);
}
public void attachToFlutterRenderer(@NonNull FlutterRenderer flutterRenderer) {
androidTouchProcessor = new AndroidTouchProcessor(flutterRenderer, /*trackMotionEvents=*/ true);
}
/**
* Called when a platform view id displayed in the current frame.
*
* @param viewId The ID of the platform view.
* @param x The left position relative to {@code FlutterView}.
* @param y The top position relative to {@code FlutterView}.
* @param width The width of the platform view.
* @param height The height of the platform view.
* @param viewWidth The original width of the platform view before applying the mutator stack.
* @param viewHeight The original height of the platform view before applying the mutator stack.
* @param mutatorsStack The mutator stack. This member is not intended for public use, and is only
* visible for testing.
*/
public void onDisplayPlatformView(
int viewId,
int x,
int y,
int width,
int height,
int viewWidth,
int viewHeight,
@NonNull FlutterMutatorsStack mutatorsStack) {
initializeRootImageViewIfNeeded();
initializePlatformViewIfNeeded(viewId);
final FlutterMutatorView parentView = platformViewParent.get(viewId);
parentView.readyToDisplay(mutatorsStack, x, y, width, height);
parentView.setVisibility(View.VISIBLE);
parentView.bringToFront();
final FrameLayout.LayoutParams layoutParams =
new FrameLayout.LayoutParams(viewWidth, viewHeight);
final View view = platformViews.get(viewId).getView();
if (view != null) {
view.setLayoutParams(layoutParams);
view.bringToFront();
}
currentFrameUsedPlatformViewIds.add(viewId);
}
/**
* Called when an overlay surface is displayed in the current frame.
*
* @param id The ID of the surface.
* @param x The left position relative to {@code FlutterView}.
* @param y The top position relative to {@code FlutterView}.
* @param width The width of the surface.
* @param height The height of the surface. This member is not intended for public use, and is
* only visible for testing.
*/
public void onDisplayOverlaySurface(int id, int x, int y, int width, int height) {
if (overlayLayerViews.get(id) == null) {
throw new IllegalStateException("The overlay surface (id:" + id + ") doesn't exist");
}
initializeRootImageViewIfNeeded();
final PlatformOverlayView overlayView = overlayLayerViews.get(id);
if (overlayView.getParent() == null) {
flutterView.addView(overlayView);
}
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams((int) width, (int) height);
layoutParams.leftMargin = (int) x;
layoutParams.topMargin = (int) y;
overlayView.setLayoutParams(layoutParams);
overlayView.setVisibility(View.VISIBLE);
overlayView.bringToFront();
currentFrameUsedOverlayLayerIds.add(id);
}
public void onBeginFrame() {
currentFrameUsedOverlayLayerIds.clear();
currentFrameUsedPlatformViewIds.clear();
}
/**
* Called by {@code FlutterJNI} when the Flutter frame was submitted.
*
* <p>This member is not intended for public use, and is only visible for testing.
*/
public void onEndFrame() {
// If there are no platform views in the current frame,
// then revert the image view surface and use the previous surface.
//
// Otherwise, acquire the latest image.
if (flutterViewConvertedToImageView && currentFrameUsedPlatformViewIds.isEmpty()) {
flutterViewConvertedToImageView = false;
flutterView.revertImageView(
() -> {
// Destroy overlay surfaces once the surface reversion is completed.
finishFrame(false);
});
return;
}
// Whether the current frame was rendered using ImageReaders.
//
// Since the image readers may not have images available at this point,
// this becomes true if all the required surfaces have images available.
//
// This is used to decide if the platform views can be rendered in the current frame.
// If one of the surfaces doesn't have an image, the frame may be incomplete and must be
// dropped.
// For example, a toolbar widget painted by Flutter may not be rendered.
final boolean isFrameRenderedUsingImageReaders =
flutterViewConvertedToImageView && flutterView.acquireLatestImageViewFrame();
finishFrame(isFrameRenderedUsingImageReaders);
}
private void finishFrame(boolean isFrameRenderedUsingImageReaders) {
for (int i = 0; i < overlayLayerViews.size(); i++) {
final int overlayId = overlayLayerViews.keyAt(i);
final PlatformOverlayView overlayView = overlayLayerViews.valueAt(i);
if (currentFrameUsedOverlayLayerIds.contains(overlayId)) {
flutterView.attachOverlaySurfaceToRender(overlayView);
final boolean didAcquireOverlaySurfaceImage = overlayView.acquireLatestImage();
isFrameRenderedUsingImageReaders &= didAcquireOverlaySurfaceImage;
} else {
// If the background surface isn't rendered by the image view, then the
// overlay surfaces can be detached from the rendered.
// This releases resources used by the ImageReader.
if (!flutterViewConvertedToImageView) {
overlayView.detachFromRenderer();
}
// Hide overlay surfaces that aren't rendered in the current frame.
overlayView.setVisibility(View.GONE);
flutterView.removeView(overlayView);
}
}
for (int i = 0; i < platformViewParent.size(); i++) {
final int viewId = platformViewParent.keyAt(i);
final View parentView = platformViewParent.get(viewId);
// This should only show platform views that are rendered in this frame and either:
// 1. Surface has images available in this frame or,
// 2. Surface does not have images available in this frame because the render surface should
// not be an ImageView.
//
// The platform view is appended to a mutator view.
//
// Otherwise, hide the platform view, but don't remove it from the view hierarchy yet as
// they are removed when the framework disposes the platform view widget.
if (currentFrameUsedPlatformViewIds.contains(viewId)
&& (isFrameRenderedUsingImageReaders || !synchronizeToNativeViewHierarchy)) {
parentView.setVisibility(View.VISIBLE);
} else {
parentView.setVisibility(View.GONE);
}
}
}
/**
* Creates and tracks the overlay surface.
*
* @param imageView The surface that displays the overlay.
* @return Wrapper object that provides the layer id and the surface. This member is not intended
* for public use, and is only visible for testing.
*/
@VisibleForTesting
@TargetApi(19)
@NonNull
public FlutterOverlaySurface createOverlaySurface(@NonNull PlatformOverlayView imageView) {
final int id = nextOverlayLayerId++;
overlayLayerViews.put(id, imageView);
return new FlutterOverlaySurface(id, imageView.getSurface());
}
/**
* Creates an overlay surface while the Flutter view is rendered by {@code PlatformOverlayView}.
*
* <p>This method is invoked by {@code FlutterJNI} only.
*
* <p>This member is not intended for public use, and is only visible for testing.
*/
@TargetApi(19)
@NonNull
public FlutterOverlaySurface createOverlaySurface() {
// Overlay surfaces have the same size as the background surface.
//
// This allows to reuse these surfaces in consecutive frames even
// if the drawings they contain have a different tight bound.
//
// The final view size is determined when its frame is set.
return createOverlaySurface(
new PlatformOverlayView(
flutterView.getContext(),
flutterView.getWidth(),
flutterView.getHeight(),
accessibilityEventsDelegate));
}
/**
* Destroys the overlay surfaces and removes them from the view hierarchy.
*
* <p>This method is used only internally by {@code FlutterJNI}.
*/
public void destroyOverlaySurfaces() {
for (int viewId = 0; viewId < overlayLayerViews.size(); viewId++) {
final PlatformOverlayView overlayView = overlayLayerViews.valueAt(viewId);
overlayView.detachFromRenderer();
overlayView.closeImageReader();
// Don't remove overlayView from the view hierarchy since this method can
// be called while the Android framework is iterating over the array of views.
// See ViewGroup#dispatchDetachedFromWindow(), and
// https://github.com/flutter/flutter/issues/97679.
}
}
private void removeOverlaySurfaces() {
if (flutterView == null) {
Log.e(TAG, "removeOverlaySurfaces called while flutter view is null");
return;
}
for (int viewId = 0; viewId < overlayLayerViews.size(); viewId++) {
flutterView.removeView(overlayLayerViews.valueAt(viewId));
}
overlayLayerViews.clear();
}
@VisibleForTesting
public SparseArray<PlatformOverlayView> getOverlayLayerViews() {
return overlayLayerViews;
}
}