blob: 142412ac8ccb0e7e97d7b5e79802a8fd374664aa [file] [log] [blame]
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.widget;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.annotation.UiThread;
22import android.annotation.WorkerThread;
23import android.os.AsyncTask;
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +000024import android.os.LocaleList;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080025import android.text.Selection;
26import android.text.Spannable;
27import android.text.TextUtils;
28import android.view.ActionMode;
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +010029import android.view.textclassifier.TextClassification;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080030import android.view.textclassifier.TextClassifier;
31import android.view.textclassifier.TextSelection;
32import android.widget.Editor.SelectionModifierCursorController;
33
34import com.android.internal.util.Preconditions;
35
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +010036import java.util.Objects;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080037import java.util.function.Consumer;
38import java.util.function.Supplier;
39
40/**
41 * Helper class for starting selection action mode
42 * (synchronously without the TextClassifier, asynchronously with the TextClassifier).
43 */
44@UiThread
45final class SelectionActionModeHelper {
46
47 /**
48 * Maximum time (in milliseconds) to wait for a result before timing out.
49 */
50 // TODO: Consider making this a ViewConfiguration.
51 private static final int TIMEOUT_DURATION = 200;
52
53 private final Editor mEditor;
54 private final TextClassificationHelper mTextClassificationHelper;
55
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +010056 private TextClassification mTextClassification;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080057 private AsyncTask mTextClassificationAsyncTask;
58
Abodunrinwa Toki1d775572017-05-08 16:03:01 +010059 private final SelectionTracker mSelectionTracker;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +000060
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080061 SelectionActionModeHelper(@NonNull Editor editor) {
62 mEditor = Preconditions.checkNotNull(editor);
63 final TextView textView = mEditor.getTextView();
64 mTextClassificationHelper = new TextClassificationHelper(
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +000065 textView.getTextClassifier(), textView.getText(), 0, 1, textView.getTextLocales());
Abodunrinwa Toki1d775572017-05-08 16:03:01 +010066 mSelectionTracker = new SelectionTracker(textView.getTextClassifier());
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080067 }
68
Abodunrinwa Toki66c16272017-05-03 20:22:55 +010069 public void startActionModeAsync(boolean adjustSelection) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080070 cancelAsyncTask();
Abodunrinwa Toki792d8202017-03-06 23:51:11 +000071 if (isNoOpTextClassifier() || !hasSelection()) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080072 // No need to make an async call for a no-op TextClassifier.
Abodunrinwa Toki792d8202017-03-06 23:51:11 +000073 // Do not call the TextClassifier if there is no selection.
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080074 startActionMode(null);
75 } else {
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +010076 resetTextClassificationHelper(true /* resetSelectionTag */);
Makoto Onuki1488a3a2017-05-24 12:25:46 -070077 final TextView tv = mEditor.getTextView();
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080078 mTextClassificationAsyncTask = new TextClassificationAsyncTask(
Makoto Onuki1488a3a2017-05-24 12:25:46 -070079 tv,
Abodunrinwa Toki66c16272017-05-03 20:22:55 +010080 TIMEOUT_DURATION,
81 adjustSelection
82 ? mTextClassificationHelper::suggestSelection
83 : mTextClassificationHelper::classifyText,
84 this::startActionMode)
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080085 .execute();
86 }
87 }
88
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080089 public void invalidateActionModeAsync() {
90 cancelAsyncTask();
Abodunrinwa Toki792d8202017-03-06 23:51:11 +000091 if (isNoOpTextClassifier() || !hasSelection()) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080092 // No need to make an async call for a no-op TextClassifier.
Abodunrinwa Toki792d8202017-03-06 23:51:11 +000093 // Do not call the TextClassifier if there is no selection.
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080094 invalidateActionMode(null);
95 } else {
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +010096 resetTextClassificationHelper(false /* resetSelectionTag */);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080097 mTextClassificationAsyncTask = new TextClassificationAsyncTask(
98 mEditor.getTextView(), TIMEOUT_DURATION,
99 mTextClassificationHelper::classifyText, this::invalidateActionMode)
100 .execute();
101 }
102 }
103
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100104 public void onSelectionAction() {
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100105 mSelectionTracker.onSelectionAction(mTextClassificationHelper.getSelectionTag());
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100106 }
107
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100108 public boolean resetSelection(int textIndex) {
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100109 if (mSelectionTracker.resetSelection(
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100110 textIndex, mEditor, mTextClassificationHelper.getSelectionTag())) {
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000111 invalidateActionModeAsync();
112 return true;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800113 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000114 return false;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800115 }
116
117 @Nullable
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100118 public TextClassification getTextClassification() {
119 return mTextClassification;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800120 }
121
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000122 public void onDestroyActionMode() {
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100123 mSelectionTracker.onSelectionDestroyed();
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000124 cancelAsyncTask();
125 }
126
127 private void cancelAsyncTask() {
128 if (mTextClassificationAsyncTask != null) {
129 mTextClassificationAsyncTask.cancel(true);
130 mTextClassificationAsyncTask = null;
131 }
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100132 mTextClassification = null;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000133 }
134
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800135 private boolean isNoOpTextClassifier() {
136 return mEditor.getTextView().getTextClassifier() == TextClassifier.NO_OP;
137 }
138
Abodunrinwa Toki792d8202017-03-06 23:51:11 +0000139 private boolean hasSelection() {
140 final TextView textView = mEditor.getTextView();
141 return textView.getSelectionEnd() > textView.getSelectionStart();
142 }
143
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800144 private void startActionMode(@Nullable SelectionResult result) {
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000145 final TextView textView = mEditor.getTextView();
146 final CharSequence text = textView.getText();
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100147 mSelectionTracker.setOriginalSelection(
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000148 textView.getSelectionStart(), textView.getSelectionEnd());
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800149 if (result != null && text instanceof Spannable) {
150 Selection.setSelection((Spannable) text, result.mStart, result.mEnd);
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100151 mTextClassification = result.mClassification;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800152 } else {
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100153 mTextClassification = null;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800154 }
155 if (mEditor.startSelectionActionModeInternal()) {
156 final SelectionModifierCursorController controller = mEditor.getSelectionController();
157 if (controller != null) {
158 controller.show();
159 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000160 if (result != null) {
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100161 mSelectionTracker.onSelectionStarted(
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100162 result.mStart, result.mEnd, mTextClassificationHelper.getSelectionTag());
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000163 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800164 }
165 mEditor.setRestartActionModeOnNextRefresh(false);
166 mTextClassificationAsyncTask = null;
167 }
168
169 private void invalidateActionMode(@Nullable SelectionResult result) {
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100170 mTextClassification = result != null ? result.mClassification : null;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800171 final ActionMode actionMode = mEditor.getTextActionMode();
172 if (actionMode != null) {
173 actionMode.invalidate();
174 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000175 final TextView textView = mEditor.getTextView();
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100176 mSelectionTracker.onSelectionUpdated(
177 textView.getSelectionStart(), textView.getSelectionEnd(),
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100178 mTextClassificationHelper.getSelectionTag());
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800179 mTextClassificationAsyncTask = null;
180 }
181
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100182 private void resetTextClassificationHelper(boolean resetSelectionTag) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800183 final TextView textView = mEditor.getTextView();
184 mTextClassificationHelper.reset(textView.getTextClassifier(), textView.getText(),
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +0000185 textView.getSelectionStart(), textView.getSelectionEnd(),
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100186 resetSelectionTag, textView.getTextLocales());
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800187 }
188
189 /**
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100190 * Tracks and logs smart selection changes.
191 * It is important to trigger this object's methods at the appropriate event so that it tracks
192 * smart selection events appropriately.
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000193 */
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100194 private static final class SelectionTracker {
195
196 // Log event: Smart selection happened.
197 private static final String LOG_EVENT_MULTI_SELECTION =
198 "textClassifier_multiSelection";
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100199 private static final String LOG_EVENT_SINGLE_SELECTION =
200 "textClassifier_singleSelection";
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100201
202 // Log event: Smart selection acted upon.
203 private static final String LOG_EVENT_MULTI_SELECTION_ACTION =
204 "textClassifier_multiSelection_action";
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100205 private static final String LOG_EVENT_SINGLE_SELECTION_ACTION =
206 "textClassifier_singleSelection_action";
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100207
208 // Log event: Smart selection was reset to original selection.
209 private static final String LOG_EVENT_MULTI_SELECTION_RESET =
210 "textClassifier_multiSelection_reset";
211
212 // Log event: Smart selection was user modified.
213 private static final String LOG_EVENT_MULTI_SELECTION_MODIFIED =
214 "textClassifier_multiSelection_modified";
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100215 private static final String LOG_EVENT_SINGLE_SELECTION_MODIFIED =
216 "textClassifier_singleSelection_modified";
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100217
218 private final TextClassifier mClassifier;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000219
220 private int mOriginalStart;
221 private int mOriginalEnd;
222 private int mSelectionStart;
223 private int mSelectionEnd;
224
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100225 private boolean mMultiSelection;
226 private boolean mClassifierSelection;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000227
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100228 SelectionTracker(TextClassifier classifier) {
229 mClassifier = classifier;
230 }
231
232 /**
233 * Called to initialize the original selection before smart selection is triggered.
234 */
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000235 public void setOriginalSelection(int selectionStart, int selectionEnd) {
236 mOriginalStart = selectionStart;
237 mOriginalEnd = selectionEnd;
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100238 resetSelectionFlags();
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000239 }
240
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100241 /**
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100242 * Called when selection action mode is started and the results come from a classifier.
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100243 * If the selection indices are different from the original selection indices, we have a
244 * smart selection.
245 */
246 public void onSelectionStarted(int selectionStart, int selectionEnd, String logTag) {
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100247 mClassifierSelection = !logTag.isEmpty();
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000248 mSelectionStart = selectionStart;
249 mSelectionEnd = selectionEnd;
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100250 // If the started selection is different from the original selection, we have a
251 // smart selection.
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100252 mMultiSelection =
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100253 mSelectionStart != mOriginalStart || mSelectionEnd != mOriginalEnd;
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100254 if (mMultiSelection) {
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100255 mClassifier.logEvent(logTag, LOG_EVENT_MULTI_SELECTION);
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100256 } else if (mClassifierSelection) {
257 mClassifier.logEvent(logTag, LOG_EVENT_SINGLE_SELECTION);
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100258 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000259 }
260
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100261 /**
262 * Called when selection bounds change.
263 */
264 public void onSelectionUpdated(int selectionStart, int selectionEnd, String logTag) {
265 final boolean selectionChanged =
266 selectionStart != mSelectionStart || selectionEnd != mSelectionEnd;
267 if (selectionChanged) {
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100268 if (mMultiSelection) {
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100269 mClassifier.logEvent(logTag, LOG_EVENT_MULTI_SELECTION_MODIFIED);
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100270 } else if (mClassifierSelection) {
271 mClassifier.logEvent(logTag, LOG_EVENT_SINGLE_SELECTION_MODIFIED);
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100272 }
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100273 resetSelectionFlags();
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100274 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000275 }
276
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100277 /**
278 * Called when the selection action mode is destroyed.
279 */
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000280 public void onSelectionDestroyed() {
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100281 resetSelectionFlags();
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000282 }
283
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100284 /**
285 * Logs if the action was taken on a smart selection.
286 */
287 public void onSelectionAction(String logTag) {
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100288 if (mMultiSelection) {
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100289 mClassifier.logEvent(logTag, LOG_EVENT_MULTI_SELECTION_ACTION);
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100290 } else if (mClassifierSelection) {
291 mClassifier.logEvent(logTag, LOG_EVENT_SINGLE_SELECTION_ACTION);
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100292 }
293 }
294
295 /**
296 * Returns true if the current smart selection should be reset to normal selection based on
297 * information that has been recorded about the original selection and the smart selection.
298 * The expected UX here is to allow the user to select a word inside of the smart selection
299 * on a single tap.
300 */
301 public boolean resetSelection(int textIndex, Editor editor, String logTag) {
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100302 final CharSequence text = editor.getTextView().getText();
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100303 if (mMultiSelection
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100304 && textIndex >= mSelectionStart && textIndex <= mSelectionEnd
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000305 && text instanceof Spannable) {
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000306 // Only allow a reset once.
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100307 resetSelectionFlags();
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100308 mClassifier.logEvent(logTag, LOG_EVENT_MULTI_SELECTION_RESET);
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100309 return editor.selectCurrentWord();
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000310 }
311 return false;
312 }
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100313
314 private void resetSelectionFlags() {
315 mMultiSelection = false;
316 mClassifierSelection = false;
317 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000318 }
319
320 /**
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800321 * AsyncTask for running a query on a background thread and returning the result on the
322 * UiThread. The AsyncTask times out after a specified time, returning a null result if the
323 * query has not yet returned.
324 */
325 private static final class TextClassificationAsyncTask
326 extends AsyncTask<Void, Void, SelectionResult> {
327
328 private final int mTimeOutDuration;
329 private final Supplier<SelectionResult> mSelectionResultSupplier;
330 private final Consumer<SelectionResult> mSelectionResultCallback;
331 private final TextView mTextView;
332 private final String mOriginalText;
333
334 /**
335 * @param textView the TextView
336 * @param timeOut time in milliseconds to timeout the query if it has not completed
337 * @param selectionResultSupplier fetches the selection results. Runs on a background thread
338 * @param selectionResultCallback receives the selection results. Runs on the UiThread
339 */
340 TextClassificationAsyncTask(
341 @NonNull TextView textView, int timeOut,
342 @NonNull Supplier<SelectionResult> selectionResultSupplier,
343 @NonNull Consumer<SelectionResult> selectionResultCallback) {
Makoto Onuki1488a3a2017-05-24 12:25:46 -0700344 super(textView != null ? textView.getHandler() : null);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800345 mTextView = Preconditions.checkNotNull(textView);
346 mTimeOutDuration = timeOut;
347 mSelectionResultSupplier = Preconditions.checkNotNull(selectionResultSupplier);
348 mSelectionResultCallback = Preconditions.checkNotNull(selectionResultCallback);
349 // Make a copy of the original text.
350 mOriginalText = mTextView.getText().toString();
351 }
352
353 @Override
354 @WorkerThread
355 protected SelectionResult doInBackground(Void... params) {
356 final Runnable onTimeOut = this::onTimeOut;
357 mTextView.postDelayed(onTimeOut, mTimeOutDuration);
358 final SelectionResult result = mSelectionResultSupplier.get();
359 mTextView.removeCallbacks(onTimeOut);
360 return result;
361 }
362
363 @Override
364 @UiThread
365 protected void onPostExecute(SelectionResult result) {
366 result = TextUtils.equals(mOriginalText, mTextView.getText()) ? result : null;
367 mSelectionResultCallback.accept(result);
368 }
369
370 private void onTimeOut() {
371 if (getStatus() == Status.RUNNING) {
372 onPostExecute(null);
373 }
374 cancel(true);
375 }
376 }
377
378 /**
379 * Helper class for querying the TextClassifier.
380 * It trims text so that only text necessary to provide context of the selected text is
381 * sent to the TextClassifier.
382 */
383 private static final class TextClassificationHelper {
384
Abodunrinwa Tokid2d13992017-03-24 21:43:13 +0000385 private static final int TRIM_DELTA = 120; // characters
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800386
387 private TextClassifier mTextClassifier;
388
389 /** The original TextView text. **/
390 private String mText;
391 /** Start index relative to mText. */
392 private int mSelectionStart;
393 /** End index relative to mText. */
394 private int mSelectionEnd;
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +0000395 private LocaleList mLocales;
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100396 /** A tag for the classifier that returned the latest smart selection. */
397 private String mSelectionTag = "";
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800398
399 /** Trimmed text starting from mTrimStart in mText. */
400 private CharSequence mTrimmedText;
401 /** Index indicating the start of mTrimmedText in mText. */
402 private int mTrimStart;
403 /** Start index relative to mTrimmedText */
404 private int mRelativeStart;
405 /** End index relative to mTrimmedText */
406 private int mRelativeEnd;
407
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +0100408 /** Information about the last classified text to avoid re-running a query. */
409 private CharSequence mLastClassificationText;
410 private int mLastClassificationSelectionStart;
411 private int mLastClassificationSelectionEnd;
412 private LocaleList mLastClassificationLocales;
413 private SelectionResult mLastClassificationResult;
414
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800415 TextClassificationHelper(TextClassifier textClassifier,
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +0000416 CharSequence text, int selectionStart, int selectionEnd, LocaleList locales) {
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100417 reset(textClassifier, text, selectionStart, selectionEnd, true, locales);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800418 }
419
420 @UiThread
421 public void reset(TextClassifier textClassifier,
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100422 CharSequence text, int selectionStart, int selectionEnd,
423 boolean resetSelectionTag, LocaleList locales) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800424 mTextClassifier = Preconditions.checkNotNull(textClassifier);
425 mText = Preconditions.checkNotNull(text).toString();
Abodunrinwa Toki08925e62017-05-12 13:48:50 +0100426 mLastClassificationText = null; // invalidate.
Abodunrinwa Toki792d8202017-03-06 23:51:11 +0000427 Preconditions.checkArgument(selectionEnd > selectionStart);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800428 mSelectionStart = selectionStart;
429 mSelectionEnd = selectionEnd;
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +0000430 mLocales = locales;
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100431 if (resetSelectionTag) {
432 mSelectionTag = "";
433 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800434 }
435
436 @WorkerThread
437 public SelectionResult classifyText() {
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +0100438 if (!Objects.equals(mText, mLastClassificationText)
439 || mSelectionStart != mLastClassificationSelectionStart
440 || mSelectionEnd != mLastClassificationSelectionEnd
441 || !Objects.equals(mLocales, mLastClassificationLocales)) {
442
443 mLastClassificationText = mText;
444 mLastClassificationSelectionStart = mSelectionStart;
445 mLastClassificationSelectionEnd = mSelectionEnd;
446 mLastClassificationLocales = mLocales;
447
448 trimText();
449 mLastClassificationResult = new SelectionResult(
450 mSelectionStart,
451 mSelectionEnd,
452 mTextClassifier.classifyText(
453 mTrimmedText, mRelativeStart, mRelativeEnd, mLocales));
454
455 }
456 return mLastClassificationResult;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800457 }
458
459 @WorkerThread
460 public SelectionResult suggestSelection() {
461 trimText();
462 final TextSelection sel = mTextClassifier.suggestSelection(
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +0000463 mTrimmedText, mRelativeStart, mRelativeEnd, mLocales);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800464 mSelectionStart = Math.max(0, sel.getSelectionStartIndex() + mTrimStart);
465 mSelectionEnd = Math.min(mText.length(), sel.getSelectionEndIndex() + mTrimStart);
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100466 mSelectionTag = sel.getSourceClassifier();
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800467 return classifyText();
468 }
469
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100470 String getSelectionTag() {
471 return mSelectionTag;
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100472 }
473
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800474 private void trimText() {
475 mTrimStart = Math.max(0, mSelectionStart - TRIM_DELTA);
476 final int referenceEnd = Math.min(mText.length(), mSelectionEnd + TRIM_DELTA);
477 mTrimmedText = mText.subSequence(mTrimStart, referenceEnd);
478 mRelativeStart = mSelectionStart - mTrimStart;
479 mRelativeEnd = mSelectionEnd - mTrimStart;
480 }
481 }
482
483 /**
484 * Selection result.
485 */
486 private static final class SelectionResult {
487 private final int mStart;
488 private final int mEnd;
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100489 private final TextClassification mClassification;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800490
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100491 SelectionResult(int start, int end, TextClassification classification) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800492 mStart = start;
493 mEnd = end;
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100494 mClassification = Preconditions.checkNotNull(classification);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800495 }
496 }
497}