blob: 3f4ce4462ad14a01f79bc68754031b2a2d1494f4 [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 Toki76b51dc2017-07-13 23:37:11 +010071 if (skipTextClassification()) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080072 startActionMode(null);
73 } else {
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +010074 resetTextClassificationHelper(true /* resetSelectionTag */);
Makoto Onuki1488a3a2017-05-24 12:25:46 -070075 final TextView tv = mEditor.getTextView();
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080076 mTextClassificationAsyncTask = new TextClassificationAsyncTask(
Makoto Onuki1488a3a2017-05-24 12:25:46 -070077 tv,
Abodunrinwa Toki66c16272017-05-03 20:22:55 +010078 TIMEOUT_DURATION,
79 adjustSelection
80 ? mTextClassificationHelper::suggestSelection
81 : mTextClassificationHelper::classifyText,
82 this::startActionMode)
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080083 .execute();
84 }
85 }
86
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080087 public void invalidateActionModeAsync() {
88 cancelAsyncTask();
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +010089 if (skipTextClassification()) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080090 invalidateActionMode(null);
91 } else {
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +010092 resetTextClassificationHelper(false /* resetSelectionTag */);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080093 mTextClassificationAsyncTask = new TextClassificationAsyncTask(
94 mEditor.getTextView(), TIMEOUT_DURATION,
95 mTextClassificationHelper::classifyText, this::invalidateActionMode)
96 .execute();
97 }
98 }
99
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100100 public void onSelectionAction() {
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100101 mSelectionTracker.onSelectionAction(mTextClassificationHelper.getSelectionTag());
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100102 }
103
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100104 public boolean resetSelection(int textIndex) {
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100105 if (mSelectionTracker.resetSelection(
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100106 textIndex, mEditor, mTextClassificationHelper.getSelectionTag())) {
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000107 invalidateActionModeAsync();
108 return true;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800109 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000110 return false;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800111 }
112
113 @Nullable
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100114 public TextClassification getTextClassification() {
115 return mTextClassification;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800116 }
117
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000118 public void onDestroyActionMode() {
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100119 mSelectionTracker.onSelectionDestroyed();
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000120 cancelAsyncTask();
121 }
122
123 private void cancelAsyncTask() {
124 if (mTextClassificationAsyncTask != null) {
125 mTextClassificationAsyncTask.cancel(true);
126 mTextClassificationAsyncTask = null;
127 }
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100128 mTextClassification = null;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000129 }
130
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100131 private boolean skipTextClassification() {
Abodunrinwa Toki792d8202017-03-06 23:51:11 +0000132 final TextView textView = mEditor.getTextView();
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100133 // No need to make an async call for a no-op TextClassifier.
134 final boolean noOpTextClassifier = textView.getTextClassifier() == TextClassifier.NO_OP;
135 // Do not call the TextClassifier if there is no selection.
136 final boolean noSelection = textView.getSelectionEnd() == textView.getSelectionStart();
137 // Do not call the TextClassifier if this is a password field.
138 final boolean password = textView.hasPasswordTransformationMethod()
139 || TextView.isPasswordInputType(textView.getInputType());
140 return noOpTextClassifier || noSelection || password;
Abodunrinwa Toki792d8202017-03-06 23:51:11 +0000141 }
142
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800143 private void startActionMode(@Nullable SelectionResult result) {
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000144 final TextView textView = mEditor.getTextView();
145 final CharSequence text = textView.getText();
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100146 mSelectionTracker.setOriginalSelection(
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000147 textView.getSelectionStart(), textView.getSelectionEnd());
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800148 if (result != null && text instanceof Spannable) {
149 Selection.setSelection((Spannable) text, result.mStart, result.mEnd);
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100150 mTextClassification = result.mClassification;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800151 } else {
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100152 mTextClassification = null;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800153 }
154 if (mEditor.startSelectionActionModeInternal()) {
155 final SelectionModifierCursorController controller = mEditor.getSelectionController();
156 if (controller != null) {
157 controller.show();
158 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000159 if (result != null) {
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100160 mSelectionTracker.onSelectionStarted(
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100161 result.mStart, result.mEnd, mTextClassificationHelper.getSelectionTag());
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000162 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800163 }
164 mEditor.setRestartActionModeOnNextRefresh(false);
165 mTextClassificationAsyncTask = null;
166 }
167
168 private void invalidateActionMode(@Nullable SelectionResult result) {
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100169 mTextClassification = result != null ? result.mClassification : null;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800170 final ActionMode actionMode = mEditor.getTextActionMode();
171 if (actionMode != null) {
172 actionMode.invalidate();
173 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000174 final TextView textView = mEditor.getTextView();
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100175 mSelectionTracker.onSelectionUpdated(
176 textView.getSelectionStart(), textView.getSelectionEnd(),
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100177 mTextClassificationHelper.getSelectionTag());
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800178 mTextClassificationAsyncTask = null;
179 }
180
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100181 private void resetTextClassificationHelper(boolean resetSelectionTag) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800182 final TextView textView = mEditor.getTextView();
183 mTextClassificationHelper.reset(textView.getTextClassifier(), textView.getText(),
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +0000184 textView.getSelectionStart(), textView.getSelectionEnd(),
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100185 resetSelectionTag, textView.getTextLocales());
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800186 }
187
188 /**
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100189 * Tracks and logs smart selection changes.
190 * It is important to trigger this object's methods at the appropriate event so that it tracks
191 * smart selection events appropriately.
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000192 */
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100193 private static final class SelectionTracker {
194
195 // Log event: Smart selection happened.
196 private static final String LOG_EVENT_MULTI_SELECTION =
197 "textClassifier_multiSelection";
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100198 private static final String LOG_EVENT_SINGLE_SELECTION =
199 "textClassifier_singleSelection";
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100200
201 // Log event: Smart selection acted upon.
202 private static final String LOG_EVENT_MULTI_SELECTION_ACTION =
203 "textClassifier_multiSelection_action";
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100204 private static final String LOG_EVENT_SINGLE_SELECTION_ACTION =
205 "textClassifier_singleSelection_action";
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100206
207 // Log event: Smart selection was reset to original selection.
208 private static final String LOG_EVENT_MULTI_SELECTION_RESET =
209 "textClassifier_multiSelection_reset";
210
211 // Log event: Smart selection was user modified.
212 private static final String LOG_EVENT_MULTI_SELECTION_MODIFIED =
213 "textClassifier_multiSelection_modified";
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100214 private static final String LOG_EVENT_SINGLE_SELECTION_MODIFIED =
215 "textClassifier_singleSelection_modified";
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100216
217 private final TextClassifier mClassifier;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000218
219 private int mOriginalStart;
220 private int mOriginalEnd;
221 private int mSelectionStart;
222 private int mSelectionEnd;
223
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100224 private boolean mMultiSelection;
225 private boolean mClassifierSelection;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000226
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100227 SelectionTracker(TextClassifier classifier) {
228 mClassifier = classifier;
229 }
230
231 /**
232 * Called to initialize the original selection before smart selection is triggered.
233 */
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000234 public void setOriginalSelection(int selectionStart, int selectionEnd) {
235 mOriginalStart = selectionStart;
236 mOriginalEnd = selectionEnd;
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100237 resetSelectionFlags();
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000238 }
239
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100240 /**
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100241 * Called when selection action mode is started and the results come from a classifier.
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100242 * If the selection indices are different from the original selection indices, we have a
243 * smart selection.
244 */
245 public void onSelectionStarted(int selectionStart, int selectionEnd, String logTag) {
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100246 mClassifierSelection = !logTag.isEmpty();
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000247 mSelectionStart = selectionStart;
248 mSelectionEnd = selectionEnd;
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100249 // If the started selection is different from the original selection, we have a
250 // smart selection.
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100251 mMultiSelection =
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100252 mSelectionStart != mOriginalStart || mSelectionEnd != mOriginalEnd;
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100253 if (mMultiSelection) {
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100254 mClassifier.logEvent(logTag, LOG_EVENT_MULTI_SELECTION);
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100255 } else if (mClassifierSelection) {
256 mClassifier.logEvent(logTag, LOG_EVENT_SINGLE_SELECTION);
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100257 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000258 }
259
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100260 /**
261 * Called when selection bounds change.
262 */
263 public void onSelectionUpdated(int selectionStart, int selectionEnd, String logTag) {
264 final boolean selectionChanged =
265 selectionStart != mSelectionStart || selectionEnd != mSelectionEnd;
266 if (selectionChanged) {
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100267 if (mMultiSelection) {
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100268 mClassifier.logEvent(logTag, LOG_EVENT_MULTI_SELECTION_MODIFIED);
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100269 } else if (mClassifierSelection) {
270 mClassifier.logEvent(logTag, LOG_EVENT_SINGLE_SELECTION_MODIFIED);
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100271 }
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100272 resetSelectionFlags();
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100273 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000274 }
275
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100276 /**
277 * Called when the selection action mode is destroyed.
278 */
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000279 public void onSelectionDestroyed() {
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100280 resetSelectionFlags();
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000281 }
282
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100283 /**
284 * Logs if the action was taken on a smart selection.
285 */
286 public void onSelectionAction(String logTag) {
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100287 if (mMultiSelection) {
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100288 mClassifier.logEvent(logTag, LOG_EVENT_MULTI_SELECTION_ACTION);
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100289 } else if (mClassifierSelection) {
290 mClassifier.logEvent(logTag, LOG_EVENT_SINGLE_SELECTION_ACTION);
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100291 }
292 }
293
294 /**
295 * Returns true if the current smart selection should be reset to normal selection based on
296 * information that has been recorded about the original selection and the smart selection.
297 * The expected UX here is to allow the user to select a word inside of the smart selection
298 * on a single tap.
299 */
300 public boolean resetSelection(int textIndex, Editor editor, String logTag) {
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100301 final CharSequence text = editor.getTextView().getText();
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100302 if (mMultiSelection
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100303 && textIndex >= mSelectionStart && textIndex <= mSelectionEnd
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000304 && text instanceof Spannable) {
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000305 // Only allow a reset once.
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100306 resetSelectionFlags();
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100307 mClassifier.logEvent(logTag, LOG_EVENT_MULTI_SELECTION_RESET);
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100308 return editor.selectCurrentWord();
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000309 }
310 return false;
311 }
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100312
313 private void resetSelectionFlags() {
314 mMultiSelection = false;
315 mClassifierSelection = false;
316 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000317 }
318
319 /**
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800320 * AsyncTask for running a query on a background thread and returning the result on the
321 * UiThread. The AsyncTask times out after a specified time, returning a null result if the
322 * query has not yet returned.
323 */
324 private static final class TextClassificationAsyncTask
325 extends AsyncTask<Void, Void, SelectionResult> {
326
327 private final int mTimeOutDuration;
328 private final Supplier<SelectionResult> mSelectionResultSupplier;
329 private final Consumer<SelectionResult> mSelectionResultCallback;
330 private final TextView mTextView;
331 private final String mOriginalText;
332
333 /**
334 * @param textView the TextView
335 * @param timeOut time in milliseconds to timeout the query if it has not completed
336 * @param selectionResultSupplier fetches the selection results. Runs on a background thread
337 * @param selectionResultCallback receives the selection results. Runs on the UiThread
338 */
339 TextClassificationAsyncTask(
340 @NonNull TextView textView, int timeOut,
341 @NonNull Supplier<SelectionResult> selectionResultSupplier,
342 @NonNull Consumer<SelectionResult> selectionResultCallback) {
Makoto Onuki1488a3a2017-05-24 12:25:46 -0700343 super(textView != null ? textView.getHandler() : null);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800344 mTextView = Preconditions.checkNotNull(textView);
345 mTimeOutDuration = timeOut;
346 mSelectionResultSupplier = Preconditions.checkNotNull(selectionResultSupplier);
347 mSelectionResultCallback = Preconditions.checkNotNull(selectionResultCallback);
348 // Make a copy of the original text.
349 mOriginalText = mTextView.getText().toString();
350 }
351
352 @Override
353 @WorkerThread
354 protected SelectionResult doInBackground(Void... params) {
355 final Runnable onTimeOut = this::onTimeOut;
356 mTextView.postDelayed(onTimeOut, mTimeOutDuration);
357 final SelectionResult result = mSelectionResultSupplier.get();
358 mTextView.removeCallbacks(onTimeOut);
359 return result;
360 }
361
362 @Override
363 @UiThread
364 protected void onPostExecute(SelectionResult result) {
365 result = TextUtils.equals(mOriginalText, mTextView.getText()) ? result : null;
366 mSelectionResultCallback.accept(result);
367 }
368
369 private void onTimeOut() {
370 if (getStatus() == Status.RUNNING) {
371 onPostExecute(null);
372 }
373 cancel(true);
374 }
375 }
376
377 /**
378 * Helper class for querying the TextClassifier.
379 * It trims text so that only text necessary to provide context of the selected text is
380 * sent to the TextClassifier.
381 */
382 private static final class TextClassificationHelper {
383
Abodunrinwa Tokid2d13992017-03-24 21:43:13 +0000384 private static final int TRIM_DELTA = 120; // characters
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800385
386 private TextClassifier mTextClassifier;
387
388 /** The original TextView text. **/
389 private String mText;
390 /** Start index relative to mText. */
391 private int mSelectionStart;
392 /** End index relative to mText. */
393 private int mSelectionEnd;
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +0000394 private LocaleList mLocales;
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100395 /** A tag for the classifier that returned the latest smart selection. */
396 private String mSelectionTag = "";
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800397
398 /** Trimmed text starting from mTrimStart in mText. */
399 private CharSequence mTrimmedText;
400 /** Index indicating the start of mTrimmedText in mText. */
401 private int mTrimStart;
402 /** Start index relative to mTrimmedText */
403 private int mRelativeStart;
404 /** End index relative to mTrimmedText */
405 private int mRelativeEnd;
406
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +0100407 /** Information about the last classified text to avoid re-running a query. */
408 private CharSequence mLastClassificationText;
409 private int mLastClassificationSelectionStart;
410 private int mLastClassificationSelectionEnd;
411 private LocaleList mLastClassificationLocales;
412 private SelectionResult mLastClassificationResult;
413
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800414 TextClassificationHelper(TextClassifier textClassifier,
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +0000415 CharSequence text, int selectionStart, int selectionEnd, LocaleList locales) {
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100416 reset(textClassifier, text, selectionStart, selectionEnd, true, locales);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800417 }
418
419 @UiThread
420 public void reset(TextClassifier textClassifier,
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100421 CharSequence text, int selectionStart, int selectionEnd,
422 boolean resetSelectionTag, LocaleList locales) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800423 mTextClassifier = Preconditions.checkNotNull(textClassifier);
424 mText = Preconditions.checkNotNull(text).toString();
Abodunrinwa Toki08925e62017-05-12 13:48:50 +0100425 mLastClassificationText = null; // invalidate.
Abodunrinwa Toki792d8202017-03-06 23:51:11 +0000426 Preconditions.checkArgument(selectionEnd > selectionStart);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800427 mSelectionStart = selectionStart;
428 mSelectionEnd = selectionEnd;
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +0000429 mLocales = locales;
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100430 if (resetSelectionTag) {
431 mSelectionTag = "";
432 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800433 }
434
435 @WorkerThread
436 public SelectionResult classifyText() {
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +0100437 if (!Objects.equals(mText, mLastClassificationText)
438 || mSelectionStart != mLastClassificationSelectionStart
439 || mSelectionEnd != mLastClassificationSelectionEnd
440 || !Objects.equals(mLocales, mLastClassificationLocales)) {
441
442 mLastClassificationText = mText;
443 mLastClassificationSelectionStart = mSelectionStart;
444 mLastClassificationSelectionEnd = mSelectionEnd;
445 mLastClassificationLocales = mLocales;
446
447 trimText();
448 mLastClassificationResult = new SelectionResult(
449 mSelectionStart,
450 mSelectionEnd,
451 mTextClassifier.classifyText(
452 mTrimmedText, mRelativeStart, mRelativeEnd, mLocales));
453
454 }
455 return mLastClassificationResult;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800456 }
457
458 @WorkerThread
459 public SelectionResult suggestSelection() {
460 trimText();
461 final TextSelection sel = mTextClassifier.suggestSelection(
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +0000462 mTrimmedText, mRelativeStart, mRelativeEnd, mLocales);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800463 mSelectionStart = Math.max(0, sel.getSelectionStartIndex() + mTrimStart);
464 mSelectionEnd = Math.min(mText.length(), sel.getSelectionEndIndex() + mTrimStart);
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100465 mSelectionTag = sel.getSourceClassifier();
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800466 return classifyText();
467 }
468
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100469 String getSelectionTag() {
470 return mSelectionTag;
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100471 }
472
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800473 private void trimText() {
474 mTrimStart = Math.max(0, mSelectionStart - TRIM_DELTA);
475 final int referenceEnd = Math.min(mText.length(), mSelectionEnd + TRIM_DELTA);
476 mTrimmedText = mText.subSequence(mTrimStart, referenceEnd);
477 mRelativeStart = mSelectionStart - mTrimStart;
478 mRelativeEnd = mSelectionEnd - mTrimStart;
479 }
480 }
481
482 /**
483 * Selection result.
484 */
485 private static final class SelectionResult {
486 private final int mStart;
487 private final int mEnd;
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100488 private final TextClassification mClassification;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800489
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100490 SelectionResult(int start, int end, TextClassification classification) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800491 mStart = start;
492 mEnd = end;
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100493 mClassification = Preconditions.checkNotNull(classification);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800494 }
495 }
496}