Skip to content

Commit 05ddf05

Browse files
authored
Add Spell Check Support for Android Engine (#30858)
1 parent ccde8fd commit 05ddf05

File tree

16 files changed

+596
-0
lines changed

16 files changed

+596
-0
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1400,6 +1400,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/system
14001400
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java
14011401
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java
14021402
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SettingsChannel.java
1403+
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java
14031404
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SystemChannel.java
14041405
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java
14051406
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/common/ActivityLifecycleListener.java
@@ -1424,6 +1425,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/FlutterT
14241425
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java
14251426
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
14261427
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/ListenableEditingState.java
1428+
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/SpellCheckPlugin.java
14271429
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextEditingDelta.java
14281430
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
14291431
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/localization/LocalizationPlugin.java

lib/ui/platform_dispatcher.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,14 @@ class PlatformDispatcher {
842842
_onTextScaleFactorChangedZone = Zone.current;
843843
}
844844

845+
/// Whether the spell check service is supported on the current platform.
846+
///
847+
/// This option is used by [EditableTextState] to define its
848+
/// [SpellCheckConfiguration] when a default spell check service
849+
/// is requested.
850+
bool get nativeSpellCheckServiceDefined => _nativeSpellCheckServiceDefined;
851+
bool _nativeSpellCheckServiceDefined = false;
852+
845853
/// Whether briefly displaying the characters as you type in obscured text
846854
/// fields is enabled in system settings.
847855
///
@@ -903,6 +911,12 @@ class PlatformDispatcher {
903911

904912
final double textScaleFactor = (data['textScaleFactor']! as num).toDouble();
905913
final bool alwaysUse24HourFormat = data['alwaysUse24HourFormat']! as bool;
914+
final bool? nativeSpellCheckServiceDefined = data['nativeSpellCheckServiceDefined'] as bool?;
915+
if (nativeSpellCheckServiceDefined != null) {
916+
_nativeSpellCheckServiceDefined = nativeSpellCheckServiceDefined;
917+
} else {
918+
_nativeSpellCheckServiceDefined = false;
919+
}
906920
// This field is optional.
907921
final bool? brieflyShowPassword = data['brieflyShowPassword'] as bool?;
908922
if (brieflyShowPassword != null) {

lib/ui/window.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,15 @@ class SingletonFlutterWindow extends FlutterWindow {
425425
/// observe when this value changes.
426426
double get textScaleFactor => platformDispatcher.textScaleFactor;
427427

428+
/// Whether the spell check service is supported on the current platform.
429+
///
430+
/// {@macro dart.ui.window.accessorForwardWarning}
431+
///
432+
/// This option is used by [EditableTextState] to define its
433+
/// [SpellCheckConfiguration] when spell check is enabled, but no spell check
434+
/// service is specified.
435+
bool get nativeSpellCheckServiceDefined => platformDispatcher.nativeSpellCheckServiceDefined;
436+
428437
/// Whether briefly displaying the characters as you type in obscured text
429438
/// fields is enabled in system settings.
430439
///

lib/web_ui/lib/platform_dispatcher.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ abstract class PlatformDispatcher {
8282

8383
double get textScaleFactor => configuration.textScaleFactor;
8484

85+
bool get nativeSpellCheckServiceDefined => false;
86+
8587
bool get brieflyShowPassword => true;
8688

8789
VoidCallback? get onTextScaleFactorChanged;

lib/web_ui/lib/window.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ abstract class SingletonFlutterWindow extends FlutterWindow {
4848

4949
double get textScaleFactor => platformDispatcher.textScaleFactor;
5050

51+
bool get nativeSpellCheckServiceDefined => platformDispatcher.nativeSpellCheckServiceDefined;
52+
5153
bool get brieflyShowPassword => platformDispatcher.brieflyShowPassword;
5254

5355
bool get alwaysUse24HourFormat => platformDispatcher.alwaysUse24HourFormat;

shell/platform/android/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ android_java_sources = [
236236
"io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java",
237237
"io/flutter/embedding/engine/systemchannels/RestorationChannel.java",
238238
"io/flutter/embedding/engine/systemchannels/SettingsChannel.java",
239+
"io/flutter/embedding/engine/systemchannels/SpellCheckChannel.java",
239240
"io/flutter/embedding/engine/systemchannels/SystemChannel.java",
240241
"io/flutter/embedding/engine/systemchannels/TextInputChannel.java",
241242
"io/flutter/plugin/common/ActivityLifecycleListener.java",
@@ -260,6 +261,7 @@ android_java_sources = [
260261
"io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java",
261262
"io/flutter/plugin/editing/InputConnectionAdaptor.java",
262263
"io/flutter/plugin/editing/ListenableEditingState.java",
264+
"io/flutter/plugin/editing/SpellCheckPlugin.java",
263265
"io/flutter/plugin/editing/TextEditingDelta.java",
264266
"io/flutter/plugin/editing/TextInputPlugin.java",
265267
"io/flutter/plugin/localization/LocalizationPlugin.java",

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
import android.view.autofill.AutofillValue;
3636
import android.view.inputmethod.EditorInfo;
3737
import android.view.inputmethod.InputConnection;
38+
import android.view.textservice.SpellCheckerInfo;
39+
import android.view.textservice.TextServicesManager;
3840
import android.widget.FrameLayout;
3941
import androidx.annotation.NonNull;
4042
import androidx.annotation.Nullable;
@@ -58,6 +60,7 @@
5860
import io.flutter.embedding.engine.renderer.RenderSurface;
5961
import io.flutter.embedding.engine.systemchannels.SettingsChannel;
6062
import io.flutter.plugin.common.BinaryMessenger;
63+
import io.flutter.plugin.editing.SpellCheckPlugin;
6164
import io.flutter.plugin.editing.TextInputPlugin;
6265
import io.flutter.plugin.localization.LocalizationPlugin;
6366
import io.flutter.plugin.mouse.MouseCursorPlugin;
@@ -126,10 +129,12 @@ public class FlutterView extends FrameLayout
126129
// existing, stateless system channels, e.g., MouseCursorChannel, TextInputChannel, etc.
127130
@Nullable private MouseCursorPlugin mouseCursorPlugin;
128131
@Nullable private TextInputPlugin textInputPlugin;
132+
@Nullable private SpellCheckPlugin spellCheckPlugin;
129133
@Nullable private LocalizationPlugin localizationPlugin;
130134
@Nullable private KeyboardManager keyboardManager;
131135
@Nullable private AndroidTouchProcessor androidTouchProcessor;
132136
@Nullable private AccessibilityBridge accessibilityBridge;
137+
@Nullable private TextServicesManager textServicesManager;
133138

134139
// Provides access to foldable/hinge information
135140
@Nullable private WindowInfoRepositoryCallbackAdapterWrapper windowInfoRepo;
@@ -1141,6 +1146,17 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) {
11411146
this,
11421147
this.flutterEngine.getTextInputChannel(),
11431148
this.flutterEngine.getPlatformViewsController());
1149+
1150+
try {
1151+
textServicesManager =
1152+
(TextServicesManager)
1153+
getContext().getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE);
1154+
spellCheckPlugin =
1155+
new SpellCheckPlugin(textServicesManager, this.flutterEngine.getSpellCheckChannel());
1156+
} catch (Exception e) {
1157+
Log.e(TAG, "TextServicesManager not supported by device, spell check disabled.");
1158+
}
1159+
11441160
localizationPlugin = this.flutterEngine.getLocalizationPlugin();
11451161

11461162
keyboardManager = new KeyboardManager(this);
@@ -1238,6 +1254,9 @@ public void detachFromFlutterEngine() {
12381254
textInputPlugin.getInputMethodManager().restartInput(this);
12391255
textInputPlugin.destroy();
12401256
keyboardManager.destroy();
1257+
if (spellCheckPlugin != null) {
1258+
spellCheckPlugin.destroy();
1259+
}
12411260

12421261
if (mouseCursorPlugin != null) {
12431262
mouseCursorPlugin.destroy();
@@ -1422,10 +1441,34 @@ public void removeFlutterEngineAttachmentListener(
14221441
? SettingsChannel.PlatformBrightness.dark
14231442
: SettingsChannel.PlatformBrightness.light;
14241443

1444+
boolean isNativeSpellCheckServiceDefined = false;
1445+
1446+
if (textServicesManager != null) {
1447+
if (Build.VERSION.SDK_INT >= 31) {
1448+
List<SpellCheckerInfo> enabledSpellCheckerInfos =
1449+
textServicesManager.getEnabledSpellCheckerInfos();
1450+
boolean gboardSpellCheckerEnabled =
1451+
enabledSpellCheckerInfos.stream()
1452+
.anyMatch(
1453+
spellCheckerInfo ->
1454+
spellCheckerInfo
1455+
.getPackageName()
1456+
.equals("com.google.android.inputmethod.latin"));
1457+
1458+
// Checks if enabled spell checker is the one that is suppported by Gboard, which is
1459+
// the one Flutter supports by default.
1460+
isNativeSpellCheckServiceDefined =
1461+
textServicesManager.isSpellCheckerEnabled() && gboardSpellCheckerEnabled;
1462+
} else {
1463+
isNativeSpellCheckServiceDefined = true;
1464+
}
1465+
}
1466+
14251467
flutterEngine
14261468
.getSettingsChannel()
14271469
.startMessage()
14281470
.setTextScaleFactor(getResources().getConfiguration().fontScale)
1471+
.setNativeSpellCheckServiceDefined(isNativeSpellCheckServiceDefined)
14291472
.setBrieflyShowPassword(
14301473
Settings.System.getInt(
14311474
getContext().getContentResolver(), Settings.System.TEXT_SHOW_PASSWORD, 1)

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
3333
import io.flutter.embedding.engine.systemchannels.RestorationChannel;
3434
import io.flutter.embedding.engine.systemchannels.SettingsChannel;
35+
import io.flutter.embedding.engine.systemchannels.SpellCheckChannel;
3536
import io.flutter.embedding.engine.systemchannels.SystemChannel;
3637
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
3738
import io.flutter.plugin.localization.LocalizationPlugin;
@@ -93,6 +94,7 @@ public class FlutterEngine {
9394
@NonNull private final RestorationChannel restorationChannel;
9495
@NonNull private final PlatformChannel platformChannel;
9596
@NonNull private final SettingsChannel settingsChannel;
97+
@NonNull private final SpellCheckChannel spellCheckChannel;
9698
@NonNull private final SystemChannel systemChannel;
9799
@NonNull private final TextInputChannel textInputChannel;
98100

@@ -306,6 +308,7 @@ public FlutterEngine(
306308
platformChannel = new PlatformChannel(dartExecutor);
307309
restorationChannel = new RestorationChannel(dartExecutor, waitForRestorationData);
308310
settingsChannel = new SettingsChannel(dartExecutor);
311+
spellCheckChannel = new SpellCheckChannel(dartExecutor);
309312
systemChannel = new SystemChannel(dartExecutor);
310313
textInputChannel = new TextInputChannel(dartExecutor);
311314

@@ -550,6 +553,12 @@ public TextInputChannel getTextInputChannel() {
550553
return textInputChannel;
551554
}
552555

556+
/** System channel that sends and receives spell check requests and results. */
557+
@NonNull
558+
public SpellCheckChannel getSpellCheckChannel() {
559+
return spellCheckChannel;
560+
}
561+
553562
/**
554563
* Plugin registry, which registers plugins that want to be applied to this {@code FlutterEngine}.
555564
*/

shell/platform/android/io/flutter/embedding/engine/systemchannels/SettingsChannel.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public class SettingsChannel {
1313

1414
public static final String CHANNEL_NAME = "flutter/settings";
1515
private static final String TEXT_SCALE_FACTOR = "textScaleFactor";
16+
private static final String NATIVE_SPELL_CHECK_SERVICE_DEFINED = "nativeSpellCheckServiceDefined";
1617
private static final String BRIEFLY_SHOW_PASSWORD = "brieflyShowPassword";
1718
private static final String ALWAYS_USE_24_HOUR_FORMAT = "alwaysUse24HourFormat";
1819
private static final String PLATFORM_BRIGHTNESS = "platformBrightness";
@@ -42,6 +43,13 @@ public MessageBuilder setTextScaleFactor(float textScaleFactor) {
4243
return this;
4344
}
4445

46+
@NonNull
47+
public MessageBuilder setNativeSpellCheckServiceDefined(
48+
boolean nativeSpellCheckServiceDefined) {
49+
message.put(NATIVE_SPELL_CHECK_SERVICE_DEFINED, nativeSpellCheckServiceDefined);
50+
return this;
51+
}
52+
4553
@NonNull
4654
public MessageBuilder setBrieflyShowPassword(@NonNull boolean brieflyShowPassword) {
4755
message.put(BRIEFLY_SHOW_PASSWORD, brieflyShowPassword);
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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 androidx.annotation.NonNull;
8+
import androidx.annotation.Nullable;
9+
import io.flutter.Log;
10+
import io.flutter.embedding.engine.dart.DartExecutor;
11+
import io.flutter.plugin.common.JSONMethodCodec;
12+
import io.flutter.plugin.common.MethodCall;
13+
import io.flutter.plugin.common.MethodChannel;
14+
import org.json.JSONArray;
15+
import org.json.JSONException;
16+
17+
/**
18+
* {@link SpellCheckChannel} is a platform channel that is used by the framework to initiate spell
19+
* check in the embedding and for the embedding to send back the results.
20+
*
21+
* <p>When there is new text to be spell checked, the framework will send to the embedding the
22+
* message {@code SpellCheck.initiateSpellCheck} with the {@code String} locale to spell check with
23+
* and the {@code String} of text to spell check as arguments. In response, the {@link
24+
* io.flutter.plugin.editing.SpellCheckPlugin} will make a call to Android's spell check service to
25+
* fetch spell check results for the specified text.
26+
*
27+
* <p>Once the spell check results are received by the {@link
28+
* io.flutter.plugin.editing.SpellCheckPlugin}, it will send to the framework the message {@code
29+
* SpellCheck.updateSpellCheckResults} with the {@code ArrayList<String>} of encoded spell check
30+
* results (see {@link
31+
* io.flutter.plugin.editing.SpellCheckPlugin#onGetSentenceSuggestions(SentenceSuggestionsInfo[])}
32+
* for details) with the text that these results correspond to appeneded to the front as an
33+
* argument. For example, the argument may look like: {@code {"Hello, wrold!",
34+
* "7.11.world\nword\nold"}}.
35+
*
36+
* <p>{@link io.flutter.plugin.editing.SpellCheckPlugin} implements {@link SpellCheckMethodHandler}
37+
* to initiate spell check. Implement {@link SpellCheckMethodHandler} to respond to spell check
38+
* requests.
39+
*/
40+
public class SpellCheckChannel {
41+
private static final String TAG = "SpellCheckChannel";
42+
43+
public final MethodChannel channel;
44+
private SpellCheckMethodHandler spellCheckMethodHandler;
45+
46+
@NonNull
47+
public final MethodChannel.MethodCallHandler parsingMethodHandler =
48+
new MethodChannel.MethodCallHandler() {
49+
@Override
50+
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
51+
if (spellCheckMethodHandler == null) {
52+
Log.v(
53+
TAG,
54+
"No SpellCheckeMethodHandler registered, call not forwarded to spell check API.");
55+
return;
56+
}
57+
String method = call.method;
58+
Object args = call.arguments;
59+
Log.v(TAG, "Received '" + method + "' message.");
60+
switch (method) {
61+
case "SpellCheck.initiateSpellCheck":
62+
try {
63+
final JSONArray argumentList = (JSONArray) args;
64+
String locale = argumentList.getString(0);
65+
String text = argumentList.getString(1);
66+
spellCheckMethodHandler.initiateSpellCheck(locale, text, result);
67+
} catch (JSONException exception) {
68+
result.error("error", exception.getMessage(), null);
69+
}
70+
break;
71+
default:
72+
result.notImplemented();
73+
break;
74+
}
75+
}
76+
};
77+
78+
public SpellCheckChannel(@NonNull DartExecutor dartExecutor) {
79+
channel = new MethodChannel(dartExecutor, "flutter/spellcheck", JSONMethodCodec.INSTANCE);
80+
channel.setMethodCallHandler(parsingMethodHandler);
81+
}
82+
83+
/**
84+
* Sets the {@link SpellCheckMethodHandler} which receives all requests to spell check the
85+
* specified text sent through this channel.
86+
*/
87+
public void setSpellCheckMethodHandler(
88+
@Nullable SpellCheckMethodHandler spellCheckMethodHandler) {
89+
this.spellCheckMethodHandler = spellCheckMethodHandler;
90+
}
91+
92+
public interface SpellCheckMethodHandler {
93+
/**
94+
* Requests that spell check is initiated for the specified text, which will respond to the
95+
* {@code result} with either success if spell check results are received or error if the
96+
* request is skipped.
97+
*/
98+
void initiateSpellCheck(
99+
@NonNull String locale, @NonNull String text, @NonNull MethodChannel.Result result);
100+
}
101+
}

0 commit comments

Comments
 (0)