diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index 4c28df56f499f..d004764ca7ec7 100755
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -1189,9 +1189,10 @@ 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
diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn
index 41f9e3aed0ec7..ac44391e0d85a 100644
--- a/shell/platform/android/BUILD.gn
+++ b/shell/platform/android/BUILD.gn
@@ -268,9 +268,10 @@ 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",
diff --git a/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java b/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java
index 32c6d954d5414..9e712e28043a4 100644
--- a/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java
+++ b/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java
@@ -107,7 +107,7 @@ public boolean onTouchEvent(@NonNull MotionEvent event) {
* the gesture pointers into screen coordinates.
* @return True if the event was handled.
*/
- public boolean onTouchEvent(@NonNull MotionEvent event, @NonNull Matrix transformMatrix) {
+ public boolean onTouchEvent(@NonNull MotionEvent event, Matrix transformMatrix) {
int pointerCount = event.getPointerCount();
// Prepare a data packet of the appropriate size and order.
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java
index cb6269756a9d6..39b4ebd23c704 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java
@@ -791,6 +791,7 @@ navigationBarVisible && guessBottomKeyboardInset(insets) == 0
+ viewportMetrics.viewInsetBottom);
sendViewportMetricsToFlutter();
+
return newInsets;
}
@@ -866,6 +867,21 @@ public InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs) {
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.
+ *
+ *
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.
*
diff --git a/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java b/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java
index 9be312cc53baf..d46f5346d7ab3 100644
--- a/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java
+++ b/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java
@@ -9,13 +9,13 @@
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,6 +49,31 @@ public FlutterMutatorView(@NonNull Context context) {
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;
/**
@@ -70,7 +95,7 @@ public void setOnDescendantFocusChangeListener(@NonNull OnFocusChangeListener us
new ViewTreeObserver.OnGlobalFocusChangeListener() {
@Override
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
- userFocusListener.onFocusChange(mutatorView, ViewUtils.childHasFocus(mutatorView));
+ userFocusListener.onFocusChange(mutatorView, childHasFocus(mutatorView));
}
};
observer.addOnGlobalFocusChangeListener(activeFocusListener);
diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java
index 4b50d01e856d9..87891710bd784 100644
--- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java
+++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java
@@ -14,7 +14,6 @@
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.ByteBuffer;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -65,9 +64,6 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result
case "resize":
resize(call, result);
break;
- case "offset":
- offset(call, result);
- break;
case "touch":
touch(call, result);
break;
@@ -86,40 +82,29 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result
}
private void create(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
- final Map createArgs = call.arguments();
- // TODO(egarciad): Remove the "hybrid" case.
- final boolean usesPlatformViewLayer =
+ Map createArgs = call.arguments();
+ boolean usesHybridComposition =
createArgs.containsKey("hybrid") && (boolean) createArgs.get("hybrid");
- final ByteBuffer additionalParams =
- createArgs.containsKey("params")
- ? ByteBuffer.wrap((byte[]) createArgs.get("params"))
- : null;
+ // 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);
try {
- 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);
+ if (usesHybridComposition) {
+ handler.createAndroidViewForPlatformView(request);
result.success(null);
} else {
- 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);
+ long textureId = handler.createVirtualDisplayForPlatformView(request);
result.success(textureId);
}
} catch (IllegalStateException exception) {
@@ -130,9 +115,15 @@ private void create(@NonNull MethodCall call, @NonNull MethodChannel.Result resu
private void dispose(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
Map disposeArgs = call.arguments();
int viewId = (int) disposeArgs.get("id");
+ boolean usesHybridComposition =
+ disposeArgs.containsKey("hybrid") && (boolean) disposeArgs.get("hybrid");
try {
- handler.dispose(viewId);
+ if (usesHybridComposition) {
+ handler.disposeAndroidViewForPlatformView(viewId);
+ } else {
+ handler.disposeVirtualDisplayForPlatformView(viewId);
+ }
result.success(null);
} catch (IllegalStateException exception) {
result.error("error", detailedExceptionString(exception), null);
@@ -147,28 +138,14 @@ private void resize(@NonNull MethodCall call, @NonNull MethodChannel.Result resu
(double) resizeArgs.get("width"),
(double) resizeArgs.get("height"));
try {
- final PlatformViewBufferSize sz = handler.resize(resizeRequest);
- if (sz == null) {
- result.error("error", "Failed to resize the platform view", null);
- } else {
- final Map 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 offsetArgs = call.arguments();
- try {
- handler.offset(
- (int) offsetArgs.get("id"),
- (double) offsetArgs.get("top"),
- (double) offsetArgs.get("left"));
- result.success(null);
+ handler.resizePlatformView(
+ resizeRequest,
+ new Runnable() {
+ @Override
+ public void run() {
+ result.success(null);
+ }
+ });
} catch (IllegalStateException exception) {
result.error("error", detailedExceptionString(exception), null);
}
@@ -272,40 +249,36 @@ public interface PlatformViewsHandler {
* The Flutter application would like to display a new Android {@code View}, i.e., platform
* view.
*
- * The Android View is added to the view hierarchy. This view is rendered in the Flutter
- * framework by a PlatformViewLayer.
- *
- * @param request The metadata sent from the framework.
+ *
The Android {@code View} is added to the view hierarchy.
*/
- void createForPlatformViewLayer(@NonNull PlatformViewCreationRequest request);
+ void createAndroidViewForPlatformView(@NonNull PlatformViewCreationRequest request);
/**
- * The Flutter application would like to display a new Android {@code View}, i.e., platform
- * view.
- *
- *
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.
+ * The Flutter application would like to dispose of an existing Android {@code View} rendered in
+ * the view hierarchy.
*/
- long createForTextureLayer(@NonNull PlatformViewCreationRequest request);
-
- /** The Flutter application would like to dispose of an existing Android {@code View}. */
- void dispose(int viewId);
+ void disposeAndroidViewForPlatformView(int viewId);
/**
- * The Flutter application would like to resize an existing Android {@code View}.
+ * The Flutter application would like to display a new Android {@code View}.
*
- * @param request The request to resize the platform view.
- * @return The buffer size where the platform view pixels are written to.
+ *
{@code View} is added to a {@code VirtualDisplay}. The framework uses id returned by this
+ * method to lookup the texture in the engine.
+ */
+ long createVirtualDisplayForPlatformView(@NonNull PlatformViewCreationRequest request);
+
+ /**
+ * The Flutter application would like to dispose of an existing Android {@code View} rendered in
+ * a virtual display.
*/
- PlatformViewBufferSize resize(@NonNull PlatformViewResizeRequest request);
+ void disposeVirtualDisplayForPlatformView(int viewId);
/**
- * The Flutter application would like to change the offset of an existing Android {@code View}.
+ * The Flutter application would like to resize an existing Android {@code View}, i.e., platform
+ * view.
*/
- void offset(int viewId, double top, double left);
+ void resizePlatformView(
+ @NonNull PlatformViewResizeRequest request, @NonNull Runnable onComplete);
/**
* The user touched a platform view within Flutter.
@@ -348,12 +321,6 @@ public static class PlatformViewCreationRequest {
/** 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.
*
@@ -365,20 +332,16 @@ public static class PlatformViewCreationRequest {
/** Custom parameters that are unique to the desired platform view. */
@Nullable public final ByteBuffer params;
- /** Creates a request to construct a platform view. */
+ /** Creates a request to construct a platform view that uses a virtual display. */
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;
@@ -386,7 +349,11 @@ public PlatformViewCreationRequest(
}
}
- /** Request sent from Flutter to resize a platform view. */
+ /**
+ * Request sent from Flutter to resize a platform view.
+ *
+ *
This only applies to platform views that use virtual displays.
+ */
public static class PlatformViewResizeRequest {
/** The ID of the platform view as seen by the Flutter side. */
public final int viewId;
@@ -404,20 +371,6 @@ public PlatformViewResizeRequest(int viewId, double newLogicalWidth, double newL
}
}
- /** 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. */
diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java
index 4c497e05d092b..ecdf86c71820e 100644
--- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java
+++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java
@@ -89,7 +89,9 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result
try {
final JSONObject arguments = (JSONObject) args;
final int platformViewId = arguments.getInt("platformViewId");
- textInputMethodHandler.setPlatformViewClient(platformViewId);
+ final boolean usesVirtualDisplay =
+ arguments.optBoolean("usesVirtualDisplay", false);
+ textInputMethodHandler.setPlatformViewClient(platformViewId, usesVirtualDisplay);
result.success(null);
} catch (JSONException exception) {
result.error("error", exception.getMessage(), null);
@@ -400,8 +402,10 @@ public interface TextInputMethodHandler {
* different client is set.
*
* @param id the ID of the platform view to be set as a text input client.
+ * @param usesVirtualDisplay True if the platform view uses a virtual display, false if it uses
+ * hybrid composition.
*/
- void setPlatformViewClient(int id);
+ void setPlatformViewClient(int id, boolean usesVirtualDisplay);
/**
* Sets the size and the transform matrix of the current text input client.
diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
index cfd57ef7a26ea..7ca0febb19c39 100644
--- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
+++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
@@ -54,6 +54,12 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
// Initialize the "last seen" text editing values to a non-null value.
private TextEditState mLastKnownFrameworkTextEditingState;
+ // When true following calls to createInputConnection will return the cached lastInputConnection
+ // if the input
+ // target is a platform view. See the comments on lockPlatformViewInputConnection for more
+ // details.
+ private boolean isInputConnectionLocked;
+
@SuppressLint("NewApi")
public TextInputPlugin(
View view,
@@ -99,7 +105,7 @@ public void show() {
@Override
public void hide() {
- if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
+ if (inputTarget.type == InputTarget.Type.HC_PLATFORM_VIEW) {
notifyViewExited();
} else {
hideTextInput(mView);
@@ -130,8 +136,8 @@ public void setClient(
}
@Override
- public void setPlatformViewClient(int platformViewId) {
- setPlatformViewTextInputClient(platformViewId);
+ public void setPlatformViewClient(int platformViewId, boolean usesVirtualDisplay) {
+ setPlatformViewTextInputClient(platformViewId, usesVirtualDisplay);
}
@Override
@@ -176,6 +182,34 @@ ImeSyncDeferringInsetsCallback getImeSyncCallback() {
return imeSyncCallback;
}
+ /**
+ * Use the current platform view input connection until unlockPlatformViewInputConnection is
+ * called.
+ *
+ *
The current input connection instance is cached and any following call to @{link
+ * createInputConnection} returns the cached connection until unlockPlatformViewInputConnection is
+ * called.
+ *
+ *
This is a no-op if the current input target isn't a platform view.
+ *
+ *
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.
+ *
+ *
See also: @{link lockPlatformViewInputConnection}.
+ */
+ public void unlockPlatformViewInputConnection() {
+ isInputConnectionLocked = false;
+ }
+
/**
* Detaches the text input plugin from the platform views controller.
*
@@ -258,10 +292,21 @@ public InputConnection createInputConnection(
return null;
}
- if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
+ if (inputTarget.type == InputTarget.Type.HC_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,
@@ -316,7 +361,9 @@ public InputConnection getLastInputConnection() {
* input connection.
*/
public void clearPlatformViewClient(int platformViewId) {
- if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW && inputTarget.id == platformViewId) {
+ if ((inputTarget.type == InputTarget.Type.VD_PLATFORM_VIEW
+ || inputTarget.type == InputTarget.Type.HC_PLATFORM_VIEW)
+ && inputTarget.id == platformViewId) {
inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
notifyViewExited();
mImm.hideSoftInputFromWindow(mView.getApplicationWindowToken(), 0);
@@ -377,13 +424,25 @@ void setTextInputClient(int client, TextInputChannel.Configuration configuration
// setTextInputClient will be followed by a call to setTextInputEditingState.
// Do a restartInput at that time.
mRestartInputPending = true;
+ unlockPlatformViewInputConnection();
lastClientRect = null;
mEditable.addEditingStateListener(this);
}
- private void setPlatformViewTextInputClient(int platformViewId) {
- inputTarget = new InputTarget(InputTarget.Type.PLATFORM_VIEW, platformViewId);
- lastInputConnection = null;
+ private void setPlatformViewTextInputClient(int platformViewId, boolean usesVirtualDisplay) {
+ if (usesVirtualDisplay) {
+ // We need to make sure that the Flutter view is focused so that no imm operations get short
+ // circuited.
+ // Not asking for focus here specifically manifested in a 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 static boolean composingChanged(
@@ -474,10 +533,35 @@ public void inspect(double x, double y) {
@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;
}
@@ -487,9 +571,12 @@ enum Type {
// InputConnection is managed by the TextInputPlugin, and events are forwarded to the Flutter
// framework.
FRAMEWORK_CLIENT,
- // InputConnection is managed by a platform view that is embeded in the Android view
- // hierarchy.
- PLATFORM_VIEW,
+ // InputConnection is managed by 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,
}
public InputTarget(@NonNull Type type, int id) {
diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformView.java b/shell/platform/android/io/flutter/plugin/platform/PlatformView.java
index 85ed3c40e8c2e..92f034d840d11 100644
--- a/shell/platform/android/io/flutter/plugin/platform/PlatformView.java
+++ b/shell/platform/android/io/flutter/plugin/platform/PlatformView.java
@@ -60,26 +60,24 @@ default void onFlutterViewDetached() {}
void dispose();
/**
- * Callback fired when the platform's input connection is locked, or should be used.
+ * Callback fired when the platform's input connection is locked, or should be used. See also
+ * {@link io.flutter.plugin.editing.TextInputPlugin#lockPlatformViewInputConnection}.
*
*
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.
- *
- *
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.
+ * Callback fired when the platform input connection has been unlocked. See also {@link
+ * io.flutter.plugin.editing.TextInputPlugin#lockPlatformViewInputConnection}.
*
*
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.
- *
- *
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() {}
}
diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java
deleted file mode 100644
index d7ebe184a656b..0000000000000
--- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java
+++ /dev/null
@@ -1,263 +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 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}.
- *
- *
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.
- *
- *
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.
- *
- *
{@link PlatformViewWrapper} doesn't take ownership of the {@link SurfaceTexture}. As a
- * result, the caller is responsible for releasing the texture.
- *
- *
{@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);
- }
- }
-}
diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java
index 0ab99bfe1fc66..ad693a8724880 100644
--- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java
+++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java
@@ -5,7 +5,6 @@
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. */
@@ -14,8 +13,10 @@ 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.
*/
- @Nullable
- View getPlatformViewById(int viewId);
+ View getPlatformViewById(Integer id);
+
+ /** Returns true if the platform view uses virtual displays. */
+ boolean usesVirtualDisplay(Integer id);
/**
* Attaches an accessibility bridge for this platform views accessibility delegate.
diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
index 063882fced75f..29ace9e502c2d 100644
--- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
+++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
@@ -10,6 +10,7 @@
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;
@@ -33,6 +34,7 @@
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;
@@ -68,7 +70,20 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
// dispatched.
private final AccessibilityEventsDelegate accessibilityEventsDelegate;
- // The platform views.
+ // 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 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 contextToPlatformView;
+
+ // The views returned by `PlatformView#getView()`.
+ //
+ // This only applies to hybrid composition.
private final SparseArray platformViews;
// The platform view parents that are appended to `FlutterView`.
@@ -78,19 +93,12 @@ 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 (PlatformViewLayer render).
- // TODO(egarciad): Eliminate this.
- // https://github.com/flutter/flutter/issues/96679
+ // This is only applies to hybrid composition.
private final SparseArray platformViewParent;
// Map of unique IDs to views that render overlay layers.
private final SparseArray 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 viewWrappers;
-
// Next available unique ID for use in overlayLayerViews.
private int nextOverlayLayerId = 0;
@@ -116,9 +124,7 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
- // TODO(egarciad): Remove the need for this.
- // https://github.com/flutter/flutter/issues/96679
- public void createForPlatformViewLayer(
+ public void createAndroidViewForPlatformView(
@NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
// API level 19 is required for `android.graphics.ImageReader`.
ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT);
@@ -148,182 +154,167 @@ public void createForPlatformViewLayer(
platformViews.put(request.viewId, platformView);
}
- @TargetApi(Build.VERSION_CODES.M)
@Override
- public long createForTextureLayer(
- @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
- final int viewId = request.viewId;
- if (viewWrappers.get(viewId) != null) {
- throw new IllegalStateException(
- "Trying to create an already created platform view, view id: " + viewId);
+ 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(
+ @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);
if (!validateDirection(request.direction)) {
throw new IllegalStateException(
"Trying to create a view with unknown direction value: "
+ request.direction
+ "(view id: "
- + viewId
+ + request.viewId
+ ")");
}
- if (textureRegistry == null) {
- throw new IllegalStateException(
- "Texture registry is null. This means that platform views controller was detached, view id: "
- + viewId);
- }
- if (flutterView == null) {
+
+ if (vdControllers.containsKey(request.viewId)) {
throw new IllegalStateException(
- "Flutter view is null. This means the platform views controller doesn't have an attached view, view id: "
- + viewId);
+ "Trying to create an already created platform view, view id: " + request.viewId);
}
- final PlatformViewFactory viewFactory = registry.getFactory(request.viewType);
+
+ 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);
}
- final PlatformView platformView = viewFactory.create(context, viewId, createParams);
- platformViews.put(viewId, platformView);
-
- final PlatformViewWrapper wrapperView = new PlatformViewWrapper(context);
- final TextureRegistry.SurfaceTextureEntry textureEntry =
- textureRegistry.createSurfaceTexture();
- wrapperView.setTexture(textureEntry.surfaceTexture());
- wrapperView.setTouchProcessor(androidTouchProcessor);
-
- final int physicalWidth = toPhysicalPixels(request.logicalWidth);
- final int physicalHeight = toPhysicalPixels(request.logicalHeight);
- wrapperView.setBufferSize(physicalWidth, physicalHeight);
-
- final FrameLayout.LayoutParams layoutParams =
- new FrameLayout.LayoutParams(physicalWidth, physicalHeight);
-
- final int physicalTop = toPhysicalPixels(request.logicalTop);
- final int physicalLeft = toPhysicalPixels(request.logicalLeft);
- layoutParams.topMargin = physicalTop;
- layoutParams.leftMargin = physicalLeft;
- wrapperView.setLayoutParams(layoutParams);
-
- wrapperView.setLayoutDirection(request.direction);
- wrapperView.addView(platformView.getView());
- wrapperView.setOnDescendantFocusChangeListener(
- (view, hasFocus) -> {
- if (hasFocus) {
- platformViewsChannel.invokeViewFocused(viewId);
- } else if (textInputPlugin != null) {
- textInputPlugin.clearPlatformViewClient(viewId);
- }
- });
+ int physicalWidth = toPhysicalPixels(request.logicalWidth);
+ int physicalHeight = toPhysicalPixels(request.logicalHeight);
+ validateVirtualDisplayDimensions(physicalWidth, physicalHeight);
+
+ TextureRegistry.SurfaceTextureEntry textureEntry = textureRegistry.createSurfaceTexture();
+ VirtualDisplayController vdController =
+ VirtualDisplayController.create(
+ context,
+ accessibilityEventsDelegate,
+ viewFactory,
+ textureEntry,
+ physicalWidth,
+ physicalHeight,
+ request.viewId,
+ createParams,
+ (view, hasFocus) -> {
+ if (hasFocus) {
+ platformViewsChannel.invokeViewFocused(request.viewId);
+ }
+ });
+
+ if (vdController == null) {
+ throw new IllegalStateException(
+ "Failed creating virtual display for a "
+ + request.viewType
+ + " with id: "
+ + request.viewId);
+ }
+
+ // If our FlutterEngine is already attached to a Flutter UI, provide that Android
+ // View to this new platform view.
+ if (flutterView != null) {
+ vdController.onFlutterViewAttached(flutterView);
+ }
+
+ vdControllers.put(request.viewId, vdController);
+ View platformView = vdController.getView();
+ platformView.setLayoutDirection(request.direction);
+ contextToPlatformView.put(platformView.getContext(), platformView);
+
+ // TODO(amirh): copy accessibility nodes to the FlutterView's accessibility tree.
- flutterView.addView(wrapperView);
- viewWrappers.append(viewId, wrapperView);
return textureEntry.id();
}
@Override
- 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();
-
- final ViewGroup wrapperParent = (ViewGroup) viewWrapper.getParent();
- if (wrapperParent != null) {
- wrapperParent.removeView(viewWrapper);
- }
- viewWrappers.remove(viewId);
- return;
+ 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);
}
- // 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();
- final ViewGroup mutatorViewParent = (ViewGroup) parentView.getParent();
- if (mutatorViewParent != null) {
- mutatorViewParent.removeView(parentView);
- }
- platformViewParent.remove(viewId);
+ if (textInputPlugin != null) {
+ textInputPlugin.clearPlatformViewClient(viewId);
}
- }
- @Override
- 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);
+ contextToPlatformView.remove(vdController.getView().getContext());
+ vdController.dispose();
+ vdControllers.remove(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);
- }
+ public void resizePlatformView(
+ @NonNull PlatformViewsChannel.PlatformViewResizeRequest request,
+ @NonNull Runnable onComplete) {
+ ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
- final FrameLayout.LayoutParams layoutParams =
- (FrameLayout.LayoutParams) view.getLayoutParams();
- layoutParams.width = newWidth;
- layoutParams.height = newHeight;
- view.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);
+ }
- return new PlatformViewsChannel.PlatformViewBufferSize(
- toLogicalPixels(view.getBufferWidth()), toLogicalPixels(view.getBufferHeight()));
+ int physicalWidth = toPhysicalPixels(request.newLogicalWidth);
+ int physicalHeight = toPhysicalPixels(request.newLogicalHeight);
+ validateVirtualDisplayDimensions(physicalWidth, physicalHeight);
+
+ // Resizing involved moving the platform view to a new virtual display. Doing so
+ // potentially results in losing an active input connection. To make sure we preserve
+ // the input connection when resizing we lock it here and unlock after the resize is
+ // complete.
+ lockInputConnection(vdController);
+ vdController.resize(
+ physicalWidth,
+ physicalHeight,
+ () -> {
+ unlockInputConnection(vdController);
+ onComplete.run();
+ });
}
@Override
public void onTouch(@NonNull PlatformViewsChannel.PlatformViewTouch touch) {
final int viewId = touch.viewId;
- final PlatformView platformView = platformViews.get(viewId);
- if (platformView == null) {
- Log.e(TAG, "Sending touch to an unknown view with id: " + viewId);
- return;
- }
+ float density = context.getResources().getDisplayMetrics().density;
ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
- final float density = context.getResources().getDisplayMetrics().density;
- final MotionEvent event = toMotionEvent(density, touch);
- platformView.getView().dispatchTouchEvent(event);
+ 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);
+ }
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
@@ -337,23 +328,34 @@ public void setDirection(int viewId, int direction) {
+ viewId
+ ")");
}
+
+ ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
final PlatformView platformView = platformViews.get(viewId);
- if (platformView == null) {
- Log.e(TAG, "Setting direction to an unknown view with id: " + viewId);
+ if (platformView != null) {
+ platformView.getView().setLayoutDirection(direction);
return;
}
- ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
- platformViews.get(viewId).getView().setLayoutDirection(direction);
+ 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);
}
@Override
public void clearFocus(int viewId) {
final PlatformView platformView = platformViews.get(viewId);
- if (platformView == null) {
- Log.e(TAG, "Clearing focus on an unknown view with id: " + viewId);
+ if (platformView != null) {
+ platformView.getView().clearFocus();
return;
}
- platformView.getView().clearFocus();
+ ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
+ View view = vdControllers.get(viewId).getView();
+ view.clearFocus();
}
private void ensureValidAndroidVersion(int minSdkVersion) {
@@ -373,7 +375,8 @@ public void synchronizeToNativeViewHierarchy(boolean yes) {
};
@VisibleForTesting
- public MotionEvent toMotionEvent(float density, PlatformViewsChannel.PlatformViewTouch touch) {
+ public MotionEvent toMotionEvent(
+ float density, PlatformViewsChannel.PlatformViewTouch touch, boolean usingVirtualDiplays) {
MotionEventTracker.MotionEventId motionEventId =
MotionEventTracker.MotionEventId.from(touch.motionEventId);
MotionEvent trackedEvent = motionEventTracker.pop(motionEventId);
@@ -389,7 +392,7 @@ public MotionEvent toMotionEvent(float density, PlatformViewsChannel.PlatformVie
parsePointerCoordsList(touch.rawPointerCoords, density)
.toArray(new PointerCoords[touch.pointerCount]);
- if (trackedEvent != null) {
+ if (!usingVirtualDiplays && trackedEvent != null) {
return MotionEvent.obtain(
trackedEvent.getDownTime(),
trackedEvent.getEventTime(),
@@ -428,11 +431,13 @@ public MotionEvent toMotionEvent(float density, PlatformViewsChannel.PlatformVie
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<>();
@@ -484,14 +489,13 @@ public void detach() {
* 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 newFlutterView) {
- flutterView = newFlutterView;
+ public void attachToView(@NonNull FlutterView flutterView) {
+ this.flutterView = flutterView;
// Inform all existing platform views that they are now associated with
// a Flutter View.
- for (int i = 0; i < platformViews.size(); i++) {
- final PlatformView view = platformViews.valueAt(i);
- view.onFlutterViewAttached(flutterView);
+ for (VirtualDisplayController controller : vdControllers.values()) {
+ controller.onFlutterViewAttached(flutterView);
}
}
@@ -503,16 +507,16 @@ public void attachToView(@NonNull FlutterView newFlutterView) {
* 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();
- flutterView = null;
+ this.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
@@ -543,6 +547,29 @@ public void detachTextInputPlugin() {
textInputPlugin = null;
}
+ /**
+ * Returns true if Flutter should perform input connection proxying for the view.
+ *
+ * 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;
}
@@ -560,6 +587,8 @@ public void onAttachedToJNI() {
* 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();
}
@@ -568,12 +597,37 @@ public void onPreEngineRestart() {
}
@Override
- public View getPlatformViewById(int viewId) {
- final PlatformView platformView = platformViews.get(viewId);
- if (platformView == null) {
+ 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) {
return null;
}
- return platformView.getView();
+ 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();
}
private static boolean validateDirection(int direction) {
@@ -625,6 +679,29 @@ private static PointerCoords parsePointerCoords(Object rawCoords, float density)
return coords;
}
+ // Creating a VirtualDisplay larger than the size of the device screen size
+ // could cause the device to restart: https://github.com/flutter/flutter/issues/28978
+ private void validateVirtualDisplayDimensions(int width, int height) {
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ if (height > metrics.heightPixels || width > metrics.widthPixels) {
+ String message =
+ "Creating a virtual display of size: "
+ + "["
+ + width
+ + ", "
+ + height
+ + "] may result in problems"
+ + "(https://github.com/flutter/flutter/issues/2897)."
+ + "It is larger than the device screen size: "
+ + "["
+ + metrics.widthPixels
+ + ", "
+ + metrics.heightPixels
+ + "].";
+ Log.w(TAG, message);
+ }
+ }
+
private float getDisplayDensity() {
return context.getResources().getDisplayMetrics().density;
}
@@ -633,13 +710,18 @@ private int toPhysicalPixels(double logicalPixels) {
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.dispose(platformViews.keyAt(0));
+ channelHandler.disposeAndroidViewForPlatformView(platformViews.keyAt(0));
+ }
+
+ if (contextToPlatformView.size() > 0) {
+ contextToPlatformView.clear();
}
}
diff --git a/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java
new file mode 100644
index 0000000000000..a561f73868d86
--- /dev/null
+++ b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java
@@ -0,0 +1,478 @@
+// 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.
+ *
+ *
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("")) {
+ 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);
+ }
+ }
+}
diff --git a/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java b/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java
new file mode 100644
index 0000000000000..fec53e89a6d9b
--- /dev/null
+++ b/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java
@@ -0,0 +1,249 @@
+// 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);
+ }
+ });
+ }
+ }
+}
diff --git a/shell/platform/android/io/flutter/util/ViewUtils.java b/shell/platform/android/io/flutter/util/ViewUtils.java
index bac41ac7c06ca..6ebac49a00f89 100644
--- a/shell/platform/android/io/flutter/util/ViewUtils.java
+++ b/shell/platform/android/io/flutter/util/ViewUtils.java
@@ -9,8 +9,6 @@
import android.content.ContextWrapper;
import android.os.Build;
import android.view.View;
-import android.view.ViewGroup;
-import androidx.annotation.Nullable;
public final class ViewUtils {
/**
@@ -47,28 +45,4 @@ public static int generateViewId(int fallbackId) {
}
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;
- }
}
diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java
index b79191bd09970..2381d879898e5 100644
--- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java
+++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java
@@ -573,6 +573,23 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
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
@@ -887,10 +904,17 @@ && shouldSetCollectionInfo(semanticsNode)) {
// Add the embedded view as a child of the current accessibility node if it's using
// hybrid composition.
- result.addChild(embeddedView);
- } else {
- result.addChild(rootAccessibilityView, child.id);
+ //
+ // In this case, the view is in the Activity's view hierarchy, so it doesn't need to be
+ // mirrored.
+ //
+ // See the case above for how virtual displays are handled.
+ if (!platformViewsAccessibilityDelegate.usesVirtualDisplay(child.platformViewId)) {
+ result.addChild(embeddedView);
+ continue;
+ }
}
+ result.addChild(rootAccessibilityView, child.id);
}
return result;
}
@@ -1521,7 +1545,8 @@ void updateSemantics(
if (semanticsNode.hadPreviousConfig) {
updated.add(semanticsNode);
}
- if (semanticsNode.platformViewId != -1) {
+ if (semanticsNode.platformViewId != -1
+ && !platformViewsAccessibilityDelegate.usesVirtualDisplay(semanticsNode.platformViewId)) {
View embeddedView =
platformViewsAccessibilityDelegate.getPlatformViewById(semanticsNode.platformViewId);
if (embeddedView != null) {
@@ -1933,7 +1958,9 @@ private void willRemoveSemanticsNode(SemanticsNode semanticsNodeToBeRemoved) {
embeddedAccessibilityFocusedNodeId = null;
}
- if (semanticsNodeToBeRemoved.platformViewId != -1) {
+ if (semanticsNodeToBeRemoved.platformViewId != -1
+ && !platformViewsAccessibilityDelegate.usesVirtualDisplay(
+ semanticsNodeToBeRemoved.platformViewId)) {
View embeddedView =
platformViewsAccessibilityDelegate.getPlatformViewById(
semanticsNodeToBeRemoved.platformViewId);
diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java
index beb15c38c0a8f..9b86ed08193fe 100644
--- a/shell/platform/android/io/flutter/view/FlutterView.java
+++ b/shell/platform/android/io/flutter/view/FlutterView.java
@@ -26,6 +26,7 @@
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;
@@ -426,6 +427,14 @@ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
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);
diff --git a/shell/platform/android/test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorViewTest.java b/shell/platform/android/test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorViewTest.java
index d4bdf50dc873a..16e5496ef9a50 100644
--- a/shell/platform/android/test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorViewTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorViewTest.java
@@ -6,6 +6,8 @@
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;
@@ -79,6 +81,49 @@ public void canDragViews() {
}
}
+ @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);
diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewWrapperTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewWrapperTest.java
deleted file mode 100644
index 1c2d4f5168028..0000000000000
--- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewWrapperTest.java
+++ /dev/null
@@ -1,265 +0,0 @@
-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 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 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);
- }
-}
diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java
index 0398f88003630..66669638e910e 100644
--- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java
+++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java
@@ -17,6 +17,7 @@
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;
@@ -55,6 +56,120 @@
@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() {
@@ -94,7 +209,11 @@ public void itUsesActionEventTypeFromMotionEventForHybridPlatformViews() {
motionEventId.getId());
MotionEvent resolvedEvent =
- platformViewsController.toMotionEvent(/*density=*/ 1, frameWorkTouch);
+ platformViewsController.toMotionEvent(
+ 1, // density
+ frameWorkTouch,
+ false // usingVirtualDisplays
+ );
assertNotEquals(resolvedEvent.getAction(), frameWorkTouch.action);
assertEquals(resolvedEvent.getAction(), original.getAction());
@@ -178,6 +297,66 @@ public void createPlatformViewMessage__throwsIfViewIsNull() {
});
}
+ @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() {
@@ -593,6 +772,13 @@ public void destroyOverlaySurfaces__doesNotRemoveOverlayView() {
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() {
diff --git a/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java b/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java
new file mode 100644
index 0000000000000..2cc177fd86648
--- /dev/null
+++ b/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java
@@ -0,0 +1,82 @@
+package io.flutter.plugin.platform;
+
+import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
+import static android.os.Build.VERSION_CODES.P;
+import static android.os.Build.VERSION_CODES.R;
+import static junit.framework.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.view.Display;
+import android.view.inputmethod.InputMethodManager;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@Config(manifest = Config.NONE)
+@RunWith(AndroidJUnit4.class)
+@TargetApi(P)
+public class SingleViewPresentationTest {
+ @Test
+ @Config(minSdk = JELLY_BEAN_MR1, maxSdk = R)
+ public void returnsOuterContextInputMethodManager() {
+ // There's a bug in Android Q caused by the IMM being instanced per display.
+ // https://github.com/flutter/flutter/issues/38375. We need the context returned by
+ // SingleViewPresentation to be consistent from its instantiation instead of defaulting to
+ // what the system would have returned at call time.
+
+ // It's not possible to set up the exact same conditions as the unit test in the bug here,
+ // but we can make sure that we're wrapping the Context passed in at instantiation time and
+ // returning the same InputMethodManager from it. This test passes in a Spy context instance
+ // that initially returns a mock. Without the bugfix this test falls back to Robolectric's
+ // system service instead of the spy's and fails.
+
+ // Create an SVP under test with a Context that returns a local IMM mock.
+ Context context = spy(RuntimeEnvironment.application);
+ InputMethodManager expected = mock(InputMethodManager.class);
+ when(context.getSystemService(Context.INPUT_METHOD_SERVICE)).thenReturn(expected);
+ DisplayManager dm = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
+ SingleViewPresentation svp =
+ new SingleViewPresentation(context, dm.getDisplay(0), null, null, null, false);
+
+ // Get the IMM from the SVP's context.
+ InputMethodManager actual =
+ (InputMethodManager) svp.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+
+ // This should be the mocked instance from construction, not the IMM from the greater
+ // Android OS (or Robolectric's shadow, in this case).
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ @Config(minSdk = JELLY_BEAN_MR1, maxSdk = R)
+ public void returnsOuterContextInputMethodManager_createDisplayContext() {
+ // The IMM should also persist across display contexts created from the base context.
+
+ // Create an SVP under test with a Context that returns a local IMM mock.
+ Context context = spy(RuntimeEnvironment.application);
+ InputMethodManager expected = mock(InputMethodManager.class);
+ when(context.getSystemService(Context.INPUT_METHOD_SERVICE)).thenReturn(expected);
+ Display display =
+ ((DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE)).getDisplay(0);
+ SingleViewPresentation svp =
+ new SingleViewPresentation(context, display, null, null, null, false);
+
+ // Get the IMM from the SVP's context.
+ InputMethodManager actual =
+ (InputMethodManager)
+ svp.getContext()
+ .createDisplayContext(display)
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+
+ // This should be the mocked instance from construction, not the IMM from the greater
+ // Android OS (or Robolectric's shadow, in this case).
+ assertEquals(expected, actual);
+ }
+}
diff --git a/shell/platform/android/test/io/flutter/util/ViewUtilsTest.java b/shell/platform/android/test/io/flutter/util/ViewUtilsTest.java
index 2adbc6c2afaba..937aa06dae408 100644
--- a/shell/platform/android/test/io/flutter/util/ViewUtilsTest.java
+++ b/shell/platform/android/test/io/flutter/util/ViewUtilsTest.java
@@ -5,16 +5,11 @@
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;
@@ -35,47 +30,4 @@ public void canGetActivity() {
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));
- }
}
diff --git a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java
index 194465e9a5570..a1135bb6873ff 100644
--- a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java
+++ b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java
@@ -1467,6 +1467,7 @@ public void itProducesPlatformViewNodeForHybridComposition() {
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);
@@ -1504,6 +1505,7 @@ public void itMakesPlatformViewImportantForAccessibility() {
View embeddedView = mock(View.class);
when(accessibilityDelegate.getPlatformViewById(1)).thenReturn(embeddedView);
+ when(accessibilityDelegate.usesVirtualDisplay(1)).thenReturn(false);
TestSemanticsUpdate testSemanticsRootUpdate = root.toUpdate();
testSemanticsRootUpdate.sendUpdateToBridge(accessibilityBridge);
@@ -1538,6 +1540,7 @@ public void itMakesPlatformViewNoImportantForAccessibility() {
View embeddedView = mock(View.class);
when(accessibilityDelegate.getPlatformViewById(1)).thenReturn(embeddedView);
+ when(accessibilityDelegate.usesVirtualDisplay(1)).thenReturn(false);
TestSemanticsUpdate testSemanticsRootWithPlatformViewUpdate = rootWithPlatformView.toUpdate();
testSemanticsRootWithPlatformViewUpdate.sendUpdateToBridge(accessibilityBridge);
@@ -1552,6 +1555,34 @@ public void itMakesPlatformViewNoImportantForAccessibility() {
.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);
diff --git a/tools/android_lint/baseline.xml b/tools/android_lint/baseline.xml
index 00aad19954db8..0e47fcf9f88ea 100644
--- a/tools/android_lint/baseline.xml
+++ b/tools/android_lint/baseline.xml
@@ -133,6 +133,17 @@
column="27"/>
+
+
+
+
+
@@ -39,21 +40,20 @@
-
-
+
-
+
@@ -151,12 +151,13 @@
+
+
-
@@ -177,8 +178,8 @@
-
+