diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 2f9eb9fa699b6..cc483755af75f 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -44294,6 +44294,7 @@ ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/syst ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SystemChannel.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/common/ActivityLifecycleListener.java + ../../../flutter/LICENSE @@ -44317,6 +44318,7 @@ ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/Flutte ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ListenableEditingState.java + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/SpellCheckPlugin.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextEditingDelta.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java + ../../../flutter/LICENSE @@ -47176,6 +47178,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/system FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SettingsChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SystemChannel.java @@ -47202,6 +47205,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/FlutterT FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ListenableEditingState.java +FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/SpellCheckPlugin.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextEditingDelta.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 6f062431cf747..a0745b4107fe7 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -294,6 +294,7 @@ android_java_sources = [ "io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java", "io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java", "io/flutter/embedding/engine/systemchannels/RestorationChannel.java", + "io/flutter/embedding/engine/systemchannels/ScribeChannel.java", "io/flutter/embedding/engine/systemchannels/SettingsChannel.java", "io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java", "io/flutter/embedding/engine/systemchannels/SystemChannel.java", @@ -320,6 +321,7 @@ android_java_sources = [ "io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java", "io/flutter/plugin/editing/InputConnectionAdaptor.java", "io/flutter/plugin/editing/ListenableEditingState.java", + "io/flutter/plugin/editing/ScribePlugin.java", "io/flutter/plugin/editing/SpellCheckPlugin.java", "io/flutter/plugin/editing/TextEditingDelta.java", "io/flutter/plugin/editing/TextInputPlugin.java", diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 0d5ae3a8efc5f..3f3f0470d8f54 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -63,6 +63,7 @@ import io.flutter.embedding.engine.renderer.RenderSurface; import io.flutter.embedding.engine.systemchannels.SettingsChannel; import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.editing.ScribePlugin; import io.flutter.plugin.editing.SpellCheckPlugin; import io.flutter.plugin.editing.TextInputPlugin; import io.flutter.plugin.localization.LocalizationPlugin; @@ -133,6 +134,7 @@ public class FlutterView extends FrameLayout @Nullable private MouseCursorPlugin mouseCursorPlugin; @Nullable private TextInputPlugin textInputPlugin; @Nullable private SpellCheckPlugin spellCheckPlugin; + @Nullable private ScribePlugin scribePlugin; @Nullable private LocalizationPlugin localizationPlugin; @Nullable private KeyboardManager keyboardManager; @Nullable private AndroidTouchProcessor androidTouchProcessor; @@ -1120,10 +1122,12 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) { if (Build.VERSION.SDK_INT >= API_LEVELS.API_24) { mouseCursorPlugin = new MouseCursorPlugin(this, this.flutterEngine.getMouseCursorChannel()); } + textInputPlugin = new TextInputPlugin( this, this.flutterEngine.getTextInputChannel(), + this.flutterEngine.getScribeChannel(), this.flutterEngine.getPlatformViewsController()); try { @@ -1136,6 +1140,10 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) { Log.e(TAG, "TextServicesManager not supported by device, spell check disabled."); } + scribePlugin = + new ScribePlugin( + this, textInputPlugin.getInputMethodManager(), this.flutterEngine.getScribeChannel()); + localizationPlugin = this.flutterEngine.getLocalizationPlugin(); keyboardManager = new KeyboardManager(this); diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java index 4c80b90a603f7..683dfeace7937 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java @@ -34,6 +34,7 @@ import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.embedding.engine.systemchannels.ProcessTextChannel; import io.flutter.embedding.engine.systemchannels.RestorationChannel; +import io.flutter.embedding.engine.systemchannels.ScribeChannel; import io.flutter.embedding.engine.systemchannels.SettingsChannel; import io.flutter.embedding.engine.systemchannels.SpellCheckChannel; import io.flutter.embedding.engine.systemchannels.SystemChannel; @@ -100,6 +101,7 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater { @NonNull private final RestorationChannel restorationChannel; @NonNull private final PlatformChannel platformChannel; @NonNull private final ProcessTextChannel processTextChannel; + @NonNull private final ScribeChannel scribeChannel; @NonNull private final SettingsChannel settingsChannel; @NonNull private final SpellCheckChannel spellCheckChannel; @NonNull private final SystemChannel systemChannel; @@ -337,6 +339,7 @@ public FlutterEngine( platformChannel = new PlatformChannel(dartExecutor); processTextChannel = new ProcessTextChannel(dartExecutor, context.getPackageManager()); restorationChannel = new RestorationChannel(dartExecutor, waitForRestorationData); + scribeChannel = new ScribeChannel(dartExecutor); settingsChannel = new SettingsChannel(dartExecutor); spellCheckChannel = new SpellCheckChannel(dartExecutor); systemChannel = new SystemChannel(dartExecutor); @@ -610,6 +613,12 @@ public TextInputChannel getTextInputChannel() { return textInputChannel; } + /** System channel that sends and receives Scribe requests and results. */ + @NonNull + public ScribeChannel getScribeChannel() { + return scribeChannel; + } + /** System channel that sends and receives spell check requests and results. */ @NonNull public SpellCheckChannel getSpellCheckChannel() { diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java new file mode 100644 index 0000000000000..7070000ab17c5 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java @@ -0,0 +1,147 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.embedding.engine.systemchannels; + +import static io.flutter.Build.API_LEVELS; + +import android.annotation.TargetApi; +import android.os.Build; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; +import io.flutter.Log; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.plugin.common.JSONMethodCodec; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +/** + * {@link ScribeChannel} is a platform channel that is used by the framework to facilitate the + * Scribe handwriting text input feature. + */ +public class ScribeChannel { + private static final String TAG = "ScribeChannel"; + + @VisibleForTesting + public static final String METHOD_IS_FEATURE_AVAILABLE = "Scribe.isFeatureAvailable"; + + @VisibleForTesting + public static final String METHOD_IS_STYLUS_HANDWRITING_AVAILABLE = + "Scribe.isStylusHandwritingAvailable"; + + @VisibleForTesting + public static final String METHOD_START_STYLUS_HANDWRITING = "Scribe.startStylusHandwriting"; + + public final MethodChannel channel; + private ScribeMethodHandler scribeMethodHandler; + + @NonNull + public final MethodChannel.MethodCallHandler parsingMethodHandler = + new MethodChannel.MethodCallHandler() { + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + if (scribeMethodHandler == null) { + Log.v(TAG, "No ScribeMethodHandler registered. Scribe call not handled."); + return; + } + String method = call.method; + Log.v(TAG, "Received '" + method + "' message."); + switch (method) { + case METHOD_IS_FEATURE_AVAILABLE: + isFeatureAvailable(call, result); + break; + case METHOD_IS_STYLUS_HANDWRITING_AVAILABLE: + isStylusHandwritingAvailable(call, result); + break; + case METHOD_START_STYLUS_HANDWRITING: + startStylusHandwriting(call, result); + break; + default: + result.notImplemented(); + break; + } + } + }; + + private void isFeatureAvailable(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + try { + final boolean isAvailable = scribeMethodHandler.isFeatureAvailable(); + result.success(isAvailable); + } catch (IllegalStateException exception) { + result.error("error", exception.getMessage(), null); + } + } + + private void isStylusHandwritingAvailable( + @NonNull MethodCall call, @NonNull MethodChannel.Result result) { + if (Build.VERSION.SDK_INT < API_LEVELS.API_34) { + result.error("error", "Requires API level 34 or higher.", null); + return; + } + + try { + final boolean isAvailable = scribeMethodHandler.isStylusHandwritingAvailable(); + result.success(isAvailable); + } catch (IllegalStateException exception) { + result.error("error", exception.getMessage(), null); + } + } + + private void startStylusHandwriting( + @NonNull MethodCall call, @NonNull MethodChannel.Result result) { + if (Build.VERSION.SDK_INT < API_LEVELS.API_33) { + result.error("error", "Requires API level 33 or higher.", null); + return; + } + + try { + scribeMethodHandler.startStylusHandwriting(); + result.success(null); + } catch (IllegalStateException exception) { + result.error("error", exception.getMessage(), null); + } + } + + public ScribeChannel(@NonNull DartExecutor dartExecutor) { + channel = new MethodChannel(dartExecutor, "flutter/scribe", JSONMethodCodec.INSTANCE); + channel.setMethodCallHandler(parsingMethodHandler); + } + + /** + * Sets the {@link ScribeMethodHandler} which receives all requests for scribe sent through this + * channel. + */ + public void setScribeMethodHandler(@Nullable ScribeMethodHandler scribeMethodHandler) { + this.scribeMethodHandler = scribeMethodHandler; + } + + public interface ScribeMethodHandler { + /** + * Responds to the {@code result} with success and a boolean indicating whether or not stylus + * handwriting is available. + */ + boolean isFeatureAvailable(); + + /** + * Responds to the {@code result} with success and a boolean indicating whether or not stylus + * handwriting is available. + */ + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + boolean isStylusHandwritingAvailable(); + + /** + * Requests to start Scribe stylus handwriting, which will respond to the {@code result} with + * either success if handwriting input has started or error otherwise. + */ + @TargetApi(API_LEVELS.API_33) + @RequiresApi(API_LEVELS.API_33) + void startStylusHandwriting(); + } + + // TODO(justinmc): Scribe stylus gestures should be supported here. + // https://github.com/flutter/flutter/issues/156018 +} diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index 1f688d7beb281..aa96ea02645c9 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -33,6 +33,7 @@ import androidx.core.view.inputmethod.InputConnectionCompat; import io.flutter.Log; import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.systemchannels.ScribeChannel; import io.flutter.embedding.engine.systemchannels.TextInputChannel; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; @@ -51,6 +52,7 @@ public interface KeyboardDelegate { private final View mFlutterView; private final int mClient; + private final ScribeChannel scribeChannel; private final TextInputChannel textInputChannel; private final ListenableEditingState mEditable; private final EditorInfo mEditorInfo; @@ -69,6 +71,7 @@ public InputConnectionAdaptor( View view, int client, TextInputChannel textInputChannel, + ScribeChannel scribeChannel, KeyboardDelegate keyboardDelegate, ListenableEditingState editable, EditorInfo editorInfo, @@ -77,6 +80,7 @@ public InputConnectionAdaptor( mFlutterView = view; mClient = client; this.textInputChannel = textInputChannel; + this.scribeChannel = scribeChannel; mEditable = editable; mEditable.addEditingStateListener(this); mEditorInfo = editorInfo; @@ -100,10 +104,19 @@ public InputConnectionAdaptor( View view, int client, TextInputChannel textInputChannel, + ScribeChannel scribeChannel, KeyboardDelegate keyboardDelegate, ListenableEditingState editable, EditorInfo editorInfo) { - this(view, client, textInputChannel, keyboardDelegate, editable, editorInfo, new FlutterJNI()); + this( + view, + client, + textInputChannel, + scribeChannel, + keyboardDelegate, + editable, + editorInfo, + new FlutterJNI()); } private ExtractedText getExtractedText(ExtractedTextRequest request) { @@ -262,6 +275,10 @@ public boolean setSelection(int start, int end) { return result; } + // TODO(justinmc): Scribe stylus gestures should be supported here via + // performHandwritingGesture. + // https://github.com/flutter/flutter/issues/156018 + // Sanitizes the index to ensure the index is within the range of the // contents of editable. private static int clampIndexToEditable(int index, Editable editable) { diff --git a/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java new file mode 100644 index 0000000000000..35fb2ff0d0dfd --- /dev/null +++ b/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java @@ -0,0 +1,106 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugin.editing; + +import static io.flutter.Build.API_LEVELS; + +import android.annotation.TargetApi; +import android.os.Build; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import io.flutter.embedding.engine.systemchannels.ScribeChannel; + +/** + * {@link ScribePlugin} is the implementation of all functionality needed for handwriting stylus + * text input. + * + *

The plugin handles requests for scribe sent by the {@link + * io.flutter.embedding.engine.systemchannels.ScribeChannel}. + * + *

On API versions below 33, the plugin does nothing. + */ +public class ScribePlugin implements ScribeChannel.ScribeMethodHandler { + + @NonNull private final ScribeChannel mScribeChannel; + @NonNull private final InputMethodManager mInputMethodManager; + @NonNull private View mView; + + public ScribePlugin( + @NonNull View view, @NonNull InputMethodManager imm, @NonNull ScribeChannel scribeChannel) { + if (Build.VERSION.SDK_INT >= API_LEVELS.API_33) { + view.setAutoHandwritingEnabled(false); + } + + mView = view; + mInputMethodManager = imm; + mScribeChannel = scribeChannel; + + mScribeChannel.setScribeMethodHandler(this); + } + + /** + * Sets the View in which Scribe input is handled. + * + *

Only one View can be set at any given time. + */ + public void setView(@NonNull View view) { + if (view == mView) { + return; + } + mView = view; + } + + /** + * Unregisters this {@code ScribePlugin} as the {@code ScribeChannel.ScribeMethodHandler}, for the + * {@link io.flutter.embedding.engine.systemchannels.ScribeChannel}. + * + *

Do not invoke any methods on a {@code ScribePlugin} after invoking this method. + */ + public void destroy() { + mScribeChannel.setScribeMethodHandler(null); + } + + /** + * Returns true if the InputMethodManager supports Scribe stylus handwriting input. + * + *

Call this or isFeatureAvailable before calling startStylusHandwriting to make sure it's + * available. + */ + @TargetApi(API_LEVELS.API_34) + @RequiresApi(API_LEVELS.API_34) + @Override + public boolean isStylusHandwritingAvailable() { + return mInputMethodManager.isStylusHandwritingAvailable(); + } + + /** + * Starts stylus handwriting input. + * + *

Typically isStylusHandwritingAvailable should be called first to determine whether this is + * supported by the IME. + */ + @TargetApi(API_LEVELS.API_33) + @RequiresApi(API_LEVELS.API_33) + @Override + public void startStylusHandwriting() { + mInputMethodManager.startStylusHandwriting(mView); + } + + /** + * A convenience method to check if Scribe is available. + * + *

Differs from isStylusHandwritingAvailable in that it can be called from any API level + * without throwing an error. + * + *

Call this or isStylusHandwritingAvailable before calling startStylusHandwriting to make sure + * it's available. + */ + @Override + public boolean isFeatureAvailable() { + return Build.VERSION.SDK_INT >= API_LEVELS.API_34 && isStylusHandwritingAvailable(); + } +} diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index beae70837e3c3..41cd19ed32fc4 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -29,6 +29,7 @@ import androidx.core.view.inputmethod.EditorInfoCompat; import io.flutter.Log; import io.flutter.embedding.android.KeyboardManager; +import io.flutter.embedding.engine.systemchannels.ScribeChannel; import io.flutter.embedding.engine.systemchannels.TextInputChannel; import io.flutter.embedding.engine.systemchannels.TextInputChannel.TextEditState; import io.flutter.plugin.platform.PlatformViewsController; @@ -42,6 +43,7 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch @NonNull private final View mView; @NonNull private final InputMethodManager mImm; @NonNull private final AutofillManager afm; + @NonNull private final ScribeChannel scribeChannel; @NonNull private final TextInputChannel textInputChannel; @NonNull private InputTarget inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0); @Nullable private TextInputChannel.Configuration configuration; @@ -66,6 +68,7 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch public TextInputPlugin( @NonNull View view, @NonNull TextInputChannel textInputChannel, + @NonNull ScribeChannel scribeChannel, @NonNull PlatformViewsController platformViewsController) { mView = view; // Create a default object. @@ -153,6 +156,8 @@ public void sendAppPrivateCommand(String action, Bundle data) { textInputChannel.requestExistingInputState(); + this.scribeChannel = scribeChannel; + this.platformViewsController = platformViewsController; this.platformViewsController.attachTextInputPlugin(this); } @@ -341,9 +346,23 @@ public InputConnection createInputConnection( EditorInfoCompat.setContentMimeTypes(outAttrs, imgTypeString); } + if (Build.VERSION.SDK_INT >= API_LEVELS.API_34) { + EditorInfoCompat.setStylusHandwritingEnabled(outAttrs, true); + } + // TODO(justinmc): Scribe stylus gestures should be supported here via + // outAttrs.setSupportedHandwritingGestures and + // outAttrs.setSupportedHandwritingGesturePreviews. + // https://github.com/flutter/flutter/issues/156018 + InputConnectionAdaptor connection = new InputConnectionAdaptor( - view, inputTarget.id, textInputChannel, keyboardManager, mEditable, outAttrs); + view, + inputTarget.id, + textInputChannel, + scribeChannel, + keyboardManager, + mEditable, + outAttrs); outAttrs.initialSelStart = mEditable.getSelectionStart(); outAttrs.initialSelEnd = mEditable.getSelectionEnd(); diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 8dbdae146ef83..d20ea56f1f9db 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -56,6 +56,7 @@ import io.flutter.embedding.engine.systemchannels.MouseCursorChannel; import io.flutter.embedding.engine.systemchannels.NavigationChannel; import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.embedding.engine.systemchannels.ScribeChannel; import io.flutter.embedding.engine.systemchannels.SettingsChannel; import io.flutter.embedding.engine.systemchannels.SystemChannel; import io.flutter.embedding.engine.systemchannels.TextInputChannel; @@ -237,7 +238,11 @@ public void onPostResume() { PlatformViewsController platformViewsController = mNativeView.getPluginRegistry().getPlatformViewsController(); mTextInputPlugin = - new TextInputPlugin(this, new TextInputChannel(dartExecutor), platformViewsController); + new TextInputPlugin( + this, + new TextInputChannel(dartExecutor), + new ScribeChannel(dartExecutor), + platformViewsController); mKeyboardManager = new KeyboardManager(this); if (Build.VERSION.SDK_INT >= API_LEVELS.API_24) { diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java index 132aea4ae52dd..ae1e371a48d6a 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java @@ -49,6 +49,7 @@ import io.flutter.embedding.engine.systemchannels.LocalizationChannel; import io.flutter.embedding.engine.systemchannels.MouseCursorChannel; import io.flutter.embedding.engine.systemchannels.NavigationChannel; +import io.flutter.embedding.engine.systemchannels.ScribeChannel; import io.flutter.embedding.engine.systemchannels.SettingsChannel; import io.flutter.embedding.engine.systemchannels.SystemChannel; import io.flutter.embedding.engine.systemchannels.TextInputChannel; @@ -1481,6 +1482,7 @@ private FlutterEngine mockFlutterEngine() { when(engine.getSettingsChannel()).thenReturn(fakeSettingsChannel); when(engine.getSystemChannel()).thenReturn(mock(SystemChannel.class)); when(engine.getTextInputChannel()).thenReturn(mock(TextInputChannel.class)); + when(engine.getScribeChannel()).thenReturn(mock(ScribeChannel.class)); return engine; } diff --git a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java new file mode 100644 index 0000000000000..c69f624300fcd --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/ScribeChannelTest.java @@ -0,0 +1,202 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.embedding.engine.systemchannels; + +import static io.flutter.Build.API_LEVELS; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.annotation.TargetApi; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.FlutterException; +import io.flutter.plugin.common.JSONMethodCodec; +import io.flutter.plugin.common.MethodCall; +import java.nio.ByteBuffer; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.robolectric.annotation.Config; + +@RunWith(AndroidJUnit4.class) +public class ScribeChannelTest { + private static BinaryMessenger.BinaryReply sendToBinaryMessageHandler( + BinaryMessenger.BinaryMessageHandler binaryMessageHandler, String method) { + MethodCall methodCall = new MethodCall(method, null); + ByteBuffer encodedMethodCall = JSONMethodCodec.INSTANCE.encodeMethodCall(methodCall); + BinaryMessenger.BinaryReply mockReply = mock(BinaryMessenger.BinaryReply.class); + binaryMessageHandler.onMessage((ByteBuffer) encodedMethodCall.flip(), mockReply); + return mockReply; + } + + ScribeChannel.ScribeMethodHandler mockHandler; + BinaryMessenger.BinaryMessageHandler binaryMessageHandler; + + @Before + public void setUp() { + ArgumentCaptor binaryMessageHandlerCaptor = + ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class); + DartExecutor mockBinaryMessenger = mock(DartExecutor.class); + mockHandler = mock(ScribeChannel.ScribeMethodHandler.class); + ScribeChannel scribeChannel = new ScribeChannel(mockBinaryMessenger); + + scribeChannel.setScribeMethodHandler(mockHandler); + + verify((BinaryMessenger) mockBinaryMessenger, times(1)) + .setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture()); + + binaryMessageHandler = binaryMessageHandlerCaptor.getValue(); + } + + @Config(minSdk = API_LEVELS.API_34) + @TargetApi(API_LEVELS.API_34) + @Test + public void respondsToStartStylusHandwriting() { + BinaryMessenger.BinaryReply mockReply = + sendToBinaryMessageHandler( + binaryMessageHandler, ScribeChannel.METHOD_START_STYLUS_HANDWRITING); + + verify(mockReply) + .reply( + argThat( + (ByteBuffer reply) -> { + reply.flip(); + try { + final Object decodedReply = JSONMethodCodec.INSTANCE.decodeEnvelope(reply); + return decodedReply == null; + } catch (FlutterException e) { + return false; + } + })); + verify(mockHandler).startStylusHandwriting(); + } + + @Config(minSdk = API_LEVELS.API_34) + @TargetApi(API_LEVELS.API_34) + @Test + public void respondsToIsFeatureAvailable() { + BinaryMessenger.BinaryReply mockReply = + sendToBinaryMessageHandler(binaryMessageHandler, ScribeChannel.METHOD_IS_FEATURE_AVAILABLE); + + verify(mockReply) + .reply( + argThat( + (ByteBuffer reply) -> { + reply.flip(); + try { + final Object decodedReply = JSONMethodCodec.INSTANCE.decodeEnvelope(reply); + // Should succeed and should tell whether or not Scribe is available by + // using a boolean. + return decodedReply.getClass() == java.lang.Boolean.class; + } catch (FlutterException e) { + return false; + } + })); + verify(mockHandler).isFeatureAvailable(); + } + + @Config(minSdk = API_LEVELS.API_34) + @TargetApi(API_LEVELS.API_34) + @Test + public void respondsToIsStylusHandwritingAvailable() { + BinaryMessenger.BinaryReply mockReply = + sendToBinaryMessageHandler( + binaryMessageHandler, ScribeChannel.METHOD_IS_STYLUS_HANDWRITING_AVAILABLE); + + verify(mockReply) + .reply( + argThat( + (ByteBuffer reply) -> { + reply.flip(); + try { + final Object decodedReply = JSONMethodCodec.INSTANCE.decodeEnvelope(reply); + // Should succeed and should tell whether or not Scribe is available by + // using a boolean. + return decodedReply.getClass() == java.lang.Boolean.class; + } catch (FlutterException e) { + return false; + } + })); + verify(mockHandler).isStylusHandwritingAvailable(); + } + + @Config(sdk = API_LEVELS.API_32) + @TargetApi(API_LEVELS.API_32) + @Test + public void respondsToStartStylusHandwritingWhenAPILevelUnsupported() { + BinaryMessenger.BinaryReply mockReply = + sendToBinaryMessageHandler( + binaryMessageHandler, ScribeChannel.METHOD_START_STYLUS_HANDWRITING); + + verify(mockReply) + .reply( + argThat( + (ByteBuffer reply) -> { + reply.flip(); + try { + final Object decodedReply = JSONMethodCodec.INSTANCE.decodeEnvelope(reply); + return false; + } catch (FlutterException e) { + // Should fail because the API version is too low. + return true; + } + })); + verify(mockHandler, never()).startStylusHandwriting(); + } + + @Config(sdk = API_LEVELS.API_33) + @TargetApi(API_LEVELS.API_33) + @Test + public void respondsToIsFeatureAvailableWhenAPILevelUnsupported() { + BinaryMessenger.BinaryReply mockReply = + sendToBinaryMessageHandler(binaryMessageHandler, ScribeChannel.METHOD_IS_FEATURE_AVAILABLE); + + verify(mockReply) + .reply( + argThat( + (ByteBuffer reply) -> { + reply.flip(); + try { + final Object decodedReply = JSONMethodCodec.INSTANCE.decodeEnvelope(reply); + // Should succeed and indicate that Scribe is not available. + return decodedReply.getClass() == java.lang.Boolean.class + && !((boolean) decodedReply); + } catch (FlutterException e) { + return false; + } + })); + verify(mockHandler).isFeatureAvailable(); + } + + @Config(sdk = API_LEVELS.API_33) + @TargetApi(API_LEVELS.API_33) + @Test + public void respondsToIsStylusHandwritingAvailableWhenAPILevelUnsupported() { + BinaryMessenger.BinaryReply mockReply = + sendToBinaryMessageHandler( + binaryMessageHandler, ScribeChannel.METHOD_IS_STYLUS_HANDWRITING_AVAILABLE); + + verify(mockReply) + .reply( + argThat( + (ByteBuffer reply) -> { + reply.flip(); + try { + final Object decodedReply = JSONMethodCodec.INSTANCE.decodeEnvelope(reply); + return false; + } catch (FlutterException e) { + // Should fail because the API version is too low. + return true; + } + })); + verify(mockHandler, never()).isStylusHandwritingAvailable(); + } +} diff --git a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java index 1158f1d33b0cf..8ca3469498c24 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java @@ -43,6 +43,7 @@ import io.flutter.embedding.android.KeyboardManager; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.systemchannels.ScribeChannel; import io.flutter.embedding.engine.systemchannels.TextInputChannel; import io.flutter.plugin.common.JSONMethodCodec; import io.flutter.plugin.common.MethodCall; @@ -105,6 +106,7 @@ public void inputConnectionAdaptor_ReceivesEnter() throws NullPointerException { DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class))); int inputTargetId = 0; TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + ScribeChannel scribeChannel = new ScribeChannel(dartExecutor); ListenableEditingState mEditable = new ListenableEditingState(null, testView); Selection.setSelection(mEditable, 0, 0); ListenableEditingState spyEditable = spy(mEditable); @@ -113,7 +115,13 @@ public void inputConnectionAdaptor_ReceivesEnter() throws NullPointerException { InputConnectionAdaptor inputConnectionAdaptor = new InputConnectionAdaptor( - testView, inputTargetId, textInputChannel, mockKeyboardManager, spyEditable, outAttrs); + testView, + inputTargetId, + textInputChannel, + scribeChannel, + mockKeyboardManager, + spyEditable, + outAttrs); // Send an enter key and make sure the Editable received it. FakeKeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, '\n'); @@ -199,12 +207,14 @@ public void testCommitContent() throws JSONException { FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + ScribeChannel scribeChannel = new ScribeChannel(dartExecutor); ListenableEditingState editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( testView, client, textInputChannel, + scribeChannel, mockKeyboardManager, editable, null, @@ -260,12 +270,14 @@ public void testPerformPrivateCommand_dataIsNull() throws JSONException { FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + ScribeChannel scribeChannel = new ScribeChannel(dartExecutor); ListenableEditingState editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( testView, client, textInputChannel, + scribeChannel, mockKeyboardManager, editable, null, @@ -291,12 +303,14 @@ public void testPerformPrivateCommand_dataIsByteArray() throws JSONException { FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + ScribeChannel scribeChannel = new ScribeChannel(dartExecutor); ListenableEditingState editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( testView, client, textInputChannel, + scribeChannel, mockKeyboardManager, editable, null, @@ -328,12 +342,14 @@ public void testPerformPrivateCommand_dataIsByte() throws JSONException { FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + ScribeChannel scribeChannel = new ScribeChannel(dartExecutor); ListenableEditingState editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( testView, client, textInputChannel, + scribeChannel, mockKeyboardManager, editable, null, @@ -363,12 +379,14 @@ public void testPerformPrivateCommand_dataIsCharArray() throws JSONException { FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + ScribeChannel scribeChannel = new ScribeChannel(dartExecutor); ListenableEditingState editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( testView, client, textInputChannel, + scribeChannel, mockKeyboardManager, editable, null, @@ -401,12 +419,14 @@ public void testPerformPrivateCommand_dataIsChar() throws JSONException { FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + ScribeChannel scribeChannel = new ScribeChannel(dartExecutor); ListenableEditingState editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( testView, client, textInputChannel, + scribeChannel, mockKeyboardManager, editable, null, @@ -436,12 +456,14 @@ public void testPerformPrivateCommand_dataIsCharSequenceArray() throws JSONExcep FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + ScribeChannel scribeChannel = new ScribeChannel(dartExecutor); ListenableEditingState editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( testView, client, textInputChannel, + scribeChannel, mockKeyboardManager, editable, null, @@ -475,12 +497,14 @@ public void testPerformPrivateCommand_dataIsCharSequence() throws JSONException FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + ScribeChannel scribeChannel = new ScribeChannel(dartExecutor); ListenableEditingState editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( testView, client, textInputChannel, + scribeChannel, mockKeyboardManager, editable, null, @@ -512,12 +536,14 @@ public void testPerformPrivateCommand_dataIsFloat() throws JSONException { FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + ScribeChannel scribeChannel = new ScribeChannel(dartExecutor); ListenableEditingState editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( testView, client, textInputChannel, + scribeChannel, mockKeyboardManager, editable, null, @@ -547,12 +573,14 @@ public void testPerformPrivateCommand_dataIsFloatArray() throws JSONException { FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + ScribeChannel scribeChannel = new ScribeChannel(dartExecutor); ListenableEditingState editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( testView, client, textInputChannel, + scribeChannel, mockKeyboardManager, editable, null, @@ -1080,6 +1108,7 @@ public void testExtractedText_monitoring() { testView, 1, mock(TextInputChannel.class), + mock(ScribeChannel.class), mockKeyboardManager, editable, new EditorInfo()); @@ -1131,6 +1160,7 @@ public void testCursorAnchorInfo() { testView, 1, mock(TextInputChannel.class), + mock(ScribeChannel.class), mockKeyboardManager, editable, new EditorInfo()); @@ -1277,6 +1307,7 @@ private static InputConnectionAdaptor sampleInputConnectionAdaptor( View testView = new View(ApplicationProvider.getApplicationContext()); int client = 0; TextInputChannel textInputChannel = mock(TextInputChannel.class); + ScribeChannel scribeChannel = mock(ScribeChannel.class); FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); when(mockFlutterJNI.isCodePointEmoji(anyInt())) .thenAnswer((invocation) -> Emoji.isEmoji((int) invocation.getArguments()[0])); @@ -1290,7 +1321,14 @@ private static InputConnectionAdaptor sampleInputConnectionAdaptor( .thenAnswer( (invocation) -> Emoji.isRegionalIndicatorSymbol((int) invocation.getArguments()[0])); return new InputConnectionAdaptor( - testView, client, textInputChannel, mockKeyboardManager, editable, null, mockFlutterJNI); + testView, + client, + textInputChannel, + scribeChannel, + mockKeyboardManager, + editable, + null, + mockFlutterJNI); } private static class Emoji { diff --git a/shell/platform/android/test/io/flutter/plugin/editing/ListenableEditingStateTest.java b/shell/platform/android/test/io/flutter/plugin/editing/ListenableEditingStateTest.java index 800116659901b..e867dd587b900 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/ListenableEditingStateTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/ListenableEditingStateTest.java @@ -14,6 +14,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import io.flutter.embedding.android.KeyboardManager; +import io.flutter.embedding.engine.systemchannels.ScribeChannel; import io.flutter.embedding.engine.systemchannels.TextInputChannel; import java.util.ArrayList; import org.junit.Before; @@ -280,6 +281,7 @@ public void endBatchEdit() { testView, 0, mock(TextInputChannel.class), + mock(ScribeChannel.class), mockKeyboardManager, editingState, new EditorInfo()); @@ -305,6 +307,7 @@ public void inputMethod_testSetSelection() { testView, 0, mock(TextInputChannel.class), + mock(ScribeChannel.class), mockKeyboardManager, editingState, new EditorInfo()); @@ -339,6 +342,7 @@ public void inputMethod_testSetComposition() { testView, 0, mock(TextInputChannel.class), + mock(ScribeChannel.class), mockKeyboardManager, editingState, new EditorInfo()); @@ -399,6 +403,7 @@ public void inputMethod_testCommitText() { testView, 0, mock(TextInputChannel.class), + mock(ScribeChannel.class), mockKeyboardManager, editingState, new EditorInfo()); diff --git a/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java new file mode 100644 index 0000000000000..386c75a35c79f --- /dev/null +++ b/shell/platform/android/test/io/flutter/plugin/editing/ScribePluginTest.java @@ -0,0 +1,106 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugin.editing; + +import static io.flutter.Build.API_LEVELS; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import io.flutter.embedding.engine.systemchannels.ScribeChannel; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +@RunWith(AndroidJUnit4.class) +public class ScribePluginTest { + private final Context ctx = ApplicationProvider.getApplicationContext(); + + ScribePlugin scribePlugin; + InputMethodManager mockImm; + View testView; + + @Before + public void setUp() { + ScribeChannel mockScribeChannel = mock(ScribeChannel.class); + testView = new View(ctx); + mockImm = mock(InputMethodManager.class); + if (Build.VERSION.SDK_INT >= API_LEVELS.API_34) { + when(mockImm.isStylusHandwritingAvailable()).thenReturn(true); + } + scribePlugin = new ScribePlugin(testView, mockImm, mockScribeChannel); + } + + @Config(minSdk = API_LEVELS.API_34) + @TargetApi(API_LEVELS.API_34) + @Test + public void scribePluginIsFeatureAvailable() { + assertEquals(scribePlugin.isFeatureAvailable(), true); + + verify(mockImm).isStylusHandwritingAvailable(); + } + + @Config(minSdk = API_LEVELS.API_34) + @TargetApi(API_LEVELS.API_34) + @Test + public void scribePluginIsStylusHandwritingAvailable() { + assertEquals(scribePlugin.isStylusHandwritingAvailable(), true); + + verify(mockImm).isStylusHandwritingAvailable(); + } + + @Config(minSdk = API_LEVELS.API_34) + @TargetApi(API_LEVELS.API_34) + @Test + public void scribePluginStartStylusHandwriting() { + scribePlugin.startStylusHandwriting(); + + verify(mockImm).startStylusHandwriting(testView); + } + + @Config(sdk = API_LEVELS.API_32) + @TargetApi(API_LEVELS.API_32) + @Test + public void scribePluginStartStylusHandwritingWhenAPILevelUnsupported() { + assertNotNull(scribePlugin); + + assertThrows( + NoSuchMethodError.class, + () -> { + scribePlugin.startStylusHandwriting(); + }); + } + + @Config(sdk = API_LEVELS.API_33) + @TargetApi(API_LEVELS.API_33) + @Test + public void scribePluginIsFeatureAvailableWhenAPILevelUnsupported() { + assertEquals(scribePlugin.isFeatureAvailable(), false); + } + + @Config(sdk = API_LEVELS.API_33) + @TargetApi(API_LEVELS.API_33) + @Test + public void scribePluginIsStylusHandwritingAvailableWhenAPILevelUnsupported() { + assertNotNull(scribePlugin); + + assertThrows( + NoSuchMethodError.class, + () -> { + scribePlugin.isStylusHandwritingAvailable(); + }); + } +} diff --git a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java index 2656902b52543..77f71f0aaf923 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -2,6 +2,7 @@ import static io.flutter.Build.API_LEVELS; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -45,6 +46,7 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; +import androidx.core.view.inputmethod.EditorInfoCompat; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import io.flutter.embedding.android.FlutterView; @@ -54,6 +56,7 @@ import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.loader.FlutterLoader; import io.flutter.embedding.engine.renderer.FlutterRenderer; +import io.flutter.embedding.engine.systemchannels.ScribeChannel; import io.flutter.embedding.engine.systemchannels.TextInputChannel; import io.flutter.embedding.engine.systemchannels.TextInputChannel.TextEditState; import io.flutter.plugin.common.BinaryMessenger; @@ -133,8 +136,10 @@ public void textInputPlugin_RequestsReattachOnCreation() throws JSONException { FlutterJNI mockFlutterJni = mock(FlutterJNI.class); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class))); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class); @@ -152,8 +157,10 @@ public void setTextInputEditingState_doesNotInvokeUpdateEditingState() { testImm.setCurrentInputMethodSubtype(inputMethodSubtype); View testView = new View(ctx); TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class))); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); textInputPlugin.setTextInputClient( 0, new TextInputChannel.Configuration( @@ -194,8 +201,10 @@ public void setTextInputEditingState_willNotThrowWithoutSetTextInputClient() { testImm.setCurrentInputMethodSubtype(inputMethodSubtype); View testView = new View(ctx); TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class))); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); // Here's no textInputPlugin.setTextInputClient() textInputPlugin.setTextInputEditingState( @@ -211,8 +220,10 @@ public void setTextInputEditingState_doesNotInvokeUpdateEditingStateWithDeltas() testImm.setCurrentInputMethodSubtype(inputMethodSubtype); View testView = new View(ctx); TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class))); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); textInputPlugin.setTextInputClient( 0, new TextInputChannel.Configuration( @@ -261,8 +272,10 @@ public void textEditingDelta_TestUpdateEditingValueWithDeltasIsNotInvokedWhenDel EditorInfo outAttrs = new EditorInfo(); outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE; TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class))); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); CharSequence newText = "I do not fear computers. I fear the lack of them."; // Change InputTarget to FRAMEWORK_CLIENT. @@ -374,8 +387,10 @@ public void textEditingDelta_TestUpdateEditingValueIsNotInvokedWhenDeltaModelEna EditorInfo outAttrs = new EditorInfo(); outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE; TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class))); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); CharSequence newText = "I do not fear computers. I fear the lack of them."; final TextEditingDelta expectedDelta = new TextEditingDelta("", 0, 0, newText, newText.length(), newText.length(), 0, 49); @@ -503,8 +518,10 @@ public void textEditingDelta_TestDeltaIsCreatedWhenComposingTextSetIsInserting() EditorInfo outAttrs = new EditorInfo(); outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE; TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class))); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); CharSequence newText = "I do not fear computers. I fear the lack of them."; final TextEditingDelta expectedDelta = new TextEditingDelta("", 0, 0, newText, newText.length(), newText.length(), 0, 49); @@ -612,8 +629,10 @@ public void textEditingDelta_TestDeltaIsCreatedWhenComposingTextSetIsDeleting() EditorInfo outAttrs = new EditorInfo(); outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE; TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class))); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); CharSequence newText = "I do not fear computers. I fear the lack of them."; final TextEditingDelta expectedDelta = new TextEditingDelta( @@ -722,8 +741,10 @@ public void textEditingDelta_TestDeltaIsCreatedWhenComposingTextSetIsReplacing() EditorInfo outAttrs = new EditorInfo(); outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE; TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class))); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); CharSequence newText = "helfo"; final TextEditingDelta expectedDelta = new TextEditingDelta(newText, 0, 5, "hello", 5, 5, 0, 5); @@ -829,8 +850,10 @@ public void inputConnectionAdaptor_RepeatFilter() throws NullPointerException { EditorInfo outAttrs = new EditorInfo(); outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE; TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class))); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); // Change InputTarget to FRAMEWORK_CLIENT. textInputPlugin.setTextInputClient( @@ -920,8 +943,10 @@ public void setTextInputEditingState_doesNotRestartWhenTextIsIdentical() { testImm.setCurrentInputMethodSubtype(inputMethodSubtype); View testView = new View(ctx); TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); textInputPlugin.setTextInputClient( 0, new TextInputChannel.Configuration( @@ -958,8 +983,10 @@ public void setTextInputEditingState_alwaysSetEditableWhenDifferent() { testImm.setCurrentInputMethodSubtype(inputMethodSubtype); View testView = new View(ctx); TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); textInputPlugin.setTextInputClient( 0, new TextInputChannel.Configuration( @@ -1006,8 +1033,10 @@ public void setTextInputEditingState_restartsIMEOnlyWhenFrameworkChangesComposin testImm.setCurrentInputMethodSubtype(inputMethodSubtype); View testView = new View(ctx); TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); textInputPlugin.setTextInputClient( 0, new TextInputChannel.Configuration( @@ -1107,8 +1136,10 @@ public void setTextInputEditingState_nullInputMethodSubtype() { View testView = new View(ctx); TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); textInputPlugin.setTextInputClient( 0, new TextInputChannel.Configuration( @@ -1134,8 +1165,10 @@ public void setTextInputEditingState_nullInputMethodSubtype() { public void destroy_clearTextInputMethodHandler() { View testView = new View(ctx); TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class))); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); verify(textInputChannel, times(1)).setTextInputMethodHandler(isNotNull()); textInputPlugin.destroy(); verify(textInputChannel, times(1)).setTextInputMethodHandler(isNull()); @@ -1150,8 +1183,10 @@ private void verifyInputConnection(TextInputChannel.TextInputType textInputType) View testView = new View(ctx); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class))); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); textInputPlugin.setTextInputClient( 0, new TextInputChannel.Configuration( @@ -1225,8 +1260,10 @@ public void inputConnection_finishComposingTextUpdatesIMM() throws JSONException View testView = new View(ctx); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class))); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); textInputPlugin.setTextInputClient( 0, new TextInputChannel.Configuration( @@ -1263,8 +1300,10 @@ public void inputConnection_textInputTypeNone() { View testView = new View(ctx); DartExecutor dartExecutor = mock(DartExecutor.class); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); textInputPlugin.setTextInputClient( 0, new TextInputChannel.Configuration( @@ -1293,8 +1332,10 @@ public void showTextInput_textInputTypeNone() { View testView = new View(ctx); DartExecutor dartExecutor = mock(DartExecutor.class); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); textInputPlugin.setTextInputClient( 0, new TextInputChannel.Configuration( @@ -1321,8 +1362,10 @@ public void inputConnection_textInputTypeMultilineAndSuggestionsDisabled() { View testView = new View(ctx); DartExecutor dartExecutor = mock(DartExecutor.class); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); textInputPlugin.setTextInputClient( 0, new TextInputChannel.Configuration( @@ -1351,6 +1394,74 @@ public void inputConnection_textInputTypeMultilineAndSuggestionsDisabled() { | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); } + @Config(minSdk = API_LEVELS.API_34) + @TargetApi(API_LEVELS.API_34) + @Test + public void inputConnection_setsStylusHandwritingAvailable() { + View testView = new View(ctx); + DartExecutor dartExecutor = mock(DartExecutor.class); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); + TextInputPlugin textInputPlugin = + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); + textInputPlugin.setTextInputClient( + 0, + new TextInputChannel.Configuration( + false, + false, + true, + true, + false, + TextInputChannel.TextCapitalization.NONE, + new TextInputChannel.InputType(TextInputChannel.TextInputType.MULTILINE, false, false), + null, + null, + null, + null, + null)); + + EditorInfo editorInfo = new EditorInfo(); + InputConnection connection = + textInputPlugin.createInputConnection(testView, mock(KeyboardManager.class), editorInfo); + + assertTrue(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo)); + } + + @Config(sdk = API_LEVELS.API_32) + @TargetApi(API_LEVELS.API_32) + @Test + public void inputConnection_doesNotcallSetsStylusHandwritingAvailableWhenAPILevelUnsupported() { + View testView = new View(ctx); + DartExecutor dartExecutor = mock(DartExecutor.class); + TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); + TextInputPlugin textInputPlugin = + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); + textInputPlugin.setTextInputClient( + 0, + new TextInputChannel.Configuration( + false, + false, + true, + true, + false, + TextInputChannel.TextCapitalization.NONE, + new TextInputChannel.InputType(TextInputChannel.TextInputType.MULTILINE, false, false), + null, + null, + null, + null, + null)); + + EditorInfo editorInfo = new EditorInfo(); + InputConnection connection = + textInputPlugin.createInputConnection(testView, mock(KeyboardManager.class), editorInfo); + + assertFalse(EditorInfoCompat.isStylusHandwritingEnabled(editorInfo)); + } + // -------- Start: Autofill Tests ------- @Test public void autofill_enabledByDefault() { @@ -1359,8 +1470,10 @@ public void autofill_enabledByDefault() { } FlutterView testView = new FlutterView(ctx); TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); final TextInputChannel.Configuration.Autofill autofill = new TextInputChannel.Configuration.Autofill( "1", new String[] {}, null, new TextInputChannel.TextEditState("", 0, 0, -1, -1)); @@ -1419,8 +1532,10 @@ public void autofill_canBeDisabled() { } FlutterView testView = new FlutterView(ctx); TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); final TextInputChannel.Configuration.Autofill autofill = new TextInputChannel.Configuration.Autofill( "1", new String[] {}, null, new TextInputChannel.TextEditState("", 0, 0, -1, -1)); @@ -1456,8 +1571,10 @@ public void autofill_hintText() { } FlutterView testView = new FlutterView(ctx); TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); final TextInputChannel.Configuration.Autofill autofill = new TextInputChannel.Configuration.Autofill( "1", @@ -1501,8 +1618,10 @@ public void autofill_onProvideVirtualViewStructure() { } FlutterView testView = getTestView(); TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); final TextInputChannel.Configuration.Autofill autofill1 = new TextInputChannel.Configuration.Autofill( "1", @@ -1593,8 +1712,10 @@ public void autofill_onProvideVirtualViewStructure_singular_textfield() { // Migrate to ActivityScenario by following https://github.com/robolectric/robolectric/pull/4736 FlutterView testView = getTestView(); TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); final TextInputChannel.Configuration.Autofill autofill = new TextInputChannel.Configuration.Autofill( "1", @@ -1646,8 +1767,10 @@ public void autofill_testLifeCycle() { TestAfm testAfm = Shadow.extract(ctx.getSystemService(AutofillManager.class)); FlutterView testView = getTestView(); TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); // Set up an autofill scenario with 2 fields. final TextInputChannel.Configuration.Autofill autofill1 = @@ -1728,6 +1851,7 @@ public void autofill_testLifeCycle() { testView, 0, mock(TextInputChannel.class), + mock(ScribeChannel.class), mockKeyboardManager, (ListenableEditingState) textInputPlugin.getEditable(), new EditorInfo()); @@ -1782,8 +1906,10 @@ public void autofill_testAutofillUpdatesTheFramework() { TestAfm testAfm = Shadow.extract(ctx.getSystemService(AutofillManager.class)); FlutterView testView = getTestView(); TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class))); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); // Set up an autofill scenario with 2 fields. final TextInputChannel.Configuration.Autofill autofill1 = @@ -1877,8 +2003,10 @@ public void autofill_doesNotCrashAfterClearClientCall() { } FlutterView testView = new FlutterView(ctx); TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class))); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); // Set up an autofill scenario with 2 fields. final TextInputChannel.Configuration.Autofill autofillConfig = new TextInputChannel.Configuration.Autofill( @@ -1928,8 +2056,10 @@ public void autofill_testSetTextIpnutClientUpdatesSideFields() { TestAfm testAfm = Shadow.extract(ctx.getSystemService(AutofillManager.class)); FlutterView testView = getTestView(); TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); // Set up an autofill scenario with 2 fields. final TextInputChannel.Configuration.Autofill autofill1 = @@ -2041,6 +2171,7 @@ public void sendAppPrivateCommand_dataIsEmpty() throws JSONException { ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class); DartExecutor mockBinaryMessenger = mock(DartExecutor.class); TextInputChannel textInputChannel = new TextInputChannel(mockBinaryMessenger); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); EventHandler mockEventHandler = mock(EventHandler.class); TestImm testImm = Shadow.extract(ctx.getSystemService(Context.INPUT_METHOD_SERVICE)); @@ -2048,7 +2179,8 @@ public void sendAppPrivateCommand_dataIsEmpty() throws JSONException { View testView = new View(ctx); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); verify(mockBinaryMessenger, times(1)) .setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture()); @@ -2072,6 +2204,7 @@ public void sendAppPrivateCommand_hasData() throws JSONException { ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class); DartExecutor mockBinaryMessenger = mock(DartExecutor.class); TextInputChannel textInputChannel = new TextInputChannel(mockBinaryMessenger); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); EventHandler mockEventHandler = mock(EventHandler.class); TestImm testImm = Shadow.extract(ctx.getSystemService(Context.INPUT_METHOD_SERVICE)); @@ -2079,7 +2212,8 @@ public void sendAppPrivateCommand_hasData() throws JSONException { View testView = new View(ctx); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); verify(mockBinaryMessenger, times(1)) .setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture()); @@ -2108,8 +2242,10 @@ public void ime_windowInsetsSync_notLaidOutBehindNavigation_excludesNavigationBa when(testView.getWindowSystemUiVisibility()).thenReturn(View.SYSTEM_UI_FLAG_LAYOUT_STABLE); TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback(); FlutterEngine flutterEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni)); FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni)); @@ -2189,8 +2325,10 @@ public void ime_windowInsetsSync_laidOutBehindNavigation_includesNavigationBars( View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback(); FlutterEngine flutterEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni)); FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni)); @@ -2268,8 +2406,10 @@ public void lastWindowInsets_updatedOnSecondOnProgressCall() { when(testView.getWindowSystemUiVisibility()).thenReturn(View.SYSTEM_UI_FLAG_LAYOUT_STABLE); TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); TextInputPlugin textInputPlugin = - new TextInputPlugin(testView, textInputChannel, mock(PlatformViewsController.class)); + new TextInputPlugin( + testView, textInputChannel, scribeChannel, mock(PlatformViewsController.class)); ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback(); FlutterEngine flutterEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni)); FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni)); diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java index f9ebf55087fd4..4b36393b71924 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java @@ -44,6 +44,7 @@ import io.flutter.embedding.engine.systemchannels.MouseCursorChannel; import io.flutter.embedding.engine.systemchannels.PlatformViewsChannel; import io.flutter.embedding.engine.systemchannels.PlatformViewsChannel.PlatformViewTouch; +import io.flutter.embedding.engine.systemchannels.ScribeChannel; import io.flutter.embedding.engine.systemchannels.SettingsChannel; import io.flutter.embedding.engine.systemchannels.TextInputChannel; import io.flutter.plugin.common.MethodCall; @@ -1668,6 +1669,7 @@ public void scheduleFrame() {} when(engine.getMouseCursorChannel()).thenReturn(mock(MouseCursorChannel.class)); when(engine.getTextInputChannel()).thenReturn(mock(TextInputChannel.class)); when(engine.getSettingsChannel()).thenReturn(new SettingsChannel(executor)); + when(engine.getScribeChannel()).thenReturn(mock(ScribeChannel.class)); when(engine.getPlatformViewsController()).thenReturn(platformViewsController); when(engine.getLocalizationPlugin()).thenReturn(mock(LocalizationPlugin.class)); when(engine.getAccessibilityChannel()).thenReturn(mock(AccessibilityChannel.class));