Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit a489914

Browse files
authored
Scribe (Android stylus handwriting text input) (#52943)
The engine API for Android's Scribe stylus handwriting input. Just bare bones handwriting itself, does not support special gestures, which will come in subsequent PR(s).
1 parent de569ba commit a489914

File tree

16 files changed

+851
-39
lines changed

16 files changed

+851
-39
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44294,6 +44294,7 @@ ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/syst
4429444294
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java + ../../../flutter/LICENSE
4429544295
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java + ../../../flutter/LICENSE
4429644296
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java + ../../../flutter/LICENSE
44297+
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java + ../../../flutter/LICENSE
4429744298
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java + ../../../flutter/LICENSE
4429844299
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SystemChannel.java + ../../../flutter/LICENSE
4429944300
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
4431744318
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java + ../../../flutter/LICENSE
4431844319
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java + ../../../flutter/LICENSE
4431944320
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ListenableEditingState.java + ../../../flutter/LICENSE
44321+
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java + ../../../flutter/LICENSE
4432044322
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/SpellCheckPlugin.java + ../../../flutter/LICENSE
4432144323
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextEditingDelta.java + ../../../flutter/LICENSE
4432244324
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
4717647178
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java
4717747179
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java
4717847180
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java
47181+
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/ScribeChannel.java
4717947182
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SettingsChannel.java
4718047183
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java
4718147184
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
4720247205
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java
4720347206
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
4720447207
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ListenableEditingState.java
47208+
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ScribePlugin.java
4720547209
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/SpellCheckPlugin.java
4720647210
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextEditingDelta.java
4720747211
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java

shell/platform/android/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ android_java_sources = [
294294
"io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java",
295295
"io/flutter/embedding/engine/systemchannels/ProcessTextChannel.java",
296296
"io/flutter/embedding/engine/systemchannels/RestorationChannel.java",
297+
"io/flutter/embedding/engine/systemchannels/ScribeChannel.java",
297298
"io/flutter/embedding/engine/systemchannels/SettingsChannel.java",
298299
"io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java",
299300
"io/flutter/embedding/engine/systemchannels/SystemChannel.java",
@@ -320,6 +321,7 @@ android_java_sources = [
320321
"io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java",
321322
"io/flutter/plugin/editing/InputConnectionAdaptor.java",
322323
"io/flutter/plugin/editing/ListenableEditingState.java",
324+
"io/flutter/plugin/editing/ScribePlugin.java",
323325
"io/flutter/plugin/editing/SpellCheckPlugin.java",
324326
"io/flutter/plugin/editing/TextEditingDelta.java",
325327
"io/flutter/plugin/editing/TextInputPlugin.java",

shell/platform/android/io/flutter/embedding/android/FlutterView.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
import io.flutter.embedding.engine.renderer.RenderSurface;
6464
import io.flutter.embedding.engine.systemchannels.SettingsChannel;
6565
import io.flutter.plugin.common.BinaryMessenger;
66+
import io.flutter.plugin.editing.ScribePlugin;
6667
import io.flutter.plugin.editing.SpellCheckPlugin;
6768
import io.flutter.plugin.editing.TextInputPlugin;
6869
import io.flutter.plugin.localization.LocalizationPlugin;
@@ -133,6 +134,7 @@ public class FlutterView extends FrameLayout
133134
@Nullable private MouseCursorPlugin mouseCursorPlugin;
134135
@Nullable private TextInputPlugin textInputPlugin;
135136
@Nullable private SpellCheckPlugin spellCheckPlugin;
137+
@Nullable private ScribePlugin scribePlugin;
136138
@Nullable private LocalizationPlugin localizationPlugin;
137139
@Nullable private KeyboardManager keyboardManager;
138140
@Nullable private AndroidTouchProcessor androidTouchProcessor;
@@ -1120,10 +1122,12 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) {
11201122
if (Build.VERSION.SDK_INT >= API_LEVELS.API_24) {
11211123
mouseCursorPlugin = new MouseCursorPlugin(this, this.flutterEngine.getMouseCursorChannel());
11221124
}
1125+
11231126
textInputPlugin =
11241127
new TextInputPlugin(
11251128
this,
11261129
this.flutterEngine.getTextInputChannel(),
1130+
this.flutterEngine.getScribeChannel(),
11271131
this.flutterEngine.getPlatformViewsController());
11281132

11291133
try {
@@ -1136,6 +1140,10 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) {
11361140
Log.e(TAG, "TextServicesManager not supported by device, spell check disabled.");
11371141
}
11381142

1143+
scribePlugin =
1144+
new ScribePlugin(
1145+
this, textInputPlugin.getInputMethodManager(), this.flutterEngine.getScribeChannel());
1146+
11391147
localizationPlugin = this.flutterEngine.getLocalizationPlugin();
11401148

11411149
keyboardManager = new KeyboardManager(this);

shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
3535
import io.flutter.embedding.engine.systemchannels.ProcessTextChannel;
3636
import io.flutter.embedding.engine.systemchannels.RestorationChannel;
37+
import io.flutter.embedding.engine.systemchannels.ScribeChannel;
3738
import io.flutter.embedding.engine.systemchannels.SettingsChannel;
3839
import io.flutter.embedding.engine.systemchannels.SpellCheckChannel;
3940
import io.flutter.embedding.engine.systemchannels.SystemChannel;
@@ -100,6 +101,7 @@ public class FlutterEngine implements ViewUtils.DisplayUpdater {
100101
@NonNull private final RestorationChannel restorationChannel;
101102
@NonNull private final PlatformChannel platformChannel;
102103
@NonNull private final ProcessTextChannel processTextChannel;
104+
@NonNull private final ScribeChannel scribeChannel;
103105
@NonNull private final SettingsChannel settingsChannel;
104106
@NonNull private final SpellCheckChannel spellCheckChannel;
105107
@NonNull private final SystemChannel systemChannel;
@@ -337,6 +339,7 @@ public FlutterEngine(
337339
platformChannel = new PlatformChannel(dartExecutor);
338340
processTextChannel = new ProcessTextChannel(dartExecutor, context.getPackageManager());
339341
restorationChannel = new RestorationChannel(dartExecutor, waitForRestorationData);
342+
scribeChannel = new ScribeChannel(dartExecutor);
340343
settingsChannel = new SettingsChannel(dartExecutor);
341344
spellCheckChannel = new SpellCheckChannel(dartExecutor);
342345
systemChannel = new SystemChannel(dartExecutor);
@@ -610,6 +613,12 @@ public TextInputChannel getTextInputChannel() {
610613
return textInputChannel;
611614
}
612615

616+
/** System channel that sends and receives Scribe requests and results. */
617+
@NonNull
618+
public ScribeChannel getScribeChannel() {
619+
return scribeChannel;
620+
}
621+
613622
/** System channel that sends and receives spell check requests and results. */
614623
@NonNull
615624
public SpellCheckChannel getSpellCheckChannel() {
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.embedding.engine.systemchannels;
6+
7+
import static io.flutter.Build.API_LEVELS;
8+
9+
import android.annotation.TargetApi;
10+
import android.os.Build;
11+
import androidx.annotation.NonNull;
12+
import androidx.annotation.Nullable;
13+
import androidx.annotation.RequiresApi;
14+
import androidx.annotation.VisibleForTesting;
15+
import io.flutter.Log;
16+
import io.flutter.embedding.engine.dart.DartExecutor;
17+
import io.flutter.plugin.common.JSONMethodCodec;
18+
import io.flutter.plugin.common.MethodCall;
19+
import io.flutter.plugin.common.MethodChannel;
20+
21+
/**
22+
* {@link ScribeChannel} is a platform channel that is used by the framework to facilitate the
23+
* Scribe handwriting text input feature.
24+
*/
25+
public class ScribeChannel {
26+
private static final String TAG = "ScribeChannel";
27+
28+
@VisibleForTesting
29+
public static final String METHOD_IS_FEATURE_AVAILABLE = "Scribe.isFeatureAvailable";
30+
31+
@VisibleForTesting
32+
public static final String METHOD_IS_STYLUS_HANDWRITING_AVAILABLE =
33+
"Scribe.isStylusHandwritingAvailable";
34+
35+
@VisibleForTesting
36+
public static final String METHOD_START_STYLUS_HANDWRITING = "Scribe.startStylusHandwriting";
37+
38+
public final MethodChannel channel;
39+
private ScribeMethodHandler scribeMethodHandler;
40+
41+
@NonNull
42+
public final MethodChannel.MethodCallHandler parsingMethodHandler =
43+
new MethodChannel.MethodCallHandler() {
44+
@Override
45+
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
46+
if (scribeMethodHandler == null) {
47+
Log.v(TAG, "No ScribeMethodHandler registered. Scribe call not handled.");
48+
return;
49+
}
50+
String method = call.method;
51+
Log.v(TAG, "Received '" + method + "' message.");
52+
switch (method) {
53+
case METHOD_IS_FEATURE_AVAILABLE:
54+
isFeatureAvailable(call, result);
55+
break;
56+
case METHOD_IS_STYLUS_HANDWRITING_AVAILABLE:
57+
isStylusHandwritingAvailable(call, result);
58+
break;
59+
case METHOD_START_STYLUS_HANDWRITING:
60+
startStylusHandwriting(call, result);
61+
break;
62+
default:
63+
result.notImplemented();
64+
break;
65+
}
66+
}
67+
};
68+
69+
private void isFeatureAvailable(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
70+
try {
71+
final boolean isAvailable = scribeMethodHandler.isFeatureAvailable();
72+
result.success(isAvailable);
73+
} catch (IllegalStateException exception) {
74+
result.error("error", exception.getMessage(), null);
75+
}
76+
}
77+
78+
private void isStylusHandwritingAvailable(
79+
@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
80+
if (Build.VERSION.SDK_INT < API_LEVELS.API_34) {
81+
result.error("error", "Requires API level 34 or higher.", null);
82+
return;
83+
}
84+
85+
try {
86+
final boolean isAvailable = scribeMethodHandler.isStylusHandwritingAvailable();
87+
result.success(isAvailable);
88+
} catch (IllegalStateException exception) {
89+
result.error("error", exception.getMessage(), null);
90+
}
91+
}
92+
93+
private void startStylusHandwriting(
94+
@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
95+
if (Build.VERSION.SDK_INT < API_LEVELS.API_33) {
96+
result.error("error", "Requires API level 33 or higher.", null);
97+
return;
98+
}
99+
100+
try {
101+
scribeMethodHandler.startStylusHandwriting();
102+
result.success(null);
103+
} catch (IllegalStateException exception) {
104+
result.error("error", exception.getMessage(), null);
105+
}
106+
}
107+
108+
public ScribeChannel(@NonNull DartExecutor dartExecutor) {
109+
channel = new MethodChannel(dartExecutor, "flutter/scribe", JSONMethodCodec.INSTANCE);
110+
channel.setMethodCallHandler(parsingMethodHandler);
111+
}
112+
113+
/**
114+
* Sets the {@link ScribeMethodHandler} which receives all requests for scribe sent through this
115+
* channel.
116+
*/
117+
public void setScribeMethodHandler(@Nullable ScribeMethodHandler scribeMethodHandler) {
118+
this.scribeMethodHandler = scribeMethodHandler;
119+
}
120+
121+
public interface ScribeMethodHandler {
122+
/**
123+
* Responds to the {@code result} with success and a boolean indicating whether or not stylus
124+
* handwriting is available.
125+
*/
126+
boolean isFeatureAvailable();
127+
128+
/**
129+
* Responds to the {@code result} with success and a boolean indicating whether or not stylus
130+
* handwriting is available.
131+
*/
132+
@TargetApi(API_LEVELS.API_34)
133+
@RequiresApi(API_LEVELS.API_34)
134+
boolean isStylusHandwritingAvailable();
135+
136+
/**
137+
* Requests to start Scribe stylus handwriting, which will respond to the {@code result} with
138+
* either success if handwriting input has started or error otherwise.
139+
*/
140+
@TargetApi(API_LEVELS.API_33)
141+
@RequiresApi(API_LEVELS.API_33)
142+
void startStylusHandwriting();
143+
}
144+
145+
// TODO(justinmc): Scribe stylus gestures should be supported here.
146+
// https://github.com/flutter/flutter/issues/156018
147+
}

shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import androidx.core.view.inputmethod.InputConnectionCompat;
3434
import io.flutter.Log;
3535
import io.flutter.embedding.engine.FlutterJNI;
36+
import io.flutter.embedding.engine.systemchannels.ScribeChannel;
3637
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
3738
import java.io.ByteArrayOutputStream;
3839
import java.io.FileNotFoundException;
@@ -51,6 +52,7 @@ public interface KeyboardDelegate {
5152

5253
private final View mFlutterView;
5354
private final int mClient;
55+
private final ScribeChannel scribeChannel;
5456
private final TextInputChannel textInputChannel;
5557
private final ListenableEditingState mEditable;
5658
private final EditorInfo mEditorInfo;
@@ -69,6 +71,7 @@ public InputConnectionAdaptor(
6971
View view,
7072
int client,
7173
TextInputChannel textInputChannel,
74+
ScribeChannel scribeChannel,
7275
KeyboardDelegate keyboardDelegate,
7376
ListenableEditingState editable,
7477
EditorInfo editorInfo,
@@ -77,6 +80,7 @@ public InputConnectionAdaptor(
7780
mFlutterView = view;
7881
mClient = client;
7982
this.textInputChannel = textInputChannel;
83+
this.scribeChannel = scribeChannel;
8084
mEditable = editable;
8185
mEditable.addEditingStateListener(this);
8286
mEditorInfo = editorInfo;
@@ -100,10 +104,19 @@ public InputConnectionAdaptor(
100104
View view,
101105
int client,
102106
TextInputChannel textInputChannel,
107+
ScribeChannel scribeChannel,
103108
KeyboardDelegate keyboardDelegate,
104109
ListenableEditingState editable,
105110
EditorInfo editorInfo) {
106-
this(view, client, textInputChannel, keyboardDelegate, editable, editorInfo, new FlutterJNI());
111+
this(
112+
view,
113+
client,
114+
textInputChannel,
115+
scribeChannel,
116+
keyboardDelegate,
117+
editable,
118+
editorInfo,
119+
new FlutterJNI());
107120
}
108121

109122
private ExtractedText getExtractedText(ExtractedTextRequest request) {
@@ -262,6 +275,10 @@ public boolean setSelection(int start, int end) {
262275
return result;
263276
}
264277

278+
// TODO(justinmc): Scribe stylus gestures should be supported here via
279+
// performHandwritingGesture.
280+
// https://github.com/flutter/flutter/issues/156018
281+
265282
// Sanitizes the index to ensure the index is within the range of the
266283
// contents of editable.
267284
private static int clampIndexToEditable(int index, Editable editable) {

0 commit comments

Comments
 (0)