From b3e7732cf9b3fa628c769fa3f3f4f1494b5d0e98 Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Mon, 1 Jun 2020 18:08:43 -0700 Subject: [PATCH] System mouse cursor: Android (#18569) Adds system mouse cursor to the Android engine. --- ci/licenses_golden/licenses_flutter | 2 + shell/platform/android/BUILD.gn | 3 + .../embedding/android/FlutterView.java | 18 ++- .../embedding/engine/FlutterEngine.java | 9 ++ .../systemchannels/MouseCursorChannel.java | 83 ++++++++++++++ .../plugin/mouse/MouseCursorPlugin.java | 107 ++++++++++++++++++ .../android/io/flutter/view/FlutterView.java | 20 +++- .../test/io/flutter/FlutterTestSuite.java | 2 + ...lutterActivityAndFragmentDelegateTest.java | 2 + .../plugin/mouse/MouseCursorPluginTest.java | 71 ++++++++++++ 10 files changed, 315 insertions(+), 2 deletions(-) create mode 100644 shell/platform/android/io/flutter/embedding/engine/systemchannels/MouseCursorChannel.java create mode 100644 shell/platform/android/io/flutter/plugin/mouse/MouseCursorPlugin.java create mode 100644 shell/platform/android/test/io/flutter/plugin/mouse/MouseCursorPluginTest.java diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 1fbe5193b..8c799234c 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -727,6 +727,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/system FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LifecycleChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/MouseCursorChannel.java 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 @@ -754,6 +755,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/common/StringCod FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/mouse/MouseCursorPlugin.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/AccessibilityEventsDelegate.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformView.java diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 2d40c673b..077e203f6 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -177,6 +177,7 @@ android_java_sources = [ "io/flutter/embedding/engine/systemchannels/KeyEventChannel.java", "io/flutter/embedding/engine/systemchannels/LifecycleChannel.java", "io/flutter/embedding/engine/systemchannels/LocalizationChannel.java", + "io/flutter/embedding/engine/systemchannels/MouseCursorChannel.java", "io/flutter/embedding/engine/systemchannels/NavigationChannel.java", "io/flutter/embedding/engine/systemchannels/PlatformChannel.java", "io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java", @@ -204,6 +205,7 @@ android_java_sources = [ "io/flutter/plugin/editing/FlutterTextUtils.java", "io/flutter/plugin/editing/InputConnectionAdaptor.java", "io/flutter/plugin/editing/TextInputPlugin.java", + "io/flutter/plugin/mouse/MouseCursorPlugin.java", "io/flutter/plugin/platform/AccessibilityEventsDelegate.java", "io/flutter/plugin/platform/PlatformPlugin.java", "io/flutter/plugin/platform/PlatformView.java", @@ -442,6 +444,7 @@ action("robolectric_tests") { "test/io/flutter/plugin/common/StandardMethodCodecTest.java", "test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java", "test/io/flutter/plugin/editing/TextInputPluginTest.java", + "test/io/flutter/plugin/mouse/MouseCursorPluginTest.java", "test/io/flutter/plugin/platform/PlatformPluginTest.java", "test/io/flutter/plugin/platform/SingleViewPresentationTest.java", "test/io/flutter/plugins/GeneratedPluginRegistrant.java", diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 7f8b7440e..a2091e2c2 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -17,6 +17,7 @@ import android.util.AttributeSet; import android.util.SparseArray; import android.view.KeyEvent; import android.view.MotionEvent; +import android.view.PointerIcon; import android.view.Surface; import android.view.View; import android.view.ViewStructure; @@ -39,6 +40,7 @@ import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener; import io.flutter.embedding.engine.renderer.RenderSurface; import io.flutter.embedding.engine.systemchannels.SettingsChannel; import io.flutter.plugin.editing.TextInputPlugin; +import io.flutter.plugin.mouse.MouseCursorPlugin; import io.flutter.plugin.platform.PlatformViewsController; import io.flutter.view.AccessibilityBridge; import java.util.ArrayList; @@ -75,7 +77,7 @@ import java.util.Set; * See https://source.android.com/devices/graphics/arch-tv#surface_or_texture for more * information comparing {@link android.view.SurfaceView} and {@link android.view.TextureView}. */ -public class FlutterView extends FrameLayout { +public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseCursorViewDelegate { private static final String TAG = "FlutterView"; // Internal view hierarchy references. @@ -97,6 +99,7 @@ public class FlutterView extends FrameLayout { // // These components essentially add some additional behavioral logic on top of // existing, stateless system channels, e.g., KeyEventChannel, TextInputChannel, etc. + @Nullable private MouseCursorPlugin mouseCursorPlugin; @Nullable private TextInputPlugin textInputPlugin; @Nullable private AndroidKeyProcessor androidKeyProcessor; @Nullable private AndroidTouchProcessor androidTouchProcessor; @@ -756,6 +759,16 @@ public class FlutterView extends FrameLayout { } // -------- End: Accessibility --------- + // -------- Start: Mouse ------- + @Override + @TargetApi(Build.VERSION_CODES.N) + @RequiresApi(Build.VERSION_CODES.N) + @NonNull + public PointerIcon getSystemPointerIcon(int type) { + return PointerIcon.getSystemIcon(getContext(), type); + } + // -------- End: Mouse --------- + /** * Connects this {@code FlutterView} to the given {@link FlutterEngine}. * @@ -794,6 +807,9 @@ public class FlutterView extends FrameLayout { // Initialize various components that know how to process Android View I/O // in a way that Flutter understands. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mouseCursorPlugin = new MouseCursorPlugin(this, this.flutterEngine.getMouseCursorChannel()); + } textInputPlugin = new TextInputPlugin( this, diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index 271a1a8ec..cd22f732d 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -21,6 +21,7 @@ import io.flutter.embedding.engine.systemchannels.AccessibilityChannel; import io.flutter.embedding.engine.systemchannels.KeyEventChannel; import io.flutter.embedding.engine.systemchannels.LifecycleChannel; import io.flutter.embedding.engine.systemchannels.LocalizationChannel; +import io.flutter.embedding.engine.systemchannels.MouseCursorChannel; import io.flutter.embedding.engine.systemchannels.NavigationChannel; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.embedding.engine.systemchannels.SettingsChannel; @@ -77,6 +78,7 @@ public class FlutterEngine { @NonNull private final KeyEventChannel keyEventChannel; @NonNull private final LifecycleChannel lifecycleChannel; @NonNull private final LocalizationChannel localizationChannel; + @NonNull private final MouseCursorChannel mouseCursorChannel; @NonNull private final NavigationChannel navigationChannel; @NonNull private final PlatformChannel platformChannel; @NonNull private final SettingsChannel settingsChannel; @@ -218,6 +220,7 @@ public class FlutterEngine { keyEventChannel = new KeyEventChannel(dartExecutor); lifecycleChannel = new LifecycleChannel(dartExecutor); localizationChannel = new LocalizationChannel(dartExecutor); + mouseCursorChannel = new MouseCursorChannel(dartExecutor); navigationChannel = new NavigationChannel(dartExecutor); platformChannel = new PlatformChannel(dartExecutor); settingsChannel = new SettingsChannel(dartExecutor); @@ -391,6 +394,12 @@ public class FlutterEngine { return systemChannel; } + /** System channel that sends and receives text input requests and state. */ + @NonNull + public MouseCursorChannel getMouseCursorChannel() { + return mouseCursorChannel; + } + /** System channel that sends and receives text input requests and state. */ @NonNull public TextInputChannel getTextInputChannel() { diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/MouseCursorChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/MouseCursorChannel.java new file mode 100644 index 000000000..6440c7c9b --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/MouseCursorChannel.java @@ -0,0 +1,83 @@ +// 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 android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +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.util.HashMap; + +/** System channel that receives requests for mouse cursor behavior, e.g., set as system cursors. */ +public class MouseCursorChannel { + private static final String TAG = "MouseCursorChannel"; + + @NonNull public final MethodChannel channel; + @Nullable private MouseCursorMethodHandler mouseCursorMethodHandler; + + public MouseCursorChannel(@NonNull DartExecutor dartExecutor) { + channel = new MethodChannel(dartExecutor, "flutter/mousecursor", StandardMethodCodec.INSTANCE); + channel.setMethodCallHandler(parsingMethodCallHandler); + } + + /** + * Sets the {@link MouseCursorMethodHandler} which receives all events and requests that are + * parsed from the underlying platform channel. + */ + public void setMethodHandler(@Nullable MouseCursorMethodHandler mouseCursorMethodHandler) { + this.mouseCursorMethodHandler = mouseCursorMethodHandler; + } + + @NonNull + private final MethodChannel.MethodCallHandler parsingMethodCallHandler = + new MethodChannel.MethodCallHandler() { + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + if (mouseCursorMethodHandler == null) { + // If no explicit mouseCursorMethodHandler has been registered then we don't + // need to forward this call to an API. Return. + return; + } + + final String method = call.method; + Log.v(TAG, "Received '" + method + "' message."); + try { + // More methods are expected to be added here, hence the switch. + switch (method) { + case "activateSystemCursor": + @SuppressWarnings("unchecked") + final HashMap data = (HashMap) call.arguments; + final String kind = (String) data.get("kind"); + try { + mouseCursorMethodHandler.activateSystemCursor(kind); + } catch (Exception e) { + result.error("error", "Error when setting cursors: " + e.getMessage(), null); + break; + } + result.success(true); + break; + default: + } + } catch (Exception e) { + result.error("error", "Unhandled error: " + e.getMessage(), null); + } + } + }; + + @VisibleForTesting + public void synthesizeMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + parsingMethodCallHandler.onMethodCall(call, result); + } + + public interface MouseCursorMethodHandler { + // Called when the pointer should start displaying a system mouse cursor + // specified by {@code shapeCode}. + public void activateSystemCursor(@NonNull String kind); + } +} diff --git a/shell/platform/android/io/flutter/plugin/mouse/MouseCursorPlugin.java b/shell/platform/android/io/flutter/plugin/mouse/MouseCursorPlugin.java new file mode 100644 index 000000000..f78d780ec --- /dev/null +++ b/shell/platform/android/io/flutter/plugin/mouse/MouseCursorPlugin.java @@ -0,0 +1,107 @@ +// 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.mouse; + +import android.annotation.TargetApi; +import android.os.Build; +import android.view.PointerIcon; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import io.flutter.embedding.engine.systemchannels.MouseCursorChannel; +import java.util.HashMap; + +/** A mandatory plugin that handles mouse cursor requests. */ +@TargetApi(Build.VERSION_CODES.N) +@RequiresApi(Build.VERSION_CODES.N) +public class MouseCursorPlugin { + @NonNull private final MouseCursorViewDelegate mView; + @NonNull private final MouseCursorChannel mouseCursorChannel; + + public MouseCursorPlugin( + @NonNull MouseCursorViewDelegate view, @NonNull MouseCursorChannel mouseCursorChannel) { + mView = view; + + this.mouseCursorChannel = mouseCursorChannel; + mouseCursorChannel.setMethodHandler( + new MouseCursorChannel.MouseCursorMethodHandler() { + @Override + public void activateSystemCursor(@NonNull String kind) { + mView.setPointerIcon(resolveSystemCursor(kind)); + } + }); + } + + /** + * Return a pointer icon object for a system cursor. + * + *

This method guarantees to return a non-null object. + */ + private PointerIcon resolveSystemCursor(@NonNull String kind) { + if (MouseCursorPlugin.systemCursorConstants == null) { + // Initialize the map when first used, because the map can grow big in the future (~70) + // and most mobile devices will not use them. + MouseCursorPlugin.systemCursorConstants = + new HashMap() { + private static final long serialVersionUID = 1L; + + { + put("none", Integer.valueOf(PointerIcon.TYPE_NULL)); + // "basic": default + put("click", Integer.valueOf(PointerIcon.TYPE_HAND)); + put("text", Integer.valueOf(PointerIcon.TYPE_TEXT)); + // "forbidden": default + put("grab", Integer.valueOf(PointerIcon.TYPE_GRAB)); + put("grabbing", Integer.valueOf(PointerIcon.TYPE_GRABBING)); + } + }; + } + + final int cursorConstant = + MouseCursorPlugin.systemCursorConstants.getOrDefault(kind, PointerIcon.TYPE_ARROW); + return mView.getSystemPointerIcon(cursorConstant); + } + + /** + * Detaches the text input plugin from the platform views controller. + * + *

The MouseCursorPlugin instance should not be used after calling this. + */ + public void destroy() { + mouseCursorChannel.setMethodHandler(null); + } + + /** + * A map from Flutter's system cursor {@code kind} to Android's pointer icon constants. + * + *

It is null until the first time a system cursor is requested, at which time it is filled + * with the entire mapping. + */ + @NonNull private static HashMap systemCursorConstants; + + /** + * Delegate interface for requesting the system to display a pointer icon object. + * + *

Typically implemented by an {@link android.view.View}, such as a {@code FlutterView}. + */ + public interface MouseCursorViewDelegate { + /** + * Gets a system pointer icon object for the given {@code type}. + * + *

If typeis not recognized, returns the default pointer icon. + * + *

This is typically implemented by calling {@link android.view.PointerIcon.getSystemIcon} + * with the context associated with this view. + */ + public PointerIcon getSystemPointerIcon(int type); + + /** + * Request the pointer to display the specified icon object. + * + *

If the delegate is implemented by a {@link android.view.View}, then this method is + * automatically implemented by View. + */ + public void setPointerIcon(@NonNull PointerIcon icon); + } +} diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 739e58054..d5c79c9eb 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -24,6 +24,7 @@ import android.util.Log; import android.util.SparseArray; import android.view.KeyEvent; import android.view.MotionEvent; +import android.view.PointerIcon; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; @@ -49,6 +50,7 @@ import io.flutter.embedding.engine.systemchannels.AccessibilityChannel; import io.flutter.embedding.engine.systemchannels.KeyEventChannel; import io.flutter.embedding.engine.systemchannels.LifecycleChannel; import io.flutter.embedding.engine.systemchannels.LocalizationChannel; +import io.flutter.embedding.engine.systemchannels.MouseCursorChannel; import io.flutter.embedding.engine.systemchannels.NavigationChannel; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.embedding.engine.systemchannels.SettingsChannel; @@ -57,6 +59,7 @@ import io.flutter.embedding.engine.systemchannels.TextInputChannel; import io.flutter.plugin.common.ActivityLifecycleListener; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.editing.TextInputPlugin; +import io.flutter.plugin.mouse.MouseCursorPlugin; import io.flutter.plugin.platform.PlatformPlugin; import io.flutter.plugin.platform.PlatformViewsController; import java.nio.ByteBuffer; @@ -72,7 +75,8 @@ import java.util.concurrent.atomic.AtomicLong; *

Deprecation: {@link io.flutter.embedding.android.FlutterView} is the new API that now replaces * this class. See https://flutter.dev/go/android-project-migration for more migration details. */ -public class FlutterView extends SurfaceView implements BinaryMessenger, TextureRegistry { +public class FlutterView extends SurfaceView + implements BinaryMessenger, TextureRegistry, MouseCursorPlugin.MouseCursorViewDelegate { /** * Interface for those objects that maintain and expose a reference to a {@code FlutterView} (such * as a full-screen Flutter activity). @@ -120,6 +124,7 @@ public class FlutterView extends SurfaceView implements BinaryMessenger, Texture private final SystemChannel systemChannel; private final InputMethodManager mImm; private final TextInputPlugin mTextInputPlugin; + private final MouseCursorPlugin mMouseCursorPlugin; private final AndroidKeyProcessor androidKeyProcessor; private final AndroidTouchProcessor androidTouchProcessor; private AccessibilityBridge mAccessibilityNodeProvider; @@ -221,6 +226,11 @@ public class FlutterView extends SurfaceView implements BinaryMessenger, Texture mNativeView.getPluginRegistry().getPlatformViewsController(); mTextInputPlugin = new TextInputPlugin(this, new TextInputChannel(dartExecutor), platformViewsController); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mMouseCursorPlugin = new MouseCursorPlugin(this, new MouseCursorChannel(dartExecutor)); + } else { + mMouseCursorPlugin = null; + } androidKeyProcessor = new AndroidKeyProcessor(keyEventChannel, mTextInputPlugin); androidTouchProcessor = new AndroidTouchProcessor(flutterRenderer); mNativeView @@ -793,6 +803,14 @@ public class FlutterView extends SurfaceView implements BinaryMessenger, Texture } } + @Override + @TargetApi(Build.VERSION_CODES.N) + @RequiresApi(Build.VERSION_CODES.N) + @NonNull + public PointerIcon getSystemPointerIcon(int type) { + return PointerIcon.getSystemIcon(getContext(), type); + } + @Override @UiThread public void send(String channel, ByteBuffer message) { diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java index 820e428fc..7d0d84ac1 100644 --- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -20,6 +20,7 @@ import io.flutter.plugin.common.StandardMessageCodecTest; import io.flutter.plugin.common.StandardMethodCodecTest; import io.flutter.plugin.editing.InputConnectionAdaptorTest; import io.flutter.plugin.editing.TextInputPluginTest; +import io.flutter.plugin.mouse.MouseCursorPluginTest; import io.flutter.plugin.platform.PlatformPluginTest; import io.flutter.plugin.platform.SingleViewPresentationTest; import io.flutter.util.PreconditionsTest; @@ -58,6 +59,7 @@ import test.io.flutter.embedding.engine.dart.DartExecutorTest; SingleViewPresentationTest.class, SmokeTest.class, TextInputPluginTest.class, + MouseCursorPluginTest.class, AccessibilityBridgeTest.class, }) /** Runs all of the unit tests listed in the {@code @SuiteClasses} annotation. */ 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 b6c97d56f..e024f2530 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java @@ -27,6 +27,7 @@ import io.flutter.embedding.engine.renderer.FlutterRenderer; import io.flutter.embedding.engine.systemchannels.AccessibilityChannel; import io.flutter.embedding.engine.systemchannels.LifecycleChannel; import io.flutter.embedding.engine.systemchannels.LocalizationChannel; +import io.flutter.embedding.engine.systemchannels.MouseCursorChannel; import io.flutter.embedding.engine.systemchannels.NavigationChannel; import io.flutter.embedding.engine.systemchannels.SettingsChannel; import io.flutter.embedding.engine.systemchannels.SystemChannel; @@ -615,6 +616,7 @@ public class FlutterActivityAndFragmentDelegateTest { when(engine.getNavigationChannel()).thenReturn(mock(NavigationChannel.class)); when(engine.getSystemChannel()).thenReturn(mock(SystemChannel.class)); when(engine.getTextInputChannel()).thenReturn(mock(TextInputChannel.class)); + when(engine.getMouseCursorChannel()).thenReturn(mock(MouseCursorChannel.class)); when(engine.getActivityControlSurface()).thenReturn(mock(ActivityControlSurface.class)); return engine; diff --git a/shell/platform/android/test/io/flutter/plugin/mouse/MouseCursorPluginTest.java b/shell/platform/android/test/io/flutter/plugin/mouse/MouseCursorPluginTest.java new file mode 100644 index 000000000..60c275bd1 --- /dev/null +++ b/shell/platform/android/test/io/flutter/plugin/mouse/MouseCursorPluginTest.java @@ -0,0 +1,71 @@ +package io.flutter.plugin.mouse; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.annotation.TargetApi; +import android.view.PointerIcon; +import io.flutter.embedding.android.FlutterView; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.systemchannels.MouseCursorChannel; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import java.util.HashMap; +import org.json.JSONException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@Config( + manifest = Config.NONE, + shadows = {}) +@RunWith(RobolectricTestRunner.class) +@TargetApi(24) +public class MouseCursorPluginTest { + @Test + public void mouseCursorPlugin_SetsSystemCursorOnRequest() throws JSONException { + // Initialize a general MouseCursorPlugin. + FlutterView testView = spy(new FlutterView(RuntimeEnvironment.application)); + MouseCursorChannel mouseCursorChannel = new MouseCursorChannel(mock(DartExecutor.class)); + + MouseCursorPlugin mouseCursorPlugin = new MouseCursorPlugin(testView, mouseCursorChannel); + + final StoredResult methodResult = new StoredResult(); + mouseCursorChannel.synthesizeMethodCall( + new MethodCall( + "activateSystemCursor", + new HashMap() { + private static final long serialVersionUID = 1L; + + { + put("device", 1); + put("kind", "text"); + } + }), + methodResult); + verify(testView, times(1)).getSystemPointerIcon(PointerIcon.TYPE_TEXT); + verify(testView, times(1)).setPointerIcon(any(PointerIcon.class)); + assertEquals(methodResult.result, Boolean.TRUE); + } +} + +class StoredResult implements MethodChannel.Result { + Object result; + + @Override + public void success(Object result) { + this.result = result; + } + + @Override + public void error(String errorCode, String errorMessage, Object errorDetails) {} + + @Override + public void notImplemented() {} +} -- GitLab