blob: 51ca8052409052bb1ff27991969b0325417a560d [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;
Jan Althaus5a030942018-04-04 19:40:38 +020042import android.view.textclassifier.SelectionSessionLogger;
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +010043import android.view.textclassifier.TextClassification;
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +000044import android.view.textclassifier.TextClassificationConstants;
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +000045import android.view.textclassifier.TextClassificationContext;
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +000046import android.view.textclassifier.TextClassificationManager;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080047import android.view.textclassifier.TextClassifier;
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +000048import android.view.textclassifier.TextClassifierEvent;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080049import android.view.textclassifier.TextSelection;
50import android.widget.Editor.SelectionModifierCursorController;
51
Petar Šegina91df3f92017-08-15 16:20:43 +010052import com.android.internal.annotations.VisibleForTesting;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080053import com.android.internal.util.Preconditions;
54
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +010055import java.text.BreakIterator;
Petar Šegina701ba332017-08-01 17:57:26 +010056import java.util.ArrayList;
Petar Šegina7c8196f2017-09-11 18:03:14 +010057import java.util.Comparator;
Petar Šegina701ba332017-08-01 17:57:26 +010058import java.util.List;
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +010059import java.util.Objects;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080060import java.util.function.Consumer;
Petar Šegina7c8196f2017-09-11 18:03:14 +010061import java.util.function.Function;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080062import java.util.function.Supplier;
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +010063import java.util.regex.Pattern;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080064
65/**
66 * Helper class for starting selection action mode
67 * (synchronously without the TextClassifier, asynchronously with the TextClassifier).
Petar Šegina91df3f92017-08-15 16:20:43 +010068 * @hide
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080069 */
70@UiThread
Petar Šegina91df3f92017-08-15 16:20:43 +010071@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
Petar Šeginaba1b8562017-08-31 18:09:16 +010072public final class SelectionActionModeHelper {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080073
Jan Althausb3513a12017-09-22 18:26:06 +020074 private static final String LOG_TAG = "SelectActionModeHelper";
75
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080076 private final Editor mEditor;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +010077 private final TextView mTextView;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080078 private final TextClassificationHelper mTextClassificationHelper;
79
Abodunrinwa Toki52096912018-03-21 23:14:42 +000080 @Nullable private TextClassification mTextClassification;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080081 private AsyncTask mTextClassificationAsyncTask;
82
Abodunrinwa Toki1d775572017-05-08 16:03:01 +010083 private final SelectionTracker mSelectionTracker;
Petar Šegina5ab7bb22017-09-05 20:48:42 +010084
85 // TODO remove nullable marker once the switch gating the feature gets removed
86 @Nullable
Petar Šegina701ba332017-08-01 17:57:26 +010087 private final SmartSelectSprite mSmartSelectSprite;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +000088
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080089 SelectionActionModeHelper(@NonNull Editor editor) {
90 mEditor = Preconditions.checkNotNull(editor);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +010091 mTextView = mEditor.getTextView();
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080092 mTextClassificationHelper = new TextClassificationHelper(
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +010093 mTextView.getContext(),
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +000094 mTextView::getTextClassifier,
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +010095 getText(mTextView),
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +010096 0, 1, mTextView.getTextLocales());
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +010097 mSelectionTracker = new SelectionTracker(mTextView);
Petar Šegina701ba332017-08-01 17:57:26 +010098
Abodunrinwa Tokic2449b82018-05-01 21:36:48 +010099 if (getTextClassificationSettings().isSmartSelectionAnimationEnabled()) {
Petar Šegina5ab7bb22017-09-05 20:48:42 +0100100 mSmartSelectSprite = new SmartSelectSprite(mTextView.getContext(),
Jan Althaus80620c52018-02-02 17:39:22 +0100101 editor.getTextView().mHighlightColor, mTextView::invalidate);
Petar Šegina701ba332017-08-01 17:57:26 +0100102 } else {
103 mSmartSelectSprite = null;
104 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800105 }
106
Richard Ledley26b87222017-11-30 10:54:08 +0000107 /**
108 * Starts Selection ActionMode.
109 */
110 public void startSelectionActionModeAsync(boolean adjustSelection) {
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100111 // Check if the smart selection should run for editable text.
Abodunrinwa Tokic2449b82018-05-01 21:36:48 +0100112 adjustSelection &= getTextClassificationSettings().isSmartSelectionEnabled();
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100113
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100114 mSelectionTracker.onOriginalSelection(
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100115 getText(mTextView),
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100116 mTextView.getSelectionStart(),
Jan Althaus92c6dec2018-02-02 09:20:14 +0100117 mTextView.getSelectionEnd(),
118 false /*isLink*/);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800119 cancelAsyncTask();
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100120 if (skipTextClassification()) {
Richard Ledley26b87222017-11-30 10:54:08 +0000121 startSelectionActionMode(null);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800122 } else {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100123 resetTextClassificationHelper();
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800124 mTextClassificationAsyncTask = new TextClassificationAsyncTask(
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100125 mTextView,
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +0100126 mTextClassificationHelper.getTimeoutDuration(),
Abodunrinwa Toki66c16272017-05-03 20:22:55 +0100127 adjustSelection
128 ? mTextClassificationHelper::suggestSelection
129 : mTextClassificationHelper::classifyText,
Petar Šegina701ba332017-08-01 17:57:26 +0100130 mSmartSelectSprite != null
Richard Ledley26b87222017-11-30 10:54:08 +0000131 ? this::startSelectionActionModeWithSmartSelectAnimation
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000132 : this::startSelectionActionMode,
133 mTextClassificationHelper::getOriginalSelection)
Richard Ledley26b87222017-11-30 10:54:08 +0000134 .execute();
135 }
136 }
137
138 /**
139 * Starts Link ActionMode.
140 */
Richard Ledley27db81b2018-03-01 12:34:55 +0000141 public void startLinkActionModeAsync(int start, int end) {
142 mSelectionTracker.onOriginalSelection(getText(mTextView), start, end, true /*isLink*/);
Richard Ledley26b87222017-11-30 10:54:08 +0000143 cancelAsyncTask();
144 if (skipTextClassification()) {
145 startLinkActionMode(null);
146 } else {
Richard Ledley27db81b2018-03-01 12:34:55 +0000147 resetTextClassificationHelper(start, end);
Richard Ledley26b87222017-11-30 10:54:08 +0000148 mTextClassificationAsyncTask = new TextClassificationAsyncTask(
149 mTextView,
150 mTextClassificationHelper.getTimeoutDuration(),
151 mTextClassificationHelper::classifyText,
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000152 this::startLinkActionMode,
153 mTextClassificationHelper::getOriginalSelection)
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800154 .execute();
155 }
156 }
157
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800158 public void invalidateActionModeAsync() {
159 cancelAsyncTask();
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100160 if (skipTextClassification()) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800161 invalidateActionMode(null);
162 } else {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100163 resetTextClassificationHelper();
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800164 mTextClassificationAsyncTask = new TextClassificationAsyncTask(
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100165 mTextView,
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +0100166 mTextClassificationHelper.getTimeoutDuration(),
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100167 mTextClassificationHelper::classifyText,
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000168 this::invalidateActionMode,
169 mTextClassificationHelper::getOriginalSelection)
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800170 .execute();
171 }
172 }
173
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000174 /** Reports a selection action event. */
175 public void onSelectionAction(int menuItemId, @Nullable String actionLabel) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100176 mSelectionTracker.onSelectionAction(
177 mTextView.getSelectionStart(), mTextView.getSelectionEnd(),
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000178 getActionType(menuItemId), actionLabel, mTextClassification);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100179 }
180
181 public void onSelectionDrag() {
182 mSelectionTracker.onSelectionAction(
183 mTextView.getSelectionStart(), mTextView.getSelectionEnd(),
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000184 SelectionEvent.ACTION_DRAG, /* actionLabel= */ null, mTextClassification);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100185 }
186
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100187 public void onTextChanged(int start, int end) {
188 mSelectionTracker.onTextChanged(start, end, mTextClassification);
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100189 }
190
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100191 public boolean resetSelection(int textIndex) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100192 if (mSelectionTracker.resetSelection(textIndex, mEditor)) {
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000193 invalidateActionModeAsync();
194 return true;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800195 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000196 return false;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800197 }
198
199 @Nullable
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100200 public TextClassification getTextClassification() {
201 return mTextClassification;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800202 }
203
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000204 public void onDestroyActionMode() {
Petar Šegina701ba332017-08-01 17:57:26 +0100205 cancelSmartSelectAnimation();
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100206 mSelectionTracker.onSelectionDestroyed();
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000207 cancelAsyncTask();
208 }
209
Petar Šegina5ab7bb22017-09-05 20:48:42 +0100210 public void onDraw(final Canvas canvas) {
Jan Althaus80620c52018-02-02 17:39:22 +0100211 if (isDrawingHighlight() && mSmartSelectSprite != null) {
Petar Šegina5ab7bb22017-09-05 20:48:42 +0100212 mSmartSelectSprite.draw(canvas);
213 }
214 }
215
Jan Althaus80620c52018-02-02 17:39:22 +0100216 public boolean isDrawingHighlight() {
217 return mSmartSelectSprite != null && mSmartSelectSprite.isAnimationActive();
218 }
219
Abodunrinwa Tokic2449b82018-05-01 21:36:48 +0100220 private TextClassificationConstants getTextClassificationSettings() {
221 return TextClassificationManager.getSettings(mTextView.getContext());
222 }
223
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000224 private void cancelAsyncTask() {
225 if (mTextClassificationAsyncTask != null) {
226 mTextClassificationAsyncTask.cancel(true);
227 mTextClassificationAsyncTask = null;
228 }
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100229 mTextClassification = null;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000230 }
231
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100232 private boolean skipTextClassification() {
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100233 // No need to make an async call for a no-op TextClassifier.
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000234 final boolean noOpTextClassifier = mTextView.usesNoOpTextClassifier();
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100235 // Do not call the TextClassifier if there is no selection.
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100236 final boolean noSelection = mTextView.getSelectionEnd() == mTextView.getSelectionStart();
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100237 // Do not call the TextClassifier if this is a password field.
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100238 final boolean password = mTextView.hasPasswordTransformationMethod()
239 || TextView.isPasswordInputType(mTextView.getInputType());
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100240 return noOpTextClassifier || noSelection || password;
Abodunrinwa Toki792d8202017-03-06 23:51:11 +0000241 }
242
Richard Ledley26b87222017-11-30 10:54:08 +0000243 private void startLinkActionMode(@Nullable SelectionResult result) {
244 startActionMode(Editor.TextActionMode.TEXT_LINK, result);
245 }
246
247 private void startSelectionActionMode(@Nullable SelectionResult result) {
248 startActionMode(Editor.TextActionMode.SELECTION, result);
249 }
250
251 private void startActionMode(
252 @Editor.TextActionMode int actionMode, @Nullable SelectionResult result) {
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100253 final CharSequence text = getText(mTextView);
Richard Ledley26b87222017-11-30 10:54:08 +0000254 if (result != null && text instanceof Spannable
Richard Ledley27db81b2018-03-01 12:34:55 +0000255 && (mTextView.isTextSelectable() || mTextView.isTextEditable())) {
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100256 // Do not change the selection if TextClassifier should be dark launched.
Abodunrinwa Tokic2449b82018-05-01 21:36:48 +0100257 if (!getTextClassificationSettings().isModelDarkLaunchEnabled()) {
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100258 Selection.setSelection((Spannable) text, result.mStart, result.mEnd);
Richard Ledley724eff92017-12-21 10:11:34 +0000259 mTextView.invalidate();
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100260 }
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100261 mTextClassification = result.mClassification;
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000262 } else if (result != null && actionMode == Editor.TextActionMode.TEXT_LINK) {
Richard Ledley27db81b2018-03-01 12:34:55 +0000263 mTextClassification = result.mClassification;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800264 } else {
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100265 mTextClassification = null;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800266 }
Richard Ledley26b87222017-11-30 10:54:08 +0000267 if (mEditor.startActionModeInternal(actionMode)) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800268 final SelectionModifierCursorController controller = mEditor.getSelectionController();
Richard Ledley26b87222017-11-30 10:54:08 +0000269 if (controller != null
270 && (mTextView.isTextSelectable() || mTextView.isTextEditable())) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800271 controller.show();
272 }
Richard Ledley724eff92017-12-21 10:11:34 +0000273 if (result != null) {
274 switch (actionMode) {
275 case Editor.TextActionMode.SELECTION:
276 mSelectionTracker.onSmartSelection(result);
277 break;
278 case Editor.TextActionMode.TEXT_LINK:
279 mSelectionTracker.onLinkSelected(result);
280 break;
281 default:
282 break;
283 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000284 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800285 }
286 mEditor.setRestartActionModeOnNextRefresh(false);
287 mTextClassificationAsyncTask = null;
288 }
289
Richard Ledley26b87222017-11-30 10:54:08 +0000290 private void startSelectionActionModeWithSmartSelectAnimation(
291 @Nullable SelectionResult result) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100292 final Layout layout = mTextView.getLayout();
Petar Šegina701ba332017-08-01 17:57:26 +0100293
Mihai Popacce6e822018-05-08 19:18:43 +0100294 final Runnable onAnimationEndCallback = () -> {
Mihai Popaa9d27ea2018-05-25 11:57:17 +0100295 final SelectionResult startSelectionResult;
296 if (result != null && result.mStart >= 0 && result.mEnd <= getText(mTextView).length()
Mihai Popa6849bd322018-05-21 15:34:16 +0100297 && result.mStart <= result.mEnd) {
Mihai Popaa9d27ea2018-05-25 11:57:17 +0100298 startSelectionResult = result;
299 } else {
300 startSelectionResult = null;
Mihai Popacce6e822018-05-08 19:18:43 +0100301 }
Mihai Popaa9d27ea2018-05-25 11:57:17 +0100302 startSelectionActionMode(startSelectionResult);
Mihai Popacce6e822018-05-08 19:18:43 +0100303 };
Petar Šegina701ba332017-08-01 17:57:26 +0100304 // TODO do not trigger the animation if the change included only non-printable characters
305 final boolean didSelectionChange =
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100306 result != null && (mTextView.getSelectionStart() != result.mStart
307 || mTextView.getSelectionEnd() != result.mEnd);
Petar Šegina701ba332017-08-01 17:57:26 +0100308
309 if (!didSelectionChange) {
310 onAnimationEndCallback.run();
311 return;
312 }
313
Petar Šegina7c8196f2017-09-11 18:03:14 +0100314 final List<SmartSelectSprite.RectangleWithTextSelectionLayout> selectionRectangles =
Petar Šegina701ba332017-08-01 17:57:26 +0100315 convertSelectionToRectangles(layout, result.mStart, result.mEnd);
316
Petar Šegina91df3f92017-08-15 16:20:43 +0100317 final PointF touchPoint = new PointF(
318 mEditor.getLastUpPositionX(),
319 mEditor.getLastUpPositionY());
Petar Šegina701ba332017-08-01 17:57:26 +0100320
Petar Šegina91df3f92017-08-15 16:20:43 +0100321 final PointF animationStartPoint =
Petar Šegina7c8196f2017-09-11 18:03:14 +0100322 movePointInsideNearestRectangle(touchPoint, selectionRectangles,
323 SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle);
Petar Šegina701ba332017-08-01 17:57:26 +0100324
325 mSmartSelectSprite.startAnimation(
Petar Šegina91df3f92017-08-15 16:20:43 +0100326 animationStartPoint,
Petar Šegina701ba332017-08-01 17:57:26 +0100327 selectionRectangles,
328 onAnimationEndCallback);
329 }
330
Petar Šegina7c8196f2017-09-11 18:03:14 +0100331 private List<SmartSelectSprite.RectangleWithTextSelectionLayout> convertSelectionToRectangles(
332 final Layout layout, final int start, final int end) {
333 final List<SmartSelectSprite.RectangleWithTextSelectionLayout> result = new ArrayList<>();
Petar Šegina72729252017-08-31 15:25:06 +0100334
Petar Šegina7c8196f2017-09-11 18:03:14 +0100335 final Layout.SelectionRectangleConsumer consumer =
336 (left, top, right, bottom, textSelectionLayout) -> mergeRectangleIntoList(
337 result,
338 new RectF(left, top, right, bottom),
339 SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle,
340 r -> new SmartSelectSprite.RectangleWithTextSelectionLayout(r,
341 textSelectionLayout)
342 );
343
344 layout.getSelection(start, end, consumer);
345
346 result.sort(Comparator.comparing(
347 SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle,
348 SmartSelectSprite.RECTANGLE_COMPARATOR));
349
Petar Šegina701ba332017-08-01 17:57:26 +0100350 return result;
351 }
352
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +0100353 // TODO: Move public pure functions out of this class and make it package-private.
Petar Šeginaba1b8562017-08-31 18:09:16 +0100354 /**
Petar Šegina7c8196f2017-09-11 18:03:14 +0100355 * Merges a {@link RectF} into an existing list of any objects which contain a rectangle.
356 * While merging, this method makes sure that:
Petar Šeginaba1b8562017-08-31 18:09:16 +0100357 *
358 * <ol>
359 * <li>No rectangle is redundant (contained within a bigger rectangle)</li>
360 * <li>Rectangles of the same height and vertical position that intersect get merged</li>
361 * </ol>
362 *
Petar Šegina7c8196f2017-09-11 18:03:14 +0100363 * @param list the list of rectangles (or other rectangle containers) to merge the new
364 * rectangle into
Petar Šeginaba1b8562017-08-31 18:09:16 +0100365 * @param candidate the {@link RectF} to merge into the list
Petar Šegina7c8196f2017-09-11 18:03:14 +0100366 * @param extractor a function that can extract a {@link RectF} from an element of the given
367 * list
368 * @param packer a function that can wrap the resulting {@link RectF} into an element that
369 * the list contains
Petar Šeginaba1b8562017-08-31 18:09:16 +0100370 * @hide
371 */
372 @VisibleForTesting
Petar Šegina7c8196f2017-09-11 18:03:14 +0100373 public static <T> void mergeRectangleIntoList(final List<T> list,
374 final RectF candidate, final Function<T, RectF> extractor,
375 final Function<RectF, T> packer) {
Petar Šeginaba1b8562017-08-31 18:09:16 +0100376 if (candidate.isEmpty()) {
377 return;
378 }
379
380 final int elementCount = list.size();
381 for (int index = 0; index < elementCount; ++index) {
Petar Šegina7c8196f2017-09-11 18:03:14 +0100382 final RectF existingRectangle = extractor.apply(list.get(index));
Petar Šeginaba1b8562017-08-31 18:09:16 +0100383 if (existingRectangle.contains(candidate)) {
384 return;
385 }
386 if (candidate.contains(existingRectangle)) {
387 existingRectangle.setEmpty();
388 continue;
389 }
390
391 final boolean rectanglesContinueEachOther = candidate.left == existingRectangle.right
392 || candidate.right == existingRectangle.left;
393 final boolean canMerge = candidate.top == existingRectangle.top
394 && candidate.bottom == existingRectangle.bottom
395 && (RectF.intersects(candidate, existingRectangle)
396 || rectanglesContinueEachOther);
397
398 if (canMerge) {
399 candidate.union(existingRectangle);
400 existingRectangle.setEmpty();
401 }
402 }
403
404 for (int index = elementCount - 1; index >= 0; --index) {
Petar Šegina7c8196f2017-09-11 18:03:14 +0100405 final RectF rectangle = extractor.apply(list.get(index));
406 if (rectangle.isEmpty()) {
Petar Šeginaba1b8562017-08-31 18:09:16 +0100407 list.remove(index);
408 }
409 }
410
Petar Šegina7c8196f2017-09-11 18:03:14 +0100411 list.add(packer.apply(candidate));
Petar Šeginaba1b8562017-08-31 18:09:16 +0100412 }
413
414
Petar Šegina91df3f92017-08-15 16:20:43 +0100415 /** @hide */
416 @VisibleForTesting
Petar Šegina7c8196f2017-09-11 18:03:14 +0100417 public static <T> PointF movePointInsideNearestRectangle(final PointF point,
418 final List<T> list, final Function<T, RectF> extractor) {
Petar Šegina91df3f92017-08-15 16:20:43 +0100419 float bestX = -1;
420 float bestY = -1;
421 double bestDistance = Double.MAX_VALUE;
422
Petar Šegina7c8196f2017-09-11 18:03:14 +0100423 final int elementCount = list.size();
Petar Šeginaba1b8562017-08-31 18:09:16 +0100424 for (int index = 0; index < elementCount; ++index) {
Petar Šegina7c8196f2017-09-11 18:03:14 +0100425 final RectF rectangle = extractor.apply(list.get(index));
Petar Šegina91df3f92017-08-15 16:20:43 +0100426 final float candidateY = rectangle.centerY();
427 final float candidateX;
428
429 if (point.x > rectangle.right) {
430 candidateX = rectangle.right;
431 } else if (point.x < rectangle.left) {
432 candidateX = rectangle.left;
433 } else {
434 candidateX = point.x;
435 }
436
437 final double candidateDistance = Math.pow(point.x - candidateX, 2)
438 + Math.pow(point.y - candidateY, 2);
439
440 if (candidateDistance < bestDistance) {
441 bestX = candidateX;
442 bestY = candidateY;
443 bestDistance = candidateDistance;
444 }
445 }
446
447 return new PointF(bestX, bestY);
448 }
449
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800450 private void invalidateActionMode(@Nullable SelectionResult result) {
Petar Šegina701ba332017-08-01 17:57:26 +0100451 cancelSmartSelectAnimation();
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100452 mTextClassification = result != null ? result.mClassification : null;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800453 final ActionMode actionMode = mEditor.getTextActionMode();
454 if (actionMode != null) {
455 actionMode.invalidate();
456 }
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100457 mSelectionTracker.onSelectionUpdated(
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100458 mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mTextClassification);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800459 mTextClassificationAsyncTask = null;
460 }
461
Richard Ledley26b87222017-11-30 10:54:08 +0000462 private void resetTextClassificationHelper(int selectionStart, int selectionEnd) {
463 if (selectionStart < 0 || selectionEnd < 0) {
464 // Use selection indices
465 selectionStart = mTextView.getSelectionStart();
466 selectionEnd = mTextView.getSelectionEnd();
467 }
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100468 mTextClassificationHelper.init(
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000469 mTextView::getTextClassifier,
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100470 getText(mTextView),
Richard Ledley26b87222017-11-30 10:54:08 +0000471 selectionStart, selectionEnd,
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100472 mTextView.getTextLocales());
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800473 }
474
Richard Ledley26b87222017-11-30 10:54:08 +0000475 private void resetTextClassificationHelper() {
476 resetTextClassificationHelper(-1, -1);
477 }
478
Petar Šegina701ba332017-08-01 17:57:26 +0100479 private void cancelSmartSelectAnimation() {
480 if (mSmartSelectSprite != null) {
481 mSmartSelectSprite.cancelAnimation();
482 }
483 }
484
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800485 /**
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100486 * Tracks and logs smart selection changes.
487 * It is important to trigger this object's methods at the appropriate event so that it tracks
488 * smart selection events appropriately.
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000489 */
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100490 private static final class SelectionTracker {
491
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100492 private final TextView mTextView;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100493 private SelectionMetricsLogger mLogger;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000494
495 private int mOriginalStart;
496 private int mOriginalEnd;
497 private int mSelectionStart;
498 private int mSelectionEnd;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100499 private boolean mAllowReset;
Jan Althausb3513a12017-09-22 18:26:06 +0200500 private final LogAbandonRunnable mDelayedLogAbandon = new LogAbandonRunnable();
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000501
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100502 SelectionTracker(TextView textView) {
503 mTextView = Preconditions.checkNotNull(textView);
504 mLogger = new SelectionMetricsLogger(textView);
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100505 }
506
507 /**
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100508 * Called when the original selection happens, before smart selection is triggered.
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100509 */
Jan Althaus92c6dec2018-02-02 09:20:14 +0100510 public void onOriginalSelection(
511 CharSequence text, int selectionStart, int selectionEnd, boolean isLink) {
Jan Althausb3513a12017-09-22 18:26:06 +0200512 // If we abandoned a selection and created a new one very shortly after, we may still
513 // have a pending request to log ABANDON, which we flush here.
514 mDelayedLogAbandon.flush();
515
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100516 mOriginalStart = mSelectionStart = selectionStart;
517 mOriginalEnd = mSelectionEnd = selectionEnd;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100518 mAllowReset = false;
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100519 maybeInvalidateLogger();
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000520 mLogger.logSelectionStarted(
521 mTextView.getTextClassificationSession(),
522 mTextView.getTextClassificationContext(),
523 text,
524 selectionStart,
Jan Althaus92c6dec2018-02-02 09:20:14 +0100525 isLink ? SelectionEvent.INVOCATION_LINK : SelectionEvent.INVOCATION_MANUAL);
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000526 }
527
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100528 /**
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100529 * Called when selection action mode is started and the results come from a classifier.
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100530 */
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100531 public void onSmartSelection(SelectionResult result) {
Richard Ledley724eff92017-12-21 10:11:34 +0000532 onClassifiedSelection(result);
533 mLogger.logSelectionModified(
534 result.mStart, result.mEnd, result.mClassification, result.mSelection);
535 }
536
537 /**
538 * Called when link action mode is started and the classification comes from a classifier.
539 */
540 public void onLinkSelected(SelectionResult result) {
541 onClassifiedSelection(result);
542 // TODO: log (b/70246800)
543 }
544
545 private void onClassifiedSelection(SelectionResult result) {
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100546 if (isSelectionStarted()) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100547 mSelectionStart = result.mStart;
548 mSelectionEnd = result.mEnd;
549 mAllowReset = mSelectionStart != mOriginalStart || mSelectionEnd != mOriginalEnd;
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100550 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000551 }
552
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100553 /**
554 * Called when selection bounds change.
555 */
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100556 public void onSelectionUpdated(
557 int selectionStart, int selectionEnd,
558 @Nullable TextClassification classification) {
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100559 if (isSelectionStarted()) {
560 mSelectionStart = selectionStart;
561 mSelectionEnd = selectionEnd;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100562 mAllowReset = false;
563 mLogger.logSelectionModified(selectionStart, selectionEnd, classification, null);
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100564 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000565 }
566
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100567 /**
568 * Called when the selection action mode is destroyed.
569 */
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000570 public void onSelectionDestroyed() {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100571 mAllowReset = false;
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100572 // Wait a few ms to see if the selection was destroyed because of a text change event.
Jan Althausb3513a12017-09-22 18:26:06 +0200573 mDelayedLogAbandon.schedule(100 /* ms */);
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000574 }
575
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100576 /**
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100577 * Called when an action is taken on a smart selection.
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100578 */
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100579 public void onSelectionAction(
580 int selectionStart, int selectionEnd,
581 @SelectionEvent.ActionType int action,
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000582 @Nullable String actionLabel,
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100583 @Nullable TextClassification classification) {
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100584 if (isSelectionStarted()) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100585 mAllowReset = false;
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000586 mLogger.logSelectionAction(
587 selectionStart, selectionEnd, action, actionLabel, classification);
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100588 }
589 }
590
591 /**
592 * Returns true if the current smart selection should be reset to normal selection based on
593 * information that has been recorded about the original selection and the smart selection.
594 * The expected UX here is to allow the user to select a word inside of the smart selection
595 * on a single tap.
596 */
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100597 public boolean resetSelection(int textIndex, Editor editor) {
598 final TextView textView = editor.getTextView();
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100599 if (isSelectionStarted()
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100600 && mAllowReset
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100601 && textIndex >= mSelectionStart && textIndex <= mSelectionEnd
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100602 && getText(textView) instanceof Spannable) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100603 mAllowReset = false;
604 boolean selected = editor.selectCurrentWord();
605 if (selected) {
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100606 mSelectionStart = editor.getTextView().getSelectionStart();
607 mSelectionEnd = editor.getTextView().getSelectionEnd();
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100608 mLogger.logSelectionAction(
609 textView.getSelectionStart(), textView.getSelectionEnd(),
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000610 SelectionEvent.ACTION_RESET,
611 /* actionLabel= */ null, /* classification= */ null);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100612 }
613 return selected;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000614 }
615 return false;
616 }
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100617
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100618 public void onTextChanged(int start, int end, TextClassification classification) {
619 if (isSelectionStarted() && start == mSelectionStart && end == mSelectionEnd) {
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000620 onSelectionAction(
621 start, end, SelectionEvent.ACTION_OVERTYPE,
622 /* actionLabel= */ null, classification);
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100623 }
624 }
625
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100626 private void maybeInvalidateLogger() {
627 if (mLogger.isEditTextLogger() != mTextView.isTextEditable()) {
628 mLogger = new SelectionMetricsLogger(mTextView);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100629 }
630 }
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100631
632 private boolean isSelectionStarted() {
633 return mSelectionStart >= 0 && mSelectionEnd >= 0 && mSelectionStart != mSelectionEnd;
634 }
Jan Althausb3513a12017-09-22 18:26:06 +0200635
636 /** A helper for keeping track of pending abandon logging requests. */
637 private final class LogAbandonRunnable implements Runnable {
638 private boolean mIsPending;
639
640 /** Schedules an abandon to be logged with the given delay. Flush if necessary. */
641 void schedule(int delayMillis) {
642 if (mIsPending) {
643 Log.e(LOG_TAG, "Force flushing abandon due to new scheduling request");
644 flush();
645 }
646 mIsPending = true;
647 mTextView.postDelayed(this, delayMillis);
648 }
649
650 /** If there is a pending log request, execute it now. */
651 void flush() {
652 mTextView.removeCallbacks(this);
653 run();
654 }
655
656 @Override
657 public void run() {
658 if (mIsPending) {
659 mLogger.logSelectionAction(
660 mSelectionStart, mSelectionEnd,
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000661 SelectionEvent.ACTION_ABANDON,
662 /* actionLabel= */ null, /* classification= */ null);
Jan Althausb3513a12017-09-22 18:26:06 +0200663 mSelectionStart = mSelectionEnd = -1;
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100664 mLogger.endTextClassificationSession();
Jan Althausb3513a12017-09-22 18:26:06 +0200665 mIsPending = false;
666 }
667 }
668 }
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100669 }
670
671 // TODO: Write tests
672 /**
673 * Metrics logging helper.
674 *
675 * This logger logs selection by word indices. The initial (start) single word selection is
676 * logged at [0, 1) -- end index is exclusive. Other word indices are logged relative to the
677 * initial single word selection.
678 * e.g. New York city, NY. Suppose the initial selection is "York" in
679 * "New York city, NY", then "York" is at [0, 1), "New" is at [-1, 0], and "city" is at [1, 2).
680 * "New York" is at [-1, 1).
681 * Part selection of a word e.g. "or" is counted as selecting the
682 * entire word i.e. equivalent to "York", and each special character is counted as a word, e.g.
683 * "," is at [2, 3). Whitespaces are ignored.
Abodunrinwa Tokiad52f4b2018-02-06 23:32:41 +0000684 *
685 * NOTE that the definition of a word is defined by the TextClassifier's Logger's token
686 * iterator.
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100687 */
688 private static final class SelectionMetricsLogger {
689
690 private static final String LOG_TAG = "SelectionMetricsLogger";
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100691 private static final Pattern PATTERN_WHITESPACE = Pattern.compile("\\s+");
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100692
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100693 private final boolean mEditTextLogger;
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000694 private final BreakIterator mTokenIterator;
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100695
696 @Nullable private TextClassifier mClassificationSession;
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000697 @Nullable private TextClassificationContext mClassificationContext;
698
699 @Nullable private TextClassifierEvent mTranslateViewEvent;
700 @Nullable private TextClassifierEvent mTranslateClickEvent;
701
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100702 private int mStartIndex;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100703 private String mText;
704
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100705 SelectionMetricsLogger(TextView textView) {
706 Preconditions.checkNotNull(textView);
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100707 mEditTextLogger = textView.isTextEditable();
Jan Althaus5a030942018-04-04 19:40:38 +0200708 mTokenIterator = SelectionSessionLogger.getTokenIterator(textView.getTextLocale());
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000709 }
710
Jan Althaus92c6dec2018-02-02 09:20:14 +0100711 public void logSelectionStarted(
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100712 TextClassifier classificationSession,
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000713 TextClassificationContext classificationContext,
Jan Althaus92c6dec2018-02-02 09:20:14 +0100714 CharSequence text, int index,
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000715 @InvocationMethod int invocationMethod) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100716 try {
717 Preconditions.checkNotNull(text);
718 Preconditions.checkArgumentInRange(index, 0, text.length(), "index");
719 if (mText == null || !mText.contentEquals(text)) {
720 mText = text.toString();
721 }
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000722 mTokenIterator.setText(mText);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100723 mStartIndex = index;
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100724 mClassificationSession = classificationSession;
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000725 mClassificationContext = classificationContext;
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100726 if (hasActiveClassificationSession()) {
727 mClassificationSession.onSelectionEvent(
728 SelectionEvent.createSelectionStartedEvent(invocationMethod, 0));
729 }
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100730 } catch (Exception e) {
731 // Avoid crashes due to logging.
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000732 Log.e(LOG_TAG, "" + e.getMessage(), e);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100733 }
734 }
735
736 public void logSelectionModified(int start, int end,
737 @Nullable TextClassification classification, @Nullable TextSelection selection) {
738 try {
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100739 if (hasActiveClassificationSession()) {
740 Preconditions.checkArgumentInRange(start, 0, mText.length(), "start");
741 Preconditions.checkArgumentInRange(end, start, mText.length(), "end");
742 int[] wordIndices = getWordDelta(start, end);
743 if (selection != null) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100744 mClassificationSession.onSelectionEvent(
745 SelectionEvent.createSelectionModifiedEvent(
746 wordIndices[0], wordIndices[1], selection));
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100747 } else if (classification != null) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100748 mClassificationSession.onSelectionEvent(
749 SelectionEvent.createSelectionModifiedEvent(
750 wordIndices[0], wordIndices[1], classification));
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100751 } else {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100752 mClassificationSession.onSelectionEvent(
753 SelectionEvent.createSelectionModifiedEvent(
754 wordIndices[0], wordIndices[1]));
755 }
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000756 maybeGenerateTranslateViewEvent(classification);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100757 }
758 } catch (Exception e) {
759 // Avoid crashes due to logging.
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000760 Log.e(LOG_TAG, "" + e.getMessage(), e);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100761 }
762 }
763
764 public void logSelectionAction(
765 int start, int end,
766 @SelectionEvent.ActionType int action,
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000767 @Nullable String actionLabel,
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100768 @Nullable TextClassification classification) {
769 try {
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100770 if (hasActiveClassificationSession()) {
771 Preconditions.checkArgumentInRange(start, 0, mText.length(), "start");
772 Preconditions.checkArgumentInRange(end, start, mText.length(), "end");
773 int[] wordIndices = getWordDelta(start, end);
774 if (classification != null) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100775 mClassificationSession.onSelectionEvent(
776 SelectionEvent.createSelectionActionEvent(
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100777 wordIndices[0], wordIndices[1], action,
778 classification));
779 } else {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100780 mClassificationSession.onSelectionEvent(
781 SelectionEvent.createSelectionActionEvent(
782 wordIndices[0], wordIndices[1], action));
783 }
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000784
785 maybeGenerateTranslateClickEvent(classification, actionLabel);
786
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100787 if (SelectionEvent.isTerminal(action)) {
788 endTextClassificationSession();
789 }
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100790 }
791 } catch (Exception e) {
792 // Avoid crashes due to logging.
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000793 Log.e(LOG_TAG, "" + e.getMessage(), e);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100794 }
795 }
796
797 public boolean isEditTextLogger() {
798 return mEditTextLogger;
799 }
800
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100801 public void endTextClassificationSession() {
802 if (hasActiveClassificationSession()) {
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000803 maybeReportTranslateEvents();
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100804 mClassificationSession.destroy();
805 }
806 }
807
808 private boolean hasActiveClassificationSession() {
809 return mClassificationSession != null && !mClassificationSession.isDestroyed();
810 }
811
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100812 private int[] getWordDelta(int start, int end) {
813 int[] wordIndices = new int[2];
814
815 if (start == mStartIndex) {
816 wordIndices[0] = 0;
817 } else if (start < mStartIndex) {
818 wordIndices[0] = -countWordsForward(start);
819 } else { // start > mStartIndex
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100820 wordIndices[0] = countWordsBackward(start);
821
822 // For the selection start index, avoid counting a partial word backwards.
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000823 if (!mTokenIterator.isBoundary(start)
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100824 && !isWhitespace(
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000825 mTokenIterator.preceding(start),
826 mTokenIterator.following(start))) {
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100827 // We counted a partial word. Remove it.
828 wordIndices[0]--;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100829 }
830 }
831
832 if (end == mStartIndex) {
833 wordIndices[1] = 0;
834 } else if (end < mStartIndex) {
835 wordIndices[1] = -countWordsForward(end);
836 } else { // end > mStartIndex
837 wordIndices[1] = countWordsBackward(end);
838 }
839
840 return wordIndices;
841 }
842
843 private int countWordsBackward(int from) {
844 Preconditions.checkArgument(from >= mStartIndex);
845 int wordCount = 0;
846 int offset = from;
847 while (offset > mStartIndex) {
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000848 int start = mTokenIterator.preceding(offset);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100849 if (!isWhitespace(start, offset)) {
850 wordCount++;
851 }
852 offset = start;
853 }
854 return wordCount;
855 }
856
857 private int countWordsForward(int from) {
858 Preconditions.checkArgument(from <= mStartIndex);
859 int wordCount = 0;
860 int offset = from;
861 while (offset < mStartIndex) {
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000862 int end = mTokenIterator.following(offset);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100863 if (!isWhitespace(offset, end)) {
864 wordCount++;
865 }
866 offset = end;
867 }
868 return wordCount;
869 }
870
871 private boolean isWhitespace(int start, int end) {
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100872 return PATTERN_WHITESPACE.matcher(mText.substring(start, end)).matches();
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100873 }
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000874
875 private void maybeGenerateTranslateViewEvent(@Nullable TextClassification classification) {
876 if (classification != null) {
877 final TextClassifierEvent event = generateTranslateEvent(
878 TextClassifierEvent.TYPE_ACTIONS_SHOWN,
879 classification, mClassificationContext, /* actionLabel= */null);
880 mTranslateViewEvent = (event != null) ? event : mTranslateViewEvent;
881 }
882 }
883
884 private void maybeGenerateTranslateClickEvent(
885 @Nullable TextClassification classification, String actionLabel) {
886 if (classification != null) {
887 mTranslateClickEvent = generateTranslateEvent(
888 TextClassifierEvent.TYPE_SMART_ACTION,
889 classification, mClassificationContext, actionLabel);
890 }
891 }
892
893 private void maybeReportTranslateEvents() {
894 // Translate view and click events should only be logged once per selection session.
895 if (mTranslateViewEvent != null) {
896 mClassificationSession.onTextClassifierEvent(mTranslateViewEvent);
897 mTranslateViewEvent = null;
898 }
899 if (mTranslateClickEvent != null) {
900 mClassificationSession.onTextClassifierEvent(mTranslateClickEvent);
901 mTranslateClickEvent = null;
902 }
903 }
904
905 @Nullable
906 private static TextClassifierEvent generateTranslateEvent(
907 int eventType, TextClassification classification,
908 TextClassificationContext classificationContext, @Nullable String actionLabel) {
909
910 // The platform attempts to log "views" and "clicks" of the "Translate" action.
911 // Views are logged if a user is presented with the translate action during a selection
912 // session.
913 // Clicks are logged if the user clicks on the translate action.
914 // The index of the translate action is also logged to indicate whether it might have
915 // been in the main panel or overflow panel of the selection toolbar.
916 // NOTE that the "views" metric may be flawed if a TextView removes the translate menu
917 // item via a custom action mode callback or does not show a selection menu item.
918
919 final RemoteAction translateAction = ExtrasUtils.findTranslateAction(classification);
920 if (translateAction == null) {
921 // No translate action present. Nothing to log. Exit.
922 return null;
923 }
924
925 if (eventType == TextClassifierEvent.TYPE_SMART_ACTION
926 && !translateAction.getTitle().toString().equals(actionLabel)) {
927 // Clicked action is not a translate action. Nothing to log. Exit.
928 // Note that we don't expect an actionLabel for "view" events.
929 return null;
930 }
931
932 final Bundle foreignLanguageExtra = ExtrasUtils.getForeignLanguageExtra(classification);
933 final String language = ExtrasUtils.getEntityType(foreignLanguageExtra);
934 final float score = ExtrasUtils.getScore(foreignLanguageExtra);
935 final String model = ExtrasUtils.getModelName(foreignLanguageExtra);
Abodunrinwa Toki6d063372019-04-11 22:36:04 +0100936 return new TextClassifierEvent.LanguageDetectionEvent.Builder(eventType)
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000937 .setEventContext(classificationContext)
938 .setResultId(classification.getId())
939 .setEntityTypes(language)
Abodunrinwa Toki6d063372019-04-11 22:36:04 +0100940 .setScores(score)
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +0000941 .setActionIndices(classification.getActions().indexOf(translateAction))
942 .setModelName(model)
943 .build();
944 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000945 }
946
947 /**
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800948 * AsyncTask for running a query on a background thread and returning the result on the
949 * UiThread. The AsyncTask times out after a specified time, returning a null result if the
950 * query has not yet returned.
951 */
952 private static final class TextClassificationAsyncTask
953 extends AsyncTask<Void, Void, SelectionResult> {
954
955 private final int mTimeOutDuration;
956 private final Supplier<SelectionResult> mSelectionResultSupplier;
957 private final Consumer<SelectionResult> mSelectionResultCallback;
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000958 private final Supplier<SelectionResult> mTimeOutResultSupplier;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800959 private final TextView mTextView;
960 private final String mOriginalText;
961
962 /**
963 * @param textView the TextView
964 * @param timeOut time in milliseconds to timeout the query if it has not completed
965 * @param selectionResultSupplier fetches the selection results. Runs on a background thread
966 * @param selectionResultCallback receives the selection results. Runs on the UiThread
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000967 * @param timeOutResultSupplier default result if the task times out
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800968 */
969 TextClassificationAsyncTask(
970 @NonNull TextView textView, int timeOut,
971 @NonNull Supplier<SelectionResult> selectionResultSupplier,
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000972 @NonNull Consumer<SelectionResult> selectionResultCallback,
973 @NonNull Supplier<SelectionResult> timeOutResultSupplier) {
Makoto Onuki1488a3a2017-05-24 12:25:46 -0700974 super(textView != null ? textView.getHandler() : null);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800975 mTextView = Preconditions.checkNotNull(textView);
976 mTimeOutDuration = timeOut;
977 mSelectionResultSupplier = Preconditions.checkNotNull(selectionResultSupplier);
978 mSelectionResultCallback = Preconditions.checkNotNull(selectionResultCallback);
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000979 mTimeOutResultSupplier = Preconditions.checkNotNull(timeOutResultSupplier);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800980 // Make a copy of the original text.
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100981 mOriginalText = getText(mTextView).toString();
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800982 }
983
984 @Override
985 @WorkerThread
986 protected SelectionResult doInBackground(Void... params) {
987 final Runnable onTimeOut = this::onTimeOut;
988 mTextView.postDelayed(onTimeOut, mTimeOutDuration);
989 final SelectionResult result = mSelectionResultSupplier.get();
990 mTextView.removeCallbacks(onTimeOut);
991 return result;
992 }
993
994 @Override
995 @UiThread
996 protected void onPostExecute(SelectionResult result) {
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100997 result = TextUtils.equals(mOriginalText, getText(mTextView)) ? result : null;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800998 mSelectionResultCallback.accept(result);
999 }
1000
1001 private void onTimeOut() {
1002 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);
1053 mContext = Preconditions.checkNotNull(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) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001059 mTextClassifier = Preconditions.checkNotNull(textClassifier);
1060 mText = Preconditions.checkNotNull(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}