-
Notifications
You must be signed in to change notification settings - Fork 6k
Scribe (Android stylus handwriting text input) #52943
Changes from 42 commits
38f1ee1
f5c45e2
2c70875
2da6b2e
815e6cd
da21044
cdf7366
a298ad0
39249a5
14ff786
8217442
d5e0301
39a4c8e
27ca8db
521fb22
2a27c55
5046264
f9856a2
989eb2a
47bd6b7
948d387
c2f1425
5d385c5
0a54134
aa95c06
0962507
885f4bb
8ef8f49
49fa99b
585ac33
250adbc
e96d8bc
8056d17
016223e
cc69690
6f1a6d8
1ae66f1
f019841
d802eb2
3685c9d
0ceab93
bc4dafd
00621e2
53212fc
02c8f43
6d86717
44c22d5
06edcdb
df68cb8
37cab6c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| // 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.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"; | ||
|
|
||
| @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; | ||
| Object args = call.arguments; | ||
| Log.v(TAG, "Received '" + method + "' message."); | ||
| switch (method) { | ||
| 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 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", 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 { | ||
| /** | ||
| * 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(); | ||
| } | ||
|
|
||
| // TODO(justinmc): Scribe stylus gestures should be supported here. | ||
| // https://github.com/flutter/flutter/issues/156018 | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| // 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. | ||
| * | ||
| * <p>The plugin handles requests for scribe sent by the {@link | ||
| * io.flutter.embedding.engine.systemchannels.ScribeChannel}. | ||
| * | ||
| * <p>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 public View mView; | ||
|
||
|
|
||
| public ScribePlugin( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the view that is passed in here. Views in andoid IIRC do not have a long lifespan and this view is final. What happens when the app is backgrounded?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's the FlutterView from when ScribePlugin is created in FlutterView.java: scribePlugin =
new ScribePlugin(
this, textInputPlugin.getInputMethodManager(), this.flutterEngine.getScribeChannel());I need it in order to call InputMethodManager.startStylusHandwriting. Is there a better way to get ahold of the View at that time that's called maybe?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not so confident as to block the pr but I suspect that you can't make that variable final. That there is not a 1 to 1 relationship between flutter view and plugin instantiation. I am specifically thinking of add to app and cached engine scenarios. None of our existing tests would catch this because they would not execute this code path.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (From another thread) I've made a view setter so it can be updated if needed. |
||
| @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); | ||
| } | ||
|
|
||
| /** | ||
| * Unregisters this {@code ScribePlugin} as the {@code ScribeChannel.ScribeMethodHandler}, for the | ||
| * {@link io.flutter.embedding.engine.systemchannels.ScribeChannel}. | ||
| * | ||
| * <p>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. | ||
| * | ||
| * <p>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() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The public interface for ScribeMethodHandler does not annotate these methods as requiring api 34 so there is no expectation for callers that work on a ScribeMethodHandler that they should need to check api level. Either provide a branch inside the method that returns a default or modify the interface to have these annotations.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 I'll add the annotations to ScribeMethodHandler's methods. |
||
| return mInputMethodManager.isStylusHandwritingAvailable(); | ||
| } | ||
|
|
||
| /** | ||
| * Starts stylus handwriting input. | ||
| * | ||
| * <p>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() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here with interface vs implementation annotations. |
||
| mInputMethodManager.startStylusHandwriting(mView); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean that the dart side of the plugin needs to check if it is running on android and check the api level?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess in practice this probably means that you want to call this wrapped in a
try/catchin Dart. My thought process is that this method channel method is just a proxy for InputMethodManager.isStylusHandwritingAvailable. If I'm not even able to call that, I should error, rather than succeed withfalse, which the app developer might interpret to mean that InputMethodManager.isStylusHandwritingAvailable returned false.If that sounds reasonable then I'll at least make sure this is clearly documented in the framework.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update here: #52943 (comment)