From 38f1ee18671fb94b70d29f64193ecc028b3aae1b Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Mon, 20 May 2024 16:11:25 -0700 Subject: [PATCH 01/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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 From 948d387e0ad1b10892da4039c490449fe2f1942e Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 1 Oct 2024 13:05:50 -0700 Subject: [PATCH 19/47] Scribe gestures will be handled in a separate PR --- .../engine/systemchannels/ScribeChannel.java | 64 +------------------ .../editing/InputConnectionAdaptor.java | 38 +---------- 2 files changed, 5 insertions(+), 97 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 c6925cdea9e5c..529d152805a7c 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java @@ -4,14 +4,6 @@ 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; @@ -19,7 +11,6 @@ import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.StandardMethodCodec; -import java.util.HashMap; /** * {@link ScribeChannel} is a platform channel that is used by the framework to facilitate the @@ -80,57 +71,6 @@ 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); - - final HashMap gestureMap = new HashMap<>(); - 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. https://developer.android.com/reference/android/view/inputmethod/HandwritingGesture#public-methods - - channel.invokeMethod("ScribeClient.performHandwritingGesture", gestureMap, result); - } + // 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 b48451f9af161..dace128696163 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -13,7 +13,6 @@ 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; @@ -30,7 +29,6 @@ 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; @@ -38,7 +36,6 @@ 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; @@ -281,41 +278,12 @@ public boolean setSelection(int start, int end) { return result; } - @Override - public boolean previewHandwritingGesture( - PreviewableHandwritingGesture gesture, CancellationSignal cancellationSignal) { - System.out.println("justin previewHandwritingGesture gesture: " + gesture + ", " + cancellationSignal); - - scribeChannel.previewHandwritingGesture(gesture, cancellationSignal); - return true; - } - + // TODO(justinmc): Scribe stylus gestures should be supported here. + // https://github.com/flutter/flutter/issues/156018 @Override public void performHandwritingGesture( HandwritingGesture gesture, Executor executor, IntConsumer consumer) { - System.out.println("justin performHandwritingGesture gesture: " + gesture); - - 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)); - } - // 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); + executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_UNSUPPORTED)); } // Sanitizes the index to ensure the index is within the range of the From c2f1425a3be047150ed4f4b6fbd431fc32982f08 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 1 Oct 2024 13:30:10 -0700 Subject: [PATCH 20/47] isStylusHandwritingAvailable method --- .../engine/systemchannels/ScribeChannel.java | 16 +++++++++++++++- .../io/flutter/plugin/editing/ScribePlugin.java | 17 ++++++++++++++++- 2 files changed, 31 insertions(+), 2 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 529d152805a7c..ad36bcf5288db 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java @@ -28,13 +28,21 @@ 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. Scribe call not handled."); return; } String method = call.method; Object args = call.arguments; Log.v(TAG, "Received '" + method + "' message."); switch (method) { + case "Scribe.isStylusHandwritingAvailable": + try { + final boolean isAvailable = scribeMethodHandler.isStylusHandwritingAvailable(); + result.success(isAvailable); + } catch (IllegalStateException exception) { + result.error("error", exception.getMessage(), null); + } + break; case "Scribe.startStylusHandwriting": try { scribeMethodHandler.startStylusHandwriting(); @@ -64,6 +72,12 @@ public void setScribeMethodHandler(@Nullable ScribeMethodHandler scribeMethodHan } public interface ScribeMethodHandler { + /** + * Responds to the {@code result} with success and a boolean indicating whether or not stylus + * hadnwriting is available. + */ + 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. diff --git a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java index 367f1cbc419aa..e845257006c96 100644 --- a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java @@ -43,7 +43,22 @@ public void destroy() { mScribeChannel.setScribeMethodHandler(null); } - /** Starts stylus handwriting input. */ + /** + * Returns true if the InputMethodManager supports Scribe stylus handwriting input. + * + *

Call this before calling startStylusHandwriting to make sure it's available. + */ + @Override + public boolean isStylusHandwritingAvailable() { + return mImm.isStylusHandwritingAvailable(); + } + + /** + * Starts stylus handwriting input. + * + *

Typically isStylusHandwritingAvailable should be called first to determine whether this is + * supported by the IME. + */ @Override public void startStylusHandwriting() { if (!mImm.isStylusHandwritingAvailable()) { From 5d385c51bffaf67e473f4a698a78e89cf8ab5ef7 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 1 Oct 2024 13:46:38 -0700 Subject: [PATCH 21/47] Don't need to call isStylusHandwritingAvailable, that's on the caller --- .../android/io/flutter/plugin/editing/ScribePlugin.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java index e845257006c96..186f3d537e940 100644 --- a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java @@ -61,11 +61,6 @@ public boolean isStylusHandwritingAvailable() { */ @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 0a541344d59fd2aa035df33186636b4b901c31ec Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 1 Oct 2024 13:55:18 -0700 Subject: [PATCH 22/47] Remove support for gestures, now shows dialog when gesture performed --- .../plugin/editing/TextInputPlugin.java | 31 +++---------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index e046ed871c31c..4d3d62c66417d 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -20,17 +20,9 @@ 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; @@ -42,9 +34,7 @@ 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.Set; /** Android implementation of the text input plugin. */ public class TextInputPlugin implements ListenableEditingState.EditingStateWatcher { @@ -357,23 +347,10 @@ 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)); + // 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( From aa95c06be444fc19f22ec3f746125905ea540312 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 1 Oct 2024 14:20:20 -0700 Subject: [PATCH 23/47] Let's keep it a separate channel for now and check in review --- .../android/io/flutter/embedding/android/FlutterView.java | 1 - .../android/io/flutter/plugin/editing/TextInputPlugin.java | 2 -- 2 files changed, 3 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index a85714c7ab39d..46f384d5354bc 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -1127,7 +1127,6 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) { new TextInputPlugin( this, this.flutterEngine.getTextInputChannel(), - // TODO(justinmc): This could just be part of TextInputChannel... this.flutterEngine.getScribeChannel(), this.flutterEngine.getPlatformViewsController()); diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 4d3d62c66417d..6b19d24d3263b 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -354,8 +354,6 @@ public InputConnection createInputConnection( InputConnectionAdaptor connection = new InputConnectionAdaptor( - // TODO(justinmc): scribeChannel could be part of textInputChannel - // instead of adding a new parameter here. view, inputTarget.id, textInputChannel, From 09625078efafc99d9376ef6a5c4837de0b54ec25 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 1 Oct 2024 15:32:45 -0700 Subject: [PATCH 24/47] ScribePluginTest --- .../plugin/editing/ScribePluginTest.java | 275 ++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java 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..5e775174d14a8 --- /dev/null +++ b/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java @@ -0,0 +1,275 @@ +package io.flutter.plugin.editing; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.view.textservice.SentenceSuggestionsInfo; +import android.view.textservice.SuggestionsInfo; +import android.view.textservice.TextInfo; +import android.view.textservice.TextServicesManager; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.systemchannels.ScribeChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.StandardMethodCodec; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Locale; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; + +@RunWith(AndroidJUnit4.class) +public class ScribePluginTest { + private final Context ctx = ApplicationProvider.getApplicationContext(); + + private static void sendToBinaryMessageHandler( + BinaryMessenger.BinaryMessageHandler binaryMessageHandler, String method) { + MethodCall methodCall = new MethodCall(method, null); + ByteBuffer encodedMethodCall = StandardMethodCodec.INSTANCE.encodeMethodCall(methodCall); + binaryMessageHandler.onMessage( + (ByteBuffer) encodedMethodCall.flip(), mock(BinaryMessenger.BinaryReply.class)); + } + + @SuppressWarnings("deprecation") + // setMessageHandler is deprecated. + @Test + public void respondsToSpellCheckChannelMessage() { + ArgumentCaptor binaryMessageHandlerCaptor = + ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class); + DartExecutor mockBinaryMessenger = mock(DartExecutor.class); + ScribeChannel.ScribeMethodHandler mockHandler = + mock(ScribeChannel.ScribeMethodHandler.class); + ScribeChannel scribeChannel = new ScribeChannel(mockBinaryMessenger); + + scribeChannel.setScribeMethodHandler(mockHandler); + + verify(mockBinaryMessenger, times(1)) + .setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture()); + + BinaryMessenger.BinaryMessageHandler binaryMessageHandler = + binaryMessageHandlerCaptor.getValue(); + + sendToBinaryMessageHandler( + binaryMessageHandler, + "Scribe.startStylusHandwriting"); + + verify(mockHandler) + .startStylusHandwriting(); + } + + /* + @Test + public void initiateSpellCheckPerformsSpellCheckWhenNoResultPending() { + ScribeChannel fakeScribeChannel = mock(ScribeChannel.class); + InputMethodSubtype inputMethodSubtype = mock(InputMethodSubtype.class); + TestImm testImm = Shadow.extract(ctx.getSystemService(Context.INPUT_METHOD_SERVICE)); + testImm.setCurrentInputMethodSubtype(inputMethodSubtype); + View testView = new View(ctx); + ScribePlugin scribePlugin = + spy(new ScribePlugin(testView, testImm, fakeScribeChannel)); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + SpellCheckerSession fakeSpellCheckerSession = mock(SpellCheckerSession.class); + + when(fakeTextServicesManager.newSpellCheckerSession( + null, new Locale("en", "US"), spellCheckPlugin, true)) + .thenReturn(fakeSpellCheckerSession); + + spellCheckPlugin.initiateSpellCheck("en-US", "Hello, wrold!", mockResult); + + verify(spellCheckPlugin).performSpellCheck("en-US", "Hello, wrold!"); + } + + @Test + public void initiateSpellCheckThrowsErrorWhenResultPending() { + SpellCheckChannel fakeSpellCheckChannel = mock(SpellCheckChannel.class); + TextServicesManager fakeTextServicesManager = mock(TextServicesManager.class); + SpellCheckPlugin spellCheckPlugin = + spy(new SpellCheckPlugin(fakeTextServicesManager, fakeSpellCheckChannel)); + MethodChannel.Result mockPendingResult = mock(MethodChannel.Result.class); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + spellCheckPlugin.pendingResult = mockPendingResult; + + spellCheckPlugin.initiateSpellCheck("en-US", "Hello, wrold!", mockResult); + + verify(mockResult).error("error", "Previous spell check request still pending.", null); + verify(spellCheckPlugin, never()).performSpellCheck("en-US", "Hello, wrold!"); + } + + @Test + public void destroyClosesSpellCheckerSessionAndClearsSpellCheckMethodHandler() { + Context fakeContext = mock(Context.class); + SpellCheckChannel fakeSpellCheckChannel = mock(SpellCheckChannel.class); + TextServicesManager fakeTextServicesManager = mock(TextServicesManager.class); + when(fakeContext.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE)) + .thenReturn(fakeTextServicesManager); + SpellCheckPlugin spellCheckPlugin = + spy(new SpellCheckPlugin(fakeTextServicesManager, fakeSpellCheckChannel)); + SpellCheckerSession fakeSpellCheckerSession = mock(SpellCheckerSession.class); + + when(fakeTextServicesManager.newSpellCheckerSession( + null, new Locale("en", "US"), spellCheckPlugin, true)) + .thenReturn(fakeSpellCheckerSession); + + spellCheckPlugin.performSpellCheck("en-US", "Hello, wrold!"); + spellCheckPlugin.destroy(); + + verify(fakeSpellCheckChannel).setSpellCheckMethodHandler(isNull()); + verify(fakeSpellCheckerSession).close(); + } + + @Test + public void performSpellCheckSendsRequestToAndroidSpellCheckService() { + Context fakeContext = mock(Context.class); + SpellCheckChannel fakeSpellCheckChannel = mock(SpellCheckChannel.class); + TextServicesManager fakeTextServicesManager = mock(TextServicesManager.class); + when(fakeContext.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE)) + .thenReturn(fakeTextServicesManager); + SpellCheckPlugin spellCheckPlugin = + spy(new SpellCheckPlugin(fakeTextServicesManager, fakeSpellCheckChannel)); + SpellCheckerSession fakeSpellCheckerSession = mock(SpellCheckerSession.class); + Locale english_US = new Locale("en", "US"); + + when(fakeTextServicesManager.newSpellCheckerSession(null, english_US, spellCheckPlugin, true)) + .thenReturn(fakeSpellCheckerSession); + + int maxSuggestions = 5; + ArgumentCaptor textInfosCaptor = ArgumentCaptor.forClass(TextInfo[].class); + ArgumentCaptor maxSuggestionsCaptor = ArgumentCaptor.forClass(Integer.class); + + spellCheckPlugin.performSpellCheck("en-US", "Hello, wrold!"); + + verify(fakeSpellCheckerSession) + .getSentenceSuggestions(textInfosCaptor.capture(), maxSuggestionsCaptor.capture()); + assertEquals("Hello, wrold!", textInfosCaptor.getValue()[0].getText()); + assertEquals(Integer.valueOf(maxSuggestions), maxSuggestionsCaptor.getValue()); + } + + @Test + public void performSpellCheckCreatesNewSpellCheckerSession() { + Context fakeContext = mock(Context.class); + SpellCheckChannel fakeSpellCheckChannel = mock(SpellCheckChannel.class); + TextServicesManager fakeTextServicesManager = mock(TextServicesManager.class); + when(fakeContext.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE)) + .thenReturn(fakeTextServicesManager); + SpellCheckPlugin spellCheckPlugin = + spy(new SpellCheckPlugin(fakeTextServicesManager, fakeSpellCheckChannel)); + SpellCheckerSession fakeSpellCheckerSession = mock(SpellCheckerSession.class); + Locale english_US = new Locale("en", "US"); + + when(fakeTextServicesManager.newSpellCheckerSession(null, english_US, spellCheckPlugin, true)) + .thenReturn(fakeSpellCheckerSession); + + spellCheckPlugin.performSpellCheck("en-US", "Hello, worl!"); + spellCheckPlugin.performSpellCheck("en-US", "Hello, world!"); + + verify(fakeTextServicesManager, times(1)) + .newSpellCheckerSession(null, english_US, spellCheckPlugin, true); + } + + @Test + public void onGetSentenceSuggestionsResultsWithSuccessAndNoResultsProperly() { + TextServicesManager fakeTextServicesManager = mock(TextServicesManager.class); + SpellCheckChannel fakeSpellCheckChannel = mock(SpellCheckChannel.class); + SpellCheckPlugin spellCheckPlugin = + spy(new SpellCheckPlugin(fakeTextServicesManager, fakeSpellCheckChannel)); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + spellCheckPlugin.pendingResult = mockResult; + + spellCheckPlugin.onGetSentenceSuggestions(new SentenceSuggestionsInfo[] {}); + + verify(mockResult).success(new ArrayList>()); + } + + @Test + public void onGetSentenceSuggestionsResultsWithSuccessAndResultsProperly() { + TextServicesManager fakeTextServicesManager = mock(TextServicesManager.class); + SpellCheckChannel fakeSpellCheckChannel = mock(SpellCheckChannel.class); + SpellCheckPlugin spellCheckPlugin = + spy(new SpellCheckPlugin(fakeTextServicesManager, fakeSpellCheckChannel)); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + spellCheckPlugin.pendingResult = mockResult; + + spellCheckPlugin.onGetSentenceSuggestions( + new SentenceSuggestionsInfo[] { + new SentenceSuggestionsInfo( + (new SuggestionsInfo[] { + new SuggestionsInfo( + SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO, + new String[] {"world", "word", "old"}) + }), + new int[] {7}, + new int[] {5}) + }); + + ArrayList> expectedResults = new ArrayList>(); + HashMap expectedResult = new HashMap(); + + expectedResult.put(SpellCheckPlugin.START_INDEX_KEY, 7); + expectedResult.put(SpellCheckPlugin.END_INDEX_KEY, 12); + expectedResult.put( + SpellCheckPlugin.SUGGESTIONS_KEY, + new ArrayList(Arrays.asList("world", "word", "old"))); + expectedResults.add(expectedResult); + + verify(mockResult).success(expectedResults); + } + + @Test + public void onGetSentenceSuggestionsResultsWithSuccessAndNoResultsWhenSuggestionsAreInvalid() { + TextServicesManager fakeTextServicesManager = mock(TextServicesManager.class); + SpellCheckChannel fakeSpellCheckChannel = mock(SpellCheckChannel.class); + SpellCheckPlugin spellCheckPlugin = + spy(new SpellCheckPlugin(fakeTextServicesManager, fakeSpellCheckChannel)); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + spellCheckPlugin.pendingResult = mockResult; + + spellCheckPlugin.onGetSentenceSuggestions( + new SentenceSuggestionsInfo[] { + new SentenceSuggestionsInfo( + (new SuggestionsInfo[] { + new SuggestionsInfo( + SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO, + // This is the suggestion that may be provided by the Samsung spell checker: + new String[] {""}) + }), + new int[] {7}, + new int[] {5}) + }); + + verify(mockResult).success(new ArrayList>()); + } + + @Test + public void onGetSentenceSuggestionsResultsWithSuccessAndNoResultsWhenSuggestionsAreInvalid2() { + TextServicesManager fakeTextServicesManager = mock(TextServicesManager.class); + SpellCheckChannel fakeSpellCheckChannel = mock(SpellCheckChannel.class); + SpellCheckPlugin spellCheckPlugin = + spy(new SpellCheckPlugin(fakeTextServicesManager, fakeSpellCheckChannel)); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + spellCheckPlugin.pendingResult = mockResult; + + spellCheckPlugin.onGetSentenceSuggestions( + new SentenceSuggestionsInfo[] { + // This "suggestion" may be provided by the Samsung spell checker: + null + }); + + verify(mockResult).success(new ArrayList>()); + } + */ +} From 885f4bb0dfe9809c1ee59a8dec7e80beadba7237 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 1 Oct 2024 16:17:41 -0700 Subject: [PATCH 25/47] Get tests working, and add basic tests for ScribePlugin --- .../editing/InputConnectionAdaptorTest.java | 42 +++- .../editing/ListenableEditingStateTest.java | 5 + .../plugin/editing/ScribePluginTest.java | 235 ++---------------- .../plugin/editing/TextInputPluginTest.java | 138 +++++++--- 4 files changed, 170 insertions(+), 250 deletions(-) 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 index 5e775174d14a8..8b2256be47c76 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java @@ -1,34 +1,19 @@ package io.flutter.plugin.editing; -import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.any; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.isNull; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import android.content.Context; -import android.view.textservice.SentenceSuggestionsInfo; -import android.view.textservice.SuggestionsInfo; -import android.view.textservice.TextInfo; -import android.view.textservice.TextServicesManager; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.systemchannels.ScribeChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.StandardMethodCodec; import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Locale; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -48,12 +33,11 @@ private static void sendToBinaryMessageHandler( @SuppressWarnings("deprecation") // setMessageHandler is deprecated. @Test - public void respondsToSpellCheckChannelMessage() { + public void respondsToStartStylusHandwriting() { ArgumentCaptor binaryMessageHandlerCaptor = ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class); DartExecutor mockBinaryMessenger = mock(DartExecutor.class); - ScribeChannel.ScribeMethodHandler mockHandler = - mock(ScribeChannel.ScribeMethodHandler.class); + ScribeChannel.ScribeMethodHandler mockHandler = mock(ScribeChannel.ScribeMethodHandler.class); ScribeChannel scribeChannel = new ScribeChannel(mockBinaryMessenger); scribeChannel.setScribeMethodHandler(mockHandler); @@ -64,212 +48,35 @@ public void respondsToSpellCheckChannelMessage() { BinaryMessenger.BinaryMessageHandler binaryMessageHandler = binaryMessageHandlerCaptor.getValue(); - sendToBinaryMessageHandler( - binaryMessageHandler, - "Scribe.startStylusHandwriting"); + sendToBinaryMessageHandler(binaryMessageHandler, "Scribe.startStylusHandwriting"); - verify(mockHandler) - .startStylusHandwriting(); - } - - /* - @Test - public void initiateSpellCheckPerformsSpellCheckWhenNoResultPending() { - ScribeChannel fakeScribeChannel = mock(ScribeChannel.class); - InputMethodSubtype inputMethodSubtype = mock(InputMethodSubtype.class); - TestImm testImm = Shadow.extract(ctx.getSystemService(Context.INPUT_METHOD_SERVICE)); - testImm.setCurrentInputMethodSubtype(inputMethodSubtype); - View testView = new View(ctx); - ScribePlugin scribePlugin = - spy(new ScribePlugin(testView, testImm, fakeScribeChannel)); - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - SpellCheckerSession fakeSpellCheckerSession = mock(SpellCheckerSession.class); - - when(fakeTextServicesManager.newSpellCheckerSession( - null, new Locale("en", "US"), spellCheckPlugin, true)) - .thenReturn(fakeSpellCheckerSession); - - spellCheckPlugin.initiateSpellCheck("en-US", "Hello, wrold!", mockResult); - - verify(spellCheckPlugin).performSpellCheck("en-US", "Hello, wrold!"); - } + verify(mockHandler).startStylusHandwriting(); - @Test - public void initiateSpellCheckThrowsErrorWhenResultPending() { - SpellCheckChannel fakeSpellCheckChannel = mock(SpellCheckChannel.class); - TextServicesManager fakeTextServicesManager = mock(TextServicesManager.class); - SpellCheckPlugin spellCheckPlugin = - spy(new SpellCheckPlugin(fakeTextServicesManager, fakeSpellCheckChannel)); - MethodChannel.Result mockPendingResult = mock(MethodChannel.Result.class); - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - spellCheckPlugin.pendingResult = mockPendingResult; - - spellCheckPlugin.initiateSpellCheck("en-US", "Hello, wrold!", mockResult); - - verify(mockResult).error("error", "Previous spell check request still pending.", null); - verify(spellCheckPlugin, never()).performSpellCheck("en-US", "Hello, wrold!"); + // TODO(justinmc): Ensure mImm.startStylusHandwriting was called. } + @SuppressWarnings("deprecation") + // setMessageHandler is deprecated. @Test - public void destroyClosesSpellCheckerSessionAndClearsSpellCheckMethodHandler() { - Context fakeContext = mock(Context.class); - SpellCheckChannel fakeSpellCheckChannel = mock(SpellCheckChannel.class); - TextServicesManager fakeTextServicesManager = mock(TextServicesManager.class); - when(fakeContext.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE)) - .thenReturn(fakeTextServicesManager); - SpellCheckPlugin spellCheckPlugin = - spy(new SpellCheckPlugin(fakeTextServicesManager, fakeSpellCheckChannel)); - SpellCheckerSession fakeSpellCheckerSession = mock(SpellCheckerSession.class); - - when(fakeTextServicesManager.newSpellCheckerSession( - null, new Locale("en", "US"), spellCheckPlugin, true)) - .thenReturn(fakeSpellCheckerSession); - - spellCheckPlugin.performSpellCheck("en-US", "Hello, wrold!"); - spellCheckPlugin.destroy(); - - verify(fakeSpellCheckChannel).setSpellCheckMethodHandler(isNull()); - verify(fakeSpellCheckerSession).close(); - } - - @Test - public void performSpellCheckSendsRequestToAndroidSpellCheckService() { - Context fakeContext = mock(Context.class); - SpellCheckChannel fakeSpellCheckChannel = mock(SpellCheckChannel.class); - TextServicesManager fakeTextServicesManager = mock(TextServicesManager.class); - when(fakeContext.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE)) - .thenReturn(fakeTextServicesManager); - SpellCheckPlugin spellCheckPlugin = - spy(new SpellCheckPlugin(fakeTextServicesManager, fakeSpellCheckChannel)); - SpellCheckerSession fakeSpellCheckerSession = mock(SpellCheckerSession.class); - Locale english_US = new Locale("en", "US"); - - when(fakeTextServicesManager.newSpellCheckerSession(null, english_US, spellCheckPlugin, true)) - .thenReturn(fakeSpellCheckerSession); - - int maxSuggestions = 5; - ArgumentCaptor textInfosCaptor = ArgumentCaptor.forClass(TextInfo[].class); - ArgumentCaptor maxSuggestionsCaptor = ArgumentCaptor.forClass(Integer.class); - - spellCheckPlugin.performSpellCheck("en-US", "Hello, wrold!"); - - verify(fakeSpellCheckerSession) - .getSentenceSuggestions(textInfosCaptor.capture(), maxSuggestionsCaptor.capture()); - assertEquals("Hello, wrold!", textInfosCaptor.getValue()[0].getText()); - assertEquals(Integer.valueOf(maxSuggestions), maxSuggestionsCaptor.getValue()); - } - - @Test - public void performSpellCheckCreatesNewSpellCheckerSession() { - Context fakeContext = mock(Context.class); - SpellCheckChannel fakeSpellCheckChannel = mock(SpellCheckChannel.class); - TextServicesManager fakeTextServicesManager = mock(TextServicesManager.class); - when(fakeContext.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE)) - .thenReturn(fakeTextServicesManager); - SpellCheckPlugin spellCheckPlugin = - spy(new SpellCheckPlugin(fakeTextServicesManager, fakeSpellCheckChannel)); - SpellCheckerSession fakeSpellCheckerSession = mock(SpellCheckerSession.class); - Locale english_US = new Locale("en", "US"); - - when(fakeTextServicesManager.newSpellCheckerSession(null, english_US, spellCheckPlugin, true)) - .thenReturn(fakeSpellCheckerSession); - - spellCheckPlugin.performSpellCheck("en-US", "Hello, worl!"); - spellCheckPlugin.performSpellCheck("en-US", "Hello, world!"); - - verify(fakeTextServicesManager, times(1)) - .newSpellCheckerSession(null, english_US, spellCheckPlugin, true); - } - - @Test - public void onGetSentenceSuggestionsResultsWithSuccessAndNoResultsProperly() { - TextServicesManager fakeTextServicesManager = mock(TextServicesManager.class); - SpellCheckChannel fakeSpellCheckChannel = mock(SpellCheckChannel.class); - SpellCheckPlugin spellCheckPlugin = - spy(new SpellCheckPlugin(fakeTextServicesManager, fakeSpellCheckChannel)); - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - spellCheckPlugin.pendingResult = mockResult; - - spellCheckPlugin.onGetSentenceSuggestions(new SentenceSuggestionsInfo[] {}); - - verify(mockResult).success(new ArrayList>()); - } - - @Test - public void onGetSentenceSuggestionsResultsWithSuccessAndResultsProperly() { - TextServicesManager fakeTextServicesManager = mock(TextServicesManager.class); - SpellCheckChannel fakeSpellCheckChannel = mock(SpellCheckChannel.class); - SpellCheckPlugin spellCheckPlugin = - spy(new SpellCheckPlugin(fakeTextServicesManager, fakeSpellCheckChannel)); - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - spellCheckPlugin.pendingResult = mockResult; - - spellCheckPlugin.onGetSentenceSuggestions( - new SentenceSuggestionsInfo[] { - new SentenceSuggestionsInfo( - (new SuggestionsInfo[] { - new SuggestionsInfo( - SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO, - new String[] {"world", "word", "old"}) - }), - new int[] {7}, - new int[] {5}) - }); - - ArrayList> expectedResults = new ArrayList>(); - HashMap expectedResult = new HashMap(); - - expectedResult.put(SpellCheckPlugin.START_INDEX_KEY, 7); - expectedResult.put(SpellCheckPlugin.END_INDEX_KEY, 12); - expectedResult.put( - SpellCheckPlugin.SUGGESTIONS_KEY, - new ArrayList(Arrays.asList("world", "word", "old"))); - expectedResults.add(expectedResult); - - verify(mockResult).success(expectedResults); - } + public void respondsToisStylusHandwritingAvailable() { + ArgumentCaptor binaryMessageHandlerCaptor = + ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class); + DartExecutor mockBinaryMessenger = mock(DartExecutor.class); + ScribeChannel.ScribeMethodHandler mockHandler = mock(ScribeChannel.ScribeMethodHandler.class); + ScribeChannel scribeChannel = new ScribeChannel(mockBinaryMessenger); - @Test - public void onGetSentenceSuggestionsResultsWithSuccessAndNoResultsWhenSuggestionsAreInvalid() { - TextServicesManager fakeTextServicesManager = mock(TextServicesManager.class); - SpellCheckChannel fakeSpellCheckChannel = mock(SpellCheckChannel.class); - SpellCheckPlugin spellCheckPlugin = - spy(new SpellCheckPlugin(fakeTextServicesManager, fakeSpellCheckChannel)); - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - spellCheckPlugin.pendingResult = mockResult; + scribeChannel.setScribeMethodHandler(mockHandler); - spellCheckPlugin.onGetSentenceSuggestions( - new SentenceSuggestionsInfo[] { - new SentenceSuggestionsInfo( - (new SuggestionsInfo[] { - new SuggestionsInfo( - SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO, - // This is the suggestion that may be provided by the Samsung spell checker: - new String[] {""}) - }), - new int[] {7}, - new int[] {5}) - }); + verify(mockBinaryMessenger, times(1)) + .setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture()); - verify(mockResult).success(new ArrayList>()); - } + BinaryMessenger.BinaryMessageHandler binaryMessageHandler = + binaryMessageHandlerCaptor.getValue(); - @Test - public void onGetSentenceSuggestionsResultsWithSuccessAndNoResultsWhenSuggestionsAreInvalid2() { - TextServicesManager fakeTextServicesManager = mock(TextServicesManager.class); - SpellCheckChannel fakeSpellCheckChannel = mock(SpellCheckChannel.class); - SpellCheckPlugin spellCheckPlugin = - spy(new SpellCheckPlugin(fakeTextServicesManager, fakeSpellCheckChannel)); - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - spellCheckPlugin.pendingResult = mockResult; + sendToBinaryMessageHandler(binaryMessageHandler, "Scribe.isStylusHandwritingAvailable"); - spellCheckPlugin.onGetSentenceSuggestions( - new SentenceSuggestionsInfo[] { - // This "suggestion" may be provided by the Samsung spell checker: - null - }); + verify(mockHandler).isStylusHandwritingAvailable(); - verify(mockResult).success(new ArrayList>()); + // TODO(justinmc): Ensure mImm.isStylusHandwritingAvailable was called. } - */ } 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..0dca2b6ebc0d9 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -54,6 +54,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 +134,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 +155,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 +199,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 +218,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 +270,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 +385,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 +516,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 +627,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 +739,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 +848,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 +941,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 +981,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 +1031,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 +1134,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 +1163,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 +1181,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 +1258,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 +1298,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 +1330,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 +1360,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( @@ -1359,8 +1400,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 +1462,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 +1501,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 +1548,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 +1642,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 +1697,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 +1781,7 @@ public void autofill_testLifeCycle() { testView, 0, mock(TextInputChannel.class), + mock(ScribeChannel.class), mockKeyboardManager, (ListenableEditingState) textInputPlugin.getEditable(), new EditorInfo()); @@ -1782,8 +1836,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 +1933,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 +1986,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 +2101,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 +2109,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 +2134,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 +2142,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 +2172,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 +2255,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 +2336,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)); From 8ef8f49b9b523b5ea0d887841750fd8a89b43daf Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 1 Oct 2024 16:21:09 -0700 Subject: [PATCH 26/47] License check fix --- ci/licenses_golden/licenses_flutter | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 90af1ef2d726a..f3b4f09c39dea 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -44102,6 +44102,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 @@ -44125,6 +44126,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 @@ -46978,6 +46980,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 @@ -47004,6 +47007,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 From 49fa99b51ff3a6d3a25c886b184ebccb561c19ff Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Wed, 2 Oct 2024 11:03:21 -0700 Subject: [PATCH 27/47] Test for ScribePlugin --- .../plugin/editing/ScribePluginTest.java | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java index 8b2256be47c76..cd458b3a6c5eb 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java @@ -1,11 +1,17 @@ package io.flutter.plugin.editing; +import static io.flutter.Build.API_LEVELS; +import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import android.annotation.TargetApi; import android.content.Context; +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.dart.DartExecutor; @@ -17,6 +23,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.robolectric.annotation.Config; @RunWith(AndroidJUnit4.class) public class ScribePluginTest { @@ -58,7 +65,7 @@ public void respondsToStartStylusHandwriting() { @SuppressWarnings("deprecation") // setMessageHandler is deprecated. @Test - public void respondsToisStylusHandwritingAvailable() { + public void respondsToIsStylusHandwritingAvailable() { ArgumentCaptor binaryMessageHandlerCaptor = ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class); DartExecutor mockBinaryMessenger = mock(DartExecutor.class); @@ -79,4 +86,21 @@ public void respondsToisStylusHandwritingAvailable() { // TODO(justinmc): Ensure mImm.isStylusHandwritingAvailable was called. } + + // TODO(justinmc): Maybe move the other tests to a ScribeChannel test file? + // TODO(justinmc): What happens when at another API level? + @Config(sdk = API_LEVELS.API_34) + @TargetApi(API_LEVELS.API_34) + @Test + public void scribePluginIsStylusHandwritingAvailable() { + ScribeChannel mockScribeChannel = mock(ScribeChannel.class); + View testView = new View(ctx); + InputMethodManager mockImm = mock(InputMethodManager.class); + when(mockImm.isStylusHandwritingAvailable()).thenReturn(true); + ScribePlugin scribePlugin = new ScribePlugin(testView, mockImm, mockScribeChannel); + + assertEquals(scribePlugin.isStylusHandwritingAvailable(), true); + + verify(mockImm).isStylusHandwritingAvailable(); + } } From 585ac33e2305a7c56bd9bacb2df685908bfad36c Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Wed, 2 Oct 2024 13:16:28 -0700 Subject: [PATCH 28/47] Enforce api levels --- .../android/io/flutter/plugin/editing/ScribePlugin.java | 4 ++++ .../test/io/flutter/plugin/editing/ScribePluginTest.java | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java index 186f3d537e940..f6cdc17ea1782 100644 --- a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java @@ -48,6 +48,8 @@ public void destroy() { * *

Call this before calling startStylusHandwriting to make sure it's available. */ + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) @Override public boolean isStylusHandwritingAvailable() { return mImm.isStylusHandwritingAvailable(); @@ -59,6 +61,8 @@ public boolean isStylusHandwritingAvailable() { *

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() { mImm.startStylusHandwriting(mView); diff --git a/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java index cd458b3a6c5eb..fdd423c723113 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java @@ -88,7 +88,6 @@ public void respondsToIsStylusHandwritingAvailable() { } // TODO(justinmc): Maybe move the other tests to a ScribeChannel test file? - // TODO(justinmc): What happens when at another API level? @Config(sdk = API_LEVELS.API_34) @TargetApi(API_LEVELS.API_34) @Test From 250adbc28c2ae7c2e27ea33a963c6286d62b8d94 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Wed, 2 Oct 2024 13:57:02 -0700 Subject: [PATCH 29/47] Separate Plugin and Channel tests --- .../flutter/plugin/editing/ScribePlugin.java | 4 + .../systemchannels/ScribeChannelTest.java | 77 +++++++++++++++++ .../plugin/editing/ScribePluginTest.java | 82 ++++--------------- 3 files changed, 98 insertions(+), 65 deletions(-) create mode 100644 shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java diff --git a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java index f6cdc17ea1782..70ff10fe0cf68 100644 --- a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java @@ -4,9 +4,13 @@ package io.flutter.plugin.editing; +import static io.flutter.Build.API_LEVELS; + +import android.annotation.TargetApi; import android.view.View; import android.view.inputmethod.InputMethodManager; import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; import io.flutter.embedding.engine.systemchannels.ScribeChannel; /** 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..a3e81ca6a9a93 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java @@ -0,0 +1,77 @@ +// 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 org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +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.MethodCall; +import io.flutter.plugin.common.StandardMethodCodec; +import java.nio.ByteBuffer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; + +@RunWith(AndroidJUnit4.class) +public class ScribeChannelTest { + private static void sendToBinaryMessageHandler( + BinaryMessenger.BinaryMessageHandler binaryMessageHandler, String method) { + MethodCall methodCall = new MethodCall(method, null); + ByteBuffer encodedMethodCall = StandardMethodCodec.INSTANCE.encodeMethodCall(methodCall); + binaryMessageHandler.onMessage( + (ByteBuffer) encodedMethodCall.flip(), mock(BinaryMessenger.BinaryReply.class)); + } + + @SuppressWarnings("deprecation") + // setMessageHandler is deprecated. + @Test + public void respondsToStartStylusHandwriting() { + ArgumentCaptor binaryMessageHandlerCaptor = + ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class); + DartExecutor mockBinaryMessenger = mock(DartExecutor.class); + ScribeChannel.ScribeMethodHandler mockHandler = mock(ScribeChannel.ScribeMethodHandler.class); + ScribeChannel scribeChannel = new ScribeChannel(mockBinaryMessenger); + + scribeChannel.setScribeMethodHandler(mockHandler); + + verify(mockBinaryMessenger, times(1)) + .setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture()); + + BinaryMessenger.BinaryMessageHandler binaryMessageHandler = + binaryMessageHandlerCaptor.getValue(); + + sendToBinaryMessageHandler(binaryMessageHandler, "Scribe.startStylusHandwriting"); + + verify(mockHandler).startStylusHandwriting(); + } + + @SuppressWarnings("deprecation") + // setMessageHandler is deprecated. + @Test + public void respondsToIsStylusHandwritingAvailable() { + ArgumentCaptor binaryMessageHandlerCaptor = + ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class); + DartExecutor mockBinaryMessenger = mock(DartExecutor.class); + ScribeChannel.ScribeMethodHandler mockHandler = mock(ScribeChannel.ScribeMethodHandler.class); + ScribeChannel scribeChannel = new ScribeChannel(mockBinaryMessenger); + + scribeChannel.setScribeMethodHandler(mockHandler); + + verify(mockBinaryMessenger, times(1)) + .setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture()); + + BinaryMessenger.BinaryMessageHandler binaryMessageHandler = + binaryMessageHandlerCaptor.getValue(); + + sendToBinaryMessageHandler(binaryMessageHandler, "Scribe.isStylusHandwritingAvailable"); + + verify(mockHandler).isStylusHandwritingAvailable(); + } +} diff --git a/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java index fdd423c723113..1965a28731d4b 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java @@ -1,10 +1,12 @@ +// 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.mockito.Mockito.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -14,92 +16,42 @@ import android.view.inputmethod.InputMethodManager; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.systemchannels.ScribeChannel; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.StandardMethodCodec; -import java.nio.ByteBuffer; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; import org.robolectric.annotation.Config; @RunWith(AndroidJUnit4.class) public class ScribePluginTest { private final Context ctx = ApplicationProvider.getApplicationContext(); - private static void sendToBinaryMessageHandler( - BinaryMessenger.BinaryMessageHandler binaryMessageHandler, String method) { - MethodCall methodCall = new MethodCall(method, null); - ByteBuffer encodedMethodCall = StandardMethodCodec.INSTANCE.encodeMethodCall(methodCall); - binaryMessageHandler.onMessage( - (ByteBuffer) encodedMethodCall.flip(), mock(BinaryMessenger.BinaryReply.class)); - } - - @SuppressWarnings("deprecation") - // setMessageHandler is deprecated. - @Test - public void respondsToStartStylusHandwriting() { - ArgumentCaptor binaryMessageHandlerCaptor = - ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class); - DartExecutor mockBinaryMessenger = mock(DartExecutor.class); - ScribeChannel.ScribeMethodHandler mockHandler = mock(ScribeChannel.ScribeMethodHandler.class); - ScribeChannel scribeChannel = new ScribeChannel(mockBinaryMessenger); - - scribeChannel.setScribeMethodHandler(mockHandler); - - verify(mockBinaryMessenger, times(1)) - .setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture()); - - BinaryMessenger.BinaryMessageHandler binaryMessageHandler = - binaryMessageHandlerCaptor.getValue(); - - sendToBinaryMessageHandler(binaryMessageHandler, "Scribe.startStylusHandwriting"); - - verify(mockHandler).startStylusHandwriting(); - - // TODO(justinmc): Ensure mImm.startStylusHandwriting was called. - } - - @SuppressWarnings("deprecation") - // setMessageHandler is deprecated. + @Config(sdk = API_LEVELS.API_34) + @TargetApi(API_LEVELS.API_34) @Test - public void respondsToIsStylusHandwritingAvailable() { - ArgumentCaptor binaryMessageHandlerCaptor = - ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class); - DartExecutor mockBinaryMessenger = mock(DartExecutor.class); - ScribeChannel.ScribeMethodHandler mockHandler = mock(ScribeChannel.ScribeMethodHandler.class); - ScribeChannel scribeChannel = new ScribeChannel(mockBinaryMessenger); - - scribeChannel.setScribeMethodHandler(mockHandler); - - verify(mockBinaryMessenger, times(1)) - .setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture()); - - BinaryMessenger.BinaryMessageHandler binaryMessageHandler = - binaryMessageHandlerCaptor.getValue(); - - sendToBinaryMessageHandler(binaryMessageHandler, "Scribe.isStylusHandwritingAvailable"); + public void scribePluginIsStylusHandwritingAvailable() { + ScribeChannel mockScribeChannel = mock(ScribeChannel.class); + View testView = new View(ctx); + InputMethodManager mockImm = mock(InputMethodManager.class); + when(mockImm.isStylusHandwritingAvailable()).thenReturn(true); + ScribePlugin scribePlugin = new ScribePlugin(testView, mockImm, mockScribeChannel); - verify(mockHandler).isStylusHandwritingAvailable(); + assertEquals(scribePlugin.isStylusHandwritingAvailable(), true); - // TODO(justinmc): Ensure mImm.isStylusHandwritingAvailable was called. + verify(mockImm).isStylusHandwritingAvailable(); } - // TODO(justinmc): Maybe move the other tests to a ScribeChannel test file? @Config(sdk = API_LEVELS.API_34) @TargetApi(API_LEVELS.API_34) @Test - public void scribePluginIsStylusHandwritingAvailable() { + public void scribePluginStartStylusHandwriting() { ScribeChannel mockScribeChannel = mock(ScribeChannel.class); View testView = new View(ctx); InputMethodManager mockImm = mock(InputMethodManager.class); when(mockImm.isStylusHandwritingAvailable()).thenReturn(true); ScribePlugin scribePlugin = new ScribePlugin(testView, mockImm, mockScribeChannel); - assertEquals(scribePlugin.isStylusHandwritingAvailable(), true); + scribePlugin.startStylusHandwriting(); - verify(mockImm).isStylusHandwritingAvailable(); + verify(mockImm).startStylusHandwriting(testView); } } From e96d8bc47f7d52772a8f7996296ed31e9585a968 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Wed, 2 Oct 2024 15:51:37 -0700 Subject: [PATCH 30/47] Only start scribe when api available --- .../android/io/flutter/plugin/editing/TextInputPlugin.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 6b19d24d3263b..41cd19ed32fc4 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -346,7 +346,9 @@ public InputConnection createInputConnection( EditorInfoCompat.setContentMimeTypes(outAttrs, imgTypeString); } - EditorInfoCompat.setStylusHandwritingEnabled(outAttrs, true); + 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. From 8056d170c06e276bc2e515971ed4592032b78dcd Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Wed, 2 Oct 2024 16:02:26 -0700 Subject: [PATCH 31/47] Test for TextInputPlugin change --- .../plugin/editing/TextInputPluginTest.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) 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 0dca2b6ebc0d9..35969e6ab16f9 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -45,6 +45,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; @@ -1392,6 +1393,40 @@ public void inputConnection_textInputTypeMultilineAndSuggestionsDisabled() { | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); } + @Config(sdk = 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)); + } + // -------- Start: Autofill Tests ------- @Test public void autofill_enabledByDefault() { From 016223e3dbbecd4e174059ccb5dd9b95a02d86db Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Thu, 3 Oct 2024 10:24:17 -0700 Subject: [PATCH 32/47] Add missing api_level guards --- .../io/flutter/plugin/editing/InputConnectionAdaptor.java | 2 ++ .../android/io/flutter/plugin/editing/ScribePlugin.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index dace128696163..8d2af2d19dd28 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -280,6 +280,8 @@ public boolean setSelection(int start, int end) { // TODO(justinmc): Scribe stylus gestures should be supported here. // https://github.com/flutter/flutter/issues/156018 + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) @Override public void performHandwritingGesture( HandwritingGesture gesture, Executor executor, IntConsumer consumer) { diff --git a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java index 70ff10fe0cf68..fa69d20c16f52 100644 --- a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java @@ -26,6 +26,8 @@ public class ScribePlugin implements ScribeChannel.ScribeMethodHandler { private final InputMethodManager mImm; @NonNull private final View mView; + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) public ScribePlugin( @NonNull View view, @NonNull InputMethodManager imm, @NonNull ScribeChannel scribeChannel) { view.setAutoHandwritingEnabled(false); From cc6969029ffb2c57da902b3a7929886eed87ecaf Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Thu, 3 Oct 2024 15:54:28 -0700 Subject: [PATCH 33/47] Move method channel handlers to their own private methods --- .../engine/systemchannels/ScribeChannel.java | 32 ++++++++++++------- 1 file changed, 20 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 ad36bcf5288db..120bcace44a24 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java @@ -36,20 +36,10 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result Log.v(TAG, "Received '" + method + "' message."); switch (method) { case "Scribe.isStylusHandwritingAvailable": - try { - final boolean isAvailable = scribeMethodHandler.isStylusHandwritingAvailable(); - result.success(isAvailable); - } catch (IllegalStateException exception) { - result.error("error", exception.getMessage(), null); - } + isStylusHandwritingAvailable(call, result); break; case "Scribe.startStylusHandwriting": - try { - scribeMethodHandler.startStylusHandwriting(); - result.success(null); - } catch (IllegalStateException exception) { - result.error("error", exception.getMessage(), null); - } + startStylusHandwriting(call, result); break; default: result.notImplemented(); @@ -58,6 +48,24 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result } }; + private void isStylusHandwritingAvailable(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + 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) { + 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", StandardMethodCodec.INSTANCE); channel.setMethodCallHandler(parsingMethodHandler); From 6f1a6d8951045bbfff0b16174ebefa8631bef6d0 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Thu, 3 Oct 2024 16:26:45 -0700 Subject: [PATCH 34/47] Misc Reid's review comments --- .../engine/systemchannels/ScribeChannel.java | 22 ++++++++++- .../editing/InputConnectionAdaptor.java | 13 +------ .../flutter/plugin/editing/ScribePlugin.java | 8 ++-- .../systemchannels/ScribeChannelTest.java | 37 +++++++------------ .../plugin/editing/ScribePluginTest.java | 30 ++++++++------- .../plugin/editing/TextInputPluginTest.java | 2 +- 6 files changed, 57 insertions(+), 55 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 120bcace44a24..f9b79c72b8247 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java @@ -4,8 +4,13 @@ package io.flutter.embedding.engine.systemchannels; +import static io.flutter.Build.API_LEVELS; + +import android.annotation.TargetApi; 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.MethodCall; @@ -19,6 +24,13 @@ public class ScribeChannel { private static final String TAG = "ScribeChannel"; + @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; @@ -48,7 +60,8 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result } }; - private void isStylusHandwritingAvailable(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + private void isStylusHandwritingAvailable( + @NonNull MethodCall call, @NonNull MethodChannel.Result result) { try { final boolean isAvailable = scribeMethodHandler.isStylusHandwritingAvailable(); result.success(isAvailable); @@ -57,7 +70,8 @@ private void isStylusHandwritingAvailable(@NonNull MethodCall call, @NonNull Met } } - private void startStylusHandwriting(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + private void startStylusHandwriting( + @NonNull MethodCall call, @NonNull MethodChannel.Result result) { try { scribeMethodHandler.startStylusHandwriting(); result.success(null); @@ -84,12 +98,16 @@ public interface ScribeMethodHandler { * Responds to the {@code result} with success and a boolean indicating whether or not stylus * hadnwriting 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(); } diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index 8d2af2d19dd28..aa96ea02645c9 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -26,7 +26,6 @@ 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; @@ -42,8 +41,6 @@ import java.io.InputStream; 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 { @@ -278,15 +275,9 @@ public boolean setSelection(int start, int end) { return result; } - // TODO(justinmc): Scribe stylus gestures should be supported here. + // TODO(justinmc): Scribe stylus gestures should be supported here via + // performHandwritingGesture. // https://github.com/flutter/flutter/issues/156018 - @TargetApi(API_LEVELS.API_34) - @RequiresApi(API_LEVELS.API_34) - @Override - public void performHandwritingGesture( - HandwritingGesture gesture, Executor executor, IntConsumer consumer) { - executor.execute(() -> consumer.accept(HANDWRITING_GESTURE_RESULT_UNSUPPORTED)); - } // Sanitizes the index to ensure the index is within the range of the // contents of editable. diff --git a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java index fa69d20c16f52..4308e36a80a7f 100644 --- a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java @@ -23,7 +23,7 @@ public class ScribePlugin implements ScribeChannel.ScribeMethodHandler { private final ScribeChannel mScribeChannel; - private final InputMethodManager mImm; + private final InputMethodManager mInputMethodManager; @NonNull private final View mView; @TargetApi(API_LEVELS.API_34) @@ -33,7 +33,7 @@ public ScribePlugin( view.setAutoHandwritingEnabled(false); mView = view; - mImm = imm; + mInputMethodManager = imm; mScribeChannel = scribeChannel; mScribeChannel.setScribeMethodHandler(this); @@ -58,7 +58,7 @@ public void destroy() { @RequiresApi(API_LEVELS.API_34) @Override public boolean isStylusHandwritingAvailable() { - return mImm.isStylusHandwritingAvailable(); + return mInputMethodManager.isStylusHandwritingAvailable(); } /** @@ -71,6 +71,6 @@ public boolean isStylusHandwritingAvailable() { @RequiresApi(API_LEVELS.API_33) @Override public void startStylusHandwriting() { - mImm.startStylusHandwriting(mView); + mInputMethodManager.startStylusHandwriting(mView); } } 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 index a3e81ca6a9a93..06e6a2b0c5908 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java @@ -15,6 +15,7 @@ import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.StandardMethodCodec; import java.nio.ByteBuffer; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -29,14 +30,17 @@ private static void sendToBinaryMessageHandler( (ByteBuffer) encodedMethodCall.flip(), mock(BinaryMessenger.BinaryReply.class)); } + ScribeChannel.ScribeMethodHandler mockHandler; + BinaryMessenger.BinaryMessageHandler binaryMessageHandler; + @SuppressWarnings("deprecation") // setMessageHandler is deprecated. - @Test - public void respondsToStartStylusHandwriting() { + @Before + public void setUp() { ArgumentCaptor binaryMessageHandlerCaptor = ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class); DartExecutor mockBinaryMessenger = mock(DartExecutor.class); - ScribeChannel.ScribeMethodHandler mockHandler = mock(ScribeChannel.ScribeMethodHandler.class); + mockHandler = mock(ScribeChannel.ScribeMethodHandler.class); ScribeChannel scribeChannel = new ScribeChannel(mockBinaryMessenger); scribeChannel.setScribeMethodHandler(mockHandler); @@ -44,33 +48,20 @@ public void respondsToStartStylusHandwriting() { verify(mockBinaryMessenger, times(1)) .setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture()); - BinaryMessenger.BinaryMessageHandler binaryMessageHandler = - binaryMessageHandlerCaptor.getValue(); + binaryMessageHandler = binaryMessageHandlerCaptor.getValue(); + } - sendToBinaryMessageHandler(binaryMessageHandler, "Scribe.startStylusHandwriting"); + @Test + public void respondsToStartStylusHandwriting() { + sendToBinaryMessageHandler(binaryMessageHandler, ScribeChannel.METHOD_START_STYLUS_HANDWRITING); verify(mockHandler).startStylusHandwriting(); } - @SuppressWarnings("deprecation") - // setMessageHandler is deprecated. @Test public void respondsToIsStylusHandwritingAvailable() { - ArgumentCaptor binaryMessageHandlerCaptor = - ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class); - DartExecutor mockBinaryMessenger = mock(DartExecutor.class); - ScribeChannel.ScribeMethodHandler mockHandler = mock(ScribeChannel.ScribeMethodHandler.class); - ScribeChannel scribeChannel = new ScribeChannel(mockBinaryMessenger); - - scribeChannel.setScribeMethodHandler(mockHandler); - - verify(mockBinaryMessenger, times(1)) - .setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture()); - - BinaryMessenger.BinaryMessageHandler binaryMessageHandler = - binaryMessageHandlerCaptor.getValue(); - - sendToBinaryMessageHandler(binaryMessageHandler, "Scribe.isStylusHandwritingAvailable"); + sendToBinaryMessageHandler( + binaryMessageHandler, ScribeChannel.METHOD_IS_STYLUS_HANDWRITING_AVAILABLE); verify(mockHandler).isStylusHandwritingAvailable(); } diff --git a/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java index 1965a28731d4b..63f0d3842dbd8 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java @@ -17,6 +17,7 @@ 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; @@ -25,31 +26,32 @@ public class ScribePluginTest { private final Context ctx = ApplicationProvider.getApplicationContext(); - @Config(sdk = API_LEVELS.API_34) - @TargetApi(API_LEVELS.API_34) - @Test - public void scribePluginIsStylusHandwritingAvailable() { + ScribePlugin scribePlugin; + InputMethodManager mockImm; + View testView; + + @Before + public void setUp() { ScribeChannel mockScribeChannel = mock(ScribeChannel.class); - View testView = new View(ctx); - InputMethodManager mockImm = mock(InputMethodManager.class); + testView = new View(ctx); + mockImm = mock(InputMethodManager.class); when(mockImm.isStylusHandwritingAvailable()).thenReturn(true); - ScribePlugin scribePlugin = new ScribePlugin(testView, mockImm, mockScribeChannel); + scribePlugin = new ScribePlugin(testView, mockImm, mockScribeChannel); + } + @Config(minSdk = API_LEVELS.API_34) + @TargetApi(API_LEVELS.API_34) + @Test + public void scribePluginIsStylusHandwritingAvailable() { assertEquals(scribePlugin.isStylusHandwritingAvailable(), true); verify(mockImm).isStylusHandwritingAvailable(); } - @Config(sdk = API_LEVELS.API_34) + @Config(minSdk = API_LEVELS.API_34) @TargetApi(API_LEVELS.API_34) @Test public void scribePluginStartStylusHandwriting() { - ScribeChannel mockScribeChannel = mock(ScribeChannel.class); - View testView = new View(ctx); - InputMethodManager mockImm = mock(InputMethodManager.class); - when(mockImm.isStylusHandwritingAvailable()).thenReturn(true); - ScribePlugin scribePlugin = new ScribePlugin(testView, mockImm, mockScribeChannel); - scribePlugin.startStylusHandwriting(); verify(mockImm).startStylusHandwriting(testView); 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 35969e6ab16f9..f07b73d042451 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -1393,7 +1393,7 @@ public void inputConnection_textInputTypeMultilineAndSuggestionsDisabled() { | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); } - @Config(sdk = API_LEVELS.API_34) + @Config(minSdk = API_LEVELS.API_34) @TargetApi(API_LEVELS.API_34) @Test public void inputConnection_setsStylusHandwritingAvailable() { From 1ae66f11d92e7c7e5dfc189735f3d3bcf6442638 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Fri, 4 Oct 2024 10:51:58 -0700 Subject: [PATCH 35/47] Test unsupported api levels in scribeplugintest. Allow it to be created when unsupported. --- .../embedding/android/FlutterView.java | 8 ++--- .../flutter/plugin/editing/ScribePlugin.java | 13 +++++--- .../plugin/editing/ScribePluginTest.java | 33 ++++++++++++++++++- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 46f384d5354bc..3f3f0470d8f54 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -1140,11 +1140,9 @@ 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()); - } + scribePlugin = + new ScribePlugin( + this, textInputPlugin.getInputMethodManager(), this.flutterEngine.getScribeChannel()); localizationPlugin = this.flutterEngine.getLocalizationPlugin(); diff --git a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java index 4308e36a80a7f..ccb476d061573 100644 --- a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java @@ -7,6 +7,7 @@ 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; @@ -19,18 +20,20 @@ * *

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 { - private final ScribeChannel mScribeChannel; - private final InputMethodManager mInputMethodManager; + @NonNull private final ScribeChannel mScribeChannel; + @NonNull private final InputMethodManager mInputMethodManager; @NonNull private final View mView; - @TargetApi(API_LEVELS.API_34) - @RequiresApi(API_LEVELS.API_34) public ScribePlugin( @NonNull View view, @NonNull InputMethodManager imm, @NonNull ScribeChannel scribeChannel) { - view.setAutoHandwritingEnabled(false); + if (Build.VERSION.SDK_INT >= API_LEVELS.API_33) { + view.setAutoHandwritingEnabled(false); + } mView = view; mInputMethodManager = imm; diff --git a/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java index 63f0d3842dbd8..aac053ed263bf 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java @@ -6,12 +6,15 @@ 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; @@ -35,7 +38,9 @@ public void setUp() { ScribeChannel mockScribeChannel = mock(ScribeChannel.class); testView = new View(ctx); mockImm = mock(InputMethodManager.class); - when(mockImm.isStylusHandwritingAvailable()).thenReturn(true); + if (Build.VERSION.SDK_INT >= API_LEVELS.API_34) { + when(mockImm.isStylusHandwritingAvailable()).thenReturn(true); + } scribePlugin = new ScribePlugin(testView, mockImm, mockScribeChannel); } @@ -56,4 +61,30 @@ public void scribePluginStartStylusHandwriting() { verify(mockImm).startStylusHandwriting(testView); } + + @Config(sdk = API_LEVELS.API_33) + @TargetApi(API_LEVELS.API_33) + @Test + public void scribePluginStartStylusHandwritingWhenAPILevelUnsupported() { + assertNotNull(scribePlugin); + + assertThrows( + NoSuchMethodError.class, + () -> { + scribePlugin.startStylusHandwriting(); + }); + } + + @Config(sdk = API_LEVELS.API_32) + @TargetApi(API_LEVELS.API_32) + @Test + public void scribePluginIsStylusHandwritingAvailableWhenAPILevelUnsupported() { + assertNotNull(scribePlugin); + + assertThrows( + NoSuchMethodError.class, + () -> { + scribePlugin.isStylusHandwritingAvailable(); + }); + } } From f0198413b8a14b81e370dba1a3061d340bc6e88f Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Fri, 4 Oct 2024 12:43:33 -0700 Subject: [PATCH 36/47] Some work on testing api versions in ScribeChannel --- .../systemchannels/ScribeChannelTest.java | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) 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 index 06e6a2b0c5908..47f936ed9c549 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java @@ -4,11 +4,13 @@ package io.flutter.embedding.engine.systemchannels; +import static io.flutter.Build.API_LEVELS; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; 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; @@ -19,15 +21,17 @@ 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 void sendToBinaryMessageHandler( + private static BinaryMessenger.BinaryReply sendToBinaryMessageHandler( BinaryMessenger.BinaryMessageHandler binaryMessageHandler, String method) { MethodCall methodCall = new MethodCall(method, null); ByteBuffer encodedMethodCall = StandardMethodCodec.INSTANCE.encodeMethodCall(methodCall); - binaryMessageHandler.onMessage( - (ByteBuffer) encodedMethodCall.flip(), mock(BinaryMessenger.BinaryReply.class)); + BinaryMessenger.BinaryReply mockReply = mock(BinaryMessenger.BinaryReply.class); + binaryMessageHandler.onMessage((ByteBuffer) encodedMethodCall.flip(), mockReply); + return mockReply; } ScribeChannel.ScribeMethodHandler mockHandler; @@ -51,6 +55,8 @@ public void setUp() { binaryMessageHandler = binaryMessageHandlerCaptor.getValue(); } + @Config(minSdk = API_LEVELS.API_34) + @TargetApi(API_LEVELS.API_34) @Test public void respondsToStartStylusHandwriting() { sendToBinaryMessageHandler(binaryMessageHandler, ScribeChannel.METHOD_START_STYLUS_HANDWRITING); @@ -58,6 +64,8 @@ public void respondsToStartStylusHandwriting() { verify(mockHandler).startStylusHandwriting(); } + @Config(minSdk = API_LEVELS.API_34) + @TargetApi(API_LEVELS.API_34) @Test public void respondsToIsStylusHandwritingAvailable() { sendToBinaryMessageHandler( @@ -65,4 +73,22 @@ public void respondsToIsStylusHandwritingAvailable() { verify(mockHandler).isStylusHandwritingAvailable(); } + + /* + @Config(sdk = API_LEVELS.API_32) + @TargetApi(API_LEVELS.API_32) + @Test + public void respondsToIsStylusHandwritingAvailableWhenAPILevelUnsupported() { + BinaryMessenger.BinaryReply mockReply = sendToBinaryMessageHandler( + binaryMessageHandler, ScribeChannel.METHOD_IS_STYLUS_HANDWRITING_AVAILABLE); + + // TODO(justinmc): reply thing is binary, which is hard to read. Could you + // use BasicMessageChannel or something instead? Or, follow Reid's latest + // comment and come back to this. + //verify(mockReply).reply(null); + verify(mockReply).reply("error"); + + verify(mockHandler).isStylusHandwritingAvailable(); + } + */ } From d802eb2de0489beff49f0245c72179442d472473 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Fri, 4 Oct 2024 15:13:59 -0700 Subject: [PATCH 37/47] Works without deprecated method --- .../embedding/engine/systemchannels/ScribeChannel.java | 4 ++-- .../embedding/engine/systemchannels/ScribeChannelTest.java | 5 ++--- 2 files changed, 4 insertions(+), 5 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 f9b79c72b8247..e6a8856ae02e0 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java @@ -47,10 +47,10 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result Object args = call.arguments; Log.v(TAG, "Received '" + method + "' message."); switch (method) { - case "Scribe.isStylusHandwritingAvailable": + case METHOD_IS_STYLUS_HANDWRITING_AVAILABLE: isStylusHandwritingAvailable(call, result); break; - case "Scribe.startStylusHandwriting": + case METHOD_START_STYLUS_HANDWRITING: startStylusHandwriting(call, result); break; default: 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 index 47f936ed9c549..360aaf70914e6 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java @@ -37,8 +37,6 @@ private static BinaryMessenger.BinaryReply sendToBinaryMessageHandler( ScribeChannel.ScribeMethodHandler mockHandler; BinaryMessenger.BinaryMessageHandler binaryMessageHandler; - @SuppressWarnings("deprecation") - // setMessageHandler is deprecated. @Before public void setUp() { ArgumentCaptor binaryMessageHandlerCaptor = @@ -49,7 +47,7 @@ public void setUp() { scribeChannel.setScribeMethodHandler(mockHandler); - verify(mockBinaryMessenger, times(1)) + verify((BinaryMessenger) mockBinaryMessenger, times(1)) .setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture()); binaryMessageHandler = binaryMessageHandlerCaptor.getValue(); @@ -68,6 +66,7 @@ public void respondsToStartStylusHandwriting() { @TargetApi(API_LEVELS.API_34) @Test public void respondsToIsStylusHandwritingAvailable() { + sendToBinaryMessageHandler( binaryMessageHandler, ScribeChannel.METHOD_IS_STYLUS_HANDWRITING_AVAILABLE); From 3685c9df0f6a512b358c8c28245b62258f8054b5 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Fri, 4 Oct 2024 16:28:22 -0700 Subject: [PATCH 38/47] Test ScribeChannel when old api level I wish I could test the result... --- .../engine/systemchannels/ScribeChannel.java | 11 +++++ .../systemchannels/ScribeChannelTest.java | 41 ++++++++++++------- 2 files changed, 37 insertions(+), 15 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 e6a8856ae02e0..f64d4f7516715 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java @@ -7,6 +7,7 @@ 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; @@ -62,6 +63,11 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result 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); @@ -72,6 +78,11 @@ private void isStylusHandwritingAvailable( 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); 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 index 360aaf70914e6..13c65674e89c6 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java @@ -7,6 +7,7 @@ import static io.flutter.Build.API_LEVELS; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -57,8 +58,11 @@ public void setUp() { @TargetApi(API_LEVELS.API_34) @Test public void respondsToStartStylusHandwriting() { - sendToBinaryMessageHandler(binaryMessageHandler, ScribeChannel.METHOD_START_STYLUS_HANDWRITING); + BinaryMessenger.BinaryReply mockReply = + sendToBinaryMessageHandler( + binaryMessageHandler, ScribeChannel.METHOD_START_STYLUS_HANDWRITING); + verify(mockReply).reply(any(ByteBuffer.class)); verify(mockHandler).startStylusHandwriting(); } @@ -66,28 +70,35 @@ public void respondsToStartStylusHandwriting() { @TargetApi(API_LEVELS.API_34) @Test public void respondsToIsStylusHandwritingAvailable() { + BinaryMessenger.BinaryReply mockReply = + sendToBinaryMessageHandler( + binaryMessageHandler, ScribeChannel.METHOD_IS_STYLUS_HANDWRITING_AVAILABLE); - sendToBinaryMessageHandler( - binaryMessageHandler, ScribeChannel.METHOD_IS_STYLUS_HANDWRITING_AVAILABLE); - + verify(mockReply).reply(any(ByteBuffer.class)); verify(mockHandler).isStylusHandwritingAvailable(); } - /* @Config(sdk = API_LEVELS.API_32) @TargetApi(API_LEVELS.API_32) @Test - public void respondsToIsStylusHandwritingAvailableWhenAPILevelUnsupported() { - BinaryMessenger.BinaryReply mockReply = sendToBinaryMessageHandler( - binaryMessageHandler, ScribeChannel.METHOD_IS_STYLUS_HANDWRITING_AVAILABLE); + public void respondsToStartStylusHandwritingWhenAPILevelUnsupported() { + BinaryMessenger.BinaryReply mockReply = + sendToBinaryMessageHandler( + binaryMessageHandler, ScribeChannel.METHOD_START_STYLUS_HANDWRITING); - // TODO(justinmc): reply thing is binary, which is hard to read. Could you - // use BasicMessageChannel or something instead? Or, follow Reid's latest - // comment and come back to this. - //verify(mockReply).reply(null); - verify(mockReply).reply("error"); + verify(mockReply).reply(any(ByteBuffer.class)); + verify(mockHandler, never()).startStylusHandwriting(); + } - verify(mockHandler).isStylusHandwritingAvailable(); + @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(any(ByteBuffer.class)); + verify(mockHandler, never()).isStylusHandwritingAvailable(); } - */ } From 0ceab93a7aa0ed995293b592976775887ccb3169 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Fri, 4 Oct 2024 16:29:54 -0700 Subject: [PATCH 39/47] Make view public so it could be updated if the view changes --- .../android/io/flutter/plugin/editing/ScribePlugin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java index ccb476d061573..09c6e3e23d02d 100644 --- a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java @@ -27,7 +27,7 @@ public class ScribePlugin implements ScribeChannel.ScribeMethodHandler { @NonNull private final ScribeChannel mScribeChannel; @NonNull private final InputMethodManager mInputMethodManager; - @NonNull private final View mView; + @NonNull public View mView; public ScribePlugin( @NonNull View view, @NonNull InputMethodManager imm, @NonNull ScribeChannel scribeChannel) { From bc4dafd3888aca08f7846e431986e2aebaf68b07 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Fri, 4 Oct 2024 16:42:40 -0700 Subject: [PATCH 40/47] TestInputPlugin unsupported test --- .../plugin/editing/TextInputPluginTest.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) 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 f07b73d042451..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; @@ -1427,6 +1428,40 @@ public void inputConnection_setsStylusHandwritingAvailable() { 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() { From 00621e20d4aaf01ed142ea1aeeb52504ff5623fa Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Mon, 7 Oct 2024 11:11:31 -0700 Subject: [PATCH 41/47] Private but settable view --- .../io/flutter/plugin/editing/ScribePlugin.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java index 09c6e3e23d02d..d02d6cda2f0b3 100644 --- a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java @@ -27,7 +27,7 @@ public class ScribePlugin implements ScribeChannel.ScribeMethodHandler { @NonNull private final ScribeChannel mScribeChannel; @NonNull private final InputMethodManager mInputMethodManager; - @NonNull public View mView; + @NonNull private View mView; public ScribePlugin( @NonNull View view, @NonNull InputMethodManager imm, @NonNull ScribeChannel scribeChannel) { @@ -42,6 +42,18 @@ public ScribePlugin( 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}. From 53212fc404a10231df8824afacdf8a46915fded9 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Mon, 7 Oct 2024 11:23:07 -0700 Subject: [PATCH 42/47] Use jsonmethodcodec --- .../embedding/engine/systemchannels/ScribeChannel.java | 5 ++--- .../embedding/engine/systemchannels/ScribeChannelTest.java | 4 ++-- 2 files changed, 4 insertions(+), 5 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 f64d4f7516715..59522ed0c7658 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java @@ -14,9 +14,9 @@ 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; -import io.flutter.plugin.common.StandardMethodCodec; /** * {@link ScribeChannel} is a platform channel that is used by the framework to facilitate the @@ -45,7 +45,6 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result return; } String method = call.method; - Object args = call.arguments; Log.v(TAG, "Received '" + method + "' message."); switch (method) { case METHOD_IS_STYLUS_HANDWRITING_AVAILABLE: @@ -92,7 +91,7 @@ private void startStylusHandwriting( } public ScribeChannel(@NonNull DartExecutor dartExecutor) { - channel = new MethodChannel(dartExecutor, "flutter/scribe", StandardMethodCodec.INSTANCE); + channel = new MethodChannel(dartExecutor, "flutter/scribe", JSONMethodCodec.INSTANCE); channel.setMethodCallHandler(parsingMethodHandler); } 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 index 13c65674e89c6..8be56c4d82f6d 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java @@ -15,8 +15,8 @@ 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.JSONMethodCodec; import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.StandardMethodCodec; import java.nio.ByteBuffer; import org.junit.Before; import org.junit.Test; @@ -29,7 +29,7 @@ public class ScribeChannelTest { private static BinaryMessenger.BinaryReply sendToBinaryMessageHandler( BinaryMessenger.BinaryMessageHandler binaryMessageHandler, String method) { MethodCall methodCall = new MethodCall(method, null); - ByteBuffer encodedMethodCall = StandardMethodCodec.INSTANCE.encodeMethodCall(methodCall); + ByteBuffer encodedMethodCall = JSONMethodCodec.INSTANCE.encodeMethodCall(methodCall); BinaryMessenger.BinaryReply mockReply = mock(BinaryMessenger.BinaryReply.class); binaryMessageHandler.onMessage((ByteBuffer) encodedMethodCall.flip(), mockReply); return mockReply; From 02c8f430c92a3bbb8ed91f85cbd87459051a3e9b Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Mon, 7 Oct 2024 13:38:33 -0700 Subject: [PATCH 43/47] Test reply value now that we're using jsonmethodcodec --- .../systemchannels/ScribeChannelTest.java | 58 +++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) 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 index 8be56c4d82f6d..a6653e9552ee6 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java @@ -6,6 +6,7 @@ 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; @@ -15,6 +16,7 @@ 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; @@ -62,7 +64,18 @@ public void respondsToStartStylusHandwriting() { sendToBinaryMessageHandler( binaryMessageHandler, ScribeChannel.METHOD_START_STYLUS_HANDWRITING); - verify(mockReply).reply(any(ByteBuffer.class)); + 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(); } @@ -74,7 +87,20 @@ public void respondsToIsStylusHandwritingAvailable() { sendToBinaryMessageHandler( binaryMessageHandler, ScribeChannel.METHOD_IS_STYLUS_HANDWRITING_AVAILABLE); - verify(mockReply).reply(any(ByteBuffer.class)); + 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(); } @@ -86,7 +112,19 @@ public void respondsToStartStylusHandwritingWhenAPILevelUnsupported() { sendToBinaryMessageHandler( binaryMessageHandler, ScribeChannel.METHOD_START_STYLUS_HANDWRITING); - verify(mockReply).reply(any(ByteBuffer.class)); + 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(); } @@ -98,7 +136,19 @@ public void respondsToIsStylusHandwritingAvailableWhenAPILevelUnsupported() { sendToBinaryMessageHandler( binaryMessageHandler, ScribeChannel.METHOD_IS_STYLUS_HANDWRITING_AVAILABLE); - verify(mockReply).reply(any(ByteBuffer.class)); + 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(); } } From 6d86717e753f14c555dc889a60f184bb290fbf42 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Mon, 7 Oct 2024 14:17:06 -0700 Subject: [PATCH 44/47] Fix test missing mocked scribechannel --- .../android/FlutterActivityAndFragmentDelegateTest.java | 2 ++ 1 file changed, 2 insertions(+) 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; } From 44c22d5696a47b4b3596d56d62a67dc25f31f811 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Mon, 7 Oct 2024 14:52:26 -0700 Subject: [PATCH 45/47] Fix platformviewscontrollertest due to missing scribechannel again. --- .../io/flutter/plugin/platform/PlatformViewsControllerTest.java | 2 ++ 1 file changed, 2 insertions(+) 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)); From 06edcdbe919e05a75cd9af5d01680ce04d14edbc Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Mon, 7 Oct 2024 14:57:33 -0700 Subject: [PATCH 46/47] Fix mixed up api version in test --- .../test/io/flutter/plugin/editing/ScribePluginTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java index aac053ed263bf..5c07634161249 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java @@ -62,8 +62,8 @@ public void scribePluginStartStylusHandwriting() { verify(mockImm).startStylusHandwriting(testView); } - @Config(sdk = API_LEVELS.API_33) - @TargetApi(API_LEVELS.API_33) + @Config(sdk = API_LEVELS.API_32) + @TargetApi(API_LEVELS.API_32) @Test public void scribePluginStartStylusHandwritingWhenAPILevelUnsupported() { assertNotNull(scribePlugin); @@ -75,8 +75,8 @@ public void scribePluginStartStylusHandwritingWhenAPILevelUnsupported() { }); } - @Config(sdk = API_LEVELS.API_32) - @TargetApi(API_LEVELS.API_32) + @Config(sdk = API_LEVELS.API_33) + @TargetApi(API_LEVELS.API_33) @Test public void scribePluginIsStylusHandwritingAvailableWhenAPILevelUnsupported() { assertNotNull(scribePlugin); From df68cb8aa6ad5cf3e6f307ed260c9fb2ec48c9b6 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Fri, 18 Oct 2024 13:14:38 -0700 Subject: [PATCH 47/47] isFeatureAvailable convenience method --- .../engine/systemchannels/ScribeChannel.java | 23 ++++++++- .../flutter/plugin/editing/ScribePlugin.java | 17 ++++++- .../systemchannels/ScribeChannelTest.java | 48 +++++++++++++++++++ .../plugin/editing/ScribePluginTest.java | 16 +++++++ 4 files changed, 102 insertions(+), 2 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 59522ed0c7658..7070000ab17c5 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java @@ -25,6 +25,9 @@ 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"; @@ -47,6 +50,9 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result 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; @@ -60,6 +66,15 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result } }; + 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) { @@ -106,7 +121,13 @@ public void setScribeMethodHandler(@Nullable ScribeMethodHandler scribeMethodHan public interface ScribeMethodHandler { /** * Responds to the {@code result} with success and a boolean indicating whether or not stylus - * hadnwriting is available. + * 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) diff --git a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java index d02d6cda2f0b3..35fb2ff0d0dfd 100644 --- a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java @@ -67,7 +67,8 @@ public void destroy() { /** * Returns true if the InputMethodManager supports Scribe stylus handwriting input. * - *

Call this before calling startStylusHandwriting to make sure it's available. + *

Call this or isFeatureAvailable before calling startStylusHandwriting to make sure it's + * available. */ @TargetApi(API_LEVELS.API_34) @RequiresApi(API_LEVELS.API_34) @@ -88,4 +89,18 @@ public boolean isStylusHandwritingAvailable() { 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/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java index a6653e9552ee6..c69f624300fcd 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java @@ -79,6 +79,30 @@ public void respondsToStartStylusHandwriting() { 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 @@ -128,6 +152,30 @@ public void respondsToStartStylusHandwritingWhenAPILevelUnsupported() { 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 diff --git a/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java index 5c07634161249..386c75a35c79f 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java @@ -44,6 +44,15 @@ public void setUp() { 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 @@ -75,6 +84,13 @@ public void scribePluginStartStylusHandwritingWhenAPILevelUnsupported() { }); } + @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