From 38f1ee18671fb94b70d29f64193ecc028b3aae1b Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Mon, 20 May 2024 16:11:25 -0700 Subject: [PATCH 01/18] Proof of concept that can call startStylusHandwriting --- shell/platform/android/BUILD.gn | 2 + .../embedding/android/FlutterView.java | 5 ++ .../embedding/engine/FlutterEngine.java | 9 +++ .../engine/systemchannels/ScribeChannel.java | 78 +++++++++++++++++++ .../flutter/plugin/editing/ScribePlugin.java | 64 +++++++++++++++ 5 files changed, 158 insertions(+) create mode 100644 shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java create mode 100644 shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 0829a94c73052..a8b064a98e8cf 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -284,6 +284,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", @@ -310,6 +311,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 7272b5125ba05..3fbf8601470fc 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -62,6 +62,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; @@ -132,6 +133,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; @@ -1119,6 +1121,9 @@ 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..af3814041525f --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java @@ -0,0 +1,78 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.embedding.engine.systemchannels; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.Log; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.StandardMethodCodec; + +/** + * {@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"; + + 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) { + // TODO(justinmc): Log here (using the correct log method) to see if + // it's getting called. + if (scribeMethodHandler == null) { + Log.v( + TAG, + "No ScribeMethodHandler registered, call not forwarded to spell check API."); + return; + } + String method = call.method; + Object args = call.arguments; + Log.v(TAG, "Received '" + method + "' message."); + switch (method) { + case "Scribe.startStylusHandwriting": + try { + scribeMethodHandler.startStylusHandwriting(result); + } catch (IllegalStateException exception) { + result.error("error", exception.getMessage(), null); + } + break; + default: + result.notImplemented(); + break; + } + } + }; + + public ScribeChannel(@NonNull DartExecutor dartExecutor) { + channel = new MethodChannel(dartExecutor, "flutter/scribe", StandardMethodCodec.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 { + /** + * Requests to start Scribe stylus handwriting, which will respond to the + * {@code result} with either success if handwriting input has started or + * error otherwise. + */ + void startStylusHandwriting(@NonNull MethodChannel.Result result); + } +} 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..272912f858915 --- /dev/null +++ b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java @@ -0,0 +1,64 @@ +// 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 android.view.inputmethod.InputMethodManager; +import android.view.textservice.SentenceSuggestionsInfo; +import android.view.textservice.SuggestionsInfo; +import android.view.textservice.TextInfo; +import android.view.View; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import io.flutter.embedding.engine.systemchannels.ScribeChannel; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.localization.LocalizationPlugin; + +/** + * {@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}. + */ +public class ScribePlugin + implements ScribeChannel.ScribeMethodHandler { + + private final ScribeChannel mScribeChannel; + private final InputMethodManager mImm; + @NonNull private final View mView; + + @VisibleForTesting MethodChannel.Result pendingResult; + + public ScribePlugin( + @NonNull View view, + @NonNull InputMethodManager imm, + @NonNull ScribeChannel scribeChannel) { + mView = view; + mImm = imm; + mScribeChannel = scribeChannel; + + mScribeChannel.setScribeMethodHandler(this); + } + + /** + * 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); + } + + /** + * Starts stylus handwriting input. + */ + @Override + public void startStylusHandwriting( + @NonNull MethodChannel.Result result) { + mImm.startStylusHandwriting(mView); + } +} From f5c45e231889f87460809c22d384b80dca51a545 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 21 May 2024 11:24:44 -0700 Subject: [PATCH 02/18] Fix the notification 'this app does not support handwriting input' --- .../android/io/flutter/plugin/editing/ScribePlugin.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java index 272912f858915..61f1e16957b7a 100644 --- a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java @@ -35,6 +35,8 @@ public ScribePlugin( @NonNull View view, @NonNull InputMethodManager imm, @NonNull ScribeChannel scribeChannel) { + view.setAutoHandwritingEnabled(false); + mView = view; mImm = imm; mScribeChannel = scribeChannel; From 2c7087564b5b06844e0962db599e88559a28b8ed Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 21 May 2024 12:14:49 -0700 Subject: [PATCH 03/18] Correctly report success/error --- .../embedding/engine/systemchannels/ScribeChannel.java | 7 +++---- .../android/io/flutter/plugin/editing/ScribePlugin.java | 5 +---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java index af3814041525f..fd92346b1e6ea 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java @@ -27,8 +27,6 @@ public class ScribeChannel { new MethodChannel.MethodCallHandler() { @Override public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - // TODO(justinmc): Log here (using the correct log method) to see if - // it's getting called. if (scribeMethodHandler == null) { Log.v( TAG, @@ -41,7 +39,8 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result switch (method) { case "Scribe.startStylusHandwriting": try { - scribeMethodHandler.startStylusHandwriting(result); + scribeMethodHandler.startStylusHandwriting(); + result.success(null); } catch (IllegalStateException exception) { result.error("error", exception.getMessage(), null); } @@ -73,6 +72,6 @@ public interface ScribeMethodHandler { * {@code result} with either success if handwriting input has started or * error otherwise. */ - void startStylusHandwriting(@NonNull MethodChannel.Result result); + void startStylusHandwriting(); } } diff --git a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java index 61f1e16957b7a..5834ac10e7948 100644 --- a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java @@ -29,8 +29,6 @@ public class ScribePlugin private final InputMethodManager mImm; @NonNull private final View mView; - @VisibleForTesting MethodChannel.Result pendingResult; - public ScribePlugin( @NonNull View view, @NonNull InputMethodManager imm, @@ -59,8 +57,7 @@ public void destroy() { * Starts stylus handwriting input. */ @Override - public void startStylusHandwriting( - @NonNull MethodChannel.Result result) { + public void startStylusHandwriting() { mImm.startStylusHandwriting(mView); } } From 2da6b2ee3c1daad704025ceb30e7ffa63716cecf Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 21 May 2024 16:40:21 -0700 Subject: [PATCH 04/18] Show icon on hover --- .../io/flutter/embedding/android/FlutterView.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 3fbf8601470fc..7f745c5a35b64 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -39,6 +39,7 @@ import android.view.inputmethod.InputConnection; import android.view.textservice.SpellCheckerInfo; import android.view.textservice.TextServicesManager; +import android.view.InputDevice; import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -828,6 +829,20 @@ public InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs) { return textInputPlugin.createInputConnection(this, keyboardManager, outAttrs); } + @Override + public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) { + // TODO(justinmc): Also need to check if over a valid field and if stylus + // input is supported. + // Maybe have to do this in the framework and show a Flutter icon? + final int toolType = event.getToolType(pointerIndex); + if (!event.isFromSource(InputDevice.SOURCE_MOUSE) + && event.isFromSource(InputDevice.SOURCE_STYLUS) + && toolType == MotionEvent.TOOL_TYPE_STYLUS) { + return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HANDWRITING); + } + return super.onResolvePointerIcon(event, pointerIndex); + } + /** * Allows a {@code View} that is not currently the input connection target to invoke commands on * the {@link android.view.inputmethod.InputMethodManager}, which is otherwise disallowed. From 815e6cdb5fcf5803979ab54ef549a3278b49442e Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 21 May 2024 16:50:33 -0700 Subject: [PATCH 05/18] Trying the other gestures, but not getting called... --- .../editing/InputConnectionAdaptor.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index 1f688d7beb281..ba42b7578dc5f 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -26,6 +26,7 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.HandwritingGesture; import android.view.inputmethod.InputContentInfo; import android.view.inputmethod.InputMethodManager; import androidx.annotation.NonNull; @@ -38,6 +39,8 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.util.concurrent.Executor; +import java.util.function.IntConsumer; import java.util.HashMap; import java.util.Map; @@ -262,6 +265,24 @@ public boolean setSelection(int start, int end) { return result; } + @Override + public void performHandwritingGesture (HandwritingGesture gesture, Executor executor, IntConsumer consumer) { + // TODO(justinmc): Why doesn't this ever get called? + System.out.println("justin performHandwritingGesture."); + System.out.println("justin performHandwritingGesture gesture: " + gesture); + + /* +InputConnection#HANDWRITING_GESTURE_RESULT_SUCCESS +InputConnection#HANDWRITING_GESTURE_RESULT_FAILED +InputConnection#HANDWRITING_GESTURE_RESULT_FALLBACK +The gesture is performed and fallback text is inserted. +InputConnection#HANDWRITING_GESTURE_RESULT_UNSUPPORTED +The gesture is not supported by the editor +InputConnection#HANDWRITING_GESTURE_RESULT_CANCELLED + */ + executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_SUCCESS)); + } + // Sanitizes the index to ensure the index is within the range of the // contents of editable. private static int clampIndexToEditable(int index, Editable editable) { From da210444d3ad98acaa451c04503e166e76b3a409 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Wed, 22 May 2024 16:28:13 -0700 Subject: [PATCH 06/18] Use isStylusHandwritingAvailable, as recommended in the doc --- .../android/io/flutter/plugin/editing/ScribePlugin.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java index 5834ac10e7948..51ebb80cb643d 100644 --- a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java @@ -58,6 +58,11 @@ public void destroy() { */ @Override public void startStylusHandwriting() { + if (!mImm.isStylusHandwritingAvailable()) { + // TODO(justinmc): Maybe I should throw an error here. Or maybe I should + // expose this method and call it from the framework first. + return; + } mImm.startStylusHandwriting(mView); } } From cdf736637f7726daefe9c1a2ad573f13f0375c58 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Thu, 23 May 2024 12:59:36 -0700 Subject: [PATCH 07/18] TODO for upgraded androidx --- .../io/flutter/plugin/editing/TextInputPlugin.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index d6c3c6f90d483..63b2eeabf0554 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -341,6 +341,14 @@ public InputConnection createInputConnection( EditorInfoCompat.setContentMimeTypes(outAttrs, imgTypeString); } + // TODO(justinmc): If our AndroidX Core version can be upgraded, then + // uncomment this. + /* + if (EditorInfoCompat.isStylusHandwritingAvailable(outAttrs)) { + EditorInfoCompat.setStylusHandwritingEnabled(outAttrs, true); + } + */ + InputConnectionAdaptor connection = new InputConnectionAdaptor( view, inputTarget.id, textInputChannel, keyboardManager, mEditable, outAttrs); From 39249a505d4f6f3e0e5c7652d49e2098bcfe66e0 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 28 May 2024 13:38:41 -0700 Subject: [PATCH 08/18] AndroidX has been upgraded, though the PR seems to be getting reverted... --- .../android/io/flutter/plugin/editing/TextInputPlugin.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 63b2eeabf0554..688e12a183a21 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -341,13 +341,9 @@ public InputConnection createInputConnection( EditorInfoCompat.setContentMimeTypes(outAttrs, imgTypeString); } - // TODO(justinmc): If our AndroidX Core version can be upgraded, then - // uncomment this. - /* - if (EditorInfoCompat.isStylusHandwritingAvailable(outAttrs)) { + if (EditorInfoCompat.isStylusHandwritingEnabled(outAttrs)) { EditorInfoCompat.setStylusHandwritingEnabled(outAttrs, true); } - */ InputConnectionAdaptor connection = new InputConnectionAdaptor( From 14ff7865eb0df8b956e4917fda3704d0badbd58d Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 28 May 2024 16:46:13 -0700 Subject: [PATCH 09/18] Overriding preview as well, and checking if we ever get a call (no) --- .../flutter/plugin/editing/InputConnectionAdaptor.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index ba42b7578dc5f..a60bc37b9c4bd 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -13,6 +13,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.CancellationSignal; import android.text.DynamicLayout; import android.text.Editable; import android.text.InputType; @@ -29,6 +30,7 @@ import android.view.inputmethod.HandwritingGesture; import android.view.inputmethod.InputContentInfo; import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.PreviewableHandwritingGesture; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.core.view.inputmethod.InputConnectionCompat; @@ -265,6 +267,14 @@ public boolean setSelection(int start, int end) { return result; } + @Override + public boolean previewHandwritingGesture (PreviewableHandwritingGesture gesture, + CancellationSignal cancellationSignal) { + System.out.println("justin previewHandwritingGesture."); + System.out.println("justin previewHandwritingGesture gesture: " + gesture); + return true; + } + @Override public void performHandwritingGesture (HandwritingGesture gesture, Executor executor, IntConsumer consumer) { // TODO(justinmc): Why doesn't this ever get called? From 82174424cfacd6899aeb659f4fbdc0e5600f2dff Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 28 May 2024 16:46:33 -0700 Subject: [PATCH 10/18] Declare that we support all the handwriting gestures! --- .../flutter/plugin/editing/TextInputPlugin.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 688e12a183a21..b526d78cf16a2 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -20,9 +20,17 @@ import android.view.autofill.AutofillId; import android.view.autofill.AutofillManager; import android.view.autofill.AutofillValue; +import android.view.inputmethod.DeleteGesture; +import android.view.inputmethod.DeleteRangeGesture; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InsertGesture; +import android.view.inputmethod.InsertModeGesture; +import android.view.inputmethod.JoinOrSplitGesture; +import android.view.inputmethod.RemoveSpaceGesture; +import android.view.inputmethod.SelectGesture; +import android.view.inputmethod.SelectRangeGesture; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -33,7 +41,10 @@ import io.flutter.embedding.engine.systemchannels.TextInputChannel.TextEditState; import io.flutter.plugin.platform.PlatformViewsController; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; +import java.util.Set; /** Android implementation of the text input plugin. */ public class TextInputPlugin implements ListenableEditingState.EditingStateWatcher { @@ -342,7 +353,11 @@ public InputConnection createInputConnection( } if (EditorInfoCompat.isStylusHandwritingEnabled(outAttrs)) { + // TODO(justinmc): This is never called. Is it that + // isStylusHandwritingEnabled is false? EditorInfoCompat.setStylusHandwritingEnabled(outAttrs, true); + outAttrs.setSupportedHandwritingGestures(Arrays.asList(SelectGesture.class, SelectRangeGesture.class, InsertGesture.class, InsertModeGesture.class, DeleteGesture.class, DeleteRangeGesture.class, SelectRangeGesture.class, JoinOrSplitGesture.class, RemoveSpaceGesture.class)); + outAttrs.setSupportedHandwritingGesturePreviews(Set.of(SelectGesture.class, SelectRangeGesture.class, DeleteGesture.class, DeleteRangeGesture.class)); } InputConnectionAdaptor connection = From d5e030110be0153f3b3eb5310e58494084b6a685 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Wed, 29 May 2024 10:51:12 -0700 Subject: [PATCH 11/18] Well that's why that wasn't getting called --- .../io/flutter/plugin/editing/TextInputPlugin.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index b526d78cf16a2..8cf71cc5b208a 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -352,13 +352,9 @@ public InputConnection createInputConnection( EditorInfoCompat.setContentMimeTypes(outAttrs, imgTypeString); } - if (EditorInfoCompat.isStylusHandwritingEnabled(outAttrs)) { - // TODO(justinmc): This is never called. Is it that - // isStylusHandwritingEnabled is false? - EditorInfoCompat.setStylusHandwritingEnabled(outAttrs, true); - outAttrs.setSupportedHandwritingGestures(Arrays.asList(SelectGesture.class, SelectRangeGesture.class, InsertGesture.class, InsertModeGesture.class, DeleteGesture.class, DeleteRangeGesture.class, SelectRangeGesture.class, JoinOrSplitGesture.class, RemoveSpaceGesture.class)); - outAttrs.setSupportedHandwritingGesturePreviews(Set.of(SelectGesture.class, SelectRangeGesture.class, DeleteGesture.class, DeleteRangeGesture.class)); - } + EditorInfoCompat.setStylusHandwritingEnabled(outAttrs, true); + outAttrs.setSupportedHandwritingGestures(Arrays.asList(SelectGesture.class, SelectRangeGesture.class, InsertGesture.class, InsertModeGesture.class, DeleteGesture.class, DeleteRangeGesture.class, SelectRangeGesture.class, JoinOrSplitGesture.class, RemoveSpaceGesture.class)); + outAttrs.setSupportedHandwritingGesturePreviews(Set.of(SelectGesture.class, SelectRangeGesture.class, DeleteGesture.class, DeleteRangeGesture.class)); InputConnectionAdaptor connection = new InputConnectionAdaptor( From 39a4c8ec4dc1e6530f5372a54caeee0049dd6150 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Wed, 29 May 2024 12:02:18 -0700 Subject: [PATCH 12/18] Version gate --- .../android/io/flutter/embedding/android/FlutterView.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 7f745c5a35b64..7b9c0c3ee6d8b 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -1136,8 +1136,10 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) { Log.e(TAG, "TextServicesManager not supported by device, spell check disabled."); } + if (Build.VERSION.SDK_INT >= API_LEVELS.API_34) { scribePlugin = new ScribePlugin(this, textInputPlugin.getInputMethodManager(), this.flutterEngine.getScribeChannel()); + } localizationPlugin = this.flutterEngine.getLocalizationPlugin(); From 27ca8db2c027a95b3901d6bd8af2e9c045a15e10 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Wed, 29 May 2024 16:16:28 -0700 Subject: [PATCH 13/18] Send selection gesture to framework --- .../embedding/android/FlutterView.java | 3 ++ .../engine/systemchannels/ScribeChannel.java | 17 +++++++++ .../editing/InputConnectionAdaptor.java | 35 ++++++++++++++++--- .../plugin/editing/TextInputPlugin.java | 9 ++++- .../android/io/flutter/view/FlutterView.java | 3 +- 5 files changed, 60 insertions(+), 7 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 7b9c0c3ee6d8b..379ae248b04af 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -1120,10 +1120,13 @@ 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(), + // TODO(justinmc): This could just be part of TextInputChannel... + this.flutterEngine.getScribeChannel(), this.flutterEngine.getPlatformViewsController()); try { diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java index fd92346b1e6ea..6589ee3d97dc0 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java @@ -4,6 +4,8 @@ package io.flutter.embedding.engine.systemchannels; +import android.graphics.RectF; +import android.view.inputmethod.SelectGesture; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.flutter.Log; @@ -11,6 +13,8 @@ import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.StandardMethodCodec; +import java.util.HashMap; +import java.util.Arrays; /** * {@link ScribeChannel} is a platform channel that is used by the framework to facilitate @@ -74,4 +78,17 @@ public interface ScribeMethodHandler { */ void startStylusHandwriting(); } + + public void performHandwritingSelectGesture(SelectGesture gesture, MethodChannel.Result result) { + System.out.println("justin sending performSelectionGesture for gesture: " + gesture); + final HashMap selectionAreaMap = new HashMap<>(); + final RectF selectionArea = gesture.getSelectionArea(); + selectionAreaMap.put("bottom", selectionArea.bottom); + selectionAreaMap.put("top", selectionArea.top); + selectionAreaMap.put("left", selectionArea.left); + selectionAreaMap.put("right", selectionArea.right); + final HashMap gestureMap = new HashMap<>(); + gestureMap.put("selectionArea", selectionAreaMap); + channel.invokeMethod("ScribeClient.performSelectionGesture", Arrays.asList(gestureMap), result); + } } diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index a60bc37b9c4bd..d9acbf2df2ad4 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -31,12 +31,15 @@ import android.view.inputmethod.InputContentInfo; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.PreviewableHandwritingGesture; +import android.view.inputmethod.SelectGesture; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; 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 io.flutter.plugin.common.MethodChannel; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; @@ -56,6 +59,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; @@ -74,6 +78,7 @@ public InputConnectionAdaptor( View view, int client, TextInputChannel textInputChannel, + ScribeChannel scribeChannel, KeyboardDelegate keyboardDelegate, ListenableEditingState editable, EditorInfo editorInfo, @@ -82,6 +87,7 @@ public InputConnectionAdaptor( mFlutterView = view; mClient = client; this.textInputChannel = textInputChannel; + this.scribeChannel = scribeChannel; mEditable = editable; mEditable.addEditingStateListener(this); mEditorInfo = editorInfo; @@ -105,10 +111,11 @@ 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) { @@ -270,17 +277,36 @@ public boolean setSelection(int start, int end) { @Override public boolean previewHandwritingGesture (PreviewableHandwritingGesture gesture, CancellationSignal cancellationSignal) { - System.out.println("justin previewHandwritingGesture."); System.out.println("justin previewHandwritingGesture gesture: " + gesture); return true; } @Override public void performHandwritingGesture (HandwritingGesture gesture, Executor executor, IntConsumer consumer) { - // TODO(justinmc): Why doesn't this ever get called? - System.out.println("justin performHandwritingGesture."); System.out.println("justin performHandwritingGesture gesture: " + gesture); + if (gesture instanceof SelectGesture) { + final MethodChannel.Result result = new MethodChannel.Result() { + @Override + public void success(Object result) { + executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_SUCCESS)); + } + + @Override + public void error(String errorCode, String errorMessage, Object errorDetails) { + executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_FAILED)); + } + + @Override + public void notImplemented() { + executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_UNSUPPORTED)); + } + }; + scribeChannel.performHandwritingSelectGesture((SelectGesture) gesture, result); + return; + } + + executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_UNSUPPORTED)); /* InputConnection#HANDWRITING_GESTURE_RESULT_SUCCESS InputConnection#HANDWRITING_GESTURE_RESULT_FAILED @@ -290,7 +316,6 @@ public void performHandwritingGesture (HandwritingGesture gesture, Executor exec The gesture is not supported by the editor InputConnection#HANDWRITING_GESTURE_RESULT_CANCELLED */ - executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_SUCCESS)); } // Sanitizes the index to ensure the index is within the range of the diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 8cf71cc5b208a..49dd24dd26911 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -37,6 +37,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; @@ -53,6 +54,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; @@ -77,6 +79,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. @@ -164,6 +167,8 @@ public void sendAppPrivateCommand(String action, Bundle data) { textInputChannel.requestExistingInputState(); + this.scribeChannel = scribeChannel; + this.platformViewsController = platformViewsController; this.platformViewsController.attachTextInputPlugin(this); } @@ -358,7 +363,9 @@ public InputConnection createInputConnection( InputConnectionAdaptor connection = new InputConnectionAdaptor( - view, inputTarget.id, textInputChannel, keyboardManager, mEditable, outAttrs); + // TODO(justinmc): scribeChannel could be part of textInputChannel + // instead of adding a new parameter here. + 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 62f2a17505174..ecb4898bef2b3 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,7 @@ 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) { From 521fb2278da494be53f3fe6c452cbe7ab9f51dc7 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Thu, 30 May 2024 16:38:06 -0700 Subject: [PATCH 14/18] TODO for granularity --- .../flutter/embedding/engine/systemchannels/ScribeChannel.java | 1 + 1 file changed, 1 insertion(+) diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java index 6589ee3d97dc0..b602244281dfe 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java @@ -87,6 +87,7 @@ public void performHandwritingSelectGesture(SelectGesture gesture, MethodChannel selectionAreaMap.put("top", selectionArea.top); selectionAreaMap.put("left", selectionArea.left); selectionAreaMap.put("right", selectionArea.right); + // TODO(justinmc): Include granularity. final HashMap gestureMap = new HashMap<>(); gestureMap.put("selectionArea", selectionAreaMap); channel.invokeMethod("ScribeClient.performSelectionGesture", Arrays.asList(gestureMap), result); From 5046264600d5945df71877d631bcbddf4d28631f Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Mon, 30 Sep 2024 13:50:15 -0700 Subject: [PATCH 15/18] Auto formatting --- .../embedding/android/FlutterView.java | 9 +-- .../engine/systemchannels/ScribeChannel.java | 22 +++--- .../editing/InputConnectionAdaptor.java | 70 +++++++++++-------- .../flutter/plugin/editing/ScribePlugin.java | 28 +++----- .../plugin/editing/TextInputPlugin.java | 28 ++++++-- .../android/io/flutter/view/FlutterView.java | 6 +- 6 files changed, 91 insertions(+), 72 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 5e227019b1be2..716b943a58fff 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -24,6 +24,7 @@ import android.util.SparseArray; import android.view.Display; import android.view.DisplayCutout; +import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.PointerIcon; @@ -40,7 +41,6 @@ import android.view.inputmethod.InputConnection; import android.view.textservice.SpellCheckerInfo; import android.view.textservice.TextServicesManager; -import android.view.InputDevice; import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -853,8 +853,8 @@ public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) { // Maybe have to do this in the framework and show a Flutter icon? final int toolType = event.getToolType(pointerIndex); if (!event.isFromSource(InputDevice.SOURCE_MOUSE) - && event.isFromSource(InputDevice.SOURCE_STYLUS) - && toolType == MotionEvent.TOOL_TYPE_STYLUS) { + && event.isFromSource(InputDevice.SOURCE_STYLUS) + && toolType == MotionEvent.TOOL_TYPE_STYLUS) { return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HANDWRITING); } return super.onResolvePointerIcon(event, pointerIndex); @@ -1158,7 +1158,8 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) { if (Build.VERSION.SDK_INT >= API_LEVELS.API_34) { scribePlugin = - new ScribePlugin(this, textInputPlugin.getInputMethodManager(), this.flutterEngine.getScribeChannel()); + new ScribePlugin( + this, textInputPlugin.getInputMethodManager(), this.flutterEngine.getScribeChannel()); } localizationPlugin = this.flutterEngine.getLocalizationPlugin(); diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java index b602244281dfe..4ee05fd28c032 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java @@ -13,12 +13,12 @@ import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.StandardMethodCodec; -import java.util.HashMap; import java.util.Arrays; +import java.util.HashMap; /** - * {@link ScribeChannel} is a platform channel that is used by the framework to facilitate - * the Scribe handwriting text input feature. + * {@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"; @@ -32,9 +32,7 @@ public class ScribeChannel { @Override public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { if (scribeMethodHandler == null) { - Log.v( - TAG, - "No ScribeMethodHandler registered, call not forwarded to spell check API."); + Log.v(TAG, "No ScribeMethodHandler registered, call not forwarded to spell check API."); return; } String method = call.method; @@ -62,19 +60,17 @@ public ScribeChannel(@NonNull DartExecutor dartExecutor) { } /** - * Sets the {@link ScribeMethodHandler} which receives all requests for scribe - * sent through this channel. + * Sets the {@link ScribeMethodHandler} which receives all requests for scribe sent through this + * channel. */ - public void setScribeMethodHandler( - @Nullable ScribeMethodHandler scribeMethodHandler) { + public void setScribeMethodHandler(@Nullable ScribeMethodHandler scribeMethodHandler) { this.scribeMethodHandler = scribeMethodHandler; } public interface ScribeMethodHandler { /** - * Requests to start Scribe stylus handwriting, which will respond to the - * {@code result} with either success if handwriting input has started or - * error otherwise. + * Requests to start Scribe stylus handwriting, which will respond to the {@code result} with + * either success if handwriting input has started or error otherwise. */ void startStylusHandwriting(); } diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index d9acbf2df2ad4..2f5b1a159f8eb 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -44,10 +44,10 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.util.concurrent.Executor; -import java.util.function.IntConsumer; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.Executor; +import java.util.function.IntConsumer; public class InputConnectionAdaptor extends BaseInputConnection implements ListenableEditingState.EditingStateWatcher { @@ -115,7 +115,15 @@ public InputConnectionAdaptor( KeyboardDelegate keyboardDelegate, ListenableEditingState editable, EditorInfo editorInfo) { - this(view, client, textInputChannel, scribeChannel, keyboardDelegate, editable, editorInfo, new FlutterJNI()); + this( + view, + client, + textInputChannel, + scribeChannel, + keyboardDelegate, + editable, + editorInfo, + new FlutterJNI()); } private ExtractedText getExtractedText(ExtractedTextRequest request) { @@ -275,47 +283,49 @@ public boolean setSelection(int start, int end) { } @Override - public boolean previewHandwritingGesture (PreviewableHandwritingGesture gesture, - CancellationSignal cancellationSignal) { + public boolean previewHandwritingGesture( + PreviewableHandwritingGesture gesture, CancellationSignal cancellationSignal) { System.out.println("justin previewHandwritingGesture gesture: " + gesture); return true; } @Override - public void performHandwritingGesture (HandwritingGesture gesture, Executor executor, IntConsumer consumer) { + public void performHandwritingGesture( + HandwritingGesture gesture, Executor executor, IntConsumer consumer) { System.out.println("justin performHandwritingGesture gesture: " + gesture); if (gesture instanceof SelectGesture) { - final MethodChannel.Result result = new MethodChannel.Result() { - @Override - public void success(Object result) { - executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_SUCCESS)); - } - - @Override - public void error(String errorCode, String errorMessage, Object errorDetails) { - executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_FAILED)); - } - - @Override - public void notImplemented() { - executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_UNSUPPORTED)); - } - }; + final MethodChannel.Result result = + new MethodChannel.Result() { + @Override + public void success(Object result) { + executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_SUCCESS)); + } + + @Override + public void error(String errorCode, String errorMessage, Object errorDetails) { + executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_FAILED)); + } + + @Override + public void notImplemented() { + executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_UNSUPPORTED)); + } + }; scribeChannel.performHandwritingSelectGesture((SelectGesture) gesture, result); return; } executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_UNSUPPORTED)); /* -InputConnection#HANDWRITING_GESTURE_RESULT_SUCCESS -InputConnection#HANDWRITING_GESTURE_RESULT_FAILED -InputConnection#HANDWRITING_GESTURE_RESULT_FALLBACK -The gesture is performed and fallback text is inserted. -InputConnection#HANDWRITING_GESTURE_RESULT_UNSUPPORTED -The gesture is not supported by the editor -InputConnection#HANDWRITING_GESTURE_RESULT_CANCELLED - */ + InputConnection#HANDWRITING_GESTURE_RESULT_SUCCESS + InputConnection#HANDWRITING_GESTURE_RESULT_FAILED + InputConnection#HANDWRITING_GESTURE_RESULT_FALLBACK + The gesture is performed and fallback text is inserted. + InputConnection#HANDWRITING_GESTURE_RESULT_UNSUPPORTED + The gesture is not supported by the editor + InputConnection#HANDWRITING_GESTURE_RESULT_CANCELLED + */ } // Sanitizes the index to ensure the index is within the range of the diff --git a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java index 51ebb80cb643d..367f1cbc419aa 100644 --- a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java @@ -4,35 +4,26 @@ package io.flutter.plugin.editing; -import android.view.inputmethod.InputMethodManager; -import android.view.textservice.SentenceSuggestionsInfo; -import android.view.textservice.SuggestionsInfo; -import android.view.textservice.TextInfo; import android.view.View; +import android.view.inputmethod.InputMethodManager; import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; import io.flutter.embedding.engine.systemchannels.ScribeChannel; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.localization.LocalizationPlugin; /** - * {@link ScribePlugin} is the implementation of all functionality needed for - * handwriting stylus text input. + * {@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}. */ -public class ScribePlugin - implements ScribeChannel.ScribeMethodHandler { +public class ScribePlugin implements ScribeChannel.ScribeMethodHandler { private final ScribeChannel mScribeChannel; private final InputMethodManager mImm; @NonNull private final View mView; public ScribePlugin( - @NonNull View view, - @NonNull InputMethodManager imm, - @NonNull ScribeChannel scribeChannel) { + @NonNull View view, @NonNull InputMethodManager imm, @NonNull ScribeChannel scribeChannel) { view.setAutoHandwritingEnabled(false); mView = view; @@ -43,9 +34,8 @@ public ScribePlugin( } /** - * Unregisters this {@code ScribePlugin} as the {@code - * ScribeChannel.ScribeMethodHandler}, for the {@link - * io.flutter.embedding.engine.systemchannels.ScribeChannel}. + * 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. */ @@ -53,9 +43,7 @@ public void destroy() { mScribeChannel.setScribeMethodHandler(null); } - /** - * Starts stylus handwriting input. - */ + /** Starts stylus handwriting input. */ @Override public void startStylusHandwriting() { if (!mImm.isStylusHandwritingAvailable()) { diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 6fba6ac1868b8..e046ed871c31c 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -44,7 +44,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.List; import java.util.Set; /** Android implementation of the text input plugin. */ @@ -358,14 +357,35 @@ public InputConnection createInputConnection( } EditorInfoCompat.setStylusHandwritingEnabled(outAttrs, true); - outAttrs.setSupportedHandwritingGestures(Arrays.asList(SelectGesture.class, SelectRangeGesture.class, InsertGesture.class, InsertModeGesture.class, DeleteGesture.class, DeleteRangeGesture.class, SelectRangeGesture.class, JoinOrSplitGesture.class, RemoveSpaceGesture.class)); - outAttrs.setSupportedHandwritingGesturePreviews(Set.of(SelectGesture.class, SelectRangeGesture.class, DeleteGesture.class, DeleteRangeGesture.class)); + outAttrs.setSupportedHandwritingGestures( + Arrays.asList( + SelectGesture.class, + SelectRangeGesture.class, + InsertGesture.class, + InsertModeGesture.class, + DeleteGesture.class, + DeleteRangeGesture.class, + SelectRangeGesture.class, + JoinOrSplitGesture.class, + RemoveSpaceGesture.class)); + outAttrs.setSupportedHandwritingGesturePreviews( + Set.of( + SelectGesture.class, + SelectRangeGesture.class, + DeleteGesture.class, + DeleteRangeGesture.class)); InputConnectionAdaptor connection = new InputConnectionAdaptor( // TODO(justinmc): scribeChannel could be part of textInputChannel // instead of adding a new parameter here. - view, inputTarget.id, textInputChannel, scribeChannel, 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 8f57676150aa6..d20ea56f1f9db 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -238,7 +238,11 @@ public void onPostResume() { PlatformViewsController platformViewsController = mNativeView.getPluginRegistry().getPlatformViewsController(); mTextInputPlugin = - new TextInputPlugin(this, new TextInputChannel(dartExecutor), new ScribeChannel(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) { From f9856a20cee0848db56415f6ee5428eb05f13565 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Mon, 30 Sep 2024 13:52:40 -0700 Subject: [PATCH 16/18] Pointer functionality is being moved to a separate PR --- .../io/flutter/embedding/android/FlutterView.java | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 716b943a58fff..a85714c7ab39d 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -24,7 +24,6 @@ import android.util.SparseArray; import android.view.Display; import android.view.DisplayCutout; -import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.PointerIcon; @@ -846,20 +845,6 @@ public InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs) { return textInputPlugin.createInputConnection(this, keyboardManager, outAttrs); } - @Override - public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) { - // TODO(justinmc): Also need to check if over a valid field and if stylus - // input is supported. - // Maybe have to do this in the framework and show a Flutter icon? - final int toolType = event.getToolType(pointerIndex); - if (!event.isFromSource(InputDevice.SOURCE_MOUSE) - && event.isFromSource(InputDevice.SOURCE_STYLUS) - && toolType == MotionEvent.TOOL_TYPE_STYLUS) { - return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HANDWRITING); - } - return super.onResolvePointerIcon(event, pointerIndex); - } - /** * Allows a {@code View} that is not currently the input connection target to invoke commands on * the {@link android.view.inputmethod.InputMethodManager}, which is otherwise disallowed. From 989eb2adc3dbd176c0ac4e98c5882bc13168f9e8 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Mon, 30 Sep 2024 17:13:24 -0700 Subject: [PATCH 17/18] WIP Generic handwriting gesture call --- .../engine/systemchannels/ScribeChannel.java | 43 +++++++++++++----- .../editing/InputConnectionAdaptor.java | 44 +++++++++---------- 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java index 4ee05fd28c032..5af44235a290e 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java @@ -5,6 +5,8 @@ package io.flutter.embedding.engine.systemchannels; import android.graphics.RectF; +import android.view.inputmethod.DeleteGesture; +import android.view.inputmethod.HandwritingGesture; import android.view.inputmethod.SelectGesture; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -13,7 +15,6 @@ import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.StandardMethodCodec; -import java.util.Arrays; import java.util.HashMap; /** @@ -75,17 +76,35 @@ public interface ScribeMethodHandler { void startStylusHandwriting(); } - public void performHandwritingSelectGesture(SelectGesture gesture, MethodChannel.Result result) { - System.out.println("justin sending performSelectionGesture for gesture: " + gesture); - final HashMap selectionAreaMap = new HashMap<>(); - final RectF selectionArea = gesture.getSelectionArea(); - selectionAreaMap.put("bottom", selectionArea.bottom); - selectionAreaMap.put("top", selectionArea.top); - selectionAreaMap.put("left", selectionArea.left); - selectionAreaMap.put("right", selectionArea.right); - // TODO(justinmc): Include granularity. + public void performHandwritingGesture(HandwritingGesture gesture, MethodChannel.Result result) { + System.out.println("justin sending performHandwritingGesture for gesture: " + gesture); + final HashMap gestureMap = new HashMap<>(); - gestureMap.put("selectionArea", selectionAreaMap); - channel.invokeMethod("ScribeClient.performSelectionGesture", Arrays.asList(gestureMap), result); + if (gesture instanceof SelectGesture) { + final SelectGesture selectGesture = (SelectGesture) gesture; + final HashMap selectionAreaMap = new HashMap<>(); + final RectF selectionArea = selectGesture.getSelectionArea(); + selectionAreaMap.put("bottom", selectionArea.bottom); + selectionAreaMap.put("top", selectionArea.top); + selectionAreaMap.put("left", selectionArea.left); + selectionAreaMap.put("right", selectionArea.right); + gestureMap.put("type", "select"); + gestureMap.put("granularity", selectGesture.getGranularity()); + gestureMap.put("selectionArea", selectionAreaMap); + } else if (gesture instanceof DeleteGesture) { + final DeleteGesture deleteGesture = (DeleteGesture) gesture; + final HashMap deletionAreaMap = new HashMap<>(); + final RectF deletionArea = deleteGesture.getDeletionArea(); + deletionAreaMap.put("bottom", deletionArea.bottom); + deletionAreaMap.put("top", deletionArea.top); + deletionAreaMap.put("left", deletionArea.left); + deletionAreaMap.put("right", deletionArea.right); + gestureMap.put("type", "delete"); + gestureMap.put("granularity", deleteGesture.getGranularity()); + gestureMap.put("deletionArea", deletionAreaMap); + } + // TODO(justinmc): All other gestures. + + channel.invokeMethod("ScribeClient.performHandwritingGesture", gestureMap, result); } } diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index 2f5b1a159f8eb..12be7c5f6ea5d 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -31,7 +31,6 @@ import android.view.inputmethod.InputContentInfo; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.PreviewableHandwritingGesture; -import android.view.inputmethod.SelectGesture; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.core.view.inputmethod.InputConnectionCompat; @@ -286,6 +285,7 @@ public boolean setSelection(int start, int end) { public boolean previewHandwritingGesture( PreviewableHandwritingGesture gesture, CancellationSignal cancellationSignal) { System.out.println("justin previewHandwritingGesture gesture: " + gesture); + // TODO(justinmc): Send via another methodchannel call. return true; } @@ -294,29 +294,25 @@ public void performHandwritingGesture( HandwritingGesture gesture, Executor executor, IntConsumer consumer) { System.out.println("justin performHandwritingGesture gesture: " + gesture); - if (gesture instanceof SelectGesture) { - final MethodChannel.Result result = - new MethodChannel.Result() { - @Override - public void success(Object result) { - executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_SUCCESS)); - } - - @Override - public void error(String errorCode, String errorMessage, Object errorDetails) { - executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_FAILED)); - } - - @Override - public void notImplemented() { - executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_UNSUPPORTED)); - } - }; - scribeChannel.performHandwritingSelectGesture((SelectGesture) gesture, result); - return; - } - - executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_UNSUPPORTED)); + final MethodChannel.Result result = + new MethodChannel.Result() { + @Override + public void success(Object result) { + executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_SUCCESS)); + } + + @Override + public void error(String errorCode, String errorMessage, Object errorDetails) { + executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_FAILED)); + } + + @Override + public void notImplemented() { + executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_UNSUPPORTED)); + } + }; + + scribeChannel.performHandwritingGesture((HandwritingGesture) gesture, result); /* InputConnection#HANDWRITING_GESTURE_RESULT_SUCCESS InputConnection#HANDWRITING_GESTURE_RESULT_FAILED From 47bd6b7ce4f9faad1ab7ce9b8c6911e45269a561 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 1 Oct 2024 12:38:08 -0700 Subject: [PATCH 18/18] WIP previewHandwritingGesture --- .../engine/systemchannels/ScribeChannel.java | 28 ++++++++++++++++++- .../editing/InputConnectionAdaptor.java | 16 ++++------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java index 5af44235a290e..c6925cdea9e5c 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java @@ -5,9 +5,13 @@ package io.flutter.embedding.engine.systemchannels; import android.graphics.RectF; +import android.os.CancellationSignal; import android.view.inputmethod.DeleteGesture; +import android.view.inputmethod.DeleteRangeGesture; import android.view.inputmethod.HandwritingGesture; +import android.view.inputmethod.PreviewableHandwritingGesture; import android.view.inputmethod.SelectGesture; +import android.view.inputmethod.SelectRangeGesture; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.flutter.Log; @@ -76,6 +80,28 @@ public interface ScribeMethodHandler { void startStylusHandwriting(); } + public void previewHandwritingGesture(PreviewableHandwritingGesture gesture, CancellationSignal cancellationSignal) { + System.out.println("justin sending previewHandwritingGesture for gesture: " + gesture); + final HashMap gestureMap = new HashMap<>(); + if (gesture instanceof DeleteGesture) { + gestureMap.put("type", "delete"); + } else if (gesture instanceof DeleteRangeGesture) { + gestureMap.put("type", "deleteRange"); + } else if (gesture instanceof SelectGesture) { + gestureMap.put("type", "select"); + } else if (gesture instanceof SelectRangeGesture) { + gestureMap.put("type", "selectRange"); + } else { + return; + } + + // TODO(justinmc): You'll need to provide some kind of API that allows users + // to cancel a previewed gesture. Maybe keep ahold of cancellationSignal + // here, then provide platform channel methods for cancel, isCanceled, + // setOnCancelListener, and throwIfCanceled. + channel.invokeMethod("ScribeClient.previewHandwritingGesture", gestureMap); + } + public void performHandwritingGesture(HandwritingGesture gesture, MethodChannel.Result result) { System.out.println("justin sending performHandwritingGesture for gesture: " + gesture); @@ -103,7 +129,7 @@ public void performHandwritingGesture(HandwritingGesture gesture, MethodChannel. gestureMap.put("granularity", deleteGesture.getGranularity()); gestureMap.put("deletionArea", deletionAreaMap); } - // TODO(justinmc): All other gestures. + // TODO(justinmc): All other gestures. https://developer.android.com/reference/android/view/inputmethod/HandwritingGesture#public-methods channel.invokeMethod("ScribeClient.performHandwritingGesture", gestureMap, result); } diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index 12be7c5f6ea5d..b48451f9af161 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -284,8 +284,9 @@ public boolean setSelection(int start, int end) { @Override public boolean previewHandwritingGesture( PreviewableHandwritingGesture gesture, CancellationSignal cancellationSignal) { - System.out.println("justin previewHandwritingGesture gesture: " + gesture); - // TODO(justinmc): Send via another methodchannel call. + System.out.println("justin previewHandwritingGesture gesture: " + gesture + ", " + cancellationSignal); + + scribeChannel.previewHandwritingGesture(gesture, cancellationSignal); return true; } @@ -310,18 +311,11 @@ public void error(String errorCode, String errorMessage, Object errorDetails) { public void notImplemented() { executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_UNSUPPORTED)); } + // TODO(justinmc): There are a few other HANDWRITING_GESTURE_RESULTs + // that may be useful, see https://developer.android.com/reference/android/view/inputmethod/InputConnection }; scribeChannel.performHandwritingGesture((HandwritingGesture) gesture, result); - /* - InputConnection#HANDWRITING_GESTURE_RESULT_SUCCESS - InputConnection#HANDWRITING_GESTURE_RESULT_FAILED - InputConnection#HANDWRITING_GESTURE_RESULT_FALLBACK - The gesture is performed and fallback text is inserted. - InputConnection#HANDWRITING_GESTURE_RESULT_UNSUPPORTED - The gesture is not supported by the editor - InputConnection#HANDWRITING_GESTURE_RESULT_CANCELLED - */ } // Sanitizes the index to ensure the index is within the range of the