Handle SurfaceView in a VirtualDisplay (flutter/engine#33599)

This commit is contained in:
Emmanuel Garcia
2022-06-22 19:39:05 -07:00
committed by GitHub
parent 0586f7b588
commit 135a1c8e56
39 changed files with 2309 additions and 255 deletions

View File

@@ -1478,6 +1478,8 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/Platfor
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java
FILE: ../../../flutter/shell/platform/android/io/flutter/util/PathUtils.java
FILE: ../../../flutter/shell/platform/android/io/flutter/util/Preconditions.java
FILE: ../../../flutter/shell/platform/android/io/flutter/util/Predicate.java

View File

@@ -286,6 +286,8 @@ android_java_sources = [
"io/flutter/plugin/platform/PlatformViewWrapper.java",
"io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java",
"io/flutter/plugin/platform/PlatformViewsController.java",
"io/flutter/plugin/platform/SingleViewPresentation.java",
"io/flutter/plugin/platform/VirtualDisplayController.java",
"io/flutter/util/PathUtils.java",
"io/flutter/util/Preconditions.java",
"io/flutter/util/Predicate.java",

View File

@@ -873,6 +873,21 @@ public class FlutterView extends FrameLayout
return textInputPlugin.createInputConnection(this, keyboardManager, outAttrs);
}
/**
* Allows a {@code View} that is not currently the input connection target to invoke commands on
* the {@link android.view.inputmethod.InputMethodManager}, which is otherwise disallowed.
*
* <p>Returns true to allow non-input-connection-targets to invoke methods on {@code
* InputMethodManager}, or false to exclusively allow the input connection target to invoke such
* methods.
*/
@Override
public boolean checkInputConnectionProxy(View view) {
return flutterEngine != null
? flutterEngine.getPlatformViewsController().checkInputConnectionProxy(view)
: super.checkInputConnectionProxy(view);
}
/**
* Invoked when a hardware key is pressed or released.
*

View File

@@ -147,15 +147,18 @@ public class PlatformViewsChannel {
(double) resizeArgs.get("width"),
(double) resizeArgs.get("height"));
try {
final PlatformViewBufferSize sz = handler.resize(resizeRequest);
if (sz == null) {
result.error("error", "Failed to resize the platform view", null);
} else {
final Map<String, Object> response = new HashMap<>();
response.put("width", (double) sz.width);
response.put("height", (double) sz.height);
result.success(response);
}
handler.resize(
resizeRequest,
(PlatformViewBufferSize bufferSize) -> {
if (bufferSize == null) {
result.error("error", "Failed to resize the platform view", null);
} else {
final Map<String, Object> response = new HashMap<>();
response.put("width", (double) bufferSize.width);
response.put("height", (double) bufferSize.height);
result.success(response);
}
});
} catch (IllegalStateException exception) {
result.error("error", detailedExceptionString(exception), null);
}
@@ -298,9 +301,11 @@ public class PlatformViewsChannel {
* The Flutter application would like to resize an existing Android {@code View}.
*
* @param request The request to resize the platform view.
* @return The buffer size where the platform view pixels are written to.
* @param onComplete Once the resize is completed, this is the handler to notify the size of the
* platform view buffer.
*/
PlatformViewBufferSize resize(@NonNull PlatformViewResizeRequest request);
void resize(
@NonNull PlatformViewResizeRequest request, @NonNull PlatformViewBufferResized onComplete);
/**
* The Flutter application would like to change the offset of an existing Android {@code View}.
@@ -418,6 +423,11 @@ public class PlatformViewsChannel {
}
}
/** Allows to notify when a platform view buffer has been resized. */
public interface PlatformViewBufferResized {
void run(@Nullable PlatformViewBufferSize bufferSize);
}
/** The state of a touch event in Flutter within a platform view. */
public static class PlatformViewTouch {
/** The ID of the platform view as seen by the Flutter side. */

View File

@@ -89,7 +89,9 @@ public class TextInputChannel {
try {
final JSONObject arguments = (JSONObject) args;
final int platformViewId = arguments.getInt("platformViewId");
textInputMethodHandler.setPlatformViewClient(platformViewId);
final boolean usesVirtualDisplay =
arguments.optBoolean("usesVirtualDisplay", false);
textInputMethodHandler.setPlatformViewClient(platformViewId, usesVirtualDisplay);
result.success(null);
} catch (JSONException exception) {
result.error("error", exception.getMessage(), null);
@@ -401,8 +403,10 @@ public class TextInputChannel {
* different client is set.
*
* @param id the ID of the platform view to be set as a text input client.
* @param usesVirtualDisplay True if the platform view uses a virtual display, false if it uses
* hybrid composition.
*/
void setPlatformViewClient(int id);
void setPlatformViewClient(int id, boolean usesVirtualDisplay);
/**
* Sets the size and the transform matrix of the current text input client.

View File

@@ -54,6 +54,12 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
// Initialize the "last seen" text editing values to a non-null value.
private TextEditState mLastKnownFrameworkTextEditingState;
// When true following calls to createInputConnection will return the cached lastInputConnection
// if the input
// target is a platform view. See the comments on lockPlatformViewInputConnection for more
// details.
private boolean isInputConnectionLocked;
@SuppressLint("NewApi")
public TextInputPlugin(
@NonNull View view,
@@ -99,7 +105,7 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
@Override
public void hide() {
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
if (inputTarget.type == InputTarget.Type.PHYSICAL_DISPLAY_PLATFORM_VIEW) {
notifyViewExited();
} else {
hideTextInput(mView);
@@ -130,8 +136,8 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
}
@Override
public void setPlatformViewClient(int platformViewId) {
setPlatformViewTextInputClient(platformViewId);
public void setPlatformViewClient(int platformViewId, boolean usesVirtualDisplay) {
setPlatformViewTextInputClient(platformViewId, usesVirtualDisplay);
}
@Override
@@ -176,6 +182,36 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
return imeSyncCallback;
}
/**
* Use the current platform view input connection until unlockPlatformViewInputConnection is
* called.
*
* <p>The current input connection instance is cached and any following call to @{link
* createInputConnection} returns the cached connection until unlockPlatformViewInputConnection is
* called.
*
* <p>This is a no-op if the current input target isn't a platform view.
*
* <p>This is used to preserve an input connection when moving a platform view from one virtual
* display to another.
*/
public void lockPlatformViewInputConnection() {
if (inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW) {
isInputConnectionLocked = true;
}
}
/**
* Unlocks the input connection.
*
* <p>See also: @{link lockPlatformViewInputConnection}.
*/
public void unlockPlatformViewInputConnection() {
if (inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW) {
isInputConnectionLocked = false;
}
}
/**
* Detaches the text input plugin from the platform views controller.
*
@@ -259,10 +295,21 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
return null;
}
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
if (inputTarget.type == InputTarget.Type.PHYSICAL_DISPLAY_PLATFORM_VIEW) {
return null;
}
if (inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW) {
if (isInputConnectionLocked) {
return lastInputConnection;
}
lastInputConnection =
platformViewsController
.getPlatformViewById(inputTarget.id)
.onCreateInputConnection(outAttrs);
return lastInputConnection;
}
outAttrs.inputType =
inputTypeFromTextInputType(
configuration.inputType,
@@ -317,7 +364,9 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
* input connection.
*/
public void clearPlatformViewClient(int platformViewId) {
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW && inputTarget.id == platformViewId) {
if ((inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW
|| inputTarget.type == InputTarget.Type.PHYSICAL_DISPLAY_PLATFORM_VIEW)
&& inputTarget.id == platformViewId) {
inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
notifyViewExited();
mImm.hideSoftInputFromWindow(mView.getApplicationWindowToken(), 0);
@@ -378,13 +427,26 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
// setTextInputClient will be followed by a call to setTextInputEditingState.
// Do a restartInput at that time.
mRestartInputPending = true;
unlockPlatformViewInputConnection();
lastClientRect = null;
mEditable.addEditingStateListener(this);
}
private void setPlatformViewTextInputClient(int platformViewId) {
inputTarget = new InputTarget(InputTarget.Type.PLATFORM_VIEW, platformViewId);
lastInputConnection = null;
private void setPlatformViewTextInputClient(int platformViewId, boolean usesVirtualDisplay) {
if (usesVirtualDisplay) {
// We need to make sure that the Flutter view is focused so that no imm operations get short
// circuited.
// Not asking for focus here specifically manifested in a bug on API 28 devices where the
// platform view's request to show a keyboard was ignored.
mView.requestFocus();
inputTarget = new InputTarget(InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW, platformViewId);
mImm.restartInput(mView);
mRestartInputPending = false;
} else {
inputTarget =
new InputTarget(InputTarget.Type.PHYSICAL_DISPLAY_PLATFORM_VIEW, platformViewId);
lastInputConnection = null;
}
}
private static boolean composingChanged(
@@ -475,10 +537,28 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
@VisibleForTesting
void clearTextInputClient() {
if (inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW) {
// This only applies to platform views that use a virtual display.
// Focus changes in the framework tree have no guarantees on the order focus nodes are
// notified. A node that lost focus may be notified before or after a node that gained focus.
// When moving the focus from a Flutter text field to an AndroidView, it is possible that the
// Flutter text field's focus node will be notified that it lost focus after the AndroidView
// was notified that it gained focus. When this happens the text field will send a
// clearTextInput command which we ignore.
// By doing this we prevent the framework from clearing a platform view input client (the only
// way to do so is to set a new framework text client). I don't see an obvious use case for
// "clearing" a platform view's text input client, and it may be error prone as we don't know
// how the platform view manages the input connection and we probably shouldn't interfere.
// If we ever want to allow the framework to clear a platform view text client we should
// probably consider changing the focus manager such that focus nodes that lost focus are
// notified before focus nodes that gained focus as part of the same focus event.
return;
}
mEditable.removeEditingStateListener(this);
notifyViewExited();
updateAutofillConfigurationIfNeeded(null);
inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
unlockPlatformViewInputConnection();
lastClientRect = null;
}
@@ -488,9 +568,12 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
// InputConnection is managed by the TextInputPlugin, and events are forwarded to the Flutter
// framework.
FRAMEWORK_CLIENT,
// InputConnection is managed by a platform view that is embeded in the Android view
// hierarchy.
PLATFORM_VIEW,
// InputConnection is managed by a platform view that is presented on a virtual display.
VIRTUAL_DISPLAY_PLATFORM_VIEW,
// InputConnection is managed by a platform view that is embedded in the activity's view
// hierarchy. This view hierarchy is displayed in a physical display within the aplication
// display area.
PHYSICAL_DISPLAY_PLATFORM_VIEW,
}
public InputTarget(@NonNull Type type, int id) {

View File

@@ -66,11 +66,8 @@ public interface PlatformView {
*
* <p>This hook only exists for rare cases where the plugin relies on the state of the input
* connection. This probably doesn't need to be implemented.
*
* <p>This method is deprecated, and will be removed in a future release.
*/
@SuppressLint("NewApi")
@Deprecated
default void onInputConnectionLocked() {}
/**
@@ -78,10 +75,7 @@ public interface PlatformView {
*
* <p>This hook only exists for rare cases where the plugin relies on the state of the input
* connection. This probably doesn't need to be implemented.
*
* <p>This method is deprecated, and will be removed in a future release.
*/
@SuppressLint("NewApi")
@Deprecated
default void onInputConnectionUnlocked() {}
}

View File

@@ -28,7 +28,7 @@ public abstract class PlatformViewFactory {
* null, or no arguments were sent from the Flutter app.
*/
@NonNull
public abstract PlatformView create(@Nullable Context context, int viewId, @Nullable Object args);
public abstract PlatformView create(Context context, int viewId, @Nullable Object args);
/** Returns the codec to be used for decoding the args parameter of {@link #create}. */
@Nullable

View File

@@ -18,6 +18,9 @@ public interface PlatformViewsAccessibilityDelegate {
@Nullable
View getPlatformViewById(int viewId);
/** Returns true if the platform view uses virtual displays. */
boolean usesVirtualDisplay(int id);
/**
* Attaches an accessibility bridge for this platform views accessibility delegate.
*

View File

@@ -9,9 +9,11 @@ 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;
@@ -30,9 +32,11 @@ 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;
@@ -46,6 +50,19 @@ import java.util.List;
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;
@@ -68,6 +85,17 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
// 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;
@@ -123,21 +151,22 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
private final PlatformViewsChannel.PlatformViewsHandler channelHandler =
new PlatformViewsChannel.PlatformViewsHandler() {
@TargetApi(Build.VERSION_CODES.KITKAT)
@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(Build.VERSION_CODES.KITKAT);
ensureValidAndroidVersion(19);
final int viewId = request.viewId;
if (!validateDirection(request.direction)) {
throw new IllegalStateException(
"Trying to create a view with unknown direction value: "
+ request.direction
+ "(view id: "
+ request.viewId
+ viewId
+ ")");
}
@@ -152,12 +181,13 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
createParams = factory.getCreateArgsCodec().decodeMessage(request.params);
}
final PlatformView platformView = factory.create(context, request.viewId, createParams);
final PlatformView platformView = factory.create(context, viewId, createParams);
platformView.getView().setLayoutDirection(request.direction);
platformViews.put(request.viewId, platformView);
platformViews.put(viewId, platformView);
Log.i(TAG, "Using hybrid composition for platform view: " + viewId);
}
@TargetApi(Build.VERSION_CODES.M)
@TargetApi(20)
@Override
public long createForTextureLayer(
@NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
@@ -194,35 +224,12 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
createParams = viewFactory.getCreateArgsCodec().decodeMessage(request.params);
}
final PlatformView platformView = viewFactory.create(context, viewId, createParams);
// The virtual display controller will change the embedded view context.
final Context embeddedViewContext = new MutableContextWrapper(context);
final PlatformView platformView =
viewFactory.create(embeddedViewContext, viewId, createParams);
platformViews.put(viewId, platformView);
PlatformViewWrapper wrapperView;
long txId;
if (usesSoftwareRendering) {
wrapperView = new PlatformViewWrapper(context);
txId = -1;
} else {
final TextureRegistry.SurfaceTextureEntry textureEntry =
textureRegistry.createSurfaceTexture();
wrapperView = new PlatformViewWrapper(context, textureEntry);
txId = textureEntry.id();
}
wrapperView.setTouchProcessor(androidTouchProcessor);
final int physicalWidth = toPhysicalPixels(request.logicalWidth);
final int physicalHeight = toPhysicalPixels(request.logicalHeight);
wrapperView.setBufferSize(physicalWidth, physicalHeight);
final FrameLayout.LayoutParams layoutParams =
new FrameLayout.LayoutParams(physicalWidth, physicalHeight);
final int physicalTop = toPhysicalPixels(request.logicalTop);
final int physicalLeft = toPhysicalPixels(request.logicalLeft);
layoutParams.topMargin = physicalTop;
layoutParams.leftMargin = physicalLeft;
wrapperView.setLayoutParams(layoutParams);
final View embeddedView = platformView.getView();
if (embeddedView == null) {
throw new IllegalStateException(
@@ -231,9 +238,103 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
throw new IllegalStateException(
"The Android view returned from PlatformView#getView() was already added to a parent view.");
}
embeddedView.setLayoutParams(new FrameLayout.LayoutParams(physicalWidth, physicalHeight));
embeddedView.setLayoutDirection(request.direction);
final int physicalWidth = toPhysicalPixels(request.logicalWidth);
final int physicalHeight = toPhysicalPixels(request.logicalHeight);
// Case 1. Add the view to a virtual display if 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 virtual display is wired up to a GL texture that is composed by the Flutter engine.
// Also, use virtual display if the API level is 20, 21 or 22 since the Case 2. requires
// at least API level 23.
final boolean shouldUseVirtualDisplay =
ViewUtils.hasChildViewOfType(embeddedView, VIEW_TYPES_REQUIRE_VIRTUAL_DISPLAY)
|| Build.VERSION.SDK_INT < 23;
if (!usesSoftwareRendering && shouldUseVirtualDisplay) {
Log.i(TAG, "Hosting view in a virtual display for platform view: " + viewId);
// API level 20 is required to use VirtualDisplay#setSurface.
ensureValidAndroidVersion(20);
final TextureRegistry.SurfaceTextureEntry textureEntry =
textureRegistry.createSurfaceTexture();
final VirtualDisplayController vdController =
VirtualDisplayController.create(
context,
accessibilityEventsDelegate,
platformView,
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);
contextToEmbeddedView.put(embeddedView.getContext(), embeddedView);
return textureEntry.id();
}
// Case 2. Attach the view to the Android view hierarchy and record their 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: " + viewId);
PlatformViewWrapper viewWrapper;
long txId;
if (usesSoftwareRendering) {
viewWrapper = new PlatformViewWrapper(context);
txId = -1;
} else {
final TextureRegistry.SurfaceTextureEntry textureEntry =
textureRegistry.createSurfaceTexture();
viewWrapper = new PlatformViewWrapper(context, textureEntry);
txId = 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.
// This isn't needed when the virtual display is used because the virtual display itself
// is sized.
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.
@@ -244,8 +345,12 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
embeddedView.setImportantForAccessibility(
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
wrapperView.addView(embeddedView);
wrapperView.setOnDescendantFocusChangeListener(
// 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(viewId);
@@ -253,19 +358,37 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
textInputPlugin.clearPlatformViewClient(viewId);
}
});
flutterView.addView(wrapperView);
viewWrappers.append(viewId, wrapperView);
flutterView.addView(viewWrapper);
viewWrappers.append(viewId, viewWrapper);
return txId;
}
@Override
public void dispose(int viewId) {
final PlatformView platformView = platformViews.get(viewId);
if (platformView != null) {
platformViews.remove(viewId);
platformView.dispose();
if (platformView == null) {
Log.e(TAG, "Disposing unknown platform view with id: " + viewId);
return;
}
// The platform view is displayed using a TextureLayer.
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();
@@ -295,33 +418,65 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
@Override
public void offset(int viewId, double top, double left) {
final PlatformViewWrapper wrapper = viewWrappers.get(viewId);
if (wrapper == null) {
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) wrapper.getLayoutParams();
(FrameLayout.LayoutParams) viewWrapper.getLayoutParams();
layoutParams.topMargin = physicalTop;
layoutParams.leftMargin = physicalLeft;
wrapper.setLayoutParams(layoutParams);
viewWrapper.setLayoutParams(layoutParams);
}
@Override
public PlatformViewsChannel.PlatformViewBufferSize resize(
@NonNull PlatformViewsChannel.PlatformViewResizeRequest request) {
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;
final PlatformView platformView = platformViews.get(viewId);
final PlatformViewWrapper view = viewWrappers.get(viewId);
if (platformView == null || view == null) {
Log.e(TAG, "Resizing unknown platform view with id: " + viewId);
return null;
}
final int newWidth = toPhysicalPixels(request.newLogicalWidth);
final int newHeight = toPhysicalPixels(request.newLogicalHeight);
if (usesVirtualDisplay(viewId)) {
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);
onComplete.run(
new PlatformViewsChannel.PlatformViewBufferSize(
toLogicalPixels(vdController.getBufferWidth()),
toLogicalPixels(vdController.getBufferHeight())));
});
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
@@ -330,46 +485,56 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
// 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 (newWidth > view.getBufferWidth() || newHeight > view.getBufferHeight()) {
view.setBufferSize(newWidth, newHeight);
if (physicalWidth > viewWrapper.getBufferWidth()
|| physicalHeight > viewWrapper.getBufferHeight()) {
viewWrapper.setBufferSize(physicalWidth, physicalHeight);
}
final ViewGroup.LayoutParams viewWrapperLayoutParams = view.getLayoutParams();
viewWrapperLayoutParams.width = newWidth;
viewWrapperLayoutParams.height = newHeight;
view.setLayoutParams(viewWrapperLayoutParams);
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 = newWidth;
embeddedViewLayoutParams.height = newHeight;
embeddedViewLayoutParams.width = physicalWidth;
embeddedViewLayoutParams.height = physicalHeight;
embeddedView.setLayoutParams(embeddedViewLayoutParams);
}
return new PlatformViewsChannel.PlatformViewBufferSize(
toLogicalPixels(view.getBufferWidth()), toLogicalPixels(view.getBufferHeight()));
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;
}
ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
final float density = context.getResources().getDisplayMetrics().density;
final MotionEvent event = toMotionEvent(density, touch);
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(Build.VERSION_CODES.JELLY_BEAN_MR1)
@TargetApi(17)
@Override
public void setDirection(int viewId, int direction) {
if (!validateDirection(direction)) {
@@ -380,33 +545,47 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
+ viewId
+ ")");
}
final PlatformView platformView = platformViews.get(viewId);
if (platformView == null) {
Log.e(TAG, "Setting direction to an unknown view with id: " + viewId);
return;
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();
}
ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
final View view = platformView.getView();
if (view == null) {
if (embeddedView == null) {
Log.e(TAG, "Setting direction to a null view with id: " + viewId);
return;
}
view.setLayoutDirection(direction);
embeddedView.setLayoutDirection(direction);
}
@Override
public void clearFocus(int viewId) {
final PlatformView platformView = platformViews.get(viewId);
if (platformView == null) {
Log.e(TAG, "Clearing focus on an unknown view with id: " + viewId);
return;
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();
}
final View view = platformView.getView();
if (view == null) {
if (embeddedView == null) {
Log.e(TAG, "Clearing focus on a null view with id: " + viewId);
return;
}
view.clearFocus();
embeddedView.clearFocus();
}
private void ensureValidAndroidVersion(int minSdkVersion) {
@@ -426,7 +605,8 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
};
@VisibleForTesting
public MotionEvent toMotionEvent(float density, PlatformViewsChannel.PlatformViewTouch touch) {
public MotionEvent toMotionEvent(
float density, PlatformViewsChannel.PlatformViewTouch touch, boolean usingVirtualDiplay) {
MotionEventTracker.MotionEventId motionEventId =
MotionEventTracker.MotionEventId.from(touch.motionEventId);
MotionEvent trackedEvent = motionEventTracker.pop(motionEventId);
@@ -442,7 +622,7 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
parsePointerCoordsList(touch.rawPointerCoords, density)
.toArray(new PointerCoords[touch.pointerCount]);
if (trackedEvent != null) {
if (!usingVirtualDiplay && trackedEvent != null) {
return MotionEvent.obtain(
trackedEvent.getDownTime(),
trackedEvent.getEventTime(),
@@ -481,7 +661,9 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
public PlatformViewsController() {
registry = new PlatformViewRegistryImpl();
vdControllers = new HashMap<>();
accessibilityEventsDelegate = new AccessibilityEventsDelegate();
contextToEmbeddedView = new HashMap<>();
overlayLayerViews = new SparseArray<>();
currentFrameUsedOverlayLayerIds = new HashSet<>();
currentFrameUsedPlatformViewIds = new HashSet<>();
@@ -594,6 +776,7 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
final FlutterMutatorView view = platformViewParent.valueAt(index);
flutterView.removeView(view);
}
destroyOverlaySurfaces();
removeOverlaySurfaces();
flutterView = null;
@@ -634,6 +817,29 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
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;
}
@@ -651,16 +857,21 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
* PlatformViewsController} detaches from JNI.
*/
public void onDetachedFromJNI() {
flushAllViews();
diposeAllViews();
}
public void onPreEngineRestart() {
flushAllViews();
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;
@@ -668,6 +879,27 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
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;
}
@@ -729,9 +961,11 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
return (int) Math.round(physicalPixels / getDisplayDensity());
}
private void flushAllViews() {
private void diposeAllViews() {
while (platformViews.size() > 0) {
channelHandler.dispose(platformViews.keyAt(0));
final int viewId = platformViews.keyAt(0);
// Dispose deletes the entry from platformViews and clears associated resources.
channelHandler.dispose(viewId);
}
}

View File

@@ -0,0 +1,482 @@
// 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.content.Context.WINDOW_SERVICE;
import static android.view.View.OnFocusChangeListener;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.app.Presentation;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.MutableContextWrapper;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.Bundle;
import android.view.Display;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.inputmethod.InputMethodManager;
import android.widget.FrameLayout;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.Log;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/*
* A presentation used for hosting a single Android view in a virtual display.
*
* This presentation overrides the WindowManager's addView/removeView/updateViewLayout methods, such that views added
* directly to the WindowManager are added as part of the presentation's view hierarchy (to fakeWindowViewGroup).
*
* The view hierarchy for the presentation is as following:
*
* rootView
* / \
* / \
* / \
* container state.fakeWindowViewGroup
* |
* EmbeddedView
*/
@Keep
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
class SingleViewPresentation extends Presentation {
/*
* When an embedded view is resized in Flutterverse we move the Android view to a new virtual display
* that has the new size. This class keeps the presentation state that moves with the view to the presentation of
* the new virtual display.
*/
static class PresentationState {
// The Android view we are embedding in the Flutter app.
private PlatformView platformView;
// The InvocationHandler for a WindowManager proxy. This is essentially the custom window
// manager for the
// presentation.
private WindowManagerHandler windowManagerHandler;
// Contains views that were added directly to the window manager (e.g
// android.widget.PopupWindow).
private FakeWindowViewGroup fakeWindowViewGroup;
}
// A reference to the current accessibility bridge to which accessibility events will be
// delegated.
private final AccessibilityEventsDelegate accessibilityEventsDelegate;
private final OnFocusChangeListener focusChangeListener;
// This is the view id assigned by the Flutter framework to the embedded view, we keep it here
// so when we create the platform view we can tell it its view id.
private int viewId;
// This is the creation parameters for the platform view, we keep it here
// so when we create the platform view we can tell it its view id.
private Object createParams;
// The root view for the presentation, it has 2 childs: container which contains the embedded
// view, and
// fakeWindowViewGroup which contains views that were added directly to the presentation's window
// manager.
private AccessibilityDelegatingFrameLayout rootView;
// Contains the embedded platform view (platformView.getView()) when it is attached to the
// presentation.
private FrameLayout container;
private final PresentationState state;
private boolean startFocused = false;
// The context for the application window that hosts FlutterView.
private final Context outerContext;
/**
* Creates a presentation that will use the view factory to create a new platform view in the
* presentation's onCreate, and attach it.
*/
public SingleViewPresentation(
Context outerContext,
Display display,
PlatformView view,
AccessibilityEventsDelegate accessibilityEventsDelegate,
int viewId,
Object createParams,
OnFocusChangeListener focusChangeListener) {
super(new ImmContext(outerContext), display);
this.accessibilityEventsDelegate = accessibilityEventsDelegate;
this.viewId = viewId;
this.createParams = createParams;
this.focusChangeListener = focusChangeListener;
this.outerContext = outerContext;
state = new PresentationState();
state.platformView = view;
getWindow()
.setFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
getWindow().setType(WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION);
}
}
/**
* Creates a presentation that will attach an already existing view as its root view.
*
* <p>The display's density must match the density of the context used when the view was created.
*/
public SingleViewPresentation(
Context outerContext,
Display display,
AccessibilityEventsDelegate accessibilityEventsDelegate,
PresentationState state,
OnFocusChangeListener focusChangeListener,
boolean startFocused) {
super(new ImmContext(outerContext), display);
this.accessibilityEventsDelegate = accessibilityEventsDelegate;
this.state = state;
this.focusChangeListener = focusChangeListener;
this.outerContext = outerContext;
getWindow()
.setFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
this.startFocused = startFocused;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// This makes sure we preserve alpha for the VD's content.
getWindow().setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT));
if (state.fakeWindowViewGroup == null) {
state.fakeWindowViewGroup = new FakeWindowViewGroup(getContext());
}
if (state.windowManagerHandler == null) {
WindowManager windowManagerDelegate =
(WindowManager) getContext().getSystemService(WINDOW_SERVICE);
state.windowManagerHandler =
new WindowManagerHandler(windowManagerDelegate, state.fakeWindowViewGroup);
}
container = new FrameLayout(getContext());
// Our base mContext has already been wrapped with an IMM cache at instantiation time, but
// we want to wrap it again here to also return state.windowManagerHandler.
Context baseContext =
new PresentationContext(getContext(), state.windowManagerHandler, outerContext);
View embeddedView = state.platformView.getView();
if (embeddedView.getContext() instanceof MutableContextWrapper) {
MutableContextWrapper currentContext = (MutableContextWrapper) embeddedView.getContext();
currentContext.setBaseContext(baseContext);
} else {
throw new IllegalStateException(
"Unexpected platform view context. "
+ "When constructing a platform view in the factory, use the context from PlatformViewFactory#create, view id: "
+ viewId);
}
container.addView(embeddedView);
rootView =
new AccessibilityDelegatingFrameLayout(
getContext(), accessibilityEventsDelegate, embeddedView);
rootView.addView(container);
rootView.addView(state.fakeWindowViewGroup);
embeddedView.setOnFocusChangeListener(focusChangeListener);
rootView.setFocusableInTouchMode(true);
if (startFocused) {
embeddedView.requestFocus();
} else {
rootView.requestFocus();
}
setContentView(rootView);
}
public PresentationState detachState() {
container.removeAllViews();
rootView.removeAllViews();
return state;
}
public PlatformView getView() {
if (state.platformView == null) return null;
return state.platformView;
}
/*
* A view group that implements the same layout protocol that exist between the WindowManager and its direct
* children.
*
* Currently only a subset of the protocol is supported (gravity, x, and y).
*/
static class FakeWindowViewGroup extends ViewGroup {
// Used in onLayout to keep the bounds of the current view.
// We keep it as a member to avoid object allocations during onLayout which are discouraged.
private final Rect viewBounds;
// Used in onLayout to keep the bounds of the child views.
// We keep it as a member to avoid object allocations during onLayout which are discouraged.
private final Rect childRect;
public FakeWindowViewGroup(Context context) {
super(context);
viewBounds = new Rect();
childRect = new Rect();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
WindowManager.LayoutParams params = (WindowManager.LayoutParams) child.getLayoutParams();
viewBounds.set(l, t, r, b);
Gravity.apply(
params.gravity,
child.getMeasuredWidth(),
child.getMeasuredHeight(),
viewBounds,
params.x,
params.y,
childRect);
child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.measure(atMost(widthMeasureSpec), atMost(heightMeasureSpec));
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
private static int atMost(int measureSpec) {
return MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(measureSpec), MeasureSpec.AT_MOST);
}
}
/** Answers calls for {@link InputMethodManager} with an instance cached at creation time. */
// TODO(mklim): This caches the IMM at construction time and won't pick up any changes. In rare
// cases where the FlutterView changes windows this will return an outdated instance. This
// should be fixed to instead defer returning the IMM to something that know's FlutterView's
// true Context.
private static class ImmContext extends ContextWrapper {
private @NonNull final InputMethodManager inputMethodManager;
ImmContext(Context base) {
this(base, /*inputMethodManager=*/ null);
}
private ImmContext(Context base, @Nullable InputMethodManager inputMethodManager) {
super(base);
this.inputMethodManager =
inputMethodManager != null
? inputMethodManager
: (InputMethodManager) base.getSystemService(INPUT_METHOD_SERVICE);
}
@Override
public Object getSystemService(String name) {
if (INPUT_METHOD_SERVICE.equals(name)) {
return inputMethodManager;
}
return super.getSystemService(name);
}
@Override
public Context createDisplayContext(Display display) {
Context displayContext = super.createDisplayContext(display);
return new ImmContext(displayContext, inputMethodManager);
}
}
/** Proxies a Context replacing the WindowManager with our custom instance. */
// TODO(mklim): This caches the IMM at construction time and won't pick up any changes. In rare
// cases where the FlutterView changes windows this will return an outdated instance. This
// should be fixed to instead defer returning the IMM to something that know's FlutterView's
// true Context.
private static class PresentationContext extends ContextWrapper {
private @NonNull final WindowManagerHandler windowManagerHandler;
private @Nullable WindowManager windowManager;
private final Context flutterAppWindowContext;
PresentationContext(
Context base,
@NonNull WindowManagerHandler windowManagerHandler,
Context flutterAppWindowContext) {
super(base);
this.windowManagerHandler = windowManagerHandler;
this.flutterAppWindowContext = flutterAppWindowContext;
}
@Override
public Object getSystemService(String name) {
if (WINDOW_SERVICE.equals(name)) {
if (isCalledFromAlertDialog()) {
// Alert dialogs are showing on top of the entire application and should not be limited to
// the virtual
// display. If we detect that an android.app.AlertDialog constructor is what's fetching
// the window manager
// we return the one for the application's window.
//
// Note that if we don't do this AlertDialog will throw a ClassCastException as down the
// line it tries
// to case this instance to a WindowManagerImpl which the object returned by
// getWindowManager is not
// a subclass of.
return flutterAppWindowContext.getSystemService(name);
}
return getWindowManager();
}
return super.getSystemService(name);
}
private WindowManager getWindowManager() {
if (windowManager == null) {
windowManager = windowManagerHandler.getWindowManager();
}
return windowManager;
}
private boolean isCalledFromAlertDialog() {
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
for (int i = 0; i < stackTraceElements.length && i < 11; i++) {
if (stackTraceElements[i].getClassName().equals(AlertDialog.class.getCanonicalName())
&& stackTraceElements[i].getMethodName().equals("<init>")) {
return true;
}
}
return false;
}
}
/*
* A dynamic proxy handler for a WindowManager with custom overrides.
*
* The presentation's window manager delegates all calls to the default window manager.
* WindowManager#addView calls triggered by views that are attached to the virtual display are crashing
* (see: https://github.com/flutter/flutter/issues/20714). This was triggered when selecting text in an embedded
* WebView (as the selection handles are implemented as popup windows).
*
* This dynamic proxy overrides the addView, removeView, removeViewImmediate, and updateViewLayout methods
* to prevent these crashes.
*
* This will be more efficient as a static proxy that's not using reflection, but as the engine is currently
* not being built against the latest Android SDK we cannot override all relevant method.
* Tracking issue for upgrading the engine's Android sdk: https://github.com/flutter/flutter/issues/20717
*/
static class WindowManagerHandler implements InvocationHandler {
private static final String TAG = "PlatformViewsController";
private final WindowManager delegate;
FakeWindowViewGroup fakeWindowRootView;
WindowManagerHandler(WindowManager delegate, FakeWindowViewGroup fakeWindowViewGroup) {
this.delegate = delegate;
fakeWindowRootView = fakeWindowViewGroup;
}
public WindowManager getWindowManager() {
return (WindowManager)
Proxy.newProxyInstance(
WindowManager.class.getClassLoader(), new Class<?>[] {WindowManager.class}, this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
switch (method.getName()) {
case "addView":
addView(args);
return null;
case "removeView":
removeView(args);
return null;
case "removeViewImmediate":
removeViewImmediate(args);
return null;
case "updateViewLayout":
updateViewLayout(args);
return null;
}
try {
return method.invoke(delegate, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
}
private void addView(Object[] args) {
if (fakeWindowRootView == null) {
Log.w(TAG, "Embedded view called addView while detached from presentation");
return;
}
View view = (View) args[0];
WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) args[1];
fakeWindowRootView.addView(view, layoutParams);
}
private void removeView(Object[] args) {
if (fakeWindowRootView == null) {
Log.w(TAG, "Embedded view called removeView while detached from presentation");
return;
}
View view = (View) args[0];
fakeWindowRootView.removeView(view);
}
private void removeViewImmediate(Object[] args) {
if (fakeWindowRootView == null) {
Log.w(TAG, "Embedded view called removeViewImmediate while detached from presentation");
return;
}
View view = (View) args[0];
view.clearAnimation();
fakeWindowRootView.removeView(view);
}
private void updateViewLayout(Object[] args) {
if (fakeWindowRootView == null) {
Log.w(TAG, "Embedded view called updateViewLayout while detached from presentation");
return;
}
View view = (View) args[0];
WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) args[1];
fakeWindowRootView.updateViewLayout(view, layoutParams);
}
}
private static class AccessibilityDelegatingFrameLayout extends FrameLayout {
private final AccessibilityEventsDelegate accessibilityEventsDelegate;
private final View embeddedView;
public AccessibilityDelegatingFrameLayout(
Context context,
AccessibilityEventsDelegate accessibilityEventsDelegate,
View embeddedView) {
super(context);
this.accessibilityEventsDelegate = accessibilityEventsDelegate;
this.embeddedView = embeddedView;
}
@Override
public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) {
return accessibilityEventsDelegate.requestSendAccessibilityEvent(embeddedView, child, event);
}
}
}

View File

@@ -0,0 +1,305 @@
// 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.View.OnFocusChangeListener;
import android.annotation.TargetApi;
import android.content.Context;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.View;
import android.view.ViewTreeObserver;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import io.flutter.Log;
import io.flutter.view.TextureRegistry;
import java.util.Locale;
@TargetApi(20)
class VirtualDisplayController {
private static String TAG = "VirtualDisplayController";
public static VirtualDisplayController create(
Context context,
AccessibilityEventsDelegate accessibilityEventsDelegate,
PlatformView view,
TextureRegistry.SurfaceTextureEntry textureEntry,
int width,
int height,
int viewId,
Object createParams,
OnFocusChangeListener focusChangeListener) {
int selectedWidth = width;
int selectedHeight = height;
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
if (selectedWidth == 0 || selectedHeight == 0) {
return null;
}
// Prevent https://github.com/flutter/flutter/issues/2897.
if (selectedWidth > metrics.widthPixels || selectedHeight > metrics.heightPixels) {
float aspectRatio = (float) selectedWidth / (float) selectedHeight;
int maybeWidth = (int) (metrics.heightPixels * aspectRatio);
int maybeHeight = (int) (metrics.widthPixels / aspectRatio);
if (maybeHeight <= metrics.heightPixels) {
selectedWidth = metrics.widthPixels;
selectedHeight = maybeHeight;
} else if (maybeWidth <= metrics.widthPixels) {
selectedHeight = metrics.heightPixels;
selectedWidth = maybeWidth;
} else {
return null;
}
String message =
String.format(
Locale.US,
"Resizing virtual display of size: [%d, %d] to size [%d, %d] "
+ "since it's larger than the device display size [%d, %d].",
width,
height,
selectedWidth,
selectedHeight,
metrics.widthPixels,
metrics.heightPixels);
Log.w(TAG, message);
}
textureEntry.surfaceTexture().setDefaultBufferSize(selectedWidth, selectedHeight);
Surface surface = new Surface(textureEntry.surfaceTexture());
DisplayManager displayManager =
(DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
int densityDpi = context.getResources().getDisplayMetrics().densityDpi;
VirtualDisplay virtualDisplay =
displayManager.createVirtualDisplay(
"flutter-vd", selectedWidth, selectedHeight, densityDpi, surface, 0);
if (virtualDisplay == null) {
return null;
}
VirtualDisplayController controller =
new VirtualDisplayController(
context,
accessibilityEventsDelegate,
virtualDisplay,
view,
surface,
textureEntry,
focusChangeListener,
viewId,
createParams);
controller.bufferWidth = selectedWidth;
controller.bufferHeight = selectedHeight;
return controller;
}
@VisibleForTesting SingleViewPresentation presentation;
private final Context context;
private final AccessibilityEventsDelegate accessibilityEventsDelegate;
private final int densityDpi;
private final TextureRegistry.SurfaceTextureEntry textureEntry;
private final OnFocusChangeListener focusChangeListener;
private final Surface surface;
private VirtualDisplay virtualDisplay;
private int bufferWidth;
private int bufferHeight;
private VirtualDisplayController(
Context context,
AccessibilityEventsDelegate accessibilityEventsDelegate,
VirtualDisplay virtualDisplay,
PlatformView view,
Surface surface,
TextureRegistry.SurfaceTextureEntry textureEntry,
OnFocusChangeListener focusChangeListener,
int viewId,
Object createParams) {
this.context = context;
this.accessibilityEventsDelegate = accessibilityEventsDelegate;
this.textureEntry = textureEntry;
this.focusChangeListener = focusChangeListener;
this.surface = surface;
this.virtualDisplay = virtualDisplay;
densityDpi = context.getResources().getDisplayMetrics().densityDpi;
presentation =
new SingleViewPresentation(
context,
this.virtualDisplay.getDisplay(),
view,
accessibilityEventsDelegate,
viewId,
createParams,
focusChangeListener);
presentation.show();
}
public int getBufferWidth() {
return bufferWidth;
}
public int getBufferHeight() {
return bufferHeight;
}
public void resize(final int width, final int height, final Runnable onNewSizeFrameAvailable) {
boolean isFocused = getView().isFocused();
final SingleViewPresentation.PresentationState presentationState = presentation.detachState();
// We detach the surface to prevent it being destroyed when releasing the vd.
//
// setSurface is only available starting API 20. We could support API 19 by re-creating a new
// SurfaceTexture here. This will require refactoring the TextureRegistry to allow recycling
// texture
// entry IDs.
virtualDisplay.setSurface(null);
virtualDisplay.release();
bufferWidth = width;
bufferHeight = height;
textureEntry.surfaceTexture().setDefaultBufferSize(width, height);
DisplayManager displayManager =
(DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
virtualDisplay =
displayManager.createVirtualDisplay("flutter-vd", width, height, densityDpi, surface, 0);
final View embeddedView = getView();
// There's a bug in Android version older than O where view tree observer onDrawListeners don't
// get properly
// merged when attaching to window, as a workaround we register the on draw listener after the
// view is attached.
embeddedView.addOnAttachStateChangeListener(
new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
OneTimeOnDrawListener.schedule(
embeddedView,
new Runnable() {
@Override
public void run() {
// We need some delay here until the frame propagates through the vd surface to
// to the texture,
// 128ms was picked pretty arbitrarily based on trial and error.
// As long as we invoke the runnable after a new frame is available we avoid the
// scaling jank
// described in: https://github.com/flutter/flutter/issues/19572
// We should ideally run onNewSizeFrameAvailable ASAP to make the embedded view
// more responsive
// following a resize.
embeddedView.postDelayed(onNewSizeFrameAvailable, 128);
}
});
embeddedView.removeOnAttachStateChangeListener(this);
}
@Override
public void onViewDetachedFromWindow(View v) {}
});
// Create a new SingleViewPresentation and show() it before we cancel() the existing
// presentation. Calling show() and cancel() in this order fixes
// https://github.com/flutter/flutter/issues/26345 and maintains seamless transition
// of the contents of the presentation.
SingleViewPresentation newPresentation =
new SingleViewPresentation(
context,
virtualDisplay.getDisplay(),
accessibilityEventsDelegate,
presentationState,
focusChangeListener,
isFocused);
newPresentation.show();
presentation.cancel();
presentation = newPresentation;
}
public void dispose() {
// Fix rare crash on HuaWei device described in: https://github.com/flutter/engine/pull/9192
presentation.cancel();
presentation.detachState();
virtualDisplay.release();
textureEntry.release();
}
/** See {@link PlatformView#onFlutterViewAttached(View)} */
/*package*/ void onFlutterViewAttached(@NonNull View flutterView) {
if (presentation == null || presentation.getView() == null) {
return;
}
presentation.getView().onFlutterViewAttached(flutterView);
}
/** See {@link PlatformView#onFlutterViewDetached()} */
/*package*/ void onFlutterViewDetached() {
if (presentation == null || presentation.getView() == null) {
return;
}
presentation.getView().onFlutterViewDetached();
}
/*package*/ void onInputConnectionLocked() {
if (presentation == null || presentation.getView() == null) {
return;
}
presentation.getView().onInputConnectionLocked();
}
/*package*/ void onInputConnectionUnlocked() {
if (presentation == null || presentation.getView() == null) {
return;
}
presentation.getView().onInputConnectionUnlocked();
}
public View getView() {
if (presentation == null) return null;
PlatformView platformView = presentation.getView();
return platformView.getView();
}
/** Dispatches a motion event to the presentation for this controller. */
public void dispatchTouchEvent(MotionEvent event) {
if (presentation == null) return;
presentation.dispatchTouchEvent(event);
}
static class OneTimeOnDrawListener implements ViewTreeObserver.OnDrawListener {
static void schedule(View view, Runnable runnable) {
OneTimeOnDrawListener listener = new OneTimeOnDrawListener(view, runnable);
view.getViewTreeObserver().addOnDrawListener(listener);
}
final View mView;
Runnable mOnDrawRunnable;
OneTimeOnDrawListener(View view, Runnable onDrawRunnable) {
this.mView = view;
this.mOnDrawRunnable = onDrawRunnable;
}
@Override
public void onDraw() {
if (mOnDrawRunnable == null) {
return;
}
mOnDrawRunnable.run();
mOnDrawRunnable = null;
mView.post(
new Runnable() {
@Override
public void run() {
mView.getViewTreeObserver().removeOnDrawListener(OneTimeOnDrawListener.this);
}
});
}
}
}

View File

@@ -10,6 +10,7 @@ import android.content.ContextWrapper;
import android.os.Build;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public final class ViewUtils {
@@ -56,16 +57,58 @@ public final class ViewUtils {
* @return True if the current view or any descendant view has focus.
*/
public static boolean childHasFocus(@Nullable View root) {
return traverseHierarchy(root, (View view) -> view.hasFocus());
}
/**
* Returns true if the root or any child view is an instance of the given types.
*
* @param root The root view.
* @param viewTypes The types of views.
* @return true if any child view is an instance of any of the given types.
*/
public static boolean hasChildViewOfType(@Nullable View root, Class<? extends View>[] viewTypes) {
return traverseHierarchy(
root,
(View view) -> {
for (int i = 0; i < viewTypes.length; i++) {
final Class<? extends View> viewType = viewTypes[i];
if (viewType.isInstance(view)) {
return true;
}
}
return false;
});
}
/** Allows to visit a view. */
public interface ViewVisitor {
boolean run(@NonNull View view);
}
/**
* Traverses the view hierarchy in pre-order and runs the visitor for each child view including
* the root view.
*
* <p>If the visitor returns true, the traversal stops, and the method returns true.
*
* <p>If the visitor returns false, the traversal continues until all views are visited.
*
* @param root The root view.
* @param visitor The visitor.
* @return true if the visitor returned true for a given view.
*/
public static boolean traverseHierarchy(@Nullable View root, @NonNull ViewVisitor visitor) {
if (root == null) {
return false;
}
if (root.hasFocus()) {
if (visitor.run(root)) {
return true;
}
if (root instanceof ViewGroup) {
final ViewGroup viewGroup = (ViewGroup) root;
for (int idx = 0; idx < viewGroup.getChildCount(); idx++) {
if (childHasFocus(viewGroup.getChildAt(idx))) {
if (traverseHierarchy(viewGroup.getChildAt(idx), visitor)) {
return true;
}
}

View File

@@ -573,6 +573,26 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
return null;
}
// Generate accessibility node for platform views using a virtual display.
//
// In this case, register the accessibility node in the view embedder,
// so the accessibility tree can be mirrored as a subtree of the Flutter accessibility tree.
// This is in constrast to hybrid composition where the embedded view is in the view hiearchy,
// so it doesn't need to be mirrored.
//
// See the case down below for how hybrid composition is handled.
if (semanticsNode.platformViewId != -1) {
if (platformViewsAccessibilityDelegate.usesVirtualDisplay(semanticsNode.platformViewId)) {
View embeddedView =
platformViewsAccessibilityDelegate.getPlatformViewById(semanticsNode.platformViewId);
if (embeddedView == null) {
return null;
}
Rect bounds = semanticsNode.getGlobalRect();
return accessibilityViewEmbedder.getRootNode(embeddedView, semanticsNode.id, bounds);
}
}
AccessibilityNodeInfo result =
obtainAccessibilityNodeInfo(rootAccessibilityView, virtualViewId);
// Work around for https://github.com/flutter/flutter/issues/2101
@@ -885,12 +905,19 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
View embeddedView =
platformViewsAccessibilityDelegate.getPlatformViewById(child.platformViewId);
// Add the embedded view as a child of the current accessibility node if it's using
// hybrid composition.
result.addChild(embeddedView);
} else {
result.addChild(rootAccessibilityView, child.id);
// Add the embedded view as a child of the current accessibility node if it's not
// using a virtual display.
//
// In this case, the view is in the Activity's view hierarchy, so it doesn't need to be
// mirrored.
//
// See the case above for how virtual displays are handled.
if (!platformViewsAccessibilityDelegate.usesVirtualDisplay(child.platformViewId)) {
result.addChild(embeddedView);
continue;
}
}
result.addChild(rootAccessibilityView, child.id);
}
return result;
}
@@ -1522,7 +1549,8 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
if (semanticsNode.hadPreviousConfig) {
updated.add(semanticsNode);
}
if (semanticsNode.platformViewId != -1) {
if (semanticsNode.platformViewId != -1
&& !platformViewsAccessibilityDelegate.usesVirtualDisplay(semanticsNode.platformViewId)) {
View embeddedView =
platformViewsAccessibilityDelegate.getPlatformViewById(semanticsNode.platformViewId);
if (embeddedView != null) {

View File

@@ -26,6 +26,7 @@ import android.view.PointerIcon;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewStructure;
import android.view.WindowInsets;
@@ -421,6 +422,14 @@ public class FlutterView extends SurfaceView
return mTextInputPlugin.createInputConnection(this, mKeyboardManager, outAttrs);
}
@Override
public boolean checkInputConnectionProxy(View view) {
return mNativeView
.getPluginRegistry()
.getPlatformViewsController()
.checkInputConnectionProxy(view);
}
@Override
public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) {
super.onProvideAutofillVirtualStructure(structure, flags);

View File

@@ -8,6 +8,7 @@ import static org.mockito.Mockito.*;
import static org.robolectric.Shadows.shadowOf;
import android.content.Context;
import android.content.MutableContextWrapper;
import android.content.res.AssetManager;
import android.graphics.SurfaceTexture;
import android.util.SparseArray;
@@ -18,6 +19,7 @@ import android.view.SurfaceView;
import android.view.View;
import android.view.ViewParent;
import android.widget.FrameLayout;
import android.widget.FrameLayout.LayoutParams;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import io.flutter.embedding.android.FlutterImageView;
@@ -57,6 +59,119 @@ import org.robolectric.shadows.ShadowSurfaceView;
@RunWith(AndroidJUnit4.class)
public class PlatformViewsControllerTest {
@Ignore
@Test
public void itNotifiesVirtualDisplayControllersOfViewAttachmentAndDetachment() {
// Setup test structure.
FlutterView fakeFlutterView = new FlutterView(ApplicationProvider.getApplicationContext());
// Create fake VirtualDisplayControllers. This requires internal knowledge of
// PlatformViewsController. We know that all PlatformViewsController does is
// forward view attachment/detachment calls to it's VirtualDisplayControllers.
//
// TODO(mattcarroll): once PlatformViewsController is refactored into testable
// pieces, remove this test and avoid verifying private behavior.
VirtualDisplayController fakeVdController1 = mock(VirtualDisplayController.class);
VirtualDisplayController fakeVdController2 = mock(VirtualDisplayController.class);
// Create the PlatformViewsController that is under test.
PlatformViewsController platformViewsController = new PlatformViewsController();
// Manually inject fake VirtualDisplayControllers into the PlatformViewsController.
platformViewsController.vdControllers.put(0, fakeVdController1);
platformViewsController.vdControllers.put(1, fakeVdController1);
// Execute test & verify results.
// Attach PlatformViewsController to the fake Flutter View.
platformViewsController.attachToView(fakeFlutterView);
// Verify that all virtual display controllers were notified of View attachment.
verify(fakeVdController1, times(1)).onFlutterViewAttached(eq(fakeFlutterView));
verify(fakeVdController1, never()).onFlutterViewDetached();
verify(fakeVdController2, times(1)).onFlutterViewAttached(eq(fakeFlutterView));
verify(fakeVdController2, never()).onFlutterViewDetached();
// Detach PlatformViewsController from the fake Flutter View.
platformViewsController.detachFromView();
// Verify that all virtual display controllers were notified of the View detachment.
verify(fakeVdController1, times(1)).onFlutterViewAttached(eq(fakeFlutterView));
verify(fakeVdController1, times(1)).onFlutterViewDetached();
verify(fakeVdController2, times(1)).onFlutterViewAttached(eq(fakeFlutterView));
verify(fakeVdController2, times(1)).onFlutterViewDetached();
}
@Ignore
@Test
public void itCancelsOldPresentationOnResize() {
// Setup test structure.
// Create a fake View that represents the View that renders a Flutter UI.
View fakeFlutterView = new View(ApplicationProvider.getApplicationContext());
// Create fake VirtualDisplayControllers. This requires internal knowledge of
// PlatformViewsController. We know that all PlatformViewsController does is
// forward view attachment/detachment calls to it's VirtualDisplayControllers.
//
// TODO(mattcarroll): once PlatformViewsController is refactored into testable
// pieces, remove this test and avoid verifying private behavior.
VirtualDisplayController fakeVdController1 = mock(VirtualDisplayController.class);
SingleViewPresentation presentation = fakeVdController1.presentation;
fakeVdController1.resize(10, 10, null);
assertEquals(fakeVdController1.presentation != presentation, true);
assertEquals(presentation.isShowing(), false);
}
@Test
public void itUsesActionEventTypeFromFrameworkEventForVirtualDisplays() {
MotionEventTracker motionEventTracker = MotionEventTracker.getInstance();
PlatformViewsController platformViewsController = new PlatformViewsController();
MotionEvent original =
MotionEvent.obtain(
100, // downTime
100, // eventTime
1, // action
0, // x
0, // y
0 // metaState
);
// track an event that will later get passed to us from framework
MotionEventTracker.MotionEventId motionEventId = motionEventTracker.track(original);
PlatformViewTouch frameWorkTouch =
new PlatformViewTouch(
0, // viewId
original.getDownTime(),
original.getEventTime(),
2, // action
1, // pointerCount
Arrays.asList(Arrays.asList(0, 0)), // pointer properties
Arrays.asList(Arrays.asList(0., 1., 2., 3., 4., 5., 6., 7., 8.)), // pointer coords
original.getMetaState(),
original.getButtonState(),
original.getXPrecision(),
original.getYPrecision(),
original.getDeviceId(),
original.getEdgeFlags(),
original.getSource(),
original.getFlags(),
motionEventId.getId());
MotionEvent resolvedEvent =
platformViewsController.toMotionEvent(
1, // density
frameWorkTouch,
true // usingVirtualDisplays
);
assertEquals(resolvedEvent.getAction(), frameWorkTouch.action);
assertNotEquals(resolvedEvent.getAction(), original.getAction());
}
@Ignore
@Test
public void itUsesActionEventTypeFromMotionEventForHybridPlatformViews() {
@@ -96,7 +211,8 @@ public class PlatformViewsControllerTest {
motionEventId.getId());
MotionEvent resolvedEvent =
platformViewsController.toMotionEvent(/*density=*/ 1, frameWorkTouch);
platformViewsController.toMotionEvent(
/*density=*/ 1, frameWorkTouch, /*usingVirtualDisplay=*/ false);
assertNotEquals(resolvedEvent.getAction(), frameWorkTouch.action);
assertEquals(resolvedEvent.getAction(), original.getAction());
@@ -295,6 +411,68 @@ public class PlatformViewsControllerTest {
});
}
@Test
@Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class})
public void onDetachedFromJNI_clearsPlatformViewContext() {
PlatformViewsController platformViewsController = new PlatformViewsController();
int platformViewId = 0;
assertNull(platformViewsController.getPlatformViewById(platformViewId));
PlatformViewFactory viewFactory = mock(PlatformViewFactory.class);
PlatformView platformView = mock(PlatformView.class);
SurfaceView pv = mock(SurfaceView.class);
when(pv.getContext()).thenReturn(mock(MutableContextWrapper.class));
when(pv.getLayoutParams()).thenReturn(new LayoutParams(1, 1));
when(platformView.getView()).thenReturn(pv);
when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView);
platformViewsController.getRegistry().registerViewFactory("testType", viewFactory);
FlutterJNI jni = new FlutterJNI();
attach(jni, platformViewsController);
// Simulate create call from the framework.
createPlatformView(
jni, platformViewsController, platformViewId, "testType", /* hybrid=*/ false);
assertFalse(platformViewsController.contextToEmbeddedView.isEmpty());
platformViewsController.onDetachedFromJNI();
assertTrue(platformViewsController.contextToEmbeddedView.isEmpty());
}
@Test
@Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class})
public void onPreEngineRestart_clearsPlatformViewContext() {
PlatformViewsController platformViewsController = new PlatformViewsController();
int platformViewId = 0;
assertNull(platformViewsController.getPlatformViewById(platformViewId));
PlatformViewFactory viewFactory = mock(PlatformViewFactory.class);
PlatformView platformView = mock(PlatformView.class);
SurfaceView pv = mock(SurfaceView.class);
when(pv.getContext()).thenReturn(mock(MutableContextWrapper.class));
when(pv.getLayoutParams()).thenReturn(new LayoutParams(1, 1));
when(platformView.getView()).thenReturn(pv);
when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView);
platformViewsController.getRegistry().registerViewFactory("testType", viewFactory);
FlutterJNI jni = new FlutterJNI();
attach(jni, platformViewsController);
// Simulate create call from the framework.
createPlatformView(
jni, platformViewsController, platformViewId, "testType", /* hybrid=*/ false);
assertFalse(platformViewsController.contextToEmbeddedView.isEmpty());
platformViewsController.onDetachedFromJNI();
assertTrue(platformViewsController.contextToEmbeddedView.isEmpty());
}
@Test
@Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class})
public void createPlatformViewMessage__throwsIfViewHasParent() {
@@ -810,6 +988,13 @@ public class PlatformViewsControllerTest {
verify(flutterView, never()).removeView(overlayImageView);
}
@Test
public void checkInputConnectionProxy__falseIfViewIsNull() {
final PlatformViewsController platformViewsController = new PlatformViewsController();
boolean shouldProxying = platformViewsController.checkInputConnectionProxy(null);
assertFalse(shouldProxying);
}
@Test
@Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class})
public void convertPlatformViewRenderSurfaceAsDefault() {

View File

@@ -0,0 +1,82 @@
package io.flutter.plugin.platform;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.P;
import static android.os.Build.VERSION_CODES.R;
import static junit.framework.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import android.annotation.TargetApi;
import android.content.Context;
import android.hardware.display.DisplayManager;
import android.view.Display;
import android.view.inputmethod.InputMethodManager;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
@Config(manifest = Config.NONE)
@RunWith(AndroidJUnit4.class)
@TargetApi(P)
public class SingleViewPresentationTest {
@Test
@Config(minSdk = JELLY_BEAN_MR1, maxSdk = R)
public void returnsOuterContextInputMethodManager() {
// There's a bug in Android Q caused by the IMM being instanced per display.
// https://github.com/flutter/flutter/issues/38375. We need the context returned by
// SingleViewPresentation to be consistent from its instantiation instead of defaulting to
// what the system would have returned at call time.
// It's not possible to set up the exact same conditions as the unit test in the bug here,
// but we can make sure that we're wrapping the Context passed in at instantiation time and
// returning the same InputMethodManager from it. This test passes in a Spy context instance
// that initially returns a mock. Without the bugfix this test falls back to Robolectric's
// system service instead of the spy's and fails.
// Create an SVP under test with a Context that returns a local IMM mock.
Context context = spy(RuntimeEnvironment.application);
InputMethodManager expected = mock(InputMethodManager.class);
when(context.getSystemService(Context.INPUT_METHOD_SERVICE)).thenReturn(expected);
DisplayManager dm = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
SingleViewPresentation svp =
new SingleViewPresentation(context, dm.getDisplay(0), null, null, null, false);
// Get the IMM from the SVP's context.
InputMethodManager actual =
(InputMethodManager) svp.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
// This should be the mocked instance from construction, not the IMM from the greater
// Android OS (or Robolectric's shadow, in this case).
assertEquals(expected, actual);
}
@Test
@Config(minSdk = JELLY_BEAN_MR1, maxSdk = R)
public void returnsOuterContextInputMethodManager_createDisplayContext() {
// The IMM should also persist across display contexts created from the base context.
// Create an SVP under test with a Context that returns a local IMM mock.
Context context = spy(RuntimeEnvironment.application);
InputMethodManager expected = mock(InputMethodManager.class);
when(context.getSystemService(Context.INPUT_METHOD_SERVICE)).thenReturn(expected);
Display display =
((DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE)).getDisplay(0);
SingleViewPresentation svp =
new SingleViewPresentation(context, display, null, null, null, false);
// Get the IMM from the SVP's context.
InputMethodManager actual =
(InputMethodManager)
svp.getContext()
.createDisplayContext(display)
.getSystemService(Context.INPUT_METHOD_SERVICE);
// This should be the mocked instance from construction, not the IMM from the greater
// Android OS (or Robolectric's shadow, in this case).
assertEquals(expected, actual);
}
}

View File

@@ -1527,6 +1527,7 @@ public class AccessibilityBridgeTest {
View embeddedView = mock(View.class);
when(accessibilityDelegate.getPlatformViewById(1)).thenReturn(embeddedView);
when(accessibilityDelegate.usesVirtualDisplay(1)).thenReturn(false);
AccessibilityNodeInfo nodeInfo = mock(AccessibilityNodeInfo.class);
when(embeddedView.createAccessibilityNodeInfo()).thenReturn(nodeInfo);
@@ -1564,6 +1565,7 @@ public class AccessibilityBridgeTest {
View embeddedView = mock(View.class);
when(accessibilityDelegate.getPlatformViewById(1)).thenReturn(embeddedView);
when(accessibilityDelegate.usesVirtualDisplay(1)).thenReturn(false);
TestSemanticsUpdate testSemanticsRootUpdate = root.toUpdate();
testSemanticsRootUpdate.sendUpdateToBridge(accessibilityBridge);
@@ -1598,6 +1600,7 @@ public class AccessibilityBridgeTest {
View embeddedView = mock(View.class);
when(accessibilityDelegate.getPlatformViewById(1)).thenReturn(embeddedView);
when(accessibilityDelegate.usesVirtualDisplay(1)).thenReturn(false);
TestSemanticsUpdate testSemanticsRootWithPlatformViewUpdate = rootWithPlatformView.toUpdate();
testSemanticsRootWithPlatformViewUpdate.sendUpdateToBridge(accessibilityBridge);
@@ -1612,6 +1615,34 @@ public class AccessibilityBridgeTest {
.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
}
@Test
public void itProducesPlatformViewNodeForVirtualDisplay() {
PlatformViewsAccessibilityDelegate accessibilityDelegate =
mock(PlatformViewsAccessibilityDelegate.class);
AccessibilityViewEmbedder accessibilityViewEmbedder = mock(AccessibilityViewEmbedder.class);
AccessibilityBridge accessibilityBridge =
setUpBridge(
/*rootAccessibilityView=*/ null,
/*accessibilityChannel=*/ null,
/*accessibilityManager=*/ null,
/*contentResolver=*/ null,
accessibilityViewEmbedder,
accessibilityDelegate);
TestSemanticsNode platformView = new TestSemanticsNode();
platformView.platformViewId = 1;
TestSemanticsUpdate testSemanticsUpdate = platformView.toUpdate();
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);
View embeddedView = mock(View.class);
when(accessibilityDelegate.getPlatformViewById(1)).thenReturn(embeddedView);
when(accessibilityDelegate.usesVirtualDisplay(1)).thenReturn(true);
accessibilityBridge.createAccessibilityNodeInfo(0);
verify(accessibilityViewEmbedder).getRootNode(eq(embeddedView), eq(0), any(Rect.class));
}
@Test
public void releaseDropsChannelMessageHandler() {
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);

View File

@@ -127,7 +127,7 @@ def main():
dest='activity_name',
action='store',
help='The activity to launch as it appears in AndroidManifest.xml, '
'e.g. .TextPlatformViewActivity'
'e.g. .PlatformViewsActivity'
)
parser.add_argument(
'--adb-path',

View File

@@ -593,7 +593,7 @@ def RunAndroidTests(android_variant='android_debug_unopt', adb_path=None):
RunCmd([
systrace_test, '--adb-path', adb_path, '--apk-path', scenario_apk,
'--package-name', 'dev.flutter.scenarios', '--activity-name',
'.TextPlatformViewActivity'
'.PlatformViewsActivity'
])

View File

@@ -13,16 +13,19 @@ _android_sources = [
"app/src/androidTest/java/dev/flutter/scenariosui/MemoryLeakTests.java",
"app/src/androidTest/java/dev/flutter/scenariosui/PlatformTextureUiTests.java",
"app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewUiTests.java",
"app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithSurfaceViewUiTest.java",
"app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithTextureViewUiTest.java",
"app/src/androidTest/java/dev/flutter/scenariosui/ScreenshotUtil.java",
"app/src/androidTest/java/dev/flutter/scenariosui/SpawnEngineTests.java",
"app/src/main/AndroidManifest.xml",
"app/src/main/java/dev/flutter/scenarios/PlatformViewsActivity.java",
"app/src/main/java/dev/flutter/scenarios/SpawnedEngineActivity.java",
"app/src/main/java/dev/flutter/scenarios/StrictModeFlutterActivity.java",
"app/src/main/java/dev/flutter/scenarios/SurfacePlatformViewFactory.java",
"app/src/main/java/dev/flutter/scenarios/TestActivity.java",
"app/src/main/java/dev/flutter/scenarios/TestableFlutterActivity.java",
"app/src/main/java/dev/flutter/scenarios/TextPlatformView.java",
"app/src/main/java/dev/flutter/scenarios/TextPlatformViewActivity.java",
"app/src/main/java/dev/flutter/scenarios/TextPlatformViewFactory.java",
"app/src/main/java/dev/flutter/scenarios/TexturePlatformViewFactory.java",
"build.gradle",
]

View File

@@ -9,7 +9,7 @@ import androidx.annotation.NonNull;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import dev.flutter.scenarios.TextPlatformViewActivity;
import dev.flutter.scenarios.PlatformViewsActivity;
import leakcanary.FailTestOnLeak;
import org.junit.Rule;
import org.junit.Test;
@@ -19,9 +19,9 @@ import org.junit.runner.RunWith;
@LargeTest
public class MemoryLeakTests {
@Rule @NonNull
public ActivityTestRule<TextPlatformViewActivity> activityRule =
public ActivityTestRule<PlatformViewsActivity> activityRule =
new ActivityTestRule<>(
TextPlatformViewActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false);
PlatformViewsActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false);
@Test
@FailTestOnLeak
@@ -29,6 +29,7 @@ public class MemoryLeakTests {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.putExtra("scenario_name", "platform_view");
intent.putExtra("use_android_view", true);
intent.putExtra("view_type", PlatformViewsActivity.TEXT_VIEW_PV);
activityRule.launchActivity(intent);
}

View File

@@ -5,11 +5,12 @@
package dev.flutter.scenariosui;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import androidx.annotation.NonNull;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import dev.flutter.scenarios.TextPlatformViewActivity;
import dev.flutter.scenarios.PlatformViewsActivity;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -21,29 +22,33 @@ public class PlatformTextureUiTests {
Intent intent;
@Rule @NonNull
public ActivityTestRule<TextPlatformViewActivity> activityRule =
public ActivityTestRule<PlatformViewsActivity> activityRule =
new ActivityTestRule<>(
TextPlatformViewActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false);
PlatformViewsActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false);
private static String goldName(String suffix) {
return "PlatformTextureUiTests_" + suffix;
}
@Before
public void setUp() {
intent = new Intent(Intent.ACTION_MAIN);
// Render a texture.
intent.putExtra("use_android_view", false);
intent.putExtra("view_type", PlatformViewsActivity.TEXT_VIEW_PV);
}
@Test
public void testPlatformView() throws Exception {
intent.putExtra("scenario_name", "platform_view");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), "PlatformTextureUiTests_testPlatformView");
ScreenshotUtil.capture(activityRule.launchActivity(intent), goldName("testPlatformView"));
}
@Test
public void testPlatformViewMultiple() throws Exception {
intent.putExtra("scenario_name", "platform_view_multiple");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), "PlatformTextureUiTests_testPlatformViewMultiple");
activityRule.launchActivity(intent), goldName("testPlatformViewMultiple"));
}
@Test
@@ -51,65 +56,64 @@ public class PlatformTextureUiTests {
intent.putExtra("scenario_name", "platform_view_multiple_background_foreground");
ScreenshotUtil.capture(
activityRule.launchActivity(intent),
"PlatformTextureUiTests_testPlatformViewMultipleBackgroundForeground");
goldName("testPlatformViewMultipleBackgroundForeground"));
}
@Test
public void testPlatformViewCliprect() throws Exception {
intent.putExtra("scenario_name", "platform_view_cliprect");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), "PlatformTextureUiTests_testPlatformViewCliprect");
activityRule.launchActivity(intent), goldName("testPlatformViewCliprect"));
}
@Test
public void testPlatformViewCliprrect() throws Exception {
intent.putExtra("scenario_name", "platform_view_cliprrect");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), "PlatformTextureUiTests_testPlatformViewCliprrect");
activityRule.launchActivity(intent), goldName("testPlatformViewCliprrect"));
}
@Test
public void testPlatformViewClippath() throws Exception {
intent.putExtra("scenario_name", "platform_view_clippath");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), "PlatformTextureUiTests_testPlatformViewClippath");
activityRule.launchActivity(intent), goldName("testPlatformViewClippath"));
}
@Test
public void testPlatformViewTransform() throws Exception {
intent.putExtra("scenario_name", "platform_view_transform");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), "PlatformTextureUiTests_testPlatformViewTransform");
activityRule.launchActivity(intent), goldName("testPlatformViewTransform"));
}
@Test
public void testPlatformViewOpacity() throws Exception {
intent.putExtra("scenario_name", "platform_view_opacity");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), "PlatformTextureUiTests_testPlatformViewOpacity");
activityRule.launchActivity(intent), goldName("testPlatformViewOpacity"));
}
@Test
public void testPlatformViewRotate() throws Exception {
intent.putExtra("scenario_name", "platform_view_rotate");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), "PlatformTextureUiTests_testPlatformViewRotate");
PlatformViewsActivity activity = activityRule.launchActivity(intent);
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
ScreenshotUtil.capture(activity, goldName("testPlatformViewRotate"));
}
@Test
public void testPlatformViewMultipleWithoutOverlays() throws Exception {
intent.putExtra("scenario_name", "platform_view_multiple_without_overlays");
ScreenshotUtil.capture(
activityRule.launchActivity(intent),
"PlatformTextureUiTests_testPlatformViewMultipleWithoutOverlays");
activityRule.launchActivity(intent), goldName("testPlatformViewMultipleWithoutOverlays"));
}
@Test
public void testPlatformViewTwoIntersectingOverlays() throws Exception {
intent.putExtra("scenario_name", "platform_view_two_intersecting_overlays");
ScreenshotUtil.capture(
activityRule.launchActivity(intent),
"PlatformTextureUiTests_testPlatformViewTwoIntersectingOverlays");
activityRule.launchActivity(intent), goldName("testPlatformViewTwoIntersectingOverlays"));
}
@Test
@@ -117,6 +121,6 @@ public class PlatformTextureUiTests {
intent.putExtra("scenario_name", "platform_view_no_overlay_intersection");
ScreenshotUtil.capture(
activityRule.launchActivity(intent),
"PlatformTextureUiTests_testPlatformViewWithoutOverlayIntersection");
goldName("testPlatformViewWithoutOverlayIntersection"));
}
}

View File

@@ -5,11 +5,12 @@
package dev.flutter.scenariosui;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import androidx.annotation.NonNull;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import dev.flutter.scenarios.TextPlatformViewActivity;
import dev.flutter.scenarios.PlatformViewsActivity;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -21,29 +22,33 @@ public class PlatformViewUiTests {
Intent intent;
@Rule @NonNull
public ActivityTestRule<TextPlatformViewActivity> activityRule =
public ActivityTestRule<PlatformViewsActivity> activityRule =
new ActivityTestRule<>(
TextPlatformViewActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false);
PlatformViewsActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false);
private static String goldName(String suffix) {
return "PlatformViewUiTests_" + suffix;
}
@Before
public void setUp() {
intent = new Intent(Intent.ACTION_MAIN);
// Render a native android view.
intent.putExtra("use_android_view", true);
intent.putExtra("view_type", PlatformViewsActivity.TEXT_VIEW_PV);
}
@Test
public void testPlatformView() throws Exception {
intent.putExtra("scenario_name", "platform_view");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), "PlatformViewUiTests_testPlatformView");
ScreenshotUtil.capture(activityRule.launchActivity(intent), goldName("testPlatformView"));
}
@Test
public void testPlatformViewMultiple() throws Exception {
intent.putExtra("scenario_name", "platform_view_multiple");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), "PlatformViewUiTests_testPlatformViewMultiple");
activityRule.launchActivity(intent), goldName("testPlatformViewMultiple"));
}
@Test
@@ -51,65 +56,64 @@ public class PlatformViewUiTests {
intent.putExtra("scenario_name", "platform_view_multiple_background_foreground");
ScreenshotUtil.capture(
activityRule.launchActivity(intent),
"PlatformViewUiTests_testPlatformViewMultipleBackgroundForeground");
goldName("testPlatformViewMultipleBackgroundForeground"));
}
@Test
public void testPlatformViewCliprect() throws Exception {
intent.putExtra("scenario_name", "platform_view_cliprect");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), "PlatformViewUiTests_testPlatformViewCliprect");
activityRule.launchActivity(intent), goldName("testPlatformViewCliprect"));
}
@Test
public void testPlatformViewCliprrect() throws Exception {
intent.putExtra("scenario_name", "platform_view_cliprrect");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), "PlatformViewUiTests_testPlatformViewCliprrect");
activityRule.launchActivity(intent), goldName("testPlatformViewCliprrect"));
}
@Test
public void testPlatformViewClippath() throws Exception {
intent.putExtra("scenario_name", "platform_view_clippath");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), "PlatformViewUiTests_testPlatformViewClippath");
activityRule.launchActivity(intent), goldName("testPlatformViewClippath"));
}
@Test
public void testPlatformViewTransform() throws Exception {
intent.putExtra("scenario_name", "platform_view_transform");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), "PlatformViewUiTests_testPlatformViewTransform");
activityRule.launchActivity(intent), goldName("testPlatformViewTransform"));
}
@Test
public void testPlatformViewOpacity() throws Exception {
intent.putExtra("scenario_name", "platform_view_opacity");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), "PlatformViewUiTests_testPlatformViewOpacity");
activityRule.launchActivity(intent), goldName("testPlatformViewOpacity"));
}
@Test
public void testPlatformViewRotate() throws Exception {
intent.putExtra("scenario_name", "platform_view_rotate");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), "PlatformViewUiTests_testPlatformViewRotate");
PlatformViewsActivity activity = activityRule.launchActivity(intent);
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
ScreenshotUtil.capture(activity, goldName("testPlatformViewRotate"));
}
@Test
public void testPlatformViewMultipleWithoutOverlays() throws Exception {
intent.putExtra("scenario_name", "platform_view_multiple_without_overlays");
ScreenshotUtil.capture(
activityRule.launchActivity(intent),
"PlatformViewUiTests_testPlatformViewMultipleWithoutOverlays");
activityRule.launchActivity(intent), goldName("testPlatformViewMultipleWithoutOverlays"));
}
@Test
public void testPlatformViewTwoIntersectingOverlays() throws Exception {
intent.putExtra("scenario_name", "platform_view_two_intersecting_overlays");
ScreenshotUtil.capture(
activityRule.launchActivity(intent),
"PlatformViewUiTests_testPlatformViewTwoIntersectingOverlays");
activityRule.launchActivity(intent), goldName("testPlatformViewTwoIntersectingOverlays"));
}
@Test
@@ -117,6 +121,6 @@ public class PlatformViewUiTests {
intent.putExtra("scenario_name", "platform_view_no_overlay_intersection");
ScreenshotUtil.capture(
activityRule.launchActivity(intent),
"PlatformViewUiTests_testPlatformViewWithoutOverlayIntersection");
goldName("testPlatformViewWithoutOverlayIntersection"));
}
}

View File

@@ -0,0 +1,134 @@
// 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 dev.flutter.scenariosui;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import androidx.annotation.NonNull;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import dev.flutter.scenarios.PlatformViewsActivity;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class PlatformViewWithSurfaceViewUiTest {
Intent intent;
@Rule @NonNull
public ActivityTestRule<PlatformViewsActivity> activityRule =
new ActivityTestRule<>(
PlatformViewsActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false);
private static String goldName(String suffix) {
return "PlatformViewWithSurfaceViewUiTest_" + suffix;
}
@Before
public void setUp() {
intent = new Intent(Intent.ACTION_MAIN);
// Render a texture.
intent.putExtra("use_android_view", false);
intent.putExtra("view_type", PlatformViewsActivity.SURFACE_VIEW_PV);
}
@Test
public void testPlatformView() throws Exception {
intent.putExtra("scenario_name", "platform_view");
ScreenshotUtil.capture(activityRule.launchActivity(intent), goldName("testPlatformView"));
}
@Test
public void testPlatformViewMultiple() throws Exception {
intent.putExtra("scenario_name", "platform_view_multiple");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), goldName("testPlatformViewMultiple"));
}
@Test
public void testPlatformViewMultipleBackgroundForeground() throws Exception {
intent.putExtra("scenario_name", "platform_view_multiple_background_foreground");
ScreenshotUtil.capture(
activityRule.launchActivity(intent),
goldName("testPlatformViewMultipleBackgroundForeground"));
}
@Test
public void testPlatformViewCliprect() throws Exception {
intent.putExtra("scenario_name", "platform_view_cliprect");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), goldName("testPlatformViewCliprect"));
}
@Test
public void testPlatformViewCliprrect() throws Exception {
intent.putExtra("scenario_name", "platform_view_cliprrect");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), goldName("testPlatformViewCliprrect"));
}
@Test
public void testPlatformViewClippath() throws Exception {
intent.putExtra("scenario_name", "platform_view_clippath");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), goldName("testPlatformViewClippath"));
}
@Test
public void testPlatformViewTransform() throws Exception {
intent.putExtra("scenario_name", "platform_view_transform");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), goldName("testPlatformViewTransform"));
}
@Test
public void testPlatformViewOpacity() throws Exception {
intent.putExtra("scenario_name", "platform_view_opacity");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), goldName("testPlatformViewOpacity"));
}
@Test
public void testPlatformViewRotate() throws Exception {
intent.putExtra("scenario_name", "platform_view_rotate");
PlatformViewsActivity activity = activityRule.launchActivity(intent);
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
ScreenshotUtil.capture(activity, goldName("testPlatformViewRotate"));
}
@Test
public void testPlatformViewMultipleWithoutOverlays() throws Exception {
intent.putExtra("scenario_name", "platform_view_multiple_without_overlays");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), goldName("testPlatformViewMultipleWithoutOverlays"));
}
@Test
public void testPlatformViewTwoIntersectingOverlays() throws Exception {
intent.putExtra("scenario_name", "platform_view_two_intersecting_overlays");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), goldName("testPlatformViewTwoIntersectingOverlays"));
}
@Test
public void testPlatformViewWithoutOverlayIntersection() throws Exception {
intent.putExtra("scenario_name", "platform_view_no_overlay_intersection");
ScreenshotUtil.capture(
activityRule.launchActivity(intent),
goldName("testPlatformViewWithoutOverlayIntersection"));
}
@Test
public void testPlatformViewLargerThanDisplaySize() throws Exception {
// Regression test for https://github.com/flutter/flutter/issues/2897.
intent.putExtra("scenario_name", "platform_view_larger_than_display_size");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), goldName("testPlatformViewLargerThanDisplaySize"));
}
}

View File

@@ -0,0 +1,124 @@
// 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 dev.flutter.scenariosui;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import androidx.annotation.NonNull;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import dev.flutter.scenarios.PlatformViewsActivity;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class PlatformViewWithTextureViewUiTest {
Intent intent;
@Rule @NonNull
public ActivityTestRule<PlatformViewsActivity> activityRule =
new ActivityTestRule<>(
PlatformViewsActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false);
private static String goldName(String suffix) {
return "PlatformViewWithTextureViewUiTest_" + suffix;
}
@Before
public void setUp() {
intent = new Intent(Intent.ACTION_MAIN);
intent.putExtra("view_type", PlatformViewsActivity.TEXTURE_VIEW_PV);
}
@Test
public void testPlatformView() throws Exception {
intent.putExtra("scenario_name", "platform_view");
ScreenshotUtil.capture(activityRule.launchActivity(intent), goldName("testPlatformView"));
}
@Test
public void testPlatformViewMultiple() throws Exception {
intent.putExtra("scenario_name", "platform_view_multiple");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), goldName("testPlatformViewMultiple"));
}
@Test
public void testPlatformViewMultipleBackgroundForeground() throws Exception {
intent.putExtra("scenario_name", "platform_view_multiple_background_foreground");
ScreenshotUtil.capture(
activityRule.launchActivity(intent),
goldName("testPlatformViewMultipleBackgroundForeground"));
}
@Test
public void testPlatformViewCliprect() throws Exception {
intent.putExtra("scenario_name", "platform_view_cliprect");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), goldName("testPlatformViewCliprect"));
}
@Test
public void testPlatformViewCliprrect() throws Exception {
intent.putExtra("scenario_name", "platform_view_cliprrect");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), goldName("testPlatformViewCliprrect"));
}
@Test
public void testPlatformViewClippath() throws Exception {
intent.putExtra("scenario_name", "platform_view_clippath");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), goldName("testPlatformViewClippath"));
}
@Test
public void testPlatformViewTransform() throws Exception {
intent.putExtra("scenario_name", "platform_view_transform");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), goldName("testPlatformViewTransform"));
}
@Test
public void testPlatformViewOpacity() throws Exception {
intent.putExtra("scenario_name", "platform_view_opacity");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), goldName("testPlatformViewOpacity"));
}
@Test
public void testPlatformViewRotate() throws Exception {
intent.putExtra("scenario_name", "platform_view_rotate");
PlatformViewsActivity activity = activityRule.launchActivity(intent);
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
ScreenshotUtil.capture(activity, goldName("testPlatformViewRotate"));
}
@Test
public void testPlatformViewMultipleWithoutOverlays() throws Exception {
intent.putExtra("scenario_name", "platform_view_multiple_without_overlays");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), goldName("testPlatformViewMultipleWithoutOverlays"));
}
@Test
public void testPlatformViewTwoIntersectingOverlays() throws Exception {
intent.putExtra("scenario_name", "platform_view_two_intersecting_overlays");
ScreenshotUtil.capture(
activityRule.launchActivity(intent), goldName("testPlatformViewTwoIntersectingOverlays"));
}
@Test
public void testPlatformViewWithoutOverlayIntersection() throws Exception {
intent.putExtra("scenario_name", "platform_view_no_overlay_intersection");
ScreenshotUtil.capture(
activityRule.launchActivity(intent),
goldName("testPlatformViewWithoutOverlayIntersection"));
}
}

View File

@@ -9,7 +9,7 @@
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".TextPlatformViewActivity"
android:name=".PlatformViewsActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:launchMode="singleTop"

View File

@@ -0,0 +1,33 @@
// 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 dev.flutter.scenarios;
import androidx.annotation.NonNull;
import io.flutter.embedding.engine.FlutterEngine;
public class PlatformViewsActivity extends TestActivity {
public static final String TEXT_VIEW_PV = "scenarios/textPlatformView";
public static final String SURFACE_VIEW_PV = "scenarios/surfacePlatformV";
public static final String TEXTURE_VIEW_PV = "scenarios/texturePlatformV";
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
flutterEngine
.getPlatformViewsController()
.getRegistry()
.registerViewFactory(TEXT_VIEW_PV, new TextPlatformViewFactory());
flutterEngine
.getPlatformViewsController()
.getRegistry()
.registerViewFactory(SURFACE_VIEW_PV, new SurfacePlatformViewFactory());
flutterEngine
.getPlatformViewsController()
.getRegistry()
.registerViewFactory(TEXTURE_VIEW_PV, new TexturePlatformViewFactory());
}
}

View File

@@ -0,0 +1,101 @@
// 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 dev.flutter.scenarios;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.Log;
import io.flutter.plugin.common.MessageCodec;
import io.flutter.plugin.common.StringCodec;
import io.flutter.plugin.platform.PlatformView;
import io.flutter.plugin.platform.PlatformViewFactory;
import java.nio.ByteBuffer;
@TargetApi(23)
public final class SurfacePlatformViewFactory extends PlatformViewFactory {
SurfacePlatformViewFactory() {
super(
new MessageCodec<Object>() {
@Nullable
@Override
public ByteBuffer encodeMessage(@Nullable Object o) {
if (o instanceof String) {
return StringCodec.INSTANCE.encodeMessage((String) o);
}
return null;
}
@Nullable
@Override
public Object decodeMessage(@Nullable ByteBuffer byteBuffer) {
return StringCodec.INSTANCE.decodeMessage(byteBuffer);
}
});
}
@SuppressWarnings("unchecked")
@Override
@NonNull
public PlatformView create(@NonNull Context context, int id, @Nullable Object args) {
return new SurfacePlatformView(context);
}
private static class SurfacePlatformView implements PlatformView {
static String TAG = "SurfacePlatformView";
final SurfaceView surfaceView;
@SuppressWarnings("unchecked")
SurfacePlatformView(@NonNull final Context context) {
surfaceView = new SurfaceView(context);
surfaceView
.getHolder()
.addCallback(
new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.i(TAG, "surfaceCreated");
final Surface surface = holder.getSurface();
final Canvas canvas = surface.lockHardwareCanvas();
canvas.drawColor(Color.WHITE);
final Paint paint = new Paint();
paint.setColor(Color.RED);
canvas.drawCircle(canvas.getWidth() / 2, canvas.getHeight() / 2, 20, paint);
surface.unlockCanvasAndPost(canvas);
}
@Override
public void surfaceChanged(
SurfaceHolder holder, int format, int width, int height) {
Log.i(TAG, "surfaceChanged");
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.i(TAG, "surfaceDestroyed");
}
});
}
@Override
@NonNull
public View getView() {
return surfaceView;
}
@Override
public void dispose() {}
}
}

View File

@@ -84,6 +84,7 @@ public abstract class TestActivity extends TestableFlutterActivity {
test.put("name", "animated_color_square");
}
test.put("use_android_view", launchIntent.getBooleanExtra("use_android_view", false));
test.put("view_type", launchIntent.getStringExtra("view_type"));
getScenarioParams(test);
channel.invokeMethod("set_scenario", test);
}

View File

@@ -1,46 +0,0 @@
// 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 dev.flutter.scenarios;
import android.content.Context;
import android.graphics.Color;
import android.view.Choreographer;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.plugin.platform.PlatformView;
public class TextPlatformView implements PlatformView {
final TextView textView;
@SuppressWarnings("unchecked")
TextPlatformView(@NonNull final Context context, int id, @Nullable String params) {
textView = new TextView(context);
textView.setTextSize(72);
textView.setBackgroundColor(Color.rgb(255, 255, 255));
textView.setText(params);
// Investigate why this is needed to pass some gold tests.
Choreographer.getInstance()
.postFrameCallbackDelayed(
new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
textView.invalidate();
}
},
500);
}
@Override
@NonNull
public View getView() {
return textView;
}
@Override
public void dispose() {}
}

View File

@@ -1,21 +0,0 @@
// 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 dev.flutter.scenarios;
import androidx.annotation.NonNull;
import io.flutter.embedding.engine.FlutterEngine;
public class TextPlatformViewActivity extends TestActivity {
static final String TAG = "Scenarios";
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
flutterEngine
.getPlatformViewsController()
.getRegistry()
.registerViewFactory("scenarios/textPlatformView", new TextPlatformViewFactory());
}
}

View File

@@ -5,6 +5,10 @@
package dev.flutter.scenarios;
import android.content.Context;
import android.graphics.Color;
import android.view.Choreographer;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.plugin.common.MessageCodec;
@@ -41,4 +45,36 @@ public final class TextPlatformViewFactory extends PlatformViewFactory {
String params = (String) args;
return new TextPlatformView(context, id, params);
}
private static class TextPlatformView implements PlatformView {
final TextView textView;
@SuppressWarnings("unchecked")
TextPlatformView(@NonNull final Context context, int id, @Nullable String params) {
textView = new TextView(context);
textView.setTextSize(72);
textView.setBackgroundColor(Color.WHITE);
textView.setText(params);
// Investigate why this is needed to pass some gold tests.
Choreographer.getInstance()
.postFrameCallbackDelayed(
new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
textView.invalidate();
}
},
500);
}
@Override
@NonNull
public View getView() {
return textView;
}
@Override
public void dispose() {}
}
}

View File

@@ -0,0 +1,112 @@
// 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 dev.flutter.scenarios;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.SurfaceTexture;
import android.view.Choreographer;
import android.view.TextureView;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.Log;
import io.flutter.plugin.common.MessageCodec;
import io.flutter.plugin.common.StringCodec;
import io.flutter.plugin.platform.PlatformView;
import io.flutter.plugin.platform.PlatformViewFactory;
import java.nio.ByteBuffer;
@TargetApi(23)
public final class TexturePlatformViewFactory extends PlatformViewFactory {
TexturePlatformViewFactory() {
super(
new MessageCodec<Object>() {
@Nullable
@Override
public ByteBuffer encodeMessage(@Nullable Object o) {
if (o instanceof String) {
return StringCodec.INSTANCE.encodeMessage((String) o);
}
return null;
}
@Nullable
@Override
public Object decodeMessage(@Nullable ByteBuffer byteBuffer) {
return StringCodec.INSTANCE.decodeMessage(byteBuffer);
}
});
}
@SuppressWarnings("unchecked")
@Override
@NonNull
public PlatformView create(@NonNull Context context, int id, @Nullable Object args) {
return new TexturePlatformView(context);
}
private static class TexturePlatformView implements PlatformView {
static String TAG = "TexturePlatformView";
final TextureView textureView;
@SuppressWarnings("unchecked")
TexturePlatformView(@NonNull final Context context) {
textureView = new TextureView(context);
textureView.setSurfaceTextureListener(
new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
Log.i(TAG, "onSurfaceTextureAvailable");
final Canvas canvas = textureView.lockCanvas();
canvas.drawColor(Color.WHITE);
final Paint paint = new Paint();
paint.setColor(Color.GREEN);
canvas.drawCircle(canvas.getWidth() / 2, canvas.getHeight() / 2, 20, paint);
textureView.unlockCanvasAndPost(canvas);
Choreographer.getInstance()
.postFrameCallbackDelayed(
new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
textureView.invalidate();
}
},
500);
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
Log.i(TAG, "onSurfaceTextureDestroyed");
return true;
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
Log.i(TAG, "onSurfaceTextureSizeChanged");
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
Log.i(TAG, "onSurfaceTextureUpdated");
}
});
}
@Override
@NonNull
public View getView() {
return textureView;
}
@Override
public void dispose() {}
}
}

View File

@@ -114,6 +114,42 @@ class PlatformViewNoOverlayIntersectionScenario extends Scenario
}
}
/// A platform view that is larger than the display size.
/// This is only applicable on Android while using virtual displays.
/// Related issue: https://github.com/flutter/flutter/issues/2897.
class PlatformViewLargerThanDisplaySize extends Scenario
with _BasePlatformViewScenarioMixin {
/// Creates the PlatformView scenario.
///
/// The [dispatcher] parameter must not be null.
PlatformViewLargerThanDisplaySize(
PlatformDispatcher dispatcher, {
required this.id,
}) : assert(dispatcher != null),
super(dispatcher);
/// The platform view identifier.
final int id;
@override
void onBeginFrame(Duration duration) {
final SceneBuilder builder = SceneBuilder();
addPlatformView(
id,
dispatcher: dispatcher,
sceneBuilder: builder,
width: 15000,
height: 60000,
);
finishBuilder(
builder,
);
}
}
/// A simple platform view with an overlay that partially intersects with the platform view.
class PlatformViewPartialIntersectionScenario extends Scenario
with _BasePlatformViewScenarioMixin {
@@ -1049,7 +1085,12 @@ void addPlatformView(
double height = 500,
String viewType = 'scenarios/textPlatformView',
}) {
if (scenarioParams['view_type'] is String) {
viewType = scenarioParams['view_type'];
}
final String platformViewKey = '$viewType-$id';
if (_createdPlatformViews.containsKey(platformViewKey)) {
addPlatformViewToSceneBuilder(
id,
@@ -1060,9 +1101,10 @@ void addPlatformView(
);
return;
}
bool usesAndroidHybridComposition = false;
if (scenarioParams['use_android_view'] != null) {
usesAndroidHybridComposition = scenarioParams['use_android_view'] as bool;
if (scenarioParams['use_android_view'] is bool) {
usesAndroidHybridComposition = scenarioParams['use_android_view'];
}
const int _valueTrue = 1;
@@ -1079,9 +1121,9 @@ void addPlatformView(
_valueMap,
if (Platform.isIOS) 3, // 3 entries in map for iOS.
if (Platform.isAndroid && !usesAndroidHybridComposition)
6, // 6 entries in map for virtual displays on Android.
6, // 6 entries in map for texture on Android.
if (Platform.isAndroid && usesAndroidHybridComposition)
5, // 5 entries in map for Android views.
5, // 5 entries in map for hybrid composition on Android.
_valueString,
'id'.length,
...utf8.encode('id'),

View File

@@ -26,13 +26,13 @@ abstract class Scenario {
///
/// See [PlatformDispatcher.onDrawFrame] for more details.
void onDrawFrame() {
Future<void>.delayed(const Duration(seconds: 1), () {
if (_didScheduleScreenshot) {
dispatcher.sendPlatformMessage('take_screenshot', null, null);
} else {
_didScheduleScreenshot = true;
dispatcher.scheduleFrame();
}
if (_didScheduleScreenshot) {
dispatcher.sendPlatformMessage('take_screenshot', null, null);
return;
}
Future<void>.delayed(const Duration(seconds: 2), () {
_didScheduleScreenshot = true;
dispatcher.scheduleFrame();
});
}

View File

@@ -24,6 +24,7 @@ Map<String, ScenarioFactory> _scenarios = <String, ScenarioFactory>{
'locale_initialization': () => LocaleInitialization(PlatformDispatcher.instance),
'platform_view': () => PlatformViewScenario(PlatformDispatcher.instance, id: _viewId++),
'platform_view_no_overlay_intersection': () => PlatformViewNoOverlayIntersectionScenario(PlatformDispatcher.instance, id: _viewId++),
'platform_view_larger_than_display_size': () => PlatformViewLargerThanDisplaySize(PlatformDispatcher.instance, id: _viewId++),
'platform_view_partial_intersection': () => PlatformViewPartialIntersectionScenario(PlatformDispatcher.instance, id: _viewId++),
'platform_view_two_intersecting_overlays': () => PlatformViewTwoIntersectingOverlaysScenario(PlatformDispatcher.instance, id: _viewId++),
'platform_view_one_overlay_two_intersecting_overlays': () => PlatformViewOneOverlayTwoIntersectingOverlaysScenario(PlatformDispatcher.instance, id: _viewId++),

View File

@@ -100,4 +100,15 @@
column="18"/>
</issue>
<issue
id="UseSparseArrays"
message="Use `new SparseArray&lt;VirtualDisplayController>(...)` instead for better performance"
errorLine1=" vdControllers = new HashMap&lt;>();"
errorLine2=" ~~~~~~~~~~~~~~~">
<location
file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java"
line="61"
column="25"/>
</issue>
</issues>

View File

@@ -107,7 +107,9 @@
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroup.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRegistry.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRegistryImpl.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewFactory.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/AccessibilityEventsDelegate.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformView.java" />