Improve platform views performance (flutter/engine#31198)

This commit is contained in:
Emmanuel Garcia
2022-02-11 21:15:10 -08:00
committed by GitHub
parent dafb66d3cd
commit 9ee76a6572
25 changed files with 925 additions and 1610 deletions

View File

@@ -1189,10 +1189,9 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/Platfor
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewFactory.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRegistry.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRegistryImpl.java
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

@@ -268,10 +268,9 @@ android_java_sources = [
"io/flutter/plugin/platform/PlatformViewFactory.java",
"io/flutter/plugin/platform/PlatformViewRegistry.java",
"io/flutter/plugin/platform/PlatformViewRegistryImpl.java",
"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

@@ -107,7 +107,7 @@ public class AndroidTouchProcessor {
* the gesture pointers into screen coordinates.
* @return True if the event was handled.
*/
public boolean onTouchEvent(@NonNull MotionEvent event, Matrix transformMatrix) {
public boolean onTouchEvent(@NonNull MotionEvent event, @NonNull Matrix transformMatrix) {
int pointerCount = event.getPointerCount();
// Prepare a data packet of the appropriate size and order.

View File

@@ -791,7 +791,6 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
+ viewportMetrics.viewInsetBottom);
sendViewportMetricsToFlutter();
return newInsets;
}
@@ -867,21 +866,6 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
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

@@ -9,13 +9,13 @@ import android.graphics.Matrix;
import android.graphics.Path;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import io.flutter.embedding.android.AndroidTouchProcessor;
import io.flutter.util.ViewUtils;
/**
* A view that applies the {@link io.flutter.embedding.engine.mutatorsstack.FlutterMutatorsStack} to
@@ -49,31 +49,6 @@ public class FlutterMutatorView extends FrameLayout {
this(context, 1, /* androidTouchProcessor=*/ null);
}
/**
* Determines if the current view or any descendant view has focus.
*
* @param root The root view.
* @return True if the current view or any descendant view has focus.
*/
@VisibleForTesting
public static boolean childHasFocus(@Nullable View root) {
if (root == null) {
return false;
}
if (root.hasFocus()) {
return true;
}
if (root instanceof ViewGroup) {
final ViewGroup viewGroup = (ViewGroup) root;
for (int idx = 0; idx < viewGroup.getChildCount(); idx++) {
if (childHasFocus(viewGroup.getChildAt(idx))) {
return true;
}
}
}
return false;
}
@Nullable @VisibleForTesting ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener;
/**
@@ -95,7 +70,7 @@ public class FlutterMutatorView extends FrameLayout {
new ViewTreeObserver.OnGlobalFocusChangeListener() {
@Override
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
userFocusListener.onFocusChange(mutatorView, childHasFocus(mutatorView));
userFocusListener.onFocusChange(mutatorView, ViewUtils.childHasFocus(mutatorView));
}
};
observer.addOnGlobalFocusChangeListener(activeFocusListener);

View File

@@ -14,6 +14,7 @@ import io.flutter.plugin.common.StandardMethodCodec;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -64,6 +65,9 @@ public class PlatformViewsChannel {
case "resize":
resize(call, result);
break;
case "offset":
offset(call, result);
break;
case "touch":
touch(call, result);
break;
@@ -82,29 +86,40 @@ public class PlatformViewsChannel {
}
private void create(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
Map<String, Object> createArgs = call.arguments();
boolean usesHybridComposition =
final Map<String, Object> createArgs = call.arguments();
// TODO(egarciad): Remove the "hybrid" case.
final boolean usesPlatformViewLayer =
createArgs.containsKey("hybrid") && (boolean) createArgs.get("hybrid");
// In hybrid mode, the size of the view is determined by the size of the Flow layer.
double width = (usesHybridComposition) ? 0 : (double) createArgs.get("width");
double height = (usesHybridComposition) ? 0 : (double) createArgs.get("height");
PlatformViewCreationRequest request =
new PlatformViewCreationRequest(
(int) createArgs.get("id"),
(String) createArgs.get("viewType"),
width,
height,
(int) createArgs.get("direction"),
createArgs.containsKey("params")
? ByteBuffer.wrap((byte[]) createArgs.get("params"))
: null);
final ByteBuffer additionalParams =
createArgs.containsKey("params")
? ByteBuffer.wrap((byte[]) createArgs.get("params"))
: null;
try {
if (usesHybridComposition) {
handler.createAndroidViewForPlatformView(request);
if (usesPlatformViewLayer) {
final PlatformViewCreationRequest request =
new PlatformViewCreationRequest(
(int) createArgs.get("id"),
(String) createArgs.get("viewType"),
0,
0,
0,
0,
(int) createArgs.get("direction"),
additionalParams);
handler.createForPlatformViewLayer(request);
result.success(null);
} else {
long textureId = handler.createVirtualDisplayForPlatformView(request);
final PlatformViewCreationRequest request =
new PlatformViewCreationRequest(
(int) createArgs.get("id"),
(String) createArgs.get("viewType"),
createArgs.containsKey("top") ? (double) createArgs.get("top") : 0.0,
createArgs.containsKey("left") ? (double) createArgs.get("left") : 0.0,
(double) createArgs.get("width"),
(double) createArgs.get("height"),
(int) createArgs.get("direction"),
additionalParams);
long textureId = handler.createForTextureLayer(request);
result.success(textureId);
}
} catch (IllegalStateException exception) {
@@ -115,15 +130,9 @@ public class PlatformViewsChannel {
private void dispose(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
Map<String, Object> disposeArgs = call.arguments();
int viewId = (int) disposeArgs.get("id");
boolean usesHybridComposition =
disposeArgs.containsKey("hybrid") && (boolean) disposeArgs.get("hybrid");
try {
if (usesHybridComposition) {
handler.disposeAndroidViewForPlatformView(viewId);
} else {
handler.disposeVirtualDisplayForPlatformView(viewId);
}
handler.dispose(viewId);
result.success(null);
} catch (IllegalStateException exception) {
result.error("error", detailedExceptionString(exception), null);
@@ -138,14 +147,28 @@ public class PlatformViewsChannel {
(double) resizeArgs.get("width"),
(double) resizeArgs.get("height"));
try {
handler.resizePlatformView(
resizeRequest,
new Runnable() {
@Override
public void run() {
result.success(null);
}
});
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);
}
} catch (IllegalStateException exception) {
result.error("error", detailedExceptionString(exception), null);
}
}
private void offset(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
Map<String, Object> offsetArgs = call.arguments();
try {
handler.offset(
(int) offsetArgs.get("id"),
(double) offsetArgs.get("top"),
(double) offsetArgs.get("left"));
result.success(null);
} catch (IllegalStateException exception) {
result.error("error", detailedExceptionString(exception), null);
}
@@ -249,36 +272,40 @@ public class PlatformViewsChannel {
* The Flutter application would like to display a new Android {@code View}, i.e., platform
* view.
*
* <p>The Android {@code View} is added to the view hierarchy.
*/
void createAndroidViewForPlatformView(@NonNull PlatformViewCreationRequest request);
/**
* The Flutter application would like to dispose of an existing Android {@code View} rendered in
* the view hierarchy.
*/
void disposeAndroidViewForPlatformView(int viewId);
/**
* The Flutter application would like to display a new Android {@code View}.
* <p>The Android View is added to the view hierarchy. This view is rendered in the Flutter
* framework by a PlatformViewLayer.
*
* <p>{@code View} is added to a {@code VirtualDisplay}. The framework uses id returned by this
* method to lookup the texture in the engine.
* @param request The metadata sent from the framework.
*/
long createVirtualDisplayForPlatformView(@NonNull PlatformViewCreationRequest request);
void createForPlatformViewLayer(@NonNull PlatformViewCreationRequest request);
/**
* The Flutter application would like to dispose of an existing Android {@code View} rendered in
* a virtual display.
*/
void disposeVirtualDisplayForPlatformView(int viewId);
/**
* The Flutter application would like to resize an existing Android {@code View}, i.e., platform
* The Flutter application would like to display a new Android {@code View}, i.e., platform
* view.
*
* <p>The Android View is added to the view hierarchy. This view is rendered in the Flutter
* framework by a TextureLayer.
*
* @param request The metadata sent from the framework.
* @return The texture ID.
*/
void resizePlatformView(
@NonNull PlatformViewResizeRequest request, @NonNull Runnable onComplete);
long createForTextureLayer(@NonNull PlatformViewCreationRequest request);
/** The Flutter application would like to dispose of an existing Android {@code View}. */
void dispose(int viewId);
/**
* 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.
*/
PlatformViewBufferSize resize(@NonNull PlatformViewResizeRequest request);
/**
* The Flutter application would like to change the offset of an existing Android {@code View}.
*/
void offset(int viewId, double top, double left);
/**
* The user touched a platform view within Flutter.
@@ -321,6 +348,12 @@ public class PlatformViewsChannel {
/** The density independent height to display the platform view. */
public final double logicalHeight;
/** The density independent top position to display the platform view. */
public final double logicalTop;
/** The density independent left position to display the platform view. */
public final double logicalLeft;
/**
* The layout direction of the new platform view.
*
@@ -332,16 +365,20 @@ public class PlatformViewsChannel {
/** Custom parameters that are unique to the desired platform view. */
@Nullable public final ByteBuffer params;
/** Creates a request to construct a platform view that uses a virtual display. */
/** Creates a request to construct a platform view. */
public PlatformViewCreationRequest(
int viewId,
@NonNull String viewType,
double logicalTop,
double logicalLeft,
double logicalWidth,
double logicalHeight,
int direction,
@Nullable ByteBuffer params) {
this.viewId = viewId;
this.viewType = viewType;
this.logicalTop = logicalTop;
this.logicalLeft = logicalLeft;
this.logicalWidth = logicalWidth;
this.logicalHeight = logicalHeight;
this.direction = direction;
@@ -349,11 +386,7 @@ public class PlatformViewsChannel {
}
}
/**
* Request sent from Flutter to resize a platform view.
*
* <p>This only applies to platform views that use virtual displays.
*/
/** Request sent from Flutter to resize a platform view. */
public static class PlatformViewResizeRequest {
/** The ID of the platform view as seen by the Flutter side. */
public final int viewId;
@@ -371,6 +404,20 @@ public class PlatformViewsChannel {
}
}
/** The platform view buffer size. */
public static class PlatformViewBufferSize {
/** The width of the screen buffer. */
public final int width;
/** The height of the screen buffer. */
public final int height;
public PlatformViewBufferSize(int width, int height) {
this.width = width;
this.height = height;
}
}
/** 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,9 +89,7 @@ public class TextInputChannel {
try {
final JSONObject arguments = (JSONObject) args;
final int platformViewId = arguments.getInt("platformViewId");
final boolean usesVirtualDisplay =
arguments.optBoolean("usesVirtualDisplay", false);
textInputMethodHandler.setPlatformViewClient(platformViewId, usesVirtualDisplay);
textInputMethodHandler.setPlatformViewClient(platformViewId);
result.success(null);
} catch (JSONException exception) {
result.error("error", exception.getMessage(), null);
@@ -402,10 +400,8 @@ 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, boolean usesVirtualDisplay);
void setPlatformViewClient(int id);
/**
* Sets the size and the transform matrix of the current text input client.

View File

@@ -54,12 +54,6 @@ 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(
View view,
@@ -105,7 +99,7 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
@Override
public void hide() {
if (inputTarget.type == InputTarget.Type.HC_PLATFORM_VIEW) {
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
notifyViewExited();
} else {
hideTextInput(mView);
@@ -136,8 +130,8 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
}
@Override
public void setPlatformViewClient(int platformViewId, boolean usesVirtualDisplay) {
setPlatformViewTextInputClient(platformViewId, usesVirtualDisplay);
public void setPlatformViewClient(int platformViewId) {
setPlatformViewTextInputClient(platformViewId);
}
@Override
@@ -182,34 +176,6 @@ 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.VD_PLATFORM_VIEW) {
isInputConnectionLocked = true;
}
}
/**
* Unlocks the input connection.
*
* <p>See also: @{link lockPlatformViewInputConnection}.
*/
public void unlockPlatformViewInputConnection() {
isInputConnectionLocked = false;
}
/**
* Detaches the text input plugin from the platform views controller.
*
@@ -292,21 +258,10 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
return null;
}
if (inputTarget.type == InputTarget.Type.HC_PLATFORM_VIEW) {
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
return null;
}
if (inputTarget.type == InputTarget.Type.VD_PLATFORM_VIEW) {
if (isInputConnectionLocked) {
return lastInputConnection;
}
lastInputConnection =
platformViewsController
.getPlatformViewById(inputTarget.id)
.onCreateInputConnection(outAttrs);
return lastInputConnection;
}
outAttrs.inputType =
inputTypeFromTextInputType(
configuration.inputType,
@@ -361,9 +316,7 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
* input connection.
*/
public void clearPlatformViewClient(int platformViewId) {
if ((inputTarget.type == InputTarget.Type.VD_PLATFORM_VIEW
|| inputTarget.type == InputTarget.Type.HC_PLATFORM_VIEW)
&& inputTarget.id == platformViewId) {
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW && inputTarget.id == platformViewId) {
inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
notifyViewExited();
mImm.hideSoftInputFromWindow(mView.getApplicationWindowToken(), 0);
@@ -424,25 +377,13 @@ 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, 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 but on API 28 devices where the
// platform view's request to show a keyboard was ignored.
mView.requestFocus();
inputTarget = new InputTarget(InputTarget.Type.VD_PLATFORM_VIEW, platformViewId);
mImm.restartInput(mView);
mRestartInputPending = false;
} else {
inputTarget = new InputTarget(InputTarget.Type.HC_PLATFORM_VIEW, platformViewId);
lastInputConnection = null;
}
private void setPlatformViewTextInputClient(int platformViewId) {
inputTarget = new InputTarget(InputTarget.Type.PLATFORM_VIEW, platformViewId);
lastInputConnection = null;
}
private static boolean composingChanged(
@@ -533,35 +474,10 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
@VisibleForTesting
void clearTextInputClient() {
if (inputTarget.type == InputTarget.Type.VD_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;
}
@@ -571,12 +487,9 @@ 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 an embedded platform view that is backed by a virtual
// display (VD).
VD_PLATFORM_VIEW,
// InputConnection is managed by an embedded platform view that is embeded in the Android view
// hierarchy, and uses hybrid composition (HC).
HC_PLATFORM_VIEW,
// InputConnection is managed by a platform view that is embeded in the Android view
// hierarchy.
PLATFORM_VIEW,
}
public InputTarget(@NonNull Type type, int id) {

View File

@@ -60,24 +60,26 @@ public interface PlatformView {
void dispose();
/**
* Callback fired when the platform's input connection is locked, or should be used. See also
* {@link io.flutter.plugin.editing.TextInputPlugin#lockPlatformViewInputConnection}.
* Callback fired when the platform's input connection is locked, or should be used.
*
* <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.
*/
// Default interface methods are supported on all min SDK versions of Android.
@SuppressLint("NewApi")
@Deprecated
default void onInputConnectionLocked() {}
/**
* Callback fired when the platform input connection has been unlocked. See also {@link
* io.flutter.plugin.editing.TextInputPlugin#lockPlatformViewInputConnection}.
* Callback fired when the platform input connection has been unlocked.
*
* <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.
*/
// Default interface methods are supported on all min SDK versions of Android.
@SuppressLint("NewApi")
@Deprecated
default void onInputConnectionUnlocked() {}
}

View File

@@ -0,0 +1,263 @@
// 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 android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.BlendMode;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.SurfaceTexture;
import android.os.Build;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import io.flutter.Log;
import io.flutter.embedding.android.AndroidTouchProcessor;
import io.flutter.util.ViewUtils;
/**
* Wraps a platform view to intercept gestures and project this view onto a {@link SurfaceTexture}.
*
* <p>An Android platform view is composed by the engine using a {@code TextureLayer}. The view is
* embeded to the Android view hierarchy like a normal view, but it's projected onto a {@link
* SurfaceTexture}, so it can be efficiently composed by the engine.
*
* <p>Since the view is in the Android view hierarchy, keyboard and accessibility interactions
* behave normally.
*/
@TargetApi(23)
class PlatformViewWrapper extends FrameLayout {
private static final String TAG = "PlatformViewWrapper";
private int prevLeft;
private int prevTop;
private int left;
private int top;
private int bufferWidth;
private int bufferHeight;
private SurfaceTexture tx;
private Surface surface;
private AndroidTouchProcessor touchProcessor;
@Nullable @VisibleForTesting ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener;
public PlatformViewWrapper(@NonNull Context context) {
super(context);
setWillNotDraw(false);
}
/**
* Sets the touch processor that allows to intercept gestures.
*
* @param newTouchProcessor The touch processor.
*/
public void setTouchProcessor(@Nullable AndroidTouchProcessor newTouchProcessor) {
touchProcessor = newTouchProcessor;
}
/**
* Sets the texture where the view is projected onto.
*
* <p>{@link PlatformViewWrapper} doesn't take ownership of the {@link SurfaceTexture}. As a
* result, the caller is responsible for releasing the texture.
*
* <p>{@link io.flutter.view.TextureRegistry} is responsible for creating and registering textures
* in the engine. Therefore, the engine is responsible for also releasing the texture.
*
* @param newTx The texture where the view is projected onto.
*/
@SuppressLint("NewApi")
public void setTexture(@Nullable SurfaceTexture newTx) {
if (Build.VERSION.SDK_INT < 23) {
Log.e(
TAG,
"Platform views cannot be displayed below API level 23. "
+ "You can prevent this issue by setting `minSdkVersion: 23` in build.gradle.");
return;
}
tx = newTx;
if (bufferWidth > 0 && bufferHeight > 0) {
tx.setDefaultBufferSize(bufferWidth, bufferHeight);
}
if (surface != null) {
surface.release();
}
surface = createSurface(newTx);
// Fill the entire canvas with a transparent color.
// As a result, the background color of the platform view container is displayed
// to the user until the platform view draws its first frame.
final Canvas canvas = surface.lockHardwareCanvas();
try {
if (Build.VERSION.SDK_INT >= 29) {
canvas.drawColor(Color.TRANSPARENT, BlendMode.CLEAR);
} else {
canvas.drawColor(Color.TRANSPARENT);
}
} finally {
surface.unlockCanvasAndPost(canvas);
}
}
@NonNull
@VisibleForTesting
protected Surface createSurface(@NonNull SurfaceTexture tx) {
return new Surface(tx);
}
/** Returns the texture where the view is projected. */
@Nullable
public SurfaceTexture getTexture() {
return tx;
}
/**
* Sets the layout parameters for this view.
*
* @param params The new parameters.
*/
public void setLayoutParams(@NonNull FrameLayout.LayoutParams params) {
super.setLayoutParams(params);
left = params.leftMargin;
top = params.topMargin;
}
/**
* Sets the size of the image buffer.
*
* @param width The width of the screen buffer.
* @param height The height of the screen buffer.
*/
public void setBufferSize(int width, int height) {
bufferWidth = width;
bufferHeight = height;
if (tx != null) {
tx.setDefaultBufferSize(width, height);
}
}
/** Returns the image buffer width. */
public int getBufferWidth() {
return bufferWidth;
}
/** Returns the image buffer height. */
public int getBufferHeight() {
return bufferHeight;
}
/** Releases the surface. */
public void release() {
// Don't release the texture.
tx = null;
if (surface != null) {
surface.release();
surface = null;
}
}
@Override
public boolean onInterceptTouchEvent(@NonNull MotionEvent event) {
return true;
}
@Override
public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {
super.onDescendantInvalidated(child, target);
invalidate();
}
@Override
@SuppressLint("NewApi")
public void draw(Canvas canvas) {
if (surface == null || !surface.isValid()) {
Log.e(TAG, "Invalid surface. The platform view cannot be displayed.");
return;
}
if (tx == null || tx.isReleased()) {
Log.e(TAG, "Invalid texture. The platform view cannot be displayed.");
return;
}
// Override the canvas that this subtree of views will use to draw.
final Canvas surfaceCanvas = surface.lockHardwareCanvas();
try {
// Clear the current pixels in the canvas.
// This helps when a WebView renders an HTML document with transparent background.
if (Build.VERSION.SDK_INT >= 29) {
surfaceCanvas.drawColor(Color.TRANSPARENT, BlendMode.CLEAR);
} else {
surfaceCanvas.drawColor(Color.TRANSPARENT);
}
super.draw(surfaceCanvas);
} finally {
surface.unlockCanvasAndPost(surfaceCanvas);
}
}
@Override
@SuppressLint("ClickableViewAccessibility")
public boolean onTouchEvent(@NonNull MotionEvent event) {
if (touchProcessor == null) {
return super.onTouchEvent(event);
}
final Matrix screenMatrix = new Matrix();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
prevLeft = left;
prevTop = top;
screenMatrix.postTranslate(left, top);
break;
case MotionEvent.ACTION_MOVE:
// While the view is dragged, use the left and top positions as
// they were at the moment the touch event fired.
screenMatrix.postTranslate(prevLeft, prevTop);
prevLeft = left;
prevTop = top;
break;
case MotionEvent.ACTION_UP:
default:
screenMatrix.postTranslate(left, top);
break;
}
return touchProcessor.onTouchEvent(event, screenMatrix);
}
public void setOnDescendantFocusChangeListener(@NonNull OnFocusChangeListener userFocusListener) {
unsetOnDescendantFocusChangeListener();
final ViewTreeObserver observer = getViewTreeObserver();
if (observer.isAlive() && activeFocusListener == null) {
activeFocusListener =
new ViewTreeObserver.OnGlobalFocusChangeListener() {
@Override
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
userFocusListener.onFocusChange(
PlatformViewWrapper.this, ViewUtils.childHasFocus(PlatformViewWrapper.this));
}
};
observer.addOnGlobalFocusChangeListener(activeFocusListener);
}
}
public void unsetOnDescendantFocusChangeListener() {
final ViewTreeObserver observer = getViewTreeObserver();
if (observer.isAlive() && activeFocusListener != null) {
final ViewTreeObserver.OnGlobalFocusChangeListener currFocusListener = activeFocusListener;
activeFocusListener = null;
observer.removeOnGlobalFocusChangeListener(currFocusListener);
}
}
}

View File

@@ -5,6 +5,7 @@
package io.flutter.plugin.platform;
import android.view.View;
import androidx.annotation.Nullable;
import io.flutter.view.AccessibilityBridge;
/** Facilitates interaction between the accessibility bridge and embedded platform views. */
@@ -13,10 +14,8 @@ public interface PlatformViewsAccessibilityDelegate {
* Returns the root of the view hierarchy for the platform view with the requested id, or null if
* there is no corresponding view.
*/
View getPlatformViewById(Integer id);
/** Returns true if the platform view uses virtual displays. */
boolean usesVirtualDisplay(Integer id);
@Nullable
View getPlatformViewById(int viewId);
/**
* Attaches an accessibility bridge for this platform views accessibility delegate.

View File

@@ -10,7 +10,6 @@ import static android.view.MotionEvent.PointerProperties;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.DisplayMetrics;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.View;
@@ -34,7 +33,6 @@ import io.flutter.plugin.editing.TextInputPlugin;
import io.flutter.view.AccessibilityBridge;
import io.flutter.view.TextureRegistry;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -70,20 +68,7 @@ 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 platform view hosted in this virtual display.
// Since each virtual display has it's unique context this allows associating any view with the
// platform view that
// it is associated with(e.g if a platform view creates other views in the same virtual display.
@VisibleForTesting /* package */ final HashMap<Context, View> contextToPlatformView;
// The views returned by `PlatformView#getView()`.
//
// This only applies to hybrid composition.
// The platform views.
private final SparseArray<PlatformView> platformViews;
// The platform view parents that are appended to `FlutterView`.
@@ -93,12 +78,19 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
// This view provides a wrapper that applies scene builder operations to the platform view.
// For example, a transform matrix, or setting opacity on the platform view layer.
//
// This is only applies to hybrid composition.
// This is only applies to hybrid composition (PlatformViewLayer render).
// TODO(egarciad): Eliminate this.
// https://github.com/flutter/flutter/issues/96679
private final SparseArray<FlutterMutatorView> platformViewParent;
// Map of unique IDs to views that render overlay layers.
private final SparseArray<FlutterImageView> overlayLayerViews;
// View wrappers are FrameLayouts that contain a single child view.
// This child view is the platform view.
// This only applies to hybrid composition (TextureLayer render).
private final SparseArray<PlatformViewWrapper> viewWrappers;
// Next available unique ID for use in overlayLayerViews.
private int nextOverlayLayerId = 0;
@@ -124,7 +116,9 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public void createAndroidViewForPlatformView(
// 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);
@@ -154,167 +148,182 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
platformViews.put(request.viewId, platformView);
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public void disposeAndroidViewForPlatformView(int viewId) {
// Hybrid view.
final PlatformView platformView = platformViews.get(viewId);
final FlutterMutatorView parentView = platformViewParent.get(viewId);
if (platformView != null) {
if (parentView != null) {
parentView.removeView(platformView.getView());
}
platformViews.remove(viewId);
platformView.dispose();
}
if (parentView != null) {
parentView.unsetOnDescendantFocusChangeListener();
((ViewGroup) parentView.getParent()).removeView(parentView);
platformViewParent.remove(viewId);
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
@Override
public long createVirtualDisplayForPlatformView(
public long createForTextureLayer(
@NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
// API level 20 is required for VirtualDisplay#setSurface which we use when resizing a
// platform view.
ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
final int viewId = request.viewId;
if (viewWrappers.get(viewId) != null) {
throw new IllegalStateException(
"Trying to create an already created platform view, view id: " + viewId);
}
if (!validateDirection(request.direction)) {
throw new IllegalStateException(
"Trying to create a view with unknown direction value: "
+ request.direction
+ "(view id: "
+ request.viewId
+ viewId
+ ")");
}
if (vdControllers.containsKey(request.viewId)) {
if (textureRegistry == null) {
throw new IllegalStateException(
"Trying to create an already created platform view, view id: " + request.viewId);
"Texture registry is null. This means that platform views controller was detached, view id: "
+ viewId);
}
PlatformViewFactory viewFactory = registry.getFactory(request.viewType);
if (flutterView == null) {
throw new IllegalStateException(
"Flutter view is null. This means the platform views controller doesn't have an attached view, view id: "
+ viewId);
}
final PlatformViewFactory viewFactory = registry.getFactory(request.viewType);
if (viewFactory == null) {
throw new IllegalStateException(
"Trying to create a platform view of unregistered type: " + request.viewType);
}
Object createParams = null;
if (request.params != null) {
createParams = viewFactory.getCreateArgsCodec().decodeMessage(request.params);
}
int physicalWidth = toPhysicalPixels(request.logicalWidth);
int physicalHeight = toPhysicalPixels(request.logicalHeight);
validateVirtualDisplayDimensions(physicalWidth, physicalHeight);
final PlatformView platformView = viewFactory.create(context, viewId, createParams);
platformViews.put(viewId, platformView);
TextureRegistry.SurfaceTextureEntry textureEntry = textureRegistry.createSurfaceTexture();
VirtualDisplayController vdController =
VirtualDisplayController.create(
context,
accessibilityEventsDelegate,
viewFactory,
textureEntry,
physicalWidth,
physicalHeight,
request.viewId,
createParams,
(view, hasFocus) -> {
if (hasFocus) {
platformViewsChannel.invokeViewFocused(request.viewId);
}
});
final PlatformViewWrapper wrapperView = new PlatformViewWrapper(context);
final TextureRegistry.SurfaceTextureEntry textureEntry =
textureRegistry.createSurfaceTexture();
wrapperView.setTexture(textureEntry.surfaceTexture());
wrapperView.setTouchProcessor(androidTouchProcessor);
if (vdController == null) {
throw new IllegalStateException(
"Failed creating virtual display for a "
+ request.viewType
+ " with id: "
+ request.viewId);
}
final int physicalWidth = toPhysicalPixels(request.logicalWidth);
final int physicalHeight = toPhysicalPixels(request.logicalHeight);
wrapperView.setBufferSize(physicalWidth, physicalHeight);
// 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);
}
final FrameLayout.LayoutParams layoutParams =
new FrameLayout.LayoutParams(physicalWidth, physicalHeight);
vdControllers.put(request.viewId, vdController);
View platformView = vdController.getView();
platformView.setLayoutDirection(request.direction);
contextToPlatformView.put(platformView.getContext(), platformView);
final int physicalTop = toPhysicalPixels(request.logicalTop);
final int physicalLeft = toPhysicalPixels(request.logicalLeft);
layoutParams.topMargin = physicalTop;
layoutParams.leftMargin = physicalLeft;
wrapperView.setLayoutParams(layoutParams);
// TODO(amirh): copy accessibility nodes to the FlutterView's accessibility tree.
wrapperView.setLayoutDirection(request.direction);
wrapperView.addView(platformView.getView());
wrapperView.setOnDescendantFocusChangeListener(
(view, hasFocus) -> {
if (hasFocus) {
platformViewsChannel.invokeViewFocused(viewId);
} else if (textInputPlugin != null) {
textInputPlugin.clearPlatformViewClient(viewId);
}
});
flutterView.addView(wrapperView);
viewWrappers.append(viewId, wrapperView);
return textureEntry.id();
}
@Override
public void disposeVirtualDisplayForPlatformView(int viewId) {
ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
VirtualDisplayController vdController = vdControllers.get(viewId);
if (vdController == null) {
throw new IllegalStateException(
"Trying to dispose a platform view with unknown id: " + viewId);
public void dispose(int viewId) {
final PlatformView platformView = platformViews.get(viewId);
if (platformView != null) {
final ViewGroup pvParent = (ViewGroup) platformView.getView().getParent();
if (pvParent != null) {
pvParent.removeView(platformView.getView());
}
platformViews.remove(viewId);
platformView.dispose();
}
// The platform view is displayed using a TextureLayer.
final PlatformViewWrapper viewWrapper = viewWrappers.get(viewId);
if (viewWrapper != null) {
viewWrapper.release();
viewWrapper.unsetOnDescendantFocusChangeListener();
if (textInputPlugin != null) {
textInputPlugin.clearPlatformViewClient(viewId);
final ViewGroup wrapperParent = (ViewGroup) viewWrapper.getParent();
if (wrapperParent != null) {
wrapperParent.removeView(viewWrapper);
}
viewWrappers.remove(viewId);
return;
}
// The platform view is displayed using a PlatformViewLayer.
// TODO(egarciad): Eliminate this case.
// https://github.com/flutter/flutter/issues/96679
final FlutterMutatorView parentView = platformViewParent.get(viewId);
if (parentView != null) {
parentView.unsetOnDescendantFocusChangeListener();
contextToPlatformView.remove(vdController.getView().getContext());
vdController.dispose();
vdControllers.remove(viewId);
final ViewGroup mutatorViewParent = (ViewGroup) parentView.getParent();
if (mutatorViewParent != null) {
mutatorViewParent.removeView(parentView);
}
platformViewParent.remove(viewId);
}
}
@Override
public void resizePlatformView(
@NonNull PlatformViewsChannel.PlatformViewResizeRequest request,
@NonNull Runnable onComplete) {
ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
public void offset(int viewId, double top, double left) {
final PlatformViewWrapper wrapper = viewWrappers.get(viewId);
if (wrapper == 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();
layoutParams.topMargin = physicalTop;
layoutParams.leftMargin = physicalLeft;
wrapper.setLayoutParams(layoutParams);
}
final VirtualDisplayController vdController = vdControllers.get(request.viewId);
if (vdController == null) {
throw new IllegalStateException(
"Trying to resize a platform view with unknown id: " + request.viewId);
@Override
public PlatformViewsChannel.PlatformViewBufferSize resize(
@NonNull PlatformViewsChannel.PlatformViewResizeRequest request) {
final int viewId = request.viewId;
final PlatformViewWrapper view = viewWrappers.get(viewId);
if (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);
// Resize the buffer only when the current buffer size is smaller than the new size.
// This is required to prevent a situation when smooth keyboard animation
// resizes the texture too often, such that the GPU and the platform thread don't agree on
// the
// timing of the new size.
// Resizing the texture causes pixel stretching since the size of the GL texture used in
// the engine
// is set by the framework, but the texture buffer size is set by the platform down below.
if (newWidth > view.getBufferWidth() || newHeight > view.getBufferHeight()) {
view.setBufferSize(newWidth, newHeight);
}
int physicalWidth = toPhysicalPixels(request.newLogicalWidth);
int physicalHeight = toPhysicalPixels(request.newLogicalHeight);
validateVirtualDisplayDimensions(physicalWidth, physicalHeight);
final FrameLayout.LayoutParams layoutParams =
(FrameLayout.LayoutParams) view.getLayoutParams();
layoutParams.width = newWidth;
layoutParams.height = newHeight;
view.setLayoutParams(layoutParams);
// 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();
});
return new PlatformViewsChannel.PlatformViewBufferSize(
toLogicalPixels(view.getBufferWidth()), toLogicalPixels(view.getBufferHeight()));
}
@Override
public void onTouch(@NonNull PlatformViewsChannel.PlatformViewTouch touch) {
final int viewId = touch.viewId;
float density = context.getResources().getDisplayMetrics().density;
ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
if (vdControllers.containsKey(viewId)) {
final MotionEvent event = toMotionEvent(density, touch, /*usingVirtualDiplays=*/ true);
vdControllers.get(touch.viewId).dispatchTouchEvent(event);
} else if (platformViews.get(viewId) != null) {
final MotionEvent event = toMotionEvent(density, touch, /*usingVirtualDiplays=*/ false);
View view = platformViews.get(touch.viewId).getView();
if (view != null) {
view.dispatchTouchEvent(event);
}
} else {
throw new IllegalStateException("Sending touch to an unknown view with id: " + viewId);
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);
platformView.getView().dispatchTouchEvent(event);
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
@@ -328,34 +337,23 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
+ viewId
+ ")");
}
ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
final PlatformView platformView = platformViews.get(viewId);
if (platformView != null) {
platformView.getView().setLayoutDirection(direction);
if (platformView == null) {
Log.e(TAG, "Setting direction to an unknown view with id: " + viewId);
return;
}
VirtualDisplayController controller = vdControllers.get(viewId);
if (controller == null) {
throw new IllegalStateException(
"Trying to set direction: "
+ direction
+ " to an unknown platform view with id: "
+ viewId);
}
controller.getView().setLayoutDirection(direction);
ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
platformViews.get(viewId).getView().setLayoutDirection(direction);
}
@Override
public void clearFocus(int viewId) {
final PlatformView platformView = platformViews.get(viewId);
if (platformView != null) {
platformView.getView().clearFocus();
if (platformView == null) {
Log.e(TAG, "Clearing focus on an unknown view with id: " + viewId);
return;
}
ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
View view = vdControllers.get(viewId).getView();
view.clearFocus();
platformView.getView().clearFocus();
}
private void ensureValidAndroidVersion(int minSdkVersion) {
@@ -375,8 +373,7 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
};
@VisibleForTesting
public MotionEvent toMotionEvent(
float density, PlatformViewsChannel.PlatformViewTouch touch, boolean usingVirtualDiplays) {
public MotionEvent toMotionEvent(float density, PlatformViewsChannel.PlatformViewTouch touch) {
MotionEventTracker.MotionEventId motionEventId =
MotionEventTracker.MotionEventId.from(touch.motionEventId);
MotionEvent trackedEvent = motionEventTracker.pop(motionEventId);
@@ -392,7 +389,7 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
parsePointerCoordsList(touch.rawPointerCoords, density)
.toArray(new PointerCoords[touch.pointerCount]);
if (!usingVirtualDiplays && trackedEvent != null) {
if (trackedEvent != null) {
return MotionEvent.obtain(
trackedEvent.getDownTime(),
trackedEvent.getEventTime(),
@@ -431,13 +428,11 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
public PlatformViewsController() {
registry = new PlatformViewRegistryImpl();
vdControllers = new HashMap<>();
accessibilityEventsDelegate = new AccessibilityEventsDelegate();
contextToPlatformView = new HashMap<>();
overlayLayerViews = new SparseArray<>();
currentFrameUsedOverlayLayerIds = new HashSet<>();
currentFrameUsedPlatformViewIds = new HashSet<>();
viewWrappers = new SparseArray<>();
platformViews = new SparseArray<>();
platformViewParent = new SparseArray<>();
@@ -489,13 +484,14 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
* This {@code PlatformViewsController} and its {@code FlutterEngine} is now attached to an
* Android {@code View} that renders a Flutter UI.
*/
public void attachToView(@NonNull FlutterView flutterView) {
this.flutterView = flutterView;
public void attachToView(@NonNull FlutterView newFlutterView) {
flutterView = newFlutterView;
// Inform all existing platform views that they are now associated with
// a Flutter View.
for (VirtualDisplayController controller : vdControllers.values()) {
controller.onFlutterViewAttached(flutterView);
for (int i = 0; i < platformViews.size(); i++) {
final PlatformView view = platformViews.valueAt(i);
view.onFlutterViewAttached(flutterView);
}
}
@@ -507,16 +503,16 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
* the previously attached {@code View}.
*/
public void detachFromView() {
for (int i = 0; i < platformViews.size(); i++) {
final PlatformView view = platformViews.valueAt(i);
view.onFlutterViewDetached();
}
// TODO(egarciad): Remove this.
// https://github.com/flutter/flutter/issues/96679
destroyOverlaySurfaces();
removeOverlaySurfaces();
this.flutterView = null;
flutterView = null;
flutterViewConvertedToImageView = false;
// Inform all existing platform views that they are no longer associated with
// a Flutter View.
for (VirtualDisplayController controller : vdControllers.values()) {
controller.onFlutterViewDetached();
}
}
@Override
@@ -547,29 +543,6 @@ 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 (!contextToPlatformView.containsKey(view.getContext())) {
return false;
}
View platformView = contextToPlatformView.get(view.getContext());
if (platformView == view) {
return true;
}
return platformView.checkInputConnectionProxy(view);
}
public PlatformViewRegistry getRegistry() {
return registry;
}
@@ -587,8 +560,6 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
* PlatformViewsController} detaches from JNI.
*/
public void onDetachedFromJNI() {
// Dispose all virtual displays so that any future updates to textures will not be
// propagated to the native peer.
flushAllViews();
}
@@ -597,37 +568,12 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
}
@Override
public View getPlatformViewById(Integer id) {
// Hybrid composition.
if (platformViews.get(id) != null) {
return platformViews.get(id).getView();
}
VirtualDisplayController controller = vdControllers.get(id);
if (controller == null) {
public View getPlatformViewById(int viewId) {
final PlatformView platformView = platformViews.get(viewId);
if (platformView == null) {
return null;
}
return controller.getView();
}
@Override
public boolean usesVirtualDisplay(Integer 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();
return platformView.getView();
}
private static boolean validateDirection(int direction) {
@@ -679,29 +625,6 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
return coords;
}
// Creating a VirtualDisplay larger than the size of the device screen size
// could cause the device to restart: https://github.com/flutter/flutter/issues/28978
private void validateVirtualDisplayDimensions(int width, int height) {
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
if (height > metrics.heightPixels || width > metrics.widthPixels) {
String message =
"Creating a virtual display of size: "
+ "["
+ width
+ ", "
+ height
+ "] may result in problems"
+ "(https://github.com/flutter/flutter/issues/2897)."
+ "It is larger than the device screen size: "
+ "["
+ metrics.widthPixels
+ ", "
+ metrics.heightPixels
+ "].";
Log.w(TAG, message);
}
}
private float getDisplayDensity() {
return context.getResources().getDisplayMetrics().density;
}
@@ -710,18 +633,13 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
return (int) Math.round(logicalPixels * getDisplayDensity());
}
private int toLogicalPixels(double physicalPixels) {
return (int) Math.round(physicalPixels / getDisplayDensity());
}
private void flushAllViews() {
for (VirtualDisplayController controller : vdControllers.values()) {
controller.dispose();
}
vdControllers.clear();
while (platformViews.size() > 0) {
channelHandler.disposeAndroidViewForPlatformView(platformViews.keyAt(0));
}
if (contextToPlatformView.size() > 0) {
contextToPlatformView.clear();
channelHandler.dispose(platformViews.keyAt(0));
}
}

View File

@@ -1,478 +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 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.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;
}
private final PlatformViewFactory viewFactory;
// 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,
PlatformViewFactory viewFactory,
AccessibilityEventsDelegate accessibilityEventsDelegate,
int viewId,
Object createParams,
OnFocusChangeListener focusChangeListener) {
super(new ImmContext(outerContext), display);
this.viewFactory = viewFactory;
this.accessibilityEventsDelegate = accessibilityEventsDelegate;
this.viewId = viewId;
this.createParams = createParams;
this.focusChangeListener = focusChangeListener;
this.outerContext = outerContext;
state = new PresentationState();
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;
viewFactory = null;
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 context =
new PresentationContext(getContext(), state.windowManagerHandler, outerContext);
if (state.platformView == null) {
state.platformView = viewFactory.create(context, viewId, createParams);
}
View embeddedView = state.platformView.getView();
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

@@ -1,249 +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 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.os.Build;
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.view.TextureRegistry;
@TargetApi(Build.VERSION_CODES.KITKAT_WATCH)
class VirtualDisplayController {
public static VirtualDisplayController create(
Context context,
AccessibilityEventsDelegate accessibilityEventsDelegate,
PlatformViewFactory viewFactory,
TextureRegistry.SurfaceTextureEntry textureEntry,
int width,
int height,
int viewId,
Object createParams,
OnFocusChangeListener focusChangeListener) {
textureEntry.surfaceTexture().setDefaultBufferSize(width, height);
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", width, height, densityDpi, surface, 0);
if (virtualDisplay == null) {
return null;
}
return new VirtualDisplayController(
context,
accessibilityEventsDelegate,
virtualDisplay,
viewFactory,
surface,
textureEntry,
focusChangeListener,
viewId,
createParams);
}
private final Context context;
private final AccessibilityEventsDelegate accessibilityEventsDelegate;
private final int densityDpi;
private final TextureRegistry.SurfaceTextureEntry textureEntry;
private final OnFocusChangeListener focusChangeListener;
private VirtualDisplay virtualDisplay;
@VisibleForTesting SingleViewPresentation presentation;
private final Surface surface;
private VirtualDisplayController(
Context context,
AccessibilityEventsDelegate accessibilityEventsDelegate,
VirtualDisplay virtualDisplay,
PlatformViewFactory viewFactory,
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(),
viewFactory,
accessibilityEventsDelegate,
viewId,
createParams,
focusChangeListener);
presentation.show();
}
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();
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() {
PlatformView view = presentation.getView();
// Fix rare crash on HuaWei device described in: https://github.com/flutter/engine/pull/9192
presentation.cancel();
presentation.detachState();
view.dispose();
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);
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
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

@@ -9,6 +9,8 @@ import android.content.Context;
import android.content.ContextWrapper;
import android.os.Build;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
public final class ViewUtils {
/**
@@ -45,4 +47,28 @@ public final class ViewUtils {
}
return fallbackId;
}
/**
* Determines if the current view or any descendant view has focus.
*
* @param root The root view.
* @return True if the current view or any descendant view has focus.
*/
public static boolean childHasFocus(@Nullable View root) {
if (root == null) {
return false;
}
if (root.hasFocus()) {
return true;
}
if (root instanceof ViewGroup) {
final ViewGroup viewGroup = (ViewGroup) root;
for (int idx = 0; idx < viewGroup.getChildCount(); idx++) {
if (childHasFocus(viewGroup.getChildAt(idx))) {
return true;
}
}
}
return false;
}
}

View File

@@ -573,23 +573,6 @@ 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) {
View embeddedView =
platformViewsAccessibilityDelegate.getPlatformViewById(semanticsNode.platformViewId);
if (platformViewsAccessibilityDelegate.usesVirtualDisplay(semanticsNode.platformViewId)) {
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
@@ -904,17 +887,10 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
// Add the embedded view as a child of the current accessibility node if it's using
// hybrid composition.
//
// 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(embeddedView);
} else {
result.addChild(rootAccessibilityView, child.id);
}
result.addChild(rootAccessibilityView, child.id);
}
return result;
}
@@ -1545,8 +1521,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
if (semanticsNode.hadPreviousConfig) {
updated.add(semanticsNode);
}
if (semanticsNode.platformViewId != -1
&& !platformViewsAccessibilityDelegate.usesVirtualDisplay(semanticsNode.platformViewId)) {
if (semanticsNode.platformViewId != -1) {
View embeddedView =
platformViewsAccessibilityDelegate.getPlatformViewById(semanticsNode.platformViewId);
if (embeddedView != null) {
@@ -1958,9 +1933,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
embeddedAccessibilityFocusedNodeId = null;
}
if (semanticsNodeToBeRemoved.platformViewId != -1
&& !platformViewsAccessibilityDelegate.usesVirtualDisplay(
semanticsNodeToBeRemoved.platformViewId)) {
if (semanticsNodeToBeRemoved.platformViewId != -1) {
View embeddedView =
platformViewsAccessibilityDelegate.getPlatformViewById(
semanticsNodeToBeRemoved.platformViewId);

View File

@@ -26,7 +26,6 @@ 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;
@@ -427,14 +426,6 @@ 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

@@ -6,8 +6,6 @@ import static org.mockito.Mockito.*;
import android.graphics.Matrix;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import io.flutter.embedding.android.AndroidTouchProcessor;
@@ -81,49 +79,6 @@ public class FlutterMutatorViewTest {
}
}
@Test
public void childHasFocus_rootHasFocus() {
final View rootView = mock(View.class);
when(rootView.hasFocus()).thenReturn(true);
assertTrue(FlutterMutatorView.childHasFocus(rootView));
}
@Test
public void childHasFocus_rootDoesNotHaveFocus() {
final View rootView = mock(View.class);
when(rootView.hasFocus()).thenReturn(false);
assertFalse(FlutterMutatorView.childHasFocus(rootView));
}
@Test
public void childHasFocus_rootIsNull() {
assertFalse(FlutterMutatorView.childHasFocus(null));
}
@Test
public void childHasFocus_childHasFocus() {
final View childView = mock(View.class);
when(childView.hasFocus()).thenReturn(true);
final ViewGroup rootView = mock(ViewGroup.class);
when(rootView.getChildCount()).thenReturn(1);
when(rootView.getChildAt(0)).thenReturn(childView);
assertTrue(FlutterMutatorView.childHasFocus(rootView));
}
@Test
public void childHasFocus_childDoesNotHaveFocus() {
final View childView = mock(View.class);
when(childView.hasFocus()).thenReturn(false);
final ViewGroup rootView = mock(ViewGroup.class);
when(rootView.getChildCount()).thenReturn(1);
when(rootView.getChildAt(0)).thenReturn(childView);
assertFalse(FlutterMutatorView.childHasFocus(rootView));
}
@Test
public void focusChangeListener_hasFocus() {
final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);

View File

@@ -0,0 +1,265 @@
package io.flutter.plugin.platform;
import static android.view.View.OnFocusChangeListener;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.BlendMode;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.SurfaceTexture;
import android.view.Surface;
import android.view.View;
import android.view.ViewTreeObserver;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.robolectric.RuntimeEnvironment;
@TargetApi(31)
@RunWith(AndroidJUnit4.class)
public class PlatformViewWrapperTest {
@Test
public void setTexture_writesToBuffer() {
final Surface surface = mock(Surface.class);
final Context ctx = ApplicationProvider.getApplicationContext();
final PlatformViewWrapper wrapper =
new PlatformViewWrapper(ctx) {
@Override
protected Surface createSurface(@NonNull SurfaceTexture tx) {
return surface;
}
};
final SurfaceTexture tx = mock(SurfaceTexture.class);
when(tx.isReleased()).thenReturn(false);
final Canvas canvas = mock(Canvas.class);
when(surface.lockHardwareCanvas()).thenReturn(canvas);
// Test.
wrapper.setTexture(tx);
// Verify.
verify(surface, times(1)).lockHardwareCanvas();
verify(surface, times(1)).unlockCanvasAndPost(canvas);
verify(canvas, times(1)).drawColor(Color.TRANSPARENT, BlendMode.CLEAR);
verifyNoMoreInteractions(surface);
verifyNoMoreInteractions(canvas);
}
@Test
public void draw_writesToBuffer() {
final Surface surface = mock(Surface.class);
final Context ctx = ApplicationProvider.getApplicationContext();
final PlatformViewWrapper wrapper =
new PlatformViewWrapper(ctx) {
@Override
protected Surface createSurface(@NonNull SurfaceTexture tx) {
return surface;
}
};
wrapper.addView(
new View(ctx) {
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
canvas.drawColor(Color.RED);
}
});
final int size = 100;
wrapper.measure(size, size);
wrapper.layout(0, 0, size, size);
final SurfaceTexture tx = mock(SurfaceTexture.class);
when(tx.isReleased()).thenReturn(false);
when(surface.lockHardwareCanvas()).thenReturn(mock(Canvas.class));
wrapper.setTexture(tx);
reset(surface);
final Canvas canvas = mock(Canvas.class);
when(surface.lockHardwareCanvas()).thenReturn(canvas);
when(surface.isValid()).thenReturn(true);
// Test.
wrapper.invalidate();
wrapper.draw(new Canvas());
// Verify.
verify(canvas, times(1)).drawColor(Color.TRANSPARENT, BlendMode.CLEAR);
verify(surface, times(1)).isValid();
verify(surface, times(1)).lockHardwareCanvas();
verify(surface, times(1)).unlockCanvasAndPost(canvas);
verifyNoMoreInteractions(surface);
verifyNoMoreInteractions(canvas);
}
@Test
public void release() {
final Surface surface = mock(Surface.class);
final Context ctx = ApplicationProvider.getApplicationContext();
final PlatformViewWrapper wrapper =
new PlatformViewWrapper(ctx) {
@Override
protected Surface createSurface(@NonNull SurfaceTexture tx) {
return surface;
}
};
final SurfaceTexture tx = mock(SurfaceTexture.class);
when(tx.isReleased()).thenReturn(false);
final Canvas canvas = mock(Canvas.class);
when(surface.lockHardwareCanvas()).thenReturn(canvas);
wrapper.setTexture(tx);
reset(surface);
reset(tx);
// Test.
wrapper.release();
// Verify.
verify(surface, times(1)).release();
verifyNoMoreInteractions(surface);
verifyNoMoreInteractions(tx);
}
@Test
public void focusChangeListener_hasFocus() {
final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
when(viewTreeObserver.isAlive()).thenReturn(true);
final PlatformViewWrapper view =
new PlatformViewWrapper(RuntimeEnvironment.application) {
@Override
public ViewTreeObserver getViewTreeObserver() {
return viewTreeObserver;
}
@Override
public boolean hasFocus() {
return true;
}
};
final OnFocusChangeListener focusListener = mock(OnFocusChangeListener.class);
view.setOnDescendantFocusChangeListener(focusListener);
final ArgumentCaptor<ViewTreeObserver.OnGlobalFocusChangeListener> focusListenerCaptor =
ArgumentCaptor.forClass(ViewTreeObserver.OnGlobalFocusChangeListener.class);
verify(viewTreeObserver).addOnGlobalFocusChangeListener(focusListenerCaptor.capture());
focusListenerCaptor.getValue().onGlobalFocusChanged(null, null);
verify(focusListener).onFocusChange(view, true);
}
@Test
public void focusChangeListener_doesNotHaveFocus() {
final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
when(viewTreeObserver.isAlive()).thenReturn(true);
final PlatformViewWrapper view =
new PlatformViewWrapper(RuntimeEnvironment.application) {
@Override
public ViewTreeObserver getViewTreeObserver() {
return viewTreeObserver;
}
@Override
public boolean hasFocus() {
return false;
}
};
final OnFocusChangeListener focusListener = mock(OnFocusChangeListener.class);
view.setOnDescendantFocusChangeListener(focusListener);
final ArgumentCaptor<ViewTreeObserver.OnGlobalFocusChangeListener> focusListenerCaptor =
ArgumentCaptor.forClass(ViewTreeObserver.OnGlobalFocusChangeListener.class);
verify(viewTreeObserver).addOnGlobalFocusChangeListener(focusListenerCaptor.capture());
focusListenerCaptor.getValue().onGlobalFocusChanged(null, null);
verify(focusListener).onFocusChange(view, false);
}
@Test
public void focusChangeListener_viewTreeObserverIsAliveFalseDoesNotThrow() {
final PlatformViewWrapper view =
new PlatformViewWrapper(RuntimeEnvironment.application) {
@Override
public ViewTreeObserver getViewTreeObserver() {
final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
when(viewTreeObserver.isAlive()).thenReturn(false);
return viewTreeObserver;
}
};
view.setOnDescendantFocusChangeListener(mock(OnFocusChangeListener.class));
}
@Test
public void setOnDescendantFocusChangeListener_keepsSingleListener() {
final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
when(viewTreeObserver.isAlive()).thenReturn(true);
final PlatformViewWrapper view =
new PlatformViewWrapper(RuntimeEnvironment.application) {
@Override
public ViewTreeObserver getViewTreeObserver() {
return viewTreeObserver;
}
};
assertNull(view.activeFocusListener);
view.setOnDescendantFocusChangeListener(mock(OnFocusChangeListener.class));
assertNotNull(view.activeFocusListener);
final ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener =
view.activeFocusListener;
view.setOnDescendantFocusChangeListener(mock(OnFocusChangeListener.class));
assertNotNull(view.activeFocusListener);
verify(viewTreeObserver, times(1)).removeOnGlobalFocusChangeListener(activeFocusListener);
}
@Test
public void unsetOnDescendantFocusChangeListener_removesActiveListener() {
final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
when(viewTreeObserver.isAlive()).thenReturn(true);
final PlatformViewWrapper view =
new PlatformViewWrapper(RuntimeEnvironment.application) {
@Override
public ViewTreeObserver getViewTreeObserver() {
return viewTreeObserver;
}
};
assertNull(view.activeFocusListener);
view.setOnDescendantFocusChangeListener(mock(OnFocusChangeListener.class));
assertNotNull(view.activeFocusListener);
final ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener =
view.activeFocusListener;
view.unsetOnDescendantFocusChangeListener();
assertNull(view.activeFocusListener);
view.unsetOnDescendantFocusChangeListener();
verify(viewTreeObserver, times(1)).removeOnGlobalFocusChangeListener(activeFocusListener);
}
}

View File

@@ -17,7 +17,6 @@ import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewParent;
import android.widget.FrameLayout.LayoutParams;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import io.flutter.embedding.android.FlutterImageView;
import io.flutter.embedding.android.FlutterView;
@@ -56,120 +55,6 @@ import org.robolectric.shadows.ShadowSurfaceView;
@RunWith(AndroidJUnit4.class)
public class PlatformViewsControllerTest {
@Ignore
@Test
public void itNotifiesVirtualDisplayControllersOfViewAttachmentAndDetachment() {
// Setup test structure.
// Create a fake View that represents the View that renders a Flutter UI.
FlutterView fakeFlutterView = new FlutterView(RuntimeEnvironment.systemContext);
// 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(RuntimeEnvironment.systemContext);
// 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() {
@@ -209,11 +94,7 @@ public class PlatformViewsControllerTest {
motionEventId.getId());
MotionEvent resolvedEvent =
platformViewsController.toMotionEvent(
1, // density
frameWorkTouch,
false // usingVirtualDisplays
);
platformViewsController.toMotionEvent(/*density=*/ 1, frameWorkTouch);
assertNotEquals(resolvedEvent.getAction(), frameWorkTouch.action);
assertEquals(resolvedEvent.getAction(), original.getAction());
@@ -297,66 +178,6 @@ 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);
View pv = mock(View.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.contextToPlatformView.isEmpty());
platformViewsController.onDetachedFromJNI();
assertTrue(platformViewsController.contextToPlatformView.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);
View pv = mock(View.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.contextToPlatformView.isEmpty());
platformViewsController.onDetachedFromJNI();
assertTrue(platformViewsController.contextToPlatformView.isEmpty());
}
@Test
@Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class})
public void createPlatformViewMessage__throwsIfViewHasParent() {
@@ -772,13 +593,6 @@ 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

@@ -1,82 +0,0 @@
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

@@ -5,11 +5,16 @@
package io.flutter.util;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.view.View;
import android.view.ViewGroup;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -30,4 +35,47 @@ public class ViewUtilsTest {
ContextWrapper wrapper = new ContextWrapper(new ContextWrapper(activity));
assertEquals(activity, ViewUtils.getActivity(wrapper));
}
@Test
public void childHasFocus_rootHasFocus() {
final View rootView = mock(View.class);
when(rootView.hasFocus()).thenReturn(true);
assertTrue(ViewUtils.childHasFocus(rootView));
}
@Test
public void childHasFocus_rootDoesNotHaveFocus() {
final View rootView = mock(View.class);
when(rootView.hasFocus()).thenReturn(false);
assertFalse(ViewUtils.childHasFocus(rootView));
}
@Test
public void childHasFocus_rootIsNull() {
assertFalse(ViewUtils.childHasFocus(null));
}
@Test
public void childHasFocus_childHasFocus() {
final View childView = mock(View.class);
when(childView.hasFocus()).thenReturn(true);
final ViewGroup rootView = mock(ViewGroup.class);
when(rootView.getChildCount()).thenReturn(1);
when(rootView.getChildAt(0)).thenReturn(childView);
assertTrue(ViewUtils.childHasFocus(rootView));
}
@Test
public void childHasFocus_childDoesNotHaveFocus() {
final View childView = mock(View.class);
when(childView.hasFocus()).thenReturn(false);
final ViewGroup rootView = mock(ViewGroup.class);
when(rootView.getChildCount()).thenReturn(1);
when(rootView.getChildAt(0)).thenReturn(childView);
assertFalse(ViewUtils.childHasFocus(rootView));
}
}

View File

@@ -1467,7 +1467,6 @@ 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);
@@ -1505,7 +1504,6 @@ 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);
@@ -1540,7 +1538,6 @@ 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);
@@ -1555,34 +1552,6 @@ 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

@@ -133,17 +133,6 @@
column="27"/>
</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>
<issue
id="ClickableViewAccessibility"
message="Custom view `FlutterView` overrides `onTouchEvent` but not `performClick`"

View File

@@ -11,7 +11,6 @@
<src file="../../../flutter/shell/platform/android/test/io/flutter/util/PreconditionsTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugins/GeneratedPluginRegistrant.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/FlutterTestSuite.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/android/KeyChannelResponderTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterTextureViewTest.java" />
@@ -40,20 +39,21 @@
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/TextInputChannelTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/DeferredComponentChannelTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/AccessibilityChannelTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/KeyEventChannelTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/RestorationChannelTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterShellArgsTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManagerTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewWrapperTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/mouse/MouseCursorPluginTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/common/StandardMessageCodecTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/common/BinaryCodecTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/common/StandardMethodCodecTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/editing/ListenableEditingStateTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/editing/TextEditingDeltaTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/localization/LocalizationPluginTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java" />
@@ -151,13 +151,12 @@
<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" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/mouse/MouseCursorPlugin.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java" />
@@ -178,8 +177,8 @@
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/MethodCall.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/EventChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/MethodChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextEditingDelta.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextEditingDelta.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/ListenableEditingState.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java" />