[Android] add HC++ platform view class. (#161829)

Part of the HC++ project for Android. This adds a current unused and
unusable separate manager class for the new platform view strategy.
Subsequent PRs will fill in the engine implementation as well as adding
a mechanism to detect if support is available and provide a framework
opt in.
This commit is contained in:
Jonah Williams 2025-01-30 12:02:15 -08:00 committed by GitHub
parent 5d705328dc
commit a4927668cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 2047 additions and 50 deletions

View File

@ -41699,6 +41699,7 @@ ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/syst
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/NavigationChannel.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel2.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java + ../../../flutter/LICENSE
@ -41742,6 +41743,7 @@ ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/Platf
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController2.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewFakeWindowViewGroup.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewWindowManager.java + ../../../flutter/LICENSE
@ -44653,6 +44655,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/system
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/NavigationChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel2.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java
@ -44700,6 +44703,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/Platfor
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController2.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewFakeWindowViewGroup.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewWindowManager.java

View File

@ -283,6 +283,7 @@ android_java_sources = [
"io/flutter/embedding/engine/systemchannels/NavigationChannel.java",
"io/flutter/embedding/engine/systemchannels/PlatformChannel.java",
"io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java",
"io/flutter/embedding/engine/systemchannels/PlatformViewsChannel2.java",
"io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java",
"io/flutter/embedding/engine/systemchannels/RestorationChannel.java",
"io/flutter/embedding/engine/systemchannels/ScribeChannel.java",
@ -330,6 +331,7 @@ android_java_sources = [
"io/flutter/plugin/platform/PlatformViewWrapper.java",
"io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java",
"io/flutter/plugin/platform/PlatformViewsController.java",
"io/flutter/plugin/platform/PlatformViewsController2.java",
"io/flutter/plugin/platform/SingleViewFakeWindowViewGroup.java",
"io/flutter/plugin/platform/SingleViewPresentation.java",
"io/flutter/plugin/platform/SingleViewWindowManager.java",

View File

@ -1122,7 +1122,8 @@ public class FlutterView extends FrameLayout
this,
this.flutterEngine.getTextInputChannel(),
this.flutterEngine.getScribeChannel(),
this.flutterEngine.getPlatformViewsController());
this.flutterEngine.getPlatformViewsController(),
this.flutterEngine.getPlatformViewsController2());
try {
textServicesManager =
@ -1162,6 +1163,11 @@ public class FlutterView extends FrameLayout
.getPlatformViewsController()
.attachToFlutterRenderer(this.flutterEngine.getRenderer());
this.flutterEngine.getPlatformViewsController2().attachAccessibilityBridge(accessibilityBridge);
this.flutterEngine
.getPlatformViewsController2()
.attachToFlutterRenderer(this.flutterEngine.getRenderer());
// Inform the Android framework that it should retrieve a new InputConnection
// now that an engine is attached.
// TODO(mattcarroll): once this is proven to work, move this line ot TextInputPlugin
@ -1179,6 +1185,7 @@ public class FlutterView extends FrameLayout
sendViewportMetricsToFlutter();
flutterEngine.getPlatformViewsController().attachToView(this);
flutterEngine.getPlatformViewsController2().attachToView(this);
// Notify engine attachment listeners of the attachment.
for (FlutterEngineAttachmentListener listener : flutterEngineAttachmentListeners) {
@ -1219,9 +1226,11 @@ public class FlutterView extends FrameLayout
getContext().getContentResolver().unregisterContentObserver(systemSettingsObserver);
flutterEngine.getPlatformViewsController().detachFromView();
flutterEngine.getPlatformViewsController2().detachFromView();
// Disconnect the FlutterEngine's PlatformViewsController from the AccessibilityBridge.
flutterEngine.getPlatformViewsController().detachAccessibilityBridge();
flutterEngine.getPlatformViewsController2().detachAccessibilityBridge();
// Disconnect and clean up the AccessibilityBridge.
accessibilityBridge.release();

View File

@ -41,6 +41,7 @@ import io.flutter.embedding.engine.systemchannels.SystemChannel;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.plugin.localization.LocalizationPlugin;
import io.flutter.plugin.platform.PlatformViewsController;
import io.flutter.plugin.platform.PlatformViewsController2;
import io.flutter.plugin.text.ProcessTextPlugin;
import io.flutter.util.ViewUtils;
import java.util.HashSet;
@ -109,6 +110,7 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
// Platform Views.
@NonNull private final PlatformViewsController platformViewsController;
@NonNull private final PlatformViewsController2 platformViewsController2;
// Engine Lifecycle.
@NonNull private final Set<EngineLifecycleListener> engineLifecycleListeners = new HashSet<>();
@ -124,6 +126,7 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
}
platformViewsController.onPreEngineRestart();
platformViewsController2.onPreEngineRestart();
restorationChannel.clearData();
}
@ -360,8 +363,11 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
flutterLoader.ensureInitializationComplete(context, dartVmArgs);
}
PlatformViewsController2 platformViewsController2 = new PlatformViewsController2();
flutterJNI.addEngineLifecycleListener(engineLifecycleListener);
flutterJNI.setPlatformViewsController(platformViewsController);
flutterJNI.setPlatformViewsController2(platformViewsController2);
flutterJNI.setLocalizationPlugin(localizationPlugin);
flutterJNI.setDeferredComponentManager(injector.deferredComponentManager());
@ -377,7 +383,7 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
this.renderer = new FlutterRenderer(flutterJNI);
this.platformViewsController = platformViewsController;
this.platformViewsController.onAttachedToJNI();
this.platformViewsController2 = platformViewsController2;
this.pluginRegistry =
new FlutterEngineConnectionRegistry(
@ -472,6 +478,7 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
// The order that these things are destroyed is important.
pluginRegistry.destroy();
platformViewsController.onDetachedFromJNI();
platformViewsController2.onDetachedFromJNI();
dartExecutor.onDetachedFromJNI();
flutterJNI.removeEngineLifecycleListener(engineLifecycleListener);
flutterJNI.setDeferredComponentManager(null);
@ -648,6 +655,11 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
return platformViewsController;
}
@NonNull
public PlatformViewsController2 getPlatformViewsController2() {
return platformViewsController2;
}
@NonNull
public ActivityControlSurface getActivityControlSurface() {
return pluginRegistry;

View File

@ -18,6 +18,7 @@ import android.util.DisplayMetrics;
import android.util.Size;
import android.util.TypedValue;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.SurfaceHolder;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
@ -36,6 +37,7 @@ import io.flutter.embedding.engine.systemchannels.SettingsChannel;
import io.flutter.plugin.common.StandardMessageCodec;
import io.flutter.plugin.localization.LocalizationPlugin;
import io.flutter.plugin.platform.PlatformViewsController;
import io.flutter.plugin.platform.PlatformViewsController2;
import io.flutter.util.Preconditions;
import io.flutter.view.AccessibilityBridge;
import io.flutter.view.FlutterCallbackInformation;
@ -388,6 +390,7 @@ public class FlutterJNI {
@Nullable private PlatformMessageHandler platformMessageHandler;
@Nullable private LocalizationPlugin localizationPlugin;
@Nullable private PlatformViewsController platformViewsController;
@Nullable private PlatformViewsController2 platformViewsController2;
@Nullable private DeferredComponentManager deferredComponentManager;
@ -765,6 +768,13 @@ public class FlutterJNI {
this.platformViewsController = platformViewsController;
}
@UiThread
public void setPlatformViewsController2(
@NonNull PlatformViewsController2 platformViewsController2) {
ensureRunningOnMainThread();
this.platformViewsController2 = platformViewsController2;
}
// ------ Start Accessibility Support -----
/**
* Sets the {@link AccessibilityDelegate} for the attached Flutter context.
@ -1274,6 +1284,75 @@ public class FlutterJNI {
}
// ----- End Engine Lifecycle Support ----
// ----- New Platform Views ----------
@SuppressWarnings("unused")
@UiThread
public SurfaceControl.Transaction createTransaction() {
if (platformViewsController2 == null) {
throw new RuntimeException("");
}
return platformViewsController2.createTransaction();
}
@SuppressWarnings("unused")
@UiThread
public void swapTransactions() {
if (platformViewsController2 == null) {
throw new RuntimeException("");
}
platformViewsController2.swapTransactions();
}
@SuppressWarnings("unused")
@UiThread
public void applyTransactions() {
if (platformViewsController2 == null) {
throw new RuntimeException("");
}
platformViewsController2.applyTransactions();
}
@SuppressWarnings("unused")
@UiThread
public FlutterOverlaySurface createOverlaySurface2() {
if (platformViewsController2 == null) {
throw new RuntimeException(
"platformViewsController must be set before attempting to position an overlay surface");
}
return platformViewsController2.createOverlaySurface();
}
@SuppressWarnings("unused")
@UiThread
public void destroyOverlaySurface2() {
ensureRunningOnMainThread();
if (platformViewsController2 == null) {
throw new RuntimeException(
"platformViewsController must be set before attempting to destroy an overlay surface");
}
platformViewsController2.destroyOverlaySurface();
}
@UiThread
public void onDisplayPlatformView2(
int viewId,
int x,
int y,
int width,
int height,
int viewWidth,
int viewHeight,
FlutterMutatorsStack mutatorsStack) {
ensureRunningOnMainThread();
if (platformViewsController2 == null) {
throw new RuntimeException(
"platformViewsController must be set before attempting to position a platform view");
}
platformViewsController2.onDisplayPlatformView(
viewId, x, y, width, height, viewWidth, viewHeight, mutatorsStack);
}
// ----- Start Localization Support ----
/** Sets the localization plugin that is used in various localization methods. */

View File

@ -0,0 +1,325 @@
// 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.embedding.engine.systemchannels;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.Log;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.StandardMethodCodec;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Map;
/**
* System channel that sends 2-way communication between Flutter and Android to facilitate embedding
* of Android Views within a Flutter application.
*
* <p>Register a {@link PlatformViewsHandler} to implement the Android side of this channel.
*/
public class PlatformViewsChannel2 {
private static final String TAG = "PlatformViewsChannel2";
private final MethodChannel channel;
private PlatformViewsHandler handler;
/**
* Constructs a {@code PlatformViewsChannel} that connects Android to the Dart code running in
* {@code dartExecutor}.
*
* <p>The given {@code dartExecutor} is permitted to be idle or executing code.
*
* <p>See {@link DartExecutor}.
*/
public PlatformViewsChannel2(@NonNull DartExecutor dartExecutor) {
channel =
new MethodChannel(dartExecutor, "flutter/platform_views_2", StandardMethodCodec.INSTANCE);
channel.setMethodCallHandler(parsingHandler);
}
public void invokeViewFocused(int viewId) {
if (channel == null) {
return;
}
channel.invokeMethod("viewFocused", viewId);
}
private static String detailedExceptionString(Exception exception) {
return Log.getStackTraceString(exception);
}
private final MethodChannel.MethodCallHandler parsingHandler =
new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
// If there is no handler to respond to this message then we don't need to
// parse it. Return.
if (handler == null) {
return;
}
Log.v(TAG, "Received '" + call.method + "' message.");
switch (call.method) {
case "create":
create(call, result);
break;
case "dispose":
dispose(call, result);
break;
case "touch":
touch(call, result);
break;
case "setDirection":
setDirection(call, result);
break;
case "clearFocus":
clearFocus(call, result);
break;
default:
result.notImplemented();
}
}
private void create(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
final Map<String, Object> createArgs = call.arguments();
final ByteBuffer additionalParams =
createArgs.containsKey("params")
? ByteBuffer.wrap((byte[]) createArgs.get("params"))
: null;
try {
final PlatformViewCreationRequest request =
new PlatformViewCreationRequest(
(int) createArgs.get("id"),
(String) createArgs.get("viewType"),
0,
0,
(int) createArgs.get("direction"),
additionalParams);
handler.createPlatformView(request);
result.success(null);
} catch (IllegalStateException exception) {
result.error("error", detailedExceptionString(exception), null);
}
}
private void dispose(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
Map<String, Object> disposeArgs = call.arguments();
int viewId = (int) disposeArgs.get("id");
try {
handler.dispose(viewId);
result.success(null);
} catch (IllegalStateException exception) {
result.error("error", detailedExceptionString(exception), null);
}
}
private void touch(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
List<Object> args = call.arguments();
PlatformViewTouch touch =
new PlatformViewTouch(
(int) args.get(0),
(Number) args.get(1),
(Number) args.get(2),
(int) args.get(3),
(int) args.get(4),
args.get(5),
args.get(6),
(int) args.get(7),
(int) args.get(8),
(float) (double) args.get(9),
(float) (double) args.get(10),
(int) args.get(11),
(int) args.get(12),
(int) args.get(13),
(int) args.get(14),
((Number) args.get(15)).longValue());
try {
handler.onTouch(touch);
result.success(null);
} catch (IllegalStateException exception) {
result.error("error", detailedExceptionString(exception), null);
}
}
private void setDirection(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
Map<String, Object> setDirectionArgs = call.arguments();
int newDirectionViewId = (int) setDirectionArgs.get("id");
int direction = (int) setDirectionArgs.get("direction");
try {
handler.setDirection(newDirectionViewId, direction);
result.success(null);
} catch (IllegalStateException exception) {
result.error("error", detailedExceptionString(exception), null);
}
}
private void clearFocus(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
int viewId = call.arguments();
try {
handler.clearFocus(viewId);
result.success(null);
} catch (IllegalStateException exception) {
result.error("error", detailedExceptionString(exception), null);
}
}
};
/**
* Sets the {@link PlatformViewsHandler} which receives all events and requests that are parsed
* from the underlying platform views channel.
*/
public void setPlatformViewsHandler(@Nullable PlatformViewsHandler handler) {
this.handler = handler;
}
/**
* Handler that receives platform view messages sent from Flutter to Android through a given
* {@link PlatformViewsChannel}.
*
* <p>To register a {@code PlatformViewsHandler} with a {@link PlatformViewsChannel2}, see {@link
* PlatformViewsChannel2#setPlatformViewsHandler(PlatformViewsHandler)}.
*/
public interface PlatformViewsHandler {
/**
* The Flutter application would like to display a new Android {@code View}, i.e., platform
* view.
*/
void createPlatformView(@NonNull PlatformViewCreationRequest request);
/** The Flutter application would like to dispose of an existing Android {@code View}. */
void dispose(int viewId);
/**
* The user touched a platform view within Flutter.
*
* <p>Touch data is reported in {@code touch}.
*/
void onTouch(@NonNull PlatformViewTouch touch);
/**
* The Flutter application would like to change the layout direction of an existing Android
* {@code View}, i.e., platform view.
*/
void setDirection(int viewId, int direction);
/** Clears the focus from the platform view with a give id if it is currently focused. */
void clearFocus(int viewId);
}
/** Request sent from Flutter to create a new platform view. */
public static class PlatformViewCreationRequest {
/** The ID of the platform view as seen by the Flutter side. */
public final int viewId;
@NonNull public final String viewType;
public final double logicalWidth;
public final double logicalHeight;
/**
* The layout direction of the new platform view.
*
* <p>See {@link android.view.View#LAYOUT_DIRECTION_LTR} and {@link
* android.view.View#LAYOUT_DIRECTION_RTL}
*/
public final int direction;
/** Custom parameters that are unique to the desired platform view. */
@Nullable public final ByteBuffer params;
public PlatformViewCreationRequest(
int viewId,
@NonNull String viewType,
double logicalWidth,
double logicalHeight,
int direction,
@Nullable ByteBuffer params) {
this.viewId = viewId;
this.viewType = viewType;
this.logicalWidth = logicalWidth;
this.logicalHeight = logicalHeight;
this.direction = direction;
this.params = params;
}
}
/** 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. */
public final int viewId;
/** The amount of time that the touch has been pressed. */
@NonNull public final Number downTime;
@NonNull public final Number eventTime;
public final int action;
/** The number of pointers (e.g, fingers) involved in the touch event. */
public final int pointerCount;
/**
* Properties for each pointer, encoded in a raw format. Expected to be formatted as a
* List[List[Integer]], where each inner list has two items: - An id, at index 0, corresponding
* to {@link android.view.MotionEvent.PointerProperties#id} - A tool type, at index 1,
* corresponding to {@link android.view.MotionEvent.PointerProperties#toolType}.
*/
@NonNull public final Object rawPointerPropertiesList;
/** Coordinates for each pointer, encoded in a raw format. */
@NonNull public final Object rawPointerCoords;
public final int metaState;
public final int buttonState;
/** Coordinate precision along the x-axis. */
public final float xPrecision;
/** Coordinate precision along the y-axis. */
public final float yPrecision;
public final int deviceId;
public final int edgeFlags;
public final int source;
public final int flags;
public final long motionEventId;
public PlatformViewTouch(
int viewId,
@NonNull Number downTime,
@NonNull Number eventTime,
int action,
int pointerCount,
@NonNull Object rawPointerPropertiesList,
@NonNull Object rawPointerCoords,
int metaState,
int buttonState,
float xPrecision,
float yPrecision,
int deviceId,
int edgeFlags,
int source,
int flags,
long motionEventId) {
this.viewId = viewId;
this.downTime = downTime;
this.eventTime = eventTime;
this.action = action;
this.pointerCount = pointerCount;
this.rawPointerPropertiesList = rawPointerPropertiesList;
this.rawPointerCoords = rawPointerCoords;
this.metaState = metaState;
this.buttonState = buttonState;
this.xPrecision = xPrecision;
this.yPrecision = yPrecision;
this.deviceId = deviceId;
this.edgeFlags = edgeFlags;
this.source = source;
this.flags = flags;
this.motionEventId = motionEventId;
}
}
}

View File

@ -33,6 +33,7 @@ import io.flutter.embedding.engine.systemchannels.ScribeChannel;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.embedding.engine.systemchannels.TextInputChannel.TextEditState;
import io.flutter.plugin.platform.PlatformViewsController;
import io.flutter.plugin.platform.PlatformViewsController2;
import java.util.ArrayList;
import java.util.HashMap;
@ -52,6 +53,7 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
private boolean mRestartInputPending;
@Nullable private InputConnection lastInputConnection;
@NonNull private PlatformViewsController platformViewsController;
@NonNull private PlatformViewsController2 platformViewsController2;
@Nullable private Rect lastClientRect;
private ImeSyncDeferringInsetsCallback imeSyncCallback;
@ -69,7 +71,8 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
@NonNull View view,
@NonNull TextInputChannel textInputChannel,
@NonNull ScribeChannel scribeChannel,
@NonNull PlatformViewsController platformViewsController) {
@NonNull PlatformViewsController platformViewsController,
@NonNull PlatformViewsController2 platformViewsController2) {
mView = view;
// Create a default object.
mEditable = new ListenableEditingState(null, mView);
@ -160,6 +163,8 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
this.platformViewsController = platformViewsController;
this.platformViewsController.attachTextInputPlugin(this);
this.platformViewsController2 = platformViewsController2;
this.platformViewsController2.attachTextInputPlugin(this);
}
@NonNull
@ -215,6 +220,7 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
@SuppressLint("NewApi")
public void destroy() {
platformViewsController.detachTextInputPlugin();
platformViewsController2.detachTextInputPlugin();
textInputChannel.setTextInputMethodHandler(null);
notifyViewExited();
mEditable.removeEditingStateListener(this);

View File

@ -926,14 +926,6 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega
return registry;
}
/**
* Invoked when the {@link io.flutter.embedding.engine.FlutterEngine} that owns this {@link
* PlatformViewsController} attaches to JNI.
*/
public void onAttachedToJNI() {
// Currently no action needs to be taken after JNI attachment.
}
/**
* Invoked when the {@link io.flutter.embedding.engine.FlutterEngine} that owns this {@link
* PlatformViewsController} detaches from JNI.

View File

@ -0,0 +1,679 @@
// 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 io.flutter.Build.API_LEVELS;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.PixelFormat;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.MotionEvent.PointerCoords;
import android.view.MotionEvent.PointerProperties;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import io.flutter.Log;
import io.flutter.embedding.android.AndroidTouchProcessor;
import io.flutter.embedding.android.FlutterView;
import io.flutter.embedding.android.MotionEventTracker;
import io.flutter.embedding.engine.FlutterOverlaySurface;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.mutatorsstack.*;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.systemchannels.PlatformViewsChannel2;
import io.flutter.plugin.editing.TextInputPlugin;
import io.flutter.view.AccessibilityBridge;
import java.util.ArrayList;
import java.util.List;
/**
* Manages platform views.
*
* <p>Each {@link io.flutter.embedding.engine.FlutterEngine} has a single platform views controller.
* A platform views controller can be attached to at most one Flutter view.
*/
public class PlatformViewsController2 implements PlatformViewsAccessibilityDelegate {
private static final String TAG = "PlatformViewsController2";
private final PlatformViewRegistryImpl registry;
private AndroidTouchProcessor androidTouchProcessor;
private Context context;
private FlutterView flutterView;
@Nullable private TextInputPlugin textInputPlugin;
private PlatformViewsChannel2 platformViewsChannel;
private final AccessibilityEventsDelegate accessibilityEventsDelegate;
private final SparseArray<PlatformView> platformViews;
private final SparseArray<FlutterMutatorView> platformViewParent;
private final MotionEventTracker motionEventTracker;
private final ArrayList<SurfaceControl.Transaction> pendingTransactions;
private final ArrayList<SurfaceControl.Transaction> activeTransactions;
private Surface overlayerSurface = null;
public PlatformViewsController2() {
registry = new PlatformViewRegistryImpl();
accessibilityEventsDelegate = new AccessibilityEventsDelegate();
platformViews = new SparseArray<>();
platformViewParent = new SparseArray<>();
pendingTransactions = new ArrayList<>();
activeTransactions = new ArrayList<>();
motionEventTracker = MotionEventTracker.getInstance();
}
@Override
public boolean usesVirtualDisplay(int id) {
return false;
}
public PlatformView createFlutterPlatformView(
@NonNull PlatformViewsChannel2.PlatformViewCreationRequest request) {
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);
}
final PlatformView platformView = viewFactory.create(context, request.viewId, createParams);
// Configure the view to match the requested layout direction.
final View embeddedView = platformView.getView();
if (embeddedView == null) {
throw new IllegalStateException(
"PlatformView#getView() returned null, but an Android view reference was expected.");
}
embeddedView.setLayoutDirection(request.direction);
platformViews.put(request.viewId, platformView);
maybeInvokeOnFlutterViewAttached(platformView);
return platformView;
}
/**
* Translates an original touch event to have the same locations as the ones that Flutter
* calculates (because original + flutter's - original = flutter's).
*
* @param originalEvent The saved original input event.
* @param pointerCoords The coordinates that Flutter thinks the touch is happening at.
*/
private static void translateMotionEvent(
MotionEvent originalEvent, PointerCoords[] pointerCoords) {
if (pointerCoords.length < 1) {
return;
}
float xOffset = pointerCoords[0].x - originalEvent.getX();
float yOffset = pointerCoords[0].y - originalEvent.getY();
originalEvent.offsetLocation(xOffset, yOffset);
}
@VisibleForTesting
public MotionEvent toMotionEvent(float density, PlatformViewsChannel2.PlatformViewTouch touch) {
MotionEventTracker.MotionEventId motionEventId =
MotionEventTracker.MotionEventId.from(touch.motionEventId);
MotionEvent trackedEvent = motionEventTracker.pop(motionEventId);
// Pointer coordinates in the tracked events are global to FlutterView
// The framework converts them to be local to a widget, given that
// motion events operate on local coords, we need to replace these in the tracked
// event with their local counterparts.
// Compute this early so it can be used as input to translateNonVirtualDisplayMotionEvent.
PointerCoords[] pointerCoords =
parsePointerCoordsList(touch.rawPointerCoords, density)
.toArray(new PointerCoords[touch.pointerCount]);
if (trackedEvent != null) {
// We have the original event, deliver it after offsetting as it will pass the verifiable
// input check.
translateMotionEvent(trackedEvent, pointerCoords);
return trackedEvent;
}
// We don't have a reference to the original MotionEvent.
// In this case we manually recreate a MotionEvent to be delivered. This MotionEvent
// will fail the verifiable input check.
PointerProperties[] pointerProperties =
parsePointerPropertiesList(touch.rawPointerPropertiesList)
.toArray(new PointerProperties[touch.pointerCount]);
return MotionEvent.obtain(
touch.downTime.longValue(),
touch.eventTime.longValue(),
touch.action,
touch.pointerCount,
pointerProperties,
pointerCoords,
touch.metaState,
touch.buttonState,
touch.xPrecision,
touch.yPrecision,
touch.deviceId,
touch.edgeFlags,
touch.source,
touch.flags);
}
/**
* Attaches this platform views controller to its input and output channels.
*
* @param context The base context that will be passed to embedded views created by this
* controller. This should be the context of the Activity hosting the Flutter application.
* @param dartExecutor The dart execution context, which is used to set up a system channel.
*/
public void attach(@Nullable Context context, @NonNull DartExecutor dartExecutor) {
if (this.context != null) {
throw new AssertionError(
"A PlatformViewsController can only be attached to a single output target.\n"
+ "attach was called while the PlatformViewsController was already attached.");
}
this.context = context;
platformViewsChannel = new PlatformViewsChannel2(dartExecutor);
platformViewsChannel.setPlatformViewsHandler(channelHandler);
}
/**
* Detaches this platform views controller.
*
* <p>This is typically called when a Flutter applications moves to run in the background, or is
* destroyed. After calling this the platform views controller will no longer listen to it's
* previous messenger, and will not maintain references to the texture registry, context, and
* messenger passed to the previous attach call.
*/
@UiThread
public void detach() {
if (platformViewsChannel != null) {
platformViewsChannel.setPlatformViewsHandler(null);
}
destroyOverlaySurface();
platformViewsChannel = null;
context = null;
}
/**
* Attaches the controller to a {@link FlutterView}.
*
* <p>When {@link io.flutter.embedding.android.FlutterFragment} is used, this method is called
* after the device rotates since the FlutterView is recreated after a rotation.
*/
public void attachToView(@NonNull FlutterView newFlutterView) {
flutterView = newFlutterView;
// Add wrapper for platform views that are composed at the view hierarchy level.
for (int index = 0; index < platformViewParent.size(); index++) {
final FlutterMutatorView view = platformViewParent.valueAt(index);
flutterView.addView(view);
}
// Notify platform views that they are now attached to a FlutterView.
for (int index = 0; index < platformViews.size(); index++) {
final PlatformView view = platformViews.valueAt(index);
view.onFlutterViewAttached(flutterView);
}
}
/**
* Detaches the controller from {@link FlutterView}.
*
* <p>When {@link io.flutter.embedding.android.FlutterFragment} is used, this method is called
* when the device rotates since the FlutterView is detached from the fragment. The next time the
* fragment needs to be displayed, a new Flutter view is created, so attachToView is called again.
*/
public void detachFromView() {
// Remove wrapper for platform views that are composed at the view hierarchy level.
for (int index = 0; index < platformViewParent.size(); index++) {
final FlutterMutatorView view = platformViewParent.valueAt(index);
flutterView.removeView(view);
}
destroyOverlaySurface();
flutterView = null;
// Notify that the platform view have been detached from FlutterView.
for (int index = 0; index < platformViews.size(); index++) {
final PlatformView view = platformViews.valueAt(index);
view.onFlutterViewDetached();
}
}
private void maybeInvokeOnFlutterViewAttached(PlatformView view) {
if (flutterView == null) {
Log.i(TAG, "null flutterView");
// There is currently no FlutterView that we are attached to.
return;
}
view.onFlutterViewAttached(flutterView);
}
@Override
public void attachAccessibilityBridge(@NonNull AccessibilityBridge accessibilityBridge) {
accessibilityEventsDelegate.setAccessibilityBridge(accessibilityBridge);
}
@Override
public void detachAccessibilityBridge() {
accessibilityEventsDelegate.setAccessibilityBridge(null);
}
/**
* Attaches this controller to a text input plugin.
*
* <p>While a text input plugin is available, the platform views controller interacts with it to
* facilitate delegation of text input connections to platform views.
*
* <p>A platform views controller should be attached to a text input plugin whenever it is
* possible for the Flutter framework to receive text input.
*/
public void attachTextInputPlugin(@NonNull TextInputPlugin textInputPlugin) {
this.textInputPlugin = textInputPlugin;
}
/** Detaches this controller from the currently attached text input plugin. */
public void detachTextInputPlugin() {
textInputPlugin = null;
}
public PlatformViewRegistry getRegistry() {
return registry;
}
/**
* Invoked when the {@link io.flutter.embedding.engine.FlutterEngine} that owns this {@link
* PlatformViewsController} detaches from JNI.
*/
public void onDetachedFromJNI() {
diposeAllViews();
}
public void onPreEngineRestart() {
diposeAllViews();
}
@Override
@Nullable
public View getPlatformViewById(int viewId) {
final PlatformView platformView = platformViews.get(viewId);
if (platformView == null) {
return null;
}
return platformView.getView();
}
private void lockInputConnection(@NonNull VirtualDisplayController controller) {
if (textInputPlugin == null) {
return;
}
textInputPlugin.lockPlatformViewInputConnection();
controller.onInputConnectionLocked();
}
private void unlockInputConnection(@NonNull VirtualDisplayController controller) {
if (textInputPlugin == null) {
return;
}
textInputPlugin.unlockPlatformViewInputConnection();
controller.onInputConnectionUnlocked();
}
private static boolean validateDirection(int direction) {
return direction == View.LAYOUT_DIRECTION_LTR || direction == View.LAYOUT_DIRECTION_RTL;
}
@SuppressWarnings("unchecked")
private static List<PointerProperties> parsePointerPropertiesList(Object rawPropertiesList) {
List<Object> rawProperties = (List<Object>) rawPropertiesList;
List<PointerProperties> pointerProperties = new ArrayList<>();
for (Object o : rawProperties) {
pointerProperties.add(parsePointerProperties(o));
}
return pointerProperties;
}
@SuppressWarnings("unchecked")
private static PointerProperties parsePointerProperties(Object rawProperties) {
List<Object> propertiesList = (List<Object>) rawProperties;
PointerProperties properties = new MotionEvent.PointerProperties();
properties.id = (int) propertiesList.get(0);
properties.toolType = (int) propertiesList.get(1);
return properties;
}
@SuppressWarnings("unchecked")
private static List<PointerCoords> parsePointerCoordsList(Object rawCoordsList, float density) {
List<Object> rawCoords = (List<Object>) rawCoordsList;
List<PointerCoords> pointerCoords = new ArrayList<>();
for (Object o : rawCoords) {
pointerCoords.add(parsePointerCoords(o, density));
}
return pointerCoords;
}
@SuppressWarnings("unchecked")
private static PointerCoords parsePointerCoords(Object rawCoords, float density) {
List<Object> coordsList = (List<Object>) rawCoords;
PointerCoords coords = new MotionEvent.PointerCoords();
coords.orientation = (float) (double) coordsList.get(0);
coords.pressure = (float) (double) coordsList.get(1);
coords.size = (float) (double) coordsList.get(2);
coords.toolMajor = (float) ((double) coordsList.get(3) * density);
coords.toolMinor = (float) ((double) coordsList.get(4) * density);
coords.touchMajor = (float) ((double) coordsList.get(5) * density);
coords.touchMinor = (float) ((double) coordsList.get(6) * density);
coords.x = (float) ((double) coordsList.get(7) * density);
coords.y = (float) ((double) coordsList.get(8) * density);
return coords;
}
private float getDisplayDensity() {
return context.getResources().getDisplayMetrics().density;
}
private int toPhysicalPixels(double logicalPixels) {
return (int) Math.round(logicalPixels * getDisplayDensity());
}
private int toLogicalPixels(double physicalPixels, float displayDensity) {
return (int) Math.round(physicalPixels / displayDensity);
}
private int toLogicalPixels(double physicalPixels) {
return toLogicalPixels(physicalPixels, getDisplayDensity());
}
private void diposeAllViews() {
while (platformViews.size() > 0) {
final int viewId = platformViews.keyAt(0);
// Dispose deletes the entry from platformViews and clears associated resources.
channelHandler.dispose(viewId);
}
}
/**
* Disposes a single
*
* @param viewId the PlatformView ID.
*/
@VisibleForTesting
public void disposePlatformView(int viewId) {
channelHandler.dispose(viewId);
}
/**
* Initializes a platform view and adds it to the view hierarchy.
*
* @param viewId The view ID. This member is not intended for public use, and is only visible for
* testing.
*/
@VisibleForTesting
boolean initializePlatformViewIfNeeded(int viewId) {
final PlatformView platformView = platformViews.get(viewId);
if (platformView == null) {
return false;
}
if (platformViewParent.get(viewId) != null) {
return true;
}
final View embeddedView = platformView.getView();
if (embeddedView == null) {
throw new IllegalStateException(
"PlatformView#getView() returned null, but an Android view reference was expected.");
}
if (embeddedView.getParent() != null) {
throw new IllegalStateException(
"The Android view returned from PlatformView#getView() was already added to a parent"
+ " view.");
}
final FlutterMutatorView parentView =
new FlutterMutatorView(
context, context.getResources().getDisplayMetrics().density, androidTouchProcessor);
parentView.setOnDescendantFocusChangeListener(
(view, hasFocus) -> {
if (hasFocus) {
platformViewsChannel.invokeViewFocused(viewId);
} else if (textInputPlugin != null) {
textInputPlugin.clearPlatformViewClient(viewId);
}
});
platformViewParent.put(viewId, parentView);
// Accessibility in the embedded view is initially disabled because if a Flutter app disabled
// accessibility in the first frame, the embedding won't receive an update to disable
// accessibility since the embedding never received an update to enable it.
// The AccessibilityBridge keeps track of the accessibility nodes, and handles the deltas when
// the framework sends a new a11y tree to the embedding.
// To prevent races, the framework populate the SemanticsNode after the platform view has been
// created.
embeddedView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
parentView.addView(embeddedView);
flutterView.addView(parentView);
return true;
}
public void attachToFlutterRenderer(@NonNull FlutterRenderer flutterRenderer) {
androidTouchProcessor = new AndroidTouchProcessor(flutterRenderer, /*trackMotionEvents=*/ true);
}
/**
* Called when a platform view id displayed in the current frame.
*
* @param viewId The ID of the platform view.
* @param x The left position relative to {@code FlutterView}.
* @param y The top position relative to {@code FlutterView}.
* @param width The width of the platform view.
* @param height The height of the platform view.
* @param viewWidth The original width of the platform view before applying the mutator stack.
* @param viewHeight The original height of the platform view before applying the mutator stack.
* @param mutatorsStack The mutator stack. This member is not intended for public use, and is only
* visible for testing.
*/
public void onDisplayPlatformView(
int viewId,
int x,
int y,
int width,
int height,
int viewWidth,
int viewHeight,
@NonNull FlutterMutatorsStack mutatorsStack) {
if (!initializePlatformViewIfNeeded(viewId)) {
return;
}
final FlutterMutatorView parentView = platformViewParent.get(viewId);
parentView.readyToDisplay(mutatorsStack, x, y, width, height);
parentView.setVisibility(View.VISIBLE);
parentView.bringToFront();
final FrameLayout.LayoutParams layoutParams =
new FrameLayout.LayoutParams(viewWidth, viewHeight);
final View view = platformViews.get(viewId).getView();
if (view != null) {
view.setLayoutParams(layoutParams);
view.bringToFront();
}
}
@TargetApi(API_LEVELS.API_34)
@RequiresApi(API_LEVELS.API_34)
public void onEndFrame() {
SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
for (int i = 0; i < activeTransactions.size(); i++) {
tx = tx.merge(activeTransactions.get(i));
}
activeTransactions.clear();
flutterView.invalidate();
flutterView.getRootSurfaceControl().applyTransactionOnDraw(tx);
}
// NOT called from UI thread.
public synchronized void swapTransactions() {
activeTransactions.clear();
for (int i = 0; i < pendingTransactions.size(); i++) {
activeTransactions.add(pendingTransactions.get(i));
}
pendingTransactions.clear();
}
// NOT called from UI thread.
@TargetApi(API_LEVELS.API_34)
@RequiresApi(API_LEVELS.API_34)
public SurfaceControl.Transaction createTransaction() {
SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
pendingTransactions.add(tx);
return tx;
}
// NOT called from UI thread.
@TargetApi(API_LEVELS.API_34)
@RequiresApi(API_LEVELS.API_34)
public void applyTransactions() {
SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
for (int i = 0; i < pendingTransactions.size(); i++) {
tx = tx.merge(pendingTransactions.get(i));
}
tx.apply();
pendingTransactions.clear();
}
@TargetApi(API_LEVELS.API_34)
@RequiresApi(API_LEVELS.API_34)
public FlutterOverlaySurface createOverlaySurface() {
if (overlayerSurface == null) {
final SurfaceControl.Builder surfaceControlBuilder = new SurfaceControl.Builder();
surfaceControlBuilder.setBufferSize(flutterView.getWidth(), flutterView.getHeight());
surfaceControlBuilder.setFormat(PixelFormat.RGBA_8888);
surfaceControlBuilder.setName("Flutter Overlay Surface");
surfaceControlBuilder.setOpaque(false);
final SurfaceControl surfaceControl = surfaceControlBuilder.build();
final SurfaceControl.Transaction tx =
flutterView.getRootSurfaceControl().buildReparentTransaction(surfaceControl);
tx.setLayer(surfaceControl, 1000);
tx.apply();
overlayerSurface = new Surface(surfaceControl);
}
return new FlutterOverlaySurface(0, overlayerSurface);
}
public void destroyOverlaySurface() {
if (overlayerSurface != null) {
overlayerSurface.release();
overlayerSurface = null;
}
}
//// Message Handler ///////
private final PlatformViewsChannel2.PlatformViewsHandler channelHandler =
new PlatformViewsChannel2.PlatformViewsHandler() {
@Override
public void createPlatformView(
@NonNull PlatformViewsChannel2.PlatformViewCreationRequest request) {
createFlutterPlatformView(request);
}
@Override
public void dispose(int viewId) {
final PlatformView platformView = platformViews.get(viewId);
if (platformView == null) {
Log.e(TAG, "Disposing unknown platform view with id: " + viewId);
return;
}
if (platformView.getView() != null) {
final View embeddedView = platformView.getView();
final ViewGroup pvParent = (ViewGroup) embeddedView.getParent();
if (pvParent != null) {
// Eagerly remove the embedded view from the PlatformViewWrapper.
// Without this call, we see some crashes because removing the view
// is used as a signal to stop processing.
pvParent.removeView(embeddedView);
}
}
platformViews.remove(viewId);
try {
platformView.dispose();
} catch (RuntimeException exception) {
Log.e(TAG, "Disposing platform view threw an exception", exception);
}
// The platform view is displayed using a PlatformViewLayer.
final FlutterMutatorView parentView = platformViewParent.get(viewId);
if (parentView != null) {
parentView.removeAllViews();
parentView.unsetOnDescendantFocusChangeListener();
final ViewGroup mutatorViewParent = (ViewGroup) parentView.getParent();
if (mutatorViewParent != null) {
mutatorViewParent.removeView(parentView);
}
platformViewParent.remove(viewId);
}
}
@Override
public void onTouch(@NonNull PlatformViewsChannel2.PlatformViewTouch touch) {
final int viewId = touch.viewId;
final float density = context.getResources().getDisplayMetrics().density;
final PlatformView platformView = platformViews.get(viewId);
if (platformView == null) {
Log.e(TAG, "Sending touch to an unknown view with id: " + viewId);
return;
}
final View view = platformView.getView();
if (view == null) {
Log.e(TAG, "Sending touch to a null view with id: " + viewId);
return;
}
final MotionEvent event = toMotionEvent(density, touch);
view.dispatchTouchEvent(event);
}
@Override
public void setDirection(int viewId, int direction) {
final PlatformView platformView = platformViews.get(viewId);
if (platformView == null) {
Log.e(TAG, "Setting direction to an unknown view with id: " + viewId);
return;
}
View embeddedView = platformView.getView();
if (embeddedView == null) {
Log.e(TAG, "Setting direction to a null view with id: " + viewId);
return;
}
embeddedView.setLayoutDirection(direction);
}
@Override
public void clearFocus(int viewId) {
final PlatformView platformView = platformViews.get(viewId);
if (platformView == null) {
Log.e(TAG, "Clearing focus on an unknown view with id: " + viewId);
return;
}
View embeddedView = platformView.getView();
if (embeddedView == null) {
Log.e(TAG, "Clearing focus on a null view with id: " + viewId);
return;
}
embeddedView.clearFocus();
}
};
}

View File

@ -55,6 +55,7 @@ import io.flutter.embedding.engine.systemchannels.SystemChannel;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.plugin.localization.LocalizationPlugin;
import io.flutter.plugin.platform.PlatformViewsController;
import io.flutter.plugin.platform.PlatformViewsController2;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@ -1475,6 +1476,7 @@ public class FlutterActivityAndFragmentDelegateTest {
when(engine.getNavigationChannel()).thenReturn(mock(NavigationChannel.class));
when(engine.getBackGestureChannel()).thenReturn(mock(BackGestureChannel.class));
when(engine.getPlatformViewsController()).thenReturn(mock(PlatformViewsController.class));
when(engine.getPlatformViewsController2()).thenReturn(mock(PlatformViewsController2.class));
FlutterRenderer renderer = mock(FlutterRenderer.class);
when(engine.getRenderer()).thenReturn(renderer);

View File

@ -53,6 +53,7 @@ import io.flutter.embedding.engine.loader.FlutterLoader;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.systemchannels.SettingsChannel;
import io.flutter.plugin.platform.PlatformViewsController;
import io.flutter.plugin.platform.PlatformViewsController2;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
@ -83,6 +84,7 @@ public class FlutterViewTest {
@Mock FlutterJNI mockFlutterJni;
@Mock FlutterLoader mockFlutterLoader;
@Spy PlatformViewsController platformViewsController;
@Spy PlatformViewsController2 platformViewsController2;
@Before
public void setUp() {
@ -97,10 +99,12 @@ public class FlutterViewTest {
FlutterView flutterView = new FlutterView(ctx);
FlutterEngine flutterEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni));
when(flutterEngine.getPlatformViewsController()).thenReturn(platformViewsController);
when(flutterEngine.getPlatformViewsController2()).thenReturn(platformViewsController2);
flutterView.attachToFlutterEngine(flutterEngine);
verify(platformViewsController, times(1)).attachToView(flutterView);
verify(platformViewsController2, times(1)).attachToView(flutterView);
}
@Test
@ -117,11 +121,13 @@ public class FlutterViewTest {
FlutterView flutterView = new FlutterView(ctx);
FlutterEngine flutterEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni));
when(flutterEngine.getPlatformViewsController()).thenReturn(platformViewsController);
when(flutterEngine.getPlatformViewsController2()).thenReturn(platformViewsController2);
flutterView.attachToFlutterEngine(flutterEngine);
flutterView.detachFromFlutterEngine();
verify(platformViewsController, times(1)).detachFromView();
verify(platformViewsController2, times(1)).detachFromView();
}
@Test

View File

@ -234,7 +234,6 @@ public class FlutterEngineTest {
platformViewsController,
/*dartVmArgs=*/ new String[] {},
/*automaticallyRegisterPlugins=*/ false);
verify(platformViewsController, times(1)).onAttachedToJNI();
engine.destroy();
verify(platformViewsController, times(1)).onDetachedFromJNI();

View File

@ -63,6 +63,7 @@ import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.JSONMethodCodec;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.platform.PlatformViewsController;
import io.flutter.plugin.platform.PlatformViewsController2;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
@ -139,7 +140,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
ArgumentCaptor<String> channelCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<ByteBuffer> bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
@ -160,7 +165,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@ -204,7 +213,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
// Here's no textInputPlugin.setTextInputClient()
textInputPlugin.setTextInputEditingState(
@ -223,7 +236,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@ -275,7 +292,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
CharSequence newText = "I do not fear computers. I fear the lack of them.";
// Change InputTarget to FRAMEWORK_CLIENT.
@ -390,7 +411,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
CharSequence newText = "I do not fear computers. I fear the lack of them.";
final TextEditingDelta expectedDelta =
new TextEditingDelta("", 0, 0, newText, newText.length(), newText.length(), 0, 49);
@ -521,7 +546,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
CharSequence newText = "I do not fear computers. I fear the lack of them.";
final TextEditingDelta expectedDelta =
new TextEditingDelta("", 0, 0, newText, newText.length(), newText.length(), 0, 49);
@ -632,7 +661,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
CharSequence newText = "I do not fear computers. I fear the lack of them.";
final TextEditingDelta expectedDelta =
new TextEditingDelta(
@ -744,7 +777,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
CharSequence newText = "helfo";
final TextEditingDelta expectedDelta = new TextEditingDelta(newText, 0, 5, "hello", 5, 5, 0, 5);
@ -853,7 +890,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
// Change InputTarget to FRAMEWORK_CLIENT.
textInputPlugin.setTextInputClient(
@ -946,7 +987,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@ -986,7 +1031,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@ -1036,7 +1085,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@ -1139,7 +1192,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@ -1168,7 +1225,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
verify(textInputChannel, times(1)).setTextInputMethodHandler(isNotNull());
textInputPlugin.destroy();
verify(textInputChannel, times(1)).setTextInputMethodHandler(isNull());
@ -1186,7 +1247,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@ -1263,7 +1328,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@ -1303,7 +1372,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@ -1335,7 +1408,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@ -1365,7 +1442,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@ -1399,7 +1480,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@ -1434,7 +1519,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@ -1473,7 +1562,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@ -1507,7 +1600,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@ -1542,7 +1639,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
final TextInputChannel.Configuration.Autofill autofill =
new TextInputChannel.Configuration.Autofill(
"1", new String[] {}, null, new TextInputChannel.TextEditState("", 0, 0, -1, -1));
@ -1604,7 +1705,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
final TextInputChannel.Configuration.Autofill autofill =
new TextInputChannel.Configuration.Autofill(
"1", new String[] {}, null, new TextInputChannel.TextEditState("", 0, 0, -1, -1));
@ -1643,7 +1748,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
final TextInputChannel.Configuration.Autofill autofill =
new TextInputChannel.Configuration.Autofill(
"1",
@ -1690,7 +1799,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
final TextInputChannel.Configuration.Autofill autofill1 =
new TextInputChannel.Configuration.Autofill(
"1",
@ -1784,7 +1897,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
final TextInputChannel.Configuration.Autofill autofill =
new TextInputChannel.Configuration.Autofill(
"1",
@ -1839,7 +1956,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
// Set up an autofill scenario with 2 fields.
final TextInputChannel.Configuration.Autofill autofill1 =
@ -1978,7 +2099,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
// Set up an autofill scenario with 2 fields.
final TextInputChannel.Configuration.Autofill autofill1 =
@ -2075,7 +2200,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
// Set up an autofill scenario with 2 fields.
final TextInputChannel.Configuration.Autofill autofillConfig =
new TextInputChannel.Configuration.Autofill(
@ -2128,7 +2257,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
// Set up an autofill scenario with 2 fields.
final TextInputChannel.Configuration.Autofill autofill1 =
@ -2249,7 +2382,11 @@ public class TextInputPluginTest {
View testView = new View(ctx);
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
verify(mockBinaryMessenger, times(1))
.setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture());
@ -2282,7 +2419,11 @@ public class TextInputPluginTest {
View testView = new View(ctx);
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
verify(mockBinaryMessenger, times(1))
.setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture());
@ -2314,7 +2455,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback();
FlutterEngine flutterEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni));
FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni));
@ -2397,7 +2542,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback();
FlutterEngine flutterEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni));
FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni));
@ -2478,7 +2627,11 @@ public class TextInputPluginTest {
ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
new TextInputPlugin(
testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
testView,
textInputChannel,
scribeChannel,
mock(PlatformViewsController.class),
mock(PlatformViewsController2.class));
ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback();
FlutterEngine flutterEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni));
FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni));

View File

@ -0,0 +1,726 @@
// 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 io.flutter.embedding.engine.systemchannels.PlatformViewsChannel2.PlatformViewTouch;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import android.app.Presentation;
import android.content.Context;
import android.content.res.AssetManager;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import io.flutter.embedding.android.FlutterImageView;
import io.flutter.embedding.android.FlutterSurfaceView;
import io.flutter.embedding.android.FlutterView;
import io.flutter.embedding.android.MotionEventTracker;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.mutatorsstack.FlutterMutatorView;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.systemchannels.AccessibilityChannel;
import io.flutter.embedding.engine.systemchannels.MouseCursorChannel;
import io.flutter.embedding.engine.systemchannels.PlatformViewsChannel2;
import io.flutter.embedding.engine.systemchannels.PlatformViewsChannel2.PlatformViewTouch;
import io.flutter.embedding.engine.systemchannels.ScribeChannel;
import io.flutter.embedding.engine.systemchannels.SettingsChannel;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.StandardMessageCodec;
import io.flutter.plugin.common.StandardMethodCodec;
import io.flutter.plugin.localization.LocalizationPlugin;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.shadows.ShadowDialog;
import org.robolectric.shadows.ShadowSurfaceView;
@Config(manifest = Config.NONE)
@RunWith(AndroidJUnit4.class)
public class PlatformViewsController2Test {
// An implementation of PlatformView that counts invocations of its lifecycle callbacks.
class CountingPlatformView implements PlatformView {
static final String VIEW_TYPE_ID = "CountingPlatformView";
private View view;
public CountingPlatformView(Context context) {
view = new SurfaceView(context);
}
public int disposeCalls = 0;
public int attachCalls = 0;
public int detachCalls = 0;
@Override
public void dispose() {
// We have been removed from the view hierarhy before the call to dispose.
assertNull(view.getParent());
disposeCalls++;
}
@Override
public View getView() {
return view;
}
@Override
public void onFlutterViewAttached(View flutterView) {
attachCalls++;
}
@Override
public void onFlutterViewDetached() {
detachCalls++;
}
}
@Test
@Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class})
public void itRemovesPlatformViewBeforeDiposeIsCalled() {
PlatformViewsController2 PlatformViewsController2 = new PlatformViewsController2();
FlutterJNI jni = new FlutterJNI();
attach(jni, PlatformViewsController2);
// Get the platform view registry.
PlatformViewRegistry registry = PlatformViewsController2.getRegistry();
// Register a factory for our platform view.
registry.registerViewFactory(
CountingPlatformView.VIEW_TYPE_ID,
new PlatformViewFactory(StandardMessageCodec.INSTANCE) {
@Override
public PlatformView create(Context context, int viewId, Object args) {
return new CountingPlatformView(context);
}
});
// Create the platform view.
int viewId = 0;
final PlatformViewsChannel2.PlatformViewCreationRequest request =
new PlatformViewsChannel2.PlatformViewCreationRequest(
viewId, CountingPlatformView.VIEW_TYPE_ID, 128, 128, View.LAYOUT_DIRECTION_LTR, null);
PlatformView pView = PlatformViewsController2.createFlutterPlatformView(request);
assertTrue(pView instanceof CountingPlatformView);
CountingPlatformView cpv = (CountingPlatformView) pView;
PlatformViewsController2.disposePlatformView(viewId);
assertEquals(1, cpv.disposeCalls);
}
@Test
@Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class})
public void itNotifiesPlatformViewsOfEngineAttachmentAndDetachment() {
PlatformViewsController2 PlatformViewsController2 = new PlatformViewsController2();
FlutterJNI jni = new FlutterJNI();
attach(jni, PlatformViewsController2);
// Get the platform view registry.
PlatformViewRegistry registry = PlatformViewsController2.getRegistry();
// Register a factory for our platform view.
registry.registerViewFactory(
CountingPlatformView.VIEW_TYPE_ID,
new PlatformViewFactory(StandardMessageCodec.INSTANCE) {
@Override
public PlatformView create(Context context, int viewId, Object args) {
return new CountingPlatformView(context);
}
});
// Create the platform view.
int viewId = 0;
final PlatformViewsChannel2.PlatformViewCreationRequest request =
new PlatformViewsChannel2.PlatformViewCreationRequest(
viewId, CountingPlatformView.VIEW_TYPE_ID, 128, 128, View.LAYOUT_DIRECTION_LTR, null);
PlatformView pView = PlatformViewsController2.createFlutterPlatformView(request);
assertTrue(pView instanceof CountingPlatformView);
CountingPlatformView cpv = (CountingPlatformView) pView;
assertEquals(1, cpv.attachCalls);
assertEquals(0, cpv.detachCalls);
assertEquals(0, cpv.disposeCalls);
PlatformViewsController2.detachFromView();
assertEquals(1, cpv.attachCalls);
assertEquals(1, cpv.detachCalls);
assertEquals(0, cpv.disposeCalls);
PlatformViewsController2.disposePlatformView(viewId);
}
@Test
public void itUsesActionEventTypeFromFrameworkEventAsActionChanged() {
MotionEventTracker motionEventTracker = MotionEventTracker.getInstance();
PlatformViewsController2 PlatformViewsController2 = new PlatformViewsController2();
MotionEvent original =
MotionEvent.obtain(
10, // downTime
10, // eventTime
261, // action
0, // x
0, // y
0 // metaState
);
MotionEventTracker.MotionEventId motionEventId = motionEventTracker.track(original);
PlatformViewTouch frameWorkTouch =
new PlatformViewTouch(
0, // viewId
original.getDownTime(),
original.getEventTime(),
0, // 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 =
PlatformViewsController2.toMotionEvent(
1, // density
frameWorkTouch);
assertEquals(resolvedEvent.getAction(), original.getAction());
assertNotEquals(resolvedEvent.getAction(), frameWorkTouch.action);
}
private MotionEvent makePlatformViewTouchAndInvokeToMotionEvent(
PlatformViewsController2 PlatformViewsController2,
MotionEventTracker motionEventTracker,
MotionEvent original,
boolean usingVirtualDisplays) {
MotionEventTracker.MotionEventId motionEventId = motionEventTracker.track(original);
// Construct a PlatformViewTouch.rawPointerPropertiesList by doing the inverse of
// PlatformViewsController2.parsePointerPropertiesList.
List<List<Integer>> pointerProperties =
Arrays.asList(Arrays.asList(original.getPointerId(0), original.getToolType(0)));
// Construct a PlatformViewTouch.rawPointerCoords by doing the inverse of
// PlatformViewsController2.parsePointerCoordsList.
List<List<Double>> pointerCoordinates =
Arrays.asList(
Arrays.asList(
(double) original.getOrientation(),
(double) original.getPressure(),
(double) original.getSize(),
(double) original.getToolMajor(),
(double) original.getToolMinor(),
(double) original.getTouchMajor(),
(double) original.getTouchMinor(),
(double) original.getX(),
(double) original.getY()));
// Make a platform view touch from the motion event.
PlatformViewTouch frameWorkTouchNonVd =
new PlatformViewTouch(
0, // viewId
original.getDownTime(),
original.getEventTime(),
original.getAction(),
1, // pointerCount
pointerProperties, // pointer properties
pointerCoordinates, // pointer coords
original.getMetaState(),
original.getButtonState(),
original.getXPrecision(),
original.getYPrecision(),
original.getDeviceId(),
original.getEdgeFlags(),
original.getSource(),
original.getFlags(),
motionEventId.getId());
return PlatformViewsController2.toMotionEvent(
1, // density
frameWorkTouchNonVd);
}
@Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class})
public void getPlatformViewById() {
PlatformViewsController2 PlatformViewsController2 = new PlatformViewsController2();
int platformViewId = 0;
assertNull(PlatformViewsController2.getPlatformViewById(platformViewId));
PlatformViewFactory viewFactory = mock(PlatformViewFactory.class);
PlatformView platformView = mock(PlatformView.class);
View androidView = mock(View.class);
when(platformView.getView()).thenReturn(androidView);
when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView);
PlatformViewsController2.getRegistry().registerViewFactory("testType", viewFactory);
FlutterJNI jni = new FlutterJNI();
attach(jni, PlatformViewsController2);
// Simulate create call from the framework.
createPlatformView(jni, PlatformViewsController2, platformViewId, "testType");
assertTrue(PlatformViewsController2.initializePlatformViewIfNeeded(platformViewId));
View resultAndroidView = PlatformViewsController2.getPlatformViewById(platformViewId);
assertNotNull(resultAndroidView);
assertEquals(resultAndroidView, androidView);
}
@Test
@Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class})
public void createPlatformViewMessage_initializesAndroidView() {
PlatformViewsController2 PlatformViewsController2 = new PlatformViewsController2();
int platformViewId = 0;
assertNull(PlatformViewsController2.getPlatformViewById(platformViewId));
PlatformViewFactory viewFactory = mock(PlatformViewFactory.class);
PlatformView platformView = mock(PlatformView.class);
when(platformView.getView()).thenReturn(mock(View.class));
when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView);
PlatformViewsController2.getRegistry().registerViewFactory("testType", viewFactory);
FlutterJNI jni = new FlutterJNI();
attach(jni, PlatformViewsController2);
// Simulate create call from the framework.
createPlatformView(jni, PlatformViewsController2, platformViewId, "testType");
verify(viewFactory, times(1)).create(any(), eq(platformViewId), any());
}
@Test
@Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class})
public void createPlatformViewMessage_setsAndroidViewLayoutDirection() {
PlatformViewsController2 PlatformViewsController2 = new PlatformViewsController2();
int platformViewId = 0;
assertNull(PlatformViewsController2.getPlatformViewById(platformViewId));
PlatformViewFactory viewFactory = mock(PlatformViewFactory.class);
PlatformView platformView = mock(PlatformView.class);
View androidView = mock(View.class);
when(platformView.getView()).thenReturn(androidView);
when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView);
PlatformViewsController2.getRegistry().registerViewFactory("testType", viewFactory);
FlutterJNI jni = new FlutterJNI();
attach(jni, PlatformViewsController2);
// Simulate create call from the framework.
createPlatformView(jni, PlatformViewsController2, platformViewId, "testType");
verify(androidView, times(1)).setLayoutDirection(0);
}
@Test
@Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class})
public void createPlatformViewMessage_throwsIfViewIsNull() {
PlatformViewsController2 PlatformViewsController2 = new PlatformViewsController2();
int platformViewId = 0;
assertNull(PlatformViewsController2.getPlatformViewById(platformViewId));
PlatformViewFactory viewFactory = mock(PlatformViewFactory.class);
PlatformView platformView = mock(PlatformView.class);
when(platformView.getView()).thenReturn(null);
when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView);
PlatformViewsController2.getRegistry().registerViewFactory("testType", viewFactory);
FlutterJNI jni = new FlutterJNI();
attach(jni, PlatformViewsController2);
// Simulate create call from the framework.
createPlatformView(jni, PlatformViewsController2, platformViewId, "testType");
assertEquals(ShadowFlutterJNI.getResponses().size(), 1);
assertFalse(PlatformViewsController2.initializePlatformViewIfNeeded(platformViewId));
}
@Test
@Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class})
public void createHybridPlatformViewMessage_throwsIfViewIsNull() {
PlatformViewsController2 PlatformViewsController2 = new PlatformViewsController2();
int platformViewId = 0;
assertNull(PlatformViewsController2.getPlatformViewById(platformViewId));
PlatformViewFactory viewFactory = mock(PlatformViewFactory.class);
PlatformView platformView = mock(PlatformView.class);
when(platformView.getView()).thenReturn(null);
when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView);
PlatformViewsController2.getRegistry().registerViewFactory("testType", viewFactory);
FlutterJNI jni = new FlutterJNI();
attach(jni, PlatformViewsController2);
// Simulate create call from the framework.
createPlatformView(jni, PlatformViewsController2, platformViewId, "testType");
assertEquals(ShadowFlutterJNI.getResponses().size(), 1);
assertFalse(PlatformViewsController2.initializePlatformViewIfNeeded(platformViewId));
}
@Test
@Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class})
public void setPlatformViewDirection_throwIfPlatformViewNotFound() {
PlatformViewsController2 PlatformViewsController2 = new PlatformViewsController2();
int platformViewId = 0;
assertNull(PlatformViewsController2.getPlatformViewById(platformViewId));
PlatformViewFactory viewFactory = mock(PlatformViewFactory.class);
PlatformView platformView = mock(PlatformView.class);
final View androidView = mock(View.class);
when(platformView.getView()).thenReturn(androidView);
when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView);
PlatformViewsController2.getRegistry().registerViewFactory("testType", viewFactory);
FlutterJNI jni = new FlutterJNI();
attach(jni, PlatformViewsController2);
verify(androidView, never()).setLayoutDirection(anyInt());
// Simulate create call from the framework.
createPlatformView(jni, PlatformViewsController2, platformViewId, "testType");
assertEquals(ShadowFlutterJNI.getResponses().size(), 1);
// Simulate set direction call from the framework.
setLayoutDirection(jni, PlatformViewsController2, platformViewId, 1);
verify(androidView, times(1)).setLayoutDirection(1);
// The limit value of reply message will be equal to 2 if the layout direction is set
// successfully, otherwise it will be much more than 2 due to the reply message contains
// an error message wrapped with exception detail information.
assertEquals(ShadowFlutterJNI.getResponses().get(0).limit(), 2);
}
@Test
@Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class})
public void disposeAndroidView() {
PlatformViewsController2 PlatformViewsController2 = new PlatformViewsController2();
int platformViewId = 0;
assertNull(PlatformViewsController2.getPlatformViewById(platformViewId));
PlatformViewFactory viewFactory = mock(PlatformViewFactory.class);
PlatformView platformView = mock(PlatformView.class);
Context context = ApplicationProvider.getApplicationContext();
View androidView = new View(context);
when(platformView.getView()).thenReturn(androidView);
when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView);
PlatformViewsController2.getRegistry().registerViewFactory("testType", viewFactory);
FlutterJNI jni = new FlutterJNI();
attach(jni, PlatformViewsController2);
// Simulate create call from the framework.
createPlatformView(jni, PlatformViewsController2, platformViewId, "testType");
assertTrue(PlatformViewsController2.initializePlatformViewIfNeeded(platformViewId));
assertNotNull(androidView.getParent());
assertTrue(androidView.getParent() instanceof FlutterMutatorView);
// Simulate dispose call from the framework.
disposePlatformView(jni, PlatformViewsController2, platformViewId);
assertNull(androidView.getParent());
// Simulate create call from the framework.
createPlatformView(jni, PlatformViewsController2, platformViewId, "testType");
assertTrue(PlatformViewsController2.initializePlatformViewIfNeeded(platformViewId));
assertNotNull(androidView.getParent());
assertTrue(androidView.getParent() instanceof FlutterMutatorView);
verify(platformView, times(1)).dispose();
}
@Test
@Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class})
public void disposeNullAndroidView() {
PlatformViewsController2 PlatformViewsController2 = new PlatformViewsController2();
int platformViewId = 0;
assertNull(PlatformViewsController2.getPlatformViewById(platformViewId));
PlatformViewFactory viewFactory = mock(PlatformViewFactory.class);
PlatformView platformView = mock(PlatformView.class);
View androidView = mock(View.class);
when(platformView.getView()).thenReturn(androidView);
when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView);
PlatformViewsController2.getRegistry().registerViewFactory("testType", viewFactory);
FlutterJNI jni = new FlutterJNI();
attach(jni, PlatformViewsController2);
// Simulate create call from the framework.
createPlatformView(jni, PlatformViewsController2, platformViewId, "testType");
assertTrue(PlatformViewsController2.initializePlatformViewIfNeeded(platformViewId));
when(platformView.getView()).thenReturn(null);
// Simulate dispose call from the framework.
disposePlatformView(jni, PlatformViewsController2, platformViewId);
verify(platformView, times(1)).dispose();
}
private static ByteBuffer encodeMethodCall(MethodCall call) {
final ByteBuffer buffer = StandardMethodCodec.INSTANCE.encodeMethodCall(call);
buffer.rewind();
return buffer;
}
private static void createPlatformView(
FlutterJNI jni,
PlatformViewsController2 PlatformViewsController2,
int platformViewId,
String viewType) {
final Map<String, Object> args = new HashMap<>();
args.put("id", platformViewId);
args.put("viewType", viewType);
args.put("direction", 0);
args.put("width", 1.0);
args.put("height", 1.0);
final MethodCall platformCreateMethodCall = new MethodCall("create", args);
jni.handlePlatformMessage(
"flutter/platform_views_2",
encodeMethodCall(platformCreateMethodCall),
/*replyId=*/ 0,
/*messageData=*/ 0);
}
private static void setLayoutDirection(
FlutterJNI jni,
PlatformViewsController2 PlatformViewsController2,
int platformViewId,
int direction) {
final Map<String, Object> args = new HashMap<>();
args.put("id", platformViewId);
args.put("direction", direction);
final MethodCall platformSetDirectionMethodCall = new MethodCall("setDirection", args);
jni.handlePlatformMessage(
"flutter/platform_views_2",
encodeMethodCall(platformSetDirectionMethodCall),
/*replyId=*/ 0,
/*messageData=*/ 0);
}
private static void disposePlatformView(
FlutterJNI jni, PlatformViewsController2 PlatformViewsController2, int platformViewId) {
final Map<String, Object> args = new HashMap<>();
args.put("id", platformViewId);
final MethodCall platformDisposeMethodCall = new MethodCall("dispose", args);
jni.handlePlatformMessage(
"flutter/platform_views_2",
encodeMethodCall(platformDisposeMethodCall),
/*replyId=*/ 0,
/*messageData=*/ 0);
}
private static void synchronizeToNativeViewHierarchy(
FlutterJNI jni, PlatformViewsController2 PlatformViewsController2, boolean yes) {
final MethodCall convertMethodCall = new MethodCall("synchronizeToNativeViewHierarchy", yes);
jni.handlePlatformMessage(
"flutter/platform_views_2",
encodeMethodCall(convertMethodCall),
/*replyId=*/ 0,
/*messageData=*/ 0);
}
private static FlutterView attach(
FlutterJNI jni, PlatformViewsController2 PlatformViewsController2) {
final Context context = ApplicationProvider.getApplicationContext();
final FlutterView flutterView =
new FlutterView(context, new FlutterSurfaceView(context)) {
@Override
public FlutterImageView createImageView() {
final FlutterImageView view = mock(FlutterImageView.class);
when(view.acquireLatestImage()).thenReturn(true);
return mock(FlutterImageView.class);
}
};
attachToFlutterView(jni, PlatformViewsController2, flutterView);
return flutterView;
}
private static void attachToFlutterView(
FlutterJNI jni, PlatformViewsController2 PlatformViewsController2, FlutterView flutterView) {
final DartExecutor executor = new DartExecutor(jni, mock(AssetManager.class));
executor.onAttachedToJNI();
final Context context = ApplicationProvider.getApplicationContext();
PlatformViewsController2.attach(context, executor);
PlatformViewsController oldController = new PlatformViewsController();
final FlutterEngine engine = mock(FlutterEngine.class);
when(engine.getRenderer()).thenReturn(new FlutterRenderer(jni));
when(engine.getMouseCursorChannel()).thenReturn(mock(MouseCursorChannel.class));
when(engine.getTextInputChannel()).thenReturn(mock(TextInputChannel.class));
when(engine.getSettingsChannel()).thenReturn(new SettingsChannel(executor));
when(engine.getScribeChannel()).thenReturn(mock(ScribeChannel.class));
when(engine.getPlatformViewsController2()).thenReturn(PlatformViewsController2);
when(engine.getPlatformViewsController()).thenReturn(oldController);
when(engine.getLocalizationPlugin()).thenReturn(mock(LocalizationPlugin.class));
when(engine.getAccessibilityChannel()).thenReturn(mock(AccessibilityChannel.class));
when(engine.getDartExecutor()).thenReturn(executor);
flutterView.attachToFlutterEngine(engine);
PlatformViewsController2.attachToView(flutterView);
}
/**
* For convenience when writing tests, this allows us to make fake messages from Flutter via
* Platform Channels. Typically those calls happen on the ui thread which dispatches to the
* platform thread. Since tests run on the platform thread it makes it difficult to test without
* this, but isn't technically required.
*/
@Implements(io.flutter.embedding.engine.dart.PlatformTaskQueue.class)
public static class ShadowPlatformTaskQueue {
@Implementation
public void dispatch(Runnable runnable) {
runnable.run();
}
}
/**
* The shadow class of {@link Presentation} to simulate Presentation showing logic.
*
* <p>Robolectric doesn't support VirtualDisplay creating correctly now, so this shadow class is
* used to simulate custom logic for Presentation.
*/
@Implements(Presentation.class)
public static class ShadowPresentation extends ShadowDialog {
private boolean isShowing = false;
public ShadowPresentation() {}
@Implementation
protected void show() {
isShowing = true;
}
@Implementation
protected void dismiss() {
isShowing = false;
}
@Implementation
protected boolean isShowing() {
return isShowing;
}
}
@Implements(FlutterJNI.class)
public static class ShadowFlutterJNI {
private static SparseArray<ByteBuffer> replies = new SparseArray<>();
public ShadowFlutterJNI() {}
@Implementation
public boolean getIsSoftwareRenderingEnabled() {
return false;
}
@Implementation
public long performNativeAttach(FlutterJNI flutterJNI) {
return 1;
}
@Implementation
public void dispatchPlatformMessage(
String channel, ByteBuffer message, int position, int responseId) {}
@Implementation
public void onSurfaceCreated(Surface surface) {}
@Implementation
public void onSurfaceDestroyed() {}
@Implementation
public void onSurfaceWindowChanged(Surface surface) {}
@Implementation
public void setViewportMetrics(
float devicePixelRatio,
int physicalWidth,
int physicalHeight,
int physicalPaddingTop,
int physicalPaddingRight,
int physicalPaddingBottom,
int physicalPaddingLeft,
int physicalViewInsetTop,
int physicalViewInsetRight,
int physicalViewInsetBottom,
int physicalViewInsetLeft,
int systemGestureInsetTop,
int systemGestureInsetRight,
int systemGestureInsetBottom,
int systemGestureInsetLeft,
int physicalTouchSlop,
int[] displayFeaturesBounds,
int[] displayFeaturesType,
int[] displayFeaturesState) {}
@Implementation
public void invokePlatformMessageResponseCallback(
int responseId, ByteBuffer message, int position) {
replies.put(responseId, message);
}
public static SparseArray<ByteBuffer> getResponses() {
return replies;
}
}
@Implements(SurfaceView.class)
public static class ShadowFlutterSurfaceView extends ShadowSurfaceView {
private final FakeSurfaceHolder holder = new FakeSurfaceHolder();
public static class FakeSurfaceHolder extends ShadowSurfaceView.FakeSurfaceHolder {
private final Surface surface = mock(Surface.class);
public Surface getSurface() {
return surface;
}
@Implementation
public void addCallback(SurfaceHolder.Callback callback) {
callback.surfaceCreated(this);
}
}
public ShadowFlutterSurfaceView() {}
@Implementation
public SurfaceHolder getHolder() {
return holder;
}
}
}

View File

@ -1664,6 +1664,8 @@ public class PlatformViewsControllerTest {
platformViewsController.attach(context, registry, executor);
PlatformViewsController2 secondController = new PlatformViewsController2();
final FlutterEngine engine = mock(FlutterEngine.class);
when(engine.getRenderer()).thenReturn(new FlutterRenderer(jni));
when(engine.getMouseCursorChannel()).thenReturn(mock(MouseCursorChannel.class));
@ -1671,6 +1673,7 @@ public class PlatformViewsControllerTest {
when(engine.getSettingsChannel()).thenReturn(new SettingsChannel(executor));
when(engine.getScribeChannel()).thenReturn(mock(ScribeChannel.class));
when(engine.getPlatformViewsController()).thenReturn(platformViewsController);
when(engine.getPlatformViewsController2()).thenReturn(secondController);
when(engine.getLocalizationPlugin()).thenReturn(mock(LocalizationPlugin.class));
when(engine.getAccessibilityChannel()).thenReturn(mock(AccessibilityChannel.class));
when(engine.getDartExecutor()).thenReturn(executor);