diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 476cee8211866..6e12ed7f3cd7e 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -44357,6 +44357,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 @@ -44400,6 +44401,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 @@ -47296,6 +47298,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 @@ -47343,6 +47346,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 diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 4b6ea453c09ed..46a5d647e5b06 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -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", diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index a1eaed6da7f03..cc9a93fe5caeb 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -1129,7 +1129,8 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) { this, this.flutterEngine.getTextInputChannel(), this.flutterEngine.getScribeChannel(), - this.flutterEngine.getPlatformViewsController()); + this.flutterEngine.getPlatformViewsController(), + this.flutterEngine.getPlatformViewsController2()); try { textServicesManager = @@ -1168,6 +1169,10 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) { this.flutterEngine .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. @@ -1186,6 +1191,7 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) { sendViewportMetricsToFlutter(); flutterEngine.getPlatformViewsController().attachToView(this); + flutterEngine.getPlatformViewsController2().attachToView(this); // Notify engine attachment listeners of the attachment. for (FlutterEngineAttachmentListener listener : flutterEngineAttachmentListeners) { @@ -1226,9 +1232,11 @@ public void detachFromFlutterEngine() { 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(); diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index 683dfeace7937..912d61eada85f 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -41,6 +41,7 @@ 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 engineLifecycleListeners = new HashSet<>(); @@ -124,6 +126,7 @@ public void onPreEngineRestart() { } platformViewsController.onPreEngineRestart(); + platformViewsController2.onPreEngineRestart(); restorationChannel.clearData(); } @@ -359,9 +362,11 @@ public FlutterEngine( flutterLoader.startInitialization(context.getApplicationContext()); flutterLoader.ensureInitializationComplete(context, dartVmArgs); } + PlatformViewsController2 platformViewsController2 = new PlatformViewsController2(); flutterJNI.addEngineLifecycleListener(engineLifecycleListener); flutterJNI.setPlatformViewsController(platformViewsController); + flutterJNI.setPlatformViewsController2(platformViewsController2); flutterJNI.setLocalizationPlugin(localizationPlugin); flutterJNI.setDeferredComponentManager(injector.deferredComponentManager()); @@ -379,6 +384,8 @@ public FlutterEngine( this.platformViewsController = platformViewsController; this.platformViewsController.onAttachedToJNI(); + this.platformViewsController2 = platformViewsController2; + this.pluginRegistry = new FlutterEngineConnectionRegistry( context.getApplicationContext(), this, flutterLoader, group); @@ -472,6 +479,7 @@ public void destroy() { // 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 +656,11 @@ public PlatformViewsController getPlatformViewsController() { return platformViewsController; } + @NonNull + public PlatformViewsController2 getPlatformViewsController2() { + return platformViewsController2; + } + @NonNull public ActivityControlSurface getActivityControlSurface() { return pluginRegistry; diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java index 4747abac87ecc..95f217a212822 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java @@ -18,6 +18,7 @@ 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.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 boolean isCodePointRegionalIndicator(int codePoint) { @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 void setPlatformViewsController(@NonNull PlatformViewsController platform 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. @@ -1272,6 +1282,75 @@ public void destroyOverlaySurfaces() { } platformViewsController.destroyOverlaySurfaces(); } + // ----- 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); + } + // ----- End Engine Lifecycle Support ---- // ----- Start Localization Support ---- diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel2.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel2.java new file mode 100644 index 0000000000000..8966e39e4e0c4 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel2.java @@ -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. + * + *

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}. + * + *

The given {@code dartExecutor} is permitted to be idle or executing code. + * + *

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 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 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 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 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}. + * + *

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. + * + *

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. + * + *

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; + } + } +} diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 4f3adcec4d8a4..d6d6160786312 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -33,6 +33,7 @@ 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 TextInputPlugin( @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 void sendAppPrivateCommand(String action, Bundle data) { this.platformViewsController = platformViewsController; this.platformViewsController.attachTextInputPlugin(this); + this.platformViewsController2 = platformViewsController2; + this.platformViewsController2.attachTextInputPlugin(this); } @NonNull @@ -215,6 +220,7 @@ public void unlockPlatformViewInputConnection() { @SuppressLint("NewApi") public void destroy() { platformViewsController.detachTextInputPlugin(); + platformViewsController2.detachTextInputPlugin(); textInputChannel.setTextInputMethodHandler(null); notifyViewExited(); mEditable.removeEditingStateListener(this); diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController2.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController2.java new file mode 100644 index 0000000000000..5d0eba145d47a --- /dev/null +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController2.java @@ -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. + * + *

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 platformViews; + private final SparseArray platformViewParent; + private final MotionEventTracker motionEventTracker; + + private final ArrayList pendingTransactions; + private final ArrayList 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. + * + *

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}. + * + *

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}. + * + *

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. + * + *

While a text input plugin is available, the platform views controller interacts with it to + * facilitate delegation of text input connections to platform views. + * + *

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 parsePointerPropertiesList(Object rawPropertiesList) { + List rawProperties = (List) rawPropertiesList; + List pointerProperties = new ArrayList<>(); + for (Object o : rawProperties) { + pointerProperties.add(parsePointerProperties(o)); + } + return pointerProperties; + } + + @SuppressWarnings("unchecked") + private static PointerProperties parsePointerProperties(Object rawProperties) { + List propertiesList = (List) 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 parsePointerCoordsList(Object rawCoordsList, float density) { + List rawCoords = (List) rawCoordsList; + List 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 coordsList = (List) 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(); + } + }; +} diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java index ae1e371a48d6a..9cc1f115d8c81 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java @@ -55,6 +55,7 @@ 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 @@ private FlutterEngine mockFlutterEngine() { 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); diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java index 7247873ef1e0d..bc7c48ebeab3d 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java @@ -53,6 +53,7 @@ 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 void attachToFlutterEngine_alertsPlatformViews() { 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 void detachFromFlutterEngine_alertsPlatformViews() { 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 diff --git a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java index d10df7bf1aee2..d879a4abf68ce 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -63,6 +63,7 @@ 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 void textInputPlugin_RequestsReattachOnCreation() throws JSONException { 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 channelCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); @@ -160,7 +165,11 @@ public void setTextInputEditingState_doesNotInvokeUpdateEditingState() { 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 void setTextInputEditingState_willNotThrowWithoutSetTextInputClient() { 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 void setTextInputEditingState_doesNotInvokeUpdateEditingStateWithDeltas() 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 void textEditingDelta_TestUpdateEditingValueWithDeltasIsNotInvokedWhenDel 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 void textEditingDelta_TestUpdateEditingValueIsNotInvokedWhenDeltaModelEna 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 void textEditingDelta_TestDeltaIsCreatedWhenComposingTextSetIsInserting() 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 void textEditingDelta_TestDeltaIsCreatedWhenComposingTextSetIsDeleting() 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 void textEditingDelta_TestDeltaIsCreatedWhenComposingTextSetIsReplacing() 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 void inputConnectionAdaptor_RepeatFilter() throws NullPointerException { 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 void setTextInputEditingState_doesNotRestartWhenTextIsIdentical() { 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 void setTextInputEditingState_alwaysSetEditableWhenDifferent() { 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 void setTextInputEditingState_restartsIMEOnlyWhenFrameworkChangesComposin 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 void setTextInputEditingState_nullInputMethodSubtype() { 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 void destroy_clearTextInputMethodHandler() { 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 @@ private void verifyInputConnection(TextInputChannel.TextInputType textInputType) 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 void inputConnection_finishComposingTextUpdatesIMM() throws JSONException 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 void inputConnection_textInputTypeNone() { 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 void showTextInput_textInputTypeNone() { 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 void showTextInput_textInputTypeWebSearch() { 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 void inputConnection_textInputTypeMultilineAndSuggestionsDisabled() { 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( @@ -1438,7 +1523,11 @@ public void inputConnection_setsStylusHandwritingAvailable() { 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( @@ -1472,7 +1561,11 @@ public void inputConnection_doesNotcallSetsStylusHandwritingAvailableWhenAPILeve 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 void autofill_enabledByDefault() { 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)); @@ -1569,7 +1666,11 @@ public void autofill_canBeDisabled() { 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)); @@ -1608,7 +1709,11 @@ public void autofill_hintText() { 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", @@ -1655,7 +1760,11 @@ public void autofill_onProvideVirtualViewStructure() { 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", @@ -1749,7 +1858,11 @@ public void autofill_onProvideVirtualViewStructure_singular_textfield() { 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", @@ -1804,7 +1917,11 @@ public void autofill_testLifeCycle() { 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 = @@ -1943,7 +2060,11 @@ public void autofill_testAutofillUpdatesTheFramework() { 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 = @@ -2040,7 +2161,11 @@ public void autofill_doesNotCrashAfterClearClientCall() { 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( @@ -2093,7 +2218,11 @@ public void autofill_testSetTextIpnutClientUpdatesSideFields() { 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 = @@ -2214,7 +2343,11 @@ public void sendAppPrivateCommand_dataIsEmpty() throws JSONException { 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()); @@ -2247,7 +2380,11 @@ public void sendAppPrivateCommand_hasData() throws JSONException { 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()); @@ -2279,7 +2416,11 @@ public void ime_windowInsetsSync_notLaidOutBehindNavigation_excludesNavigationBa 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)); @@ -2362,7 +2503,11 @@ public void ime_windowInsetsSync_laidOutBehindNavigation_includesNavigationBars( 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)); @@ -2443,7 +2588,11 @@ public void lastWindowInsets_updatedOnSecondOnProgressCall() { 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)); diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsController2Test.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsController2Test.java new file mode 100644 index 0000000000000..eeab758349f33 --- /dev/null +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsController2Test.java @@ -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> pointerProperties = + Arrays.asList(Arrays.asList(original.getPointerId(0), original.getToolType(0))); + // Construct a PlatformViewTouch.rawPointerCoords by doing the inverse of + // PlatformViewsController2.parsePointerCoordsList. + List> 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 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 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 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. + * + *

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 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 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; + } + } +} diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java index 4b36393b71924..0b3e3aaa0ae5b 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java @@ -1664,6 +1664,8 @@ public void scheduleFrame() {} 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 void scheduleFrame() {} 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);