blob: 0d88e6986f1106af51bb508eae98cd50fc5de833 [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 Toki2b6020f2017-10-28 02:28:45 +010023import android.content.Context;
Petar Šegina5ab7bb22017-09-05 20:48:42 +010024import android.graphics.Canvas;
Petar Šegina91df3f92017-08-15 16:20:43 +010025import android.graphics.PointF;
Petar Šegina701ba332017-08-01 17:57:26 +010026import android.graphics.RectF;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080027import android.os.AsyncTask;
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +010028import android.os.Build;
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +000029import android.os.LocaleList;
Petar Šegina701ba332017-08-01 17:57:26 +010030import android.text.Layout;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080031import android.text.Selection;
32import android.text.Spannable;
33import android.text.TextUtils;
Abodunrinwa Tokia69950c2018-11-29 13:51:56 +000034import android.text.util.Linkify;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +010035import android.util.Log;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080036import android.view.ActionMode;
Abodunrinwa Tokif1d93992018-03-02 13:53:21 +000037import android.view.textclassifier.SelectionEvent;
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +000038import android.view.textclassifier.SelectionEvent.InvocationMethod;
Jan Althaus5a030942018-04-04 19:40:38 +020039import android.view.textclassifier.SelectionSessionLogger;
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +010040import android.view.textclassifier.TextClassification;
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +000041import android.view.textclassifier.TextClassificationConstants;
42import android.view.textclassifier.TextClassificationManager;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080043import android.view.textclassifier.TextClassifier;
44import android.view.textclassifier.TextSelection;
45import android.widget.Editor.SelectionModifierCursorController;
46
Petar Šegina91df3f92017-08-15 16:20:43 +010047import com.android.internal.annotations.VisibleForTesting;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080048import com.android.internal.util.Preconditions;
49
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +010050import java.text.BreakIterator;
Petar Šegina701ba332017-08-01 17:57:26 +010051import java.util.ArrayList;
Petar Šegina7c8196f2017-09-11 18:03:14 +010052import java.util.Comparator;
Petar Šegina701ba332017-08-01 17:57:26 +010053import java.util.List;
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +010054import java.util.Objects;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080055import java.util.function.Consumer;
Petar Šegina7c8196f2017-09-11 18:03:14 +010056import java.util.function.Function;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080057import java.util.function.Supplier;
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +010058import java.util.regex.Pattern;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080059
60/**
61 * Helper class for starting selection action mode
62 * (synchronously without the TextClassifier, asynchronously with the TextClassifier).
Petar Šegina91df3f92017-08-15 16:20:43 +010063 * @hide
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080064 */
65@UiThread
Petar Šegina91df3f92017-08-15 16:20:43 +010066@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
Petar Šeginaba1b8562017-08-31 18:09:16 +010067public final class SelectionActionModeHelper {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080068
Jan Althausb3513a12017-09-22 18:26:06 +020069 private static final String LOG_TAG = "SelectActionModeHelper";
70
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080071 private final Editor mEditor;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +010072 private final TextView mTextView;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080073 private final TextClassificationHelper mTextClassificationHelper;
74
Abodunrinwa Toki52096912018-03-21 23:14:42 +000075 @Nullable private TextClassification mTextClassification;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080076 private AsyncTask mTextClassificationAsyncTask;
77
Abodunrinwa Toki1d775572017-05-08 16:03:01 +010078 private final SelectionTracker mSelectionTracker;
Petar Šegina5ab7bb22017-09-05 20:48:42 +010079
80 // TODO remove nullable marker once the switch gating the feature gets removed
81 @Nullable
Petar Šegina701ba332017-08-01 17:57:26 +010082 private final SmartSelectSprite mSmartSelectSprite;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +000083
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080084 SelectionActionModeHelper(@NonNull Editor editor) {
85 mEditor = Preconditions.checkNotNull(editor);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +010086 mTextView = mEditor.getTextView();
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080087 mTextClassificationHelper = new TextClassificationHelper(
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +010088 mTextView.getContext(),
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +000089 mTextView::getTextClassifier,
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +010090 getText(mTextView),
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +010091 0, 1, mTextView.getTextLocales());
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +010092 mSelectionTracker = new SelectionTracker(mTextView);
Petar Šegina701ba332017-08-01 17:57:26 +010093
Abodunrinwa Tokic2449b82018-05-01 21:36:48 +010094 if (getTextClassificationSettings().isSmartSelectionAnimationEnabled()) {
Petar Šegina5ab7bb22017-09-05 20:48:42 +010095 mSmartSelectSprite = new SmartSelectSprite(mTextView.getContext(),
Jan Althaus80620c52018-02-02 17:39:22 +010096 editor.getTextView().mHighlightColor, mTextView::invalidate);
Petar Šegina701ba332017-08-01 17:57:26 +010097 } else {
98 mSmartSelectSprite = null;
99 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800100 }
101
Richard Ledley26b87222017-11-30 10:54:08 +0000102 /**
103 * Starts Selection ActionMode.
104 */
105 public void startSelectionActionModeAsync(boolean adjustSelection) {
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100106 // Check if the smart selection should run for editable text.
Abodunrinwa Tokic2449b82018-05-01 21:36:48 +0100107 adjustSelection &= getTextClassificationSettings().isSmartSelectionEnabled();
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100108
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100109 mSelectionTracker.onOriginalSelection(
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100110 getText(mTextView),
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100111 mTextView.getSelectionStart(),
Jan Althaus92c6dec2018-02-02 09:20:14 +0100112 mTextView.getSelectionEnd(),
113 false /*isLink*/);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800114 cancelAsyncTask();
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100115 if (skipTextClassification()) {
Richard Ledley26b87222017-11-30 10:54:08 +0000116 startSelectionActionMode(null);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800117 } else {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100118 resetTextClassificationHelper();
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800119 mTextClassificationAsyncTask = new TextClassificationAsyncTask(
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100120 mTextView,
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +0100121 mTextClassificationHelper.getTimeoutDuration(),
Abodunrinwa Toki66c16272017-05-03 20:22:55 +0100122 adjustSelection
123 ? mTextClassificationHelper::suggestSelection
124 : mTextClassificationHelper::classifyText,
Petar Šegina701ba332017-08-01 17:57:26 +0100125 mSmartSelectSprite != null
Richard Ledley26b87222017-11-30 10:54:08 +0000126 ? this::startSelectionActionModeWithSmartSelectAnimation
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000127 : this::startSelectionActionMode,
128 mTextClassificationHelper::getOriginalSelection)
Richard Ledley26b87222017-11-30 10:54:08 +0000129 .execute();
130 }
131 }
132
133 /**
134 * Starts Link ActionMode.
135 */
Richard Ledley27db81b2018-03-01 12:34:55 +0000136 public void startLinkActionModeAsync(int start, int end) {
137 mSelectionTracker.onOriginalSelection(getText(mTextView), start, end, true /*isLink*/);
Richard Ledley26b87222017-11-30 10:54:08 +0000138 cancelAsyncTask();
139 if (skipTextClassification()) {
140 startLinkActionMode(null);
141 } else {
Richard Ledley27db81b2018-03-01 12:34:55 +0000142 resetTextClassificationHelper(start, end);
Richard Ledley26b87222017-11-30 10:54:08 +0000143 mTextClassificationAsyncTask = new TextClassificationAsyncTask(
144 mTextView,
145 mTextClassificationHelper.getTimeoutDuration(),
146 mTextClassificationHelper::classifyText,
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000147 this::startLinkActionMode,
148 mTextClassificationHelper::getOriginalSelection)
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800149 .execute();
150 }
151 }
152
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800153 public void invalidateActionModeAsync() {
154 cancelAsyncTask();
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100155 if (skipTextClassification()) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800156 invalidateActionMode(null);
157 } else {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100158 resetTextClassificationHelper();
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800159 mTextClassificationAsyncTask = new TextClassificationAsyncTask(
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100160 mTextView,
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +0100161 mTextClassificationHelper.getTimeoutDuration(),
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100162 mTextClassificationHelper::classifyText,
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000163 this::invalidateActionMode,
164 mTextClassificationHelper::getOriginalSelection)
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800165 .execute();
166 }
167 }
168
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100169 public void onSelectionAction(int menuItemId) {
170 mSelectionTracker.onSelectionAction(
171 mTextView.getSelectionStart(), mTextView.getSelectionEnd(),
172 getActionType(menuItemId), mTextClassification);
173 }
174
175 public void onSelectionDrag() {
176 mSelectionTracker.onSelectionAction(
177 mTextView.getSelectionStart(), mTextView.getSelectionEnd(),
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000178 SelectionEvent.ACTION_DRAG, mTextClassification);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100179 }
180
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100181 public void onTextChanged(int start, int end) {
182 mSelectionTracker.onTextChanged(start, end, mTextClassification);
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100183 }
184
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100185 public boolean resetSelection(int textIndex) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100186 if (mSelectionTracker.resetSelection(textIndex, mEditor)) {
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000187 invalidateActionModeAsync();
188 return true;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800189 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000190 return false;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800191 }
192
193 @Nullable
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100194 public TextClassification getTextClassification() {
195 return mTextClassification;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800196 }
197
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000198 public void onDestroyActionMode() {
Petar Šegina701ba332017-08-01 17:57:26 +0100199 cancelSmartSelectAnimation();
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100200 mSelectionTracker.onSelectionDestroyed();
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000201 cancelAsyncTask();
202 }
203
Petar Šegina5ab7bb22017-09-05 20:48:42 +0100204 public void onDraw(final Canvas canvas) {
Jan Althaus80620c52018-02-02 17:39:22 +0100205 if (isDrawingHighlight() && mSmartSelectSprite != null) {
Petar Šegina5ab7bb22017-09-05 20:48:42 +0100206 mSmartSelectSprite.draw(canvas);
207 }
208 }
209
Jan Althaus80620c52018-02-02 17:39:22 +0100210 public boolean isDrawingHighlight() {
211 return mSmartSelectSprite != null && mSmartSelectSprite.isAnimationActive();
212 }
213
Abodunrinwa Tokic2449b82018-05-01 21:36:48 +0100214 private TextClassificationConstants getTextClassificationSettings() {
215 return TextClassificationManager.getSettings(mTextView.getContext());
216 }
217
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000218 private void cancelAsyncTask() {
219 if (mTextClassificationAsyncTask != null) {
220 mTextClassificationAsyncTask.cancel(true);
221 mTextClassificationAsyncTask = null;
222 }
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100223 mTextClassification = null;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000224 }
225
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100226 private boolean skipTextClassification() {
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100227 // No need to make an async call for a no-op TextClassifier.
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000228 final boolean noOpTextClassifier = mTextView.usesNoOpTextClassifier();
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100229 // Do not call the TextClassifier if there is no selection.
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100230 final boolean noSelection = mTextView.getSelectionEnd() == mTextView.getSelectionStart();
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100231 // Do not call the TextClassifier if this is a password field.
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100232 final boolean password = mTextView.hasPasswordTransformationMethod()
233 || TextView.isPasswordInputType(mTextView.getInputType());
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100234 return noOpTextClassifier || noSelection || password;
Abodunrinwa Toki792d8202017-03-06 23:51:11 +0000235 }
236
Richard Ledley26b87222017-11-30 10:54:08 +0000237 private void startLinkActionMode(@Nullable SelectionResult result) {
238 startActionMode(Editor.TextActionMode.TEXT_LINK, result);
239 }
240
241 private void startSelectionActionMode(@Nullable SelectionResult result) {
242 startActionMode(Editor.TextActionMode.SELECTION, result);
243 }
244
245 private void startActionMode(
246 @Editor.TextActionMode int actionMode, @Nullable SelectionResult result) {
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100247 final CharSequence text = getText(mTextView);
Richard Ledley26b87222017-11-30 10:54:08 +0000248 if (result != null && text instanceof Spannable
Richard Ledley27db81b2018-03-01 12:34:55 +0000249 && (mTextView.isTextSelectable() || mTextView.isTextEditable())) {
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100250 // Do not change the selection if TextClassifier should be dark launched.
Abodunrinwa Tokic2449b82018-05-01 21:36:48 +0100251 if (!getTextClassificationSettings().isModelDarkLaunchEnabled()) {
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100252 Selection.setSelection((Spannable) text, result.mStart, result.mEnd);
Richard Ledley724eff92017-12-21 10:11:34 +0000253 mTextView.invalidate();
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100254 }
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100255 mTextClassification = result.mClassification;
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000256 } else if (result != null && actionMode == Editor.TextActionMode.TEXT_LINK) {
Richard Ledley27db81b2018-03-01 12:34:55 +0000257 mTextClassification = result.mClassification;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800258 } else {
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100259 mTextClassification = null;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800260 }
Richard Ledley26b87222017-11-30 10:54:08 +0000261 if (mEditor.startActionModeInternal(actionMode)) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800262 final SelectionModifierCursorController controller = mEditor.getSelectionController();
Richard Ledley26b87222017-11-30 10:54:08 +0000263 if (controller != null
264 && (mTextView.isTextSelectable() || mTextView.isTextEditable())) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800265 controller.show();
266 }
Richard Ledley724eff92017-12-21 10:11:34 +0000267 if (result != null) {
268 switch (actionMode) {
269 case Editor.TextActionMode.SELECTION:
270 mSelectionTracker.onSmartSelection(result);
271 break;
272 case Editor.TextActionMode.TEXT_LINK:
273 mSelectionTracker.onLinkSelected(result);
274 break;
275 default:
276 break;
277 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000278 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800279 }
280 mEditor.setRestartActionModeOnNextRefresh(false);
281 mTextClassificationAsyncTask = null;
282 }
283
Richard Ledley26b87222017-11-30 10:54:08 +0000284 private void startSelectionActionModeWithSmartSelectAnimation(
285 @Nullable SelectionResult result) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100286 final Layout layout = mTextView.getLayout();
Petar Šegina701ba332017-08-01 17:57:26 +0100287
Mihai Popa6748ff32018-05-08 19:18:43 +0100288 final Runnable onAnimationEndCallback = () -> {
Mihai Popa6e8e27b2018-05-25 11:57:17 +0100289 final SelectionResult startSelectionResult;
290 if (result != null && result.mStart >= 0 && result.mEnd <= getText(mTextView).length()
Mihai Popa6748ff32018-05-08 19:18:43 +0100291 && result.mStart <= result.mEnd) {
Mihai Popa6e8e27b2018-05-25 11:57:17 +0100292 startSelectionResult = result;
293 } else {
294 startSelectionResult = null;
Mihai Popa6748ff32018-05-08 19:18:43 +0100295 }
Mihai Popa6e8e27b2018-05-25 11:57:17 +0100296 startSelectionActionMode(startSelectionResult);
Mihai Popa6748ff32018-05-08 19:18:43 +0100297 };
Petar Šegina701ba332017-08-01 17:57:26 +0100298 // TODO do not trigger the animation if the change included only non-printable characters
299 final boolean didSelectionChange =
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100300 result != null && (mTextView.getSelectionStart() != result.mStart
301 || mTextView.getSelectionEnd() != result.mEnd);
Petar Šegina701ba332017-08-01 17:57:26 +0100302
303 if (!didSelectionChange) {
304 onAnimationEndCallback.run();
305 return;
306 }
307
Petar Šegina7c8196f2017-09-11 18:03:14 +0100308 final List<SmartSelectSprite.RectangleWithTextSelectionLayout> selectionRectangles =
Petar Šegina701ba332017-08-01 17:57:26 +0100309 convertSelectionToRectangles(layout, result.mStart, result.mEnd);
310
Petar Šegina91df3f92017-08-15 16:20:43 +0100311 final PointF touchPoint = new PointF(
312 mEditor.getLastUpPositionX(),
313 mEditor.getLastUpPositionY());
Petar Šegina701ba332017-08-01 17:57:26 +0100314
Petar Šegina91df3f92017-08-15 16:20:43 +0100315 final PointF animationStartPoint =
Petar Šegina7c8196f2017-09-11 18:03:14 +0100316 movePointInsideNearestRectangle(touchPoint, selectionRectangles,
317 SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle);
Petar Šegina701ba332017-08-01 17:57:26 +0100318
319 mSmartSelectSprite.startAnimation(
Petar Šegina91df3f92017-08-15 16:20:43 +0100320 animationStartPoint,
Petar Šegina701ba332017-08-01 17:57:26 +0100321 selectionRectangles,
322 onAnimationEndCallback);
323 }
324
Petar Šegina7c8196f2017-09-11 18:03:14 +0100325 private List<SmartSelectSprite.RectangleWithTextSelectionLayout> convertSelectionToRectangles(
326 final Layout layout, final int start, final int end) {
327 final List<SmartSelectSprite.RectangleWithTextSelectionLayout> result = new ArrayList<>();
Petar Šegina72729252017-08-31 15:25:06 +0100328
Petar Šegina7c8196f2017-09-11 18:03:14 +0100329 final Layout.SelectionRectangleConsumer consumer =
330 (left, top, right, bottom, textSelectionLayout) -> mergeRectangleIntoList(
331 result,
332 new RectF(left, top, right, bottom),
333 SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle,
334 r -> new SmartSelectSprite.RectangleWithTextSelectionLayout(r,
335 textSelectionLayout)
336 );
337
338 layout.getSelection(start, end, consumer);
339
340 result.sort(Comparator.comparing(
341 SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle,
342 SmartSelectSprite.RECTANGLE_COMPARATOR));
343
Petar Šegina701ba332017-08-01 17:57:26 +0100344 return result;
345 }
346
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +0100347 // TODO: Move public pure functions out of this class and make it package-private.
Petar Šeginaba1b8562017-08-31 18:09:16 +0100348 /**
Petar Šegina7c8196f2017-09-11 18:03:14 +0100349 * Merges a {@link RectF} into an existing list of any objects which contain a rectangle.
350 * While merging, this method makes sure that:
Petar Šeginaba1b8562017-08-31 18:09:16 +0100351 *
352 * <ol>
353 * <li>No rectangle is redundant (contained within a bigger rectangle)</li>
354 * <li>Rectangles of the same height and vertical position that intersect get merged</li>
355 * </ol>
356 *
Petar Šegina7c8196f2017-09-11 18:03:14 +0100357 * @param list the list of rectangles (or other rectangle containers) to merge the new
358 * rectangle into
Petar Šeginaba1b8562017-08-31 18:09:16 +0100359 * @param candidate the {@link RectF} to merge into the list
Petar Šegina7c8196f2017-09-11 18:03:14 +0100360 * @param extractor a function that can extract a {@link RectF} from an element of the given
361 * list
362 * @param packer a function that can wrap the resulting {@link RectF} into an element that
363 * the list contains
Petar Šeginaba1b8562017-08-31 18:09:16 +0100364 * @hide
365 */
366 @VisibleForTesting
Petar Šegina7c8196f2017-09-11 18:03:14 +0100367 public static <T> void mergeRectangleIntoList(final List<T> list,
368 final RectF candidate, final Function<T, RectF> extractor,
369 final Function<RectF, T> packer) {
Petar Šeginaba1b8562017-08-31 18:09:16 +0100370 if (candidate.isEmpty()) {
371 return;
372 }
373
374 final int elementCount = list.size();
375 for (int index = 0; index < elementCount; ++index) {
Petar Šegina7c8196f2017-09-11 18:03:14 +0100376 final RectF existingRectangle = extractor.apply(list.get(index));
Petar Šeginaba1b8562017-08-31 18:09:16 +0100377 if (existingRectangle.contains(candidate)) {
378 return;
379 }
380 if (candidate.contains(existingRectangle)) {
381 existingRectangle.setEmpty();
382 continue;
383 }
384
385 final boolean rectanglesContinueEachOther = candidate.left == existingRectangle.right
386 || candidate.right == existingRectangle.left;
387 final boolean canMerge = candidate.top == existingRectangle.top
388 && candidate.bottom == existingRectangle.bottom
389 && (RectF.intersects(candidate, existingRectangle)
390 || rectanglesContinueEachOther);
391
392 if (canMerge) {
393 candidate.union(existingRectangle);
394 existingRectangle.setEmpty();
395 }
396 }
397
398 for (int index = elementCount - 1; index >= 0; --index) {
Petar Šegina7c8196f2017-09-11 18:03:14 +0100399 final RectF rectangle = extractor.apply(list.get(index));
400 if (rectangle.isEmpty()) {
Petar Šeginaba1b8562017-08-31 18:09:16 +0100401 list.remove(index);
402 }
403 }
404
Petar Šegina7c8196f2017-09-11 18:03:14 +0100405 list.add(packer.apply(candidate));
Petar Šeginaba1b8562017-08-31 18:09:16 +0100406 }
407
408
Petar Šegina91df3f92017-08-15 16:20:43 +0100409 /** @hide */
410 @VisibleForTesting
Petar Šegina7c8196f2017-09-11 18:03:14 +0100411 public static <T> PointF movePointInsideNearestRectangle(final PointF point,
412 final List<T> list, final Function<T, RectF> extractor) {
Petar Šegina91df3f92017-08-15 16:20:43 +0100413 float bestX = -1;
414 float bestY = -1;
415 double bestDistance = Double.MAX_VALUE;
416
Petar Šegina7c8196f2017-09-11 18:03:14 +0100417 final int elementCount = list.size();
Petar Šeginaba1b8562017-08-31 18:09:16 +0100418 for (int index = 0; index < elementCount; ++index) {
Petar Šegina7c8196f2017-09-11 18:03:14 +0100419 final RectF rectangle = extractor.apply(list.get(index));
Petar Šegina91df3f92017-08-15 16:20:43 +0100420 final float candidateY = rectangle.centerY();
421 final float candidateX;
422
423 if (point.x > rectangle.right) {
424 candidateX = rectangle.right;
425 } else if (point.x < rectangle.left) {
426 candidateX = rectangle.left;
427 } else {
428 candidateX = point.x;
429 }
430
431 final double candidateDistance = Math.pow(point.x - candidateX, 2)
432 + Math.pow(point.y - candidateY, 2);
433
434 if (candidateDistance < bestDistance) {
435 bestX = candidateX;
436 bestY = candidateY;
437 bestDistance = candidateDistance;
438 }
439 }
440
441 return new PointF(bestX, bestY);
442 }
443
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800444 private void invalidateActionMode(@Nullable SelectionResult result) {
Petar Šegina701ba332017-08-01 17:57:26 +0100445 cancelSmartSelectAnimation();
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100446 mTextClassification = result != null ? result.mClassification : null;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800447 final ActionMode actionMode = mEditor.getTextActionMode();
448 if (actionMode != null) {
449 actionMode.invalidate();
450 }
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100451 mSelectionTracker.onSelectionUpdated(
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100452 mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mTextClassification);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800453 mTextClassificationAsyncTask = null;
454 }
455
Richard Ledley26b87222017-11-30 10:54:08 +0000456 private void resetTextClassificationHelper(int selectionStart, int selectionEnd) {
457 if (selectionStart < 0 || selectionEnd < 0) {
458 // Use selection indices
459 selectionStart = mTextView.getSelectionStart();
460 selectionEnd = mTextView.getSelectionEnd();
461 }
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100462 mTextClassificationHelper.init(
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000463 mTextView::getTextClassifier,
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100464 getText(mTextView),
Richard Ledley26b87222017-11-30 10:54:08 +0000465 selectionStart, selectionEnd,
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100466 mTextView.getTextLocales());
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800467 }
468
Richard Ledley26b87222017-11-30 10:54:08 +0000469 private void resetTextClassificationHelper() {
470 resetTextClassificationHelper(-1, -1);
471 }
472
Petar Šegina701ba332017-08-01 17:57:26 +0100473 private void cancelSmartSelectAnimation() {
474 if (mSmartSelectSprite != null) {
475 mSmartSelectSprite.cancelAnimation();
476 }
477 }
478
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800479 /**
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100480 * Tracks and logs smart selection changes.
481 * It is important to trigger this object's methods at the appropriate event so that it tracks
482 * smart selection events appropriately.
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000483 */
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100484 private static final class SelectionTracker {
485
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100486 private final TextView mTextView;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100487 private SelectionMetricsLogger mLogger;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000488
489 private int mOriginalStart;
490 private int mOriginalEnd;
491 private int mSelectionStart;
492 private int mSelectionEnd;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100493 private boolean mAllowReset;
Jan Althausb3513a12017-09-22 18:26:06 +0200494 private final LogAbandonRunnable mDelayedLogAbandon = new LogAbandonRunnable();
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000495
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100496 SelectionTracker(TextView textView) {
497 mTextView = Preconditions.checkNotNull(textView);
498 mLogger = new SelectionMetricsLogger(textView);
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100499 }
500
501 /**
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100502 * Called when the original selection happens, before smart selection is triggered.
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100503 */
Jan Althaus92c6dec2018-02-02 09:20:14 +0100504 public void onOriginalSelection(
505 CharSequence text, int selectionStart, int selectionEnd, boolean isLink) {
Jan Althausb3513a12017-09-22 18:26:06 +0200506 // If we abandoned a selection and created a new one very shortly after, we may still
507 // have a pending request to log ABANDON, which we flush here.
508 mDelayedLogAbandon.flush();
509
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100510 mOriginalStart = mSelectionStart = selectionStart;
511 mOriginalEnd = mSelectionEnd = selectionEnd;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100512 mAllowReset = false;
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100513 maybeInvalidateLogger();
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100514 mLogger.logSelectionStarted(mTextView.getTextClassificationSession(),
515 text, selectionStart,
Jan Althaus92c6dec2018-02-02 09:20:14 +0100516 isLink ? SelectionEvent.INVOCATION_LINK : SelectionEvent.INVOCATION_MANUAL);
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000517 }
518
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100519 /**
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100520 * Called when selection action mode is started and the results come from a classifier.
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100521 */
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100522 public void onSmartSelection(SelectionResult result) {
Richard Ledley724eff92017-12-21 10:11:34 +0000523 onClassifiedSelection(result);
524 mLogger.logSelectionModified(
525 result.mStart, result.mEnd, result.mClassification, result.mSelection);
526 }
527
528 /**
529 * Called when link action mode is started and the classification comes from a classifier.
530 */
531 public void onLinkSelected(SelectionResult result) {
532 onClassifiedSelection(result);
533 // TODO: log (b/70246800)
534 }
535
536 private void onClassifiedSelection(SelectionResult result) {
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100537 if (isSelectionStarted()) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100538 mSelectionStart = result.mStart;
539 mSelectionEnd = result.mEnd;
540 mAllowReset = mSelectionStart != mOriginalStart || mSelectionEnd != mOriginalEnd;
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100541 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000542 }
543
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100544 /**
545 * Called when selection bounds change.
546 */
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100547 public void onSelectionUpdated(
548 int selectionStart, int selectionEnd,
549 @Nullable TextClassification classification) {
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100550 if (isSelectionStarted()) {
551 mSelectionStart = selectionStart;
552 mSelectionEnd = selectionEnd;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100553 mAllowReset = false;
554 mLogger.logSelectionModified(selectionStart, selectionEnd, classification, null);
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100555 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000556 }
557
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100558 /**
559 * Called when the selection action mode is destroyed.
560 */
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000561 public void onSelectionDestroyed() {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100562 mAllowReset = false;
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100563 // Wait a few ms to see if the selection was destroyed because of a text change event.
Jan Althausb3513a12017-09-22 18:26:06 +0200564 mDelayedLogAbandon.schedule(100 /* ms */);
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000565 }
566
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100567 /**
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100568 * Called when an action is taken on a smart selection.
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100569 */
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100570 public void onSelectionAction(
571 int selectionStart, int selectionEnd,
572 @SelectionEvent.ActionType int action,
573 @Nullable TextClassification classification) {
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100574 if (isSelectionStarted()) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100575 mAllowReset = false;
576 mLogger.logSelectionAction(selectionStart, selectionEnd, action, classification);
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100577 }
578 }
579
580 /**
581 * Returns true if the current smart selection should be reset to normal selection based on
582 * information that has been recorded about the original selection and the smart selection.
583 * The expected UX here is to allow the user to select a word inside of the smart selection
584 * on a single tap.
585 */
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100586 public boolean resetSelection(int textIndex, Editor editor) {
587 final TextView textView = editor.getTextView();
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100588 if (isSelectionStarted()
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100589 && mAllowReset
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100590 && textIndex >= mSelectionStart && textIndex <= mSelectionEnd
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100591 && getText(textView) instanceof Spannable) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100592 mAllowReset = false;
593 boolean selected = editor.selectCurrentWord();
594 if (selected) {
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100595 mSelectionStart = editor.getTextView().getSelectionStart();
596 mSelectionEnd = editor.getTextView().getSelectionEnd();
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100597 mLogger.logSelectionAction(
598 textView.getSelectionStart(), textView.getSelectionEnd(),
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000599 SelectionEvent.ACTION_RESET, null /* classification */);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100600 }
601 return selected;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000602 }
603 return false;
604 }
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100605
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100606 public void onTextChanged(int start, int end, TextClassification classification) {
607 if (isSelectionStarted() && start == mSelectionStart && end == mSelectionEnd) {
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000608 onSelectionAction(start, end, SelectionEvent.ACTION_OVERTYPE, classification);
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100609 }
610 }
611
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100612 private void maybeInvalidateLogger() {
613 if (mLogger.isEditTextLogger() != mTextView.isTextEditable()) {
614 mLogger = new SelectionMetricsLogger(mTextView);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100615 }
616 }
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100617
618 private boolean isSelectionStarted() {
619 return mSelectionStart >= 0 && mSelectionEnd >= 0 && mSelectionStart != mSelectionEnd;
620 }
Jan Althausb3513a12017-09-22 18:26:06 +0200621
622 /** A helper for keeping track of pending abandon logging requests. */
623 private final class LogAbandonRunnable implements Runnable {
624 private boolean mIsPending;
625
626 /** Schedules an abandon to be logged with the given delay. Flush if necessary. */
627 void schedule(int delayMillis) {
628 if (mIsPending) {
629 Log.e(LOG_TAG, "Force flushing abandon due to new scheduling request");
630 flush();
631 }
632 mIsPending = true;
633 mTextView.postDelayed(this, delayMillis);
634 }
635
636 /** If there is a pending log request, execute it now. */
637 void flush() {
638 mTextView.removeCallbacks(this);
639 run();
640 }
641
642 @Override
643 public void run() {
644 if (mIsPending) {
645 mLogger.logSelectionAction(
646 mSelectionStart, mSelectionEnd,
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000647 SelectionEvent.ACTION_ABANDON, null /* classification */);
Jan Althausb3513a12017-09-22 18:26:06 +0200648 mSelectionStart = mSelectionEnd = -1;
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100649 mLogger.endTextClassificationSession();
Jan Althausb3513a12017-09-22 18:26:06 +0200650 mIsPending = false;
651 }
652 }
653 }
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100654 }
655
656 // TODO: Write tests
657 /**
658 * Metrics logging helper.
659 *
660 * This logger logs selection by word indices. The initial (start) single word selection is
661 * logged at [0, 1) -- end index is exclusive. Other word indices are logged relative to the
662 * initial single word selection.
663 * e.g. New York city, NY. Suppose the initial selection is "York" in
664 * "New York city, NY", then "York" is at [0, 1), "New" is at [-1, 0], and "city" is at [1, 2).
665 * "New York" is at [-1, 1).
666 * Part selection of a word e.g. "or" is counted as selecting the
667 * entire word i.e. equivalent to "York", and each special character is counted as a word, e.g.
668 * "," is at [2, 3). Whitespaces are ignored.
Abodunrinwa Tokiad52f4b2018-02-06 23:32:41 +0000669 *
670 * NOTE that the definition of a word is defined by the TextClassifier's Logger's token
671 * iterator.
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100672 */
673 private static final class SelectionMetricsLogger {
674
675 private static final String LOG_TAG = "SelectionMetricsLogger";
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100676 private static final Pattern PATTERN_WHITESPACE = Pattern.compile("\\s+");
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100677
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100678 private final boolean mEditTextLogger;
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000679 private final BreakIterator mTokenIterator;
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100680
681 @Nullable private TextClassifier mClassificationSession;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100682 private int mStartIndex;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100683 private String mText;
684
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100685 SelectionMetricsLogger(TextView textView) {
686 Preconditions.checkNotNull(textView);
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100687 mEditTextLogger = textView.isTextEditable();
Jan Althaus5a030942018-04-04 19:40:38 +0200688 mTokenIterator = SelectionSessionLogger.getTokenIterator(textView.getTextLocale());
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000689 }
690
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000691 @TextClassifier.WidgetType
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000692 private static String getWidetType(TextView textView) {
693 if (textView.isTextEditable()) {
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000694 return TextClassifier.WIDGET_TYPE_EDITTEXT;
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000695 }
696 if (textView.isTextSelectable()) {
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000697 return TextClassifier.WIDGET_TYPE_TEXTVIEW;
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000698 }
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000699 return TextClassifier.WIDGET_TYPE_UNSELECTABLE_TEXTVIEW;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100700 }
701
Jan Althaus92c6dec2018-02-02 09:20:14 +0100702 public void logSelectionStarted(
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100703 TextClassifier classificationSession,
Jan Althaus92c6dec2018-02-02 09:20:14 +0100704 CharSequence text, int index,
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000705 @InvocationMethod int invocationMethod) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100706 try {
707 Preconditions.checkNotNull(text);
708 Preconditions.checkArgumentInRange(index, 0, text.length(), "index");
709 if (mText == null || !mText.contentEquals(text)) {
710 mText = text.toString();
711 }
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000712 mTokenIterator.setText(mText);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100713 mStartIndex = index;
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100714 mClassificationSession = classificationSession;
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100715 if (hasActiveClassificationSession()) {
716 mClassificationSession.onSelectionEvent(
717 SelectionEvent.createSelectionStartedEvent(invocationMethod, 0));
718 }
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100719 } catch (Exception e) {
720 // Avoid crashes due to logging.
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000721 Log.e(LOG_TAG, "" + e.getMessage(), e);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100722 }
723 }
724
725 public void logSelectionModified(int start, int end,
726 @Nullable TextClassification classification, @Nullable TextSelection selection) {
727 try {
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100728 if (hasActiveClassificationSession()) {
729 Preconditions.checkArgumentInRange(start, 0, mText.length(), "start");
730 Preconditions.checkArgumentInRange(end, start, mText.length(), "end");
731 int[] wordIndices = getWordDelta(start, end);
732 if (selection != null) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100733 mClassificationSession.onSelectionEvent(
734 SelectionEvent.createSelectionModifiedEvent(
735 wordIndices[0], wordIndices[1], selection));
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100736 } else if (classification != null) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100737 mClassificationSession.onSelectionEvent(
738 SelectionEvent.createSelectionModifiedEvent(
739 wordIndices[0], wordIndices[1], classification));
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100740 } else {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100741 mClassificationSession.onSelectionEvent(
742 SelectionEvent.createSelectionModifiedEvent(
743 wordIndices[0], wordIndices[1]));
744 }
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100745 }
746 } catch (Exception e) {
747 // Avoid crashes due to logging.
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000748 Log.e(LOG_TAG, "" + e.getMessage(), e);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100749 }
750 }
751
752 public void logSelectionAction(
753 int start, int end,
754 @SelectionEvent.ActionType int action,
755 @Nullable TextClassification classification) {
756 try {
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100757 if (hasActiveClassificationSession()) {
758 Preconditions.checkArgumentInRange(start, 0, mText.length(), "start");
759 Preconditions.checkArgumentInRange(end, start, mText.length(), "end");
760 int[] wordIndices = getWordDelta(start, end);
761 if (classification != null) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100762 mClassificationSession.onSelectionEvent(
763 SelectionEvent.createSelectionActionEvent(
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100764 wordIndices[0], wordIndices[1], action,
765 classification));
766 } else {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100767 mClassificationSession.onSelectionEvent(
768 SelectionEvent.createSelectionActionEvent(
769 wordIndices[0], wordIndices[1], action));
770 }
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100771 if (SelectionEvent.isTerminal(action)) {
772 endTextClassificationSession();
773 }
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100774 }
775 } catch (Exception e) {
776 // Avoid crashes due to logging.
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000777 Log.e(LOG_TAG, "" + e.getMessage(), e);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100778 }
779 }
780
781 public boolean isEditTextLogger() {
782 return mEditTextLogger;
783 }
784
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100785 public void endTextClassificationSession() {
786 if (hasActiveClassificationSession()) {
787 mClassificationSession.destroy();
788 }
789 }
790
791 private boolean hasActiveClassificationSession() {
792 return mClassificationSession != null && !mClassificationSession.isDestroyed();
793 }
794
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100795 private int[] getWordDelta(int start, int end) {
796 int[] wordIndices = new int[2];
797
798 if (start == mStartIndex) {
799 wordIndices[0] = 0;
800 } else if (start < mStartIndex) {
801 wordIndices[0] = -countWordsForward(start);
802 } else { // start > mStartIndex
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100803 wordIndices[0] = countWordsBackward(start);
804
805 // For the selection start index, avoid counting a partial word backwards.
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000806 if (!mTokenIterator.isBoundary(start)
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100807 && !isWhitespace(
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000808 mTokenIterator.preceding(start),
809 mTokenIterator.following(start))) {
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100810 // We counted a partial word. Remove it.
811 wordIndices[0]--;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100812 }
813 }
814
815 if (end == mStartIndex) {
816 wordIndices[1] = 0;
817 } else if (end < mStartIndex) {
818 wordIndices[1] = -countWordsForward(end);
819 } else { // end > mStartIndex
820 wordIndices[1] = countWordsBackward(end);
821 }
822
823 return wordIndices;
824 }
825
826 private int countWordsBackward(int from) {
827 Preconditions.checkArgument(from >= mStartIndex);
828 int wordCount = 0;
829 int offset = from;
830 while (offset > mStartIndex) {
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000831 int start = mTokenIterator.preceding(offset);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100832 if (!isWhitespace(start, offset)) {
833 wordCount++;
834 }
835 offset = start;
836 }
837 return wordCount;
838 }
839
840 private int countWordsForward(int from) {
841 Preconditions.checkArgument(from <= mStartIndex);
842 int wordCount = 0;
843 int offset = from;
844 while (offset < mStartIndex) {
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000845 int end = mTokenIterator.following(offset);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100846 if (!isWhitespace(offset, end)) {
847 wordCount++;
848 }
849 offset = end;
850 }
851 return wordCount;
852 }
853
854 private boolean isWhitespace(int start, int end) {
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100855 return PATTERN_WHITESPACE.matcher(mText.substring(start, end)).matches();
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100856 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000857 }
858
859 /**
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800860 * AsyncTask for running a query on a background thread and returning the result on the
861 * UiThread. The AsyncTask times out after a specified time, returning a null result if the
862 * query has not yet returned.
863 */
864 private static final class TextClassificationAsyncTask
865 extends AsyncTask<Void, Void, SelectionResult> {
866
867 private final int mTimeOutDuration;
868 private final Supplier<SelectionResult> mSelectionResultSupplier;
869 private final Consumer<SelectionResult> mSelectionResultCallback;
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000870 private final Supplier<SelectionResult> mTimeOutResultSupplier;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800871 private final TextView mTextView;
872 private final String mOriginalText;
873
874 /**
875 * @param textView the TextView
876 * @param timeOut time in milliseconds to timeout the query if it has not completed
877 * @param selectionResultSupplier fetches the selection results. Runs on a background thread
878 * @param selectionResultCallback receives the selection results. Runs on the UiThread
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000879 * @param timeOutResultSupplier default result if the task times out
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800880 */
881 TextClassificationAsyncTask(
882 @NonNull TextView textView, int timeOut,
883 @NonNull Supplier<SelectionResult> selectionResultSupplier,
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000884 @NonNull Consumer<SelectionResult> selectionResultCallback,
885 @NonNull Supplier<SelectionResult> timeOutResultSupplier) {
Makoto Onuki1488a3a2017-05-24 12:25:46 -0700886 super(textView != null ? textView.getHandler() : null);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800887 mTextView = Preconditions.checkNotNull(textView);
888 mTimeOutDuration = timeOut;
889 mSelectionResultSupplier = Preconditions.checkNotNull(selectionResultSupplier);
890 mSelectionResultCallback = Preconditions.checkNotNull(selectionResultCallback);
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000891 mTimeOutResultSupplier = Preconditions.checkNotNull(timeOutResultSupplier);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800892 // Make a copy of the original text.
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100893 mOriginalText = getText(mTextView).toString();
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800894 }
895
896 @Override
897 @WorkerThread
898 protected SelectionResult doInBackground(Void... params) {
899 final Runnable onTimeOut = this::onTimeOut;
900 mTextView.postDelayed(onTimeOut, mTimeOutDuration);
901 final SelectionResult result = mSelectionResultSupplier.get();
902 mTextView.removeCallbacks(onTimeOut);
903 return result;
904 }
905
906 @Override
907 @UiThread
908 protected void onPostExecute(SelectionResult result) {
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100909 result = TextUtils.equals(mOriginalText, getText(mTextView)) ? result : null;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800910 mSelectionResultCallback.accept(result);
911 }
912
913 private void onTimeOut() {
914 if (getStatus() == Status.RUNNING) {
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000915 onPostExecute(mTimeOutResultSupplier.get());
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800916 }
917 cancel(true);
918 }
919 }
920
921 /**
922 * Helper class for querying the TextClassifier.
923 * It trims text so that only text necessary to provide context of the selected text is
924 * sent to the TextClassifier.
925 */
926 private static final class TextClassificationHelper {
927
Abodunrinwa Tokid2d13992017-03-24 21:43:13 +0000928 private static final int TRIM_DELTA = 120; // characters
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800929
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000930 private final Context mContext;
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000931 private Supplier<TextClassifier> mTextClassifier;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800932
933 /** The original TextView text. **/
934 private String mText;
935 /** Start index relative to mText. */
936 private int mSelectionStart;
937 /** End index relative to mText. */
938 private int mSelectionEnd;
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100939
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100940 @Nullable
941 private LocaleList mDefaultLocales;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800942
943 /** Trimmed text starting from mTrimStart in mText. */
944 private CharSequence mTrimmedText;
945 /** Index indicating the start of mTrimmedText in mText. */
946 private int mTrimStart;
947 /** Start index relative to mTrimmedText */
948 private int mRelativeStart;
949 /** End index relative to mTrimmedText */
950 private int mRelativeEnd;
951
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +0100952 /** Information about the last classified text to avoid re-running a query. */
953 private CharSequence mLastClassificationText;
954 private int mLastClassificationSelectionStart;
955 private int mLastClassificationSelectionEnd;
956 private LocaleList mLastClassificationLocales;
957 private SelectionResult mLastClassificationResult;
958
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +0100959 /** Whether the TextClassifier has been initialized. */
960 private boolean mHot;
961
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000962 TextClassificationHelper(Context context, Supplier<TextClassifier> textClassifier,
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +0000963 CharSequence text, int selectionStart, int selectionEnd, LocaleList locales) {
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000964 init(textClassifier, text, selectionStart, selectionEnd, locales);
965 mContext = Preconditions.checkNotNull(context);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800966 }
967
968 @UiThread
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000969 public void init(Supplier<TextClassifier> textClassifier, CharSequence text,
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000970 int selectionStart, int selectionEnd, LocaleList locales) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800971 mTextClassifier = Preconditions.checkNotNull(textClassifier);
972 mText = Preconditions.checkNotNull(text).toString();
Abodunrinwa Toki08925e62017-05-12 13:48:50 +0100973 mLastClassificationText = null; // invalidate.
Abodunrinwa Toki792d8202017-03-06 23:51:11 +0000974 Preconditions.checkArgument(selectionEnd > selectionStart);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800975 mSelectionStart = selectionStart;
976 mSelectionEnd = selectionEnd;
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100977 mDefaultLocales = locales;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800978 }
979
980 @WorkerThread
981 public SelectionResult classifyText() {
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +0100982 mHot = true;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100983 return performClassification(null /* selection */);
984 }
985
986 @WorkerThread
987 public SelectionResult suggestSelection() {
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +0100988 mHot = true;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100989 trimText();
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100990 final TextSelection selection;
Jeff Sharkeyaa1a9112018-04-10 15:18:12 -0600991 if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.P) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100992 final TextSelection.Request request = new TextSelection.Request.Builder(
993 mTrimmedText, mRelativeStart, mRelativeEnd)
994 .setDefaultLocales(mDefaultLocales)
995 .setDarkLaunchAllowed(true)
996 .build();
997 selection = mTextClassifier.get().suggestSelection(request);
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100998 } else {
999 // Use old APIs.
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +00001000 selection = mTextClassifier.get().suggestSelection(
Abodunrinwa Toki080c8542018-03-27 00:04:06 +01001001 mTrimmedText, mRelativeStart, mRelativeEnd, mDefaultLocales);
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +01001002 }
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +01001003 // Do not classify new selection boundaries if TextClassifier should be dark launched.
Abodunrinwa Tokic2449b82018-05-01 21:36:48 +01001004 if (!isDarkLaunchEnabled()) {
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +01001005 mSelectionStart = Math.max(0, selection.getSelectionStartIndex() + mTrimStart);
1006 mSelectionEnd = Math.min(
1007 mText.length(), selection.getSelectionEndIndex() + mTrimStart);
1008 }
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001009 return performClassification(selection);
1010 }
1011
Abodunrinwa Toki52096912018-03-21 23:14:42 +00001012 public SelectionResult getOriginalSelection() {
1013 return new SelectionResult(mSelectionStart, mSelectionEnd, null, null);
1014 }
1015
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +01001016 /**
1017 * Maximum time (in milliseconds) to wait for a textclassifier result before timing out.
1018 */
1019 // TODO: Consider making this a ViewConfiguration.
1020 public int getTimeoutDuration() {
1021 if (mHot) {
1022 return 200;
1023 } else {
1024 // Return a slightly larger number than usual when the TextClassifier is first
1025 // initialized. Initialization would usually take longer than subsequent calls to
1026 // the TextClassifier. The impact of this on the UI is that we do not show the
1027 // selection handles or toolbar until after this timeout.
1028 return 500;
1029 }
1030 }
1031
Abodunrinwa Tokic2449b82018-05-01 21:36:48 +01001032 private boolean isDarkLaunchEnabled() {
1033 return TextClassificationManager.getSettings(mContext).isModelDarkLaunchEnabled();
1034 }
1035
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001036 private SelectionResult performClassification(@Nullable TextSelection selection) {
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +01001037 if (!Objects.equals(mText, mLastClassificationText)
1038 || mSelectionStart != mLastClassificationSelectionStart
1039 || mSelectionEnd != mLastClassificationSelectionEnd
Abodunrinwa Toki080c8542018-03-27 00:04:06 +01001040 || !Objects.equals(mDefaultLocales, mLastClassificationLocales)) {
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +01001041
1042 mLastClassificationText = mText;
1043 mLastClassificationSelectionStart = mSelectionStart;
1044 mLastClassificationSelectionEnd = mSelectionEnd;
Abodunrinwa Toki080c8542018-03-27 00:04:06 +01001045 mLastClassificationLocales = mDefaultLocales;
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +01001046
1047 trimText();
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +01001048 final TextClassification classification;
Abodunrinwa Tokia69950c2018-11-29 13:51:56 +00001049 if (Linkify.containsUnsupportedCharacters(mText)) {
1050 // Do not show smart actions for text containing unsupported characters.
1051 android.util.EventLog.writeEvent(0x534e4554, "116321860", -1, "");
1052 classification = TextClassification.EMPTY;
1053 } else if (mContext.getApplicationInfo().targetSdkVersion
1054 >= Build.VERSION_CODES.P) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +01001055 final TextClassification.Request request =
1056 new TextClassification.Request.Builder(
1057 mTrimmedText, mRelativeStart, mRelativeEnd)
1058 .setDefaultLocales(mDefaultLocales)
1059 .build();
1060 classification = mTextClassifier.get().classifyText(request);
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +01001061 } else {
1062 // Use old APIs.
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +00001063 classification = mTextClassifier.get().classifyText(
Abodunrinwa Toki080c8542018-03-27 00:04:06 +01001064 mTrimmedText, mRelativeStart, mRelativeEnd, mDefaultLocales);
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +01001065 }
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +01001066 mLastClassificationResult = new SelectionResult(
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +01001067 mSelectionStart, mSelectionEnd, classification, selection);
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +01001068
1069 }
1070 return mLastClassificationResult;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001071 }
1072
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001073 private void trimText() {
1074 mTrimStart = Math.max(0, mSelectionStart - TRIM_DELTA);
1075 final int referenceEnd = Math.min(mText.length(), mSelectionEnd + TRIM_DELTA);
1076 mTrimmedText = mText.subSequence(mTrimStart, referenceEnd);
1077 mRelativeStart = mSelectionStart - mTrimStart;
1078 mRelativeEnd = mSelectionEnd - mTrimStart;
1079 }
1080 }
1081
1082 /**
1083 * Selection result.
1084 */
1085 private static final class SelectionResult {
1086 private final int mStart;
1087 private final int mEnd;
Abodunrinwa Toki52096912018-03-21 23:14:42 +00001088 @Nullable private final TextClassification mClassification;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001089 @Nullable private final TextSelection mSelection;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001090
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001091 SelectionResult(int start, int end,
Abodunrinwa Toki52096912018-03-21 23:14:42 +00001092 @Nullable TextClassification classification, @Nullable TextSelection selection) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001093 mStart = start;
1094 mEnd = end;
Abodunrinwa Toki52096912018-03-21 23:14:42 +00001095 mClassification = classification;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001096 mSelection = selection;
1097 }
1098 }
1099
1100 @SelectionEvent.ActionType
1101 private static int getActionType(int menuItemId) {
1102 switch (menuItemId) {
1103 case TextView.ID_SELECT_ALL:
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +00001104 return SelectionEvent.ACTION_SELECT_ALL;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001105 case TextView.ID_CUT:
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +00001106 return SelectionEvent.ACTION_CUT;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001107 case TextView.ID_COPY:
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +00001108 return SelectionEvent.ACTION_COPY;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001109 case TextView.ID_PASTE: // fall through
1110 case TextView.ID_PASTE_AS_PLAIN_TEXT:
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +00001111 return SelectionEvent.ACTION_PASTE;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001112 case TextView.ID_SHARE:
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +00001113 return SelectionEvent.ACTION_SHARE;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001114 case TextView.ID_ASSIST:
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +00001115 return SelectionEvent.ACTION_SMART_SHARE;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001116 default:
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +00001117 return SelectionEvent.ACTION_OTHER;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001118 }
1119 }
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +01001120
1121 private static CharSequence getText(TextView textView) {
1122 // Extracts the textView's text.
1123 // TODO: Investigate why/when TextView.getText() is null.
1124 final CharSequence text = textView.getText();
1125 if (text != null) {
1126 return text;
1127 }
1128 return "";
1129 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001130}