-
Notifications
You must be signed in to change notification settings - Fork 6k
Add Spell Check Support for Android Engine #30858
Changes from 1 commit
dc93719
cd234ad
f463846
3dfae0a
546692b
5f43ff5
ee73219
624f68b
846b195
b27704c
64f0ac7
0cc3776
33f2f40
eb3c731
1877242
5372aaa
d9adbdc
34809db
10d154d
00c228e
e08171f
5ccc144
87ab60f
f4083d7
ce45427
6972b0b
3eba4de
963d9ae
6ea2ede
32c7702
e9ebd5b
8298436
993b6c8
4179f42
f999488
ade72bc
c20b62c
49583d4
49939aa
0cf00f3
48a4354
510f83a
d8e333b
61908d3
29b095f
ee28058
4f0a074
6c4d437
d73b915
66c24ed
04a2ce1
15d17f5
e6414b2
2882e3b
dfe4689
cdc6b9c
65b8850
036b15b
188c2ad
8198d4e
a2b6c90
2aac372
894f4e9
2a4ea66
c2a6550
6125e74
74d1477
cc8bf5e
5a175ec
1c02784
3e74d61
975673c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,6 @@ | |
|
|
||
| import androidx.annotation.NonNull; | ||
| import androidx.annotation.Nullable; | ||
| import androidx.annotation.VisibleForTesting; | ||
| import io.flutter.Log; | ||
| import io.flutter.embedding.engine.dart.DartExecutor; | ||
| import io.flutter.plugin.common.JSONMethodCodec; | ||
|
|
@@ -12,20 +11,49 @@ | |
| import org.json.JSONArray; | ||
| import org.json.JSONException; | ||
|
|
||
| /** | ||
| * {@link SpellCheckChannel} is a platform channel that is used by Flutter to initiate spell check | ||
| * in the Android engine and for the Android engine to send back the results. | ||
| * | ||
| * <p>If the {@link io.flutter.plugin.editing.SpellCheckPlugin} is used to handle spell check | ||
| * behavior, (such is the case by default) then there is new text to be spell checked, Flutter will | ||
| * send a message to the engine. In response, the {@link io.flutter.plugin.editing.SpellCheckPlugin} | ||
| * will make a call to Android's spell check service to fetch spell check results for the specified | ||
| * text. | ||
| * | ||
| * <p>Once the spell check results are received by the {@link | ||
| * io.flutter.plugin.editing.SpellCheckPlugin}, a message will be sent back to Flutter with the | ||
| * results. See diagram below for overview: | ||
| * ----------------------------------------------------------------------------------------------------------------------------- | ||
| * | From | To | Message | Arguments | | ||
| * | ---------------------------------------------------------------------------------------------------------------------------| | ||
| * | Flutter | Android Engine | SpellCheck.iniateSpellCheck | {@code String} locale, {@code String} text | | ||
| * |----------------------------------------------------------------------------------------------------------------------------| | ||
| * | Android Engine | Flutter | SpellCheck.updateSpellCheckResults | {@code ArrayList} of results | | ||
| * ----------------------------------------------------------------------------------------------------------------------------- | ||
| * | ||
| * <p>By default, {@link io.flutter.plugin.editing.SpellCheckPlugin} implements {@link | ||
| * io.flutter.plugin.common.MethodChannel.MethodCallHandler} to initiate spell check via the Android | ||
| * spell check service. Implement {@link SpellCheckMethodHandler} to respond to Flutter spell check | ||
| * messages. | ||
| */ | ||
| public class SpellCheckChannel { | ||
camsim99 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| private static final String TAG = "SpellCheckChannel"; | ||
|
|
||
| @NonNull public final MethodChannel channel; | ||
| @Nullable private SpellCheckMethodHandler spellCheckMethodHandler; | ||
|
||
|
|
||
| @NonNull @VisibleForTesting | ||
| @NonNull | ||
| final MethodChannel.MethodCallHandler parsingMethodHandler = | ||
| new MethodChannel.MethodCallHandler() { | ||
| @Override | ||
| public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { | ||
| if (spellCheckMethodHandler == null) { | ||
| // If no explicit SpellCheckMethodHandler has been registered then we don't | ||
| // need to forward this call to an API. Return. | ||
camsim99 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Log.v( | ||
| TAG, | ||
| "No SpellCheckeMethodHandler registered, call not forwarded to spell check API."); | ||
| return; | ||
| } | ||
| String method = call.method; | ||
|
|
@@ -51,7 +79,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result | |
| }; | ||
|
|
||
| public SpellCheckChannel(@NonNull DartExecutor dartExecutor) { | ||
| this.channel = new MethodChannel(dartExecutor, "flutter/spellcheck", JSONMethodCodec.INSTANCE); | ||
| channel = new MethodChannel(dartExecutor, "flutter/spellcheck", JSONMethodCodec.INSTANCE); | ||
| channel.setMethodCallHandler(parsingMethodHandler); | ||
| } | ||
|
|
||
|
|
@@ -76,6 +104,6 @@ public interface SpellCheckMethodHandler { | |
| * SpellCheckChannel#setSpellCheckMethodHandler(SpellCheckMethodHandler)} once spell check | ||
| * results are received from the native spell check service. | ||
| */ | ||
| void initiateSpellCheck(String locale, String text); | ||
| void initiateSpellCheck(@NonNull String locale, @NonNull String text); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,31 +12,50 @@ | |
| import android.view.textservice.TextServicesManager; | ||
| import androidx.annotation.NonNull; | ||
| import io.flutter.embedding.engine.systemchannels.SpellCheckChannel; | ||
| import io.flutter.plugin.localization.LocalizationPlugin; | ||
| import java.util.ArrayList; | ||
| import java.util.Locale; | ||
|
|
||
| /** Android implementation of the spell check plugin. */ | ||
| public class SpellCheckPlugin implements SpellCheckerSession.SpellCheckerSessionListener { | ||
|
|
||
| @NonNull private final Context mContext; | ||
| @NonNull private final SpellCheckChannel mSpellCheckChannel; | ||
| @NonNull private final TextServicesManager tsm; | ||
| /** | ||
| * {@link SpellCheckPlugin} is the implementation of all functionality needed for spell check for | ||
| * text input. | ||
| * | ||
| * <p>The plugin handles requests for spell check sent by the {@link | ||
| * io.flutter.embedding.engine.systemchannels.SpellCheckChannel} via sending requests to the Android | ||
| * spell checker. It also receive the spell check results from the service and send them back to | ||
| * Flutter through the {@link io.flutter.embedding.engine.systemchannels.SpellCheckChannel}. | ||
camsim99 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| */ | ||
| public class SpellCheckPlugin | ||
| implements SpellCheckChannel.SpellCheckMethodHandler, | ||
| SpellCheckerSession.SpellCheckerSessionListener { | ||
|
|
||
| private final Context mContext; | ||
| private final SpellCheckChannel mSpellCheckChannel; | ||
| private final TextServicesManager mTextServicesManager; | ||
| private SpellCheckerSession mSpellCheckerSession; | ||
|
|
||
| // The maximum number of suggestions that the Android spell check service is allowed to provide | ||
| // per word. | ||
camsim99 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // Same number that is used by default for Android's {@code TextView}'s. | ||
| private static final int MAX_SPELL_CHECK_SUGGESTIONS = 5; | ||
camsim99 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| public SpellCheckPlugin(@NonNull Context context, @NonNull SpellCheckChannel spellCheckChannel) { | ||
| mContext = context; | ||
| mSpellCheckChannel = spellCheckChannel; | ||
| tsm = (TextServicesManager) mContext.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE); | ||
| mTextServicesManager = | ||
| (TextServicesManager) mContext.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE); | ||
|
|
||
| mSpellCheckChannel.setSpellCheckMethodHandler( | ||
| new SpellCheckChannel.SpellCheckMethodHandler() { | ||
| @Override | ||
| public void initiateSpellCheck(String locale, String text) { | ||
| performSpellCheck(locale, text); | ||
| } | ||
| }); | ||
| mSpellCheckChannel.setSpellCheckMethodHandler(this); | ||
| } | ||
|
|
||
| /** | ||
| * Unregisters this {@code SpellCheckPlugin} as the {@code | ||
| * SpellCheckChannel.SpellCheckMethodHandler}, for the {@link | ||
| * io.flutter.embedding.engine.systemchannels.SpellCheckChannel}, and closes the most recently | ||
| * opened {@code SpellCheckerSessions}. | ||
| * | ||
| * <p>Do not invoke any methods on a {@code SpellCheckPlugin} after invoking this method. | ||
| */ | ||
| public void destroy() { | ||
camsim99 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| mSpellCheckChannel.setSpellCheckMethodHandler(null); | ||
|
|
||
|
|
@@ -45,62 +64,73 @@ public void destroy() { | |
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void initiateSpellCheck(@NonNull String locale, @NonNull String text) { | ||
| performSpellCheck(locale, text); | ||
|
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. Handle case when there's a pending
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. The approach of dropping older requests has a similar fault to the approach I pursued previously of saving the result and text awaiting a response to that result in the We already knew that, but our assumption was that if the Given all of that, I decided to have the plugin only handle one request at a time by dropping new requests when an old one is still pending. This approach may lead to results lagging slightly behind, but (i) the spell check results in any given response will correspond to the text in that response, (ii) the spell check results will be returned in chronological order, and (iii) any impacts of these results lagging behind can be handled by the logic on the framework side. This is all because with this approach, we do not have to battle the unpredictable behavior of the Android spell checker. I believe also that in further iterations, other speedup techniques can be more easily explored given these guarantees.
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. Does this approach seem to work with decent performance in practice? Maybe this is the most solid approach in the real world even though it's not the fastest theoretically.
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. Also, is there ever a risk of the spell checker never responding and locking up or is that another thing that we shouldn't worry about in practice?
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.
Yes, exactly. From playing around with it (paired with making the corrections on the framework side if the process of returning results lag behind), I don't see any difference. If you are open to playing around with it, too, that'd be great!
I think we can rely on Android for this. Android seems to have some handling for issues like that (see here). |
||
| } | ||
|
|
||
| /** Calls on the Android spell check API to spell check specified text. */ | ||
| public void performSpellCheck(String locale, String text) { | ||
camsim99 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| String[] localeCodes = locale.split("-"); | ||
| Locale parsedLocale; | ||
|
|
||
| if (localeCodes.length == 3) { | ||
| parsedLocale = new Locale(localeCodes[0], localeCodes[1], localeCodes[2]); | ||
| } else if (localeCodes.length == 2) { | ||
| parsedLocale = new Locale(localeCodes[0], localeCodes[1]); | ||
| } else { | ||
| parsedLocale = new Locale(localeCodes[0]); | ||
| } | ||
| Locale localeFromString = LocalizationPlugin.localeFromString(locale); | ||
|
|
||
| if (mSpellCheckerSession != null) { | ||
| mSpellCheckerSession.close(); | ||
|
||
| } | ||
| mSpellCheckerSession = tsm.newSpellCheckerSession(null, parsedLocale, this, true); | ||
| mSpellCheckerSession = | ||
|
||
| mTextServicesManager.newSpellCheckerSession( | ||
| null, | ||
| localeFromString, | ||
| this, | ||
| /** referToSpellCheckerLanguageSettings= */ | ||
| true); | ||
|
|
||
| TextInfo[] textInfos = new TextInfo[] {new TextInfo(text)}; | ||
| mSpellCheckerSession.getSentenceSuggestions(textInfos, 3); | ||
| mSpellCheckerSession.getSentenceSuggestions(textInfos, MAX_SPELL_CHECK_SUGGESTIONS); | ||
camsim99 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /** | ||
| * Callback for Android spell check API that decomposes results and send results through the | ||
| * {@link SpellCheckChannel}. | ||
| * | ||
| * <p>Spell check results will be encoded as a string representing the span of that result, with | ||
| * the format [start_index.end_index.suggestion_1,suggestion_2,suggestion_3] where there may be up | ||
| * to 5 suggestions. | ||
| */ | ||
| @Override | ||
| public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) { | ||
| ArrayList<String> spellCheckerSuggestionSpans = new ArrayList<String>(); | ||
|
|
||
| for (int i = 0; i < results[0].getSuggestionsCount(); i++) { | ||
| SuggestionsInfo suggestionsInfo = results[0].getSuggestionsInfoAt(i); | ||
| int suggestionsCount = suggestionsInfo.getSuggestionsCount(); | ||
| if (results.length > 0) { | ||
camsim99 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| SentenceSuggestionsInfo spellCheckResults = results[0]; | ||
|
|
||
| if (suggestionsCount > 0) { | ||
| String spellCheckerSuggestionSpan = ""; | ||
| int start = results[0].getOffsetAt(i); | ||
| int length = results[0].getLengthAt(i); | ||
| for (int i = 0; i < spellCheckResults.getSuggestionsCount(); i++) { | ||
|
|
||
camsim99 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| spellCheckerSuggestionSpan += (String.valueOf(start) + "."); | ||
| spellCheckerSuggestionSpan += (String.valueOf(start + (length - 1)) + "."); | ||
| SuggestionsInfo suggestionsInfo = spellCheckResults.getSuggestionsInfoAt(i); | ||
| int suggestionsCount = suggestionsInfo.getSuggestionsCount(); | ||
|
|
||
| for (int j = 0; j < suggestionsCount; j++) { | ||
| spellCheckerSuggestionSpan += (suggestionsInfo.getSuggestionAt(j) + ","); | ||
| } | ||
| if (suggestionsCount > 0) { | ||
camsim99 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| String spellCheckerSuggestionSpan = ""; | ||
| int start = spellCheckResults.getOffsetAt(i); | ||
| int length = spellCheckResults.getLengthAt(i); | ||
camsim99 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| spellCheckerSuggestionSpan += (String.valueOf(start) + "."); | ||
camsim99 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| spellCheckerSuggestionSpan += (String.valueOf(start + (length - 1)) + "."); | ||
camsim99 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| spellCheckerSuggestionSpans.add( | ||
| spellCheckerSuggestionSpan.substring(0, spellCheckerSuggestionSpan.length() - 1)); | ||
| for (int j = 0; j < suggestionsCount; j++) { | ||
| spellCheckerSuggestionSpan += (suggestionsInfo.getSuggestionAt(j) + ","); | ||
|
||
| } | ||
|
|
||
| spellCheckerSuggestionSpans.add( | ||
| spellCheckerSuggestionSpan.substring(0, spellCheckerSuggestionSpan.length() - 1)); | ||
|
||
| } | ||
| } | ||
| } | ||
|
|
||
| mSpellCheckChannel.updateSpellCheckResults(spellCheckerSuggestionSpans); | ||
|
||
| } | ||
|
|
||
| @Override | ||
| @SuppressWarnings("deprecation") | ||
| public void onGetSuggestions(SuggestionsInfo[] results) { | ||
camsim99 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // Deprecated callback for Android spell check API; will not use. | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.