blob: 6cb0eaa7f47d777ed7068c7651b5f3c141253753 [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 Toki57ec6ea2017-09-04 20:32:31 +010034import android.util.Log;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080035import android.view.ActionMode;
Abodunrinwa Tokif1d93992018-03-02 13:53:21 +000036import android.view.textclassifier.SelectionEvent;
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +000037import android.view.textclassifier.SelectionEvent.InvocationMethod;
Jan Althaus5a030942018-04-04 19:40:38 +020038import android.view.textclassifier.SelectionSessionLogger;
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +010039import android.view.textclassifier.TextClassification;
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +000040import android.view.textclassifier.TextClassificationConstants;
41import android.view.textclassifier.TextClassificationManager;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080042import android.view.textclassifier.TextClassifier;
43import android.view.textclassifier.TextSelection;
44import android.widget.Editor.SelectionModifierCursorController;
45
Petar Šegina91df3f92017-08-15 16:20:43 +010046import com.android.internal.annotations.VisibleForTesting;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080047import com.android.internal.util.Preconditions;
48
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +010049import java.text.BreakIterator;
Petar Šegina701ba332017-08-01 17:57:26 +010050import java.util.ArrayList;
Petar Šegina7c8196f2017-09-11 18:03:14 +010051import java.util.Comparator;
Petar Šegina701ba332017-08-01 17:57:26 +010052import java.util.List;
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +010053import java.util.Objects;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080054import java.util.function.Consumer;
Petar Šegina7c8196f2017-09-11 18:03:14 +010055import java.util.function.Function;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080056import java.util.function.Supplier;
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +010057import java.util.regex.Pattern;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080058
59/**
60 * Helper class for starting selection action mode
61 * (synchronously without the TextClassifier, asynchronously with the TextClassifier).
Petar Šegina91df3f92017-08-15 16:20:43 +010062 * @hide
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080063 */
64@UiThread
Petar Šegina91df3f92017-08-15 16:20:43 +010065@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
Petar Šeginaba1b8562017-08-31 18:09:16 +010066public final class SelectionActionModeHelper {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080067
Jan Althausb3513a12017-09-22 18:26:06 +020068 private static final String LOG_TAG = "SelectActionModeHelper";
69
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080070 private final Editor mEditor;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +010071 private final TextView mTextView;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080072 private final TextClassificationHelper mTextClassificationHelper;
73
Abodunrinwa Toki52096912018-03-21 23:14:42 +000074 @Nullable private TextClassification mTextClassification;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080075 private AsyncTask mTextClassificationAsyncTask;
76
Abodunrinwa Toki1d775572017-05-08 16:03:01 +010077 private final SelectionTracker mSelectionTracker;
Petar Šegina5ab7bb22017-09-05 20:48:42 +010078
79 // TODO remove nullable marker once the switch gating the feature gets removed
80 @Nullable
Petar Šegina701ba332017-08-01 17:57:26 +010081 private final SmartSelectSprite mSmartSelectSprite;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +000082
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080083 SelectionActionModeHelper(@NonNull Editor editor) {
84 mEditor = Preconditions.checkNotNull(editor);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +010085 mTextView = mEditor.getTextView();
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080086 mTextClassificationHelper = new TextClassificationHelper(
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +010087 mTextView.getContext(),
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +000088 mTextView::getTextClassifier,
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +010089 getText(mTextView),
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +010090 0, 1, mTextView.getTextLocales());
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +010091 mSelectionTracker = new SelectionTracker(mTextView);
Petar Šegina701ba332017-08-01 17:57:26 +010092
Abodunrinwa Tokic2449b82018-05-01 21:36:48 +010093 if (getTextClassificationSettings().isSmartSelectionAnimationEnabled()) {
Petar Šegina5ab7bb22017-09-05 20:48:42 +010094 mSmartSelectSprite = new SmartSelectSprite(mTextView.getContext(),
Jan Althaus80620c52018-02-02 17:39:22 +010095 editor.getTextView().mHighlightColor, mTextView::invalidate);
Petar Šegina701ba332017-08-01 17:57:26 +010096 } else {
97 mSmartSelectSprite = null;
98 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -080099 }
100
Richard Ledley26b87222017-11-30 10:54:08 +0000101 /**
102 * Starts Selection ActionMode.
103 */
104 public void startSelectionActionModeAsync(boolean adjustSelection) {
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100105 // Check if the smart selection should run for editable text.
Abodunrinwa Tokic2449b82018-05-01 21:36:48 +0100106 adjustSelection &= getTextClassificationSettings().isSmartSelectionEnabled();
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100107
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100108 mSelectionTracker.onOriginalSelection(
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100109 getText(mTextView),
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100110 mTextView.getSelectionStart(),
Jan Althaus92c6dec2018-02-02 09:20:14 +0100111 mTextView.getSelectionEnd(),
112 false /*isLink*/);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800113 cancelAsyncTask();
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100114 if (skipTextClassification()) {
Richard Ledley26b87222017-11-30 10:54:08 +0000115 startSelectionActionMode(null);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800116 } else {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100117 resetTextClassificationHelper();
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800118 mTextClassificationAsyncTask = new TextClassificationAsyncTask(
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100119 mTextView,
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +0100120 mTextClassificationHelper.getTimeoutDuration(),
Abodunrinwa Toki66c16272017-05-03 20:22:55 +0100121 adjustSelection
122 ? mTextClassificationHelper::suggestSelection
123 : mTextClassificationHelper::classifyText,
Petar Šegina701ba332017-08-01 17:57:26 +0100124 mSmartSelectSprite != null
Richard Ledley26b87222017-11-30 10:54:08 +0000125 ? this::startSelectionActionModeWithSmartSelectAnimation
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000126 : this::startSelectionActionMode,
127 mTextClassificationHelper::getOriginalSelection)
Richard Ledley26b87222017-11-30 10:54:08 +0000128 .execute();
129 }
130 }
131
132 /**
133 * Starts Link ActionMode.
134 */
Richard Ledley27db81b2018-03-01 12:34:55 +0000135 public void startLinkActionModeAsync(int start, int end) {
136 mSelectionTracker.onOriginalSelection(getText(mTextView), start, end, true /*isLink*/);
Richard Ledley26b87222017-11-30 10:54:08 +0000137 cancelAsyncTask();
138 if (skipTextClassification()) {
139 startLinkActionMode(null);
140 } else {
Richard Ledley27db81b2018-03-01 12:34:55 +0000141 resetTextClassificationHelper(start, end);
Richard Ledley26b87222017-11-30 10:54:08 +0000142 mTextClassificationAsyncTask = new TextClassificationAsyncTask(
143 mTextView,
144 mTextClassificationHelper.getTimeoutDuration(),
145 mTextClassificationHelper::classifyText,
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000146 this::startLinkActionMode,
147 mTextClassificationHelper::getOriginalSelection)
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800148 .execute();
149 }
150 }
151
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800152 public void invalidateActionModeAsync() {
153 cancelAsyncTask();
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100154 if (skipTextClassification()) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800155 invalidateActionMode(null);
156 } else {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100157 resetTextClassificationHelper();
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800158 mTextClassificationAsyncTask = new TextClassificationAsyncTask(
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100159 mTextView,
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +0100160 mTextClassificationHelper.getTimeoutDuration(),
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100161 mTextClassificationHelper::classifyText,
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000162 this::invalidateActionMode,
163 mTextClassificationHelper::getOriginalSelection)
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800164 .execute();
165 }
166 }
167
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100168 public void onSelectionAction(int menuItemId) {
169 mSelectionTracker.onSelectionAction(
170 mTextView.getSelectionStart(), mTextView.getSelectionEnd(),
171 getActionType(menuItemId), mTextClassification);
172 }
173
174 public void onSelectionDrag() {
175 mSelectionTracker.onSelectionAction(
176 mTextView.getSelectionStart(), mTextView.getSelectionEnd(),
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000177 SelectionEvent.ACTION_DRAG, mTextClassification);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100178 }
179
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100180 public void onTextChanged(int start, int end) {
181 mSelectionTracker.onTextChanged(start, end, mTextClassification);
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100182 }
183
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100184 public boolean resetSelection(int textIndex) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100185 if (mSelectionTracker.resetSelection(textIndex, mEditor)) {
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000186 invalidateActionModeAsync();
187 return true;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800188 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000189 return false;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800190 }
191
192 @Nullable
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100193 public TextClassification getTextClassification() {
194 return mTextClassification;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800195 }
196
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000197 public void onDestroyActionMode() {
Petar Šegina701ba332017-08-01 17:57:26 +0100198 cancelSmartSelectAnimation();
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100199 mSelectionTracker.onSelectionDestroyed();
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000200 cancelAsyncTask();
201 }
202
Petar Šegina5ab7bb22017-09-05 20:48:42 +0100203 public void onDraw(final Canvas canvas) {
Jan Althaus80620c52018-02-02 17:39:22 +0100204 if (isDrawingHighlight() && mSmartSelectSprite != null) {
Petar Šegina5ab7bb22017-09-05 20:48:42 +0100205 mSmartSelectSprite.draw(canvas);
206 }
207 }
208
Jan Althaus80620c52018-02-02 17:39:22 +0100209 public boolean isDrawingHighlight() {
210 return mSmartSelectSprite != null && mSmartSelectSprite.isAnimationActive();
211 }
212
Abodunrinwa Tokic2449b82018-05-01 21:36:48 +0100213 private TextClassificationConstants getTextClassificationSettings() {
214 return TextClassificationManager.getSettings(mTextView.getContext());
215 }
216
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000217 private void cancelAsyncTask() {
218 if (mTextClassificationAsyncTask != null) {
219 mTextClassificationAsyncTask.cancel(true);
220 mTextClassificationAsyncTask = null;
221 }
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100222 mTextClassification = null;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000223 }
224
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100225 private boolean skipTextClassification() {
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100226 // No need to make an async call for a no-op TextClassifier.
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000227 final boolean noOpTextClassifier = mTextView.usesNoOpTextClassifier();
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100228 // Do not call the TextClassifier if there is no selection.
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100229 final boolean noSelection = mTextView.getSelectionEnd() == mTextView.getSelectionStart();
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100230 // Do not call the TextClassifier if this is a password field.
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100231 final boolean password = mTextView.hasPasswordTransformationMethod()
232 || TextView.isPasswordInputType(mTextView.getInputType());
Abodunrinwa Toki76b51dc2017-07-13 23:37:11 +0100233 return noOpTextClassifier || noSelection || password;
Abodunrinwa Toki792d8202017-03-06 23:51:11 +0000234 }
235
Richard Ledley26b87222017-11-30 10:54:08 +0000236 private void startLinkActionMode(@Nullable SelectionResult result) {
237 startActionMode(Editor.TextActionMode.TEXT_LINK, result);
238 }
239
240 private void startSelectionActionMode(@Nullable SelectionResult result) {
241 startActionMode(Editor.TextActionMode.SELECTION, result);
242 }
243
244 private void startActionMode(
245 @Editor.TextActionMode int actionMode, @Nullable SelectionResult result) {
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100246 final CharSequence text = getText(mTextView);
Richard Ledley26b87222017-11-30 10:54:08 +0000247 if (result != null && text instanceof Spannable
Richard Ledley27db81b2018-03-01 12:34:55 +0000248 && (mTextView.isTextSelectable() || mTextView.isTextEditable())) {
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100249 // Do not change the selection if TextClassifier should be dark launched.
Abodunrinwa Tokic2449b82018-05-01 21:36:48 +0100250 if (!getTextClassificationSettings().isModelDarkLaunchEnabled()) {
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100251 Selection.setSelection((Spannable) text, result.mStart, result.mEnd);
Richard Ledley724eff92017-12-21 10:11:34 +0000252 mTextView.invalidate();
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100253 }
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100254 mTextClassification = result.mClassification;
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000255 } else if (result != null && actionMode == Editor.TextActionMode.TEXT_LINK) {
Richard Ledley27db81b2018-03-01 12:34:55 +0000256 mTextClassification = result.mClassification;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800257 } else {
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100258 mTextClassification = null;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800259 }
Richard Ledley26b87222017-11-30 10:54:08 +0000260 if (mEditor.startActionModeInternal(actionMode)) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800261 final SelectionModifierCursorController controller = mEditor.getSelectionController();
Richard Ledley26b87222017-11-30 10:54:08 +0000262 if (controller != null
263 && (mTextView.isTextSelectable() || mTextView.isTextEditable())) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800264 controller.show();
265 }
Richard Ledley724eff92017-12-21 10:11:34 +0000266 if (result != null) {
267 switch (actionMode) {
268 case Editor.TextActionMode.SELECTION:
269 mSelectionTracker.onSmartSelection(result);
270 break;
271 case Editor.TextActionMode.TEXT_LINK:
272 mSelectionTracker.onLinkSelected(result);
273 break;
274 default:
275 break;
276 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000277 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800278 }
279 mEditor.setRestartActionModeOnNextRefresh(false);
280 mTextClassificationAsyncTask = null;
281 }
282
Richard Ledley26b87222017-11-30 10:54:08 +0000283 private void startSelectionActionModeWithSmartSelectAnimation(
284 @Nullable SelectionResult result) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100285 final Layout layout = mTextView.getLayout();
Petar Šegina701ba332017-08-01 17:57:26 +0100286
Mihai Popa6748ff32018-05-08 19:18:43 +0100287 final Runnable onAnimationEndCallback = () -> {
Mihai Popa6e8e27b2018-05-25 11:57:17 +0100288 final SelectionResult startSelectionResult;
289 if (result != null && result.mStart >= 0 && result.mEnd <= getText(mTextView).length()
Mihai Popa6748ff32018-05-08 19:18:43 +0100290 && result.mStart <= result.mEnd) {
Mihai Popa6e8e27b2018-05-25 11:57:17 +0100291 startSelectionResult = result;
292 } else {
293 startSelectionResult = null;
Mihai Popa6748ff32018-05-08 19:18:43 +0100294 }
Mihai Popa6e8e27b2018-05-25 11:57:17 +0100295 startSelectionActionMode(startSelectionResult);
Mihai Popa6748ff32018-05-08 19:18:43 +0100296 };
Petar Šegina701ba332017-08-01 17:57:26 +0100297 // TODO do not trigger the animation if the change included only non-printable characters
298 final boolean didSelectionChange =
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100299 result != null && (mTextView.getSelectionStart() != result.mStart
300 || mTextView.getSelectionEnd() != result.mEnd);
Petar Šegina701ba332017-08-01 17:57:26 +0100301
302 if (!didSelectionChange) {
303 onAnimationEndCallback.run();
304 return;
305 }
306
Petar Šegina7c8196f2017-09-11 18:03:14 +0100307 final List<SmartSelectSprite.RectangleWithTextSelectionLayout> selectionRectangles =
Petar Šegina701ba332017-08-01 17:57:26 +0100308 convertSelectionToRectangles(layout, result.mStart, result.mEnd);
309
Petar Šegina91df3f92017-08-15 16:20:43 +0100310 final PointF touchPoint = new PointF(
311 mEditor.getLastUpPositionX(),
312 mEditor.getLastUpPositionY());
Petar Šegina701ba332017-08-01 17:57:26 +0100313
Petar Šegina91df3f92017-08-15 16:20:43 +0100314 final PointF animationStartPoint =
Petar Šegina7c8196f2017-09-11 18:03:14 +0100315 movePointInsideNearestRectangle(touchPoint, selectionRectangles,
316 SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle);
Petar Šegina701ba332017-08-01 17:57:26 +0100317
318 mSmartSelectSprite.startAnimation(
Petar Šegina91df3f92017-08-15 16:20:43 +0100319 animationStartPoint,
Petar Šegina701ba332017-08-01 17:57:26 +0100320 selectionRectangles,
321 onAnimationEndCallback);
322 }
323
Petar Šegina7c8196f2017-09-11 18:03:14 +0100324 private List<SmartSelectSprite.RectangleWithTextSelectionLayout> convertSelectionToRectangles(
325 final Layout layout, final int start, final int end) {
326 final List<SmartSelectSprite.RectangleWithTextSelectionLayout> result = new ArrayList<>();
Petar Šegina72729252017-08-31 15:25:06 +0100327
Petar Šegina7c8196f2017-09-11 18:03:14 +0100328 final Layout.SelectionRectangleConsumer consumer =
329 (left, top, right, bottom, textSelectionLayout) -> mergeRectangleIntoList(
330 result,
331 new RectF(left, top, right, bottom),
332 SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle,
333 r -> new SmartSelectSprite.RectangleWithTextSelectionLayout(r,
334 textSelectionLayout)
335 );
336
337 layout.getSelection(start, end, consumer);
338
339 result.sort(Comparator.comparing(
340 SmartSelectSprite.RectangleWithTextSelectionLayout::getRectangle,
341 SmartSelectSprite.RECTANGLE_COMPARATOR));
342
Petar Šegina701ba332017-08-01 17:57:26 +0100343 return result;
344 }
345
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +0100346 // TODO: Move public pure functions out of this class and make it package-private.
Petar Šeginaba1b8562017-08-31 18:09:16 +0100347 /**
Petar Šegina7c8196f2017-09-11 18:03:14 +0100348 * Merges a {@link RectF} into an existing list of any objects which contain a rectangle.
349 * While merging, this method makes sure that:
Petar Šeginaba1b8562017-08-31 18:09:16 +0100350 *
351 * <ol>
352 * <li>No rectangle is redundant (contained within a bigger rectangle)</li>
353 * <li>Rectangles of the same height and vertical position that intersect get merged</li>
354 * </ol>
355 *
Petar Šegina7c8196f2017-09-11 18:03:14 +0100356 * @param list the list of rectangles (or other rectangle containers) to merge the new
357 * rectangle into
Petar Šeginaba1b8562017-08-31 18:09:16 +0100358 * @param candidate the {@link RectF} to merge into the list
Petar Šegina7c8196f2017-09-11 18:03:14 +0100359 * @param extractor a function that can extract a {@link RectF} from an element of the given
360 * list
361 * @param packer a function that can wrap the resulting {@link RectF} into an element that
362 * the list contains
Petar Šeginaba1b8562017-08-31 18:09:16 +0100363 * @hide
364 */
365 @VisibleForTesting
Petar Šegina7c8196f2017-09-11 18:03:14 +0100366 public static <T> void mergeRectangleIntoList(final List<T> list,
367 final RectF candidate, final Function<T, RectF> extractor,
368 final Function<RectF, T> packer) {
Petar Šeginaba1b8562017-08-31 18:09:16 +0100369 if (candidate.isEmpty()) {
370 return;
371 }
372
373 final int elementCount = list.size();
374 for (int index = 0; index < elementCount; ++index) {
Petar Šegina7c8196f2017-09-11 18:03:14 +0100375 final RectF existingRectangle = extractor.apply(list.get(index));
Petar Šeginaba1b8562017-08-31 18:09:16 +0100376 if (existingRectangle.contains(candidate)) {
377 return;
378 }
379 if (candidate.contains(existingRectangle)) {
380 existingRectangle.setEmpty();
381 continue;
382 }
383
384 final boolean rectanglesContinueEachOther = candidate.left == existingRectangle.right
385 || candidate.right == existingRectangle.left;
386 final boolean canMerge = candidate.top == existingRectangle.top
387 && candidate.bottom == existingRectangle.bottom
388 && (RectF.intersects(candidate, existingRectangle)
389 || rectanglesContinueEachOther);
390
391 if (canMerge) {
392 candidate.union(existingRectangle);
393 existingRectangle.setEmpty();
394 }
395 }
396
397 for (int index = elementCount - 1; index >= 0; --index) {
Petar Šegina7c8196f2017-09-11 18:03:14 +0100398 final RectF rectangle = extractor.apply(list.get(index));
399 if (rectangle.isEmpty()) {
Petar Šeginaba1b8562017-08-31 18:09:16 +0100400 list.remove(index);
401 }
402 }
403
Petar Šegina7c8196f2017-09-11 18:03:14 +0100404 list.add(packer.apply(candidate));
Petar Šeginaba1b8562017-08-31 18:09:16 +0100405 }
406
407
Petar Šegina91df3f92017-08-15 16:20:43 +0100408 /** @hide */
409 @VisibleForTesting
Petar Šegina7c8196f2017-09-11 18:03:14 +0100410 public static <T> PointF movePointInsideNearestRectangle(final PointF point,
411 final List<T> list, final Function<T, RectF> extractor) {
Petar Šegina91df3f92017-08-15 16:20:43 +0100412 float bestX = -1;
413 float bestY = -1;
414 double bestDistance = Double.MAX_VALUE;
415
Petar Šegina7c8196f2017-09-11 18:03:14 +0100416 final int elementCount = list.size();
Petar Šeginaba1b8562017-08-31 18:09:16 +0100417 for (int index = 0; index < elementCount; ++index) {
Petar Šegina7c8196f2017-09-11 18:03:14 +0100418 final RectF rectangle = extractor.apply(list.get(index));
Petar Šegina91df3f92017-08-15 16:20:43 +0100419 final float candidateY = rectangle.centerY();
420 final float candidateX;
421
422 if (point.x > rectangle.right) {
423 candidateX = rectangle.right;
424 } else if (point.x < rectangle.left) {
425 candidateX = rectangle.left;
426 } else {
427 candidateX = point.x;
428 }
429
430 final double candidateDistance = Math.pow(point.x - candidateX, 2)
431 + Math.pow(point.y - candidateY, 2);
432
433 if (candidateDistance < bestDistance) {
434 bestX = candidateX;
435 bestY = candidateY;
436 bestDistance = candidateDistance;
437 }
438 }
439
440 return new PointF(bestX, bestY);
441 }
442
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800443 private void invalidateActionMode(@Nullable SelectionResult result) {
Petar Šegina701ba332017-08-01 17:57:26 +0100444 cancelSmartSelectAnimation();
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100445 mTextClassification = result != null ? result.mClassification : null;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800446 final ActionMode actionMode = mEditor.getTextActionMode();
447 if (actionMode != null) {
448 actionMode.invalidate();
449 }
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100450 mSelectionTracker.onSelectionUpdated(
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100451 mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mTextClassification);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800452 mTextClassificationAsyncTask = null;
453 }
454
Richard Ledley26b87222017-11-30 10:54:08 +0000455 private void resetTextClassificationHelper(int selectionStart, int selectionEnd) {
456 if (selectionStart < 0 || selectionEnd < 0) {
457 // Use selection indices
458 selectionStart = mTextView.getSelectionStart();
459 selectionEnd = mTextView.getSelectionEnd();
460 }
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +0100461 mTextClassificationHelper.init(
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000462 mTextView::getTextClassifier,
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100463 getText(mTextView),
Richard Ledley26b87222017-11-30 10:54:08 +0000464 selectionStart, selectionEnd,
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100465 mTextView.getTextLocales());
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800466 }
467
Richard Ledley26b87222017-11-30 10:54:08 +0000468 private void resetTextClassificationHelper() {
469 resetTextClassificationHelper(-1, -1);
470 }
471
Petar Šegina701ba332017-08-01 17:57:26 +0100472 private void cancelSmartSelectAnimation() {
473 if (mSmartSelectSprite != null) {
474 mSmartSelectSprite.cancelAnimation();
475 }
476 }
477
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800478 /**
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100479 * Tracks and logs smart selection changes.
480 * It is important to trigger this object's methods at the appropriate event so that it tracks
481 * smart selection events appropriately.
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000482 */
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100483 private static final class SelectionTracker {
484
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100485 private final TextView mTextView;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100486 private SelectionMetricsLogger mLogger;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000487
488 private int mOriginalStart;
489 private int mOriginalEnd;
490 private int mSelectionStart;
491 private int mSelectionEnd;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100492 private boolean mAllowReset;
Jan Althausb3513a12017-09-22 18:26:06 +0200493 private final LogAbandonRunnable mDelayedLogAbandon = new LogAbandonRunnable();
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000494
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100495 SelectionTracker(TextView textView) {
496 mTextView = Preconditions.checkNotNull(textView);
497 mLogger = new SelectionMetricsLogger(textView);
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100498 }
499
500 /**
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100501 * Called when the original selection happens, before smart selection is triggered.
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100502 */
Jan Althaus92c6dec2018-02-02 09:20:14 +0100503 public void onOriginalSelection(
504 CharSequence text, int selectionStart, int selectionEnd, boolean isLink) {
Jan Althausb3513a12017-09-22 18:26:06 +0200505 // If we abandoned a selection and created a new one very shortly after, we may still
506 // have a pending request to log ABANDON, which we flush here.
507 mDelayedLogAbandon.flush();
508
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100509 mOriginalStart = mSelectionStart = selectionStart;
510 mOriginalEnd = mSelectionEnd = selectionEnd;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100511 mAllowReset = false;
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100512 maybeInvalidateLogger();
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100513 mLogger.logSelectionStarted(mTextView.getTextClassificationSession(),
514 text, selectionStart,
Jan Althaus92c6dec2018-02-02 09:20:14 +0100515 isLink ? SelectionEvent.INVOCATION_LINK : SelectionEvent.INVOCATION_MANUAL);
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000516 }
517
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100518 /**
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100519 * Called when selection action mode is started and the results come from a classifier.
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100520 */
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100521 public void onSmartSelection(SelectionResult result) {
Richard Ledley724eff92017-12-21 10:11:34 +0000522 onClassifiedSelection(result);
523 mLogger.logSelectionModified(
524 result.mStart, result.mEnd, result.mClassification, result.mSelection);
525 }
526
527 /**
528 * Called when link action mode is started and the classification comes from a classifier.
529 */
530 public void onLinkSelected(SelectionResult result) {
531 onClassifiedSelection(result);
532 // TODO: log (b/70246800)
533 }
534
535 private void onClassifiedSelection(SelectionResult result) {
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100536 if (isSelectionStarted()) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100537 mSelectionStart = result.mStart;
538 mSelectionEnd = result.mEnd;
539 mAllowReset = mSelectionStart != mOriginalStart || mSelectionEnd != mOriginalEnd;
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100540 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000541 }
542
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100543 /**
544 * Called when selection bounds change.
545 */
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100546 public void onSelectionUpdated(
547 int selectionStart, int selectionEnd,
548 @Nullable TextClassification classification) {
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100549 if (isSelectionStarted()) {
550 mSelectionStart = selectionStart;
551 mSelectionEnd = selectionEnd;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100552 mAllowReset = false;
553 mLogger.logSelectionModified(selectionStart, selectionEnd, classification, null);
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100554 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000555 }
556
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100557 /**
558 * Called when the selection action mode is destroyed.
559 */
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000560 public void onSelectionDestroyed() {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100561 mAllowReset = false;
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100562 // Wait a few ms to see if the selection was destroyed because of a text change event.
Jan Althausb3513a12017-09-22 18:26:06 +0200563 mDelayedLogAbandon.schedule(100 /* ms */);
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000564 }
565
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100566 /**
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100567 * Called when an action is taken on a smart selection.
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100568 */
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100569 public void onSelectionAction(
570 int selectionStart, int selectionEnd,
571 @SelectionEvent.ActionType int action,
572 @Nullable TextClassification classification) {
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100573 if (isSelectionStarted()) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100574 mAllowReset = false;
575 mLogger.logSelectionAction(selectionStart, selectionEnd, action, classification);
Abodunrinwa Toki1d775572017-05-08 16:03:01 +0100576 }
577 }
578
579 /**
580 * Returns true if the current smart selection should be reset to normal selection based on
581 * information that has been recorded about the original selection and the smart selection.
582 * The expected UX here is to allow the user to select a word inside of the smart selection
583 * on a single tap.
584 */
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100585 public boolean resetSelection(int textIndex, Editor editor) {
586 final TextView textView = editor.getTextView();
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100587 if (isSelectionStarted()
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100588 && mAllowReset
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100589 && textIndex >= mSelectionStart && textIndex <= mSelectionEnd
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100590 && getText(textView) instanceof Spannable) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100591 mAllowReset = false;
592 boolean selected = editor.selectCurrentWord();
593 if (selected) {
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100594 mSelectionStart = editor.getTextView().getSelectionStart();
595 mSelectionEnd = editor.getTextView().getSelectionEnd();
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100596 mLogger.logSelectionAction(
597 textView.getSelectionStart(), textView.getSelectionEnd(),
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000598 SelectionEvent.ACTION_RESET, null /* classification */);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100599 }
600 return selected;
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000601 }
602 return false;
603 }
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100604
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100605 public void onTextChanged(int start, int end, TextClassification classification) {
606 if (isSelectionStarted() && start == mSelectionStart && end == mSelectionEnd) {
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000607 onSelectionAction(start, end, SelectionEvent.ACTION_OVERTYPE, classification);
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100608 }
609 }
610
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100611 private void maybeInvalidateLogger() {
612 if (mLogger.isEditTextLogger() != mTextView.isTextEditable()) {
613 mLogger = new SelectionMetricsLogger(mTextView);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100614 }
615 }
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +0100616
617 private boolean isSelectionStarted() {
618 return mSelectionStart >= 0 && mSelectionEnd >= 0 && mSelectionStart != mSelectionEnd;
619 }
Jan Althausb3513a12017-09-22 18:26:06 +0200620
621 /** A helper for keeping track of pending abandon logging requests. */
622 private final class LogAbandonRunnable implements Runnable {
623 private boolean mIsPending;
624
625 /** Schedules an abandon to be logged with the given delay. Flush if necessary. */
626 void schedule(int delayMillis) {
627 if (mIsPending) {
628 Log.e(LOG_TAG, "Force flushing abandon due to new scheduling request");
629 flush();
630 }
631 mIsPending = true;
632 mTextView.postDelayed(this, delayMillis);
633 }
634
635 /** If there is a pending log request, execute it now. */
636 void flush() {
637 mTextView.removeCallbacks(this);
638 run();
639 }
640
641 @Override
642 public void run() {
643 if (mIsPending) {
644 mLogger.logSelectionAction(
645 mSelectionStart, mSelectionEnd,
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000646 SelectionEvent.ACTION_ABANDON, null /* classification */);
Jan Althausb3513a12017-09-22 18:26:06 +0200647 mSelectionStart = mSelectionEnd = -1;
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100648 mLogger.endTextClassificationSession();
Jan Althausb3513a12017-09-22 18:26:06 +0200649 mIsPending = false;
650 }
651 }
652 }
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100653 }
654
655 // TODO: Write tests
656 /**
657 * Metrics logging helper.
658 *
659 * This logger logs selection by word indices. The initial (start) single word selection is
660 * logged at [0, 1) -- end index is exclusive. Other word indices are logged relative to the
661 * initial single word selection.
662 * e.g. New York city, NY. Suppose the initial selection is "York" in
663 * "New York city, NY", then "York" is at [0, 1), "New" is at [-1, 0], and "city" is at [1, 2).
664 * "New York" is at [-1, 1).
665 * Part selection of a word e.g. "or" is counted as selecting the
666 * entire word i.e. equivalent to "York", and each special character is counted as a word, e.g.
667 * "," is at [2, 3). Whitespaces are ignored.
Abodunrinwa Tokiad52f4b2018-02-06 23:32:41 +0000668 *
669 * NOTE that the definition of a word is defined by the TextClassifier's Logger's token
670 * iterator.
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100671 */
672 private static final class SelectionMetricsLogger {
673
674 private static final String LOG_TAG = "SelectionMetricsLogger";
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100675 private static final Pattern PATTERN_WHITESPACE = Pattern.compile("\\s+");
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100676
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100677 private final boolean mEditTextLogger;
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000678 private final BreakIterator mTokenIterator;
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100679
680 @Nullable private TextClassifier mClassificationSession;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100681 private int mStartIndex;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100682 private String mText;
683
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100684 SelectionMetricsLogger(TextView textView) {
685 Preconditions.checkNotNull(textView);
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100686 mEditTextLogger = textView.isTextEditable();
Jan Althaus5a030942018-04-04 19:40:38 +0200687 mTokenIterator = SelectionSessionLogger.getTokenIterator(textView.getTextLocale());
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000688 }
689
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000690 @TextClassifier.WidgetType
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000691 private static String getWidetType(TextView textView) {
692 if (textView.isTextEditable()) {
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000693 return TextClassifier.WIDGET_TYPE_EDITTEXT;
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000694 }
695 if (textView.isTextSelectable()) {
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000696 return TextClassifier.WIDGET_TYPE_TEXTVIEW;
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000697 }
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000698 return TextClassifier.WIDGET_TYPE_UNSELECTABLE_TEXTVIEW;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100699 }
700
Jan Althaus92c6dec2018-02-02 09:20:14 +0100701 public void logSelectionStarted(
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100702 TextClassifier classificationSession,
Jan Althaus92c6dec2018-02-02 09:20:14 +0100703 CharSequence text, int index,
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000704 @InvocationMethod int invocationMethod) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100705 try {
706 Preconditions.checkNotNull(text);
707 Preconditions.checkArgumentInRange(index, 0, text.length(), "index");
708 if (mText == null || !mText.contentEquals(text)) {
709 mText = text.toString();
710 }
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000711 mTokenIterator.setText(mText);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100712 mStartIndex = index;
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100713 mClassificationSession = classificationSession;
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100714 if (hasActiveClassificationSession()) {
715 mClassificationSession.onSelectionEvent(
716 SelectionEvent.createSelectionStartedEvent(invocationMethod, 0));
717 }
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100718 } catch (Exception e) {
719 // Avoid crashes due to logging.
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000720 Log.e(LOG_TAG, "" + e.getMessage(), e);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100721 }
722 }
723
724 public void logSelectionModified(int start, int end,
725 @Nullable TextClassification classification, @Nullable TextSelection selection) {
726 try {
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100727 if (hasActiveClassificationSession()) {
728 Preconditions.checkArgumentInRange(start, 0, mText.length(), "start");
729 Preconditions.checkArgumentInRange(end, start, mText.length(), "end");
730 int[] wordIndices = getWordDelta(start, end);
731 if (selection != null) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100732 mClassificationSession.onSelectionEvent(
733 SelectionEvent.createSelectionModifiedEvent(
734 wordIndices[0], wordIndices[1], selection));
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100735 } else if (classification != null) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100736 mClassificationSession.onSelectionEvent(
737 SelectionEvent.createSelectionModifiedEvent(
738 wordIndices[0], wordIndices[1], classification));
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100739 } else {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100740 mClassificationSession.onSelectionEvent(
741 SelectionEvent.createSelectionModifiedEvent(
742 wordIndices[0], wordIndices[1]));
743 }
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100744 }
745 } catch (Exception e) {
746 // Avoid crashes due to logging.
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000747 Log.e(LOG_TAG, "" + e.getMessage(), e);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100748 }
749 }
750
751 public void logSelectionAction(
752 int start, int end,
753 @SelectionEvent.ActionType int action,
754 @Nullable TextClassification classification) {
755 try {
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100756 if (hasActiveClassificationSession()) {
757 Preconditions.checkArgumentInRange(start, 0, mText.length(), "start");
758 Preconditions.checkArgumentInRange(end, start, mText.length(), "end");
759 int[] wordIndices = getWordDelta(start, end);
760 if (classification != null) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100761 mClassificationSession.onSelectionEvent(
762 SelectionEvent.createSelectionActionEvent(
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100763 wordIndices[0], wordIndices[1], action,
764 classification));
765 } else {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100766 mClassificationSession.onSelectionEvent(
767 SelectionEvent.createSelectionActionEvent(
768 wordIndices[0], wordIndices[1], action));
769 }
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100770 if (SelectionEvent.isTerminal(action)) {
771 endTextClassificationSession();
772 }
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100773 }
774 } catch (Exception e) {
775 // Avoid crashes due to logging.
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000776 Log.e(LOG_TAG, "" + e.getMessage(), e);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100777 }
778 }
779
780 public boolean isEditTextLogger() {
781 return mEditTextLogger;
782 }
783
Abodunrinwa Tokif299fc02018-05-17 17:36:25 +0100784 public void endTextClassificationSession() {
785 if (hasActiveClassificationSession()) {
786 mClassificationSession.destroy();
787 }
788 }
789
790 private boolean hasActiveClassificationSession() {
791 return mClassificationSession != null && !mClassificationSession.isDestroyed();
792 }
793
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100794 private int[] getWordDelta(int start, int end) {
795 int[] wordIndices = new int[2];
796
797 if (start == mStartIndex) {
798 wordIndices[0] = 0;
799 } else if (start < mStartIndex) {
800 wordIndices[0] = -countWordsForward(start);
801 } else { // start > mStartIndex
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100802 wordIndices[0] = countWordsBackward(start);
803
804 // For the selection start index, avoid counting a partial word backwards.
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000805 if (!mTokenIterator.isBoundary(start)
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100806 && !isWhitespace(
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000807 mTokenIterator.preceding(start),
808 mTokenIterator.following(start))) {
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100809 // We counted a partial word. Remove it.
810 wordIndices[0]--;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100811 }
812 }
813
814 if (end == mStartIndex) {
815 wordIndices[1] = 0;
816 } else if (end < mStartIndex) {
817 wordIndices[1] = -countWordsForward(end);
818 } else { // end > mStartIndex
819 wordIndices[1] = countWordsBackward(end);
820 }
821
822 return wordIndices;
823 }
824
825 private int countWordsBackward(int from) {
826 Preconditions.checkArgument(from >= mStartIndex);
827 int wordCount = 0;
828 int offset = from;
829 while (offset > mStartIndex) {
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000830 int start = mTokenIterator.preceding(offset);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100831 if (!isWhitespace(start, offset)) {
832 wordCount++;
833 }
834 offset = start;
835 }
836 return wordCount;
837 }
838
839 private int countWordsForward(int from) {
840 Preconditions.checkArgument(from <= mStartIndex);
841 int wordCount = 0;
842 int offset = from;
843 while (offset < mStartIndex) {
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +0000844 int end = mTokenIterator.following(offset);
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100845 if (!isWhitespace(offset, end)) {
846 wordCount++;
847 }
848 offset = end;
849 }
850 return wordCount;
851 }
852
853 private boolean isWhitespace(int start, int end) {
Abodunrinwa Tokid62a86e2017-09-11 18:19:53 +0100854 return PATTERN_WHITESPACE.matcher(mText.substring(start, end)).matches();
Abodunrinwa Tokie78ac522017-05-23 14:51:22 +0100855 }
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +0000856 }
857
858 /**
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800859 * AsyncTask for running a query on a background thread and returning the result on the
860 * UiThread. The AsyncTask times out after a specified time, returning a null result if the
861 * query has not yet returned.
862 */
863 private static final class TextClassificationAsyncTask
864 extends AsyncTask<Void, Void, SelectionResult> {
865
866 private final int mTimeOutDuration;
867 private final Supplier<SelectionResult> mSelectionResultSupplier;
868 private final Consumer<SelectionResult> mSelectionResultCallback;
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000869 private final Supplier<SelectionResult> mTimeOutResultSupplier;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800870 private final TextView mTextView;
871 private final String mOriginalText;
872
873 /**
874 * @param textView the TextView
875 * @param timeOut time in milliseconds to timeout the query if it has not completed
876 * @param selectionResultSupplier fetches the selection results. Runs on a background thread
877 * @param selectionResultCallback receives the selection results. Runs on the UiThread
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000878 * @param timeOutResultSupplier default result if the task times out
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800879 */
880 TextClassificationAsyncTask(
881 @NonNull TextView textView, int timeOut,
882 @NonNull Supplier<SelectionResult> selectionResultSupplier,
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000883 @NonNull Consumer<SelectionResult> selectionResultCallback,
884 @NonNull Supplier<SelectionResult> timeOutResultSupplier) {
Makoto Onuki1488a3a2017-05-24 12:25:46 -0700885 super(textView != null ? textView.getHandler() : null);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800886 mTextView = Preconditions.checkNotNull(textView);
887 mTimeOutDuration = timeOut;
888 mSelectionResultSupplier = Preconditions.checkNotNull(selectionResultSupplier);
889 mSelectionResultCallback = Preconditions.checkNotNull(selectionResultCallback);
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000890 mTimeOutResultSupplier = Preconditions.checkNotNull(timeOutResultSupplier);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800891 // Make a copy of the original text.
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100892 mOriginalText = getText(mTextView).toString();
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800893 }
894
895 @Override
896 @WorkerThread
897 protected SelectionResult doInBackground(Void... params) {
898 final Runnable onTimeOut = this::onTimeOut;
899 mTextView.postDelayed(onTimeOut, mTimeOutDuration);
900 final SelectionResult result = mSelectionResultSupplier.get();
901 mTextView.removeCallbacks(onTimeOut);
902 return result;
903 }
904
905 @Override
906 @UiThread
907 protected void onPostExecute(SelectionResult result) {
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +0100908 result = TextUtils.equals(mOriginalText, getText(mTextView)) ? result : null;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800909 mSelectionResultCallback.accept(result);
910 }
911
912 private void onTimeOut() {
913 if (getStatus() == Status.RUNNING) {
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000914 onPostExecute(mTimeOutResultSupplier.get());
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800915 }
916 cancel(true);
917 }
918 }
919
920 /**
921 * Helper class for querying the TextClassifier.
922 * It trims text so that only text necessary to provide context of the selected text is
923 * sent to the TextClassifier.
924 */
925 private static final class TextClassificationHelper {
926
Abodunrinwa Tokid2d13992017-03-24 21:43:13 +0000927 private static final int TRIM_DELTA = 120; // characters
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800928
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000929 private final Context mContext;
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000930 private Supplier<TextClassifier> mTextClassifier;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800931
932 /** The original TextView text. **/
933 private String mText;
934 /** Start index relative to mText. */
935 private int mSelectionStart;
936 /** End index relative to mText. */
937 private int mSelectionEnd;
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100938
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100939 @Nullable
940 private LocaleList mDefaultLocales;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800941
942 /** Trimmed text starting from mTrimStart in mText. */
943 private CharSequence mTrimmedText;
944 /** Index indicating the start of mTrimmedText in mText. */
945 private int mTrimStart;
946 /** Start index relative to mTrimmedText */
947 private int mRelativeStart;
948 /** End index relative to mTrimmedText */
949 private int mRelativeEnd;
950
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +0100951 /** Information about the last classified text to avoid re-running a query. */
952 private CharSequence mLastClassificationText;
953 private int mLastClassificationSelectionStart;
954 private int mLastClassificationSelectionEnd;
955 private LocaleList mLastClassificationLocales;
956 private SelectionResult mLastClassificationResult;
957
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +0100958 /** Whether the TextClassifier has been initialized. */
959 private boolean mHot;
960
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000961 TextClassificationHelper(Context context, Supplier<TextClassifier> textClassifier,
Abodunrinwa Toki4cfda0b2017-02-28 18:56:47 +0000962 CharSequence text, int selectionStart, int selectionEnd, LocaleList locales) {
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000963 init(textClassifier, text, selectionStart, selectionEnd, locales);
964 mContext = Preconditions.checkNotNull(context);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800965 }
966
967 @UiThread
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000968 public void init(Supplier<TextClassifier> textClassifier, CharSequence text,
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000969 int selectionStart, int selectionEnd, LocaleList locales) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800970 mTextClassifier = Preconditions.checkNotNull(textClassifier);
971 mText = Preconditions.checkNotNull(text).toString();
Abodunrinwa Toki08925e62017-05-12 13:48:50 +0100972 mLastClassificationText = null; // invalidate.
Abodunrinwa Toki792d8202017-03-06 23:51:11 +0000973 Preconditions.checkArgument(selectionEnd > selectionStart);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800974 mSelectionStart = selectionStart;
975 mSelectionEnd = selectionEnd;
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100976 mDefaultLocales = locales;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800977 }
978
979 @WorkerThread
980 public SelectionResult classifyText() {
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +0100981 mHot = true;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100982 return performClassification(null /* selection */);
983 }
984
985 @WorkerThread
986 public SelectionResult suggestSelection() {
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +0100987 mHot = true;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +0100988 trimText();
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100989 final TextSelection selection;
Jeff Sharkeyaa1a9112018-04-10 15:18:12 -0600990 if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.P) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +0100991 final TextSelection.Request request = new TextSelection.Request.Builder(
992 mTrimmedText, mRelativeStart, mRelativeEnd)
993 .setDefaultLocales(mDefaultLocales)
994 .setDarkLaunchAllowed(true)
995 .build();
996 selection = mTextClassifier.get().suggestSelection(request);
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +0100997 } else {
998 // Use old APIs.
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +0000999 selection = mTextClassifier.get().suggestSelection(
Abodunrinwa Toki080c8542018-03-27 00:04:06 +01001000 mTrimmedText, mRelativeStart, mRelativeEnd, mDefaultLocales);
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +01001001 }
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +01001002 // Do not classify new selection boundaries if TextClassifier should be dark launched.
Abodunrinwa Tokic2449b82018-05-01 21:36:48 +01001003 if (!isDarkLaunchEnabled()) {
Abodunrinwa Toki0e6b43e2017-09-19 23:18:40 +01001004 mSelectionStart = Math.max(0, selection.getSelectionStartIndex() + mTrimStart);
1005 mSelectionEnd = Math.min(
1006 mText.length(), selection.getSelectionEndIndex() + mTrimStart);
1007 }
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001008 return performClassification(selection);
1009 }
1010
Abodunrinwa Toki52096912018-03-21 23:14:42 +00001011 public SelectionResult getOriginalSelection() {
1012 return new SelectionResult(mSelectionStart, mSelectionEnd, null, null);
1013 }
1014
Abodunrinwa Toki3521f3f2017-09-27 02:38:55 +01001015 /**
1016 * Maximum time (in milliseconds) to wait for a textclassifier result before timing out.
1017 */
1018 // TODO: Consider making this a ViewConfiguration.
1019 public int getTimeoutDuration() {
1020 if (mHot) {
1021 return 200;
1022 } else {
1023 // Return a slightly larger number than usual when the TextClassifier is first
1024 // initialized. Initialization would usually take longer than subsequent calls to
1025 // the TextClassifier. The impact of this on the UI is that we do not show the
1026 // selection handles or toolbar until after this timeout.
1027 return 500;
1028 }
1029 }
1030
Abodunrinwa Tokic2449b82018-05-01 21:36:48 +01001031 private boolean isDarkLaunchEnabled() {
1032 return TextClassificationManager.getSettings(mContext).isModelDarkLaunchEnabled();
1033 }
1034
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001035 private SelectionResult performClassification(@Nullable TextSelection selection) {
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +01001036 if (!Objects.equals(mText, mLastClassificationText)
1037 || mSelectionStart != mLastClassificationSelectionStart
1038 || mSelectionEnd != mLastClassificationSelectionEnd
Abodunrinwa Toki080c8542018-03-27 00:04:06 +01001039 || !Objects.equals(mDefaultLocales, mLastClassificationLocales)) {
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +01001040
1041 mLastClassificationText = mText;
1042 mLastClassificationSelectionStart = mSelectionStart;
1043 mLastClassificationSelectionEnd = mSelectionEnd;
Abodunrinwa Toki080c8542018-03-27 00:04:06 +01001044 mLastClassificationLocales = mDefaultLocales;
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +01001045
1046 trimText();
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +01001047 final TextClassification classification;
Jeff Sharkeyaa1a9112018-04-10 15:18:12 -06001048 if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.P) {
Abodunrinwa Toki080c8542018-03-27 00:04:06 +01001049 final TextClassification.Request request =
1050 new TextClassification.Request.Builder(
1051 mTrimmedText, mRelativeStart, mRelativeEnd)
1052 .setDefaultLocales(mDefaultLocales)
1053 .build();
1054 classification = mTextClassifier.get().classifyText(request);
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +01001055 } else {
1056 // Use old APIs.
Abodunrinwa Toki88be5a62018-03-23 04:01:28 +00001057 classification = mTextClassifier.get().classifyText(
Abodunrinwa Toki080c8542018-03-27 00:04:06 +01001058 mTrimmedText, mRelativeStart, mRelativeEnd, mDefaultLocales);
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +01001059 }
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +01001060 mLastClassificationResult = new SelectionResult(
Abodunrinwa Toki2b6020f2017-10-28 02:28:45 +01001061 mSelectionStart, mSelectionEnd, classification, selection);
Abodunrinwa Tokifdf57ba2017-05-02 20:32:23 +01001062
1063 }
1064 return mLastClassificationResult;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001065 }
1066
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001067 private void trimText() {
1068 mTrimStart = Math.max(0, mSelectionStart - TRIM_DELTA);
1069 final int referenceEnd = Math.min(mText.length(), mSelectionEnd + TRIM_DELTA);
1070 mTrimmedText = mText.subSequence(mTrimStart, referenceEnd);
1071 mRelativeStart = mSelectionStart - mTrimStart;
1072 mRelativeEnd = mSelectionEnd - mTrimStart;
1073 }
1074 }
1075
1076 /**
1077 * Selection result.
1078 */
1079 private static final class SelectionResult {
1080 private final int mStart;
1081 private final int mEnd;
Abodunrinwa Toki52096912018-03-21 23:14:42 +00001082 @Nullable private final TextClassification mClassification;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001083 @Nullable private final TextSelection mSelection;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001084
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001085 SelectionResult(int start, int end,
Abodunrinwa Toki52096912018-03-21 23:14:42 +00001086 @Nullable TextClassification classification, @Nullable TextSelection selection) {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001087 mStart = start;
1088 mEnd = end;
Abodunrinwa Toki52096912018-03-21 23:14:42 +00001089 mClassification = classification;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001090 mSelection = selection;
1091 }
1092 }
1093
1094 @SelectionEvent.ActionType
1095 private static int getActionType(int menuItemId) {
1096 switch (menuItemId) {
1097 case TextView.ID_SELECT_ALL:
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +00001098 return SelectionEvent.ACTION_SELECT_ALL;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001099 case TextView.ID_CUT:
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +00001100 return SelectionEvent.ACTION_CUT;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001101 case TextView.ID_COPY:
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +00001102 return SelectionEvent.ACTION_COPY;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001103 case TextView.ID_PASTE: // fall through
1104 case TextView.ID_PASTE_AS_PLAIN_TEXT:
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +00001105 return SelectionEvent.ACTION_PASTE;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001106 case TextView.ID_SHARE:
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +00001107 return SelectionEvent.ACTION_SHARE;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001108 case TextView.ID_ASSIST:
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +00001109 return SelectionEvent.ACTION_SMART_SHARE;
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001110 default:
Abodunrinwa Toki3bb44362017-12-05 07:33:41 +00001111 return SelectionEvent.ACTION_OTHER;
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001112 }
1113 }
Abodunrinwa Toki7c1e4252017-09-26 20:09:06 +01001114
1115 private static CharSequence getText(TextView textView) {
1116 // Extracts the textView's text.
1117 // TODO: Investigate why/when TextView.getText() is null.
1118 final CharSequence text = textView.getText();
1119 if (text != null) {
1120 return text;
1121 }
1122 return "";
1123 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08001124}