Improve platform views performance (flutter/engine#31198)
This commit is contained in:
@@ -1189,10 +1189,9 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/Platfor
|
||||
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewFactory.java
|
||||
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRegistry.java
|
||||
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRegistryImpl.java
|
||||
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java
|
||||
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java
|
||||
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
|
||||
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java
|
||||
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java
|
||||
FILE: ../../../flutter/shell/platform/android/io/flutter/util/PathUtils.java
|
||||
FILE: ../../../flutter/shell/platform/android/io/flutter/util/Preconditions.java
|
||||
FILE: ../../../flutter/shell/platform/android/io/flutter/util/Predicate.java
|
||||
|
||||
@@ -268,10 +268,9 @@ android_java_sources = [
|
||||
"io/flutter/plugin/platform/PlatformViewFactory.java",
|
||||
"io/flutter/plugin/platform/PlatformViewRegistry.java",
|
||||
"io/flutter/plugin/platform/PlatformViewRegistryImpl.java",
|
||||
"io/flutter/plugin/platform/PlatformViewWrapper.java",
|
||||
"io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java",
|
||||
"io/flutter/plugin/platform/PlatformViewsController.java",
|
||||
"io/flutter/plugin/platform/SingleViewPresentation.java",
|
||||
"io/flutter/plugin/platform/VirtualDisplayController.java",
|
||||
"io/flutter/util/PathUtils.java",
|
||||
"io/flutter/util/Preconditions.java",
|
||||
"io/flutter/util/Predicate.java",
|
||||
|
||||
@@ -107,7 +107,7 @@ public class AndroidTouchProcessor {
|
||||
* the gesture pointers into screen coordinates.
|
||||
* @return True if the event was handled.
|
||||
*/
|
||||
public boolean onTouchEvent(@NonNull MotionEvent event, Matrix transformMatrix) {
|
||||
public boolean onTouchEvent(@NonNull MotionEvent event, @NonNull Matrix transformMatrix) {
|
||||
int pointerCount = event.getPointerCount();
|
||||
|
||||
// Prepare a data packet of the appropriate size and order.
|
||||
|
||||
@@ -791,7 +791,6 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
|
||||
+ viewportMetrics.viewInsetBottom);
|
||||
|
||||
sendViewportMetricsToFlutter();
|
||||
|
||||
return newInsets;
|
||||
}
|
||||
|
||||
@@ -867,21 +866,6 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
|
||||
return textInputPlugin.createInputConnection(this, keyboardManager, outAttrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows a {@code View} that is not currently the input connection target to invoke commands on
|
||||
* the {@link android.view.inputmethod.InputMethodManager}, which is otherwise disallowed.
|
||||
*
|
||||
* <p>Returns true to allow non-input-connection-targets to invoke methods on {@code
|
||||
* InputMethodManager}, or false to exclusively allow the input connection target to invoke such
|
||||
* methods.
|
||||
*/
|
||||
@Override
|
||||
public boolean checkInputConnectionProxy(View view) {
|
||||
return flutterEngine != null
|
||||
? flutterEngine.getPlatformViewsController().checkInputConnectionProxy(view)
|
||||
: super.checkInputConnectionProxy(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a hardware key is pressed or released.
|
||||
*
|
||||
|
||||
@@ -9,13 +9,13 @@ import android.graphics.Matrix;
|
||||
import android.graphics.Path;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import io.flutter.embedding.android.AndroidTouchProcessor;
|
||||
import io.flutter.util.ViewUtils;
|
||||
|
||||
/**
|
||||
* A view that applies the {@link io.flutter.embedding.engine.mutatorsstack.FlutterMutatorsStack} to
|
||||
@@ -49,31 +49,6 @@ public class FlutterMutatorView extends FrameLayout {
|
||||
this(context, 1, /* androidTouchProcessor=*/ null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the current view or any descendant view has focus.
|
||||
*
|
||||
* @param root The root view.
|
||||
* @return True if the current view or any descendant view has focus.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public static boolean childHasFocus(@Nullable View root) {
|
||||
if (root == null) {
|
||||
return false;
|
||||
}
|
||||
if (root.hasFocus()) {
|
||||
return true;
|
||||
}
|
||||
if (root instanceof ViewGroup) {
|
||||
final ViewGroup viewGroup = (ViewGroup) root;
|
||||
for (int idx = 0; idx < viewGroup.getChildCount(); idx++) {
|
||||
if (childHasFocus(viewGroup.getChildAt(idx))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable @VisibleForTesting ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener;
|
||||
|
||||
/**
|
||||
@@ -95,7 +70,7 @@ public class FlutterMutatorView extends FrameLayout {
|
||||
new ViewTreeObserver.OnGlobalFocusChangeListener() {
|
||||
@Override
|
||||
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
|
||||
userFocusListener.onFocusChange(mutatorView, childHasFocus(mutatorView));
|
||||
userFocusListener.onFocusChange(mutatorView, ViewUtils.childHasFocus(mutatorView));
|
||||
}
|
||||
};
|
||||
observer.addOnGlobalFocusChangeListener(activeFocusListener);
|
||||
|
||||
@@ -14,6 +14,7 @@ import io.flutter.plugin.common.StandardMethodCodec;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -64,6 +65,9 @@ public class PlatformViewsChannel {
|
||||
case "resize":
|
||||
resize(call, result);
|
||||
break;
|
||||
case "offset":
|
||||
offset(call, result);
|
||||
break;
|
||||
case "touch":
|
||||
touch(call, result);
|
||||
break;
|
||||
@@ -82,29 +86,40 @@ public class PlatformViewsChannel {
|
||||
}
|
||||
|
||||
private void create(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
Map<String, Object> createArgs = call.arguments();
|
||||
boolean usesHybridComposition =
|
||||
final Map<String, Object> createArgs = call.arguments();
|
||||
// TODO(egarciad): Remove the "hybrid" case.
|
||||
final boolean usesPlatformViewLayer =
|
||||
createArgs.containsKey("hybrid") && (boolean) createArgs.get("hybrid");
|
||||
// In hybrid mode, the size of the view is determined by the size of the Flow layer.
|
||||
double width = (usesHybridComposition) ? 0 : (double) createArgs.get("width");
|
||||
double height = (usesHybridComposition) ? 0 : (double) createArgs.get("height");
|
||||
|
||||
PlatformViewCreationRequest request =
|
||||
new PlatformViewCreationRequest(
|
||||
(int) createArgs.get("id"),
|
||||
(String) createArgs.get("viewType"),
|
||||
width,
|
||||
height,
|
||||
(int) createArgs.get("direction"),
|
||||
createArgs.containsKey("params")
|
||||
? ByteBuffer.wrap((byte[]) createArgs.get("params"))
|
||||
: null);
|
||||
final ByteBuffer additionalParams =
|
||||
createArgs.containsKey("params")
|
||||
? ByteBuffer.wrap((byte[]) createArgs.get("params"))
|
||||
: null;
|
||||
try {
|
||||
if (usesHybridComposition) {
|
||||
handler.createAndroidViewForPlatformView(request);
|
||||
if (usesPlatformViewLayer) {
|
||||
final PlatformViewCreationRequest request =
|
||||
new PlatformViewCreationRequest(
|
||||
(int) createArgs.get("id"),
|
||||
(String) createArgs.get("viewType"),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
(int) createArgs.get("direction"),
|
||||
additionalParams);
|
||||
handler.createForPlatformViewLayer(request);
|
||||
result.success(null);
|
||||
} else {
|
||||
long textureId = handler.createVirtualDisplayForPlatformView(request);
|
||||
final PlatformViewCreationRequest request =
|
||||
new PlatformViewCreationRequest(
|
||||
(int) createArgs.get("id"),
|
||||
(String) createArgs.get("viewType"),
|
||||
createArgs.containsKey("top") ? (double) createArgs.get("top") : 0.0,
|
||||
createArgs.containsKey("left") ? (double) createArgs.get("left") : 0.0,
|
||||
(double) createArgs.get("width"),
|
||||
(double) createArgs.get("height"),
|
||||
(int) createArgs.get("direction"),
|
||||
additionalParams);
|
||||
long textureId = handler.createForTextureLayer(request);
|
||||
result.success(textureId);
|
||||
}
|
||||
} catch (IllegalStateException exception) {
|
||||
@@ -115,15 +130,9 @@ public class PlatformViewsChannel {
|
||||
private void dispose(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
Map<String, Object> disposeArgs = call.arguments();
|
||||
int viewId = (int) disposeArgs.get("id");
|
||||
boolean usesHybridComposition =
|
||||
disposeArgs.containsKey("hybrid") && (boolean) disposeArgs.get("hybrid");
|
||||
|
||||
try {
|
||||
if (usesHybridComposition) {
|
||||
handler.disposeAndroidViewForPlatformView(viewId);
|
||||
} else {
|
||||
handler.disposeVirtualDisplayForPlatformView(viewId);
|
||||
}
|
||||
handler.dispose(viewId);
|
||||
result.success(null);
|
||||
} catch (IllegalStateException exception) {
|
||||
result.error("error", detailedExceptionString(exception), null);
|
||||
@@ -138,14 +147,28 @@ public class PlatformViewsChannel {
|
||||
(double) resizeArgs.get("width"),
|
||||
(double) resizeArgs.get("height"));
|
||||
try {
|
||||
handler.resizePlatformView(
|
||||
resizeRequest,
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
result.success(null);
|
||||
}
|
||||
});
|
||||
final PlatformViewBufferSize sz = handler.resize(resizeRequest);
|
||||
if (sz == null) {
|
||||
result.error("error", "Failed to resize the platform view", null);
|
||||
} else {
|
||||
final Map<String, Object> response = new HashMap<>();
|
||||
response.put("width", (double) sz.width);
|
||||
response.put("height", (double) sz.height);
|
||||
result.success(response);
|
||||
}
|
||||
} catch (IllegalStateException exception) {
|
||||
result.error("error", detailedExceptionString(exception), null);
|
||||
}
|
||||
}
|
||||
|
||||
private void offset(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
Map<String, Object> offsetArgs = call.arguments();
|
||||
try {
|
||||
handler.offset(
|
||||
(int) offsetArgs.get("id"),
|
||||
(double) offsetArgs.get("top"),
|
||||
(double) offsetArgs.get("left"));
|
||||
result.success(null);
|
||||
} catch (IllegalStateException exception) {
|
||||
result.error("error", detailedExceptionString(exception), null);
|
||||
}
|
||||
@@ -249,36 +272,40 @@ public class PlatformViewsChannel {
|
||||
* The Flutter application would like to display a new Android {@code View}, i.e., platform
|
||||
* view.
|
||||
*
|
||||
* <p>The Android {@code View} is added to the view hierarchy.
|
||||
*/
|
||||
void createAndroidViewForPlatformView(@NonNull PlatformViewCreationRequest request);
|
||||
|
||||
/**
|
||||
* The Flutter application would like to dispose of an existing Android {@code View} rendered in
|
||||
* the view hierarchy.
|
||||
*/
|
||||
void disposeAndroidViewForPlatformView(int viewId);
|
||||
|
||||
/**
|
||||
* The Flutter application would like to display a new Android {@code View}.
|
||||
* <p>The Android View is added to the view hierarchy. This view is rendered in the Flutter
|
||||
* framework by a PlatformViewLayer.
|
||||
*
|
||||
* <p>{@code View} is added to a {@code VirtualDisplay}. The framework uses id returned by this
|
||||
* method to lookup the texture in the engine.
|
||||
* @param request The metadata sent from the framework.
|
||||
*/
|
||||
long createVirtualDisplayForPlatformView(@NonNull PlatformViewCreationRequest request);
|
||||
void createForPlatformViewLayer(@NonNull PlatformViewCreationRequest request);
|
||||
|
||||
/**
|
||||
* The Flutter application would like to dispose of an existing Android {@code View} rendered in
|
||||
* a virtual display.
|
||||
*/
|
||||
void disposeVirtualDisplayForPlatformView(int viewId);
|
||||
|
||||
/**
|
||||
* The Flutter application would like to resize an existing Android {@code View}, i.e., platform
|
||||
* The Flutter application would like to display a new Android {@code View}, i.e., platform
|
||||
* view.
|
||||
*
|
||||
* <p>The Android View is added to the view hierarchy. This view is rendered in the Flutter
|
||||
* framework by a TextureLayer.
|
||||
*
|
||||
* @param request The metadata sent from the framework.
|
||||
* @return The texture ID.
|
||||
*/
|
||||
void resizePlatformView(
|
||||
@NonNull PlatformViewResizeRequest request, @NonNull Runnable onComplete);
|
||||
long createForTextureLayer(@NonNull PlatformViewCreationRequest request);
|
||||
|
||||
/** The Flutter application would like to dispose of an existing Android {@code View}. */
|
||||
void dispose(int viewId);
|
||||
|
||||
/**
|
||||
* The Flutter application would like to resize an existing Android {@code View}.
|
||||
*
|
||||
* @param request The request to resize the platform view.
|
||||
* @return The buffer size where the platform view pixels are written to.
|
||||
*/
|
||||
PlatformViewBufferSize resize(@NonNull PlatformViewResizeRequest request);
|
||||
|
||||
/**
|
||||
* The Flutter application would like to change the offset of an existing Android {@code View}.
|
||||
*/
|
||||
void offset(int viewId, double top, double left);
|
||||
|
||||
/**
|
||||
* The user touched a platform view within Flutter.
|
||||
@@ -321,6 +348,12 @@ public class PlatformViewsChannel {
|
||||
/** The density independent height to display the platform view. */
|
||||
public final double logicalHeight;
|
||||
|
||||
/** The density independent top position to display the platform view. */
|
||||
public final double logicalTop;
|
||||
|
||||
/** The density independent left position to display the platform view. */
|
||||
public final double logicalLeft;
|
||||
|
||||
/**
|
||||
* The layout direction of the new platform view.
|
||||
*
|
||||
@@ -332,16 +365,20 @@ public class PlatformViewsChannel {
|
||||
/** Custom parameters that are unique to the desired platform view. */
|
||||
@Nullable public final ByteBuffer params;
|
||||
|
||||
/** Creates a request to construct a platform view that uses a virtual display. */
|
||||
/** Creates a request to construct a platform view. */
|
||||
public PlatformViewCreationRequest(
|
||||
int viewId,
|
||||
@NonNull String viewType,
|
||||
double logicalTop,
|
||||
double logicalLeft,
|
||||
double logicalWidth,
|
||||
double logicalHeight,
|
||||
int direction,
|
||||
@Nullable ByteBuffer params) {
|
||||
this.viewId = viewId;
|
||||
this.viewType = viewType;
|
||||
this.logicalTop = logicalTop;
|
||||
this.logicalLeft = logicalLeft;
|
||||
this.logicalWidth = logicalWidth;
|
||||
this.logicalHeight = logicalHeight;
|
||||
this.direction = direction;
|
||||
@@ -349,11 +386,7 @@ public class PlatformViewsChannel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request sent from Flutter to resize a platform view.
|
||||
*
|
||||
* <p>This only applies to platform views that use virtual displays.
|
||||
*/
|
||||
/** Request sent from Flutter to resize a platform view. */
|
||||
public static class PlatformViewResizeRequest {
|
||||
/** The ID of the platform view as seen by the Flutter side. */
|
||||
public final int viewId;
|
||||
@@ -371,6 +404,20 @@ public class PlatformViewsChannel {
|
||||
}
|
||||
}
|
||||
|
||||
/** The platform view buffer size. */
|
||||
public static class PlatformViewBufferSize {
|
||||
/** The width of the screen buffer. */
|
||||
public final int width;
|
||||
|
||||
/** The height of the screen buffer. */
|
||||
public final int height;
|
||||
|
||||
public PlatformViewBufferSize(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
}
|
||||
|
||||
/** The state of a touch event in Flutter within a platform view. */
|
||||
public static class PlatformViewTouch {
|
||||
/** The ID of the platform view as seen by the Flutter side. */
|
||||
|
||||
@@ -89,9 +89,7 @@ public class TextInputChannel {
|
||||
try {
|
||||
final JSONObject arguments = (JSONObject) args;
|
||||
final int platformViewId = arguments.getInt("platformViewId");
|
||||
final boolean usesVirtualDisplay =
|
||||
arguments.optBoolean("usesVirtualDisplay", false);
|
||||
textInputMethodHandler.setPlatformViewClient(platformViewId, usesVirtualDisplay);
|
||||
textInputMethodHandler.setPlatformViewClient(platformViewId);
|
||||
result.success(null);
|
||||
} catch (JSONException exception) {
|
||||
result.error("error", exception.getMessage(), null);
|
||||
@@ -402,10 +400,8 @@ public class TextInputChannel {
|
||||
* different client is set.
|
||||
*
|
||||
* @param id the ID of the platform view to be set as a text input client.
|
||||
* @param usesVirtualDisplay True if the platform view uses a virtual display, false if it uses
|
||||
* hybrid composition.
|
||||
*/
|
||||
void setPlatformViewClient(int id, boolean usesVirtualDisplay);
|
||||
void setPlatformViewClient(int id);
|
||||
|
||||
/**
|
||||
* Sets the size and the transform matrix of the current text input client.
|
||||
|
||||
@@ -54,12 +54,6 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
|
||||
// Initialize the "last seen" text editing values to a non-null value.
|
||||
private TextEditState mLastKnownFrameworkTextEditingState;
|
||||
|
||||
// When true following calls to createInputConnection will return the cached lastInputConnection
|
||||
// if the input
|
||||
// target is a platform view. See the comments on lockPlatformViewInputConnection for more
|
||||
// details.
|
||||
private boolean isInputConnectionLocked;
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public TextInputPlugin(
|
||||
View view,
|
||||
@@ -105,7 +99,7 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
|
||||
|
||||
@Override
|
||||
public void hide() {
|
||||
if (inputTarget.type == InputTarget.Type.HC_PLATFORM_VIEW) {
|
||||
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
|
||||
notifyViewExited();
|
||||
} else {
|
||||
hideTextInput(mView);
|
||||
@@ -136,8 +130,8 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlatformViewClient(int platformViewId, boolean usesVirtualDisplay) {
|
||||
setPlatformViewTextInputClient(platformViewId, usesVirtualDisplay);
|
||||
public void setPlatformViewClient(int platformViewId) {
|
||||
setPlatformViewTextInputClient(platformViewId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -182,34 +176,6 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
|
||||
return imeSyncCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the current platform view input connection until unlockPlatformViewInputConnection is
|
||||
* called.
|
||||
*
|
||||
* <p>The current input connection instance is cached and any following call to @{link
|
||||
* createInputConnection} returns the cached connection until unlockPlatformViewInputConnection is
|
||||
* called.
|
||||
*
|
||||
* <p>This is a no-op if the current input target isn't a platform view.
|
||||
*
|
||||
* <p>This is used to preserve an input connection when moving a platform view from one virtual
|
||||
* display to another.
|
||||
*/
|
||||
public void lockPlatformViewInputConnection() {
|
||||
if (inputTarget.type == InputTarget.Type.VD_PLATFORM_VIEW) {
|
||||
isInputConnectionLocked = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlocks the input connection.
|
||||
*
|
||||
* <p>See also: @{link lockPlatformViewInputConnection}.
|
||||
*/
|
||||
public void unlockPlatformViewInputConnection() {
|
||||
isInputConnectionLocked = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detaches the text input plugin from the platform views controller.
|
||||
*
|
||||
@@ -292,21 +258,10 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
|
||||
return null;
|
||||
}
|
||||
|
||||
if (inputTarget.type == InputTarget.Type.HC_PLATFORM_VIEW) {
|
||||
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (inputTarget.type == InputTarget.Type.VD_PLATFORM_VIEW) {
|
||||
if (isInputConnectionLocked) {
|
||||
return lastInputConnection;
|
||||
}
|
||||
lastInputConnection =
|
||||
platformViewsController
|
||||
.getPlatformViewById(inputTarget.id)
|
||||
.onCreateInputConnection(outAttrs);
|
||||
return lastInputConnection;
|
||||
}
|
||||
|
||||
outAttrs.inputType =
|
||||
inputTypeFromTextInputType(
|
||||
configuration.inputType,
|
||||
@@ -361,9 +316,7 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
|
||||
* input connection.
|
||||
*/
|
||||
public void clearPlatformViewClient(int platformViewId) {
|
||||
if ((inputTarget.type == InputTarget.Type.VD_PLATFORM_VIEW
|
||||
|| inputTarget.type == InputTarget.Type.HC_PLATFORM_VIEW)
|
||||
&& inputTarget.id == platformViewId) {
|
||||
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW && inputTarget.id == platformViewId) {
|
||||
inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
|
||||
notifyViewExited();
|
||||
mImm.hideSoftInputFromWindow(mView.getApplicationWindowToken(), 0);
|
||||
@@ -424,25 +377,13 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
|
||||
// setTextInputClient will be followed by a call to setTextInputEditingState.
|
||||
// Do a restartInput at that time.
|
||||
mRestartInputPending = true;
|
||||
unlockPlatformViewInputConnection();
|
||||
lastClientRect = null;
|
||||
mEditable.addEditingStateListener(this);
|
||||
}
|
||||
|
||||
private void setPlatformViewTextInputClient(int platformViewId, boolean usesVirtualDisplay) {
|
||||
if (usesVirtualDisplay) {
|
||||
// We need to make sure that the Flutter view is focused so that no imm operations get short
|
||||
// circuited.
|
||||
// Not asking for focus here specifically manifested in a but on API 28 devices where the
|
||||
// platform view's request to show a keyboard was ignored.
|
||||
mView.requestFocus();
|
||||
inputTarget = new InputTarget(InputTarget.Type.VD_PLATFORM_VIEW, platformViewId);
|
||||
mImm.restartInput(mView);
|
||||
mRestartInputPending = false;
|
||||
} else {
|
||||
inputTarget = new InputTarget(InputTarget.Type.HC_PLATFORM_VIEW, platformViewId);
|
||||
lastInputConnection = null;
|
||||
}
|
||||
private void setPlatformViewTextInputClient(int platformViewId) {
|
||||
inputTarget = new InputTarget(InputTarget.Type.PLATFORM_VIEW, platformViewId);
|
||||
lastInputConnection = null;
|
||||
}
|
||||
|
||||
private static boolean composingChanged(
|
||||
@@ -533,35 +474,10 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
|
||||
|
||||
@VisibleForTesting
|
||||
void clearTextInputClient() {
|
||||
if (inputTarget.type == InputTarget.Type.VD_PLATFORM_VIEW) {
|
||||
// This only applies to platform views that use a virtual display.
|
||||
// Focus changes in the framework tree have no guarantees on the order focus nodes are
|
||||
// notified. A node
|
||||
// that lost focus may be notified before or after a node that gained focus.
|
||||
// When moving the focus from a Flutter text field to an AndroidView, it is possible that the
|
||||
// Flutter text
|
||||
// field's focus node will be notified that it lost focus after the AndroidView was notified
|
||||
// that it gained
|
||||
// focus. When this happens the text field will send a clearTextInput command which we ignore.
|
||||
// By doing this we prevent the framework from clearing a platform view input client (the only
|
||||
// way to do so
|
||||
// is to set a new framework text client). I don't see an obvious use case for "clearing" a
|
||||
// platform view's
|
||||
// text input client, and it may be error prone as we don't know how the platform view manages
|
||||
// the input
|
||||
// connection and we probably shouldn't interfere.
|
||||
// If we ever want to allow the framework to clear a platform view text client we should
|
||||
// probably consider
|
||||
// changing the focus manager such that focus nodes that lost focus are notified before focus
|
||||
// nodes that
|
||||
// gained focus as part of the same focus event.
|
||||
return;
|
||||
}
|
||||
mEditable.removeEditingStateListener(this);
|
||||
notifyViewExited();
|
||||
updateAutofillConfigurationIfNeeded(null);
|
||||
inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
|
||||
unlockPlatformViewInputConnection();
|
||||
lastClientRect = null;
|
||||
}
|
||||
|
||||
@@ -571,12 +487,9 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
|
||||
// InputConnection is managed by the TextInputPlugin, and events are forwarded to the Flutter
|
||||
// framework.
|
||||
FRAMEWORK_CLIENT,
|
||||
// InputConnection is managed by an embedded platform view that is backed by a virtual
|
||||
// display (VD).
|
||||
VD_PLATFORM_VIEW,
|
||||
// InputConnection is managed by an embedded platform view that is embeded in the Android view
|
||||
// hierarchy, and uses hybrid composition (HC).
|
||||
HC_PLATFORM_VIEW,
|
||||
// InputConnection is managed by a platform view that is embeded in the Android view
|
||||
// hierarchy.
|
||||
PLATFORM_VIEW,
|
||||
}
|
||||
|
||||
public InputTarget(@NonNull Type type, int id) {
|
||||
|
||||
@@ -60,24 +60,26 @@ public interface PlatformView {
|
||||
void dispose();
|
||||
|
||||
/**
|
||||
* Callback fired when the platform's input connection is locked, or should be used. See also
|
||||
* {@link io.flutter.plugin.editing.TextInputPlugin#lockPlatformViewInputConnection}.
|
||||
* Callback fired when the platform's input connection is locked, or should be used.
|
||||
*
|
||||
* <p>This hook only exists for rare cases where the plugin relies on the state of the input
|
||||
* connection. This probably doesn't need to be implemented.
|
||||
*
|
||||
* <p>This method is deprecated, and will be removed in a future release.
|
||||
*/
|
||||
// Default interface methods are supported on all min SDK versions of Android.
|
||||
@SuppressLint("NewApi")
|
||||
@Deprecated
|
||||
default void onInputConnectionLocked() {}
|
||||
|
||||
/**
|
||||
* Callback fired when the platform input connection has been unlocked. See also {@link
|
||||
* io.flutter.plugin.editing.TextInputPlugin#lockPlatformViewInputConnection}.
|
||||
* Callback fired when the platform input connection has been unlocked.
|
||||
*
|
||||
* <p>This hook only exists for rare cases where the plugin relies on the state of the input
|
||||
* connection. This probably doesn't need to be implemented.
|
||||
*
|
||||
* <p>This method is deprecated, and will be removed in a future release.
|
||||
*/
|
||||
// Default interface methods are supported on all min SDK versions of Android.
|
||||
@SuppressLint("NewApi")
|
||||
@Deprecated
|
||||
default void onInputConnectionUnlocked() {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package io.flutter.plugin.platform;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.graphics.BlendMode;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.os.Build;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import io.flutter.Log;
|
||||
import io.flutter.embedding.android.AndroidTouchProcessor;
|
||||
import io.flutter.util.ViewUtils;
|
||||
|
||||
/**
|
||||
* Wraps a platform view to intercept gestures and project this view onto a {@link SurfaceTexture}.
|
||||
*
|
||||
* <p>An Android platform view is composed by the engine using a {@code TextureLayer}. The view is
|
||||
* embeded to the Android view hierarchy like a normal view, but it's projected onto a {@link
|
||||
* SurfaceTexture}, so it can be efficiently composed by the engine.
|
||||
*
|
||||
* <p>Since the view is in the Android view hierarchy, keyboard and accessibility interactions
|
||||
* behave normally.
|
||||
*/
|
||||
@TargetApi(23)
|
||||
class PlatformViewWrapper extends FrameLayout {
|
||||
private static final String TAG = "PlatformViewWrapper";
|
||||
|
||||
private int prevLeft;
|
||||
private int prevTop;
|
||||
private int left;
|
||||
private int top;
|
||||
private int bufferWidth;
|
||||
private int bufferHeight;
|
||||
private SurfaceTexture tx;
|
||||
private Surface surface;
|
||||
private AndroidTouchProcessor touchProcessor;
|
||||
|
||||
@Nullable @VisibleForTesting ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener;
|
||||
|
||||
public PlatformViewWrapper(@NonNull Context context) {
|
||||
super(context);
|
||||
setWillNotDraw(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the touch processor that allows to intercept gestures.
|
||||
*
|
||||
* @param newTouchProcessor The touch processor.
|
||||
*/
|
||||
public void setTouchProcessor(@Nullable AndroidTouchProcessor newTouchProcessor) {
|
||||
touchProcessor = newTouchProcessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the texture where the view is projected onto.
|
||||
*
|
||||
* <p>{@link PlatformViewWrapper} doesn't take ownership of the {@link SurfaceTexture}. As a
|
||||
* result, the caller is responsible for releasing the texture.
|
||||
*
|
||||
* <p>{@link io.flutter.view.TextureRegistry} is responsible for creating and registering textures
|
||||
* in the engine. Therefore, the engine is responsible for also releasing the texture.
|
||||
*
|
||||
* @param newTx The texture where the view is projected onto.
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
public void setTexture(@Nullable SurfaceTexture newTx) {
|
||||
if (Build.VERSION.SDK_INT < 23) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"Platform views cannot be displayed below API level 23. "
|
||||
+ "You can prevent this issue by setting `minSdkVersion: 23` in build.gradle.");
|
||||
return;
|
||||
}
|
||||
|
||||
tx = newTx;
|
||||
|
||||
if (bufferWidth > 0 && bufferHeight > 0) {
|
||||
tx.setDefaultBufferSize(bufferWidth, bufferHeight);
|
||||
}
|
||||
|
||||
if (surface != null) {
|
||||
surface.release();
|
||||
}
|
||||
surface = createSurface(newTx);
|
||||
|
||||
// Fill the entire canvas with a transparent color.
|
||||
// As a result, the background color of the platform view container is displayed
|
||||
// to the user until the platform view draws its first frame.
|
||||
final Canvas canvas = surface.lockHardwareCanvas();
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
canvas.drawColor(Color.TRANSPARENT, BlendMode.CLEAR);
|
||||
} else {
|
||||
canvas.drawColor(Color.TRANSPARENT);
|
||||
}
|
||||
} finally {
|
||||
surface.unlockCanvasAndPost(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@VisibleForTesting
|
||||
protected Surface createSurface(@NonNull SurfaceTexture tx) {
|
||||
return new Surface(tx);
|
||||
}
|
||||
|
||||
/** Returns the texture where the view is projected. */
|
||||
@Nullable
|
||||
public SurfaceTexture getTexture() {
|
||||
return tx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the layout parameters for this view.
|
||||
*
|
||||
* @param params The new parameters.
|
||||
*/
|
||||
public void setLayoutParams(@NonNull FrameLayout.LayoutParams params) {
|
||||
super.setLayoutParams(params);
|
||||
|
||||
left = params.leftMargin;
|
||||
top = params.topMargin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the size of the image buffer.
|
||||
*
|
||||
* @param width The width of the screen buffer.
|
||||
* @param height The height of the screen buffer.
|
||||
*/
|
||||
public void setBufferSize(int width, int height) {
|
||||
bufferWidth = width;
|
||||
bufferHeight = height;
|
||||
if (tx != null) {
|
||||
tx.setDefaultBufferSize(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the image buffer width. */
|
||||
public int getBufferWidth() {
|
||||
return bufferWidth;
|
||||
}
|
||||
|
||||
/** Returns the image buffer height. */
|
||||
public int getBufferHeight() {
|
||||
return bufferHeight;
|
||||
}
|
||||
|
||||
/** Releases the surface. */
|
||||
public void release() {
|
||||
// Don't release the texture.
|
||||
tx = null;
|
||||
if (surface != null) {
|
||||
surface.release();
|
||||
surface = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(@NonNull MotionEvent event) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {
|
||||
super.onDescendantInvalidated(child, target);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("NewApi")
|
||||
public void draw(Canvas canvas) {
|
||||
if (surface == null || !surface.isValid()) {
|
||||
Log.e(TAG, "Invalid surface. The platform view cannot be displayed.");
|
||||
return;
|
||||
}
|
||||
if (tx == null || tx.isReleased()) {
|
||||
Log.e(TAG, "Invalid texture. The platform view cannot be displayed.");
|
||||
return;
|
||||
}
|
||||
// Override the canvas that this subtree of views will use to draw.
|
||||
final Canvas surfaceCanvas = surface.lockHardwareCanvas();
|
||||
try {
|
||||
// Clear the current pixels in the canvas.
|
||||
// This helps when a WebView renders an HTML document with transparent background.
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
surfaceCanvas.drawColor(Color.TRANSPARENT, BlendMode.CLEAR);
|
||||
} else {
|
||||
surfaceCanvas.drawColor(Color.TRANSPARENT);
|
||||
}
|
||||
super.draw(surfaceCanvas);
|
||||
} finally {
|
||||
surface.unlockCanvasAndPost(surfaceCanvas);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public boolean onTouchEvent(@NonNull MotionEvent event) {
|
||||
if (touchProcessor == null) {
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
final Matrix screenMatrix = new Matrix();
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
prevLeft = left;
|
||||
prevTop = top;
|
||||
screenMatrix.postTranslate(left, top);
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
// While the view is dragged, use the left and top positions as
|
||||
// they were at the moment the touch event fired.
|
||||
screenMatrix.postTranslate(prevLeft, prevTop);
|
||||
prevLeft = left;
|
||||
prevTop = top;
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
default:
|
||||
screenMatrix.postTranslate(left, top);
|
||||
break;
|
||||
}
|
||||
return touchProcessor.onTouchEvent(event, screenMatrix);
|
||||
}
|
||||
|
||||
public void setOnDescendantFocusChangeListener(@NonNull OnFocusChangeListener userFocusListener) {
|
||||
unsetOnDescendantFocusChangeListener();
|
||||
final ViewTreeObserver observer = getViewTreeObserver();
|
||||
if (observer.isAlive() && activeFocusListener == null) {
|
||||
activeFocusListener =
|
||||
new ViewTreeObserver.OnGlobalFocusChangeListener() {
|
||||
@Override
|
||||
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
|
||||
userFocusListener.onFocusChange(
|
||||
PlatformViewWrapper.this, ViewUtils.childHasFocus(PlatformViewWrapper.this));
|
||||
}
|
||||
};
|
||||
observer.addOnGlobalFocusChangeListener(activeFocusListener);
|
||||
}
|
||||
}
|
||||
|
||||
public void unsetOnDescendantFocusChangeListener() {
|
||||
final ViewTreeObserver observer = getViewTreeObserver();
|
||||
if (observer.isAlive() && activeFocusListener != null) {
|
||||
final ViewTreeObserver.OnGlobalFocusChangeListener currFocusListener = activeFocusListener;
|
||||
activeFocusListener = null;
|
||||
observer.removeOnGlobalFocusChangeListener(currFocusListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
package io.flutter.plugin.platform;
|
||||
|
||||
import android.view.View;
|
||||
import androidx.annotation.Nullable;
|
||||
import io.flutter.view.AccessibilityBridge;
|
||||
|
||||
/** Facilitates interaction between the accessibility bridge and embedded platform views. */
|
||||
@@ -13,10 +14,8 @@ public interface PlatformViewsAccessibilityDelegate {
|
||||
* Returns the root of the view hierarchy for the platform view with the requested id, or null if
|
||||
* there is no corresponding view.
|
||||
*/
|
||||
View getPlatformViewById(Integer id);
|
||||
|
||||
/** Returns true if the platform view uses virtual displays. */
|
||||
boolean usesVirtualDisplay(Integer id);
|
||||
@Nullable
|
||||
View getPlatformViewById(int viewId);
|
||||
|
||||
/**
|
||||
* Attaches an accessibility bridge for this platform views accessibility delegate.
|
||||
|
||||
@@ -10,7 +10,6 @@ import static android.view.MotionEvent.PointerProperties;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.SparseArray;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
@@ -34,7 +33,6 @@ import io.flutter.plugin.editing.TextInputPlugin;
|
||||
import io.flutter.view.AccessibilityBridge;
|
||||
import io.flutter.view.TextureRegistry;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
@@ -70,20 +68,7 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
|
||||
// dispatched.
|
||||
private final AccessibilityEventsDelegate accessibilityEventsDelegate;
|
||||
|
||||
// TODO(mattcarroll): Refactor overall platform views to facilitate testing and then make
|
||||
// this private. This is visible as a hack to facilitate testing. This was deemed the least
|
||||
// bad option at the time of writing.
|
||||
@VisibleForTesting /* package */ final HashMap<Integer, VirtualDisplayController> vdControllers;
|
||||
|
||||
// Maps a virtual display's context to the platform view hosted in this virtual display.
|
||||
// Since each virtual display has it's unique context this allows associating any view with the
|
||||
// platform view that
|
||||
// it is associated with(e.g if a platform view creates other views in the same virtual display.
|
||||
@VisibleForTesting /* package */ final HashMap<Context, View> contextToPlatformView;
|
||||
|
||||
// The views returned by `PlatformView#getView()`.
|
||||
//
|
||||
// This only applies to hybrid composition.
|
||||
// The platform views.
|
||||
private final SparseArray<PlatformView> platformViews;
|
||||
|
||||
// The platform view parents that are appended to `FlutterView`.
|
||||
@@ -93,12 +78,19 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
|
||||
// This view provides a wrapper that applies scene builder operations to the platform view.
|
||||
// For example, a transform matrix, or setting opacity on the platform view layer.
|
||||
//
|
||||
// This is only applies to hybrid composition.
|
||||
// This is only applies to hybrid composition (PlatformViewLayer render).
|
||||
// TODO(egarciad): Eliminate this.
|
||||
// https://github.com/flutter/flutter/issues/96679
|
||||
private final SparseArray<FlutterMutatorView> platformViewParent;
|
||||
|
||||
// Map of unique IDs to views that render overlay layers.
|
||||
private final SparseArray<FlutterImageView> overlayLayerViews;
|
||||
|
||||
// View wrappers are FrameLayouts that contain a single child view.
|
||||
// This child view is the platform view.
|
||||
// This only applies to hybrid composition (TextureLayer render).
|
||||
private final SparseArray<PlatformViewWrapper> viewWrappers;
|
||||
|
||||
// Next available unique ID for use in overlayLayerViews.
|
||||
private int nextOverlayLayerId = 0;
|
||||
|
||||
@@ -124,7 +116,9 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
@Override
|
||||
public void createAndroidViewForPlatformView(
|
||||
// TODO(egarciad): Remove the need for this.
|
||||
// https://github.com/flutter/flutter/issues/96679
|
||||
public void createForPlatformViewLayer(
|
||||
@NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
|
||||
// API level 19 is required for `android.graphics.ImageReader`.
|
||||
ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT);
|
||||
@@ -154,167 +148,182 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
|
||||
platformViews.put(request.viewId, platformView);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
@Override
|
||||
public void disposeAndroidViewForPlatformView(int viewId) {
|
||||
// Hybrid view.
|
||||
final PlatformView platformView = platformViews.get(viewId);
|
||||
final FlutterMutatorView parentView = platformViewParent.get(viewId);
|
||||
if (platformView != null) {
|
||||
if (parentView != null) {
|
||||
parentView.removeView(platformView.getView());
|
||||
}
|
||||
platformViews.remove(viewId);
|
||||
platformView.dispose();
|
||||
}
|
||||
if (parentView != null) {
|
||||
parentView.unsetOnDescendantFocusChangeListener();
|
||||
((ViewGroup) parentView.getParent()).removeView(parentView);
|
||||
platformViewParent.remove(viewId);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
|
||||
@Override
|
||||
public long createVirtualDisplayForPlatformView(
|
||||
public long createForTextureLayer(
|
||||
@NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
|
||||
// API level 20 is required for VirtualDisplay#setSurface which we use when resizing a
|
||||
// platform view.
|
||||
ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
|
||||
final int viewId = request.viewId;
|
||||
if (viewWrappers.get(viewId) != null) {
|
||||
throw new IllegalStateException(
|
||||
"Trying to create an already created platform view, view id: " + viewId);
|
||||
}
|
||||
if (!validateDirection(request.direction)) {
|
||||
throw new IllegalStateException(
|
||||
"Trying to create a view with unknown direction value: "
|
||||
+ request.direction
|
||||
+ "(view id: "
|
||||
+ request.viewId
|
||||
+ viewId
|
||||
+ ")");
|
||||
}
|
||||
|
||||
if (vdControllers.containsKey(request.viewId)) {
|
||||
if (textureRegistry == null) {
|
||||
throw new IllegalStateException(
|
||||
"Trying to create an already created platform view, view id: " + request.viewId);
|
||||
"Texture registry is null. This means that platform views controller was detached, view id: "
|
||||
+ viewId);
|
||||
}
|
||||
|
||||
PlatformViewFactory viewFactory = registry.getFactory(request.viewType);
|
||||
if (flutterView == null) {
|
||||
throw new IllegalStateException(
|
||||
"Flutter view is null. This means the platform views controller doesn't have an attached view, view id: "
|
||||
+ viewId);
|
||||
}
|
||||
final PlatformViewFactory viewFactory = registry.getFactory(request.viewType);
|
||||
if (viewFactory == null) {
|
||||
throw new IllegalStateException(
|
||||
"Trying to create a platform view of unregistered type: " + request.viewType);
|
||||
}
|
||||
|
||||
Object createParams = null;
|
||||
if (request.params != null) {
|
||||
createParams = viewFactory.getCreateArgsCodec().decodeMessage(request.params);
|
||||
}
|
||||
|
||||
int physicalWidth = toPhysicalPixels(request.logicalWidth);
|
||||
int physicalHeight = toPhysicalPixels(request.logicalHeight);
|
||||
validateVirtualDisplayDimensions(physicalWidth, physicalHeight);
|
||||
final PlatformView platformView = viewFactory.create(context, viewId, createParams);
|
||||
platformViews.put(viewId, platformView);
|
||||
|
||||
TextureRegistry.SurfaceTextureEntry textureEntry = textureRegistry.createSurfaceTexture();
|
||||
VirtualDisplayController vdController =
|
||||
VirtualDisplayController.create(
|
||||
context,
|
||||
accessibilityEventsDelegate,
|
||||
viewFactory,
|
||||
textureEntry,
|
||||
physicalWidth,
|
||||
physicalHeight,
|
||||
request.viewId,
|
||||
createParams,
|
||||
(view, hasFocus) -> {
|
||||
if (hasFocus) {
|
||||
platformViewsChannel.invokeViewFocused(request.viewId);
|
||||
}
|
||||
});
|
||||
final PlatformViewWrapper wrapperView = new PlatformViewWrapper(context);
|
||||
final TextureRegistry.SurfaceTextureEntry textureEntry =
|
||||
textureRegistry.createSurfaceTexture();
|
||||
wrapperView.setTexture(textureEntry.surfaceTexture());
|
||||
wrapperView.setTouchProcessor(androidTouchProcessor);
|
||||
|
||||
if (vdController == null) {
|
||||
throw new IllegalStateException(
|
||||
"Failed creating virtual display for a "
|
||||
+ request.viewType
|
||||
+ " with id: "
|
||||
+ request.viewId);
|
||||
}
|
||||
final int physicalWidth = toPhysicalPixels(request.logicalWidth);
|
||||
final int physicalHeight = toPhysicalPixels(request.logicalHeight);
|
||||
wrapperView.setBufferSize(physicalWidth, physicalHeight);
|
||||
|
||||
// If our FlutterEngine is already attached to a Flutter UI, provide that Android
|
||||
// View to this new platform view.
|
||||
if (flutterView != null) {
|
||||
vdController.onFlutterViewAttached(flutterView);
|
||||
}
|
||||
final FrameLayout.LayoutParams layoutParams =
|
||||
new FrameLayout.LayoutParams(physicalWidth, physicalHeight);
|
||||
|
||||
vdControllers.put(request.viewId, vdController);
|
||||
View platformView = vdController.getView();
|
||||
platformView.setLayoutDirection(request.direction);
|
||||
contextToPlatformView.put(platformView.getContext(), platformView);
|
||||
final int physicalTop = toPhysicalPixels(request.logicalTop);
|
||||
final int physicalLeft = toPhysicalPixels(request.logicalLeft);
|
||||
layoutParams.topMargin = physicalTop;
|
||||
layoutParams.leftMargin = physicalLeft;
|
||||
wrapperView.setLayoutParams(layoutParams);
|
||||
|
||||
// TODO(amirh): copy accessibility nodes to the FlutterView's accessibility tree.
|
||||
wrapperView.setLayoutDirection(request.direction);
|
||||
wrapperView.addView(platformView.getView());
|
||||
wrapperView.setOnDescendantFocusChangeListener(
|
||||
(view, hasFocus) -> {
|
||||
if (hasFocus) {
|
||||
platformViewsChannel.invokeViewFocused(viewId);
|
||||
} else if (textInputPlugin != null) {
|
||||
textInputPlugin.clearPlatformViewClient(viewId);
|
||||
}
|
||||
});
|
||||
|
||||
flutterView.addView(wrapperView);
|
||||
viewWrappers.append(viewId, wrapperView);
|
||||
return textureEntry.id();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disposeVirtualDisplayForPlatformView(int viewId) {
|
||||
ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
|
||||
VirtualDisplayController vdController = vdControllers.get(viewId);
|
||||
if (vdController == null) {
|
||||
throw new IllegalStateException(
|
||||
"Trying to dispose a platform view with unknown id: " + viewId);
|
||||
public void dispose(int viewId) {
|
||||
final PlatformView platformView = platformViews.get(viewId);
|
||||
if (platformView != null) {
|
||||
final ViewGroup pvParent = (ViewGroup) platformView.getView().getParent();
|
||||
if (pvParent != null) {
|
||||
pvParent.removeView(platformView.getView());
|
||||
}
|
||||
platformViews.remove(viewId);
|
||||
platformView.dispose();
|
||||
}
|
||||
// The platform view is displayed using a TextureLayer.
|
||||
final PlatformViewWrapper viewWrapper = viewWrappers.get(viewId);
|
||||
if (viewWrapper != null) {
|
||||
viewWrapper.release();
|
||||
viewWrapper.unsetOnDescendantFocusChangeListener();
|
||||
|
||||
if (textInputPlugin != null) {
|
||||
textInputPlugin.clearPlatformViewClient(viewId);
|
||||
final ViewGroup wrapperParent = (ViewGroup) viewWrapper.getParent();
|
||||
if (wrapperParent != null) {
|
||||
wrapperParent.removeView(viewWrapper);
|
||||
}
|
||||
viewWrappers.remove(viewId);
|
||||
return;
|
||||
}
|
||||
// The platform view is displayed using a PlatformViewLayer.
|
||||
// TODO(egarciad): Eliminate this case.
|
||||
// https://github.com/flutter/flutter/issues/96679
|
||||
final FlutterMutatorView parentView = platformViewParent.get(viewId);
|
||||
if (parentView != null) {
|
||||
parentView.unsetOnDescendantFocusChangeListener();
|
||||
|
||||
contextToPlatformView.remove(vdController.getView().getContext());
|
||||
vdController.dispose();
|
||||
vdControllers.remove(viewId);
|
||||
final ViewGroup mutatorViewParent = (ViewGroup) parentView.getParent();
|
||||
if (mutatorViewParent != null) {
|
||||
mutatorViewParent.removeView(parentView);
|
||||
}
|
||||
platformViewParent.remove(viewId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resizePlatformView(
|
||||
@NonNull PlatformViewsChannel.PlatformViewResizeRequest request,
|
||||
@NonNull Runnable onComplete) {
|
||||
ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
|
||||
public void offset(int viewId, double top, double left) {
|
||||
final PlatformViewWrapper wrapper = viewWrappers.get(viewId);
|
||||
if (wrapper == null) {
|
||||
Log.e(TAG, "Setting offset for unknown platform view with id: " + viewId);
|
||||
return;
|
||||
}
|
||||
final int physicalTop = toPhysicalPixels(top);
|
||||
final int physicalLeft = toPhysicalPixels(left);
|
||||
final FrameLayout.LayoutParams layoutParams =
|
||||
(FrameLayout.LayoutParams) wrapper.getLayoutParams();
|
||||
layoutParams.topMargin = physicalTop;
|
||||
layoutParams.leftMargin = physicalLeft;
|
||||
wrapper.setLayoutParams(layoutParams);
|
||||
}
|
||||
|
||||
final VirtualDisplayController vdController = vdControllers.get(request.viewId);
|
||||
if (vdController == null) {
|
||||
throw new IllegalStateException(
|
||||
"Trying to resize a platform view with unknown id: " + request.viewId);
|
||||
@Override
|
||||
public PlatformViewsChannel.PlatformViewBufferSize resize(
|
||||
@NonNull PlatformViewsChannel.PlatformViewResizeRequest request) {
|
||||
final int viewId = request.viewId;
|
||||
final PlatformViewWrapper view = viewWrappers.get(viewId);
|
||||
if (view == null) {
|
||||
Log.e(TAG, "Resizing unknown platform view with id: " + viewId);
|
||||
return null;
|
||||
}
|
||||
final int newWidth = toPhysicalPixels(request.newLogicalWidth);
|
||||
final int newHeight = toPhysicalPixels(request.newLogicalHeight);
|
||||
|
||||
// Resize the buffer only when the current buffer size is smaller than the new size.
|
||||
// This is required to prevent a situation when smooth keyboard animation
|
||||
// resizes the texture too often, such that the GPU and the platform thread don't agree on
|
||||
// the
|
||||
// timing of the new size.
|
||||
// Resizing the texture causes pixel stretching since the size of the GL texture used in
|
||||
// the engine
|
||||
// is set by the framework, but the texture buffer size is set by the platform down below.
|
||||
if (newWidth > view.getBufferWidth() || newHeight > view.getBufferHeight()) {
|
||||
view.setBufferSize(newWidth, newHeight);
|
||||
}
|
||||
|
||||
int physicalWidth = toPhysicalPixels(request.newLogicalWidth);
|
||||
int physicalHeight = toPhysicalPixels(request.newLogicalHeight);
|
||||
validateVirtualDisplayDimensions(physicalWidth, physicalHeight);
|
||||
final FrameLayout.LayoutParams layoutParams =
|
||||
(FrameLayout.LayoutParams) view.getLayoutParams();
|
||||
layoutParams.width = newWidth;
|
||||
layoutParams.height = newHeight;
|
||||
view.setLayoutParams(layoutParams);
|
||||
|
||||
// Resizing involved moving the platform view to a new virtual display. Doing so
|
||||
// potentially results in losing an active input connection. To make sure we preserve
|
||||
// the input connection when resizing we lock it here and unlock after the resize is
|
||||
// complete.
|
||||
lockInputConnection(vdController);
|
||||
vdController.resize(
|
||||
physicalWidth,
|
||||
physicalHeight,
|
||||
() -> {
|
||||
unlockInputConnection(vdController);
|
||||
onComplete.run();
|
||||
});
|
||||
return new PlatformViewsChannel.PlatformViewBufferSize(
|
||||
toLogicalPixels(view.getBufferWidth()), toLogicalPixels(view.getBufferHeight()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTouch(@NonNull PlatformViewsChannel.PlatformViewTouch touch) {
|
||||
final int viewId = touch.viewId;
|
||||
float density = context.getResources().getDisplayMetrics().density;
|
||||
ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
|
||||
if (vdControllers.containsKey(viewId)) {
|
||||
final MotionEvent event = toMotionEvent(density, touch, /*usingVirtualDiplays=*/ true);
|
||||
vdControllers.get(touch.viewId).dispatchTouchEvent(event);
|
||||
} else if (platformViews.get(viewId) != null) {
|
||||
final MotionEvent event = toMotionEvent(density, touch, /*usingVirtualDiplays=*/ false);
|
||||
View view = platformViews.get(touch.viewId).getView();
|
||||
if (view != null) {
|
||||
view.dispatchTouchEvent(event);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException("Sending touch to an unknown view with id: " + viewId);
|
||||
final PlatformView platformView = platformViews.get(viewId);
|
||||
if (platformView == null) {
|
||||
Log.e(TAG, "Sending touch to an unknown view with id: " + viewId);
|
||||
return;
|
||||
}
|
||||
ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
|
||||
final float density = context.getResources().getDisplayMetrics().density;
|
||||
final MotionEvent event = toMotionEvent(density, touch);
|
||||
platformView.getView().dispatchTouchEvent(event);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
|
||||
@@ -328,34 +337,23 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
|
||||
+ viewId
|
||||
+ ")");
|
||||
}
|
||||
|
||||
ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
|
||||
final PlatformView platformView = platformViews.get(viewId);
|
||||
if (platformView != null) {
|
||||
platformView.getView().setLayoutDirection(direction);
|
||||
if (platformView == null) {
|
||||
Log.e(TAG, "Setting direction to an unknown view with id: " + viewId);
|
||||
return;
|
||||
}
|
||||
VirtualDisplayController controller = vdControllers.get(viewId);
|
||||
if (controller == null) {
|
||||
throw new IllegalStateException(
|
||||
"Trying to set direction: "
|
||||
+ direction
|
||||
+ " to an unknown platform view with id: "
|
||||
+ viewId);
|
||||
}
|
||||
controller.getView().setLayoutDirection(direction);
|
||||
ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
|
||||
platformViews.get(viewId).getView().setLayoutDirection(direction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearFocus(int viewId) {
|
||||
final PlatformView platformView = platformViews.get(viewId);
|
||||
if (platformView != null) {
|
||||
platformView.getView().clearFocus();
|
||||
if (platformView == null) {
|
||||
Log.e(TAG, "Clearing focus on an unknown view with id: " + viewId);
|
||||
return;
|
||||
}
|
||||
ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH);
|
||||
View view = vdControllers.get(viewId).getView();
|
||||
view.clearFocus();
|
||||
platformView.getView().clearFocus();
|
||||
}
|
||||
|
||||
private void ensureValidAndroidVersion(int minSdkVersion) {
|
||||
@@ -375,8 +373,7 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
|
||||
};
|
||||
|
||||
@VisibleForTesting
|
||||
public MotionEvent toMotionEvent(
|
||||
float density, PlatformViewsChannel.PlatformViewTouch touch, boolean usingVirtualDiplays) {
|
||||
public MotionEvent toMotionEvent(float density, PlatformViewsChannel.PlatformViewTouch touch) {
|
||||
MotionEventTracker.MotionEventId motionEventId =
|
||||
MotionEventTracker.MotionEventId.from(touch.motionEventId);
|
||||
MotionEvent trackedEvent = motionEventTracker.pop(motionEventId);
|
||||
@@ -392,7 +389,7 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
|
||||
parsePointerCoordsList(touch.rawPointerCoords, density)
|
||||
.toArray(new PointerCoords[touch.pointerCount]);
|
||||
|
||||
if (!usingVirtualDiplays && trackedEvent != null) {
|
||||
if (trackedEvent != null) {
|
||||
return MotionEvent.obtain(
|
||||
trackedEvent.getDownTime(),
|
||||
trackedEvent.getEventTime(),
|
||||
@@ -431,13 +428,11 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
|
||||
|
||||
public PlatformViewsController() {
|
||||
registry = new PlatformViewRegistryImpl();
|
||||
vdControllers = new HashMap<>();
|
||||
accessibilityEventsDelegate = new AccessibilityEventsDelegate();
|
||||
contextToPlatformView = new HashMap<>();
|
||||
overlayLayerViews = new SparseArray<>();
|
||||
currentFrameUsedOverlayLayerIds = new HashSet<>();
|
||||
currentFrameUsedPlatformViewIds = new HashSet<>();
|
||||
|
||||
viewWrappers = new SparseArray<>();
|
||||
platformViews = new SparseArray<>();
|
||||
platformViewParent = new SparseArray<>();
|
||||
|
||||
@@ -489,13 +484,14 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
|
||||
* This {@code PlatformViewsController} and its {@code FlutterEngine} is now attached to an
|
||||
* Android {@code View} that renders a Flutter UI.
|
||||
*/
|
||||
public void attachToView(@NonNull FlutterView flutterView) {
|
||||
this.flutterView = flutterView;
|
||||
public void attachToView(@NonNull FlutterView newFlutterView) {
|
||||
flutterView = newFlutterView;
|
||||
|
||||
// Inform all existing platform views that they are now associated with
|
||||
// a Flutter View.
|
||||
for (VirtualDisplayController controller : vdControllers.values()) {
|
||||
controller.onFlutterViewAttached(flutterView);
|
||||
for (int i = 0; i < platformViews.size(); i++) {
|
||||
final PlatformView view = platformViews.valueAt(i);
|
||||
view.onFlutterViewAttached(flutterView);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,16 +503,16 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
|
||||
* the previously attached {@code View}.
|
||||
*/
|
||||
public void detachFromView() {
|
||||
for (int i = 0; i < platformViews.size(); i++) {
|
||||
final PlatformView view = platformViews.valueAt(i);
|
||||
view.onFlutterViewDetached();
|
||||
}
|
||||
// TODO(egarciad): Remove this.
|
||||
// https://github.com/flutter/flutter/issues/96679
|
||||
destroyOverlaySurfaces();
|
||||
removeOverlaySurfaces();
|
||||
this.flutterView = null;
|
||||
flutterView = null;
|
||||
flutterViewConvertedToImageView = false;
|
||||
|
||||
// Inform all existing platform views that they are no longer associated with
|
||||
// a Flutter View.
|
||||
for (VirtualDisplayController controller : vdControllers.values()) {
|
||||
controller.onFlutterViewDetached();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -547,29 +543,6 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
|
||||
textInputPlugin = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if Flutter should perform input connection proxying for the view.
|
||||
*
|
||||
* <p>If the view is a platform view managed by this platform views controller returns true. Else
|
||||
* if the view was created in a platform view's VD, delegates the decision to the platform view's
|
||||
* {@link View#checkInputConnectionProxy(View)} method. Else returns false.
|
||||
*/
|
||||
public boolean checkInputConnectionProxy(@Nullable View view) {
|
||||
// View can be null on some devices
|
||||
// See: https://github.com/flutter/flutter/issues/36517
|
||||
if (view == null) {
|
||||
return false;
|
||||
}
|
||||
if (!contextToPlatformView.containsKey(view.getContext())) {
|
||||
return false;
|
||||
}
|
||||
View platformView = contextToPlatformView.get(view.getContext());
|
||||
if (platformView == view) {
|
||||
return true;
|
||||
}
|
||||
return platformView.checkInputConnectionProxy(view);
|
||||
}
|
||||
|
||||
public PlatformViewRegistry getRegistry() {
|
||||
return registry;
|
||||
}
|
||||
@@ -587,8 +560,6 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
|
||||
* PlatformViewsController} detaches from JNI.
|
||||
*/
|
||||
public void onDetachedFromJNI() {
|
||||
// Dispose all virtual displays so that any future updates to textures will not be
|
||||
// propagated to the native peer.
|
||||
flushAllViews();
|
||||
}
|
||||
|
||||
@@ -597,37 +568,12 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getPlatformViewById(Integer id) {
|
||||
// Hybrid composition.
|
||||
if (platformViews.get(id) != null) {
|
||||
return platformViews.get(id).getView();
|
||||
}
|
||||
VirtualDisplayController controller = vdControllers.get(id);
|
||||
if (controller == null) {
|
||||
public View getPlatformViewById(int viewId) {
|
||||
final PlatformView platformView = platformViews.get(viewId);
|
||||
if (platformView == null) {
|
||||
return null;
|
||||
}
|
||||
return controller.getView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean usesVirtualDisplay(Integer id) {
|
||||
return vdControllers.containsKey(id);
|
||||
}
|
||||
|
||||
private void lockInputConnection(@NonNull VirtualDisplayController controller) {
|
||||
if (textInputPlugin == null) {
|
||||
return;
|
||||
}
|
||||
textInputPlugin.lockPlatformViewInputConnection();
|
||||
controller.onInputConnectionLocked();
|
||||
}
|
||||
|
||||
private void unlockInputConnection(@NonNull VirtualDisplayController controller) {
|
||||
if (textInputPlugin == null) {
|
||||
return;
|
||||
}
|
||||
textInputPlugin.unlockPlatformViewInputConnection();
|
||||
controller.onInputConnectionUnlocked();
|
||||
return platformView.getView();
|
||||
}
|
||||
|
||||
private static boolean validateDirection(int direction) {
|
||||
@@ -679,29 +625,6 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
|
||||
return coords;
|
||||
}
|
||||
|
||||
// Creating a VirtualDisplay larger than the size of the device screen size
|
||||
// could cause the device to restart: https://github.com/flutter/flutter/issues/28978
|
||||
private void validateVirtualDisplayDimensions(int width, int height) {
|
||||
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
||||
if (height > metrics.heightPixels || width > metrics.widthPixels) {
|
||||
String message =
|
||||
"Creating a virtual display of size: "
|
||||
+ "["
|
||||
+ width
|
||||
+ ", "
|
||||
+ height
|
||||
+ "] may result in problems"
|
||||
+ "(https://github.com/flutter/flutter/issues/2897)."
|
||||
+ "It is larger than the device screen size: "
|
||||
+ "["
|
||||
+ metrics.widthPixels
|
||||
+ ", "
|
||||
+ metrics.heightPixels
|
||||
+ "].";
|
||||
Log.w(TAG, message);
|
||||
}
|
||||
}
|
||||
|
||||
private float getDisplayDensity() {
|
||||
return context.getResources().getDisplayMetrics().density;
|
||||
}
|
||||
@@ -710,18 +633,13 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
|
||||
return (int) Math.round(logicalPixels * getDisplayDensity());
|
||||
}
|
||||
|
||||
private int toLogicalPixels(double physicalPixels) {
|
||||
return (int) Math.round(physicalPixels / getDisplayDensity());
|
||||
}
|
||||
|
||||
private void flushAllViews() {
|
||||
for (VirtualDisplayController controller : vdControllers.values()) {
|
||||
controller.dispose();
|
||||
}
|
||||
vdControllers.clear();
|
||||
|
||||
while (platformViews.size() > 0) {
|
||||
channelHandler.disposeAndroidViewForPlatformView(platformViews.keyAt(0));
|
||||
}
|
||||
|
||||
if (contextToPlatformView.size() > 0) {
|
||||
contextToPlatformView.clear();
|
||||
channelHandler.dispose(platformViews.keyAt(0));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,478 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package io.flutter.plugin.platform;
|
||||
|
||||
import static android.content.Context.WINDOW_SERVICE;
|
||||
import static android.view.View.OnFocusChangeListener;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Presentation;
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.Display;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import io.flutter.Log;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
/*
|
||||
* A presentation used for hosting a single Android view in a virtual display.
|
||||
*
|
||||
* This presentation overrides the WindowManager's addView/removeView/updateViewLayout methods, such that views added
|
||||
* directly to the WindowManager are added as part of the presentation's view hierarchy (to fakeWindowViewGroup).
|
||||
*
|
||||
* The view hierarchy for the presentation is as following:
|
||||
*
|
||||
* rootView
|
||||
* / \
|
||||
* / \
|
||||
* / \
|
||||
* container state.fakeWindowViewGroup
|
||||
* |
|
||||
* EmbeddedView
|
||||
*/
|
||||
@Keep
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
|
||||
class SingleViewPresentation extends Presentation {
|
||||
|
||||
/*
|
||||
* When an embedded view is resized in Flutterverse we move the Android view to a new virtual display
|
||||
* that has the new size. This class keeps the presentation state that moves with the view to the presentation of
|
||||
* the new virtual display.
|
||||
*/
|
||||
static class PresentationState {
|
||||
// The Android view we are embedding in the Flutter app.
|
||||
private PlatformView platformView;
|
||||
|
||||
// The InvocationHandler for a WindowManager proxy. This is essentially the custom window
|
||||
// manager for the
|
||||
// presentation.
|
||||
private WindowManagerHandler windowManagerHandler;
|
||||
|
||||
// Contains views that were added directly to the window manager (e.g
|
||||
// android.widget.PopupWindow).
|
||||
private FakeWindowViewGroup fakeWindowViewGroup;
|
||||
}
|
||||
|
||||
private final PlatformViewFactory viewFactory;
|
||||
|
||||
// A reference to the current accessibility bridge to which accessibility events will be
|
||||
// delegated.
|
||||
private final AccessibilityEventsDelegate accessibilityEventsDelegate;
|
||||
|
||||
private final OnFocusChangeListener focusChangeListener;
|
||||
|
||||
// This is the view id assigned by the Flutter framework to the embedded view, we keep it here
|
||||
// so when we create the platform view we can tell it its view id.
|
||||
private int viewId;
|
||||
|
||||
// This is the creation parameters for the platform view, we keep it here
|
||||
// so when we create the platform view we can tell it its view id.
|
||||
private Object createParams;
|
||||
|
||||
// The root view for the presentation, it has 2 childs: container which contains the embedded
|
||||
// view, and
|
||||
// fakeWindowViewGroup which contains views that were added directly to the presentation's window
|
||||
// manager.
|
||||
private AccessibilityDelegatingFrameLayout rootView;
|
||||
|
||||
// Contains the embedded platform view (platformView.getView()) when it is attached to the
|
||||
// presentation.
|
||||
private FrameLayout container;
|
||||
|
||||
private final PresentationState state;
|
||||
|
||||
private boolean startFocused = false;
|
||||
|
||||
// The context for the application window that hosts FlutterView.
|
||||
private final Context outerContext;
|
||||
|
||||
/**
|
||||
* Creates a presentation that will use the view factory to create a new platform view in the
|
||||
* presentation's onCreate, and attach it.
|
||||
*/
|
||||
public SingleViewPresentation(
|
||||
Context outerContext,
|
||||
Display display,
|
||||
PlatformViewFactory viewFactory,
|
||||
AccessibilityEventsDelegate accessibilityEventsDelegate,
|
||||
int viewId,
|
||||
Object createParams,
|
||||
OnFocusChangeListener focusChangeListener) {
|
||||
super(new ImmContext(outerContext), display);
|
||||
this.viewFactory = viewFactory;
|
||||
this.accessibilityEventsDelegate = accessibilityEventsDelegate;
|
||||
this.viewId = viewId;
|
||||
this.createParams = createParams;
|
||||
this.focusChangeListener = focusChangeListener;
|
||||
this.outerContext = outerContext;
|
||||
state = new PresentationState();
|
||||
getWindow()
|
||||
.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
getWindow().setType(WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a presentation that will attach an already existing view as its root view.
|
||||
*
|
||||
* <p>The display's density must match the density of the context used when the view was created.
|
||||
*/
|
||||
public SingleViewPresentation(
|
||||
Context outerContext,
|
||||
Display display,
|
||||
AccessibilityEventsDelegate accessibilityEventsDelegate,
|
||||
PresentationState state,
|
||||
OnFocusChangeListener focusChangeListener,
|
||||
boolean startFocused) {
|
||||
super(new ImmContext(outerContext), display);
|
||||
this.accessibilityEventsDelegate = accessibilityEventsDelegate;
|
||||
viewFactory = null;
|
||||
this.state = state;
|
||||
this.focusChangeListener = focusChangeListener;
|
||||
this.outerContext = outerContext;
|
||||
getWindow()
|
||||
.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
|
||||
this.startFocused = startFocused;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// This makes sure we preserve alpha for the VD's content.
|
||||
getWindow().setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT));
|
||||
if (state.fakeWindowViewGroup == null) {
|
||||
state.fakeWindowViewGroup = new FakeWindowViewGroup(getContext());
|
||||
}
|
||||
if (state.windowManagerHandler == null) {
|
||||
WindowManager windowManagerDelegate =
|
||||
(WindowManager) getContext().getSystemService(WINDOW_SERVICE);
|
||||
state.windowManagerHandler =
|
||||
new WindowManagerHandler(windowManagerDelegate, state.fakeWindowViewGroup);
|
||||
}
|
||||
|
||||
container = new FrameLayout(getContext());
|
||||
|
||||
// Our base mContext has already been wrapped with an IMM cache at instantiation time, but
|
||||
// we want to wrap it again here to also return state.windowManagerHandler.
|
||||
Context context =
|
||||
new PresentationContext(getContext(), state.windowManagerHandler, outerContext);
|
||||
|
||||
if (state.platformView == null) {
|
||||
state.platformView = viewFactory.create(context, viewId, createParams);
|
||||
}
|
||||
|
||||
View embeddedView = state.platformView.getView();
|
||||
container.addView(embeddedView);
|
||||
rootView =
|
||||
new AccessibilityDelegatingFrameLayout(
|
||||
getContext(), accessibilityEventsDelegate, embeddedView);
|
||||
rootView.addView(container);
|
||||
rootView.addView(state.fakeWindowViewGroup);
|
||||
|
||||
embeddedView.setOnFocusChangeListener(focusChangeListener);
|
||||
rootView.setFocusableInTouchMode(true);
|
||||
if (startFocused) {
|
||||
embeddedView.requestFocus();
|
||||
} else {
|
||||
rootView.requestFocus();
|
||||
}
|
||||
setContentView(rootView);
|
||||
}
|
||||
|
||||
public PresentationState detachState() {
|
||||
container.removeAllViews();
|
||||
rootView.removeAllViews();
|
||||
return state;
|
||||
}
|
||||
|
||||
public PlatformView getView() {
|
||||
if (state.platformView == null) return null;
|
||||
return state.platformView;
|
||||
}
|
||||
|
||||
/*
|
||||
* A view group that implements the same layout protocol that exist between the WindowManager and its direct
|
||||
* children.
|
||||
*
|
||||
* Currently only a subset of the protocol is supported (gravity, x, and y).
|
||||
*/
|
||||
static class FakeWindowViewGroup extends ViewGroup {
|
||||
// Used in onLayout to keep the bounds of the current view.
|
||||
// We keep it as a member to avoid object allocations during onLayout which are discouraged.
|
||||
private final Rect viewBounds;
|
||||
|
||||
// Used in onLayout to keep the bounds of the child views.
|
||||
// We keep it as a member to avoid object allocations during onLayout which are discouraged.
|
||||
private final Rect childRect;
|
||||
|
||||
public FakeWindowViewGroup(Context context) {
|
||||
super(context);
|
||||
viewBounds = new Rect();
|
||||
childRect = new Rect();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
for (int i = 0; i < getChildCount(); i++) {
|
||||
View child = getChildAt(i);
|
||||
WindowManager.LayoutParams params = (WindowManager.LayoutParams) child.getLayoutParams();
|
||||
viewBounds.set(l, t, r, b);
|
||||
Gravity.apply(
|
||||
params.gravity,
|
||||
child.getMeasuredWidth(),
|
||||
child.getMeasuredHeight(),
|
||||
viewBounds,
|
||||
params.x,
|
||||
params.y,
|
||||
childRect);
|
||||
child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
for (int i = 0; i < getChildCount(); i++) {
|
||||
View child = getChildAt(i);
|
||||
child.measure(atMost(widthMeasureSpec), atMost(heightMeasureSpec));
|
||||
}
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
private static int atMost(int measureSpec) {
|
||||
return MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(measureSpec), MeasureSpec.AT_MOST);
|
||||
}
|
||||
}
|
||||
|
||||
/** Answers calls for {@link InputMethodManager} with an instance cached at creation time. */
|
||||
// TODO(mklim): This caches the IMM at construction time and won't pick up any changes. In rare
|
||||
// cases where the FlutterView changes windows this will return an outdated instance. This
|
||||
// should be fixed to instead defer returning the IMM to something that know's FlutterView's
|
||||
// true Context.
|
||||
private static class ImmContext extends ContextWrapper {
|
||||
private @NonNull final InputMethodManager inputMethodManager;
|
||||
|
||||
ImmContext(Context base) {
|
||||
this(base, /*inputMethodManager=*/ null);
|
||||
}
|
||||
|
||||
private ImmContext(Context base, @Nullable InputMethodManager inputMethodManager) {
|
||||
super(base);
|
||||
this.inputMethodManager =
|
||||
inputMethodManager != null
|
||||
? inputMethodManager
|
||||
: (InputMethodManager) base.getSystemService(INPUT_METHOD_SERVICE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getSystemService(String name) {
|
||||
if (INPUT_METHOD_SERVICE.equals(name)) {
|
||||
return inputMethodManager;
|
||||
}
|
||||
return super.getSystemService(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context createDisplayContext(Display display) {
|
||||
Context displayContext = super.createDisplayContext(display);
|
||||
return new ImmContext(displayContext, inputMethodManager);
|
||||
}
|
||||
}
|
||||
|
||||
/** Proxies a Context replacing the WindowManager with our custom instance. */
|
||||
// TODO(mklim): This caches the IMM at construction time and won't pick up any changes. In rare
|
||||
// cases where the FlutterView changes windows this will return an outdated instance. This
|
||||
// should be fixed to instead defer returning the IMM to something that know's FlutterView's
|
||||
// true Context.
|
||||
private static class PresentationContext extends ContextWrapper {
|
||||
private @NonNull final WindowManagerHandler windowManagerHandler;
|
||||
private @Nullable WindowManager windowManager;
|
||||
private final Context flutterAppWindowContext;
|
||||
|
||||
PresentationContext(
|
||||
Context base,
|
||||
@NonNull WindowManagerHandler windowManagerHandler,
|
||||
Context flutterAppWindowContext) {
|
||||
super(base);
|
||||
this.windowManagerHandler = windowManagerHandler;
|
||||
this.flutterAppWindowContext = flutterAppWindowContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getSystemService(String name) {
|
||||
if (WINDOW_SERVICE.equals(name)) {
|
||||
if (isCalledFromAlertDialog()) {
|
||||
// Alert dialogs are showing on top of the entire application and should not be limited to
|
||||
// the virtual
|
||||
// display. If we detect that an android.app.AlertDialog constructor is what's fetching
|
||||
// the window manager
|
||||
// we return the one for the application's window.
|
||||
//
|
||||
// Note that if we don't do this AlertDialog will throw a ClassCastException as down the
|
||||
// line it tries
|
||||
// to case this instance to a WindowManagerImpl which the object returned by
|
||||
// getWindowManager is not
|
||||
// a subclass of.
|
||||
return flutterAppWindowContext.getSystemService(name);
|
||||
}
|
||||
return getWindowManager();
|
||||
}
|
||||
return super.getSystemService(name);
|
||||
}
|
||||
|
||||
private WindowManager getWindowManager() {
|
||||
if (windowManager == null) {
|
||||
windowManager = windowManagerHandler.getWindowManager();
|
||||
}
|
||||
return windowManager;
|
||||
}
|
||||
|
||||
private boolean isCalledFromAlertDialog() {
|
||||
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
|
||||
for (int i = 0; i < stackTraceElements.length && i < 11; i++) {
|
||||
if (stackTraceElements[i].getClassName().equals(AlertDialog.class.getCanonicalName())
|
||||
&& stackTraceElements[i].getMethodName().equals("<init>")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* A dynamic proxy handler for a WindowManager with custom overrides.
|
||||
*
|
||||
* The presentation's window manager delegates all calls to the default window manager.
|
||||
* WindowManager#addView calls triggered by views that are attached to the virtual display are crashing
|
||||
* (see: https://github.com/flutter/flutter/issues/20714). This was triggered when selecting text in an embedded
|
||||
* WebView (as the selection handles are implemented as popup windows).
|
||||
*
|
||||
* This dynamic proxy overrides the addView, removeView, removeViewImmediate, and updateViewLayout methods
|
||||
* to prevent these crashes.
|
||||
*
|
||||
* This will be more efficient as a static proxy that's not using reflection, but as the engine is currently
|
||||
* not being built against the latest Android SDK we cannot override all relevant method.
|
||||
* Tracking issue for upgrading the engine's Android sdk: https://github.com/flutter/flutter/issues/20717
|
||||
*/
|
||||
static class WindowManagerHandler implements InvocationHandler {
|
||||
private static final String TAG = "PlatformViewsController";
|
||||
|
||||
private final WindowManager delegate;
|
||||
FakeWindowViewGroup fakeWindowRootView;
|
||||
|
||||
WindowManagerHandler(WindowManager delegate, FakeWindowViewGroup fakeWindowViewGroup) {
|
||||
this.delegate = delegate;
|
||||
fakeWindowRootView = fakeWindowViewGroup;
|
||||
}
|
||||
|
||||
public WindowManager getWindowManager() {
|
||||
return (WindowManager)
|
||||
Proxy.newProxyInstance(
|
||||
WindowManager.class.getClassLoader(), new Class<?>[] {WindowManager.class}, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||
switch (method.getName()) {
|
||||
case "addView":
|
||||
addView(args);
|
||||
return null;
|
||||
case "removeView":
|
||||
removeView(args);
|
||||
return null;
|
||||
case "removeViewImmediate":
|
||||
removeViewImmediate(args);
|
||||
return null;
|
||||
case "updateViewLayout":
|
||||
updateViewLayout(args);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return method.invoke(delegate, args);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw e.getCause();
|
||||
}
|
||||
}
|
||||
|
||||
private void addView(Object[] args) {
|
||||
if (fakeWindowRootView == null) {
|
||||
Log.w(TAG, "Embedded view called addView while detached from presentation");
|
||||
return;
|
||||
}
|
||||
View view = (View) args[0];
|
||||
WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) args[1];
|
||||
fakeWindowRootView.addView(view, layoutParams);
|
||||
}
|
||||
|
||||
private void removeView(Object[] args) {
|
||||
if (fakeWindowRootView == null) {
|
||||
Log.w(TAG, "Embedded view called removeView while detached from presentation");
|
||||
return;
|
||||
}
|
||||
View view = (View) args[0];
|
||||
fakeWindowRootView.removeView(view);
|
||||
}
|
||||
|
||||
private void removeViewImmediate(Object[] args) {
|
||||
if (fakeWindowRootView == null) {
|
||||
Log.w(TAG, "Embedded view called removeViewImmediate while detached from presentation");
|
||||
return;
|
||||
}
|
||||
View view = (View) args[0];
|
||||
view.clearAnimation();
|
||||
fakeWindowRootView.removeView(view);
|
||||
}
|
||||
|
||||
private void updateViewLayout(Object[] args) {
|
||||
if (fakeWindowRootView == null) {
|
||||
Log.w(TAG, "Embedded view called updateViewLayout while detached from presentation");
|
||||
return;
|
||||
}
|
||||
View view = (View) args[0];
|
||||
WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) args[1];
|
||||
fakeWindowRootView.updateViewLayout(view, layoutParams);
|
||||
}
|
||||
}
|
||||
|
||||
private static class AccessibilityDelegatingFrameLayout extends FrameLayout {
|
||||
private final AccessibilityEventsDelegate accessibilityEventsDelegate;
|
||||
private final View embeddedView;
|
||||
|
||||
public AccessibilityDelegatingFrameLayout(
|
||||
Context context,
|
||||
AccessibilityEventsDelegate accessibilityEventsDelegate,
|
||||
View embeddedView) {
|
||||
super(context);
|
||||
this.accessibilityEventsDelegate = accessibilityEventsDelegate;
|
||||
this.embeddedView = embeddedView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) {
|
||||
return accessibilityEventsDelegate.requestSendAccessibilityEvent(embeddedView, child, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package io.flutter.plugin.platform;
|
||||
|
||||
import static android.view.View.OnFocusChangeListener;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.hardware.display.VirtualDisplay;
|
||||
import android.os.Build;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import io.flutter.view.TextureRegistry;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT_WATCH)
|
||||
class VirtualDisplayController {
|
||||
|
||||
public static VirtualDisplayController create(
|
||||
Context context,
|
||||
AccessibilityEventsDelegate accessibilityEventsDelegate,
|
||||
PlatformViewFactory viewFactory,
|
||||
TextureRegistry.SurfaceTextureEntry textureEntry,
|
||||
int width,
|
||||
int height,
|
||||
int viewId,
|
||||
Object createParams,
|
||||
OnFocusChangeListener focusChangeListener) {
|
||||
textureEntry.surfaceTexture().setDefaultBufferSize(width, height);
|
||||
Surface surface = new Surface(textureEntry.surfaceTexture());
|
||||
DisplayManager displayManager =
|
||||
(DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
|
||||
|
||||
int densityDpi = context.getResources().getDisplayMetrics().densityDpi;
|
||||
VirtualDisplay virtualDisplay =
|
||||
displayManager.createVirtualDisplay("flutter-vd", width, height, densityDpi, surface, 0);
|
||||
|
||||
if (virtualDisplay == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new VirtualDisplayController(
|
||||
context,
|
||||
accessibilityEventsDelegate,
|
||||
virtualDisplay,
|
||||
viewFactory,
|
||||
surface,
|
||||
textureEntry,
|
||||
focusChangeListener,
|
||||
viewId,
|
||||
createParams);
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
private final AccessibilityEventsDelegate accessibilityEventsDelegate;
|
||||
private final int densityDpi;
|
||||
private final TextureRegistry.SurfaceTextureEntry textureEntry;
|
||||
private final OnFocusChangeListener focusChangeListener;
|
||||
private VirtualDisplay virtualDisplay;
|
||||
@VisibleForTesting SingleViewPresentation presentation;
|
||||
private final Surface surface;
|
||||
|
||||
private VirtualDisplayController(
|
||||
Context context,
|
||||
AccessibilityEventsDelegate accessibilityEventsDelegate,
|
||||
VirtualDisplay virtualDisplay,
|
||||
PlatformViewFactory viewFactory,
|
||||
Surface surface,
|
||||
TextureRegistry.SurfaceTextureEntry textureEntry,
|
||||
OnFocusChangeListener focusChangeListener,
|
||||
int viewId,
|
||||
Object createParams) {
|
||||
this.context = context;
|
||||
this.accessibilityEventsDelegate = accessibilityEventsDelegate;
|
||||
this.textureEntry = textureEntry;
|
||||
this.focusChangeListener = focusChangeListener;
|
||||
this.surface = surface;
|
||||
this.virtualDisplay = virtualDisplay;
|
||||
densityDpi = context.getResources().getDisplayMetrics().densityDpi;
|
||||
presentation =
|
||||
new SingleViewPresentation(
|
||||
context,
|
||||
this.virtualDisplay.getDisplay(),
|
||||
viewFactory,
|
||||
accessibilityEventsDelegate,
|
||||
viewId,
|
||||
createParams,
|
||||
focusChangeListener);
|
||||
presentation.show();
|
||||
}
|
||||
|
||||
public void resize(final int width, final int height, final Runnable onNewSizeFrameAvailable) {
|
||||
boolean isFocused = getView().isFocused();
|
||||
final SingleViewPresentation.PresentationState presentationState = presentation.detachState();
|
||||
// We detach the surface to prevent it being destroyed when releasing the vd.
|
||||
//
|
||||
// setSurface is only available starting API 20. We could support API 19 by re-creating a new
|
||||
// SurfaceTexture here. This will require refactoring the TextureRegistry to allow recycling
|
||||
// texture
|
||||
// entry IDs.
|
||||
virtualDisplay.setSurface(null);
|
||||
virtualDisplay.release();
|
||||
|
||||
textureEntry.surfaceTexture().setDefaultBufferSize(width, height);
|
||||
DisplayManager displayManager =
|
||||
(DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
|
||||
virtualDisplay =
|
||||
displayManager.createVirtualDisplay("flutter-vd", width, height, densityDpi, surface, 0);
|
||||
|
||||
final View embeddedView = getView();
|
||||
// There's a bug in Android version older than O where view tree observer onDrawListeners don't
|
||||
// get properly
|
||||
// merged when attaching to window, as a workaround we register the on draw listener after the
|
||||
// view is attached.
|
||||
embeddedView.addOnAttachStateChangeListener(
|
||||
new View.OnAttachStateChangeListener() {
|
||||
@Override
|
||||
public void onViewAttachedToWindow(View v) {
|
||||
OneTimeOnDrawListener.schedule(
|
||||
embeddedView,
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// We need some delay here until the frame propagates through the vd surface to
|
||||
// to the texture,
|
||||
// 128ms was picked pretty arbitrarily based on trial and error.
|
||||
// As long as we invoke the runnable after a new frame is available we avoid the
|
||||
// scaling jank
|
||||
// described in: https://github.com/flutter/flutter/issues/19572
|
||||
// We should ideally run onNewSizeFrameAvailable ASAP to make the embedded view
|
||||
// more responsive
|
||||
// following a resize.
|
||||
embeddedView.postDelayed(onNewSizeFrameAvailable, 128);
|
||||
}
|
||||
});
|
||||
embeddedView.removeOnAttachStateChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(View v) {}
|
||||
});
|
||||
|
||||
// Create a new SingleViewPresentation and show() it before we cancel() the existing
|
||||
// presentation. Calling show() and cancel() in this order fixes
|
||||
// https://github.com/flutter/flutter/issues/26345 and maintains seamless transition
|
||||
// of the contents of the presentation.
|
||||
SingleViewPresentation newPresentation =
|
||||
new SingleViewPresentation(
|
||||
context,
|
||||
virtualDisplay.getDisplay(),
|
||||
accessibilityEventsDelegate,
|
||||
presentationState,
|
||||
focusChangeListener,
|
||||
isFocused);
|
||||
newPresentation.show();
|
||||
presentation.cancel();
|
||||
presentation = newPresentation;
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
PlatformView view = presentation.getView();
|
||||
// Fix rare crash on HuaWei device described in: https://github.com/flutter/engine/pull/9192
|
||||
presentation.cancel();
|
||||
presentation.detachState();
|
||||
view.dispose();
|
||||
virtualDisplay.release();
|
||||
textureEntry.release();
|
||||
}
|
||||
|
||||
/** See {@link PlatformView#onFlutterViewAttached(View)} */
|
||||
/*package*/ void onFlutterViewAttached(@NonNull View flutterView) {
|
||||
if (presentation == null || presentation.getView() == null) {
|
||||
return;
|
||||
}
|
||||
presentation.getView().onFlutterViewAttached(flutterView);
|
||||
}
|
||||
|
||||
/** See {@link PlatformView#onFlutterViewDetached()} */
|
||||
/*package*/ void onFlutterViewDetached() {
|
||||
if (presentation == null || presentation.getView() == null) {
|
||||
return;
|
||||
}
|
||||
presentation.getView().onFlutterViewDetached();
|
||||
}
|
||||
|
||||
/*package*/ void onInputConnectionLocked() {
|
||||
if (presentation == null || presentation.getView() == null) {
|
||||
return;
|
||||
}
|
||||
presentation.getView().onInputConnectionLocked();
|
||||
}
|
||||
|
||||
/*package*/ void onInputConnectionUnlocked() {
|
||||
if (presentation == null || presentation.getView() == null) {
|
||||
return;
|
||||
}
|
||||
presentation.getView().onInputConnectionUnlocked();
|
||||
}
|
||||
|
||||
public View getView() {
|
||||
if (presentation == null) return null;
|
||||
PlatformView platformView = presentation.getView();
|
||||
return platformView.getView();
|
||||
}
|
||||
|
||||
/** Dispatches a motion event to the presentation for this controller. */
|
||||
public void dispatchTouchEvent(MotionEvent event) {
|
||||
if (presentation == null) return;
|
||||
presentation.dispatchTouchEvent(event);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||
static class OneTimeOnDrawListener implements ViewTreeObserver.OnDrawListener {
|
||||
static void schedule(View view, Runnable runnable) {
|
||||
OneTimeOnDrawListener listener = new OneTimeOnDrawListener(view, runnable);
|
||||
view.getViewTreeObserver().addOnDrawListener(listener);
|
||||
}
|
||||
|
||||
final View mView;
|
||||
Runnable mOnDrawRunnable;
|
||||
|
||||
OneTimeOnDrawListener(View view, Runnable onDrawRunnable) {
|
||||
this.mView = view;
|
||||
this.mOnDrawRunnable = onDrawRunnable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw() {
|
||||
if (mOnDrawRunnable == null) {
|
||||
return;
|
||||
}
|
||||
mOnDrawRunnable.run();
|
||||
mOnDrawRunnable = null;
|
||||
mView.post(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mView.getViewTreeObserver().removeOnDrawListener(OneTimeOnDrawListener.this);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public final class ViewUtils {
|
||||
/**
|
||||
@@ -45,4 +47,28 @@ public final class ViewUtils {
|
||||
}
|
||||
return fallbackId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the current view or any descendant view has focus.
|
||||
*
|
||||
* @param root The root view.
|
||||
* @return True if the current view or any descendant view has focus.
|
||||
*/
|
||||
public static boolean childHasFocus(@Nullable View root) {
|
||||
if (root == null) {
|
||||
return false;
|
||||
}
|
||||
if (root.hasFocus()) {
|
||||
return true;
|
||||
}
|
||||
if (root instanceof ViewGroup) {
|
||||
final ViewGroup viewGroup = (ViewGroup) root;
|
||||
for (int idx = 0; idx < viewGroup.getChildCount(); idx++) {
|
||||
if (childHasFocus(viewGroup.getChildAt(idx))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -573,23 +573,6 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate accessibility node for platform views using a virtual display.
|
||||
//
|
||||
// In this case, register the accessibility node in the view embedder,
|
||||
// so the accessibility tree can be mirrored as a subtree of the Flutter accessibility tree.
|
||||
// This is in constrast to hybrid composition where the embedded view is in the view hiearchy,
|
||||
// so it doesn't need to be mirrored.
|
||||
//
|
||||
// See the case down below for how hybrid composition is handled.
|
||||
if (semanticsNode.platformViewId != -1) {
|
||||
View embeddedView =
|
||||
platformViewsAccessibilityDelegate.getPlatformViewById(semanticsNode.platformViewId);
|
||||
if (platformViewsAccessibilityDelegate.usesVirtualDisplay(semanticsNode.platformViewId)) {
|
||||
Rect bounds = semanticsNode.getGlobalRect();
|
||||
return accessibilityViewEmbedder.getRootNode(embeddedView, semanticsNode.id, bounds);
|
||||
}
|
||||
}
|
||||
|
||||
AccessibilityNodeInfo result =
|
||||
obtainAccessibilityNodeInfo(rootAccessibilityView, virtualViewId);
|
||||
// Work around for https://github.com/flutter/flutter/issues/2101
|
||||
@@ -904,17 +887,10 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
|
||||
// Add the embedded view as a child of the current accessibility node if it's using
|
||||
// hybrid composition.
|
||||
//
|
||||
// In this case, the view is in the Activity's view hierarchy, so it doesn't need to be
|
||||
// mirrored.
|
||||
//
|
||||
// See the case above for how virtual displays are handled.
|
||||
if (!platformViewsAccessibilityDelegate.usesVirtualDisplay(child.platformViewId)) {
|
||||
result.addChild(embeddedView);
|
||||
continue;
|
||||
}
|
||||
result.addChild(embeddedView);
|
||||
} else {
|
||||
result.addChild(rootAccessibilityView, child.id);
|
||||
}
|
||||
result.addChild(rootAccessibilityView, child.id);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1545,8 +1521,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
if (semanticsNode.hadPreviousConfig) {
|
||||
updated.add(semanticsNode);
|
||||
}
|
||||
if (semanticsNode.platformViewId != -1
|
||||
&& !platformViewsAccessibilityDelegate.usesVirtualDisplay(semanticsNode.platformViewId)) {
|
||||
if (semanticsNode.platformViewId != -1) {
|
||||
View embeddedView =
|
||||
platformViewsAccessibilityDelegate.getPlatformViewById(semanticsNode.platformViewId);
|
||||
if (embeddedView != null) {
|
||||
@@ -1958,9 +1933,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
embeddedAccessibilityFocusedNodeId = null;
|
||||
}
|
||||
|
||||
if (semanticsNodeToBeRemoved.platformViewId != -1
|
||||
&& !platformViewsAccessibilityDelegate.usesVirtualDisplay(
|
||||
semanticsNodeToBeRemoved.platformViewId)) {
|
||||
if (semanticsNodeToBeRemoved.platformViewId != -1) {
|
||||
View embeddedView =
|
||||
platformViewsAccessibilityDelegate.getPlatformViewById(
|
||||
semanticsNodeToBeRemoved.platformViewId);
|
||||
|
||||
@@ -26,7 +26,6 @@ import android.view.PointerIcon;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.ViewStructure;
|
||||
import android.view.WindowInsets;
|
||||
@@ -427,14 +426,6 @@ public class FlutterView extends SurfaceView
|
||||
return mTextInputPlugin.createInputConnection(this, mKeyboardManager, outAttrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkInputConnectionProxy(View view) {
|
||||
return mNativeView
|
||||
.getPluginRegistry()
|
||||
.getPlatformViewsController()
|
||||
.checkInputConnectionProxy(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) {
|
||||
super.onProvideAutofillVirtualStructure(structure, flags);
|
||||
|
||||
@@ -6,8 +6,6 @@ import static org.mockito.Mockito.*;
|
||||
|
||||
import android.graphics.Matrix;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import io.flutter.embedding.android.AndroidTouchProcessor;
|
||||
@@ -81,49 +79,6 @@ public class FlutterMutatorViewTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void childHasFocus_rootHasFocus() {
|
||||
final View rootView = mock(View.class);
|
||||
when(rootView.hasFocus()).thenReturn(true);
|
||||
assertTrue(FlutterMutatorView.childHasFocus(rootView));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void childHasFocus_rootDoesNotHaveFocus() {
|
||||
final View rootView = mock(View.class);
|
||||
when(rootView.hasFocus()).thenReturn(false);
|
||||
assertFalse(FlutterMutatorView.childHasFocus(rootView));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void childHasFocus_rootIsNull() {
|
||||
assertFalse(FlutterMutatorView.childHasFocus(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void childHasFocus_childHasFocus() {
|
||||
final View childView = mock(View.class);
|
||||
when(childView.hasFocus()).thenReturn(true);
|
||||
|
||||
final ViewGroup rootView = mock(ViewGroup.class);
|
||||
when(rootView.getChildCount()).thenReturn(1);
|
||||
when(rootView.getChildAt(0)).thenReturn(childView);
|
||||
|
||||
assertTrue(FlutterMutatorView.childHasFocus(rootView));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void childHasFocus_childDoesNotHaveFocus() {
|
||||
final View childView = mock(View.class);
|
||||
when(childView.hasFocus()).thenReturn(false);
|
||||
|
||||
final ViewGroup rootView = mock(ViewGroup.class);
|
||||
when(rootView.getChildCount()).thenReturn(1);
|
||||
when(rootView.getChildAt(0)).thenReturn(childView);
|
||||
|
||||
assertFalse(FlutterMutatorView.childHasFocus(rootView));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void focusChangeListener_hasFocus() {
|
||||
final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
package io.flutter.plugin.platform;
|
||||
|
||||
import static android.view.View.OnFocusChangeListener;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.graphics.BlendMode;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
|
||||
@TargetApi(31)
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class PlatformViewWrapperTest {
|
||||
@Test
|
||||
public void setTexture_writesToBuffer() {
|
||||
final Surface surface = mock(Surface.class);
|
||||
final Context ctx = ApplicationProvider.getApplicationContext();
|
||||
final PlatformViewWrapper wrapper =
|
||||
new PlatformViewWrapper(ctx) {
|
||||
@Override
|
||||
protected Surface createSurface(@NonNull SurfaceTexture tx) {
|
||||
return surface;
|
||||
}
|
||||
};
|
||||
|
||||
final SurfaceTexture tx = mock(SurfaceTexture.class);
|
||||
when(tx.isReleased()).thenReturn(false);
|
||||
|
||||
final Canvas canvas = mock(Canvas.class);
|
||||
when(surface.lockHardwareCanvas()).thenReturn(canvas);
|
||||
|
||||
// Test.
|
||||
wrapper.setTexture(tx);
|
||||
|
||||
// Verify.
|
||||
verify(surface, times(1)).lockHardwareCanvas();
|
||||
verify(surface, times(1)).unlockCanvasAndPost(canvas);
|
||||
verify(canvas, times(1)).drawColor(Color.TRANSPARENT, BlendMode.CLEAR);
|
||||
verifyNoMoreInteractions(surface);
|
||||
verifyNoMoreInteractions(canvas);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void draw_writesToBuffer() {
|
||||
final Surface surface = mock(Surface.class);
|
||||
final Context ctx = ApplicationProvider.getApplicationContext();
|
||||
final PlatformViewWrapper wrapper =
|
||||
new PlatformViewWrapper(ctx) {
|
||||
@Override
|
||||
protected Surface createSurface(@NonNull SurfaceTexture tx) {
|
||||
return surface;
|
||||
}
|
||||
};
|
||||
|
||||
wrapper.addView(
|
||||
new View(ctx) {
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
super.draw(canvas);
|
||||
canvas.drawColor(Color.RED);
|
||||
}
|
||||
});
|
||||
|
||||
final int size = 100;
|
||||
wrapper.measure(size, size);
|
||||
wrapper.layout(0, 0, size, size);
|
||||
|
||||
final SurfaceTexture tx = mock(SurfaceTexture.class);
|
||||
when(tx.isReleased()).thenReturn(false);
|
||||
|
||||
when(surface.lockHardwareCanvas()).thenReturn(mock(Canvas.class));
|
||||
|
||||
wrapper.setTexture(tx);
|
||||
|
||||
reset(surface);
|
||||
|
||||
final Canvas canvas = mock(Canvas.class);
|
||||
when(surface.lockHardwareCanvas()).thenReturn(canvas);
|
||||
when(surface.isValid()).thenReturn(true);
|
||||
|
||||
// Test.
|
||||
wrapper.invalidate();
|
||||
wrapper.draw(new Canvas());
|
||||
|
||||
// Verify.
|
||||
verify(canvas, times(1)).drawColor(Color.TRANSPARENT, BlendMode.CLEAR);
|
||||
verify(surface, times(1)).isValid();
|
||||
verify(surface, times(1)).lockHardwareCanvas();
|
||||
verify(surface, times(1)).unlockCanvasAndPost(canvas);
|
||||
verifyNoMoreInteractions(surface);
|
||||
verifyNoMoreInteractions(canvas);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void release() {
|
||||
final Surface surface = mock(Surface.class);
|
||||
final Context ctx = ApplicationProvider.getApplicationContext();
|
||||
final PlatformViewWrapper wrapper =
|
||||
new PlatformViewWrapper(ctx) {
|
||||
@Override
|
||||
protected Surface createSurface(@NonNull SurfaceTexture tx) {
|
||||
return surface;
|
||||
}
|
||||
};
|
||||
|
||||
final SurfaceTexture tx = mock(SurfaceTexture.class);
|
||||
when(tx.isReleased()).thenReturn(false);
|
||||
|
||||
final Canvas canvas = mock(Canvas.class);
|
||||
when(surface.lockHardwareCanvas()).thenReturn(canvas);
|
||||
|
||||
wrapper.setTexture(tx);
|
||||
reset(surface);
|
||||
reset(tx);
|
||||
|
||||
// Test.
|
||||
wrapper.release();
|
||||
|
||||
// Verify.
|
||||
verify(surface, times(1)).release();
|
||||
verifyNoMoreInteractions(surface);
|
||||
verifyNoMoreInteractions(tx);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void focusChangeListener_hasFocus() {
|
||||
final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
|
||||
when(viewTreeObserver.isAlive()).thenReturn(true);
|
||||
|
||||
final PlatformViewWrapper view =
|
||||
new PlatformViewWrapper(RuntimeEnvironment.application) {
|
||||
@Override
|
||||
public ViewTreeObserver getViewTreeObserver() {
|
||||
return viewTreeObserver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasFocus() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
final OnFocusChangeListener focusListener = mock(OnFocusChangeListener.class);
|
||||
view.setOnDescendantFocusChangeListener(focusListener);
|
||||
|
||||
final ArgumentCaptor<ViewTreeObserver.OnGlobalFocusChangeListener> focusListenerCaptor =
|
||||
ArgumentCaptor.forClass(ViewTreeObserver.OnGlobalFocusChangeListener.class);
|
||||
verify(viewTreeObserver).addOnGlobalFocusChangeListener(focusListenerCaptor.capture());
|
||||
|
||||
focusListenerCaptor.getValue().onGlobalFocusChanged(null, null);
|
||||
verify(focusListener).onFocusChange(view, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void focusChangeListener_doesNotHaveFocus() {
|
||||
final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
|
||||
when(viewTreeObserver.isAlive()).thenReturn(true);
|
||||
|
||||
final PlatformViewWrapper view =
|
||||
new PlatformViewWrapper(RuntimeEnvironment.application) {
|
||||
@Override
|
||||
public ViewTreeObserver getViewTreeObserver() {
|
||||
return viewTreeObserver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasFocus() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
final OnFocusChangeListener focusListener = mock(OnFocusChangeListener.class);
|
||||
view.setOnDescendantFocusChangeListener(focusListener);
|
||||
|
||||
final ArgumentCaptor<ViewTreeObserver.OnGlobalFocusChangeListener> focusListenerCaptor =
|
||||
ArgumentCaptor.forClass(ViewTreeObserver.OnGlobalFocusChangeListener.class);
|
||||
verify(viewTreeObserver).addOnGlobalFocusChangeListener(focusListenerCaptor.capture());
|
||||
|
||||
focusListenerCaptor.getValue().onGlobalFocusChanged(null, null);
|
||||
verify(focusListener).onFocusChange(view, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void focusChangeListener_viewTreeObserverIsAliveFalseDoesNotThrow() {
|
||||
final PlatformViewWrapper view =
|
||||
new PlatformViewWrapper(RuntimeEnvironment.application) {
|
||||
@Override
|
||||
public ViewTreeObserver getViewTreeObserver() {
|
||||
final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
|
||||
when(viewTreeObserver.isAlive()).thenReturn(false);
|
||||
return viewTreeObserver;
|
||||
}
|
||||
};
|
||||
view.setOnDescendantFocusChangeListener(mock(OnFocusChangeListener.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setOnDescendantFocusChangeListener_keepsSingleListener() {
|
||||
final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
|
||||
when(viewTreeObserver.isAlive()).thenReturn(true);
|
||||
|
||||
final PlatformViewWrapper view =
|
||||
new PlatformViewWrapper(RuntimeEnvironment.application) {
|
||||
@Override
|
||||
public ViewTreeObserver getViewTreeObserver() {
|
||||
return viewTreeObserver;
|
||||
}
|
||||
};
|
||||
|
||||
assertNull(view.activeFocusListener);
|
||||
|
||||
view.setOnDescendantFocusChangeListener(mock(OnFocusChangeListener.class));
|
||||
assertNotNull(view.activeFocusListener);
|
||||
|
||||
final ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener =
|
||||
view.activeFocusListener;
|
||||
|
||||
view.setOnDescendantFocusChangeListener(mock(OnFocusChangeListener.class));
|
||||
assertNotNull(view.activeFocusListener);
|
||||
|
||||
verify(viewTreeObserver, times(1)).removeOnGlobalFocusChangeListener(activeFocusListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unsetOnDescendantFocusChangeListener_removesActiveListener() {
|
||||
final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
|
||||
when(viewTreeObserver.isAlive()).thenReturn(true);
|
||||
|
||||
final PlatformViewWrapper view =
|
||||
new PlatformViewWrapper(RuntimeEnvironment.application) {
|
||||
@Override
|
||||
public ViewTreeObserver getViewTreeObserver() {
|
||||
return viewTreeObserver;
|
||||
}
|
||||
};
|
||||
|
||||
assertNull(view.activeFocusListener);
|
||||
|
||||
view.setOnDescendantFocusChangeListener(mock(OnFocusChangeListener.class));
|
||||
assertNotNull(view.activeFocusListener);
|
||||
|
||||
final ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener =
|
||||
view.activeFocusListener;
|
||||
|
||||
view.unsetOnDescendantFocusChangeListener();
|
||||
assertNull(view.activeFocusListener);
|
||||
|
||||
view.unsetOnDescendantFocusChangeListener();
|
||||
verify(viewTreeObserver, times(1)).removeOnGlobalFocusChangeListener(activeFocusListener);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
import android.view.ViewParent;
|
||||
import android.widget.FrameLayout.LayoutParams;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import io.flutter.embedding.android.FlutterImageView;
|
||||
import io.flutter.embedding.android.FlutterView;
|
||||
@@ -56,120 +55,6 @@ import org.robolectric.shadows.ShadowSurfaceView;
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class PlatformViewsControllerTest {
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
public void itNotifiesVirtualDisplayControllersOfViewAttachmentAndDetachment() {
|
||||
// Setup test structure.
|
||||
// Create a fake View that represents the View that renders a Flutter UI.
|
||||
FlutterView fakeFlutterView = new FlutterView(RuntimeEnvironment.systemContext);
|
||||
|
||||
// Create fake VirtualDisplayControllers. This requires internal knowledge of
|
||||
// PlatformViewsController. We know that all PlatformViewsController does is
|
||||
// forward view attachment/detachment calls to it's VirtualDisplayControllers.
|
||||
//
|
||||
// TODO(mattcarroll): once PlatformViewsController is refactored into testable
|
||||
// pieces, remove this test and avoid verifying private behavior.
|
||||
VirtualDisplayController fakeVdController1 = mock(VirtualDisplayController.class);
|
||||
VirtualDisplayController fakeVdController2 = mock(VirtualDisplayController.class);
|
||||
|
||||
// Create the PlatformViewsController that is under test.
|
||||
PlatformViewsController platformViewsController = new PlatformViewsController();
|
||||
|
||||
// Manually inject fake VirtualDisplayControllers into the PlatformViewsController.
|
||||
platformViewsController.vdControllers.put(0, fakeVdController1);
|
||||
platformViewsController.vdControllers.put(1, fakeVdController1);
|
||||
|
||||
// Execute test & verify results.
|
||||
// Attach PlatformViewsController to the fake Flutter View.
|
||||
platformViewsController.attachToView(fakeFlutterView);
|
||||
|
||||
// Verify that all virtual display controllers were notified of View attachment.
|
||||
verify(fakeVdController1, times(1)).onFlutterViewAttached(eq(fakeFlutterView));
|
||||
verify(fakeVdController1, never()).onFlutterViewDetached();
|
||||
verify(fakeVdController2, times(1)).onFlutterViewAttached(eq(fakeFlutterView));
|
||||
verify(fakeVdController2, never()).onFlutterViewDetached();
|
||||
|
||||
// Detach PlatformViewsController from the fake Flutter View.
|
||||
platformViewsController.detachFromView();
|
||||
|
||||
// Verify that all virtual display controllers were notified of the View detachment.
|
||||
verify(fakeVdController1, times(1)).onFlutterViewAttached(eq(fakeFlutterView));
|
||||
verify(fakeVdController1, times(1)).onFlutterViewDetached();
|
||||
verify(fakeVdController2, times(1)).onFlutterViewAttached(eq(fakeFlutterView));
|
||||
verify(fakeVdController2, times(1)).onFlutterViewDetached();
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
public void itCancelsOldPresentationOnResize() {
|
||||
// Setup test structure.
|
||||
// Create a fake View that represents the View that renders a Flutter UI.
|
||||
View fakeFlutterView = new View(RuntimeEnvironment.systemContext);
|
||||
|
||||
// Create fake VirtualDisplayControllers. This requires internal knowledge of
|
||||
// PlatformViewsController. We know that all PlatformViewsController does is
|
||||
// forward view attachment/detachment calls to it's VirtualDisplayControllers.
|
||||
//
|
||||
// TODO(mattcarroll): once PlatformViewsController is refactored into testable
|
||||
// pieces, remove this test and avoid verifying private behavior.
|
||||
VirtualDisplayController fakeVdController1 = mock(VirtualDisplayController.class);
|
||||
|
||||
SingleViewPresentation presentation = fakeVdController1.presentation;
|
||||
|
||||
fakeVdController1.resize(10, 10, null);
|
||||
|
||||
assertEquals(fakeVdController1.presentation != presentation, true);
|
||||
assertEquals(presentation.isShowing(), false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itUsesActionEventTypeFromFrameworkEventForVirtualDisplays() {
|
||||
MotionEventTracker motionEventTracker = MotionEventTracker.getInstance();
|
||||
PlatformViewsController platformViewsController = new PlatformViewsController();
|
||||
|
||||
MotionEvent original =
|
||||
MotionEvent.obtain(
|
||||
100, // downTime
|
||||
100, // eventTime
|
||||
1, // action
|
||||
0, // x
|
||||
0, // y
|
||||
0 // metaState
|
||||
);
|
||||
|
||||
// track an event that will later get passed to us from framework
|
||||
MotionEventTracker.MotionEventId motionEventId = motionEventTracker.track(original);
|
||||
|
||||
PlatformViewTouch frameWorkTouch =
|
||||
new PlatformViewTouch(
|
||||
0, // viewId
|
||||
original.getDownTime(),
|
||||
original.getEventTime(),
|
||||
2, // action
|
||||
1, // pointerCount
|
||||
Arrays.asList(Arrays.asList(0, 0)), // pointer properties
|
||||
Arrays.asList(Arrays.asList(0., 1., 2., 3., 4., 5., 6., 7., 8.)), // pointer coords
|
||||
original.getMetaState(),
|
||||
original.getButtonState(),
|
||||
original.getXPrecision(),
|
||||
original.getYPrecision(),
|
||||
original.getDeviceId(),
|
||||
original.getEdgeFlags(),
|
||||
original.getSource(),
|
||||
original.getFlags(),
|
||||
motionEventId.getId());
|
||||
|
||||
MotionEvent resolvedEvent =
|
||||
platformViewsController.toMotionEvent(
|
||||
1, // density
|
||||
frameWorkTouch,
|
||||
true // usingVirtualDisplays
|
||||
);
|
||||
|
||||
assertEquals(resolvedEvent.getAction(), frameWorkTouch.action);
|
||||
assertNotEquals(resolvedEvent.getAction(), original.getAction());
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
public void itUsesActionEventTypeFromMotionEventForHybridPlatformViews() {
|
||||
@@ -209,11 +94,7 @@ public class PlatformViewsControllerTest {
|
||||
motionEventId.getId());
|
||||
|
||||
MotionEvent resolvedEvent =
|
||||
platformViewsController.toMotionEvent(
|
||||
1, // density
|
||||
frameWorkTouch,
|
||||
false // usingVirtualDisplays
|
||||
);
|
||||
platformViewsController.toMotionEvent(/*density=*/ 1, frameWorkTouch);
|
||||
|
||||
assertNotEquals(resolvedEvent.getAction(), frameWorkTouch.action);
|
||||
assertEquals(resolvedEvent.getAction(), original.getAction());
|
||||
@@ -297,66 +178,6 @@ public class PlatformViewsControllerTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class})
|
||||
public void onDetachedFromJNI_clearsPlatformViewContext() {
|
||||
PlatformViewsController platformViewsController = new PlatformViewsController();
|
||||
|
||||
int platformViewId = 0;
|
||||
assertNull(platformViewsController.getPlatformViewById(platformViewId));
|
||||
|
||||
PlatformViewFactory viewFactory = mock(PlatformViewFactory.class);
|
||||
PlatformView platformView = mock(PlatformView.class);
|
||||
|
||||
View pv = mock(View.class);
|
||||
when(pv.getLayoutParams()).thenReturn(new LayoutParams(1, 1));
|
||||
|
||||
when(platformView.getView()).thenReturn(pv);
|
||||
when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView);
|
||||
platformViewsController.getRegistry().registerViewFactory("testType", viewFactory);
|
||||
|
||||
FlutterJNI jni = new FlutterJNI();
|
||||
attach(jni, platformViewsController);
|
||||
|
||||
// Simulate create call from the framework.
|
||||
createPlatformView(
|
||||
jni, platformViewsController, platformViewId, "testType", /* hybrid=*/ false);
|
||||
|
||||
assertFalse(platformViewsController.contextToPlatformView.isEmpty());
|
||||
platformViewsController.onDetachedFromJNI();
|
||||
assertTrue(platformViewsController.contextToPlatformView.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class})
|
||||
public void onPreEngineRestart_clearsPlatformViewContext() {
|
||||
PlatformViewsController platformViewsController = new PlatformViewsController();
|
||||
|
||||
int platformViewId = 0;
|
||||
assertNull(platformViewsController.getPlatformViewById(platformViewId));
|
||||
|
||||
PlatformViewFactory viewFactory = mock(PlatformViewFactory.class);
|
||||
PlatformView platformView = mock(PlatformView.class);
|
||||
|
||||
View pv = mock(View.class);
|
||||
when(pv.getLayoutParams()).thenReturn(new LayoutParams(1, 1));
|
||||
|
||||
when(platformView.getView()).thenReturn(pv);
|
||||
when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView);
|
||||
platformViewsController.getRegistry().registerViewFactory("testType", viewFactory);
|
||||
|
||||
FlutterJNI jni = new FlutterJNI();
|
||||
attach(jni, platformViewsController);
|
||||
|
||||
// Simulate create call from the framework.
|
||||
createPlatformView(
|
||||
jni, platformViewsController, platformViewId, "testType", /* hybrid=*/ false);
|
||||
|
||||
assertFalse(platformViewsController.contextToPlatformView.isEmpty());
|
||||
platformViewsController.onDetachedFromJNI();
|
||||
assertTrue(platformViewsController.contextToPlatformView.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class})
|
||||
public void createPlatformViewMessage__throwsIfViewHasParent() {
|
||||
@@ -772,13 +593,6 @@ public class PlatformViewsControllerTest {
|
||||
verify(flutterView, never()).removeView(overlayImageView);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkInputConnectionProxy__falseIfViewIsNull() {
|
||||
final PlatformViewsController platformViewsController = new PlatformViewsController();
|
||||
boolean shouldProxying = platformViewsController.checkInputConnectionProxy(null);
|
||||
assertFalse(shouldProxying);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class})
|
||||
public void convertPlatformViewRenderSurfaceAsDefault() {
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
package io.flutter.plugin.platform;
|
||||
|
||||
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
|
||||
import static android.os.Build.VERSION_CODES.P;
|
||||
import static android.os.Build.VERSION_CODES.R;
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.view.Display;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
@Config(manifest = Config.NONE)
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@TargetApi(P)
|
||||
public class SingleViewPresentationTest {
|
||||
@Test
|
||||
@Config(minSdk = JELLY_BEAN_MR1, maxSdk = R)
|
||||
public void returnsOuterContextInputMethodManager() {
|
||||
// There's a bug in Android Q caused by the IMM being instanced per display.
|
||||
// https://github.com/flutter/flutter/issues/38375. We need the context returned by
|
||||
// SingleViewPresentation to be consistent from its instantiation instead of defaulting to
|
||||
// what the system would have returned at call time.
|
||||
|
||||
// It's not possible to set up the exact same conditions as the unit test in the bug here,
|
||||
// but we can make sure that we're wrapping the Context passed in at instantiation time and
|
||||
// returning the same InputMethodManager from it. This test passes in a Spy context instance
|
||||
// that initially returns a mock. Without the bugfix this test falls back to Robolectric's
|
||||
// system service instead of the spy's and fails.
|
||||
|
||||
// Create an SVP under test with a Context that returns a local IMM mock.
|
||||
Context context = spy(RuntimeEnvironment.application);
|
||||
InputMethodManager expected = mock(InputMethodManager.class);
|
||||
when(context.getSystemService(Context.INPUT_METHOD_SERVICE)).thenReturn(expected);
|
||||
DisplayManager dm = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
|
||||
SingleViewPresentation svp =
|
||||
new SingleViewPresentation(context, dm.getDisplay(0), null, null, null, false);
|
||||
|
||||
// Get the IMM from the SVP's context.
|
||||
InputMethodManager actual =
|
||||
(InputMethodManager) svp.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
|
||||
// This should be the mocked instance from construction, not the IMM from the greater
|
||||
// Android OS (or Robolectric's shadow, in this case).
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(minSdk = JELLY_BEAN_MR1, maxSdk = R)
|
||||
public void returnsOuterContextInputMethodManager_createDisplayContext() {
|
||||
// The IMM should also persist across display contexts created from the base context.
|
||||
|
||||
// Create an SVP under test with a Context that returns a local IMM mock.
|
||||
Context context = spy(RuntimeEnvironment.application);
|
||||
InputMethodManager expected = mock(InputMethodManager.class);
|
||||
when(context.getSystemService(Context.INPUT_METHOD_SERVICE)).thenReturn(expected);
|
||||
Display display =
|
||||
((DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE)).getDisplay(0);
|
||||
SingleViewPresentation svp =
|
||||
new SingleViewPresentation(context, display, null, null, null, false);
|
||||
|
||||
// Get the IMM from the SVP's context.
|
||||
InputMethodManager actual =
|
||||
(InputMethodManager)
|
||||
svp.getContext()
|
||||
.createDisplayContext(display)
|
||||
.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
|
||||
// This should be the mocked instance from construction, not the IMM from the greater
|
||||
// Android OS (or Robolectric's shadow, in this case).
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,16 @@
|
||||
package io.flutter.util;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -30,4 +35,47 @@ public class ViewUtilsTest {
|
||||
ContextWrapper wrapper = new ContextWrapper(new ContextWrapper(activity));
|
||||
assertEquals(activity, ViewUtils.getActivity(wrapper));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void childHasFocus_rootHasFocus() {
|
||||
final View rootView = mock(View.class);
|
||||
when(rootView.hasFocus()).thenReturn(true);
|
||||
assertTrue(ViewUtils.childHasFocus(rootView));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void childHasFocus_rootDoesNotHaveFocus() {
|
||||
final View rootView = mock(View.class);
|
||||
when(rootView.hasFocus()).thenReturn(false);
|
||||
assertFalse(ViewUtils.childHasFocus(rootView));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void childHasFocus_rootIsNull() {
|
||||
assertFalse(ViewUtils.childHasFocus(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void childHasFocus_childHasFocus() {
|
||||
final View childView = mock(View.class);
|
||||
when(childView.hasFocus()).thenReturn(true);
|
||||
|
||||
final ViewGroup rootView = mock(ViewGroup.class);
|
||||
when(rootView.getChildCount()).thenReturn(1);
|
||||
when(rootView.getChildAt(0)).thenReturn(childView);
|
||||
|
||||
assertTrue(ViewUtils.childHasFocus(rootView));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void childHasFocus_childDoesNotHaveFocus() {
|
||||
final View childView = mock(View.class);
|
||||
when(childView.hasFocus()).thenReturn(false);
|
||||
|
||||
final ViewGroup rootView = mock(ViewGroup.class);
|
||||
when(rootView.getChildCount()).thenReturn(1);
|
||||
when(rootView.getChildAt(0)).thenReturn(childView);
|
||||
|
||||
assertFalse(ViewUtils.childHasFocus(rootView));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1467,7 +1467,6 @@ public class AccessibilityBridgeTest {
|
||||
|
||||
View embeddedView = mock(View.class);
|
||||
when(accessibilityDelegate.getPlatformViewById(1)).thenReturn(embeddedView);
|
||||
when(accessibilityDelegate.usesVirtualDisplay(1)).thenReturn(false);
|
||||
|
||||
AccessibilityNodeInfo nodeInfo = mock(AccessibilityNodeInfo.class);
|
||||
when(embeddedView.createAccessibilityNodeInfo()).thenReturn(nodeInfo);
|
||||
@@ -1505,7 +1504,6 @@ public class AccessibilityBridgeTest {
|
||||
|
||||
View embeddedView = mock(View.class);
|
||||
when(accessibilityDelegate.getPlatformViewById(1)).thenReturn(embeddedView);
|
||||
when(accessibilityDelegate.usesVirtualDisplay(1)).thenReturn(false);
|
||||
|
||||
TestSemanticsUpdate testSemanticsRootUpdate = root.toUpdate();
|
||||
testSemanticsRootUpdate.sendUpdateToBridge(accessibilityBridge);
|
||||
@@ -1540,7 +1538,6 @@ public class AccessibilityBridgeTest {
|
||||
|
||||
View embeddedView = mock(View.class);
|
||||
when(accessibilityDelegate.getPlatformViewById(1)).thenReturn(embeddedView);
|
||||
when(accessibilityDelegate.usesVirtualDisplay(1)).thenReturn(false);
|
||||
|
||||
TestSemanticsUpdate testSemanticsRootWithPlatformViewUpdate = rootWithPlatformView.toUpdate();
|
||||
testSemanticsRootWithPlatformViewUpdate.sendUpdateToBridge(accessibilityBridge);
|
||||
@@ -1555,34 +1552,6 @@ public class AccessibilityBridgeTest {
|
||||
.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itProducesPlatformViewNodeForVirtualDisplay() {
|
||||
PlatformViewsAccessibilityDelegate accessibilityDelegate =
|
||||
mock(PlatformViewsAccessibilityDelegate.class);
|
||||
AccessibilityViewEmbedder accessibilityViewEmbedder = mock(AccessibilityViewEmbedder.class);
|
||||
AccessibilityBridge accessibilityBridge =
|
||||
setUpBridge(
|
||||
/*rootAccessibilityView=*/ null,
|
||||
/*accessibilityChannel=*/ null,
|
||||
/*accessibilityManager=*/ null,
|
||||
/*contentResolver=*/ null,
|
||||
accessibilityViewEmbedder,
|
||||
accessibilityDelegate);
|
||||
|
||||
TestSemanticsNode platformView = new TestSemanticsNode();
|
||||
platformView.platformViewId = 1;
|
||||
|
||||
TestSemanticsUpdate testSemanticsUpdate = platformView.toUpdate();
|
||||
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);
|
||||
|
||||
View embeddedView = mock(View.class);
|
||||
when(accessibilityDelegate.getPlatformViewById(1)).thenReturn(embeddedView);
|
||||
when(accessibilityDelegate.usesVirtualDisplay(1)).thenReturn(true);
|
||||
|
||||
accessibilityBridge.createAccessibilityNodeInfo(0);
|
||||
verify(accessibilityViewEmbedder).getRootNode(eq(embeddedView), eq(0), any(Rect.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void releaseDropsChannelMessageHandler() {
|
||||
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
|
||||
|
||||
@@ -133,17 +133,6 @@
|
||||
column="27"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UseSparseArrays"
|
||||
message="Use `new SparseArray<VirtualDisplayController>(...)` instead for better performance"
|
||||
errorLine1=" vdControllers = new HashMap<>();"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java"
|
||||
line="61"
|
||||
column="25"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="ClickableViewAccessibility"
|
||||
message="Custom view `FlutterView` overrides `onTouchEvent` but not `performClick`"
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/util/PreconditionsTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugins/GeneratedPluginRegistrant.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/FlutterTestSuite.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/android/KeyChannelResponderTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterTextureViewTest.java" />
|
||||
@@ -40,20 +39,21 @@
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/TextInputChannelTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/DeferredComponentChannelTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/AccessibilityChannelTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/KeyEventChannelTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/RestorationChannelTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterShellArgsTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManagerTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewWrapperTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/mouse/MouseCursorPluginTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/common/StandardMessageCodecTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/common/BinaryCodecTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/common/StandardMethodCodecTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/editing/ListenableEditingStateTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/editing/TextEditingDeltaTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/localization/LocalizationPluginTest.java" />
|
||||
<src file="../../../flutter/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java" />
|
||||
@@ -151,13 +151,12 @@
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroup.java" />
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java" />
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRegistry.java" />
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java" />
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRegistryImpl.java" />
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java" />
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewFactory.java" />
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/AccessibilityEventsDelegate.java" />
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformView.java" />
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java" />
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java" />
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java" />
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/mouse/MouseCursorPlugin.java" />
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java" />
|
||||
@@ -178,8 +177,8 @@
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/MethodCall.java" />
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/EventChannel.java" />
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/MethodChannel.java" />
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextEditingDelta.java" />
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java" />
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextEditingDelta.java" />
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/ListenableEditingState.java" />
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java" />
|
||||
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java" />
|
||||
|
||||
Reference in New Issue
Block a user