diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index 2f9eb9fa699b6..cc483755af75f 100644
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -44294,6 +44294,7 @@ ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/syst
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.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
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SystemChannel.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/common/ActivityLifecycleListener.java + ../../../flutter/LICENSE
@@ -44317,6 +44318,7 @@ ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/Flutte
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ListenableEditingState.java + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/SpellCheckPlugin.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextEditingDelta.java + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java + ../../../flutter/LICENSE
@@ -47176,6 +47178,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/system
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.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
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SettingsChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SystemChannel.java
@@ -47202,6 +47205,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/FlutterT
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ListenableEditingState.java
+FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/SpellCheckPlugin.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextEditingDelta.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn
index 6f062431cf747..a0745b4107fe7 100644
--- a/shell/platform/android/BUILD.gn
+++ b/shell/platform/android/BUILD.gn
@@ -294,6 +294,7 @@ android_java_sources = [
"io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java",
"io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java",
"io/flutter/embedding/engine/systemchannels/RestorationChannel.java",
+ "io/flutter/embedding/engine/systemchannels/ScribeChannel.java",
"io/flutter/embedding/engine/systemchannels/SettingsChannel.java",
"io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java",
"io/flutter/embedding/engine/systemchannels/SystemChannel.java",
@@ -320,6 +321,7 @@ android_java_sources = [
"io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java",
"io/flutter/plugin/editing/InputConnectionAdaptor.java",
"io/flutter/plugin/editing/ListenableEditingState.java",
+ "io/flutter/plugin/editing/ScribePlugin.java",
"io/flutter/plugin/editing/SpellCheckPlugin.java",
"io/flutter/plugin/editing/TextEditingDelta.java",
"io/flutter/plugin/editing/TextInputPlugin.java",
diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java
index 0d5ae3a8efc5f..3f3f0470d8f54 100644
--- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java
+++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java
@@ -63,6 +63,7 @@
import io.flutter.embedding.engine.renderer.RenderSurface;
import io.flutter.embedding.engine.systemchannels.SettingsChannel;
import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.editing.ScribePlugin;
import io.flutter.plugin.editing.SpellCheckPlugin;
import io.flutter.plugin.editing.TextInputPlugin;
import io.flutter.plugin.localization.LocalizationPlugin;
@@ -133,6 +134,7 @@ public class FlutterView extends FrameLayout
@Nullable private MouseCursorPlugin mouseCursorPlugin;
@Nullable private TextInputPlugin textInputPlugin;
@Nullable private SpellCheckPlugin spellCheckPlugin;
+ @Nullable private ScribePlugin scribePlugin;
@Nullable private LocalizationPlugin localizationPlugin;
@Nullable private KeyboardManager keyboardManager;
@Nullable private AndroidTouchProcessor androidTouchProcessor;
@@ -1120,10 +1122,12 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) {
if (Build.VERSION.SDK_INT >= API_LEVELS.API_24) {
mouseCursorPlugin = new MouseCursorPlugin(this, this.flutterEngine.getMouseCursorChannel());
}
+
textInputPlugin =
new TextInputPlugin(
this,
this.flutterEngine.getTextInputChannel(),
+ this.flutterEngine.getScribeChannel(),
this.flutterEngine.getPlatformViewsController());
try {
@@ -1136,6 +1140,10 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) {
Log.e(TAG, "TextServicesManager not supported by device, spell check disabled.");
}
+ scribePlugin =
+ new ScribePlugin(
+ this, textInputPlugin.getInputMethodManager(), this.flutterEngine.getScribeChannel());
+
localizationPlugin = this.flutterEngine.getLocalizationPlugin();
keyboardManager = new KeyboardManager(this);
diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java
index 4c80b90a603f7..683dfeace7937 100644
--- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java
+++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java
@@ -34,6 +34,7 @@
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import io.flutter.embedding.engine.systemchannels.ProcessTextChannel;
import io.flutter.embedding.engine.systemchannels.RestorationChannel;
+import io.flutter.embedding.engine.systemchannels.ScribeChannel;
import io.flutter.embedding.engine.systemchannels.SettingsChannel;
import io.flutter.embedding.engine.systemchannels.SpellCheckChannel;
import io.flutter.embedding.engine.systemchannels.SystemChannel;
@@ -100,6 +101,7 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
@NonNull private final RestorationChannel restorationChannel;
@NonNull private final PlatformChannel platformChannel;
@NonNull private final ProcessTextChannel processTextChannel;
+ @NonNull private final ScribeChannel scribeChannel;
@NonNull private final SettingsChannel settingsChannel;
@NonNull private final SpellCheckChannel spellCheckChannel;
@NonNull private final SystemChannel systemChannel;
@@ -337,6 +339,7 @@ public FlutterEngine(
platformChannel = new PlatformChannel(dartExecutor);
processTextChannel = new ProcessTextChannel(dartExecutor, context.getPackageManager());
restorationChannel = new RestorationChannel(dartExecutor, waitForRestorationData);
+ scribeChannel = new ScribeChannel(dartExecutor);
settingsChannel = new SettingsChannel(dartExecutor);
spellCheckChannel = new SpellCheckChannel(dartExecutor);
systemChannel = new SystemChannel(dartExecutor);
@@ -610,6 +613,12 @@ public TextInputChannel getTextInputChannel() {
return textInputChannel;
}
+ /** System channel that sends and receives Scribe requests and results. */
+ @NonNull
+ public ScribeChannel getScribeChannel() {
+ return scribeChannel;
+ }
+
/** System channel that sends and receives spell check requests and results. */
@NonNull
public SpellCheckChannel getSpellCheckChannel() {
diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java
new file mode 100644
index 0000000000000..7070000ab17c5
--- /dev/null
+++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java
@@ -0,0 +1,147 @@
+// 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 static io.flutter.Build.API_LEVELS;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
+import io.flutter.Log;
+import io.flutter.embedding.engine.dart.DartExecutor;
+import io.flutter.plugin.common.JSONMethodCodec;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+
+/**
+ * {@link ScribeChannel} is a platform channel that is used by the framework to facilitate the
+ * Scribe handwriting text input feature.
+ */
+public class ScribeChannel {
+ private static final String TAG = "ScribeChannel";
+
+ @VisibleForTesting
+ public static final String METHOD_IS_FEATURE_AVAILABLE = "Scribe.isFeatureAvailable";
+
+ @VisibleForTesting
+ public static final String METHOD_IS_STYLUS_HANDWRITING_AVAILABLE =
+ "Scribe.isStylusHandwritingAvailable";
+
+ @VisibleForTesting
+ public static final String METHOD_START_STYLUS_HANDWRITING = "Scribe.startStylusHandwriting";
+
+ public final MethodChannel channel;
+ private ScribeMethodHandler scribeMethodHandler;
+
+ @NonNull
+ public final MethodChannel.MethodCallHandler parsingMethodHandler =
+ new MethodChannel.MethodCallHandler() {
+ @Override
+ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
+ if (scribeMethodHandler == null) {
+ Log.v(TAG, "No ScribeMethodHandler registered. Scribe call not handled.");
+ return;
+ }
+ String method = call.method;
+ Log.v(TAG, "Received '" + method + "' message.");
+ switch (method) {
+ case METHOD_IS_FEATURE_AVAILABLE:
+ isFeatureAvailable(call, result);
+ break;
+ case METHOD_IS_STYLUS_HANDWRITING_AVAILABLE:
+ isStylusHandwritingAvailable(call, result);
+ break;
+ case METHOD_START_STYLUS_HANDWRITING:
+ startStylusHandwriting(call, result);
+ break;
+ default:
+ result.notImplemented();
+ break;
+ }
+ }
+ };
+
+ private void isFeatureAvailable(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
+ try {
+ final boolean isAvailable = scribeMethodHandler.isFeatureAvailable();
+ result.success(isAvailable);
+ } catch (IllegalStateException exception) {
+ result.error("error", exception.getMessage(), null);
+ }
+ }
+
+ private void isStylusHandwritingAvailable(
+ @NonNull MethodCall call, @NonNull MethodChannel.Result result) {
+ if (Build.VERSION.SDK_INT < API_LEVELS.API_34) {
+ result.error("error", "Requires API level 34 or higher.", null);
+ return;
+ }
+
+ try {
+ final boolean isAvailable = scribeMethodHandler.isStylusHandwritingAvailable();
+ result.success(isAvailable);
+ } catch (IllegalStateException exception) {
+ result.error("error", exception.getMessage(), null);
+ }
+ }
+
+ private void startStylusHandwriting(
+ @NonNull MethodCall call, @NonNull MethodChannel.Result result) {
+ if (Build.VERSION.SDK_INT < API_LEVELS.API_33) {
+ result.error("error", "Requires API level 33 or higher.", null);
+ return;
+ }
+
+ try {
+ scribeMethodHandler.startStylusHandwriting();
+ result.success(null);
+ } catch (IllegalStateException exception) {
+ result.error("error", exception.getMessage(), null);
+ }
+ }
+
+ public ScribeChannel(@NonNull DartExecutor dartExecutor) {
+ channel = new MethodChannel(dartExecutor, "flutter/scribe", JSONMethodCodec.INSTANCE);
+ channel.setMethodCallHandler(parsingMethodHandler);
+ }
+
+ /**
+ * Sets the {@link ScribeMethodHandler} which receives all requests for scribe sent through this
+ * channel.
+ */
+ public void setScribeMethodHandler(@Nullable ScribeMethodHandler scribeMethodHandler) {
+ this.scribeMethodHandler = scribeMethodHandler;
+ }
+
+ public interface ScribeMethodHandler {
+ /**
+ * Responds to the {@code result} with success and a boolean indicating whether or not stylus
+ * handwriting is available.
+ */
+ boolean isFeatureAvailable();
+
+ /**
+ * Responds to the {@code result} with success and a boolean indicating whether or not stylus
+ * handwriting is available.
+ */
+ @TargetApi(API_LEVELS.API_34)
+ @RequiresApi(API_LEVELS.API_34)
+ boolean isStylusHandwritingAvailable();
+
+ /**
+ * Requests to start Scribe stylus handwriting, which will respond to the {@code result} with
+ * either success if handwriting input has started or error otherwise.
+ */
+ @TargetApi(API_LEVELS.API_33)
+ @RequiresApi(API_LEVELS.API_33)
+ void startStylusHandwriting();
+ }
+
+ // TODO(justinmc): Scribe stylus gestures should be supported here.
+ // https://github.com/flutter/flutter/issues/156018
+}
diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
index 1f688d7beb281..aa96ea02645c9 100644
--- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
+++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
@@ -33,6 +33,7 @@
import androidx.core.view.inputmethod.InputConnectionCompat;
import io.flutter.Log;
import io.flutter.embedding.engine.FlutterJNI;
+import io.flutter.embedding.engine.systemchannels.ScribeChannel;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
@@ -51,6 +52,7 @@ public interface KeyboardDelegate {
private final View mFlutterView;
private final int mClient;
+ private final ScribeChannel scribeChannel;
private final TextInputChannel textInputChannel;
private final ListenableEditingState mEditable;
private final EditorInfo mEditorInfo;
@@ -69,6 +71,7 @@ public InputConnectionAdaptor(
View view,
int client,
TextInputChannel textInputChannel,
+ ScribeChannel scribeChannel,
KeyboardDelegate keyboardDelegate,
ListenableEditingState editable,
EditorInfo editorInfo,
@@ -77,6 +80,7 @@ public InputConnectionAdaptor(
mFlutterView = view;
mClient = client;
this.textInputChannel = textInputChannel;
+ this.scribeChannel = scribeChannel;
mEditable = editable;
mEditable.addEditingStateListener(this);
mEditorInfo = editorInfo;
@@ -100,10 +104,19 @@ public InputConnectionAdaptor(
View view,
int client,
TextInputChannel textInputChannel,
+ ScribeChannel scribeChannel,
KeyboardDelegate keyboardDelegate,
ListenableEditingState editable,
EditorInfo editorInfo) {
- this(view, client, textInputChannel, keyboardDelegate, editable, editorInfo, new FlutterJNI());
+ this(
+ view,
+ client,
+ textInputChannel,
+ scribeChannel,
+ keyboardDelegate,
+ editable,
+ editorInfo,
+ new FlutterJNI());
}
private ExtractedText getExtractedText(ExtractedTextRequest request) {
@@ -262,6 +275,10 @@ public boolean setSelection(int start, int end) {
return result;
}
+ // TODO(justinmc): Scribe stylus gestures should be supported here via
+ // performHandwritingGesture.
+ // https://github.com/flutter/flutter/issues/156018
+
// Sanitizes the index to ensure the index is within the range of the
// contents of editable.
private static int clampIndexToEditable(int index, Editable editable) {
diff --git a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java
new file mode 100644
index 0000000000000..35fb2ff0d0dfd
--- /dev/null
+++ b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java
@@ -0,0 +1,106 @@
+// 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.editing;
+
+import static io.flutter.Build.API_LEVELS;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import io.flutter.embedding.engine.systemchannels.ScribeChannel;
+
+/**
+ * {@link ScribePlugin} is the implementation of all functionality needed for handwriting stylus
+ * text input.
+ *
+ *
The plugin handles requests for scribe sent by the {@link
+ * io.flutter.embedding.engine.systemchannels.ScribeChannel}.
+ *
+ *
On API versions below 33, the plugin does nothing.
+ */
+public class ScribePlugin implements ScribeChannel.ScribeMethodHandler {
+
+ @NonNull private final ScribeChannel mScribeChannel;
+ @NonNull private final InputMethodManager mInputMethodManager;
+ @NonNull private View mView;
+
+ public ScribePlugin(
+ @NonNull View view, @NonNull InputMethodManager imm, @NonNull ScribeChannel scribeChannel) {
+ if (Build.VERSION.SDK_INT >= API_LEVELS.API_33) {
+ view.setAutoHandwritingEnabled(false);
+ }
+
+ mView = view;
+ mInputMethodManager = imm;
+ mScribeChannel = scribeChannel;
+
+ mScribeChannel.setScribeMethodHandler(this);
+ }
+
+ /**
+ * Sets the View in which Scribe input is handled.
+ *
+ *
Only one View can be set at any given time.
+ */
+ public void setView(@NonNull View view) {
+ if (view == mView) {
+ return;
+ }
+ mView = view;
+ }
+
+ /**
+ * Unregisters this {@code ScribePlugin} as the {@code ScribeChannel.ScribeMethodHandler}, for the
+ * {@link io.flutter.embedding.engine.systemchannels.ScribeChannel}.
+ *
+ *
Do not invoke any methods on a {@code ScribePlugin} after invoking this method.
+ */
+ public void destroy() {
+ mScribeChannel.setScribeMethodHandler(null);
+ }
+
+ /**
+ * Returns true if the InputMethodManager supports Scribe stylus handwriting input.
+ *
+ *
Call this or isFeatureAvailable before calling startStylusHandwriting to make sure it's
+ * available.
+ */
+ @TargetApi(API_LEVELS.API_34)
+ @RequiresApi(API_LEVELS.API_34)
+ @Override
+ public boolean isStylusHandwritingAvailable() {
+ return mInputMethodManager.isStylusHandwritingAvailable();
+ }
+
+ /**
+ * Starts stylus handwriting input.
+ *
+ *
Typically isStylusHandwritingAvailable should be called first to determine whether this is
+ * supported by the IME.
+ */
+ @TargetApi(API_LEVELS.API_33)
+ @RequiresApi(API_LEVELS.API_33)
+ @Override
+ public void startStylusHandwriting() {
+ mInputMethodManager.startStylusHandwriting(mView);
+ }
+
+ /**
+ * A convenience method to check if Scribe is available.
+ *
+ *
Differs from isStylusHandwritingAvailable in that it can be called from any API level
+ * without throwing an error.
+ *
+ *
Call this or isStylusHandwritingAvailable before calling startStylusHandwriting to make sure
+ * it's available.
+ */
+ @Override
+ public boolean isFeatureAvailable() {
+ return Build.VERSION.SDK_INT >= API_LEVELS.API_34 && isStylusHandwritingAvailable();
+ }
+}
diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
index beae70837e3c3..41cd19ed32fc4 100644
--- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
+++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
@@ -29,6 +29,7 @@
import androidx.core.view.inputmethod.EditorInfoCompat;
import io.flutter.Log;
import io.flutter.embedding.android.KeyboardManager;
+import io.flutter.embedding.engine.systemchannels.ScribeChannel;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.embedding.engine.systemchannels.TextInputChannel.TextEditState;
import io.flutter.plugin.platform.PlatformViewsController;
@@ -42,6 +43,7 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
@NonNull private final View mView;
@NonNull private final InputMethodManager mImm;
@NonNull private final AutofillManager afm;
+ @NonNull private final ScribeChannel scribeChannel;
@NonNull private final TextInputChannel textInputChannel;
@NonNull private InputTarget inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
@Nullable private TextInputChannel.Configuration configuration;
@@ -66,6 +68,7 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
public TextInputPlugin(
@NonNull View view,
@NonNull TextInputChannel textInputChannel,
+ @NonNull ScribeChannel scribeChannel,
@NonNull PlatformViewsController platformViewsController) {
mView = view;
// Create a default object.
@@ -153,6 +156,8 @@ public void sendAppPrivateCommand(String action, Bundle data) {
textInputChannel.requestExistingInputState();
+ this.scribeChannel = scribeChannel;
+
this.platformViewsController = platformViewsController;
this.platformViewsController.attachTextInputPlugin(this);
}
@@ -341,9 +346,23 @@ public InputConnection createInputConnection(
EditorInfoCompat.setContentMimeTypes(outAttrs, imgTypeString);
}
+ if (Build.VERSION.SDK_INT >= API_LEVELS.API_34) {
+ EditorInfoCompat.setStylusHandwritingEnabled(outAttrs, true);
+ }
+ // TODO(justinmc): Scribe stylus gestures should be supported here via
+ // outAttrs.setSupportedHandwritingGestures and
+ // outAttrs.setSupportedHandwritingGesturePreviews.
+ // https://github.com/flutter/flutter/issues/156018
+
InputConnectionAdaptor connection =
new InputConnectionAdaptor(
- view, inputTarget.id, textInputChannel, keyboardManager, mEditable, outAttrs);
+ view,
+ inputTarget.id,
+ textInputChannel,
+ scribeChannel,
+ keyboardManager,
+ mEditable,
+ outAttrs);
outAttrs.initialSelStart = mEditable.getSelectionStart();
outAttrs.initialSelEnd = mEditable.getSelectionEnd();
diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java
index 8dbdae146ef83..d20ea56f1f9db 100644
--- a/shell/platform/android/io/flutter/view/FlutterView.java
+++ b/shell/platform/android/io/flutter/view/FlutterView.java
@@ -56,6 +56,7 @@
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.ScribeChannel;
import io.flutter.embedding.engine.systemchannels.SettingsChannel;
import io.flutter.embedding.engine.systemchannels.SystemChannel;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
@@ -237,7 +238,11 @@ public void onPostResume() {
PlatformViewsController platformViewsController =
mNativeView.getPluginRegistry().getPlatformViewsController();
mTextInputPlugin =
- new TextInputPlugin(this, new TextInputChannel(dartExecutor), platformViewsController);
+ new TextInputPlugin(
+ this,
+ new TextInputChannel(dartExecutor),
+ new ScribeChannel(dartExecutor),
+ platformViewsController);
mKeyboardManager = new KeyboardManager(this);
if (Build.VERSION.SDK_INT >= API_LEVELS.API_24) {
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 132aea4ae52dd..ae1e371a48d6a 100644
--- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java
@@ -49,6 +49,7 @@
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.ScribeChannel;
import io.flutter.embedding.engine.systemchannels.SettingsChannel;
import io.flutter.embedding.engine.systemchannels.SystemChannel;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
@@ -1481,6 +1482,7 @@ private FlutterEngine mockFlutterEngine() {
when(engine.getSettingsChannel()).thenReturn(fakeSettingsChannel);
when(engine.getSystemChannel()).thenReturn(mock(SystemChannel.class));
when(engine.getTextInputChannel()).thenReturn(mock(TextInputChannel.class));
+ when(engine.getScribeChannel()).thenReturn(mock(ScribeChannel.class));
return engine;
}
diff --git a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java
new file mode 100644
index 0000000000000..c69f624300fcd
--- /dev/null
+++ b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java
@@ -0,0 +1,202 @@
+// 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 static io.flutter.Build.API_LEVELS;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.annotation.TargetApi;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import io.flutter.embedding.engine.dart.DartExecutor;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.FlutterException;
+import io.flutter.plugin.common.JSONMethodCodec;
+import io.flutter.plugin.common.MethodCall;
+import java.nio.ByteBuffer;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+public class ScribeChannelTest {
+ private static BinaryMessenger.BinaryReply sendToBinaryMessageHandler(
+ BinaryMessenger.BinaryMessageHandler binaryMessageHandler, String method) {
+ MethodCall methodCall = new MethodCall(method, null);
+ ByteBuffer encodedMethodCall = JSONMethodCodec.INSTANCE.encodeMethodCall(methodCall);
+ BinaryMessenger.BinaryReply mockReply = mock(BinaryMessenger.BinaryReply.class);
+ binaryMessageHandler.onMessage((ByteBuffer) encodedMethodCall.flip(), mockReply);
+ return mockReply;
+ }
+
+ ScribeChannel.ScribeMethodHandler mockHandler;
+ BinaryMessenger.BinaryMessageHandler binaryMessageHandler;
+
+ @Before
+ public void setUp() {
+ ArgumentCaptor binaryMessageHandlerCaptor =
+ ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class);
+ DartExecutor mockBinaryMessenger = mock(DartExecutor.class);
+ mockHandler = mock(ScribeChannel.ScribeMethodHandler.class);
+ ScribeChannel scribeChannel = new ScribeChannel(mockBinaryMessenger);
+
+ scribeChannel.setScribeMethodHandler(mockHandler);
+
+ verify((BinaryMessenger) mockBinaryMessenger, times(1))
+ .setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture());
+
+ binaryMessageHandler = binaryMessageHandlerCaptor.getValue();
+ }
+
+ @Config(minSdk = API_LEVELS.API_34)
+ @TargetApi(API_LEVELS.API_34)
+ @Test
+ public void respondsToStartStylusHandwriting() {
+ BinaryMessenger.BinaryReply mockReply =
+ sendToBinaryMessageHandler(
+ binaryMessageHandler, ScribeChannel.METHOD_START_STYLUS_HANDWRITING);
+
+ verify(mockReply)
+ .reply(
+ argThat(
+ (ByteBuffer reply) -> {
+ reply.flip();
+ try {
+ final Object decodedReply = JSONMethodCodec.INSTANCE.decodeEnvelope(reply);
+ return decodedReply == null;
+ } catch (FlutterException e) {
+ return false;
+ }
+ }));
+ verify(mockHandler).startStylusHandwriting();
+ }
+
+ @Config(minSdk = API_LEVELS.API_34)
+ @TargetApi(API_LEVELS.API_34)
+ @Test
+ public void respondsToIsFeatureAvailable() {
+ BinaryMessenger.BinaryReply mockReply =
+ sendToBinaryMessageHandler(binaryMessageHandler, ScribeChannel.METHOD_IS_FEATURE_AVAILABLE);
+
+ verify(mockReply)
+ .reply(
+ argThat(
+ (ByteBuffer reply) -> {
+ reply.flip();
+ try {
+ final Object decodedReply = JSONMethodCodec.INSTANCE.decodeEnvelope(reply);
+ // Should succeed and should tell whether or not Scribe is available by
+ // using a boolean.
+ return decodedReply.getClass() == java.lang.Boolean.class;
+ } catch (FlutterException e) {
+ return false;
+ }
+ }));
+ verify(mockHandler).isFeatureAvailable();
+ }
+
+ @Config(minSdk = API_LEVELS.API_34)
+ @TargetApi(API_LEVELS.API_34)
+ @Test
+ public void respondsToIsStylusHandwritingAvailable() {
+ BinaryMessenger.BinaryReply mockReply =
+ sendToBinaryMessageHandler(
+ binaryMessageHandler, ScribeChannel.METHOD_IS_STYLUS_HANDWRITING_AVAILABLE);
+
+ verify(mockReply)
+ .reply(
+ argThat(
+ (ByteBuffer reply) -> {
+ reply.flip();
+ try {
+ final Object decodedReply = JSONMethodCodec.INSTANCE.decodeEnvelope(reply);
+ // Should succeed and should tell whether or not Scribe is available by
+ // using a boolean.
+ return decodedReply.getClass() == java.lang.Boolean.class;
+ } catch (FlutterException e) {
+ return false;
+ }
+ }));
+ verify(mockHandler).isStylusHandwritingAvailable();
+ }
+
+ @Config(sdk = API_LEVELS.API_32)
+ @TargetApi(API_LEVELS.API_32)
+ @Test
+ public void respondsToStartStylusHandwritingWhenAPILevelUnsupported() {
+ BinaryMessenger.BinaryReply mockReply =
+ sendToBinaryMessageHandler(
+ binaryMessageHandler, ScribeChannel.METHOD_START_STYLUS_HANDWRITING);
+
+ verify(mockReply)
+ .reply(
+ argThat(
+ (ByteBuffer reply) -> {
+ reply.flip();
+ try {
+ final Object decodedReply = JSONMethodCodec.INSTANCE.decodeEnvelope(reply);
+ return false;
+ } catch (FlutterException e) {
+ // Should fail because the API version is too low.
+ return true;
+ }
+ }));
+ verify(mockHandler, never()).startStylusHandwriting();
+ }
+
+ @Config(sdk = API_LEVELS.API_33)
+ @TargetApi(API_LEVELS.API_33)
+ @Test
+ public void respondsToIsFeatureAvailableWhenAPILevelUnsupported() {
+ BinaryMessenger.BinaryReply mockReply =
+ sendToBinaryMessageHandler(binaryMessageHandler, ScribeChannel.METHOD_IS_FEATURE_AVAILABLE);
+
+ verify(mockReply)
+ .reply(
+ argThat(
+ (ByteBuffer reply) -> {
+ reply.flip();
+ try {
+ final Object decodedReply = JSONMethodCodec.INSTANCE.decodeEnvelope(reply);
+ // Should succeed and indicate that Scribe is not available.
+ return decodedReply.getClass() == java.lang.Boolean.class
+ && !((boolean) decodedReply);
+ } catch (FlutterException e) {
+ return false;
+ }
+ }));
+ verify(mockHandler).isFeatureAvailable();
+ }
+
+ @Config(sdk = API_LEVELS.API_33)
+ @TargetApi(API_LEVELS.API_33)
+ @Test
+ public void respondsToIsStylusHandwritingAvailableWhenAPILevelUnsupported() {
+ BinaryMessenger.BinaryReply mockReply =
+ sendToBinaryMessageHandler(
+ binaryMessageHandler, ScribeChannel.METHOD_IS_STYLUS_HANDWRITING_AVAILABLE);
+
+ verify(mockReply)
+ .reply(
+ argThat(
+ (ByteBuffer reply) -> {
+ reply.flip();
+ try {
+ final Object decodedReply = JSONMethodCodec.INSTANCE.decodeEnvelope(reply);
+ return false;
+ } catch (FlutterException e) {
+ // Should fail because the API version is too low.
+ return true;
+ }
+ }));
+ verify(mockHandler, never()).isStylusHandwritingAvailable();
+ }
+}
diff --git a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java
index 1158f1d33b0cf..8ca3469498c24 100644
--- a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java
+++ b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java
@@ -43,6 +43,7 @@
import io.flutter.embedding.android.KeyboardManager;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.dart.DartExecutor;
+import io.flutter.embedding.engine.systemchannels.ScribeChannel;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.plugin.common.JSONMethodCodec;
import io.flutter.plugin.common.MethodCall;
@@ -105,6 +106,7 @@ public void inputConnectionAdaptor_ReceivesEnter() throws NullPointerException {
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class)));
int inputTargetId = 0;
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+ ScribeChannel scribeChannel = new ScribeChannel(dartExecutor);
ListenableEditingState mEditable = new ListenableEditingState(null, testView);
Selection.setSelection(mEditable, 0, 0);
ListenableEditingState spyEditable = spy(mEditable);
@@ -113,7 +115,13 @@ public void inputConnectionAdaptor_ReceivesEnter() throws NullPointerException {
InputConnectionAdaptor inputConnectionAdaptor =
new InputConnectionAdaptor(
- testView, inputTargetId, textInputChannel, mockKeyboardManager, spyEditable, outAttrs);
+ testView,
+ inputTargetId,
+ textInputChannel,
+ scribeChannel,
+ mockKeyboardManager,
+ spyEditable,
+ outAttrs);
// Send an enter key and make sure the Editable received it.
FakeKeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, '\n');
@@ -199,12 +207,14 @@ public void testCommitContent() throws JSONException {
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+ ScribeChannel scribeChannel = new ScribeChannel(dartExecutor);
ListenableEditingState editable = sampleEditable(0, 0);
InputConnectionAdaptor adaptor =
new InputConnectionAdaptor(
testView,
client,
textInputChannel,
+ scribeChannel,
mockKeyboardManager,
editable,
null,
@@ -260,12 +270,14 @@ public void testPerformPrivateCommand_dataIsNull() throws JSONException {
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+ ScribeChannel scribeChannel = new ScribeChannel(dartExecutor);
ListenableEditingState editable = sampleEditable(0, 0);
InputConnectionAdaptor adaptor =
new InputConnectionAdaptor(
testView,
client,
textInputChannel,
+ scribeChannel,
mockKeyboardManager,
editable,
null,
@@ -291,12 +303,14 @@ public void testPerformPrivateCommand_dataIsByteArray() throws JSONException {
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+ ScribeChannel scribeChannel = new ScribeChannel(dartExecutor);
ListenableEditingState editable = sampleEditable(0, 0);
InputConnectionAdaptor adaptor =
new InputConnectionAdaptor(
testView,
client,
textInputChannel,
+ scribeChannel,
mockKeyboardManager,
editable,
null,
@@ -328,12 +342,14 @@ public void testPerformPrivateCommand_dataIsByte() throws JSONException {
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+ ScribeChannel scribeChannel = new ScribeChannel(dartExecutor);
ListenableEditingState editable = sampleEditable(0, 0);
InputConnectionAdaptor adaptor =
new InputConnectionAdaptor(
testView,
client,
textInputChannel,
+ scribeChannel,
mockKeyboardManager,
editable,
null,
@@ -363,12 +379,14 @@ public void testPerformPrivateCommand_dataIsCharArray() throws JSONException {
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+ ScribeChannel scribeChannel = new ScribeChannel(dartExecutor);
ListenableEditingState editable = sampleEditable(0, 0);
InputConnectionAdaptor adaptor =
new InputConnectionAdaptor(
testView,
client,
textInputChannel,
+ scribeChannel,
mockKeyboardManager,
editable,
null,
@@ -401,12 +419,14 @@ public void testPerformPrivateCommand_dataIsChar() throws JSONException {
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+ ScribeChannel scribeChannel = new ScribeChannel(dartExecutor);
ListenableEditingState editable = sampleEditable(0, 0);
InputConnectionAdaptor adaptor =
new InputConnectionAdaptor(
testView,
client,
textInputChannel,
+ scribeChannel,
mockKeyboardManager,
editable,
null,
@@ -436,12 +456,14 @@ public void testPerformPrivateCommand_dataIsCharSequenceArray() throws JSONExcep
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+ ScribeChannel scribeChannel = new ScribeChannel(dartExecutor);
ListenableEditingState editable = sampleEditable(0, 0);
InputConnectionAdaptor adaptor =
new InputConnectionAdaptor(
testView,
client,
textInputChannel,
+ scribeChannel,
mockKeyboardManager,
editable,
null,
@@ -475,12 +497,14 @@ public void testPerformPrivateCommand_dataIsCharSequence() throws JSONException
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+ ScribeChannel scribeChannel = new ScribeChannel(dartExecutor);
ListenableEditingState editable = sampleEditable(0, 0);
InputConnectionAdaptor adaptor =
new InputConnectionAdaptor(
testView,
client,
textInputChannel,
+ scribeChannel,
mockKeyboardManager,
editable,
null,
@@ -512,12 +536,14 @@ public void testPerformPrivateCommand_dataIsFloat() throws JSONException {
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+ ScribeChannel scribeChannel = new ScribeChannel(dartExecutor);
ListenableEditingState editable = sampleEditable(0, 0);
InputConnectionAdaptor adaptor =
new InputConnectionAdaptor(
testView,
client,
textInputChannel,
+ scribeChannel,
mockKeyboardManager,
editable,
null,
@@ -547,12 +573,14 @@ public void testPerformPrivateCommand_dataIsFloatArray() throws JSONException {
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+ ScribeChannel scribeChannel = new ScribeChannel(dartExecutor);
ListenableEditingState editable = sampleEditable(0, 0);
InputConnectionAdaptor adaptor =
new InputConnectionAdaptor(
testView,
client,
textInputChannel,
+ scribeChannel,
mockKeyboardManager,
editable,
null,
@@ -1080,6 +1108,7 @@ public void testExtractedText_monitoring() {
testView,
1,
mock(TextInputChannel.class),
+ mock(ScribeChannel.class),
mockKeyboardManager,
editable,
new EditorInfo());
@@ -1131,6 +1160,7 @@ public void testCursorAnchorInfo() {
testView,
1,
mock(TextInputChannel.class),
+ mock(ScribeChannel.class),
mockKeyboardManager,
editable,
new EditorInfo());
@@ -1277,6 +1307,7 @@ private static InputConnectionAdaptor sampleInputConnectionAdaptor(
View testView = new View(ApplicationProvider.getApplicationContext());
int client = 0;
TextInputChannel textInputChannel = mock(TextInputChannel.class);
+ ScribeChannel scribeChannel = mock(ScribeChannel.class);
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
when(mockFlutterJNI.isCodePointEmoji(anyInt()))
.thenAnswer((invocation) -> Emoji.isEmoji((int) invocation.getArguments()[0]));
@@ -1290,7 +1321,14 @@ private static InputConnectionAdaptor sampleInputConnectionAdaptor(
.thenAnswer(
(invocation) -> Emoji.isRegionalIndicatorSymbol((int) invocation.getArguments()[0]));
return new InputConnectionAdaptor(
- testView, client, textInputChannel, mockKeyboardManager, editable, null, mockFlutterJNI);
+ testView,
+ client,
+ textInputChannel,
+ scribeChannel,
+ mockKeyboardManager,
+ editable,
+ null,
+ mockFlutterJNI);
}
private static class Emoji {
diff --git a/shell/platform/android/test/io/flutter/plugin/editing/ListenableEditingStateTest.java b/shell/platform/android/test/io/flutter/plugin/editing/ListenableEditingStateTest.java
index 800116659901b..e867dd587b900 100644
--- a/shell/platform/android/test/io/flutter/plugin/editing/ListenableEditingStateTest.java
+++ b/shell/platform/android/test/io/flutter/plugin/editing/ListenableEditingStateTest.java
@@ -14,6 +14,7 @@
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import io.flutter.embedding.android.KeyboardManager;
+import io.flutter.embedding.engine.systemchannels.ScribeChannel;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import java.util.ArrayList;
import org.junit.Before;
@@ -280,6 +281,7 @@ public void endBatchEdit() {
testView,
0,
mock(TextInputChannel.class),
+ mock(ScribeChannel.class),
mockKeyboardManager,
editingState,
new EditorInfo());
@@ -305,6 +307,7 @@ public void inputMethod_testSetSelection() {
testView,
0,
mock(TextInputChannel.class),
+ mock(ScribeChannel.class),
mockKeyboardManager,
editingState,
new EditorInfo());
@@ -339,6 +342,7 @@ public void inputMethod_testSetComposition() {
testView,
0,
mock(TextInputChannel.class),
+ mock(ScribeChannel.class),
mockKeyboardManager,
editingState,
new EditorInfo());
@@ -399,6 +403,7 @@ public void inputMethod_testCommitText() {
testView,
0,
mock(TextInputChannel.class),
+ mock(ScribeChannel.class),
mockKeyboardManager,
editingState,
new EditorInfo());
diff --git a/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java
new file mode 100644
index 0000000000000..386c75a35c79f
--- /dev/null
+++ b/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java
@@ -0,0 +1,106 @@
+// 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.editing;
+
+import static io.flutter.Build.API_LEVELS;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import io.flutter.embedding.engine.systemchannels.ScribeChannel;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+public class ScribePluginTest {
+ private final Context ctx = ApplicationProvider.getApplicationContext();
+
+ ScribePlugin scribePlugin;
+ InputMethodManager mockImm;
+ View testView;
+
+ @Before
+ public void setUp() {
+ ScribeChannel mockScribeChannel = mock(ScribeChannel.class);
+ testView = new View(ctx);
+ mockImm = mock(InputMethodManager.class);
+ if (Build.VERSION.SDK_INT >= API_LEVELS.API_34) {
+ when(mockImm.isStylusHandwritingAvailable()).thenReturn(true);
+ }
+ scribePlugin = new ScribePlugin(testView, mockImm, mockScribeChannel);
+ }
+
+ @Config(minSdk = API_LEVELS.API_34)
+ @TargetApi(API_LEVELS.API_34)
+ @Test
+ public void scribePluginIsFeatureAvailable() {
+ assertEquals(scribePlugin.isFeatureAvailable(), true);
+
+ verify(mockImm).isStylusHandwritingAvailable();
+ }
+
+ @Config(minSdk = API_LEVELS.API_34)
+ @TargetApi(API_LEVELS.API_34)
+ @Test
+ public void scribePluginIsStylusHandwritingAvailable() {
+ assertEquals(scribePlugin.isStylusHandwritingAvailable(), true);
+
+ verify(mockImm).isStylusHandwritingAvailable();
+ }
+
+ @Config(minSdk = API_LEVELS.API_34)
+ @TargetApi(API_LEVELS.API_34)
+ @Test
+ public void scribePluginStartStylusHandwriting() {
+ scribePlugin.startStylusHandwriting();
+
+ verify(mockImm).startStylusHandwriting(testView);
+ }
+
+ @Config(sdk = API_LEVELS.API_32)
+ @TargetApi(API_LEVELS.API_32)
+ @Test
+ public void scribePluginStartStylusHandwritingWhenAPILevelUnsupported() {
+ assertNotNull(scribePlugin);
+
+ assertThrows(
+ NoSuchMethodError.class,
+ () -> {
+ scribePlugin.startStylusHandwriting();
+ });
+ }
+
+ @Config(sdk = API_LEVELS.API_33)
+ @TargetApi(API_LEVELS.API_33)
+ @Test
+ public void scribePluginIsFeatureAvailableWhenAPILevelUnsupported() {
+ assertEquals(scribePlugin.isFeatureAvailable(), false);
+ }
+
+ @Config(sdk = API_LEVELS.API_33)
+ @TargetApi(API_LEVELS.API_33)
+ @Test
+ public void scribePluginIsStylusHandwritingAvailableWhenAPILevelUnsupported() {
+ assertNotNull(scribePlugin);
+
+ assertThrows(
+ NoSuchMethodError.class,
+ () -> {
+ scribePlugin.isStylusHandwritingAvailable();
+ });
+ }
+}
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 2656902b52543..77f71f0aaf923 100644
--- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java
+++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java
@@ -2,6 +2,7 @@
import static io.flutter.Build.API_LEVELS;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
@@ -45,6 +46,7 @@
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
+import androidx.core.view.inputmethod.EditorInfoCompat;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import io.flutter.embedding.android.FlutterView;
@@ -54,6 +56,7 @@
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.loader.FlutterLoader;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
+import io.flutter.embedding.engine.systemchannels.ScribeChannel;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.embedding.engine.systemchannels.TextInputChannel.TextEditState;
import io.flutter.plugin.common.BinaryMessenger;
@@ -133,8 +136,10 @@ public void textInputPlugin_RequestsReattachOnCreation() throws JSONException {
FlutterJNI mockFlutterJni = mock(FlutterJNI.class);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
@@ -152,8 +157,10 @@ public void setTextInputEditingState_doesNotInvokeUpdateEditingState() {
testImm.setCurrentInputMethodSubtype(inputMethodSubtype);
View testView = new View(ctx);
TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@@ -194,8 +201,10 @@ public void setTextInputEditingState_willNotThrowWithoutSetTextInputClient() {
testImm.setCurrentInputMethodSubtype(inputMethodSubtype);
View testView = new View(ctx);
TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
// Here's no textInputPlugin.setTextInputClient()
textInputPlugin.setTextInputEditingState(
@@ -211,8 +220,10 @@ public void setTextInputEditingState_doesNotInvokeUpdateEditingStateWithDeltas()
testImm.setCurrentInputMethodSubtype(inputMethodSubtype);
View testView = new View(ctx);
TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@@ -261,8 +272,10 @@ public void textEditingDelta_TestUpdateEditingValueWithDeltasIsNotInvokedWhenDel
EditorInfo outAttrs = new EditorInfo();
outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE;
TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
CharSequence newText = "I do not fear computers. I fear the lack of them.";
// Change InputTarget to FRAMEWORK_CLIENT.
@@ -374,8 +387,10 @@ public void textEditingDelta_TestUpdateEditingValueIsNotInvokedWhenDeltaModelEna
EditorInfo outAttrs = new EditorInfo();
outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE;
TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.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);
@@ -503,8 +518,10 @@ public void textEditingDelta_TestDeltaIsCreatedWhenComposingTextSetIsInserting()
EditorInfo outAttrs = new EditorInfo();
outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE;
TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.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);
@@ -612,8 +629,10 @@ public void textEditingDelta_TestDeltaIsCreatedWhenComposingTextSetIsDeleting()
EditorInfo outAttrs = new EditorInfo();
outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE;
TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
CharSequence newText = "I do not fear computers. I fear the lack of them.";
final TextEditingDelta expectedDelta =
new TextEditingDelta(
@@ -722,8 +741,10 @@ public void textEditingDelta_TestDeltaIsCreatedWhenComposingTextSetIsReplacing()
EditorInfo outAttrs = new EditorInfo();
outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE;
TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
CharSequence newText = "helfo";
final TextEditingDelta expectedDelta = new TextEditingDelta(newText, 0, 5, "hello", 5, 5, 0, 5);
@@ -829,8 +850,10 @@ public void inputConnectionAdaptor_RepeatFilter() throws NullPointerException {
EditorInfo outAttrs = new EditorInfo();
outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE;
TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
// Change InputTarget to FRAMEWORK_CLIENT.
textInputPlugin.setTextInputClient(
@@ -920,8 +943,10 @@ public void setTextInputEditingState_doesNotRestartWhenTextIsIdentical() {
testImm.setCurrentInputMethodSubtype(inputMethodSubtype);
View testView = new View(ctx);
TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@@ -958,8 +983,10 @@ public void setTextInputEditingState_alwaysSetEditableWhenDifferent() {
testImm.setCurrentInputMethodSubtype(inputMethodSubtype);
View testView = new View(ctx);
TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@@ -1006,8 +1033,10 @@ public void setTextInputEditingState_restartsIMEOnlyWhenFrameworkChangesComposin
testImm.setCurrentInputMethodSubtype(inputMethodSubtype);
View testView = new View(ctx);
TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@@ -1107,8 +1136,10 @@ public void setTextInputEditingState_nullInputMethodSubtype() {
View testView = new View(ctx);
TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@@ -1134,8 +1165,10 @@ public void setTextInputEditingState_nullInputMethodSubtype() {
public void destroy_clearTextInputMethodHandler() {
View testView = new View(ctx);
TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
verify(textInputChannel, times(1)).setTextInputMethodHandler(isNotNull());
textInputPlugin.destroy();
verify(textInputChannel, times(1)).setTextInputMethodHandler(isNull());
@@ -1150,8 +1183,10 @@ private void verifyInputConnection(TextInputChannel.TextInputType textInputType)
View testView = new View(ctx);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@@ -1225,8 +1260,10 @@ public void inputConnection_finishComposingTextUpdatesIMM() throws JSONException
View testView = new View(ctx);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@@ -1263,8 +1300,10 @@ public void inputConnection_textInputTypeNone() {
View testView = new View(ctx);
DartExecutor dartExecutor = mock(DartExecutor.class);
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@@ -1293,8 +1332,10 @@ public void showTextInput_textInputTypeNone() {
View testView = new View(ctx);
DartExecutor dartExecutor = mock(DartExecutor.class);
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@@ -1321,8 +1362,10 @@ public void inputConnection_textInputTypeMultilineAndSuggestionsDisabled() {
View testView = new View(ctx);
DartExecutor dartExecutor = mock(DartExecutor.class);
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
textInputPlugin.setTextInputClient(
0,
new TextInputChannel.Configuration(
@@ -1351,6 +1394,74 @@ public void inputConnection_textInputTypeMultilineAndSuggestionsDisabled() {
| InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
}
+ @Config(minSdk = API_LEVELS.API_34)
+ @TargetApi(API_LEVELS.API_34)
+ @Test
+ public void inputConnection_setsStylusHandwritingAvailable() {
+ View testView = new View(ctx);
+ DartExecutor dartExecutor = mock(DartExecutor.class);
+ TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
+ TextInputPlugin textInputPlugin =
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
+ textInputPlugin.setTextInputClient(
+ 0,
+ new TextInputChannel.Configuration(
+ false,
+ false,
+ true,
+ true,
+ false,
+ TextInputChannel.TextCapitalization.NONE,
+ new TextInputChannel.InputType(TextInputChannel.TextInputType.MULTILINE, false, false),
+ null,
+ null,
+ null,
+ null,
+ null));
+
+ EditorInfo editorInfo = new EditorInfo();
+ InputConnection connection =
+ textInputPlugin.createInputConnection(testView, mock(KeyboardManager.class), editorInfo);
+
+ assertTrue(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo));
+ }
+
+ @Config(sdk = API_LEVELS.API_32)
+ @TargetApi(API_LEVELS.API_32)
+ @Test
+ public void inputConnection_doesNotcallSetsStylusHandwritingAvailableWhenAPILevelUnsupported() {
+ View testView = new View(ctx);
+ DartExecutor dartExecutor = mock(DartExecutor.class);
+ TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
+ TextInputPlugin textInputPlugin =
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
+ textInputPlugin.setTextInputClient(
+ 0,
+ new TextInputChannel.Configuration(
+ false,
+ false,
+ true,
+ true,
+ false,
+ TextInputChannel.TextCapitalization.NONE,
+ new TextInputChannel.InputType(TextInputChannel.TextInputType.MULTILINE, false, false),
+ null,
+ null,
+ null,
+ null,
+ null));
+
+ EditorInfo editorInfo = new EditorInfo();
+ InputConnection connection =
+ textInputPlugin.createInputConnection(testView, mock(KeyboardManager.class), editorInfo);
+
+ assertFalse(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo));
+ }
+
// -------- Start: Autofill Tests -------
@Test
public void autofill_enabledByDefault() {
@@ -1359,8 +1470,10 @@ public void autofill_enabledByDefault() {
}
FlutterView testView = new FlutterView(ctx);
TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
final TextInputChannel.Configuration.Autofill autofill =
new TextInputChannel.Configuration.Autofill(
"1", new String[] {}, null, new TextInputChannel.TextEditState("", 0, 0, -1, -1));
@@ -1419,8 +1532,10 @@ public void autofill_canBeDisabled() {
}
FlutterView testView = new FlutterView(ctx);
TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
final TextInputChannel.Configuration.Autofill autofill =
new TextInputChannel.Configuration.Autofill(
"1", new String[] {}, null, new TextInputChannel.TextEditState("", 0, 0, -1, -1));
@@ -1456,8 +1571,10 @@ public void autofill_hintText() {
}
FlutterView testView = new FlutterView(ctx);
TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
final TextInputChannel.Configuration.Autofill autofill =
new TextInputChannel.Configuration.Autofill(
"1",
@@ -1501,8 +1618,10 @@ public void autofill_onProvideVirtualViewStructure() {
}
FlutterView testView = getTestView();
TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
final TextInputChannel.Configuration.Autofill autofill1 =
new TextInputChannel.Configuration.Autofill(
"1",
@@ -1593,8 +1712,10 @@ public void autofill_onProvideVirtualViewStructure_singular_textfield() {
// Migrate to ActivityScenario by following https://github.com/robolectric/robolectric/pull/4736
FlutterView testView = getTestView();
TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
final TextInputChannel.Configuration.Autofill autofill =
new TextInputChannel.Configuration.Autofill(
"1",
@@ -1646,8 +1767,10 @@ public void autofill_testLifeCycle() {
TestAfm testAfm = Shadow.extract(ctx.getSystemService(AutofillManager.class));
FlutterView testView = getTestView();
TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
// Set up an autofill scenario with 2 fields.
final TextInputChannel.Configuration.Autofill autofill1 =
@@ -1728,6 +1851,7 @@ public void autofill_testLifeCycle() {
testView,
0,
mock(TextInputChannel.class),
+ mock(ScribeChannel.class),
mockKeyboardManager,
(ListenableEditingState) textInputPlugin.getEditable(),
new EditorInfo());
@@ -1782,8 +1906,10 @@ public void autofill_testAutofillUpdatesTheFramework() {
TestAfm testAfm = Shadow.extract(ctx.getSystemService(AutofillManager.class));
FlutterView testView = getTestView();
TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
// Set up an autofill scenario with 2 fields.
final TextInputChannel.Configuration.Autofill autofill1 =
@@ -1877,8 +2003,10 @@ public void autofill_doesNotCrashAfterClearClientCall() {
}
FlutterView testView = new FlutterView(ctx);
TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class)));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
// Set up an autofill scenario with 2 fields.
final TextInputChannel.Configuration.Autofill autofillConfig =
new TextInputChannel.Configuration.Autofill(
@@ -1928,8 +2056,10 @@ public void autofill_testSetTextIpnutClientUpdatesSideFields() {
TestAfm testAfm = Shadow.extract(ctx.getSystemService(AutofillManager.class));
FlutterView testView = getTestView();
TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
// Set up an autofill scenario with 2 fields.
final TextInputChannel.Configuration.Autofill autofill1 =
@@ -2041,6 +2171,7 @@ public void sendAppPrivateCommand_dataIsEmpty() throws JSONException {
ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class);
DartExecutor mockBinaryMessenger = mock(DartExecutor.class);
TextInputChannel textInputChannel = new TextInputChannel(mockBinaryMessenger);
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
EventHandler mockEventHandler = mock(EventHandler.class);
TestImm testImm = Shadow.extract(ctx.getSystemService(Context.INPUT_METHOD_SERVICE));
@@ -2048,7 +2179,8 @@ public void sendAppPrivateCommand_dataIsEmpty() throws JSONException {
View testView = new View(ctx);
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
verify(mockBinaryMessenger, times(1))
.setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture());
@@ -2072,6 +2204,7 @@ public void sendAppPrivateCommand_hasData() throws JSONException {
ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class);
DartExecutor mockBinaryMessenger = mock(DartExecutor.class);
TextInputChannel textInputChannel = new TextInputChannel(mockBinaryMessenger);
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
EventHandler mockEventHandler = mock(EventHandler.class);
TestImm testImm = Shadow.extract(ctx.getSystemService(Context.INPUT_METHOD_SERVICE));
@@ -2079,7 +2212,8 @@ public void sendAppPrivateCommand_hasData() throws JSONException {
View testView = new View(ctx);
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
verify(mockBinaryMessenger, times(1))
.setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture());
@@ -2108,8 +2242,10 @@ public void ime_windowInsetsSync_notLaidOutBehindNavigation_excludesNavigationBa
when(testView.getWindowSystemUiVisibility()).thenReturn(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback();
FlutterEngine flutterEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni));
FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni));
@@ -2189,8 +2325,10 @@ public void ime_windowInsetsSync_laidOutBehindNavigation_includesNavigationBars(
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class));
ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback();
FlutterEngine flutterEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni));
FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni));
@@ -2268,8 +2406,10 @@ public void lastWindowInsets_updatedOnSecondOnProgressCall() {
when(testView.getWindowSystemUiVisibility()).thenReturn(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class));
+ ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class));
TextInputPlugin textInputPlugin =
- new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class));
+ new TextInputPlugin(
+ testView, textInputChannel, scribeChannel, mock(PlatformViewsController.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/PlatformViewsControllerTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java
index f9ebf55087fd4..4b36393b71924 100644
--- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java
+++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java
@@ -44,6 +44,7 @@
import io.flutter.embedding.engine.systemchannels.MouseCursorChannel;
import io.flutter.embedding.engine.systemchannels.PlatformViewsChannel;
import io.flutter.embedding.engine.systemchannels.PlatformViewsChannel.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;
@@ -1668,6 +1669,7 @@ public void scheduleFrame() {}
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.getPlatformViewsController()).thenReturn(platformViewsController);
when(engine.getLocalizationPlugin()).thenReturn(mock(LocalizationPlugin.class));
when(engine.getAccessibilityChannel()).thenReturn(mock(AccessibilityChannel.class));