blob: 45943f512c22fdd56b6b3c22e3b21bcb157e1d3e [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;
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +000023import android.app.RemoteAction;
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +010024import android.content.Context;
Petar Šegina5ab7bb22017-09-05 20:48:42 +010025import android.graphics.Canvas;
Petar Šegina91df3f92017-08-15 16:20:43 +010026import android.graphics.PointF;
Petar Šegina701ba332017-08-01 17:57:26 +010027import android.graphics.RectF;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080028import android.os.AsyncTask;
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +010029import android.os.Build;
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +000030import android.os.Bundle;
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +000031import android.os.LocaleList;
Petar Šegina701ba332017-08-01 17:57:26 +010032import android.text.Layout;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080033import android.text.Selection;
34import android.text.Spannable;
35import android.text.TextUtils;
Abodunrinwa Tokiadc19402018-11-22 17:10:25 +000036import android.text.util.Linkify;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +010037import android.util.Log;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080038import android.view.ActionMode;
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +000039import android.view.textclassifier.ExtrasUtils;
Abodunrinwa Tokif1d93992018-03-02 13:53:21 +000040import android.view.textclassifier.SelectionEvent;
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +000041import android.view.textclassifier.SelectionEvent.InvocationMethod;
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +010042import android.view.textclassifier.TextClassification;
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +000043import android.view.textclassifier.TextClassificationConstants;
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +000044import android.view.textclassifier.TextClassificationContext;
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +000045import android.view.textclassifier.TextClassificationManager;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080046import android.view.textclassifier.TextClassifier;
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +000047import android.view.textclassifier.TextClassifierEvent;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080048import android.view.textclassifier.TextSelection;
49import android.widget.Editor.SelectionModifierCursorController;
50
Petar Šegina91df3f92017-08-15 16:20:43 +010051import com.android.internal.annotations.VisibleForTesting;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080052import com.android.internal.util.Preconditions;
53
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +010054import java.text.BreakIterator;
Petar Šegina701ba332017-08-01 17:57:26 +010055import java.util.ArrayList;
Petar Šegina7c8196f2017-09-11 18:03:14 +010056import java.util.Comparator;
Petar Šegina701ba332017-08-01 17:57:26 +010057import java.util.List;
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +010058import java.util.Objects;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080059import java.util.function.Consumer;
Petar Šegina7c8196f2017-09-11 18:03:14 +010060import java.util.function.Function;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080061import java.util.function.Supplier;
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +010062import java.util.regex.Pattern;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080063
64/**
65 * Helper class for starting selection action mode
66 * (synchronously without the TextClassifier, asynchronously with the TextClassifier).
Petar Šegina91df3f92017-08-15 16:20:43 +010067 * @hide
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080068 */
69@UiThread
Petar Šegina91df3f92017-08-15 16:20:43 +010070@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
Petar Šeginaba1b8562017-08-31 18:09:16 +010071public final class SelectionActionModeHelper {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080072
Jan Althausb3513a12017-09-22 18:26:06 +020073 private static final String LOG_TAG = "SelectActionModeHelper";
74
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080075 private final Editor mEditor;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +010076 private final TextView mTextView;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080077 private final TextClassificationHelper mTextClassificationHelper;
78
Abodunrinwa Toki52096912018-03-21 23:14:42 +000079 @Nullable private TextClassification mTextClassification;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080080 private AsyncTask mTextClassificationAsyncTask;
81
Abodunrinwa Toki1d775572017-05-08 16:03:01 +010082 private final SelectionTracker mSelectionTracker;
Petar Šegina5ab7bb22017-09-05 20:48:42 +010083
84 // TODO remove nullable marker once the switch gating the feature gets removed
85 @Nullable
Petar Šegina701ba332017-08-01 17:57:26 +010086 private final SmartSelectSprite mSmartSelectSprite;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +000087
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080088 SelectionActionModeHelper(@NonNull Editor editor) {
Daulet Zhanguzincb0d19b2019-12-18 15:08:09 +000089 mEditor = Objects.requireNonNull(editor);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +010090 mTextView = mEditor.getTextView();
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080091 mTextClassificationHelper = new TextClassificationHelper(
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +010092 mTextView.getContext(),
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +000093 mTextView::getTextClassifier,
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +010094 getText(mTextView),
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +010095 0, 1, mTextView.getTextLocales());
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +010096 mSelectionTracker = new SelectionTracker(mTextView);
Petar Šegina701ba332017-08-01 17:57:26 +010097
Abodunrinwa Tokic2449b82018-05-01 21:36:48 +010098 if (getTextClassificationSettings().isSmartSelectionAnimationEnabled()) {
Petar Šegina5ab7bb22017-09-05 20:48:42 +010099 mSmartSelectSprite = new SmartSelectSprite(mTextView.getContext(),
Jan Althaus80620c52018-02-02 17:39:22 +0100100 editor.getTextView().mHighlightColor, mTextView::invalidate);
Petar Šegina701ba332017-08-01 17:57:26 +0100101 } else {
102 mSmartSelectSprite = null;
103 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800104 }
105
Richard Ledley26b87222017-11-30 10:54:08 +0000106 /**
107 * Starts Selection ActionMode.
108 */
109 public void startSelectionActionModeAsync(boolean adjustSelection) {
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100110 // Check if the smart selection should run for editable text.
Abodunrinwa Tokic2449b82018-05-01 21:36:48 +0100111 adjustSelection &= getTextClassificationSettings().isSmartSelectionEnabled();
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100112
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100113 mSelectionTracker.onOriginalSelection(
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100114 getText(mTextView),
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100115 mTextView.getSelectionStart(),
Jan Althaus92c6dec2018-02-02 09:20:14 +0100116 mTextView.getSelectionEnd(),
117 false /*isLink*/);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800118 cancelAsyncTask();
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100119 if (skipTextClassification()) {
Richard Ledley26b87222017-11-30 10:54:08 +0000120 startSelectionActionMode(null);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800121 } else {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100122 resetTextClassificationHelper();
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800123 mTextClassificationAsyncTask = new TextClassificationAsyncTask(
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100124 mTextView,
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +0100125 mTextClassificationHelper.getTimeoutDuration(),
Abodunrinwa Toki66c16272017-05-03 20:22:55 +0100126 adjustSelection
127 ? mTextClassificationHelper::suggestSelection
128 : mTextClassificationHelper::classifyText,
Petar Šegina701ba332017-08-01 17:57:26 +0100129 mSmartSelectSprite != null
Richard Ledley26b87222017-11-30 10:54:08 +0000130 ? this::startSelectionActionModeWithSmartSelectAnimation
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000131 : this::startSelectionActionMode,
132 mTextClassificationHelper::getOriginalSelection)
Richard Ledley26b87222017-11-30 10:54:08 +0000133 .execute();
134 }
135 }
136
137 /**
138 * Starts Link ActionMode.
139 */
Richard Ledley27db81b2018-03-01 12:34:55 +0000140 public void startLinkActionModeAsync(int start, int end) {
141 mSelectionTracker.onOriginalSelection(getText(mTextView), start, end, true /*isLink*/);
Richard Ledley26b87222017-11-30 10:54:08 +0000142 cancelAsyncTask();
143 if (skipTextClassification()) {
144 startLinkActionMode(null);
145 } else {
Richard Ledley27db81b2018-03-01 12:34:55 +0000146 resetTextClassificationHelper(start, end);
Richard Ledley26b87222017-11-30 10:54:08 +0000147 mTextClassificationAsyncTask = new TextClassificationAsyncTask(
148 mTextView,
149 mTextClassificationHelper.getTimeoutDuration(),
150 mTextClassificationHelper::classifyText,
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000151 this::startLinkActionMode,
152 mTextClassificationHelper::getOriginalSelection)
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800153 .execute();
154 }
155 }
156
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800157 public void invalidateActionModeAsync() {
158 cancelAsyncTask();
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100159 if (skipTextClassification()) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800160 invalidateActionMode(null);
161 } else {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100162 resetTextClassificationHelper();
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800163 mTextClassificationAsyncTask = new TextClassificationAsyncTask(
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100164 mTextView,
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +0100165 mTextClassificationHelper.getTimeoutDuration(),
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100166 mTextClassificationHelper::classifyText,
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000167 this::invalidateActionMode,
168 mTextClassificationHelper::getOriginalSelection)
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800169 .execute();
170 }
171 }
172
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000173 /** Reports a selection action event. */
174 public void onSelectionAction(int menuItemId, @Nullable String actionLabel) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100175 mSelectionTracker.onSelectionAction(
176 mTextView.getSelectionStart(), mTextView.getSelectionEnd(),
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000177 getActionType(menuItemId), actionLabel, mTextClassification);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100178 }
179
180 public void onSelectionDrag() {
181 mSelectionTracker.onSelectionAction(
182 mTextView.getSelectionStart(), mTextView.getSelectionEnd(),
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000183 SelectionEvent.ACTION_DRAG, /* actionLabel= */ null, mTextClassification);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100184 }
185
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100186 public void onTextChanged(int start, int end) {
187 mSelectionTracker.onTextChanged(start, end, mTextClassification);
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100188 }
189
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100190 public boolean resetSelection(int textIndex) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100191 if (mSelectionTracker.resetSelection(textIndex, mEditor)) {
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000192 invalidateActionModeAsync();
193 return true;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800194 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000195 return false;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800196 }
197
198 @Nullable
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100199 public TextClassification getTextClassification() {
200 return mTextClassification;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800201 }
202
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000203 public void onDestroyActionMode() {
Petar Šegina701ba332017-08-01 17:57:26 +0100204 cancelSmartSelectAnimation();
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100205 mSelectionTracker.onSelectionDestroyed();
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000206 cancelAsyncTask();
207 }
208
Petar Šegina5ab7bb22017-09-05 20:48:42 +0100209 public void onDraw(final Canvas canvas) {
Jan Althaus80620c52018-02-02 17:39:22 +0100210 if (isDrawingHighlight() && mSmartSelectSprite != null) {
Petar Šegina5ab7bb22017-09-05 20:48:42 +0100211 mSmartSelectSprite.draw(canvas);
212 }
213 }
214
Jan Althaus80620c52018-02-02 17:39:22 +0100215 public boolean isDrawingHighlight() {
216 return mSmartSelectSprite != null && mSmartSelectSprite.isAnimationActive();
217 }
218
Abodunrinwa Tokic2449b82018-05-01 21:36:48 +0100219 private TextClassificationConstants getTextClassificationSettings() {
220 return TextClassificationManager.getSettings(mTextView.getContext());
221 }
222
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000223 private void cancelAsyncTask() {
224 if (mTextClassificationAsyncTask != null) {
225 mTextClassificationAsyncTask.cancel(true);
226 mTextClassificationAsyncTask = null;
227 }
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100228 mTextClassification = null;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000229 }
230
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100231 private boolean skipTextClassification() {
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100232 // No need to make an async call for a no-op TextClassifier.
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000233 final boolean noOpTextClassifier = mTextView.usesNoOpTextClassifier();
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100234 // Do not call the TextClassifier if there is no selection.
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100235 final boolean noSelection = mTextView.getSelectionEnd() == mTextView.getSelectionStart();
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100236 // Do not call the TextClassifier if this is a password field.
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100237 final boolean password = mTextView.hasPasswordTransformationMethod()
238 || TextView.isPasswordInputType(mTextView.getInputType());
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100239 return noOpTextClassifier || noSelection || password;
Abodunrinwa Toki792d8202017-03-06 23:51:11 +0000240 }
241
Richard Ledley26b87222017-11-30 10:54:08 +0000242 private void startLinkActionMode(@Nullable SelectionResult result) {
243 startActionMode(Editor.TextActionMode.TEXT_LINK, result);
244 }
245
246 private void startSelectionActionMode(@Nullable SelectionResult result) {
247 startActionMode(Editor.TextActionMode.SELECTION, result);
248 }
249
250 private void startActionMode(
251 @Editor.TextActionMode int actionMode, @Nullable SelectionResult result) {
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100252 final CharSequence text = getText(mTextView);
Richard Ledley26b87222017-11-30 10:54:08 +0000253 if (result != null && text instanceof Spannable
Richard Ledley27db81b2018-03-01 12:34:55 +0000254 && (mTextView.isTextSelectable() || mTextView.isTextEditable())) {
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100255 // Do not change the selection if TextClassifier should be dark launched.
Abodunrinwa Tokic2449b82018-05-01 21:36:48 +0100256 if (!getTextClassificationSettings().isModelDarkLaunchEnabled()) {
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100257 Selection.setSelection((Spannable) text, result.mStart, result.mEnd);
Richard Ledley724eff92017-12-21 10:11:34 +0000258 mTextView.invalidate();
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100259 }
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100260 mTextClassification = result.mClassification;
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000261 } else if (result != null && actionMode == Editor.TextActionMode.TEXT_LINK) {
Richard Ledley27db81b2018-03-01 12:34:55 +0000262 mTextClassification = result.mClassification;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800263 } else {
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100264 mTextClassification = null;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800265 }
Richard Ledley26b87222017-11-30 10:54:08 +0000266 if (mEditor.startActionModeInternal(actionMode)) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800267 final SelectionModifierCursorController controller = mEditor.getSelectionController();
Richard Ledley26b87222017-11-30 10:54:08 +0000268 if (controller != null
269 && (mTextView.isTextSelectable() || mTextView.isTextEditable())) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800270 controller.show();
271 }
Richard Ledley724eff92017-12-21 10:11:34 +0000272 if (result != null) {
273 switch (actionMode) {
274 case Editor.TextActionMode.SELECTION:
275 mSelectionTracker.onSmartSelection(result);
276 break;
277 case Editor.TextActionMode.TEXT_LINK:
278 mSelectionTracker.onLinkSelected(result);
279 break;
280 default:
281 break;
282 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000283 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800284 }
285 mEditor.setRestartActionModeOnNextRefresh(false);
286 mTextClassificationAsyncTask = null;
287 }
288
Richard Ledley26b87222017-11-30 10:54:08 +0000289 private void startSelectionActionModeWithSmartSelectAnimation(
290 @Nullable SelectionResult result) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100291 final Layout layout = mTextView.getLayout();
Petar Šegina701ba332017-08-01 17:57:26 +0100292
Mihai Popacce6e822018-05-08 19:18:43 +0100293 final Runnable onAnimationEndCallback = () -> {
Mihai Popaa9d27ea2018-05-25 11:57:17 +0100294 final SelectionResult startSelectionResult;
295 if (result != null && result.mStart >= 0 && result.mEnd <= getText(mTextView).length()
Mihai Popa6849bd322018-05-21 15:34:16 +0100296 && result.mStart <= result.mEnd) {
Mihai Popaa9d27ea2018-05-25 11:57:17 +0100297 startSelectionResult = result;
298 } else {
299 startSelectionResult = null;
Mihai Popacce6e822018-05-08 19:18:43 +0100300 }
Mihai Popaa9d27ea2018-05-25 11:57:17 +0100301 startSelectionActionMode(startSelectionResult);
Mihai Popacce6e822018-05-08 19:18:43 +0100302 };
Petar Šegina701ba332017-08-01 17:57:26 +0100303 // TODO do not trigger the animation if the change included only non-printable characters
304 final boolean didSelectionChange =
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100305 result != null && (mTextView.getSelectionStart() != result.mStart
306 || mTextView.getSelectionEnd() != result.mEnd);
Petar Šegina701ba332017-08-01 17:57:26 +0100307
308 if (!didSelectionChange) {
309 onAnimationEndCallback.run();
310 return;
311 }
312
Petar Šegina7c8196f2017-09-11 18:03:14 +0100313 final List<SmartSelectSprite.RectangleWithTextSelectionLayout> selectionRectangles =
Petar Šegina701ba332017-08-01 17:57:26 +0100314 convertSelectionToRectangles(layout, result.mStart, result.mEnd);
315
Petar Šegina91df3f92017-08-15 16:20:43 +0100316 final PointF touchPoint = new PointF(
317 mEditor.getLastUpPositionX(),
318 mEditor.getLastUpPositionY());
Petar Šegina701ba332017-08-01 17:57:26 +0100319
Petar Šegina91df3f92017-08-15 16:20:43 +0100320 final PointF animationStartPoint =
Petar Šegina7c8196f2017-09-11 18:03:14 +0100321 movePointInsideNearestRectangle(touchPoint, selectionRectangles,
322 SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle);
Petar Šegina701ba332017-08-01 17:57:26 +0100323
324 mSmartSelectSprite.startAnimation(
Petar Šegina91df3f92017-08-15 16:20:43 +0100325 animationStartPoint,
Petar Šegina701ba332017-08-01 17:57:26 +0100326 selectionRectangles,
327 onAnimationEndCallback);
328 }
329
Petar Šegina7c8196f2017-09-11 18:03:14 +0100330 private List<SmartSelectSprite.RectangleWithTextSelectionLayout> convertSelectionToRectangles(
331 final Layout layout, final int start, final int end) {
332 final List<SmartSelectSprite.RectangleWithTextSelectionLayout> result = new ArrayList<>();
Petar Šegina72729252017-08-31 15:25:06 +0100333
Petar Šegina7c8196f2017-09-11 18:03:14 +0100334 final Layout.SelectionRectangleConsumer consumer =
335 (left, top, right, bottom, textSelectionLayout) -> mergeRectangleIntoList(
336 result,
337 new RectF(left, top, right, bottom),
338 SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle,
339 r -> new SmartSelectSprite.RectangleWithTextSelectionLayout(r,
340 textSelectionLayout)
341 );
342
343 layout.getSelection(start, end, consumer);
344
345 result.sort(Comparator.comparing(
346 SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle,
347 SmartSelectSprite.RECTANGLE_COMPARATOR));
348
Petar Šegina701ba332017-08-01 17:57:26 +0100349 return result;
350 }
351
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +0100352 // TODO: Move public pure functions out of this class and make it package-private.
Petar Šeginaba1b8562017-08-31 18:09:16 +0100353 /**
Petar Šegina7c8196f2017-09-11 18:03:14 +0100354 * Merges a {@link RectF} into an existing list of any objects which contain a rectangle.
355 * While merging, this method makes sure that:
Petar Šeginaba1b8562017-08-31 18:09:16 +0100356 *
357 * <ol>
358 * <li>No rectangle is redundant (contained within a bigger rectangle)</li>
359 * <li>Rectangles of the same height and vertical position that intersect get merged</li>
360 * </ol>
361 *
Petar Šegina7c8196f2017-09-11 18:03:14 +0100362 * @param list the list of rectangles (or other rectangle containers) to merge the new
363 * rectangle into
Petar Šeginaba1b8562017-08-31 18:09:16 +0100364 * @param candidate the {@link RectF} to merge into the list
Petar Šegina7c8196f2017-09-11 18:03:14 +0100365 * @param extractor a function that can extract a {@link RectF} from an element of the given
366 * list
367 * @param packer a function that can wrap the resulting {@link RectF} into an element that
368 * the list contains
Petar Šeginaba1b8562017-08-31 18:09:16 +0100369 * @hide
370 */
371 @VisibleForTesting
Petar Šegina7c8196f2017-09-11 18:03:14 +0100372 public static <T> void mergeRectangleIntoList(final List<T> list,
373 final RectF candidate, final Function<T, RectF> extractor,
374 final Function<RectF, T> packer) {
Petar Šeginaba1b8562017-08-31 18:09:16 +0100375 if (candidate.isEmpty()) {
376 return;
377 }
378
379 final int elementCount = list.size();
380 for (int index = 0; index < elementCount; ++index) {
Petar Šegina7c8196f2017-09-11 18:03:14 +0100381 final RectF existingRectangle = extractor.apply(list.get(index));
Petar Šeginaba1b8562017-08-31 18:09:16 +0100382 if (existingRectangle.contains(candidate)) {
383 return;
384 }
385 if (candidate.contains(existingRectangle)) {
386 existingRectangle.setEmpty();
387 continue;
388 }
389
390 final boolean rectanglesContinueEachOther = candidate.left == existingRectangle.right
391 || candidate.right == existingRectangle.left;
392 final boolean canMerge = candidate.top == existingRectangle.top
393 && candidate.bottom == existingRectangle.bottom
394 && (RectF.intersects(candidate, existingRectangle)
395 || rectanglesContinueEachOther);
396
397 if (canMerge) {
398 candidate.union(existingRectangle);
399 existingRectangle.setEmpty();
400 }
401 }
402
403 for (int index = elementCount - 1; index >= 0; --index) {
Petar Šegina7c8196f2017-09-11 18:03:14 +0100404 final RectF rectangle = extractor.apply(list.get(index));
405 if (rectangle.isEmpty()) {
Petar Šeginaba1b8562017-08-31 18:09:16 +0100406 list.remove(index);
407 }
408 }
409
Petar Šegina7c8196f2017-09-11 18:03:14 +0100410 list.add(packer.apply(candidate));
Petar Šeginaba1b8562017-08-31 18:09:16 +0100411 }
412
413
Petar Šegina91df3f92017-08-15 16:20:43 +0100414 /** @hide */
415 @VisibleForTesting
Petar Šegina7c8196f2017-09-11 18:03:14 +0100416 public static <T> PointF movePointInsideNearestRectangle(final PointF point,
417 final List<T> list, final Function<T, RectF> extractor) {
Petar Šegina91df3f92017-08-15 16:20:43 +0100418 float bestX = -1;
419 float bestY = -1;
420 double bestDistance = Double.MAX_VALUE;
421
Petar Šegina7c8196f2017-09-11 18:03:14 +0100422 final int elementCount = list.size();
Petar Šeginaba1b8562017-08-31 18:09:16 +0100423 for (int index = 0; index < elementCount; ++index) {
Petar Šegina7c8196f2017-09-11 18:03:14 +0100424 final RectF rectangle = extractor.apply(list.get(index));
Petar Šegina91df3f92017-08-15 16:20:43 +0100425 final float candidateY = rectangle.centerY();
426 final float candidateX;
427
428 if (point.x > rectangle.right) {
429 candidateX = rectangle.right;
430 } else if (point.x < rectangle.left) {
431 candidateX = rectangle.left;
432 } else {
433 candidateX = point.x;
434 }
435
436 final double candidateDistance = Math.pow(point.x - candidateX, 2)
437 + Math.pow(point.y - candidateY, 2);
438
439 if (candidateDistance < bestDistance) {
440 bestX = candidateX;
441 bestY = candidateY;
442 bestDistance = candidateDistance;
443 }
444 }
445
446 return new PointF(bestX, bestY);
447 }
448
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800449 private void invalidateActionMode(@Nullable SelectionResult result) {
Petar Šegina701ba332017-08-01 17:57:26 +0100450 cancelSmartSelectAnimation();
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100451 mTextClassification = result != null ? result.mClassification : null;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800452 final ActionMode actionMode = mEditor.getTextActionMode();
453 if (actionMode != null) {
454 actionMode.invalidate();
455 }
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100456 mSelectionTracker.onSelectionUpdated(
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100457 mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mTextClassification);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800458 mTextClassificationAsyncTask = null;
459 }
460
Richard Ledley26b87222017-11-30 10:54:08 +0000461 private void resetTextClassificationHelper(int selectionStart, int selectionEnd) {
462 if (selectionStart < 0 || selectionEnd < 0) {
463 // Use selection indices
464 selectionStart = mTextView.getSelectionStart();
465 selectionEnd = mTextView.getSelectionEnd();
466 }
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100467 mTextClassificationHelper.init(
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000468 mTextView::getTextClassifier,
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100469 getText(mTextView),
Richard Ledley26b87222017-11-30 10:54:08 +0000470 selectionStart, selectionEnd,
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100471 mTextView.getTextLocales());
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800472 }
473
Richard Ledley26b87222017-11-30 10:54:08 +0000474 private void resetTextClassificationHelper() {
475 resetTextClassificationHelper(-1, -1);
476 }
477
Petar Šegina701ba332017-08-01 17:57:26 +0100478 private void cancelSmartSelectAnimation() {
479 if (mSmartSelectSprite != null) {
480 mSmartSelectSprite.cancelAnimation();
481 }
482 }
483
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800484 /**
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100485 * Tracks and logs smart selection changes.
486 * It is important to trigger this object's methods at the appropriate event so that it tracks
487 * smart selection events appropriately.
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000488 */
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100489 private static final class SelectionTracker {
490
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100491 private final TextView mTextView;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100492 private SelectionMetricsLogger mLogger;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000493
494 private int mOriginalStart;
495 private int mOriginalEnd;
496 private int mSelectionStart;
497 private int mSelectionEnd;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100498 private boolean mAllowReset;
Jan Althausb3513a12017-09-22 18:26:06 +0200499 private final LogAbandonRunnable mDelayedLogAbandon = new LogAbandonRunnable();
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000500
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100501 SelectionTracker(TextView textView) {
Daulet Zhanguzincb0d19b2019-12-18 15:08:09 +0000502 mTextView = Objects.requireNonNull(textView);
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100503 mLogger = new SelectionMetricsLogger(textView);
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100504 }
505
506 /**
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100507 * Called when the original selection happens, before smart selection is triggered.
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100508 */
Jan Althaus92c6dec2018-02-02 09:20:14 +0100509 public void onOriginalSelection(
510 CharSequence text, int selectionStart, int selectionEnd, boolean isLink) {
Jan Althausb3513a12017-09-22 18:26:06 +0200511 // If we abandoned a selection and created a new one very shortly after, we may still
512 // have a pending request to log ABANDON, which we flush here.
513 mDelayedLogAbandon.flush();
514
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100515 mOriginalStart = mSelectionStart = selectionStart;
516 mOriginalEnd = mSelectionEnd = selectionEnd;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100517 mAllowReset = false;
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100518 maybeInvalidateLogger();
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000519 mLogger.logSelectionStarted(
520 mTextView.getTextClassificationSession(),
521 mTextView.getTextClassificationContext(),
522 text,
523 selectionStart,
Jan Althaus92c6dec2018-02-02 09:20:14 +0100524 isLink ? SelectionEvent.INVOCATION_LINK : SelectionEvent.INVOCATION_MANUAL);
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000525 }
526
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100527 /**
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100528 * Called when selection action mode is started and the results come from a classifier.
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100529 */
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100530 public void onSmartSelection(SelectionResult result) {
Richard Ledley724eff92017-12-21 10:11:34 +0000531 onClassifiedSelection(result);
532 mLogger.logSelectionModified(
533 result.mStart, result.mEnd, result.mClassification, result.mSelection);
534 }
535
536 /**
537 * Called when link action mode is started and the classification comes from a classifier.
538 */
539 public void onLinkSelected(SelectionResult result) {
540 onClassifiedSelection(result);
541 // TODO: log (b/70246800)
542 }
543
544 private void onClassifiedSelection(SelectionResult result) {
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100545 if (isSelectionStarted()) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100546 mSelectionStart = result.mStart;
547 mSelectionEnd = result.mEnd;
548 mAllowReset = mSelectionStart != mOriginalStart || mSelectionEnd != mOriginalEnd;
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100549 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000550 }
551
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100552 /**
553 * Called when selection bounds change.
554 */
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100555 public void onSelectionUpdated(
556 int selectionStart, int selectionEnd,
557 @Nullable TextClassification classification) {
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100558 if (isSelectionStarted()) {
559 mSelectionStart = selectionStart;
560 mSelectionEnd = selectionEnd;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100561 mAllowReset = false;
562 mLogger.logSelectionModified(selectionStart, selectionEnd, classification, null);
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100563 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000564 }
565
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100566 /**
567 * Called when the selection action mode is destroyed.
568 */
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000569 public void onSelectionDestroyed() {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100570 mAllowReset = false;
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100571 // Wait a few ms to see if the selection was destroyed because of a text change event.
Jan Althausb3513a12017-09-22 18:26:06 +0200572 mDelayedLogAbandon.schedule(100 /* ms */);
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000573 }
574
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100575 /**
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100576 * Called when an action is taken on a smart selection.
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100577 */
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100578 public void onSelectionAction(
579 int selectionStart, int selectionEnd,
580 @SelectionEvent.ActionType int action,
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000581 @Nullable String actionLabel,
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100582 @Nullable TextClassification classification) {
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100583 if (isSelectionStarted()) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100584 mAllowReset = false;
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000585 mLogger.logSelectionAction(
586 selectionStart, selectionEnd, action, actionLabel, classification);
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100587 }
588 }
589
590 /**
591 * Returns true if the current smart selection should be reset to normal selection based on
592 * information that has been recorded about the original selection and the smart selection.
593 * The expected UX here is to allow the user to select a word inside of the smart selection
594 * on a single tap.
595 */
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100596 public boolean resetSelection(int textIndex, Editor editor) {
597 final TextView textView = editor.getTextView();
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100598 if (isSelectionStarted()
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100599 && mAllowReset
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100600 && textIndex >= mSelectionStart && textIndex <= mSelectionEnd
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100601 && getText(textView) instanceof Spannable) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100602 mAllowReset = false;
603 boolean selected = editor.selectCurrentWord();
604 if (selected) {
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100605 mSelectionStart = editor.getTextView().getSelectionStart();
606 mSelectionEnd = editor.getTextView().getSelectionEnd();
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100607 mLogger.logSelectionAction(
608 textView.getSelectionStart(), textView.getSelectionEnd(),
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000609 SelectionEvent.ACTION_RESET,
610 /* actionLabel= */ null, /* classification= */ null);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100611 }
612 return selected;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000613 }
614 return false;
615 }
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100616
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100617 public void onTextChanged(int start, int end, TextClassification classification) {
618 if (isSelectionStarted() && start == mSelectionStart && end == mSelectionEnd) {
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000619 onSelectionAction(
620 start, end, SelectionEvent.ACTION_OVERTYPE,
621 /* actionLabel= */ null, classification);
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100622 }
623 }
624
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100625 private void maybeInvalidateLogger() {
626 if (mLogger.isEditTextLogger() != mTextView.isTextEditable()) {
627 mLogger = new SelectionMetricsLogger(mTextView);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100628 }
629 }
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100630
631 private boolean isSelectionStarted() {
632 return mSelectionStart >= 0 && mSelectionEnd >= 0 && mSelectionStart != mSelectionEnd;
633 }
Jan Althausb3513a12017-09-22 18:26:06 +0200634
635 /** A helper for keeping track of pending abandon logging requests. */
636 private final class LogAbandonRunnable implements Runnable {
637 private boolean mIsPending;
638
639 /** Schedules an abandon to be logged with the given delay. Flush if necessary. */
640 void schedule(int delayMillis) {
641 if (mIsPending) {
642 Log.e(LOG_TAG, "Force flushing abandon due to new scheduling request");
643 flush();
644 }
645 mIsPending = true;
646 mTextView.postDelayed(this, delayMillis);
647 }
648
649 /** If there is a pending log request, execute it now. */
650 void flush() {
651 mTextView.removeCallbacks(this);
652 run();
653 }
654
655 @Override
656 public void run() {
657 if (mIsPending) {
658 mLogger.logSelectionAction(
659 mSelectionStart, mSelectionEnd,
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000660 SelectionEvent.ACTION_ABANDON,
661 /* actionLabel= */ null, /* classification= */ null);
Jan Althausb3513a12017-09-22 18:26:06 +0200662 mSelectionStart = mSelectionEnd = -1;
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100663 mLogger.endTextClassificationSession();
Jan Althausb3513a12017-09-22 18:26:06 +0200664 mIsPending = false;
665 }
666 }
667 }
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100668 }
669
670 // TODO: Write tests
671 /**
672 * Metrics logging helper.
673 *
674 * This logger logs selection by word indices. The initial (start) single word selection is
675 * logged at [0, 1) -- end index is exclusive. Other word indices are logged relative to the
676 * initial single word selection.
677 * e.g. New York city, NY. Suppose the initial selection is "York" in
678 * "New York city, NY", then "York" is at [0, 1), "New" is at [-1, 0], and "city" is at [1, 2).
679 * "New York" is at [-1, 1).
680 * Part selection of a word e.g. "or" is counted as selecting the
681 * entire word i.e. equivalent to "York", and each special character is counted as a word, e.g.
682 * "," is at [2, 3). Whitespaces are ignored.
Abodunrinwa Tokiad52f4b2018-02-06 23:32:41 +0000683 *
684 * NOTE that the definition of a word is defined by the TextClassifier's Logger's token
685 * iterator.
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100686 */
687 private static final class SelectionMetricsLogger {
688
689 private static final String LOG_TAG = "SelectionMetricsLogger";
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100690 private static final Pattern PATTERN_WHITESPACE = Pattern.compile("\\s+");
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100691
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100692 private final boolean mEditTextLogger;
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000693 private final BreakIterator mTokenIterator;
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100694
695 @Nullable private TextClassifier mClassificationSession;
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000696 @Nullable private TextClassificationContext mClassificationContext;
697
698 @Nullable private TextClassifierEvent mTranslateViewEvent;
699 @Nullable private TextClassifierEvent mTranslateClickEvent;
700
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100701 private int mStartIndex;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100702 private String mText;
703
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100704 SelectionMetricsLogger(TextView textView) {
Daulet Zhanguzincb0d19b2019-12-18 15:08:09 +0000705 Objects.requireNonNull(textView);
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100706 mEditTextLogger = textView.isTextEditable();
Tony Mak293bdf32020-02-18 11:33:43 +0000707 mTokenIterator = BreakIterator.getWordInstance(textView.getTextLocale());
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000708 }
709
Jan Althaus92c6dec2018-02-02 09:20:14 +0100710 public void logSelectionStarted(
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100711 TextClassifier classificationSession,
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000712 TextClassificationContext classificationContext,
Jan Althaus92c6dec2018-02-02 09:20:14 +0100713 CharSequence text, int index,
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000714 @InvocationMethod int invocationMethod) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100715 try {
Daulet Zhanguzincb0d19b2019-12-18 15:08:09 +0000716 Objects.requireNonNull(text);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100717 Preconditions.checkArgumentInRange(index, 0, text.length(), "index");
718 if (mText == null || !mText.contentEquals(text)) {
719 mText = text.toString();
720 }
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000721 mTokenIterator.setText(mText);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100722 mStartIndex = index;
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100723 mClassificationSession = classificationSession;
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000724 mClassificationContext = classificationContext;
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100725 if (hasActiveClassificationSession()) {
726 mClassificationSession.onSelectionEvent(
727 SelectionEvent.createSelectionStartedEvent(invocationMethod, 0));
728 }
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100729 } catch (Exception e) {
730 // Avoid crashes due to logging.
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000731 Log.e(LOG_TAG, "" + e.getMessage(), e);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100732 }
733 }
734
735 public void logSelectionModified(int start, int end,
736 @Nullable TextClassification classification, @Nullable TextSelection selection) {
737 try {
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100738 if (hasActiveClassificationSession()) {
739 Preconditions.checkArgumentInRange(start, 0, mText.length(), "start");
740 Preconditions.checkArgumentInRange(end, start, mText.length(), "end");
741 int[] wordIndices = getWordDelta(start, end);
742 if (selection != null) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100743 mClassificationSession.onSelectionEvent(
744 SelectionEvent.createSelectionModifiedEvent(
745 wordIndices[0], wordIndices[1], selection));
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100746 } else if (classification != null) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100747 mClassificationSession.onSelectionEvent(
748 SelectionEvent.createSelectionModifiedEvent(
749 wordIndices[0], wordIndices[1], classification));
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100750 } else {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100751 mClassificationSession.onSelectionEvent(
752 SelectionEvent.createSelectionModifiedEvent(
753 wordIndices[0], wordIndices[1]));
754 }
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000755 maybeGenerateTranslateViewEvent(classification);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100756 }
757 } catch (Exception e) {
758 // Avoid crashes due to logging.
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000759 Log.e(LOG_TAG, "" + e.getMessage(), e);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100760 }
761 }
762
763 public void logSelectionAction(
764 int start, int end,
765 @SelectionEvent.ActionType int action,
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000766 @Nullable String actionLabel,
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100767 @Nullable TextClassification classification) {
768 try {
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100769 if (hasActiveClassificationSession()) {
770 Preconditions.checkArgumentInRange(start, 0, mText.length(), "start");
771 Preconditions.checkArgumentInRange(end, start, mText.length(), "end");
772 int[] wordIndices = getWordDelta(start, end);
773 if (classification != null) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100774 mClassificationSession.onSelectionEvent(
775 SelectionEvent.createSelectionActionEvent(
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100776 wordIndices[0], wordIndices[1], action,
777 classification));
778 } else {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100779 mClassificationSession.onSelectionEvent(
780 SelectionEvent.createSelectionActionEvent(
781 wordIndices[0], wordIndices[1], action));
782 }
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000783
784 maybeGenerateTranslateClickEvent(classification, actionLabel);
785
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100786 if (SelectionEvent.isTerminal(action)) {
787 endTextClassificationSession();
788 }
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100789 }
790 } catch (Exception e) {
791 // Avoid crashes due to logging.
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000792 Log.e(LOG_TAG, "" + e.getMessage(), e);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100793 }
794 }
795
796 public boolean isEditTextLogger() {
797 return mEditTextLogger;
798 }
799
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100800 public void endTextClassificationSession() {
801 if (hasActiveClassificationSession()) {
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000802 maybeReportTranslateEvents();
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100803 mClassificationSession.destroy();
804 }
805 }
806
807 private boolean hasActiveClassificationSession() {
808 return mClassificationSession != null && !mClassificationSession.isDestroyed();
809 }
810
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100811 private int[] getWordDelta(int start, int end) {
812 int[] wordIndices = new int[2];
813
814 if (start == mStartIndex) {
815 wordIndices[0] = 0;
816 } else if (start < mStartIndex) {
817 wordIndices[0] = -countWordsForward(start);
818 } else { // start > mStartIndex
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100819 wordIndices[0] = countWordsBackward(start);
820
821 // For the selection start index, avoid counting a partial word backwards.
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000822 if (!mTokenIterator.isBoundary(start)
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100823 && !isWhitespace(
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000824 mTokenIterator.preceding(start),
825 mTokenIterator.following(start))) {
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100826 // We counted a partial word. Remove it.
827 wordIndices[0]--;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100828 }
829 }
830
831 if (end == mStartIndex) {
832 wordIndices[1] = 0;
833 } else if (end < mStartIndex) {
834 wordIndices[1] = -countWordsForward(end);
835 } else { // end > mStartIndex
836 wordIndices[1] = countWordsBackward(end);
837 }
838
839 return wordIndices;
840 }
841
842 private int countWordsBackward(int from) {
843 Preconditions.checkArgument(from >= mStartIndex);
844 int wordCount = 0;
845 int offset = from;
846 while (offset > mStartIndex) {
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000847 int start = mTokenIterator.preceding(offset);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100848 if (!isWhitespace(start, offset)) {
849 wordCount++;
850 }
851 offset = start;
852 }
853 return wordCount;
854 }
855
856 private int countWordsForward(int from) {
857 Preconditions.checkArgument(from <= mStartIndex);
858 int wordCount = 0;
859 int offset = from;
860 while (offset < mStartIndex) {
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000861 int end = mTokenIterator.following(offset);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100862 if (!isWhitespace(offset, end)) {
863 wordCount++;
864 }
865 offset = end;
866 }
867 return wordCount;
868 }
869
870 private boolean isWhitespace(int start, int end) {
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100871 return PATTERN_WHITESPACE.matcher(mText.substring(start, end)).matches();
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100872 }
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000873
874 private void maybeGenerateTranslateViewEvent(@Nullable TextClassification classification) {
875 if (classification != null) {
876 final TextClassifierEvent event = generateTranslateEvent(
877 TextClassifierEvent.TYPE_ACTIONS_SHOWN,
878 classification, mClassificationContext, /* actionLabel= */null);
879 mTranslateViewEvent = (event != null) ? event : mTranslateViewEvent;
880 }
881 }
882
883 private void maybeGenerateTranslateClickEvent(
884 @Nullable TextClassification classification, String actionLabel) {
885 if (classification != null) {
886 mTranslateClickEvent = generateTranslateEvent(
887 TextClassifierEvent.TYPE_SMART_ACTION,
888 classification, mClassificationContext, actionLabel);
889 }
890 }
891
892 private void maybeReportTranslateEvents() {
893 // Translate view and click events should only be logged once per selection session.
894 if (mTranslateViewEvent != null) {
895 mClassificationSession.onTextClassifierEvent(mTranslateViewEvent);
896 mTranslateViewEvent = null;
897 }
898 if (mTranslateClickEvent != null) {
899 mClassificationSession.onTextClassifierEvent(mTranslateClickEvent);
900 mTranslateClickEvent = null;
901 }
902 }
903
904 @Nullable
905 private static TextClassifierEvent generateTranslateEvent(
906 int eventType, TextClassification classification,
907 TextClassificationContext classificationContext, @Nullable String actionLabel) {
908
909 // The platform attempts to log "views" and "clicks" of the "Translate" action.
910 // Views are logged if a user is presented with the translate action during a selection
911 // session.
912 // Clicks are logged if the user clicks on the translate action.
913 // The index of the translate action is also logged to indicate whether it might have
914 // been in the main panel or overflow panel of the selection toolbar.
915 // NOTE that the "views" metric may be flawed if a TextView removes the translate menu
916 // item via a custom action mode callback or does not show a selection menu item.
917
918 final RemoteAction translateAction = ExtrasUtils.findTranslateAction(classification);
919 if (translateAction == null) {
920 // No translate action present. Nothing to log. Exit.
921 return null;
922 }
923
924 if (eventType == TextClassifierEvent.TYPE_SMART_ACTION
925 && !translateAction.getTitle().toString().equals(actionLabel)) {
926 // Clicked action is not a translate action. Nothing to log. Exit.
927 // Note that we don't expect an actionLabel for "view" events.
928 return null;
929 }
930
931 final Bundle foreignLanguageExtra = ExtrasUtils.getForeignLanguageExtra(classification);
932 final String language = ExtrasUtils.getEntityType(foreignLanguageExtra);
933 final float score = ExtrasUtils.getScore(foreignLanguageExtra);
934 final String model = ExtrasUtils.getModelName(foreignLanguageExtra);
Abodunrinwa Toki6d063372019-04-11 22:36:04 +0100935 return new TextClassifierEvent.LanguageDetectionEvent.Builder(eventType)
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000936 .setEventContext(classificationContext)
937 .setResultId(classification.getId())
938 .setEntityTypes(language)
Abodunrinwa Toki6d063372019-04-11 22:36:04 +0100939 .setScores(score)
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000940 .setActionIndices(classification.getActions().indexOf(translateAction))
941 .setModelName(model)
942 .build();
943 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000944 }
945
946 /**
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800947 * AsyncTask for running a query on a background thread and returning the result on the
948 * UiThread. The AsyncTask times out after a specified time, returning a null result if the
949 * query has not yet returned.
950 */
951 private static final class TextClassificationAsyncTask
952 extends AsyncTask<Void, Void, SelectionResult> {
953
954 private final int mTimeOutDuration;
955 private final Supplier<SelectionResult> mSelectionResultSupplier;
956 private final Consumer<SelectionResult> mSelectionResultCallback;
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000957 private final Supplier<SelectionResult> mTimeOutResultSupplier;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800958 private final TextView mTextView;
959 private final String mOriginalText;
960
961 /**
962 * @param textView the TextView
963 * @param timeOut time in milliseconds to timeout the query if it has not completed
964 * @param selectionResultSupplier fetches the selection results. Runs on a background thread
965 * @param selectionResultCallback receives the selection results. Runs on the UiThread
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000966 * @param timeOutResultSupplier default result if the task times out
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800967 */
968 TextClassificationAsyncTask(
969 @NonNull TextView textView, int timeOut,
970 @NonNull Supplier<SelectionResult> selectionResultSupplier,
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000971 @NonNull Consumer<SelectionResult> selectionResultCallback,
972 @NonNull Supplier<SelectionResult> timeOutResultSupplier) {
Makoto Onuki1488a3a2017-05-24 12:25:46 -0700973 super(textView != null ? textView.getHandler() : null);
Daulet Zhanguzincb0d19b2019-12-18 15:08:09 +0000974 mTextView = Objects.requireNonNull(textView);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800975 mTimeOutDuration = timeOut;
Daulet Zhanguzincb0d19b2019-12-18 15:08:09 +0000976 mSelectionResultSupplier = Objects.requireNonNull(selectionResultSupplier);
977 mSelectionResultCallback = Objects.requireNonNull(selectionResultCallback);
978 mTimeOutResultSupplier = Objects.requireNonNull(timeOutResultSupplier);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800979 // Make a copy of the original text.
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100980 mOriginalText = getText(mTextView).toString();
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800981 }
982
983 @Override
984 @WorkerThread
985 protected SelectionResult doInBackground(Void... params) {
986 final Runnable onTimeOut = this::onTimeOut;
987 mTextView.postDelayed(onTimeOut, mTimeOutDuration);
988 final SelectionResult result = mSelectionResultSupplier.get();
989 mTextView.removeCallbacks(onTimeOut);
990 return result;
991 }
992
993 @Override
994 @UiThread
995 protected void onPostExecute(SelectionResult result) {
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100996 result = TextUtils.equals(mOriginalText, getText(mTextView)) ? result : null;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800997 mSelectionResultCallback.accept(result);
998 }
999
1000 private void onTimeOut() {
Tony Makcdc81e82019-08-22 15:14:28 +01001001 Log.d(LOG_TAG, "Timeout in TextClassificationAsyncTask");
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001002 if (getStatus() == Status.RUNNING) {
Abodunrinwa Toki52096912018-03-21 23:14:42 +00001003 onPostExecute(mTimeOutResultSupplier.get());
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001004 }
1005 cancel(true);
1006 }
1007 }
1008
1009 /**
1010 * Helper class for querying the TextClassifier.
1011 * It trims text so that only text necessary to provide context of the selected text is
1012 * sent to the TextClassifier.
1013 */
1014 private static final class TextClassificationHelper {
1015
Abodunrinwa Tokid2d13992017-03-24 21:43:13 +00001016 private static final int TRIM_DELTA = 120; // characters
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001017
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +00001018 private final Context mContext;
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +00001019 private Supplier<TextClassifier> mTextClassifier;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001020
1021 /** The original TextView text. **/
1022 private String mText;
1023 /** Start index relative to mText. */
1024 private int mSelectionStart;
1025 /** End index relative to mText. */
1026 private int mSelectionEnd;
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +01001027
Abodunrinwa Toki080c8542018-03-27 00:04:06 +01001028 @Nullable
1029 private LocaleList mDefaultLocales;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001030
1031 /** Trimmed text starting from mTrimStart in mText. */
1032 private CharSequence mTrimmedText;
1033 /** Index indicating the start of mTrimmedText in mText. */
1034 private int mTrimStart;
1035 /** Start index relative to mTrimmedText */
1036 private int mRelativeStart;
1037 /** End index relative to mTrimmedText */
1038 private int mRelativeEnd;
1039
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +01001040 /** Information about the last classified text to avoid re-running a query. */
1041 private CharSequence mLastClassificationText;
1042 private int mLastClassificationSelectionStart;
1043 private int mLastClassificationSelectionEnd;
1044 private LocaleList mLastClassificationLocales;
1045 private SelectionResult mLastClassificationResult;
1046
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +01001047 /** Whether the TextClassifier has been initialized. */
1048 private boolean mHot;
1049
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +00001050 TextClassificationHelper(Context context, Supplier<TextClassifier> textClassifier,
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +00001051 CharSequence text, int selectionStart, int selectionEnd, LocaleList locales) {
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +00001052 init(textClassifier, text, selectionStart, selectionEnd, locales);
Daulet Zhanguzincb0d19b2019-12-18 15:08:09 +00001053 mContext = Objects.requireNonNull(context);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001054 }
1055
1056 @UiThread
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +00001057 public void init(Supplier<TextClassifier> textClassifier, CharSequence text,
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +00001058 int selectionStart, int selectionEnd, LocaleList locales) {
Daulet Zhanguzincb0d19b2019-12-18 15:08:09 +00001059 mTextClassifier = Objects.requireNonNull(textClassifier);
1060 mText = Objects.requireNonNull(text).toString();
Abodunrinwa Toki08925e62017-05-12 13:48:50 +01001061 mLastClassificationText = null; // invalidate.
Abodunrinwa Toki792d8202017-03-06 23:51:11 +00001062 Preconditions.checkArgument(selectionEnd > selectionStart);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001063 mSelectionStart = selectionStart;
1064 mSelectionEnd = selectionEnd;
Abodunrinwa Toki080c8542018-03-27 00:04:06 +01001065 mDefaultLocales = locales;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001066 }
1067
1068 @WorkerThread
1069 public SelectionResult classifyText() {
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +01001070 mHot = true;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001071 return performClassification(null /* selection */);
1072 }
1073
1074 @WorkerThread
1075 public SelectionResult suggestSelection() {
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +01001076 mHot = true;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001077 trimText();
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +01001078 final TextSelection selection;
Jeff Sharkeyaa1a9112018-04-10 15:18:12 -06001079 if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.P) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +01001080 final TextSelection.Request request = new TextSelection.Request.Builder(
1081 mTrimmedText, mRelativeStart, mRelativeEnd)
1082 .setDefaultLocales(mDefaultLocales)
1083 .setDarkLaunchAllowed(true)
1084 .build();
1085 selection = mTextClassifier.get().suggestSelection(request);
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +01001086 } else {
1087 // Use old APIs.
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +00001088 selection = mTextClassifier.get().suggestSelection(
Abodunrinwa Toki080c8542018-03-27 00:04:06 +01001089 mTrimmedText, mRelativeStart, mRelativeEnd, mDefaultLocales);
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +01001090 }
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +01001091 // Do not classify new selection boundaries if TextClassifier should be dark launched.
Abodunrinwa Tokic2449b82018-05-01 21:36:48 +01001092 if (!isDarkLaunchEnabled()) {
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +01001093 mSelectionStart = Math.max(0, selection.getSelectionStartIndex() + mTrimStart);
1094 mSelectionEnd = Math.min(
1095 mText.length(), selection.getSelectionEndIndex() + mTrimStart);
1096 }
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001097 return performClassification(selection);
1098 }
1099
Abodunrinwa Toki52096912018-03-21 23:14:42 +00001100 public SelectionResult getOriginalSelection() {
1101 return new SelectionResult(mSelectionStart, mSelectionEnd, null, null);
1102 }
1103
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +01001104 /**
1105 * Maximum time (in milliseconds) to wait for a textclassifier result before timing out.
1106 */
1107 // TODO: Consider making this a ViewConfiguration.
1108 public int getTimeoutDuration() {
1109 if (mHot) {
1110 return 200;
1111 } else {
1112 // Return a slightly larger number than usual when the TextClassifier is first
1113 // initialized. Initialization would usually take longer than subsequent calls to
1114 // the TextClassifier. The impact of this on the UI is that we do not show the
1115 // selection handles or toolbar until after this timeout.
1116 return 500;
1117 }
1118 }
1119
Abodunrinwa Tokic2449b82018-05-01 21:36:48 +01001120 private boolean isDarkLaunchEnabled() {
1121 return TextClassificationManager.getSettings(mContext).isModelDarkLaunchEnabled();
1122 }
1123
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001124 private SelectionResult performClassification(@Nullable TextSelection selection) {
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +01001125 if (!Objects.equals(mText, mLastClassificationText)
1126 || mSelectionStart != mLastClassificationSelectionStart
1127 || mSelectionEnd != mLastClassificationSelectionEnd
Abodunrinwa Toki080c8542018-03-27 00:04:06 +01001128 || !Objects.equals(mDefaultLocales, mLastClassificationLocales)) {
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +01001129
1130 mLastClassificationText = mText;
1131 mLastClassificationSelectionStart = mSelectionStart;
1132 mLastClassificationSelectionEnd = mSelectionEnd;
Abodunrinwa Toki080c8542018-03-27 00:04:06 +01001133 mLastClassificationLocales = mDefaultLocales;
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +01001134
1135 trimText();
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +01001136 final TextClassification classification;
Abodunrinwa Tokiadc19402018-11-22 17:10:25 +00001137 if (Linkify.containsUnsupportedCharacters(mText)) {
1138 // Do not show smart actions for text containing unsupported characters.
1139 android.util.EventLog.writeEvent(0x534e4554, "116321860", -1, "");
1140 classification = TextClassification.EMPTY;
1141 } else if (mContext.getApplicationInfo().targetSdkVersion
1142 >= Build.VERSION_CODES.P) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +01001143 final TextClassification.Request request =
1144 new TextClassification.Request.Builder(
1145 mTrimmedText, mRelativeStart, mRelativeEnd)
1146 .setDefaultLocales(mDefaultLocales)
1147 .build();
1148 classification = mTextClassifier.get().classifyText(request);
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +01001149 } else {
1150 // Use old APIs.
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +00001151 classification = mTextClassifier.get().classifyText(
Abodunrinwa Toki080c8542018-03-27 00:04:06 +01001152 mTrimmedText, mRelativeStart, mRelativeEnd, mDefaultLocales);
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +01001153 }
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +01001154 mLastClassificationResult = new SelectionResult(
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +01001155 mSelectionStart, mSelectionEnd, classification, selection);
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +01001156
1157 }
1158 return mLastClassificationResult;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001159 }
1160
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001161 private void trimText() {
1162 mTrimStart = Math.max(0, mSelectionStart - TRIM_DELTA);
1163 final int referenceEnd = Math.min(mText.length(), mSelectionEnd + TRIM_DELTA);
1164 mTrimmedText = mText.subSequence(mTrimStart, referenceEnd);
1165 mRelativeStart = mSelectionStart - mTrimStart;
1166 mRelativeEnd = mSelectionEnd - mTrimStart;
1167 }
1168 }
1169
1170 /**
1171 * Selection result.
1172 */
1173 private static final class SelectionResult {
1174 private final int mStart;
1175 private final int mEnd;
Abodunrinwa Toki52096912018-03-21 23:14:42 +00001176 @Nullable private final TextClassification mClassification;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001177 @Nullable private final TextSelection mSelection;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001178
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001179 SelectionResult(int start, int end,
Abodunrinwa Toki52096912018-03-21 23:14:42 +00001180 @Nullable TextClassification classification, @Nullable TextSelection selection) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001181 mStart = start;
1182 mEnd = end;
Abodunrinwa Toki52096912018-03-21 23:14:42 +00001183 mClassification = classification;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001184 mSelection = selection;
1185 }
1186 }
1187
1188 @SelectionEvent.ActionType
1189 private static int getActionType(int menuItemId) {
1190 switch (menuItemId) {
1191 case TextView.ID_SELECT_ALL:
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +00001192 return SelectionEvent.ACTION_SELECT_ALL;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001193 case TextView.ID_CUT:
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +00001194 return SelectionEvent.ACTION_CUT;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001195 case TextView.ID_COPY:
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +00001196 return SelectionEvent.ACTION_COPY;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001197 case TextView.ID_PASTE: // fall through
1198 case TextView.ID_PASTE_AS_PLAIN_TEXT:
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +00001199 return SelectionEvent.ACTION_PASTE;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001200 case TextView.ID_SHARE:
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +00001201 return SelectionEvent.ACTION_SHARE;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001202 case TextView.ID_ASSIST:
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +00001203 return SelectionEvent.ACTION_SMART_SHARE;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001204 default:
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +00001205 return SelectionEvent.ACTION_OTHER;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001206 }
1207 }
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +01001208
1209 private static CharSequence getText(TextView textView) {
1210 // Extracts the textView's text.
1211 // TODO: Investigate why/when TextView.getText() is null.
1212 final CharSequence text = textView.getText();
1213 if (text != null) {
1214 return text;
1215 }
1216 return "";
1217 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001218}