blob: ded3be4e4ef5effead2bf05d61bdc37d84998545 [file] [log] [blame]
Gilles Debunned88876a2012-03-16 17:34:04 -07001/*
2 * Copyright (C) 2012 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.R;
Mihai Popa38722382018-03-07 19:56:21 +000020import android.animation.ValueAnimator;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +090021import android.annotation.IntDef;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +090022import android.annotation.NonNull;
Yoshiki Iguchiee147722015-04-14 00:12:44 +090023import android.annotation.Nullable;
Mathew Inwood978c6e22018-08-21 15:58:55 +010024import android.annotation.UnsupportedAppUsage;
Luca Zanolin1b15ba52013-02-20 14:31:37 +000025import android.app.PendingIntent;
26import android.app.PendingIntent.CanceledException;
Jan Althaus20d346e2018-03-23 14:03:52 +010027import android.app.RemoteAction;
Gilles Debunned88876a2012-03-16 17:34:04 -070028import android.content.ClipData;
29import android.content.ClipData.Item;
30import android.content.Context;
31import android.content.Intent;
Raph Levien26d443a2015-03-30 14:18:32 -070032import android.content.UndoManager;
33import android.content.UndoOperation;
34import android.content.UndoOwner;
Gilles Debunned88876a2012-03-16 17:34:04 -070035import android.content.pm.PackageManager;
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +000036import android.content.pm.ResolveInfo;
Gilles Debunned88876a2012-03-16 17:34:04 -070037import android.content.res.TypedArray;
38import android.graphics.Canvas;
39import android.graphics.Color;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +090040import android.graphics.Matrix;
Gilles Debunned88876a2012-03-16 17:34:04 -070041import android.graphics.Paint;
42import android.graphics.Path;
Mihai Popa63ee7f12018-04-05 12:01:53 +010043import android.graphics.Point;
Mihai Popae3017462018-03-07 12:25:21 +000044import android.graphics.PointF;
John Reck32f140aa62018-10-04 15:08:24 -070045import android.graphics.RecordingCanvas;
Gilles Debunned88876a2012-03-16 17:34:04 -070046import android.graphics.Rect;
47import android.graphics.RectF;
John Reck32f140aa62018-10-04 15:08:24 -070048import android.graphics.RenderNode;
Seigo Nonaka3ed1b392016-01-19 13:54:59 +090049import android.graphics.drawable.ColorDrawable;
Gilles Debunned88876a2012-03-16 17:34:04 -070050import android.graphics.drawable.Drawable;
Mihai Popa6315a322018-10-17 17:39:57 +010051import android.os.Build;
Gilles Debunned88876a2012-03-16 17:34:04 -070052import android.os.Bundle;
Yohei Yukawa23cbe852016-05-17 16:42:58 -070053import android.os.LocaleList;
Raph Levien26d443a2015-03-30 14:18:32 -070054import android.os.Parcel;
55import android.os.Parcelable;
James Cookf59152c2015-02-26 18:03:58 -080056import android.os.ParcelableParcel;
Gilles Debunned88876a2012-03-16 17:34:04 -070057import android.os.SystemClock;
58import android.provider.Settings;
59import android.text.DynamicLayout;
60import android.text.Editable;
Raph Levien26d443a2015-03-30 14:18:32 -070061import android.text.InputFilter;
Gilles Debunned88876a2012-03-16 17:34:04 -070062import android.text.InputType;
63import android.text.Layout;
64import android.text.ParcelableSpan;
65import android.text.Selection;
66import android.text.SpanWatcher;
67import android.text.Spannable;
68import android.text.SpannableStringBuilder;
69import android.text.Spanned;
70import android.text.StaticLayout;
71import android.text.TextUtils;
Gilles Debunned88876a2012-03-16 17:34:04 -070072import android.text.method.KeyListener;
73import android.text.method.MetaKeyKeyListener;
74import android.text.method.MovementMethod;
Gilles Debunned88876a2012-03-16 17:34:04 -070075import android.text.method.WordIterator;
76import android.text.style.EasyEditSpan;
77import android.text.style.SuggestionRangeSpan;
78import android.text.style.SuggestionSpan;
79import android.text.style.TextAppearanceSpan;
80import android.text.style.URLSpan;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +090081import android.util.ArraySet;
Gilles Debunned88876a2012-03-16 17:34:04 -070082import android.util.DisplayMetrics;
83import android.util.Log;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -070084import android.util.SparseArray;
Gilles Debunned88876a2012-03-16 17:34:04 -070085import android.view.ActionMode;
86import android.view.ActionMode.Callback;
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +090087import android.view.ContextMenu;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +090088import android.view.ContextThemeWrapper;
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -070089import android.view.DragAndDropPermissions;
Gilles Debunned88876a2012-03-16 17:34:04 -070090import android.view.DragEvent;
91import android.view.Gravity;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -070092import android.view.HapticFeedbackConstants;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -080093import android.view.InputDevice;
Gilles Debunned88876a2012-03-16 17:34:04 -070094import android.view.LayoutInflater;
95import android.view.Menu;
96import android.view.MenuItem;
97import android.view.MotionEvent;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +090098import android.view.SubMenu;
Gilles Debunned88876a2012-03-16 17:34:04 -070099import android.view.View;
Gilles Debunned88876a2012-03-16 17:34:04 -0700100import android.view.View.DragShadowBuilder;
101import android.view.View.OnClickListener;
Adam Powell057a5852012-05-11 10:28:38 -0700102import android.view.ViewConfiguration;
103import android.view.ViewGroup;
Gilles Debunned88876a2012-03-16 17:34:04 -0700104import android.view.ViewGroup.LayoutParams;
Mihai Popaddf9fe02018-09-28 13:54:19 +0100105import android.view.ViewParent;
Gilles Debunned88876a2012-03-16 17:34:04 -0700106import android.view.ViewTreeObserver;
107import android.view.WindowManager;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700108import android.view.accessibility.AccessibilityNodeInfo;
Mihai Popa38722382018-03-07 19:56:21 +0000109import android.view.animation.LinearInterpolator;
Gilles Debunned88876a2012-03-16 17:34:04 -0700110import android.view.inputmethod.CorrectionInfo;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900111import android.view.inputmethod.CursorAnchorInfo;
Gilles Debunned88876a2012-03-16 17:34:04 -0700112import android.view.inputmethod.EditorInfo;
113import android.view.inputmethod.ExtractedText;
114import android.view.inputmethod.ExtractedTextRequest;
115import android.view.inputmethod.InputConnection;
116import android.view.inputmethod.InputMethodManager;
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100117import android.view.textclassifier.TextClassification;
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000118import android.view.textclassifier.TextClassificationManager;
Gilles Debunned88876a2012-03-16 17:34:04 -0700119import android.widget.AdapterView.OnItemClickListener;
120import android.widget.TextView.Drawables;
121import android.widget.TextView.OnEditorActionListener;
122
Seigo Nonakaa60160b2015-08-19 12:38:35 -0700123import com.android.internal.annotations.VisibleForTesting;
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +0000124import com.android.internal.logging.MetricsLogger;
125import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
Raph Levien26d443a2015-03-30 14:18:32 -0700126import com.android.internal.util.ArrayUtils;
127import com.android.internal.util.GrowingArrayUtils;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700128import com.android.internal.util.Preconditions;
Abodunrinwa Toki29cb7682018-04-11 21:24:20 +0100129import com.android.internal.view.FloatingActionMode;
Raph Levien26d443a2015-03-30 14:18:32 -0700130import com.android.internal.widget.EditableInputConnection;
131
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +0900132import java.lang.annotation.Retention;
133import java.lang.annotation.RetentionPolicy;
Andrei Stingaceanu2aaeefe2015-10-20 19:11:23 +0100134import java.text.BreakIterator;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +0100135import java.util.ArrayList;
Andrei Stingaceanu2aaeefe2015-10-20 19:11:23 +0100136import java.util.Arrays;
137import java.util.Comparator;
138import java.util.HashMap;
139import java.util.List;
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +0100140import java.util.Map;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700141
Gilles Debunned88876a2012-03-16 17:34:04 -0700142/**
143 * Helper class used by TextView to handle editable text views.
144 *
145 * @hide
146 */
147public class Editor {
Adam Powell057a5852012-05-11 10:28:38 -0700148 private static final String TAG = "Editor";
James Cookf59152c2015-02-26 18:03:58 -0800149 private static final boolean DEBUG_UNDO = false;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100150 // Specifies whether to use or not the magnifier when pressing the insertion or selection
151 // handles.
Andrei Stingaceanu060b3d72017-10-04 11:27:08 +0100152 private static final boolean FLAG_USE_MAGNIFIER = true;
Adam Powell057a5852012-05-11 10:28:38 -0700153
Gilles Debunned88876a2012-03-16 17:34:04 -0700154 static final int BLINK = 500;
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700155 private static final int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
Mady Mellorcc65c372015-06-17 09:25:19 -0700156 private static final float LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS = 0.5f;
Mady Mellore264ac32015-06-22 16:46:29 -0700157 private static final int UNSET_X_VALUE = -1;
Mady Mellora6a0f782015-07-10 16:43:32 -0700158 private static final int UNSET_LINE = -1;
James Cookf59152c2015-02-26 18:03:58 -0800159 // Tag used when the Editor maintains its own separate UndoManager.
160 private static final String UNDO_OWNER_TAG = "Editor";
Gilles Debunned88876a2012-03-16 17:34:04 -0700161
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900162 // Ordering constants used to place the Action Mode or context menu items in their menu.
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +0100163 private static final int MENU_ITEM_ORDER_ASSIST = 0;
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +0000164 private static final int MENU_ITEM_ORDER_UNDO = 2;
165 private static final int MENU_ITEM_ORDER_REDO = 3;
Abodunrinwa Toki5fedfb82017-02-06 19:34:00 +0000166 private static final int MENU_ITEM_ORDER_CUT = 4;
167 private static final int MENU_ITEM_ORDER_COPY = 5;
168 private static final int MENU_ITEM_ORDER_PASTE = 6;
169 private static final int MENU_ITEM_ORDER_SHARE = 7;
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +0100170 private static final int MENU_ITEM_ORDER_SELECT_ALL = 8;
171 private static final int MENU_ITEM_ORDER_REPLACE = 9;
172 private static final int MENU_ITEM_ORDER_AUTOFILL = 10;
173 private static final int MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT = 11;
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +0100174 private static final int MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START = 50;
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +0100175 private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 100;
Clara Bayarri3b69fd82015-06-03 21:52:02 +0100176
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100177 @IntDef({MagnifierHandleTrigger.SELECTION_START,
178 MagnifierHandleTrigger.SELECTION_END,
179 MagnifierHandleTrigger.INSERTION})
180 @Retention(RetentionPolicy.SOURCE)
181 private @interface MagnifierHandleTrigger {
182 int INSERTION = 0;
183 int SELECTION_START = 1;
184 int SELECTION_END = 2;
185 }
186
Richard Ledley26b87222017-11-30 10:54:08 +0000187 @IntDef({TextActionMode.SELECTION, TextActionMode.INSERTION, TextActionMode.TEXT_LINK})
188 @interface TextActionMode {
189 int SELECTION = 0;
190 int INSERTION = 1;
191 int TEXT_LINK = 2;
192 }
193
James Cookf59152c2015-02-26 18:03:58 -0800194 // Each Editor manages its own undo stack.
195 private final UndoManager mUndoManager = new UndoManager();
196 private UndoOwner mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
James Cook48e0fac2015-02-25 15:44:51 -0800197 final UndoInputFilter mUndoInputFilter = new UndoInputFilter(this);
James Cookf1dad1e2015-02-27 11:00:01 -0800198 boolean mAllowUndo = true;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700199
Abodunrinwa Toki54486c12017-04-19 21:02:36 +0100200 private final MetricsLogger mMetricsLogger = new MetricsLogger();
201
Gilles Debunned88876a2012-03-16 17:34:04 -0700202 // Cursor Controllers.
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900203 private InsertionPointCursorController mInsertionPointCursorController;
Gilles Debunned88876a2012-03-16 17:34:04 -0700204 SelectionModifierCursorController mSelectionModifierCursorController;
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100205 // Action mode used when text is selected or when actions on an insertion cursor are triggered.
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800206 private ActionMode mTextActionMode;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100207 @UnsupportedAppUsage
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900208 private boolean mInsertionControllerEnabled;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100209 @UnsupportedAppUsage
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900210 private boolean mSelectionControllerEnabled;
Gilles Debunned88876a2012-03-16 17:34:04 -0700211
Yohei Yukawac9cd9db2017-06-19 18:27:34 -0700212 private final boolean mHapticTextHandleEnabled;
213
Mihai Popa38722382018-03-07 19:56:21 +0000214 private final MagnifierMotionAnimator mMagnifierAnimator;
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000215 private final Runnable mUpdateMagnifierRunnable = new Runnable() {
216 @Override
217 public void run() {
Mihai Popa38722382018-03-07 19:56:21 +0000218 mMagnifierAnimator.update();
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000219 }
220 };
221 // Update the magnifier contents whenever anything in the view hierarchy is updated.
222 // Note: this only captures UI thread-visible changes, so it's a known issue that an animating
223 // VectorDrawable or Ripple animation will not trigger capture, since they're owned by
224 // RenderThread.
225 private final ViewTreeObserver.OnDrawListener mMagnifierOnDrawListener =
226 new ViewTreeObserver.OnDrawListener() {
227 @Override
228 public void onDraw() {
Mihai Popa38722382018-03-07 19:56:21 +0000229 if (mMagnifierAnimator != null) {
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000230 // Posting the method will ensure that updating the magnifier contents will
231 // happen right after the rendering of the current frame.
232 mTextView.post(mUpdateMagnifierRunnable);
233 }
234 }
235 };
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100236
Gilles Debunned88876a2012-03-16 17:34:04 -0700237 // Used to highlight a word when it is corrected by the IME
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900238 private CorrectionHighlighter mCorrectionHighlighter;
Gilles Debunned88876a2012-03-16 17:34:04 -0700239
240 InputContentType mInputContentType;
241 InputMethodState mInputMethodState;
242
Chris Craik956f3402015-04-27 16:41:00 -0700243 private static class TextRenderNode {
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +0900244 // Render node has 3 recording states:
245 // 1. Recorded operations are valid.
246 // #needsRecord() returns false, but needsToBeShifted is false.
247 // 2. Recorded operations are not valid, but just the position needed to be updated.
248 // #needsRecord() returns false, but needsToBeShifted is true.
249 // 3. Recorded operations are not valid. Need to record operations. #needsRecord() returns
250 // true.
Chris Craik956f3402015-04-27 16:41:00 -0700251 RenderNode renderNode;
John Reck7558aa72014-03-05 14:59:59 -0800252 boolean isDirty;
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +0900253 // Becomes true when recorded operations can be reused, but the position has to be updated.
254 boolean needsToBeShifted;
Chris Craik956f3402015-04-27 16:41:00 -0700255 public TextRenderNode(String name) {
Chris Craik956f3402015-04-27 16:41:00 -0700256 renderNode = RenderNode.create(name, null);
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +0900257 isDirty = true;
258 needsToBeShifted = true;
John Reck7558aa72014-03-05 14:59:59 -0800259 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700260 boolean needsRecord() {
John Reckc7ddcf32018-10-25 13:56:17 -0700261 return isDirty || !renderNode.hasDisplayList();
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700262 }
John Reck7558aa72014-03-05 14:59:59 -0800263 }
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900264 private TextRenderNode[] mTextRenderNodes;
Gilles Debunned88876a2012-03-16 17:34:04 -0700265
266 boolean mFrozenWithFocus;
267 boolean mSelectionMoved;
268 boolean mTouchFocusSelected;
269
270 KeyListener mKeyListener;
271 int mInputType = EditorInfo.TYPE_NULL;
272
273 boolean mDiscardNextActionUp;
274 boolean mIgnoreActionUpEvent;
275
Louis Pullen-Freilich1c400a32019-02-05 14:35:20 +0000276 /**
277 * To set a custom cursor, you should use {@link TextView#setTextCursorDrawable(Drawable)}
278 * or {@link TextView#setTextCursorDrawable(int)}.
279 */
280 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
Mihai Popaa4e39c42018-02-20 15:31:11 +0000281 private long mShowCursor;
282 private boolean mRenderCursorRegardlessTiming;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900283 private Blink mBlink;
Gilles Debunned88876a2012-03-16 17:34:04 -0700284
285 boolean mCursorVisible = true;
286 boolean mSelectAllOnFocus;
287 boolean mTextIsSelectable;
288
289 CharSequence mError;
290 boolean mErrorWasChanged;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900291 private ErrorPopup mErrorPopup;
Fabrice Di Meglio1957d282012-10-25 17:42:39 -0700292
Gilles Debunned88876a2012-03-16 17:34:04 -0700293 /**
294 * This flag is set if the TextView tries to display an error before it
295 * is attached to the window (so its position is still unknown).
296 * It causes the error to be shown later, when onAttachedToWindow()
297 * is called.
298 */
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900299 private boolean mShowErrorAfterAttach;
Gilles Debunned88876a2012-03-16 17:34:04 -0700300
301 boolean mInBatchEditControllers;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100302 @UnsupportedAppUsage
Gilles Debunne3473b2b2012-04-20 16:21:10 -0700303 boolean mShowSoftInputOnFocus = true;
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -0800304 private boolean mPreserveSelection;
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +0900305 private boolean mRestartActionModeOnNextRefresh;
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000306 private boolean mRequestingLinkActionMode;
Gilles Debunned88876a2012-03-16 17:34:04 -0700307
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800308 private SelectionActionModeHelper mSelectionActionModeHelper;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +0000309
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900310 boolean mIsBeingLongClicked;
311
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900312 private SuggestionsPopupWindow mSuggestionsPopupWindow;
Gilles Debunned88876a2012-03-16 17:34:04 -0700313 SuggestionRangeSpan mSuggestionRangeSpan;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900314 private Runnable mShowSuggestionRunnable;
Gilles Debunned88876a2012-03-16 17:34:04 -0700315
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -0700316 Drawable mDrawableForCursor = null;
Gilles Debunned88876a2012-03-16 17:34:04 -0700317
Mihai Popa6315a322018-10-17 17:39:57 +0100318 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
319 Drawable mSelectHandleLeft;
320 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
321 Drawable mSelectHandleRight;
322 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
323 Drawable mSelectHandleCenter;
Gilles Debunned88876a2012-03-16 17:34:04 -0700324
325 // Global listener that detects changes in the global position of the TextView
326 private PositionListener mPositionListener;
327
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900328 private float mLastDownPositionX, mLastDownPositionY;
Petar Å egina91df3f92017-08-15 16:20:43 +0100329 private float mLastUpPositionX, mLastUpPositionY;
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900330 private float mContextMenuAnchorX, mContextMenuAnchorY;
Gilles Debunned88876a2012-03-16 17:34:04 -0700331 Callback mCustomSelectionActionModeCallback;
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100332 Callback mCustomInsertionActionModeCallback;
Gilles Debunned88876a2012-03-16 17:34:04 -0700333
334 // Set when this TextView gained focus with some text selected. Will start selection mode.
Mathew Inwood978c6e22018-08-21 15:58:55 +0100335 @UnsupportedAppUsage
Gilles Debunned88876a2012-03-16 17:34:04 -0700336 boolean mCreatedWithASelection;
337
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +0900338 // Indicates the current tap state (first tap, double tap, or triple click).
339 private int mTapState = TAP_STATE_INITIAL;
340 private long mLastTouchUpTime = 0;
341 private static final int TAP_STATE_INITIAL = 0;
342 private static final int TAP_STATE_FIRST_TAP = 1;
343 private static final int TAP_STATE_DOUBLE_TAP = 2;
344 // Only for mouse input.
345 private static final int TAP_STATE_TRIPLE_CLICK = 3;
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100346
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900347 // The button state as of the last time #onTouchEvent is called.
348 private int mLastButtonState;
349
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100350 private Runnable mInsertionActionModeRunnable;
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100351
Jean Chalardbaf30942013-02-28 16:01:51 -0800352 // The span controller helps monitoring the changes to which the Editor needs to react:
353 // - EasyEditSpans, for which we have some UI to display on attach and on hide
354 // - SelectionSpans, for which we need to call updateSelection if an IME is attached
355 private SpanController mSpanController;
Gilles Debunned88876a2012-03-16 17:34:04 -0700356
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900357 private WordIterator mWordIterator;
Gilles Debunned88876a2012-03-16 17:34:04 -0700358 SpellChecker mSpellChecker;
359
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800360 // This word iterator is set with text and used to determine word boundaries
361 // when a user is selecting text.
362 private WordIterator mWordIteratorWithText;
363 // Indicate that the text in the word iterator needs to be updated.
364 private boolean mUpdateWordIteratorText;
365
Gilles Debunned88876a2012-03-16 17:34:04 -0700366 private Rect mTempRect;
367
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800368 private final TextView mTextView;
Gilles Debunned88876a2012-03-16 17:34:04 -0700369
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700370 final ProcessTextIntentActionsHandler mProcessTextIntentActionsHandler;
371
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700372 private final CursorAnchorInfoNotifier mCursorAnchorInfoNotifier =
373 new CursorAnchorInfoNotifier();
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900374
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100375 private final Runnable mShowFloatingToolbar = new Runnable() {
376 @Override
377 public void run() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100378 if (mTextActionMode != null) {
Abodunrinwa Toki9e211282015-06-05 02:46:57 +0100379 mTextActionMode.hide(0); // hide off.
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100380 }
381 }
382 };
383
Clara Bayarrib71dddd2015-06-04 23:17:30 +0100384 boolean mIsInsertionActionModeStartPending = false;
385
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +0900386 private final SuggestionHelper mSuggestionHelper = new SuggestionHelper();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +0900387
Gilles Debunned88876a2012-03-16 17:34:04 -0700388 Editor(TextView textView) {
389 mTextView = textView;
James Cookf59152c2015-02-26 18:03:58 -0800390 // Synchronize the filter list, which places the undo input filter at the end.
391 mTextView.setFilters(mTextView.getFilters());
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700392 mProcessTextIntentActionsHandler = new ProcessTextIntentActionsHandler(this);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -0700393 mHapticTextHandleEnabled = mTextView.getContext().getResources().getBoolean(
394 com.android.internal.R.bool.config_enableHapticTextHandle);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100395
Mihai Popa38722382018-03-07 19:56:21 +0000396 if (FLAG_USE_MAGNIFIER) {
Mihai Popac6950292018-11-15 21:32:42 +0000397 final Magnifier magnifier =
398 Magnifier.createBuilderWithOldMagnifierDefaults(mTextView).build();
Mihai Popab6ca9092018-09-24 21:14:50 +0100399 mMagnifierAnimator = new MagnifierMotionAnimator(magnifier);
Mihai Popa38722382018-03-07 19:56:21 +0000400 }
James Cookf59152c2015-02-26 18:03:58 -0800401 }
402
403 ParcelableParcel saveInstanceState() {
James Cookd2026682015-03-03 14:40:14 -0800404 ParcelableParcel state = new ParcelableParcel(getClass().getClassLoader());
405 Parcel parcel = state.getParcel();
406 mUndoManager.saveInstanceState(parcel);
407 mUndoInputFilter.saveInstanceState(parcel);
408 return state;
James Cookf59152c2015-02-26 18:03:58 -0800409 }
410
411 void restoreInstanceState(ParcelableParcel state) {
James Cookd2026682015-03-03 14:40:14 -0800412 Parcel parcel = state.getParcel();
413 mUndoManager.restoreInstanceState(parcel, state.getClassLoader());
414 mUndoInputFilter.restoreInstanceState(parcel);
James Cookf59152c2015-02-26 18:03:58 -0800415 // Re-associate this object as the owner of undo state.
416 mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
417 }
418
James Cook48e0fac2015-02-25 15:44:51 -0800419 /**
420 * Forgets all undo and redo operations for this Editor.
421 */
422 void forgetUndoRedo() {
423 UndoOwner[] owners = { mUndoOwner };
424 mUndoManager.forgetUndos(owners, -1 /* all */);
425 mUndoManager.forgetRedos(owners, -1 /* all */);
426 }
427
James Cookf59152c2015-02-26 18:03:58 -0800428 boolean canUndo() {
429 UndoOwner[] owners = { mUndoOwner };
James Cookf1dad1e2015-02-27 11:00:01 -0800430 return mAllowUndo && mUndoManager.countUndos(owners) > 0;
James Cookf59152c2015-02-26 18:03:58 -0800431 }
432
433 boolean canRedo() {
434 UndoOwner[] owners = { mUndoOwner };
James Cookf1dad1e2015-02-27 11:00:01 -0800435 return mAllowUndo && mUndoManager.countRedos(owners) > 0;
James Cookf59152c2015-02-26 18:03:58 -0800436 }
437
438 void undo() {
James Cookf1dad1e2015-02-27 11:00:01 -0800439 if (!mAllowUndo) {
440 return;
441 }
James Cookf59152c2015-02-26 18:03:58 -0800442 UndoOwner[] owners = { mUndoOwner };
443 mUndoManager.undo(owners, 1); // Undo 1 action.
444 }
445
446 void redo() {
James Cookf1dad1e2015-02-27 11:00:01 -0800447 if (!mAllowUndo) {
448 return;
449 }
James Cookf59152c2015-02-26 18:03:58 -0800450 UndoOwner[] owners = { mUndoOwner };
451 mUndoManager.redo(owners, 1); // Redo 1 action.
Gilles Debunned88876a2012-03-16 17:34:04 -0700452 }
453
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100454 void replace() {
Keisuke Kuroyanagi713be062016-02-29 16:07:54 -0800455 if (mSuggestionsPopupWindow == null) {
456 mSuggestionsPopupWindow = new SuggestionsPopupWindow();
457 }
458 hideCursorAndSpanControllers();
459 mSuggestionsPopupWindow.show();
460
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100461 int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100462 Selection.setSelection((Spannable) mTextView.getText(), middle);
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100463 }
464
Gilles Debunned88876a2012-03-16 17:34:04 -0700465 void onAttachedToWindow() {
466 if (mShowErrorAfterAttach) {
467 showError();
468 mShowErrorAfterAttach = false;
469 }
470
471 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000472 if (observer.isAlive()) {
473 // No need to create the controller.
474 // The get method will add the listener on controller creation.
475 if (mInsertionPointCursorController != null) {
476 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
477 }
478 if (mSelectionModifierCursorController != null) {
479 mSelectionModifierCursorController.resetTouchOffsets();
480 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
481 }
482 if (FLAG_USE_MAGNIFIER) {
483 observer.addOnDrawListener(mMagnifierOnDrawListener);
484 }
Gilles Debunned88876a2012-03-16 17:34:04 -0700485 }
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000486
Gilles Debunned88876a2012-03-16 17:34:04 -0700487 updateSpellCheckSpans(0, mTextView.getText().length(),
488 true /* create the spell checker if needed */);
Adam Powell057a5852012-05-11 10:28:38 -0700489
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +0900490 if (mTextView.hasSelection()) {
491 refreshTextActionMode();
Adam Powell057a5852012-05-11 10:28:38 -0700492 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900493
494 getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true);
Mikael Gullstrand5b734f22013-07-09 14:41:28 +0200495 resumeBlink();
Gilles Debunned88876a2012-03-16 17:34:04 -0700496 }
497
498 void onDetachedFromWindow() {
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900499 getPositionListener().removeSubscriber(mCursorAnchorInfoNotifier);
500
Gilles Debunned88876a2012-03-16 17:34:04 -0700501 if (mError != null) {
502 hideError();
503 }
504
Mikael Gullstrand5b734f22013-07-09 14:41:28 +0200505 suspendBlink();
Gilles Debunned88876a2012-03-16 17:34:04 -0700506
507 if (mInsertionPointCursorController != null) {
508 mInsertionPointCursorController.onDetached();
509 }
510
511 if (mSelectionModifierCursorController != null) {
512 mSelectionModifierCursorController.onDetached();
513 }
514
515 if (mShowSuggestionRunnable != null) {
516 mTextView.removeCallbacks(mShowSuggestionRunnable);
517 }
518
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100519 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100520 if (mInsertionActionModeRunnable != null) {
521 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100522 }
523
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100524 mTextView.removeCallbacks(mShowFloatingToolbar);
525
Chris Craik003cc3d2015-10-16 10:24:55 -0700526 discardTextDisplayLists();
Gilles Debunned88876a2012-03-16 17:34:04 -0700527
528 if (mSpellChecker != null) {
529 mSpellChecker.closeSession();
530 // Forces the creation of a new SpellChecker next time this window is created.
531 // Will handle the cases where the settings has been changed in the meantime.
532 mSpellChecker = null;
533 }
534
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000535 if (FLAG_USE_MAGNIFIER) {
536 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
537 if (observer.isAlive()) {
538 observer.removeOnDrawListener(mMagnifierOnDrawListener);
539 }
540 }
541
Mady Mellora2861452015-06-25 08:40:27 -0700542 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -0800543 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -0700544 }
545
Chris Craik003cc3d2015-10-16 10:24:55 -0700546 private void discardTextDisplayLists() {
Chris Craik956f3402015-04-27 16:41:00 -0700547 if (mTextRenderNodes != null) {
548 for (int i = 0; i < mTextRenderNodes.length; i++) {
549 RenderNode displayList = mTextRenderNodes[i] != null
550 ? mTextRenderNodes[i].renderNode : null;
John Reckc7ddcf32018-10-25 13:56:17 -0700551 if (displayList != null && displayList.hasDisplayList()) {
Chris Craik003cc3d2015-10-16 10:24:55 -0700552 displayList.discardDisplayList();
John Reck7558aa72014-03-05 14:59:59 -0800553 }
554 }
555 }
556 }
557
Gilles Debunned88876a2012-03-16 17:34:04 -0700558 private void showError() {
559 if (mTextView.getWindowToken() == null) {
560 mShowErrorAfterAttach = true;
561 return;
562 }
563
564 if (mErrorPopup == null) {
565 LayoutInflater inflater = LayoutInflater.from(mTextView.getContext());
566 final TextView err = (TextView) inflater.inflate(
567 com.android.internal.R.layout.textview_hint, null);
568
569 final float scale = mTextView.getResources().getDisplayMetrics().density;
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700570 mErrorPopup =
571 new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f));
Gilles Debunned88876a2012-03-16 17:34:04 -0700572 mErrorPopup.setFocusable(false);
573 // The user is entering text, so the input method is needed. We
574 // don't want the popup to be displayed on top of it.
575 mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
576 }
577
578 TextView tv = (TextView) mErrorPopup.getContentView();
579 chooseSize(mErrorPopup, mError, tv);
580 tv.setText(mError);
581
Hidehiko Tsuchiyaa0c8c1c2017-11-13 10:52:23 +0900582 mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY(),
583 Gravity.TOP | Gravity.LEFT);
Gilles Debunned88876a2012-03-16 17:34:04 -0700584 mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor());
585 }
586
587 public void setError(CharSequence error, Drawable icon) {
588 mError = TextUtils.stringOrSpannedString(error);
589 mErrorWasChanged = true;
Romain Guyd1cc1872012-11-05 17:43:25 -0800590
Gilles Debunned88876a2012-03-16 17:34:04 -0700591 if (mError == null) {
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800592 setErrorIcon(null);
Gilles Debunned88876a2012-03-16 17:34:04 -0700593 if (mErrorPopup != null) {
594 if (mErrorPopup.isShowing()) {
595 mErrorPopup.dismiss();
596 }
597
598 mErrorPopup = null;
599 }
Daniel 2 Olofssonf4ecc552013-08-13 10:30:26 +0200600 mShowErrorAfterAttach = false;
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800601 } else {
Romain Guyd1cc1872012-11-05 17:43:25 -0800602 setErrorIcon(icon);
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800603 if (mTextView.isFocused()) {
604 showError();
605 }
Romain Guyd1cc1872012-11-05 17:43:25 -0800606 }
607 }
608
609 private void setErrorIcon(Drawable icon) {
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800610 Drawables dr = mTextView.mDrawables;
611 if (dr == null) {
Fabrice Di Megliof7a5cdf2013-03-15 15:36:51 -0700612 mTextView.mDrawables = dr = new Drawables(mTextView.getContext());
Gilles Debunned88876a2012-03-16 17:34:04 -0700613 }
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800614 dr.setErrorDrawable(icon, mTextView);
615
616 mTextView.resetResolvedDrawables();
617 mTextView.invalidate();
618 mTextView.requestLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -0700619 }
620
621 private void hideError() {
622 if (mErrorPopup != null) {
623 if (mErrorPopup.isShowing()) {
624 mErrorPopup.dismiss();
625 }
626 }
627
628 mShowErrorAfterAttach = false;
629 }
630
631 /**
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800632 * Returns the X offset to make the pointy top of the error point
Gilles Debunned88876a2012-03-16 17:34:04 -0700633 * at the middle of the error icon.
634 */
635 private int getErrorX() {
636 /*
637 * The "25" is the distance between the point and the right edge
638 * of the background
639 */
640 final float scale = mTextView.getResources().getDisplayMetrics().density;
641
642 final Drawables dr = mTextView.mDrawables;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800643
644 final int layoutDirection = mTextView.getLayoutDirection();
645 int errorX;
646 int offset;
647 switch (layoutDirection) {
648 default:
649 case View.LAYOUT_DIRECTION_LTR:
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700650 offset = -(dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
651 errorX = mTextView.getWidth() - mErrorPopup.getWidth()
652 - mTextView.getPaddingRight() + offset;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800653 break;
654 case View.LAYOUT_DIRECTION_RTL:
655 offset = (dr != null ? dr.mDrawableSizeLeft : 0) / 2 - (int) (25 * scale + 0.5f);
656 errorX = mTextView.getPaddingLeft() + offset;
657 break;
658 }
659 return errorX;
Gilles Debunned88876a2012-03-16 17:34:04 -0700660 }
661
662 /**
663 * Returns the Y offset to make the pointy top of the error point
664 * at the bottom of the error icon.
665 */
666 private int getErrorY() {
667 /*
668 * Compound, not extended, because the icon is not clipped
669 * if the text height is smaller.
670 */
671 final int compoundPaddingTop = mTextView.getCompoundPaddingTop();
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700672 int vspace = mTextView.getBottom() - mTextView.getTop()
673 - mTextView.getCompoundPaddingBottom() - compoundPaddingTop;
Gilles Debunned88876a2012-03-16 17:34:04 -0700674
675 final Drawables dr = mTextView.mDrawables;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800676
677 final int layoutDirection = mTextView.getLayoutDirection();
678 int height;
679 switch (layoutDirection) {
680 default:
681 case View.LAYOUT_DIRECTION_LTR:
682 height = (dr != null ? dr.mDrawableHeightRight : 0);
683 break;
684 case View.LAYOUT_DIRECTION_RTL:
685 height = (dr != null ? dr.mDrawableHeightLeft : 0);
686 break;
687 }
688
689 int icontop = compoundPaddingTop + (vspace - height) / 2;
Gilles Debunned88876a2012-03-16 17:34:04 -0700690
691 /*
692 * The "2" is the distance between the point and the top edge
693 * of the background.
694 */
695 final float scale = mTextView.getResources().getDisplayMetrics().density;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800696 return icontop + height - mTextView.getHeight() - (int) (2 * scale + 0.5f);
Gilles Debunned88876a2012-03-16 17:34:04 -0700697 }
698
699 void createInputContentTypeIfNeeded() {
700 if (mInputContentType == null) {
701 mInputContentType = new InputContentType();
702 }
703 }
704
705 void createInputMethodStateIfNeeded() {
706 if (mInputMethodState == null) {
707 mInputMethodState = new InputMethodState();
708 }
709 }
710
Mihai Popaa4e39c42018-02-20 15:31:11 +0000711 private boolean isCursorVisible() {
Gilles Debunned88876a2012-03-16 17:34:04 -0700712 // The default value is true, even when there is no associated Editor
713 return mCursorVisible && mTextView.isTextEditable();
714 }
715
Mihai Popaa4e39c42018-02-20 15:31:11 +0000716 boolean shouldRenderCursor() {
717 if (!isCursorVisible()) {
718 return false;
719 }
720 if (mRenderCursorRegardlessTiming) {
721 return true;
722 }
723 final long showCursorDelta = SystemClock.uptimeMillis() - mShowCursor;
724 return showCursorDelta % (2 * BLINK) < BLINK;
725 }
726
Gilles Debunned88876a2012-03-16 17:34:04 -0700727 void prepareCursorControllers() {
728 boolean windowSupportsHandles = false;
729
730 ViewGroup.LayoutParams params = mTextView.getRootView().getLayoutParams();
731 if (params instanceof WindowManager.LayoutParams) {
732 WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
733 windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
734 || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
735 }
736
737 boolean enabled = windowSupportsHandles && mTextView.getLayout() != null;
738 mInsertionControllerEnabled = enabled && isCursorVisible();
739 mSelectionControllerEnabled = enabled && mTextView.textCanBeSelected();
740
741 if (!mInsertionControllerEnabled) {
742 hideInsertionPointCursorController();
743 if (mInsertionPointCursorController != null) {
744 mInsertionPointCursorController.onDetached();
745 mInsertionPointCursorController = null;
746 }
747 }
748
749 if (!mSelectionControllerEnabled) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100750 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -0700751 if (mSelectionModifierCursorController != null) {
752 mSelectionModifierCursorController.onDetached();
753 mSelectionModifierCursorController = null;
754 }
755 }
756 }
757
Seigo Nonakabb6a62c2015-03-31 21:59:30 +0900758 void hideInsertionPointCursorController() {
Gilles Debunned88876a2012-03-16 17:34:04 -0700759 if (mInsertionPointCursorController != null) {
760 mInsertionPointCursorController.hide();
761 }
762 }
763
764 /**
Mady Mellora2861452015-06-25 08:40:27 -0700765 * Hides the insertion and span controllers.
Gilles Debunned88876a2012-03-16 17:34:04 -0700766 */
Mady Mellora2861452015-06-25 08:40:27 -0700767 void hideCursorAndSpanControllers() {
Gilles Debunned88876a2012-03-16 17:34:04 -0700768 hideCursorControllers();
769 hideSpanControllers();
770 }
771
772 private void hideSpanControllers() {
Jean Chalardbaf30942013-02-28 16:01:51 -0800773 if (mSpanController != null) {
774 mSpanController.hide();
Gilles Debunned88876a2012-03-16 17:34:04 -0700775 }
776 }
777
778 private void hideCursorControllers() {
Yohei Yukawa85d08f12015-04-29 20:12:37 -0700779 // When mTextView is not ExtractEditText, we need to distinguish two kinds of focus-lost.
780 // One is the true focus lost where suggestions pop-up (if any) should be dismissed, and the
781 // other is an side effect of showing the suggestions pop-up itself. We use isShowingUp()
782 // to distinguish one from the other.
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700783 if (mSuggestionsPopupWindow != null && ((mTextView.isInExtractedMode())
784 || !mSuggestionsPopupWindow.isShowingUp())) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700785 // Should be done before hide insertion point controller since it triggers a show of it
786 mSuggestionsPopupWindow.hide();
787 }
788 hideInsertionPointCursorController();
Gilles Debunned88876a2012-03-16 17:34:04 -0700789 }
790
791 /**
792 * Create new SpellCheckSpans on the modified region.
793 */
794 private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
Satoshi Kataokad7429c12013-06-05 16:30:23 +0900795 // Remove spans whose adjacent characters are text not punctuation
796 mTextView.removeAdjacentSuggestionSpans(start);
797 mTextView.removeAdjacentSuggestionSpans(end);
798
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700799 if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled()
800 && !(mTextView.isInExtractedMode())) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700801 if (mSpellChecker == null && createSpellChecker) {
802 mSpellChecker = new SpellChecker(mTextView);
803 }
804 if (mSpellChecker != null) {
805 mSpellChecker.spellCheck(start, end);
806 }
807 }
808 }
809
810 void onScreenStateChanged(int screenState) {
811 switch (screenState) {
812 case View.SCREEN_STATE_ON:
813 resumeBlink();
814 break;
815 case View.SCREEN_STATE_OFF:
816 suspendBlink();
817 break;
818 }
819 }
820
821 private void suspendBlink() {
822 if (mBlink != null) {
823 mBlink.cancel();
824 }
825 }
826
827 private void resumeBlink() {
828 if (mBlink != null) {
829 mBlink.uncancel();
830 makeBlink();
831 }
832 }
833
834 void adjustInputType(boolean password, boolean passwordInputType,
835 boolean webPasswordInputType, boolean numberPasswordInputType) {
836 // mInputType has been set from inputType, possibly modified by mInputMethod.
837 // Specialize mInputType to [web]password if we have a text class and the original input
838 // type was a password.
839 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
840 if (password || passwordInputType) {
841 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
842 | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
843 }
844 if (webPasswordInputType) {
845 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
846 | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
847 }
848 } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
849 if (numberPasswordInputType) {
850 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
851 | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
852 }
853 }
854 }
855
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700856 private void chooseSize(@NonNull PopupWindow pop, @NonNull CharSequence text,
857 @NonNull TextView tv) {
858 final int wid = tv.getPaddingLeft() + tv.getPaddingRight();
859 final int ht = tv.getPaddingTop() + tv.getPaddingBottom();
Gilles Debunned88876a2012-03-16 17:34:04 -0700860
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700861 final int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize(
Gilles Debunned88876a2012-03-16 17:34:04 -0700862 com.android.internal.R.dimen.textview_error_popup_default_width);
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700863 final StaticLayout l = StaticLayout.Builder.obtain(text, 0, text.length(), tv.getPaint(),
864 defaultWidthInPixels)
865 .setUseLineSpacingFromFallbacks(tv.mUseFallbackLineSpacing)
866 .build();
867
Gilles Debunned88876a2012-03-16 17:34:04 -0700868 float max = 0;
869 for (int i = 0; i < l.getLineCount(); i++) {
870 max = Math.max(max, l.getLineWidth(i));
871 }
872
873 /*
874 * Now set the popup size to be big enough for the text plus the border capped
875 * to DEFAULT_MAX_POPUP_WIDTH
876 */
877 pop.setWidth(wid + (int) Math.ceil(max));
878 pop.setHeight(ht + l.getHeight());
879 }
880
881 void setFrame() {
882 if (mErrorPopup != null) {
883 TextView tv = (TextView) mErrorPopup.getContentView();
884 chooseSize(mErrorPopup, mError, tv);
885 mErrorPopup.update(mTextView, getErrorX(), getErrorY(),
886 mErrorPopup.getWidth(), mErrorPopup.getHeight());
887 }
888 }
889
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800890 private int getWordStart(int offset) {
891 // FIXME - For this and similar methods we're not doing anything to check if there's
892 // a LocaleSpan in the text, this may be something we should try handling or checking for.
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700893 int retOffset = getWordIteratorWithText().prevBoundary(offset);
Mady Mellor58c90872015-05-12 11:09:37 -0700894 if (getWordIteratorWithText().isOnPunctuation(retOffset)) {
895 // On punctuation boundary or within group of punctuation, find punctuation start.
896 retOffset = getWordIteratorWithText().getPunctuationBeginning(offset);
897 } else {
898 // Not on a punctuation boundary, find the word start.
Mady Mellore264ac32015-06-22 16:46:29 -0700899 retOffset = getWordIteratorWithText().getPrevWordBeginningOnTwoWordsBoundary(offset);
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800900 }
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700901 if (retOffset == BreakIterator.DONE) {
902 return offset;
903 }
904 return retOffset;
905 }
906
907 private int getWordEnd(int offset) {
908 int retOffset = getWordIteratorWithText().nextBoundary(offset);
Mady Mellor58c90872015-05-12 11:09:37 -0700909 if (getWordIteratorWithText().isAfterPunctuation(retOffset)) {
910 // On punctuation boundary or within group of punctuation, find punctuation end.
911 retOffset = getWordIteratorWithText().getPunctuationEnd(offset);
912 } else {
913 // Not on a punctuation boundary, find the word end.
Mady Mellore264ac32015-06-22 16:46:29 -0700914 retOffset = getWordIteratorWithText().getNextWordEndOnTwoWordBoundary(offset);
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700915 }
916 if (retOffset == BreakIterator.DONE) {
917 return offset;
918 }
919 return retOffset;
920 }
921
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900922 private boolean needsToSelectAllToSelectWordOrParagraph() {
Andrei Stingaceanu47f82ae2015-04-28 17:43:54 +0100923 if (mTextView.hasPasswordTransformationMethod()) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700924 // Always select all on a password field.
925 // Cut/copy menu entries are not available for passwords, but being able to select all
926 // is however useful to delete or paste to replace the entire content.
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900927 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -0700928 }
929
930 int inputType = mTextView.getInputType();
931 int klass = inputType & InputType.TYPE_MASK_CLASS;
932 int variation = inputType & InputType.TYPE_MASK_VARIATION;
933
934 // Specific text field types: select the entire text for these
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700935 if (klass == InputType.TYPE_CLASS_NUMBER
936 || klass == InputType.TYPE_CLASS_PHONE
937 || klass == InputType.TYPE_CLASS_DATETIME
938 || variation == InputType.TYPE_TEXT_VARIATION_URI
939 || variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
940 || variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS
941 || variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900942 return true;
943 }
944 return false;
945 }
946
947 /**
948 * Adjusts selection to the word under last touch offset. Return true if the operation was
949 * successfully performed.
950 */
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100951 boolean selectCurrentWord() {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900952 if (!mTextView.canSelectText()) {
953 return false;
954 }
955
956 if (needsToSelectAllToSelectWordOrParagraph()) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700957 return mTextView.selectAllText();
958 }
959
960 long lastTouchOffsets = getLastTouchOffsets();
961 final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
962 final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
963
964 // Safety check in case standard touch event handling has been bypassed
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -0800965 if (minOffset < 0 || minOffset > mTextView.getText().length()) return false;
966 if (maxOffset < 0 || maxOffset > mTextView.getText().length()) return false;
Gilles Debunned88876a2012-03-16 17:34:04 -0700967
968 int selectionStart, selectionEnd;
969
970 // If a URLSpan (web address, email, phone...) is found at that position, select it.
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700971 URLSpan[] urlSpans =
972 ((Spanned) mTextView.getText()).getSpans(minOffset, maxOffset, URLSpan.class);
Gilles Debunned88876a2012-03-16 17:34:04 -0700973 if (urlSpans.length >= 1) {
974 URLSpan urlSpan = urlSpans[0];
975 selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan);
976 selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan);
977 } else {
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800978 // FIXME - We should check if there's a LocaleSpan in the text, this may be
979 // something we should try handling or checking for.
Gilles Debunned88876a2012-03-16 17:34:04 -0700980 final WordIterator wordIterator = getWordIterator();
981 wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset);
982
983 selectionStart = wordIterator.getBeginning(minOffset);
984 selectionEnd = wordIterator.getEnd(maxOffset);
985
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700986 if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE
987 || selectionStart == selectionEnd) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700988 // Possible when the word iterator does not properly handle the text's language
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +0900989 long range = getCharClusterRange(minOffset);
Gilles Debunned88876a2012-03-16 17:34:04 -0700990 selectionStart = TextUtils.unpackRangeStartFromLong(range);
991 selectionEnd = TextUtils.unpackRangeEndFromLong(range);
992 }
993 }
994
995 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
996 return selectionEnd > selectionStart;
997 }
998
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900999 /**
1000 * Adjusts selection to the paragraph under last touch offset. Return true if the operation was
1001 * successfully performed.
1002 */
1003 private boolean selectCurrentParagraph() {
1004 if (!mTextView.canSelectText()) {
1005 return false;
1006 }
1007
1008 if (needsToSelectAllToSelectWordOrParagraph()) {
1009 return mTextView.selectAllText();
1010 }
1011
1012 long lastTouchOffsets = getLastTouchOffsets();
1013 final int minLastTouchOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
1014 final int maxLastTouchOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
1015
1016 final long paragraphsRange = getParagraphsRange(minLastTouchOffset, maxLastTouchOffset);
1017 final int start = TextUtils.unpackRangeStartFromLong(paragraphsRange);
1018 final int end = TextUtils.unpackRangeEndFromLong(paragraphsRange);
1019 if (start < end) {
1020 Selection.setSelection((Spannable) mTextView.getText(), start, end);
1021 return true;
1022 }
1023 return false;
1024 }
1025
1026 /**
1027 * Get the minimum range of paragraphs that contains startOffset and endOffset.
1028 */
1029 private long getParagraphsRange(int startOffset, int endOffset) {
1030 final Layout layout = mTextView.getLayout();
1031 if (layout == null) {
1032 return TextUtils.packRangeInLong(-1, -1);
1033 }
1034 final CharSequence text = mTextView.getText();
1035 int minLine = layout.getLineForOffset(startOffset);
1036 // Search paragraph start.
1037 while (minLine > 0) {
1038 final int prevLineEndOffset = layout.getLineEnd(minLine - 1);
1039 if (text.charAt(prevLineEndOffset - 1) == '\n') {
1040 break;
1041 }
1042 minLine--;
1043 }
1044 int maxLine = layout.getLineForOffset(endOffset);
1045 // Search paragraph end.
1046 while (maxLine < layout.getLineCount() - 1) {
1047 final int lineEndOffset = layout.getLineEnd(maxLine);
1048 if (text.charAt(lineEndOffset - 1) == '\n') {
1049 break;
1050 }
1051 maxLine++;
1052 }
1053 return TextUtils.packRangeInLong(layout.getLineStart(minLine), layout.getLineEnd(maxLine));
1054 }
1055
Gilles Debunned88876a2012-03-16 17:34:04 -07001056 void onLocaleChanged() {
Keisuke Kuroyanagie0ac5ac2016-03-09 15:33:30 +09001057 // Will be re-created on demand in getWordIterator and getWordIteratorWithText with the
1058 // proper new locale
Gilles Debunned88876a2012-03-16 17:34:04 -07001059 mWordIterator = null;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08001060 mWordIteratorWithText = null;
Gilles Debunned88876a2012-03-16 17:34:04 -07001061 }
1062
Gilles Debunned88876a2012-03-16 17:34:04 -07001063 public WordIterator getWordIterator() {
1064 if (mWordIterator == null) {
1065 mWordIterator = new WordIterator(mTextView.getTextServicesLocale());
1066 }
1067 return mWordIterator;
1068 }
1069
Mady Mellor2ff2cd82015-03-02 10:37:01 -08001070 private WordIterator getWordIteratorWithText() {
1071 if (mWordIteratorWithText == null) {
1072 mWordIteratorWithText = new WordIterator(mTextView.getTextServicesLocale());
1073 mUpdateWordIteratorText = true;
1074 }
1075 if (mUpdateWordIteratorText) {
1076 // FIXME - Shouldn't copy all of the text as only the area of the text relevant
1077 // to the user's selection is needed. A possible solution would be to
1078 // copy some number N of characters near the selection and then when the
1079 // user approaches N then we'd do another copy of the next N characters.
1080 CharSequence text = mTextView.getText();
1081 mWordIteratorWithText.setCharSequence(text, 0, text.length());
1082 mUpdateWordIteratorText = false;
1083 }
1084 return mWordIteratorWithText;
1085 }
1086
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +09001087 private int getNextCursorOffset(int offset, boolean findAfterGivenOffset) {
1088 final Layout layout = mTextView.getLayout();
1089 if (layout == null) return offset;
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001090 return findAfterGivenOffset == layout.isRtlCharAt(offset)
1091 ? layout.getOffsetToLeftOf(offset) : layout.getOffsetToRightOf(offset);
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +09001092 }
1093
1094 private long getCharClusterRange(int offset) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001095 final int textLength = mTextView.getText().length();
Gilles Debunned88876a2012-03-16 17:34:04 -07001096 if (offset < textLength) {
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08001097 final int clusterEndOffset = getNextCursorOffset(offset, true);
1098 return TextUtils.packRangeInLong(
1099 getNextCursorOffset(clusterEndOffset, false), clusterEndOffset);
Gilles Debunned88876a2012-03-16 17:34:04 -07001100 }
1101 if (offset - 1 >= 0) {
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08001102 final int clusterStartOffset = getNextCursorOffset(offset, false);
1103 return TextUtils.packRangeInLong(clusterStartOffset,
1104 getNextCursorOffset(clusterStartOffset, true));
Gilles Debunned88876a2012-03-16 17:34:04 -07001105 }
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +09001106 return TextUtils.packRangeInLong(offset, offset);
Gilles Debunned88876a2012-03-16 17:34:04 -07001107 }
1108
1109 private boolean touchPositionIsInSelection() {
1110 int selectionStart = mTextView.getSelectionStart();
1111 int selectionEnd = mTextView.getSelectionEnd();
1112
1113 if (selectionStart == selectionEnd) {
1114 return false;
1115 }
1116
1117 if (selectionStart > selectionEnd) {
1118 int tmp = selectionStart;
1119 selectionStart = selectionEnd;
1120 selectionEnd = tmp;
1121 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
1122 }
1123
1124 SelectionModifierCursorController selectionController = getSelectionController();
1125 int minOffset = selectionController.getMinTouchOffset();
1126 int maxOffset = selectionController.getMaxTouchOffset();
1127
1128 return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
1129 }
1130
1131 private PositionListener getPositionListener() {
1132 if (mPositionListener == null) {
1133 mPositionListener = new PositionListener();
1134 }
1135 return mPositionListener;
1136 }
1137
1138 private interface TextViewPositionListener {
1139 public void updatePosition(int parentPositionX, int parentPositionY,
1140 boolean parentPositionChanged, boolean parentScrolled);
1141 }
1142
Gilles Debunned88876a2012-03-16 17:34:04 -07001143 private boolean isOffsetVisible(int offset) {
1144 Layout layout = mTextView.getLayout();
Victoria Leaseb9b77ae2013-10-13 15:12:52 -07001145 if (layout == null) return false;
1146
Gilles Debunned88876a2012-03-16 17:34:04 -07001147 final int line = layout.getLineForOffset(offset);
1148 final int lineBottom = layout.getLineBottom(line);
1149 final int primaryHorizontal = (int) layout.getPrimaryHorizontal(offset);
Phil Weaverc2e28932016-12-08 12:29:25 -08001150 return mTextView.isPositionVisible(
1151 primaryHorizontal + mTextView.viewportToContentHorizontalOffset(),
Gilles Debunned88876a2012-03-16 17:34:04 -07001152 lineBottom + mTextView.viewportToContentVerticalOffset());
1153 }
1154
1155 /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
1156 * in the view. Returns false when the position is in the empty space of left/right of text.
1157 */
1158 private boolean isPositionOnText(float x, float y) {
1159 Layout layout = mTextView.getLayout();
1160 if (layout == null) return false;
1161
1162 final int line = mTextView.getLineAtCoordinate(y);
1163 x = mTextView.convertToLocalHorizontalCoordinate(x);
1164
1165 if (x < layout.getLineLeft(line)) return false;
1166 if (x > layout.getLineRight(line)) return false;
1167 return true;
1168 }
1169
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001170 private void startDragAndDrop() {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001171 getSelectionActionModeHelper().onSelectionDrag();
1172
Keisuke Kuroyanagifdfc93d2016-03-15 14:47:08 +09001173 // TODO: Fix drag and drop in full screen extracted mode.
1174 if (mTextView.isInExtractedMode()) {
1175 return;
1176 }
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001177 final int start = mTextView.getSelectionStart();
1178 final int end = mTextView.getSelectionEnd();
1179 CharSequence selectedText = mTextView.getTransformedText(start, end);
1180 ClipData data = ClipData.newPlainText(null, selectedText);
1181 DragLocalState localState = new DragLocalState(mTextView, start, end);
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08001182 mTextView.startDragAndDrop(data, getTextThumbnailBuilder(start, end), localState,
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001183 View.DRAG_FLAG_GLOBAL);
1184 stopTextActionMode();
1185 if (hasSelectionController()) {
1186 getSelectionController().resetTouchOffsets();
1187 }
1188 }
1189
Gilles Debunned88876a2012-03-16 17:34:04 -07001190 public boolean performLongClick(boolean handled) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001191 // Long press in empty space moves cursor and starts the insertion action mode.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001192 if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY)
1193 && mInsertionControllerEnabled) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001194 final int offset = mTextView.getOffsetForPosition(mLastDownPositionX,
1195 mLastDownPositionY);
Gilles Debunned88876a2012-03-16 17:34:04 -07001196 Selection.setSelection((Spannable) mTextView.getText(), offset);
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00001197 getInsertionController().show();
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001198 mIsInsertionActionModeStartPending = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001199 handled = true;
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001200 MetricsLogger.action(
1201 mTextView.getContext(),
1202 MetricsEvent.TEXT_LONGPRESS,
1203 TextViewMetrics.SUBTYPE_LONG_PRESS_OTHER);
Gilles Debunned88876a2012-03-16 17:34:04 -07001204 }
1205
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001206 if (!handled && mTextActionMode != null) {
Andrei Stingaceanu2aaeefe2015-10-20 19:11:23 +01001207 if (touchPositionIsInSelection()) {
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001208 startDragAndDrop();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001209 MetricsLogger.action(
1210 mTextView.getContext(),
1211 MetricsEvent.TEXT_LONGPRESS,
1212 TextViewMetrics.SUBTYPE_LONG_PRESS_DRAG_AND_DROP);
Gilles Debunned88876a2012-03-16 17:34:04 -07001213 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001214 stopTextActionMode();
Clara Bayarridfac4432015-05-15 12:18:24 +01001215 selectCurrentWordAndStartDrag();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001216 MetricsLogger.action(
1217 mTextView.getContext(),
1218 MetricsEvent.TEXT_LONGPRESS,
1219 TextViewMetrics.SUBTYPE_LONG_PRESS_SELECTION);
Gilles Debunned88876a2012-03-16 17:34:04 -07001220 }
1221 handled = true;
1222 }
1223
1224 // Start a new selection
1225 if (!handled) {
Clara Bayarridfac4432015-05-15 12:18:24 +01001226 handled = selectCurrentWordAndStartDrag();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001227 if (handled) {
1228 MetricsLogger.action(
1229 mTextView.getContext(),
1230 MetricsEvent.TEXT_LONGPRESS,
1231 TextViewMetrics.SUBTYPE_LONG_PRESS_SELECTION);
1232 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001233 }
1234
1235 return handled;
1236 }
1237
Petar Å egina91df3f92017-08-15 16:20:43 +01001238 float getLastUpPositionX() {
1239 return mLastUpPositionX;
1240 }
1241
1242 float getLastUpPositionY() {
1243 return mLastUpPositionY;
1244 }
1245
Gilles Debunned88876a2012-03-16 17:34:04 -07001246 private long getLastTouchOffsets() {
1247 SelectionModifierCursorController selectionController = getSelectionController();
1248 final int minOffset = selectionController.getMinTouchOffset();
1249 final int maxOffset = selectionController.getMaxTouchOffset();
1250 return TextUtils.packRangeInLong(minOffset, maxOffset);
1251 }
1252
1253 void onFocusChanged(boolean focused, int direction) {
1254 mShowCursor = SystemClock.uptimeMillis();
1255 ensureEndedBatchEdit();
1256
1257 if (focused) {
1258 int selStart = mTextView.getSelectionStart();
1259 int selEnd = mTextView.getSelectionEnd();
1260
1261 // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
1262 // mode for these, unless there was a specific selection already started.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001263 final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0
1264 && selEnd == mTextView.getText().length();
Gilles Debunned88876a2012-03-16 17:34:04 -07001265
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001266 mCreatedWithASelection = mFrozenWithFocus && mTextView.hasSelection()
1267 && !isFocusHighlighted;
Gilles Debunned88876a2012-03-16 17:34:04 -07001268
1269 if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
1270 // If a tap was used to give focus to that view, move cursor at tap position.
1271 // Has to be done before onTakeFocus, which can be overloaded.
1272 final int lastTapPosition = getLastTapPosition();
1273 if (lastTapPosition >= 0) {
1274 Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition);
1275 }
1276
1277 // Note this may have to be moved out of the Editor class
1278 MovementMethod mMovement = mTextView.getMovementMethod();
1279 if (mMovement != null) {
1280 mMovement.onTakeFocus(mTextView, (Spannable) mTextView.getText(), direction);
1281 }
1282
1283 // The DecorView does not have focus when the 'Done' ExtractEditText button is
1284 // pressed. Since it is the ViewAncestor's mView, it requests focus before
1285 // ExtractEditText clears focus, which gives focus to the ExtractEditText.
1286 // This special case ensure that we keep current selection in that case.
1287 // It would be better to know why the DecorView does not have focus at that time.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001288 if (((mTextView.isInExtractedMode()) || mSelectionMoved)
1289 && selStart >= 0 && selEnd >= 0) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001290 /*
1291 * Someone intentionally set the selection, so let them
1292 * do whatever it is that they wanted to do instead of
1293 * the default on-focus behavior. We reset the selection
1294 * here instead of just skipping the onTakeFocus() call
1295 * because some movement methods do something other than
1296 * just setting the selection in theirs and we still
1297 * need to go through that path.
1298 */
1299 Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
1300 }
1301
1302 if (mSelectAllOnFocus) {
1303 mTextView.selectAllText();
1304 }
1305
1306 mTouchFocusSelected = true;
1307 }
1308
1309 mFrozenWithFocus = false;
1310 mSelectionMoved = false;
1311
1312 if (mError != null) {
1313 showError();
1314 }
1315
1316 makeBlink();
1317 } else {
1318 if (mError != null) {
1319 hideError();
1320 }
1321 // Don't leave us in the middle of a batch edit.
1322 mTextView.onEndBatchEdit();
1323
Andrei Stingaceanub1891b32015-06-19 16:44:37 +01001324 if (mTextView.isInExtractedMode()) {
Mady Mellora2861452015-06-25 08:40:27 -07001325 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001326 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -07001327 } else {
Mady Mellora2861452015-06-25 08:40:27 -07001328 hideCursorAndSpanControllers();
Yohei Yukawa24df9312016-03-31 17:15:23 -07001329 if (mTextView.isTemporarilyDetached()) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001330 stopTextActionModeWithPreservingSelection();
1331 } else {
1332 stopTextActionMode();
1333 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001334 downgradeEasyCorrectionSpans();
1335 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001336 // No need to create the controller
1337 if (mSelectionModifierCursorController != null) {
1338 mSelectionModifierCursorController.resetTouchOffsets();
1339 }
Richard Ledley5f2f8202018-02-05 14:55:47 +00001340
1341 ensureNoSelectionIfNonSelectable();
1342 }
1343 }
1344
1345 private void ensureNoSelectionIfNonSelectable() {
1346 // This could be the case if a TextLink has been tapped.
1347 if (!mTextView.textCanBeSelected() && mTextView.hasSelection()) {
1348 Selection.setSelection((Spannable) mTextView.getText(),
1349 mTextView.length(), mTextView.length());
Gilles Debunned88876a2012-03-16 17:34:04 -07001350 }
1351 }
1352
1353 /**
1354 * Downgrades to simple suggestions all the easy correction spans that are not a spell check
1355 * span.
1356 */
1357 private void downgradeEasyCorrectionSpans() {
1358 CharSequence text = mTextView.getText();
1359 if (text instanceof Spannable) {
1360 Spannable spannable = (Spannable) text;
1361 SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
1362 spannable.length(), SuggestionSpan.class);
1363 for (int i = 0; i < suggestionSpans.length; i++) {
1364 int flags = suggestionSpans[i].getFlags();
1365 if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
1366 && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
1367 flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
1368 suggestionSpans[i].setFlags(flags);
1369 }
1370 }
1371 }
1372 }
1373
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +01001374 void sendOnTextChanged(int start, int before, int after) {
1375 getSelectionActionModeHelper().onTextChanged(start, start + before);
Gilles Debunned88876a2012-03-16 17:34:04 -07001376 updateSpellCheckSpans(start, start + after, false);
1377
Mady Mellor2ff2cd82015-03-02 10:37:01 -08001378 // Flip flag to indicate the word iterator needs to have the text reset.
1379 mUpdateWordIteratorText = true;
1380
Gilles Debunned88876a2012-03-16 17:34:04 -07001381 // Hide the controllers as soon as text is modified (typing, procedural...)
1382 // We do not hide the span controllers, since they can be added when a new text is
1383 // inserted into the text view (voice IME).
1384 hideCursorControllers();
Keisuke Kuroyanagif4e347d2015-06-11 17:41:00 +09001385 // Reset drag accelerator.
1386 if (mSelectionModifierCursorController != null) {
1387 mSelectionModifierCursorController.resetTouchOffsets();
1388 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001389 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07001390 }
1391
1392 private int getLastTapPosition() {
1393 // No need to create the controller at that point, no last tap position saved
1394 if (mSelectionModifierCursorController != null) {
1395 int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
1396 if (lastTapPosition >= 0) {
1397 // Safety check, should not be possible.
1398 if (lastTapPosition > mTextView.getText().length()) {
1399 lastTapPosition = mTextView.getText().length();
1400 }
1401 return lastTapPosition;
1402 }
1403 }
1404
1405 return -1;
1406 }
1407
1408 void onWindowFocusChanged(boolean hasWindowFocus) {
1409 if (hasWindowFocus) {
1410 if (mBlink != null) {
1411 mBlink.uncancel();
1412 makeBlink();
1413 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001414 if (mTextView.hasSelection() && !extractedTextModeWillBeStarted()) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001415 refreshTextActionMode();
Mady Mellora2861452015-06-25 08:40:27 -07001416 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001417 } else {
1418 if (mBlink != null) {
1419 mBlink.cancel();
1420 }
1421 if (mInputContentType != null) {
1422 mInputContentType.enterDown = false;
1423 }
1424 // Order matters! Must be done before onParentLostFocus to rely on isShowingUp
Mady Mellora2861452015-06-25 08:40:27 -07001425 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001426 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -07001427 if (mSuggestionsPopupWindow != null) {
1428 mSuggestionsPopupWindow.onParentLostFocus();
1429 }
1430
Gilles Debunnec72fba82012-06-26 14:47:07 -07001431 // Don't leave us in the middle of a batch edit. Same as in onFocusChanged
1432 ensureEndedBatchEdit();
Richard Ledley5f2f8202018-02-05 14:55:47 +00001433
1434 ensureNoSelectionIfNonSelectable();
Gilles Debunned88876a2012-03-16 17:34:04 -07001435 }
1436 }
1437
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09001438 private void updateTapState(MotionEvent event) {
1439 final int action = event.getActionMasked();
1440 if (action == MotionEvent.ACTION_DOWN) {
1441 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
1442 // Detect double tap and triple click.
1443 if (((mTapState == TAP_STATE_FIRST_TAP)
1444 || ((mTapState == TAP_STATE_DOUBLE_TAP) && isMouse))
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001445 && (SystemClock.uptimeMillis() - mLastTouchUpTime)
1446 <= ViewConfiguration.getDoubleTapTimeout()) {
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09001447 if (mTapState == TAP_STATE_FIRST_TAP) {
1448 mTapState = TAP_STATE_DOUBLE_TAP;
1449 } else {
1450 mTapState = TAP_STATE_TRIPLE_CLICK;
1451 }
1452 } else {
1453 mTapState = TAP_STATE_FIRST_TAP;
1454 }
1455 }
1456 if (action == MotionEvent.ACTION_UP) {
1457 mLastTouchUpTime = SystemClock.uptimeMillis();
1458 }
1459 }
1460
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09001461 private boolean shouldFilterOutTouchEvent(MotionEvent event) {
1462 if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) {
1463 return false;
1464 }
1465 final boolean primaryButtonStateChanged =
1466 ((mLastButtonState ^ event.getButtonState()) & MotionEvent.BUTTON_PRIMARY) != 0;
1467 final int action = event.getActionMasked();
1468 if ((action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_UP)
1469 && !primaryButtonStateChanged) {
1470 return true;
1471 }
1472 if (action == MotionEvent.ACTION_MOVE
1473 && !event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
1474 return true;
1475 }
1476 return false;
1477 }
1478
Gilles Debunned88876a2012-03-16 17:34:04 -07001479 void onTouchEvent(MotionEvent event) {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09001480 final boolean filterOutEvent = shouldFilterOutTouchEvent(event);
1481 mLastButtonState = event.getButtonState();
1482 if (filterOutEvent) {
1483 if (event.getActionMasked() == MotionEvent.ACTION_UP) {
1484 mDiscardNextActionUp = true;
1485 }
1486 return;
1487 }
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09001488 updateTapState(event);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001489 updateFloatingToolbarVisibility(event);
1490
Gilles Debunned88876a2012-03-16 17:34:04 -07001491 if (hasSelectionController()) {
1492 getSelectionController().onTouchEvent(event);
1493 }
1494
1495 if (mShowSuggestionRunnable != null) {
1496 mTextView.removeCallbacks(mShowSuggestionRunnable);
1497 mShowSuggestionRunnable = null;
1498 }
1499
Petar Å egina91df3f92017-08-15 16:20:43 +01001500 if (event.getActionMasked() == MotionEvent.ACTION_UP) {
1501 mLastUpPositionX = event.getX();
1502 mLastUpPositionY = event.getY();
1503 }
1504
Gilles Debunned88876a2012-03-16 17:34:04 -07001505 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
1506 mLastDownPositionX = event.getX();
1507 mLastDownPositionY = event.getY();
1508
1509 // Reset this state; it will be re-set if super.onTouchEvent
1510 // causes focus to move to the view.
1511 mTouchFocusSelected = false;
1512 mIgnoreActionUpEvent = false;
1513 }
1514 }
1515
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001516 private void updateFloatingToolbarVisibility(MotionEvent event) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001517 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001518 switch (event.getActionMasked()) {
1519 case MotionEvent.ACTION_MOVE:
Abodunrinwa Toki4a7aeb32017-07-13 02:06:56 +01001520 hideFloatingToolbar(ActionMode.DEFAULT_HIDE_DURATION);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001521 break;
1522 case MotionEvent.ACTION_UP: // fall through
1523 case MotionEvent.ACTION_CANCEL:
1524 showFloatingToolbar();
1525 }
1526 }
1527 }
1528
Abodunrinwa Toki4a7aeb32017-07-13 02:06:56 +01001529 void hideFloatingToolbar(int duration) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001530 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001531 mTextView.removeCallbacks(mShowFloatingToolbar);
Abodunrinwa Toki4a7aeb32017-07-13 02:06:56 +01001532 mTextActionMode.hide(duration);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001533 }
1534 }
1535
1536 private void showFloatingToolbar() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001537 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001538 // Delay "show" so it doesn't interfere with click confirmations
1539 // or double-clicks that could "dismiss" the floating toolbar.
1540 int delay = ViewConfiguration.getDoubleTapTimeout();
1541 mTextView.postDelayed(mShowFloatingToolbar, delay);
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01001542
1543 // This classifies the text and most likely returns before the toolbar is actually
1544 // shown. If not, it will update the toolbar with the result when classification
1545 // returns. We would rather not wait for a long running classification process.
1546 invalidateActionModeAsync();
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001547 }
1548 }
1549
Yohei Yukawa484d4af2018-09-17 16:47:08 -07001550 private InputMethodManager getInputMethodManager() {
1551 return mTextView.getContext().getSystemService(InputMethodManager.class);
1552 }
1553
Gilles Debunned88876a2012-03-16 17:34:04 -07001554 public void beginBatchEdit() {
1555 mInBatchEditControllers = true;
1556 final InputMethodState ims = mInputMethodState;
1557 if (ims != null) {
1558 int nesting = ++ims.mBatchEditNesting;
1559 if (nesting == 1) {
1560 ims.mCursorChanged = false;
1561 ims.mChangedDelta = 0;
1562 if (ims.mContentChanged) {
1563 // We already have a pending change from somewhere else,
1564 // so turn this into a full update.
1565 ims.mChangedStart = 0;
1566 ims.mChangedEnd = mTextView.getText().length();
1567 } else {
1568 ims.mChangedStart = EXTRACT_UNKNOWN;
1569 ims.mChangedEnd = EXTRACT_UNKNOWN;
1570 ims.mContentChanged = false;
1571 }
James Cook48e0fac2015-02-25 15:44:51 -08001572 mUndoInputFilter.beginBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001573 mTextView.onBeginBatchEdit();
1574 }
1575 }
1576 }
1577
1578 public void endBatchEdit() {
1579 mInBatchEditControllers = false;
1580 final InputMethodState ims = mInputMethodState;
1581 if (ims != null) {
1582 int nesting = --ims.mBatchEditNesting;
1583 if (nesting == 0) {
1584 finishBatchEdit(ims);
1585 }
1586 }
1587 }
1588
1589 void ensureEndedBatchEdit() {
1590 final InputMethodState ims = mInputMethodState;
1591 if (ims != null && ims.mBatchEditNesting != 0) {
1592 ims.mBatchEditNesting = 0;
1593 finishBatchEdit(ims);
1594 }
1595 }
1596
1597 void finishBatchEdit(final InputMethodState ims) {
1598 mTextView.onEndBatchEdit();
James Cook48e0fac2015-02-25 15:44:51 -08001599 mUndoInputFilter.endBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001600
1601 if (ims.mContentChanged || ims.mSelectionModeChanged) {
1602 mTextView.updateAfterEdit();
1603 reportExtractedText();
1604 } else if (ims.mCursorChanged) {
Jean Chalardc99d33f2013-02-28 16:39:47 -08001605 // Cheesy way to get us to report the current cursor location.
Gilles Debunned88876a2012-03-16 17:34:04 -07001606 mTextView.invalidateCursor();
1607 }
Jean Chalardc99d33f2013-02-28 16:39:47 -08001608 // sendUpdateSelection knows to avoid sending if the selection did
1609 // not actually change.
1610 sendUpdateSelection();
Keisuke Kuroyanagic6fad962016-05-02 15:11:41 +09001611
1612 // Show drag handles if they were blocked by batch edit mode.
1613 if (mTextActionMode != null) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001614 final CursorController cursorController = mTextView.hasSelection()
1615 ? getSelectionController() : getInsertionController();
Keisuke Kuroyanagic6fad962016-05-02 15:11:41 +09001616 if (cursorController != null && !cursorController.isActive()
1617 && !cursorController.isCursorBeingModified()) {
1618 cursorController.show();
1619 }
1620 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001621 }
1622
1623 static final int EXTRACT_NOTHING = -2;
1624 static final int EXTRACT_UNKNOWN = -1;
1625
1626 boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
1627 return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
1628 EXTRACT_UNKNOWN, outText);
1629 }
1630
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001631 private boolean extractTextInternal(@Nullable ExtractedTextRequest request,
Gilles Debunned88876a2012-03-16 17:34:04 -07001632 int partialStartOffset, int partialEndOffset, int delta,
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001633 @Nullable ExtractedText outText) {
1634 if (request == null || outText == null) {
1635 return false;
Gilles Debunned88876a2012-03-16 17:34:04 -07001636 }
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001637
1638 final CharSequence content = mTextView.getText();
1639 if (content == null) {
1640 return false;
1641 }
1642
1643 if (partialStartOffset != EXTRACT_NOTHING) {
1644 final int N = content.length();
1645 if (partialStartOffset < 0) {
1646 outText.partialStartOffset = outText.partialEndOffset = -1;
1647 partialStartOffset = 0;
1648 partialEndOffset = N;
1649 } else {
1650 // Now use the delta to determine the actual amount of text
1651 // we need.
1652 partialEndOffset += delta;
1653 // Adjust offsets to ensure we contain full spans.
1654 if (content instanceof Spanned) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001655 Spanned spanned = (Spanned) content;
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001656 Object[] spans = spanned.getSpans(partialStartOffset,
1657 partialEndOffset, ParcelableSpan.class);
1658 int i = spans.length;
1659 while (i > 0) {
1660 i--;
1661 int j = spanned.getSpanStart(spans[i]);
1662 if (j < partialStartOffset) partialStartOffset = j;
1663 j = spanned.getSpanEnd(spans[i]);
1664 if (j > partialEndOffset) partialEndOffset = j;
1665 }
1666 }
1667 outText.partialStartOffset = partialStartOffset;
1668 outText.partialEndOffset = partialEndOffset - delta;
1669
1670 if (partialStartOffset > N) {
1671 partialStartOffset = N;
1672 } else if (partialStartOffset < 0) {
1673 partialStartOffset = 0;
1674 }
1675 if (partialEndOffset > N) {
1676 partialEndOffset = N;
1677 } else if (partialEndOffset < 0) {
1678 partialEndOffset = 0;
1679 }
1680 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001681 if ((request.flags & InputConnection.GET_TEXT_WITH_STYLES) != 0) {
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001682 outText.text = content.subSequence(partialStartOffset,
1683 partialEndOffset);
1684 } else {
1685 outText.text = TextUtils.substring(content, partialStartOffset,
1686 partialEndOffset);
1687 }
1688 } else {
1689 outText.partialStartOffset = 0;
1690 outText.partialEndOffset = 0;
1691 outText.text = "";
1692 }
1693 outText.flags = 0;
1694 if (MetaKeyKeyListener.getMetaState(content, MetaKeyKeyListener.META_SELECTING) != 0) {
1695 outText.flags |= ExtractedText.FLAG_SELECTING;
1696 }
1697 if (mTextView.isSingleLine()) {
1698 outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
1699 }
1700 outText.startOffset = 0;
1701 outText.selectionStart = mTextView.getSelectionStart();
1702 outText.selectionEnd = mTextView.getSelectionEnd();
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001703 outText.hint = mTextView.getHint();
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001704 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001705 }
1706
1707 boolean reportExtractedText() {
1708 final Editor.InputMethodState ims = mInputMethodState;
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001709 if (ims == null) {
1710 return false;
1711 }
Clara Bayarri038f7a82018-06-04 15:00:07 +01001712 final boolean wasContentChanged = ims.mContentChanged;
1713 if (!wasContentChanged && !ims.mSelectionModeChanged) {
1714 return false;
1715 }
1716 ims.mContentChanged = false;
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001717 ims.mSelectionModeChanged = false;
1718 final ExtractedTextRequest req = ims.mExtractedTextRequest;
1719 if (req == null) {
1720 return false;
1721 }
Yohei Yukawa484d4af2018-09-17 16:47:08 -07001722 final InputMethodManager imm = getInputMethodManager();
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001723 if (imm == null) {
1724 return false;
1725 }
1726 if (TextView.DEBUG_EXTRACT) {
1727 Log.v(TextView.LOG_TAG, "Retrieving extracted start="
1728 + ims.mChangedStart
1729 + " end=" + ims.mChangedEnd
1730 + " delta=" + ims.mChangedDelta);
1731 }
Clara Bayarri038f7a82018-06-04 15:00:07 +01001732 if (ims.mChangedStart < 0 && !wasContentChanged) {
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001733 ims.mChangedStart = EXTRACT_NOTHING;
1734 }
1735 if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
1736 ims.mChangedDelta, ims.mExtractedText)) {
1737 if (TextView.DEBUG_EXTRACT) {
1738 Log.v(TextView.LOG_TAG,
1739 "Reporting extracted start="
1740 + ims.mExtractedText.partialStartOffset
1741 + " end=" + ims.mExtractedText.partialEndOffset
1742 + ": " + ims.mExtractedText.text);
Gilles Debunned88876a2012-03-16 17:34:04 -07001743 }
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001744
1745 imm.updateExtractedText(mTextView, req.token, ims.mExtractedText);
1746 ims.mChangedStart = EXTRACT_UNKNOWN;
1747 ims.mChangedEnd = EXTRACT_UNKNOWN;
1748 ims.mChangedDelta = 0;
1749 ims.mContentChanged = false;
1750 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001751 }
1752 return false;
1753 }
1754
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001755 private void sendUpdateSelection() {
1756 if (null != mInputMethodState && mInputMethodState.mBatchEditNesting <= 0) {
Yohei Yukawa484d4af2018-09-17 16:47:08 -07001757 final InputMethodManager imm = getInputMethodManager();
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001758 if (null != imm) {
1759 final int selectionStart = mTextView.getSelectionStart();
1760 final int selectionEnd = mTextView.getSelectionEnd();
1761 int candStart = -1;
1762 int candEnd = -1;
1763 if (mTextView.getText() instanceof Spannable) {
1764 final Spannable sp = (Spannable) mTextView.getText();
1765 candStart = EditableInputConnection.getComposingSpanStart(sp);
1766 candEnd = EditableInputConnection.getComposingSpanEnd(sp);
1767 }
Jean Chalardc99d33f2013-02-28 16:39:47 -08001768 // InputMethodManager#updateSelection skips sending the message if
1769 // none of the parameters have changed since the last time we called it.
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001770 imm.updateSelection(mTextView,
1771 selectionStart, selectionEnd, candStart, candEnd);
1772 }
1773 }
1774 }
1775
Gilles Debunned88876a2012-03-16 17:34:04 -07001776 void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint,
1777 int cursorOffsetVertical) {
1778 final int selectionStart = mTextView.getSelectionStart();
1779 final int selectionEnd = mTextView.getSelectionEnd();
1780
1781 final InputMethodState ims = mInputMethodState;
1782 if (ims != null && ims.mBatchEditNesting == 0) {
Yohei Yukawa484d4af2018-09-17 16:47:08 -07001783 InputMethodManager imm = getInputMethodManager();
Gilles Debunned88876a2012-03-16 17:34:04 -07001784 if (imm != null) {
1785 if (imm.isActive(mTextView)) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001786 if (ims.mContentChanged || ims.mSelectionModeChanged) {
1787 // We are in extract mode and the content has changed
1788 // in some way... just report complete new text to the
1789 // input method.
Yohei Yukawab6bec1a2015-05-01 16:18:25 -07001790 reportExtractedText();
Gilles Debunned88876a2012-03-16 17:34:04 -07001791 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001792 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001793 }
1794 }
1795
1796 if (mCorrectionHighlighter != null) {
1797 mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
1798 }
1799
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07001800 if (highlight != null && selectionStart == selectionEnd && mDrawableForCursor != null) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001801 drawCursor(canvas, cursorOffsetVertical);
1802 // Rely on the drawable entirely, do not draw the cursor line.
1803 // Has to be done after the IMM related code above which relies on the highlight.
1804 highlight = null;
1805 }
1806
Jan Althaus80620c52018-02-02 17:39:22 +01001807 if (mSelectionActionModeHelper != null) {
1808 mSelectionActionModeHelper.onDraw(canvas);
1809 if (mSelectionActionModeHelper.isDrawingHighlight()) {
1810 highlight = null;
1811 }
1812 }
1813
Gilles Debunned88876a2012-03-16 17:34:04 -07001814 if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
1815 drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
1816 cursorOffsetVertical);
1817 } else {
1818 layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
1819 }
1820 }
1821
1822 private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
1823 Paint highlightPaint, int cursorOffsetVertical) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001824 final long lineRange = layout.getLineRangeForDraw(canvas);
1825 int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
1826 int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
1827 if (lastLine < 0) return;
1828
1829 layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
1830 firstLine, lastLine);
1831
1832 if (layout instanceof DynamicLayout) {
Chris Craik956f3402015-04-27 16:41:00 -07001833 if (mTextRenderNodes == null) {
1834 mTextRenderNodes = ArrayUtils.emptyArray(TextRenderNode.class);
Gilles Debunned88876a2012-03-16 17:34:04 -07001835 }
1836
1837 DynamicLayout dynamicLayout = (DynamicLayout) layout;
Gilles Debunne157aafc2012-04-19 17:21:57 -07001838 int[] blockEndLines = dynamicLayout.getBlockEndLines();
Gilles Debunned88876a2012-03-16 17:34:04 -07001839 int[] blockIndices = dynamicLayout.getBlockIndices();
1840 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
Sangkyu Lee955beb22012-12-10 15:47:00 +09001841 final int indexFirstChangedBlock = dynamicLayout.getIndexFirstChangedBlock();
Gilles Debunned88876a2012-03-16 17:34:04 -07001842
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001843 final ArraySet<Integer> blockSet = dynamicLayout.getBlocksAlwaysNeedToBeRedrawn();
1844 if (blockSet != null) {
1845 for (int i = 0; i < blockSet.size(); i++) {
1846 final int blockIndex = dynamicLayout.getBlockIndex(blockSet.valueAt(i));
1847 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
1848 && mTextRenderNodes[blockIndex] != null) {
1849 mTextRenderNodes[blockIndex].needsToBeShifted = true;
1850 }
1851 }
1852 }
1853
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001854 int startBlock = Arrays.binarySearch(blockEndLines, 0, numberOfBlocks, firstLine);
1855 if (startBlock < 0) {
1856 startBlock = -(startBlock + 1);
1857 }
1858 startBlock = Math.min(indexFirstChangedBlock, startBlock);
Gilles Debunned88876a2012-03-16 17:34:04 -07001859
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001860 int startIndexToFindAvailableRenderNode = 0;
1861 int lastIndex = numberOfBlocks;
1862
1863 for (int i = startBlock; i < numberOfBlocks; i++) {
1864 final int blockIndex = blockIndices[i];
1865 if (i >= indexFirstChangedBlock
1866 && blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
1867 && mTextRenderNodes[blockIndex] != null) {
1868 mTextRenderNodes[blockIndex].needsToBeShifted = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001869 }
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001870 if (blockEndLines[i] < firstLine) {
1871 // Blocks in [indexFirstChangedBlock, firstLine) are not redrawn here. They will
1872 // be redrawn after they get scrolled into drawing range.
1873 continue;
Gilles Debunned88876a2012-03-16 17:34:04 -07001874 }
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001875 startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas, layout,
1876 highlight, highlightPaint, cursorOffsetVertical, blockEndLines,
1877 blockIndices, i, numberOfBlocks, startIndexToFindAvailableRenderNode);
1878 if (blockEndLines[i] >= lastLine) {
1879 lastIndex = Math.max(indexFirstChangedBlock, i + 1);
1880 break;
Gilles Debunned88876a2012-03-16 17:34:04 -07001881 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001882 }
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001883 if (blockSet != null) {
1884 for (int i = 0; i < blockSet.size(); i++) {
1885 final int block = blockSet.valueAt(i);
1886 final int blockIndex = dynamicLayout.getBlockIndex(block);
1887 if (blockIndex == DynamicLayout.INVALID_BLOCK_INDEX
1888 || mTextRenderNodes[blockIndex] == null
1889 || mTextRenderNodes[blockIndex].needsToBeShifted) {
1890 startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas,
1891 layout, highlight, highlightPaint, cursorOffsetVertical,
1892 blockEndLines, blockIndices, block, numberOfBlocks,
1893 startIndexToFindAvailableRenderNode);
1894 }
1895 }
1896 }
Sangkyu Lee955beb22012-12-10 15:47:00 +09001897
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001898 dynamicLayout.setIndexFirstChangedBlock(lastIndex);
Gilles Debunned88876a2012-03-16 17:34:04 -07001899 } else {
1900 // Boring layout is used for empty and hint text
1901 layout.drawText(canvas, firstLine, lastLine);
1902 }
1903 }
1904
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001905 private int drawHardwareAcceleratedInner(Canvas canvas, Layout layout, Path highlight,
1906 Paint highlightPaint, int cursorOffsetVertical, int[] blockEndLines,
1907 int[] blockIndices, int blockInfoIndex, int numberOfBlocks,
1908 int startIndexToFindAvailableRenderNode) {
1909 final int blockEndLine = blockEndLines[blockInfoIndex];
1910 int blockIndex = blockIndices[blockInfoIndex];
1911
1912 final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
1913 if (blockIsInvalid) {
1914 blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
1915 startIndexToFindAvailableRenderNode);
1916 // Note how dynamic layout's internal block indices get updated from Editor
1917 blockIndices[blockInfoIndex] = blockIndex;
1918 if (mTextRenderNodes[blockIndex] != null) {
1919 mTextRenderNodes[blockIndex].isDirty = true;
1920 }
1921 startIndexToFindAvailableRenderNode = blockIndex + 1;
1922 }
1923
1924 if (mTextRenderNodes[blockIndex] == null) {
1925 mTextRenderNodes[blockIndex] = new TextRenderNode("Text " + blockIndex);
1926 }
1927
1928 final boolean blockDisplayListIsInvalid = mTextRenderNodes[blockIndex].needsRecord();
1929 RenderNode blockDisplayList = mTextRenderNodes[blockIndex].renderNode;
1930 if (mTextRenderNodes[blockIndex].needsToBeShifted || blockDisplayListIsInvalid) {
1931 final int blockBeginLine = blockInfoIndex == 0 ?
1932 0 : blockEndLines[blockInfoIndex - 1] + 1;
1933 final int top = layout.getLineTop(blockBeginLine);
1934 final int bottom = layout.getLineBottom(blockEndLine);
1935 int left = 0;
1936 int right = mTextView.getWidth();
1937 if (mTextView.getHorizontallyScrolling()) {
1938 float min = Float.MAX_VALUE;
1939 float max = Float.MIN_VALUE;
1940 for (int line = blockBeginLine; line <= blockEndLine; line++) {
1941 min = Math.min(min, layout.getLineLeft(line));
1942 max = Math.max(max, layout.getLineRight(line));
1943 }
1944 left = (int) min;
1945 right = (int) (max + 0.5f);
1946 }
1947
1948 // Rebuild display list if it is invalid
1949 if (blockDisplayListIsInvalid) {
John Reck32f140aa62018-10-04 15:08:24 -07001950 final RecordingCanvas recordingCanvas = blockDisplayList.start(
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001951 right - left, bottom - top);
1952 try {
1953 // drawText is always relative to TextView's origin, this translation
1954 // brings this range of text back to the top left corner of the viewport
John Reck32f140aa62018-10-04 15:08:24 -07001955 recordingCanvas.translate(-left, -top);
1956 layout.drawText(recordingCanvas, blockBeginLine, blockEndLine);
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001957 mTextRenderNodes[blockIndex].isDirty = false;
1958 // No need to untranslate, previous context is popped after
1959 // drawDisplayList
1960 } finally {
John Reck32f140aa62018-10-04 15:08:24 -07001961 blockDisplayList.end(recordingCanvas);
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001962 // Same as drawDisplayList below, handled by our TextView's parent
1963 blockDisplayList.setClipToBounds(false);
1964 }
1965 }
1966
1967 // Valid display list only needs to update its drawing location.
1968 blockDisplayList.setLeftTopRightBottom(left, top, right, bottom);
1969 mTextRenderNodes[blockIndex].needsToBeShifted = false;
1970 }
John Reck32f140aa62018-10-04 15:08:24 -07001971 ((RecordingCanvas) canvas).drawRenderNode(blockDisplayList);
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001972 return startIndexToFindAvailableRenderNode;
1973 }
1974
Gilles Debunned88876a2012-03-16 17:34:04 -07001975 private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
1976 int searchStartIndex) {
Chris Craik956f3402015-04-27 16:41:00 -07001977 int length = mTextRenderNodes.length;
Gilles Debunned88876a2012-03-16 17:34:04 -07001978 for (int i = searchStartIndex; i < length; i++) {
1979 boolean blockIndexFound = false;
1980 for (int j = 0; j < numberOfBlocks; j++) {
1981 if (blockIndices[j] == i) {
1982 blockIndexFound = true;
1983 break;
1984 }
1985 }
1986 if (blockIndexFound) continue;
1987 return i;
1988 }
1989
1990 // No available index found, the pool has to grow
Chris Craik956f3402015-04-27 16:41:00 -07001991 mTextRenderNodes = GrowingArrayUtils.append(mTextRenderNodes, length, null);
Gilles Debunned88876a2012-03-16 17:34:04 -07001992 return length;
1993 }
1994
1995 private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
1996 final boolean translate = cursorOffsetVertical != 0;
1997 if (translate) canvas.translate(0, cursorOffsetVertical);
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07001998 if (mDrawableForCursor != null) {
1999 mDrawableForCursor.draw(canvas);
Gilles Debunned88876a2012-03-16 17:34:04 -07002000 }
2001 if (translate) canvas.translate(0, -cursorOffsetVertical);
2002 }
2003
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09002004 void invalidateHandlesAndActionMode() {
2005 if (mSelectionModifierCursorController != null) {
2006 mSelectionModifierCursorController.invalidateHandles();
2007 }
2008 if (mInsertionPointCursorController != null) {
2009 mInsertionPointCursorController.invalidateHandle();
2010 }
2011 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002012 invalidateActionMode();
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09002013 }
2014 }
2015
Gilles Debunneebc86af2012-04-20 15:10:47 -07002016 /**
2017 * Invalidates all the sub-display lists that overlap the specified character range
2018 */
2019 void invalidateTextDisplayList(Layout layout, int start, int end) {
Chris Craik956f3402015-04-27 16:41:00 -07002020 if (mTextRenderNodes != null && layout instanceof DynamicLayout) {
Gilles Debunneebc86af2012-04-20 15:10:47 -07002021 final int firstLine = layout.getLineForOffset(start);
2022 final int lastLine = layout.getLineForOffset(end);
2023
2024 DynamicLayout dynamicLayout = (DynamicLayout) layout;
2025 int[] blockEndLines = dynamicLayout.getBlockEndLines();
2026 int[] blockIndices = dynamicLayout.getBlockIndices();
2027 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
2028
2029 int i = 0;
2030 // Skip the blocks before firstLine
2031 while (i < numberOfBlocks) {
2032 if (blockEndLines[i] >= firstLine) break;
2033 i++;
2034 }
2035
2036 // Invalidate all subsequent blocks until lastLine is passed
2037 while (i < numberOfBlocks) {
2038 final int blockIndex = blockIndices[i];
2039 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX) {
Chris Craik956f3402015-04-27 16:41:00 -07002040 mTextRenderNodes[blockIndex].isDirty = true;
Gilles Debunneebc86af2012-04-20 15:10:47 -07002041 }
2042 if (blockEndLines[i] >= lastLine) break;
2043 i++;
2044 }
2045 }
2046 }
2047
Mathew Inwood978c6e22018-08-21 15:58:55 +01002048 @UnsupportedAppUsage
Gilles Debunned88876a2012-03-16 17:34:04 -07002049 void invalidateTextDisplayList() {
Chris Craik956f3402015-04-27 16:41:00 -07002050 if (mTextRenderNodes != null) {
2051 for (int i = 0; i < mTextRenderNodes.length; i++) {
2052 if (mTextRenderNodes[i] != null) mTextRenderNodes[i].isDirty = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07002053 }
2054 }
2055 }
2056
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002057 void updateCursorPosition() {
Mihai Popa6c7ad1d2018-12-04 15:45:00 +00002058 loadCursorDrawable();
2059 if (mDrawableForCursor == null) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002060 return;
2061 }
2062
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002063 final Layout layout = mTextView.getLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -07002064 final int offset = mTextView.getSelectionStart();
2065 final int line = layout.getLineForOffset(offset);
2066 final int top = layout.getLineTop(line);
Siyamed Sinira60b59d2017-07-26 09:26:41 -07002067 final int bottom = layout.getLineBottomWithoutSpacing(line);
Gilles Debunned88876a2012-03-16 17:34:04 -07002068
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002069 final boolean clamped = layout.shouldClampCursor(line);
2070 updateCursorPosition(top, bottom, layout.getPrimaryHorizontal(offset, clamped));
Gilles Debunned88876a2012-03-16 17:34:04 -07002071 }
2072
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002073 void refreshTextActionMode() {
2074 if (extractedTextModeWillBeStarted()) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002075 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002076 return;
2077 }
2078 final boolean hasSelection = mTextView.hasSelection();
2079 final SelectionModifierCursorController selectionController = getSelectionController();
2080 final InsertionPointCursorController insertionController = getInsertionController();
2081 if ((selectionController != null && selectionController.isCursorBeingModified())
2082 || (insertionController != null && insertionController.isCursorBeingModified())) {
2083 // ActionMode should be managed by the currently active cursor controller.
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002084 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002085 return;
2086 }
2087 if (hasSelection) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002088 hideInsertionPointCursorController();
2089 if (mTextActionMode == null) {
Keisuke Kuroyanagi0fd28c92016-04-04 17:43:06 +09002090 if (mRestartActionModeOnNextRefresh) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002091 // To avoid distraction, newly start action mode only when selection action
Keisuke Kuroyanagi0fd28c92016-04-04 17:43:06 +09002092 // mode is being restarted.
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002093 startSelectionActionModeAsync(false);
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002094 }
2095 } else if (selectionController == null || !selectionController.isActive()) {
2096 // Insertion action mode is active. Avoid dismissing the selection.
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002097 stopTextActionModeWithPreservingSelection();
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002098 startSelectionActionModeAsync(false);
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002099 } else {
2100 mTextActionMode.invalidateContentRect();
2101 }
2102 } else {
2103 // Insertion action mode is started only when insertion controller is explicitly
2104 // activated.
2105 if (insertionController == null || !insertionController.isActive()) {
2106 stopTextActionMode();
2107 } else if (mTextActionMode != null) {
2108 mTextActionMode.invalidateContentRect();
2109 }
2110 }
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002111 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002112 }
2113
Gilles Debunned88876a2012-03-16 17:34:04 -07002114 /**
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002115 * Start an Insertion action mode.
Gilles Debunned88876a2012-03-16 17:34:04 -07002116 */
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002117 void startInsertionActionMode() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002118 if (mInsertionActionModeRunnable != null) {
2119 mTextView.removeCallbacks(mInsertionActionModeRunnable);
2120 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002121 if (extractedTextModeWillBeStarted()) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002122 return;
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002123 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002124 stopTextActionMode();
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002125
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002126 ActionMode.Callback actionModeCallback =
Richard Ledley26b87222017-11-30 10:54:08 +00002127 new TextActionModeCallback(TextActionMode.INSERTION);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002128 mTextActionMode = mTextView.startActionMode(
Clara Bayarrib8ed5b72015-04-09 15:26:41 +01002129 actionModeCallback, ActionMode.TYPE_FLOATING);
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002130 if (mTextActionMode != null && getInsertionController() != null) {
2131 getInsertionController().show();
2132 }
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00002133 }
2134
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002135 @NonNull
2136 TextView getTextView() {
2137 return mTextView;
2138 }
2139
2140 @Nullable
2141 ActionMode getTextActionMode() {
2142 return mTextActionMode;
2143 }
2144
2145 void setRestartActionModeOnNextRefresh(boolean value) {
2146 mRestartActionModeOnNextRefresh = value;
2147 }
2148
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00002149 /**
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002150 * Asynchronously starts a selection action mode using the TextClassifier.
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00002151 */
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002152 void startSelectionActionModeAsync(boolean adjustSelection) {
Richard Ledley26b87222017-11-30 10:54:08 +00002153 getSelectionActionModeHelper().startSelectionActionModeAsync(adjustSelection);
2154 }
2155
Richard Ledley27db81b2018-03-01 12:34:55 +00002156 void startLinkActionModeAsync(int start, int end) {
Richard Ledley26b87222017-11-30 10:54:08 +00002157 if (!(mTextView.getText() instanceof Spannable)) {
2158 return;
2159 }
Richard Ledley26b87222017-11-30 10:54:08 +00002160 stopTextActionMode();
Abodunrinwa Toki52096912018-03-21 23:14:42 +00002161 mRequestingLinkActionMode = true;
Richard Ledley27db81b2018-03-01 12:34:55 +00002162 getSelectionActionModeHelper().startLinkActionModeAsync(start, end);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002163 }
2164
2165 /**
2166 * Asynchronously invalidates an action mode using the TextClassifier.
2167 */
Abodunrinwa Toki4ce651e2017-05-12 15:37:29 +01002168 void invalidateActionModeAsync() {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002169 getSelectionActionModeHelper().invalidateActionModeAsync();
2170 }
2171
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002172 /**
2173 * Synchronously invalidates an action mode without the TextClassifier.
2174 */
2175 private void invalidateActionMode() {
2176 if (mTextActionMode != null) {
2177 mTextActionMode.invalidate();
2178 }
2179 }
2180
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002181 private SelectionActionModeHelper getSelectionActionModeHelper() {
2182 if (mSelectionActionModeHelper == null) {
2183 mSelectionActionModeHelper = new SelectionActionModeHelper(this);
Clara Bayarri578286f2015-04-10 15:35:31 +01002184 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002185 return mSelectionActionModeHelper;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00002186 }
2187
Clara Bayarridfac4432015-05-15 12:18:24 +01002188 /**
2189 * If the TextView allows text selection, selects the current word when no existing selection
2190 * was available and starts a drag.
2191 *
2192 * @return true if the drag was started.
2193 */
2194 private boolean selectCurrentWordAndStartDrag() {
Clara Bayarri7184c8a2015-06-05 17:34:09 +01002195 if (mInsertionActionModeRunnable != null) {
2196 mTextView.removeCallbacks(mInsertionActionModeRunnable);
2197 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002198 if (extractedTextModeWillBeStarted()) {
Clara Bayarridfac4432015-05-15 12:18:24 +01002199 return false;
2200 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002201 if (!checkField()) {
Clara Bayarridfac4432015-05-15 12:18:24 +01002202 return false;
2203 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002204 if (!mTextView.hasSelection() && !selectCurrentWord()) {
2205 // No selection and cannot select a word.
2206 return false;
2207 }
2208 stopTextActionModeWithPreservingSelection();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08002209 getSelectionController().enterDrag(
2210 SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_WORD);
Clara Bayarridfac4432015-05-15 12:18:24 +01002211 return true;
2212 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002213
Clara Bayarridfac4432015-05-15 12:18:24 +01002214 /**
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002215 * Checks whether a selection can be performed on the current TextView.
Clara Bayarridfac4432015-05-15 12:18:24 +01002216 *
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002217 * @return true if a selection can be performed
Clara Bayarridfac4432015-05-15 12:18:24 +01002218 */
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002219 boolean checkField() {
Clara Bayarridfac4432015-05-15 12:18:24 +01002220 if (!mTextView.canSelectText() || !mTextView.requestFocus()) {
2221 Log.w(TextView.LOG_TAG,
2222 "TextView does not support text selection. Selection cancelled.");
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002223 return false;
2224 }
Clara Bayarridfac4432015-05-15 12:18:24 +01002225 return true;
2226 }
2227
Richard Ledley26b87222017-11-30 10:54:08 +00002228 boolean startActionModeInternal(@TextActionMode int actionMode) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002229 if (extractedTextModeWillBeStarted()) {
2230 return false;
2231 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002232 if (mTextActionMode != null) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002233 // Text action mode is already started
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002234 invalidateActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07002235 return false;
2236 }
2237
Richard Ledley724eff92017-12-21 10:11:34 +00002238 if (actionMode != TextActionMode.TEXT_LINK
2239 && (!checkField() || !mTextView.hasSelection())) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002240 return false;
2241 }
2242
Richard Ledley26b87222017-11-30 10:54:08 +00002243 ActionMode.Callback actionModeCallback = new TextActionModeCallback(actionMode);
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002244 mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING);
Gilles Debunned88876a2012-03-16 17:34:04 -07002245
Abodunrinwa Toki29cb7682018-04-11 21:24:20 +01002246 final boolean selectableText = mTextView.isTextEditable() || mTextView.isTextSelectable();
2247 if (actionMode == TextActionMode.TEXT_LINK && !selectableText
2248 && mTextActionMode instanceof FloatingActionMode) {
2249 // Make the toolbar outside-touchable so that it can be dismissed when the user clicks
2250 // outside of it.
2251 ((FloatingActionMode) mTextActionMode).setOutsideTouchable(true,
2252 () -> stopTextActionMode());
2253 }
2254
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002255 final boolean selectionStarted = mTextActionMode != null;
Abodunrinwa Toki52096912018-03-21 23:14:42 +00002256 if (selectionStarted
2257 && mTextView.isTextEditable() && !mTextView.isTextSelectable()
2258 && mShowSoftInputOnFocus) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002259 // Show the IME to be able to replace text, except when selecting non editable text.
Yohei Yukawa484d4af2018-09-17 16:47:08 -07002260 final InputMethodManager imm = getInputMethodManager();
Gilles Debunned88876a2012-03-16 17:34:04 -07002261 if (imm != null) {
2262 imm.showSoftInput(mTextView, 0, null);
2263 }
2264 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002265 return selectionStarted;
2266 }
2267
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09002268 private boolean extractedTextModeWillBeStarted() {
Andrei Stingaceanub1891b32015-06-19 16:44:37 +01002269 if (!(mTextView.isInExtractedMode())) {
Yohei Yukawa484d4af2018-09-17 16:47:08 -07002270 final InputMethodManager imm = getInputMethodManager();
Gilles Debunned88876a2012-03-16 17:34:04 -07002271 return imm != null && imm.isFullscreenMode();
2272 }
2273 return false;
2274 }
2275
2276 /**
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002277 * @return <code>true</code> if it's reasonable to offer to show suggestions depending on
2278 * the current cursor position or selection range. This method is consistent with the
2279 * method to show suggestions {@link SuggestionsPopupWindow#updateSuggestions}.
Gilles Debunned88876a2012-03-16 17:34:04 -07002280 */
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002281 private boolean shouldOfferToShowSuggestions() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002282 CharSequence text = mTextView.getText();
2283 if (!(text instanceof Spannable)) return false;
2284
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002285 final Spannable spannable = (Spannable) text;
2286 final int selectionStart = mTextView.getSelectionStart();
2287 final int selectionEnd = mTextView.getSelectionEnd();
2288 final SuggestionSpan[] suggestionSpans = spannable.getSpans(selectionStart, selectionEnd,
2289 SuggestionSpan.class);
2290 if (suggestionSpans.length == 0) {
2291 return false;
2292 }
2293 if (selectionStart == selectionEnd) {
2294 // Spans overlap the cursor.
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002295 for (int i = 0; i < suggestionSpans.length; i++) {
2296 if (suggestionSpans[i].getSuggestions().length > 0) {
2297 return true;
2298 }
2299 }
2300 return false;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002301 }
2302 int minSpanStart = mTextView.getText().length();
2303 int maxSpanEnd = 0;
2304 int unionOfSpansCoveringSelectionStartStart = mTextView.getText().length();
2305 int unionOfSpansCoveringSelectionStartEnd = 0;
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002306 boolean hasValidSuggestions = false;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002307 for (int i = 0; i < suggestionSpans.length; i++) {
2308 final int spanStart = spannable.getSpanStart(suggestionSpans[i]);
2309 final int spanEnd = spannable.getSpanEnd(suggestionSpans[i]);
2310 minSpanStart = Math.min(minSpanStart, spanStart);
2311 maxSpanEnd = Math.max(maxSpanEnd, spanEnd);
2312 if (selectionStart < spanStart || selectionStart > spanEnd) {
2313 // The span doesn't cover the current selection start point.
2314 continue;
2315 }
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002316 hasValidSuggestions =
2317 hasValidSuggestions || suggestionSpans[i].getSuggestions().length > 0;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002318 unionOfSpansCoveringSelectionStartStart =
2319 Math.min(unionOfSpansCoveringSelectionStartStart, spanStart);
2320 unionOfSpansCoveringSelectionStartEnd =
2321 Math.max(unionOfSpansCoveringSelectionStartEnd, spanEnd);
2322 }
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002323 if (!hasValidSuggestions) {
2324 return false;
2325 }
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002326 if (unionOfSpansCoveringSelectionStartStart >= unionOfSpansCoveringSelectionStartEnd) {
2327 // No spans cover the selection start point.
2328 return false;
2329 }
2330 if (minSpanStart < unionOfSpansCoveringSelectionStartStart
2331 || maxSpanEnd > unionOfSpansCoveringSelectionStartEnd) {
2332 // There is a span that is not covered by the union. In this case, we soouldn't offer
2333 // to show suggestions as it's confusing.
2334 return false;
2335 }
2336 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07002337 }
2338
2339 /**
2340 * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
2341 * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
2342 */
2343 private boolean isCursorInsideEasyCorrectionSpan() {
2344 Spannable spannable = (Spannable) mTextView.getText();
2345 SuggestionSpan[] suggestionSpans = spannable.getSpans(mTextView.getSelectionStart(),
2346 mTextView.getSelectionEnd(), SuggestionSpan.class);
2347 for (int i = 0; i < suggestionSpans.length; i++) {
2348 if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
2349 return true;
2350 }
2351 }
2352 return false;
2353 }
2354
2355 void onTouchUpEvent(MotionEvent event) {
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +01002356 if (getSelectionActionModeHelper().resetSelection(
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +00002357 getTextView().getOffsetForPosition(event.getX(), event.getY()))) {
2358 return;
2359 }
2360
Gilles Debunned88876a2012-03-16 17:34:04 -07002361 boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect();
Mady Mellora2861452015-06-25 08:40:27 -07002362 hideCursorAndSpanControllers();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002363 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07002364 CharSequence text = mTextView.getText();
2365 if (!selectAllGotFocus && text.length() > 0) {
2366 // Move cursor
2367 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
Abodunrinwa Toki52096912018-03-21 23:14:42 +00002368
2369 final boolean shouldInsertCursor = !mRequestingLinkActionMode;
2370 if (shouldInsertCursor) {
2371 Selection.setSelection((Spannable) text, offset);
2372 if (mSpellChecker != null) {
2373 // When the cursor moves, the word that was typed may need spell check
2374 mSpellChecker.onSelectionChanged();
2375 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002376 }
Andrei Stingaceanu35c550c2015-05-07 16:49:49 +01002377
Gilles Debunned88876a2012-03-16 17:34:04 -07002378 if (!extractedTextModeWillBeStarted()) {
2379 if (isCursorInsideEasyCorrectionSpan()) {
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01002380 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002381 if (mInsertionActionModeRunnable != null) {
2382 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01002383 }
2384
Abodunrinwa Toki52096912018-03-21 23:14:42 +00002385 mShowSuggestionRunnable = this::replace;
2386
Gilles Debunned88876a2012-03-16 17:34:04 -07002387 // removeCallbacks is performed on every touch
2388 mTextView.postDelayed(mShowSuggestionRunnable,
2389 ViewConfiguration.getDoubleTapTimeout());
2390 } else if (hasInsertionController()) {
Abodunrinwa Toki52096912018-03-21 23:14:42 +00002391 if (shouldInsertCursor) {
2392 getInsertionController().show();
2393 } else {
2394 getInsertionController().hide();
2395 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002396 }
2397 }
2398 }
2399 }
2400
Yohei Yukawa401e3d42019-01-19 11:49:37 -08002401 /**
2402 * Called when {@link TextView#mTextOperationUser} has changed.
2403 *
2404 * <p>Any user-specific resources need to be refreshed here.</p>
2405 */
2406 final void onTextOperationUserChanged() {
2407 if (mSpellChecker != null) {
2408 mSpellChecker.resetSession();
2409 }
2410 }
2411
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002412 protected void stopTextActionMode() {
2413 if (mTextActionMode != null) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002414 // This will hide the mSelectionModifierCursorController
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002415 mTextActionMode.finish();
Gilles Debunned88876a2012-03-16 17:34:04 -07002416 }
2417 }
2418
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002419 private void stopTextActionModeWithPreservingSelection() {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002420 if (mTextActionMode != null) {
2421 mRestartActionModeOnNextRefresh = true;
2422 }
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002423 mPreserveSelection = true;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002424 stopTextActionMode();
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002425 mPreserveSelection = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002426 }
2427
Gilles Debunned88876a2012-03-16 17:34:04 -07002428 /**
2429 * @return True if this view supports insertion handles.
2430 */
2431 boolean hasInsertionController() {
2432 return mInsertionControllerEnabled;
2433 }
2434
2435 /**
2436 * @return True if this view supports selection handles.
2437 */
2438 boolean hasSelectionController() {
2439 return mSelectionControllerEnabled;
2440 }
2441
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09002442 private InsertionPointCursorController getInsertionController() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002443 if (!mInsertionControllerEnabled) {
2444 return null;
2445 }
2446
2447 if (mInsertionPointCursorController == null) {
2448 mInsertionPointCursorController = new InsertionPointCursorController();
2449
2450 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
2451 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
2452 }
2453
2454 return mInsertionPointCursorController;
2455 }
2456
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002457 @Nullable
2458 SelectionModifierCursorController getSelectionController() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002459 if (!mSelectionControllerEnabled) {
2460 return null;
2461 }
2462
2463 if (mSelectionModifierCursorController == null) {
2464 mSelectionModifierCursorController = new SelectionModifierCursorController();
2465
2466 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
2467 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
2468 }
2469
2470 return mSelectionModifierCursorController;
2471 }
2472
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002473 @VisibleForTesting
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002474 @Nullable
2475 public Drawable getCursorDrawable() {
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07002476 return mDrawableForCursor;
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002477 }
2478
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002479 private void updateCursorPosition(int top, int bottom, float horizontal) {
Mihai Popa6c7ad1d2018-12-04 15:45:00 +00002480 loadCursorDrawable();
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07002481 final int left = clampHorizontalPosition(mDrawableForCursor, horizontal);
2482 final int width = mDrawableForCursor.getIntrinsicWidth();
2483 mDrawableForCursor.setBounds(left, top - mTempRect.top, left + width,
Gilles Debunned88876a2012-03-16 17:34:04 -07002484 bottom + mTempRect.bottom);
2485 }
2486
2487 /**
Siyamed Sinir987ec652016-02-17 19:44:41 -08002488 * Return clamped position for the drawable. If the drawable is within the boundaries of the
2489 * view, then it is offset with the left padding of the cursor drawable. If the drawable is at
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002490 * the beginning or the end of the text then its drawable edge is aligned with left or right of
Siyamed Sinir987ec652016-02-17 19:44:41 -08002491 * the view boundary. If the drawable is null, horizontal parameter is aligned to left or right
2492 * of the view.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002493 *
Siyamed Sinir987ec652016-02-17 19:44:41 -08002494 * @param drawable Drawable. Can be null.
2495 * @param horizontal Horizontal position for the drawable.
2496 * @return The clamped horizontal position for the drawable.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002497 */
Siyamed Sinir987ec652016-02-17 19:44:41 -08002498 private int clampHorizontalPosition(@Nullable final Drawable drawable, float horizontal) {
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002499 horizontal = Math.max(0.5f, horizontal - 0.5f);
2500 if (mTempRect == null) mTempRect = new Rect();
Siyamed Sinir987ec652016-02-17 19:44:41 -08002501
2502 int drawableWidth = 0;
2503 if (drawable != null) {
2504 drawable.getPadding(mTempRect);
2505 drawableWidth = drawable.getIntrinsicWidth();
2506 } else {
2507 mTempRect.setEmpty();
2508 }
2509
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002510 int scrollX = mTextView.getScrollX();
2511 float horizontalDiff = horizontal - scrollX;
2512 int viewClippedWidth = mTextView.getWidth() - mTextView.getCompoundPaddingLeft()
2513 - mTextView.getCompoundPaddingRight();
2514
2515 final int left;
2516 if (horizontalDiff >= (viewClippedWidth - 1f)) {
2517 // at the rightmost position
Siyamed Sinir987ec652016-02-17 19:44:41 -08002518 left = viewClippedWidth + scrollX - (drawableWidth - mTempRect.right);
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002519 } else if (Math.abs(horizontalDiff) <= 1f
2520 || (TextUtils.isEmpty(mTextView.getText())
Siyamed Sinir987ec652016-02-17 19:44:41 -08002521 && (TextView.VERY_WIDE - scrollX) <= (viewClippedWidth + 1f)
2522 && horizontal <= 1f)) {
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002523 // at the leftmost position
2524 left = scrollX - mTempRect.left;
2525 } else {
2526 left = (int) horizontal - mTempRect.left;
2527 }
2528 return left;
2529 }
2530
2531 /**
Gilles Debunned88876a2012-03-16 17:34:04 -07002532 * Called by the framework in response to a text auto-correction (such as fixing a typo using a
James Cookf59152c2015-02-26 18:03:58 -08002533 * a dictionary) from the current input method, provided by it calling
Gilles Debunned88876a2012-03-16 17:34:04 -07002534 * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
2535 * implementation flashes the background of the corrected word to provide feedback to the user.
2536 *
2537 * @param info The auto correct info about the text that was corrected.
2538 */
2539 public void onCommitCorrection(CorrectionInfo info) {
2540 if (mCorrectionHighlighter == null) {
2541 mCorrectionHighlighter = new CorrectionHighlighter();
2542 } else {
2543 mCorrectionHighlighter.invalidate(false);
2544 }
2545
2546 mCorrectionHighlighter.highlight(info);
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002547 mUndoInputFilter.freezeLastEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07002548 }
2549
Gilles Debunned88876a2012-03-16 17:34:04 -07002550 void onScrollChanged() {
Gilles Debunne157aafc2012-04-19 17:21:57 -07002551 if (mPositionListener != null) {
2552 mPositionListener.onScrollChanged();
2553 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002554 if (mTextActionMode != null) {
2555 mTextActionMode.invalidateContentRect();
Abodunrinwa Toki56195db2015-04-22 06:46:54 +01002556 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002557 }
2558
2559 /**
2560 * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
2561 */
2562 private boolean shouldBlink() {
2563 if (!isCursorVisible() || !mTextView.isFocused()) return false;
2564
2565 final int start = mTextView.getSelectionStart();
2566 if (start < 0) return false;
2567
2568 final int end = mTextView.getSelectionEnd();
2569 if (end < 0) return false;
2570
2571 return start == end;
2572 }
2573
2574 void makeBlink() {
2575 if (shouldBlink()) {
2576 mShowCursor = SystemClock.uptimeMillis();
2577 if (mBlink == null) mBlink = new Blink();
John Reckd0374c62015-10-20 13:25:01 -07002578 mTextView.removeCallbacks(mBlink);
2579 mTextView.postDelayed(mBlink, BLINK);
Gilles Debunned88876a2012-03-16 17:34:04 -07002580 } else {
John Reckd0374c62015-10-20 13:25:01 -07002581 if (mBlink != null) mTextView.removeCallbacks(mBlink);
Gilles Debunned88876a2012-03-16 17:34:04 -07002582 }
2583 }
2584
John Reckd0374c62015-10-20 13:25:01 -07002585 private class Blink implements Runnable {
Gilles Debunned88876a2012-03-16 17:34:04 -07002586 private boolean mCancelled;
2587
2588 public void run() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002589 if (mCancelled) {
2590 return;
2591 }
2592
John Reckd0374c62015-10-20 13:25:01 -07002593 mTextView.removeCallbacks(this);
Gilles Debunned88876a2012-03-16 17:34:04 -07002594
2595 if (shouldBlink()) {
2596 if (mTextView.getLayout() != null) {
2597 mTextView.invalidateCursorPath();
2598 }
2599
John Reckd0374c62015-10-20 13:25:01 -07002600 mTextView.postDelayed(this, BLINK);
Gilles Debunned88876a2012-03-16 17:34:04 -07002601 }
2602 }
2603
2604 void cancel() {
2605 if (!mCancelled) {
John Reckd0374c62015-10-20 13:25:01 -07002606 mTextView.removeCallbacks(this);
Gilles Debunned88876a2012-03-16 17:34:04 -07002607 mCancelled = true;
2608 }
2609 }
2610
2611 void uncancel() {
2612 mCancelled = false;
2613 }
2614 }
2615
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002616 private DragShadowBuilder getTextThumbnailBuilder(int start, int end) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002617 TextView shadowView = (TextView) View.inflate(mTextView.getContext(),
2618 com.android.internal.R.layout.text_drag_thumbnail, null);
2619
2620 if (shadowView == null) {
2621 throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
2622 }
2623
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002624 if (end - start > DRAG_SHADOW_MAX_TEXT_LENGTH) {
2625 final long range = getCharClusterRange(start + DRAG_SHADOW_MAX_TEXT_LENGTH);
2626 end = TextUtils.unpackRangeEndFromLong(range);
Gilles Debunned88876a2012-03-16 17:34:04 -07002627 }
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002628 final CharSequence text = mTextView.getTransformedText(start, end);
Gilles Debunned88876a2012-03-16 17:34:04 -07002629 shadowView.setText(text);
2630 shadowView.setTextColor(mTextView.getTextColors());
2631
Alan Viverettebb98ebd2015-05-08 17:17:44 -07002632 shadowView.setTextAppearance(R.styleable.Theme_textAppearanceLarge);
Gilles Debunned88876a2012-03-16 17:34:04 -07002633 shadowView.setGravity(Gravity.CENTER);
2634
2635 shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
2636 ViewGroup.LayoutParams.WRAP_CONTENT));
2637
2638 final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
2639 shadowView.measure(size, size);
2640
2641 shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
2642 shadowView.invalidate();
2643 return new DragShadowBuilder(shadowView);
2644 }
2645
2646 private static class DragLocalState {
2647 public TextView sourceTextView;
2648 public int start, end;
2649
2650 public DragLocalState(TextView sourceTextView, int start, int end) {
2651 this.sourceTextView = sourceTextView;
2652 this.start = start;
2653 this.end = end;
2654 }
2655 }
2656
2657 void onDrop(DragEvent event) {
Ben Murdoch3dac4602017-01-17 11:27:37 +00002658 SpannableStringBuilder content = new SpannableStringBuilder();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002659
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -07002660 final DragAndDropPermissions permissions = DragAndDropPermissions.obtain(event);
2661 if (permissions != null) {
2662 permissions.takeTransient();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002663 }
2664
2665 try {
2666 ClipData clipData = event.getClipData();
2667 final int itemCount = clipData.getItemCount();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002668 for (int i = 0; i < itemCount; i++) {
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002669 Item item = clipData.getItemAt(i);
2670 content.append(item.coerceToStyledText(mTextView.getContext()));
2671 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002672 } finally {
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -07002673 if (permissions != null) {
2674 permissions.release();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002675 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002676 }
2677
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002678 mTextView.beginBatchEdit();
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002679 mUndoInputFilter.freezeLastEdit();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002680 try {
2681 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
2682 Object localState = event.getLocalState();
2683 DragLocalState dragLocalState = null;
2684 if (localState instanceof DragLocalState) {
2685 dragLocalState = (DragLocalState) localState;
Gilles Debunned88876a2012-03-16 17:34:04 -07002686 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002687 boolean dragDropIntoItself = dragLocalState != null
2688 && dragLocalState.sourceTextView == mTextView;
Gilles Debunned88876a2012-03-16 17:34:04 -07002689
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002690 if (dragDropIntoItself) {
2691 if (offset >= dragLocalState.start && offset < dragLocalState.end) {
2692 // A drop inside the original selection discards the drop.
2693 return;
2694 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002695 }
2696
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002697 final int originalLength = mTextView.getText().length();
2698 int min = offset;
2699 int max = offset;
2700
2701 Selection.setSelection((Spannable) mTextView.getText(), max);
2702 mTextView.replaceText_internal(min, max, content);
2703
2704 if (dragDropIntoItself) {
2705 int dragSourceStart = dragLocalState.start;
2706 int dragSourceEnd = dragLocalState.end;
2707 if (max <= dragSourceStart) {
2708 // Inserting text before selection has shifted positions
2709 final int shift = mTextView.getText().length() - originalLength;
2710 dragSourceStart += shift;
2711 dragSourceEnd += shift;
2712 }
2713
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08002714 // Delete original selection
2715 mTextView.deleteText_internal(dragSourceStart, dragSourceEnd);
Gilles Debunned88876a2012-03-16 17:34:04 -07002716
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08002717 // Make sure we do not leave two adjacent spaces.
2718 final int prevCharIdx = Math.max(0, dragSourceStart - 1);
2719 final int nextCharIdx = Math.min(mTextView.getText().length(), dragSourceStart + 1);
2720 if (nextCharIdx > prevCharIdx + 1) {
2721 CharSequence t = mTextView.getTransformedText(prevCharIdx, nextCharIdx);
2722 if (Character.isSpaceChar(t.charAt(0)) && Character.isSpaceChar(t.charAt(1))) {
2723 mTextView.deleteText_internal(prevCharIdx, prevCharIdx + 1);
2724 }
Victoria Lease91373202012-09-07 16:41:59 -07002725 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002726 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002727 } finally {
2728 mTextView.endBatchEdit();
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002729 mUndoInputFilter.freezeLastEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07002730 }
2731 }
2732
Gilles Debunnec62589c2012-04-12 14:50:23 -07002733 public void addSpanWatchers(Spannable text) {
2734 final int textLength = text.length();
2735
2736 if (mKeyListener != null) {
2737 text.setSpan(mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
2738 }
2739
Jean Chalardbaf30942013-02-28 16:01:51 -08002740 if (mSpanController == null) {
2741 mSpanController = new SpanController();
Gilles Debunnec62589c2012-04-12 14:50:23 -07002742 }
Jean Chalardbaf30942013-02-28 16:01:51 -08002743 text.setSpan(mSpanController, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002744 }
2745
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002746 void setContextMenuAnchor(float x, float y) {
2747 mContextMenuAnchorX = x;
2748 mContextMenuAnchorY = y;
2749 }
2750
2751 void onCreateContextMenu(ContextMenu menu) {
2752 if (mIsBeingLongClicked || Float.isNaN(mContextMenuAnchorX)
2753 || Float.isNaN(mContextMenuAnchorY)) {
2754 return;
2755 }
2756 final int offset = mTextView.getOffsetForPosition(mContextMenuAnchorX, mContextMenuAnchorY);
2757 if (offset == -1) {
2758 return;
2759 }
Siyamed Sinir532f3c92017-06-15 18:22:31 -07002760
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002761 stopTextActionModeWithPreservingSelection();
Siyamed Sinir532f3c92017-06-15 18:22:31 -07002762 if (mTextView.canSelectText()) {
2763 final boolean isOnSelection = mTextView.hasSelection()
2764 && offset >= mTextView.getSelectionStart()
2765 && offset <= mTextView.getSelectionEnd();
2766 if (!isOnSelection) {
2767 // Right clicked position is not on the selection. Remove the selection and move the
2768 // cursor to the right clicked position.
2769 Selection.setSelection((Spannable) mTextView.getText(), offset);
2770 stopTextActionMode();
2771 }
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002772 }
2773
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002774 if (shouldOfferToShowSuggestions()) {
Keisuke Kuroyanagi182f5fe2016-03-11 16:31:29 +09002775 final SuggestionInfo[] suggestionInfoArray =
2776 new SuggestionInfo[SuggestionSpan.SUGGESTIONS_MAX_SIZE];
2777 for (int i = 0; i < suggestionInfoArray.length; i++) {
2778 suggestionInfoArray[i] = new SuggestionInfo();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002779 }
2780 final SubMenu subMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, MENU_ITEM_ORDER_REPLACE,
2781 com.android.internal.R.string.replace);
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002782 final int numItems = mSuggestionHelper.getSuggestionInfo(suggestionInfoArray, null);
Keisuke Kuroyanagi182f5fe2016-03-11 16:31:29 +09002783 for (int i = 0; i < numItems; i++) {
2784 final SuggestionInfo info = suggestionInfoArray[i];
2785 subMenu.add(Menu.NONE, Menu.NONE, i, info.mText)
2786 .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
2787 @Override
2788 public boolean onMenuItemClick(MenuItem item) {
2789 replaceWithSuggestion(info);
2790 return true;
2791 }
2792 });
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002793 }
2794 }
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002795
2796 menu.add(Menu.NONE, TextView.ID_UNDO, MENU_ITEM_ORDER_UNDO,
2797 com.android.internal.R.string.undo)
2798 .setAlphabeticShortcut('z')
2799 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2800 .setEnabled(mTextView.canUndo());
2801 menu.add(Menu.NONE, TextView.ID_REDO, MENU_ITEM_ORDER_REDO,
2802 com.android.internal.R.string.redo)
2803 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2804 .setEnabled(mTextView.canRedo());
2805
2806 menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
2807 com.android.internal.R.string.cut)
2808 .setAlphabeticShortcut('x')
2809 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2810 .setEnabled(mTextView.canCut());
2811 menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
2812 com.android.internal.R.string.copy)
2813 .setAlphabeticShortcut('c')
2814 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2815 .setEnabled(mTextView.canCopy());
2816 menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
2817 com.android.internal.R.string.paste)
2818 .setAlphabeticShortcut('v')
2819 .setEnabled(mTextView.canPaste())
2820 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01002821 menu.add(Menu.NONE, TextView.ID_PASTE_AS_PLAIN_TEXT, MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002822 com.android.internal.R.string.paste_as_plain_text)
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01002823 .setEnabled(mTextView.canPasteAsPlainText())
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002824 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2825 menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
2826 com.android.internal.R.string.share)
2827 .setEnabled(mTextView.canShare())
2828 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2829 menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
2830 com.android.internal.R.string.selectAll)
2831 .setAlphabeticShortcut('a')
2832 .setEnabled(mTextView.canSelectAllText())
2833 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Felipe Leme2ac463e2017-03-13 14:06:25 -07002834 menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
Felipe Leme555bcac2017-06-26 12:53:56 -07002835 android.R.string.autofill)
Felipe Leme2ac463e2017-03-13 14:06:25 -07002836 .setEnabled(mTextView.canRequestAutofill())
2837 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002838
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002839 mPreserveSelection = true;
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002840 }
2841
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002842 @Nullable
2843 private SuggestionSpan findEquivalentSuggestionSpan(
2844 @NonNull SuggestionSpanInfo suggestionSpanInfo) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002845 final Editable editable = (Editable) mTextView.getText();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002846 if (editable.getSpanStart(suggestionSpanInfo.mSuggestionSpan) >= 0) {
2847 // Exactly same span is found.
2848 return suggestionSpanInfo.mSuggestionSpan;
2849 }
2850 // Suggestion span couldn't be found. Try to find a suggestion span that has the same
2851 // contents.
2852 final SuggestionSpan[] suggestionSpans = editable.getSpans(suggestionSpanInfo.mSpanStart,
2853 suggestionSpanInfo.mSpanEnd, SuggestionSpan.class);
2854 for (final SuggestionSpan suggestionSpan : suggestionSpans) {
2855 final int start = editable.getSpanStart(suggestionSpan);
2856 if (start != suggestionSpanInfo.mSpanStart) {
2857 continue;
2858 }
2859 final int end = editable.getSpanEnd(suggestionSpan);
2860 if (end != suggestionSpanInfo.mSpanEnd) {
2861 continue;
2862 }
2863 if (suggestionSpan.equals(suggestionSpanInfo.mSuggestionSpan)) {
2864 return suggestionSpan;
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08002865 }
2866 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002867 return null;
2868 }
2869
2870 private void replaceWithSuggestion(@NonNull final SuggestionInfo suggestionInfo) {
2871 final SuggestionSpan targetSuggestionSpan = findEquivalentSuggestionSpan(
2872 suggestionInfo.mSuggestionSpanInfo);
2873 if (targetSuggestionSpan == null) {
2874 // Span has been removed
2875 return;
2876 }
2877 final Editable editable = (Editable) mTextView.getText();
2878 final int spanStart = editable.getSpanStart(targetSuggestionSpan);
2879 final int spanEnd = editable.getSpanEnd(targetSuggestionSpan);
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08002880 if (spanStart < 0 || spanEnd <= spanStart) {
2881 // Span has been removed
2882 return;
2883 }
2884
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002885 final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
2886 // SuggestionSpans are removed by replace: save them before
2887 SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
2888 SuggestionSpan.class);
2889 final int length = suggestionSpans.length;
2890 int[] suggestionSpansStarts = new int[length];
2891 int[] suggestionSpansEnds = new int[length];
2892 int[] suggestionSpansFlags = new int[length];
2893 for (int i = 0; i < length; i++) {
2894 final SuggestionSpan suggestionSpan = suggestionSpans[i];
2895 suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
2896 suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
2897 suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
2898
2899 // Remove potential misspelled flags
2900 int suggestionSpanFlags = suggestionSpan.getFlags();
2901 if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
2902 suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
2903 suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
2904 suggestionSpan.setFlags(suggestionSpanFlags);
2905 }
2906 }
2907
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002908 // Swap text content between actual text and Suggestion span
2909 final int suggestionStart = suggestionInfo.mSuggestionStart;
2910 final int suggestionEnd = suggestionInfo.mSuggestionEnd;
2911 final String suggestion = suggestionInfo.mText.subSequence(
2912 suggestionStart, suggestionEnd).toString();
2913 mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
2914
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002915 String[] suggestions = targetSuggestionSpan.getSuggestions();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002916 suggestions[suggestionInfo.mSuggestionIndex] = originalText;
2917
2918 // Restore previous SuggestionSpans
2919 final int lengthDelta = suggestion.length() - (spanEnd - spanStart);
2920 for (int i = 0; i < length; i++) {
2921 // Only spans that include the modified region make sense after replacement
2922 // Spans partially included in the replaced region are removed, there is no
2923 // way to assign them a valid range after replacement
2924 if (suggestionSpansStarts[i] <= spanStart && suggestionSpansEnds[i] >= spanEnd) {
2925 mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
2926 suggestionSpansEnds[i] + lengthDelta, suggestionSpansFlags[i]);
2927 }
2928 }
2929 // Move cursor at the end of the replaced word
2930 final int newCursorPosition = spanEnd + lengthDelta;
2931 mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
2932 }
2933
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002934 private final MenuItem.OnMenuItemClickListener mOnContextMenuItemClickListener =
2935 new MenuItem.OnMenuItemClickListener() {
2936 @Override
2937 public boolean onMenuItemClick(MenuItem item) {
2938 if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
2939 return true;
2940 }
2941 return mTextView.onTextContextMenuItem(item.getItemId());
2942 }
2943 };
2944
Gilles Debunned88876a2012-03-16 17:34:04 -07002945 /**
2946 * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
2947 * pop-up should be displayed.
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07002948 * Also monitors {@link Selection} to call back to the attached input method.
Gilles Debunned88876a2012-03-16 17:34:04 -07002949 */
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09002950 private class SpanController implements SpanWatcher {
Gilles Debunned88876a2012-03-16 17:34:04 -07002951
2952 private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
2953
2954 private EasyEditPopupWindow mPopupWindow;
2955
Gilles Debunned88876a2012-03-16 17:34:04 -07002956 private Runnable mHidePopup;
2957
Jean Chalardbaf30942013-02-28 16:01:51 -08002958 // This function is pure but inner classes can't have static functions
2959 private boolean isNonIntermediateSelectionSpan(final Spannable text,
2960 final Object span) {
2961 return (Selection.SELECTION_START == span || Selection.SELECTION_END == span)
2962 && (text.getSpanFlags(span) & Spanned.SPAN_INTERMEDIATE) == 0;
2963 }
2964
Gilles Debunnec62589c2012-04-12 14:50:23 -07002965 @Override
2966 public void onSpanAdded(Spannable text, Object span, int start, int end) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002967 if (isNonIntermediateSelectionSpan(text, span)) {
2968 sendUpdateSelection();
2969 } else if (span instanceof EasyEditSpan) {
Gilles Debunnec62589c2012-04-12 14:50:23 -07002970 if (mPopupWindow == null) {
2971 mPopupWindow = new EasyEditPopupWindow();
2972 mHidePopup = new Runnable() {
2973 @Override
2974 public void run() {
2975 hide();
2976 }
2977 };
2978 }
2979
2980 // Make sure there is only at most one EasyEditSpan in the text
2981 if (mPopupWindow.mEasyEditSpan != null) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002982 mPopupWindow.mEasyEditSpan.setDeleteEnabled(false);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002983 }
2984
2985 mPopupWindow.setEasyEditSpan((EasyEditSpan) span);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002986 mPopupWindow.setOnDeleteListener(new EasyEditDeleteListener() {
2987 @Override
2988 public void onDeleteClick(EasyEditSpan span) {
2989 Editable editable = (Editable) mTextView.getText();
2990 int start = editable.getSpanStart(span);
2991 int end = editable.getSpanEnd(span);
2992 if (start >= 0 && end >= 0) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002993 sendEasySpanNotification(EasyEditSpan.TEXT_DELETED, span);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002994 mTextView.deleteText_internal(start, end);
2995 }
2996 editable.removeSpan(span);
2997 }
2998 });
Gilles Debunnec62589c2012-04-12 14:50:23 -07002999
3000 if (mTextView.getWindowVisibility() != View.VISIBLE) {
3001 // The window is not visible yet, ignore the text change.
3002 return;
3003 }
3004
3005 if (mTextView.getLayout() == null) {
3006 // The view has not been laid out yet, ignore the text change
3007 return;
3008 }
3009
3010 if (extractedTextModeWillBeStarted()) {
3011 // The input is in extract mode. Do not handle the easy edit in
3012 // the original TextView, as the ExtractEditText will do
3013 return;
3014 }
3015
3016 mPopupWindow.show();
3017 mTextView.removeCallbacks(mHidePopup);
3018 mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
3019 }
3020 }
3021
3022 @Override
3023 public void onSpanRemoved(Spannable text, Object span, int start, int end) {
Jean Chalardbaf30942013-02-28 16:01:51 -08003024 if (isNonIntermediateSelectionSpan(text, span)) {
3025 sendUpdateSelection();
3026 } else if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
Gilles Debunnec62589c2012-04-12 14:50:23 -07003027 hide();
3028 }
3029 }
3030
3031 @Override
3032 public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd,
3033 int newStart, int newEnd) {
Jean Chalardbaf30942013-02-28 16:01:51 -08003034 if (isNonIntermediateSelectionSpan(text, span)) {
3035 sendUpdateSelection();
3036 } else if (mPopupWindow != null && span instanceof EasyEditSpan) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003037 EasyEditSpan easyEditSpan = (EasyEditSpan) span;
Jean Chalardbaf30942013-02-28 16:01:51 -08003038 sendEasySpanNotification(EasyEditSpan.TEXT_MODIFIED, easyEditSpan);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003039 text.removeSpan(easyEditSpan);
Gilles Debunnec62589c2012-04-12 14:50:23 -07003040 }
3041 }
3042
Gilles Debunned88876a2012-03-16 17:34:04 -07003043 public void hide() {
3044 if (mPopupWindow != null) {
3045 mPopupWindow.hide();
3046 mTextView.removeCallbacks(mHidePopup);
3047 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003048 }
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003049
Jean Chalardbaf30942013-02-28 16:01:51 -08003050 private void sendEasySpanNotification(int textChangedType, EasyEditSpan span) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003051 try {
3052 PendingIntent pendingIntent = span.getPendingIntent();
3053 if (pendingIntent != null) {
3054 Intent intent = new Intent();
3055 intent.putExtra(EasyEditSpan.EXTRA_TEXT_CHANGED_TYPE, textChangedType);
3056 pendingIntent.send(mTextView.getContext(), 0, intent);
3057 }
3058 } catch (CanceledException e) {
3059 // This should not happen, as we should try to send the intent only once.
3060 Log.w(TAG, "PendingIntent for notification cannot be sent", e);
3061 }
3062 }
3063 }
3064
3065 /**
3066 * Listens for the delete event triggered by {@link EasyEditPopupWindow}.
3067 */
3068 private interface EasyEditDeleteListener {
3069
3070 /**
3071 * Clicks the delete pop-up.
3072 */
3073 void onDeleteClick(EasyEditSpan span);
Gilles Debunned88876a2012-03-16 17:34:04 -07003074 }
3075
3076 /**
3077 * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07003078 * by {@link SpanController}.
Gilles Debunned88876a2012-03-16 17:34:04 -07003079 */
3080 private class EasyEditPopupWindow extends PinnedPopupWindow
3081 implements OnClickListener {
3082 private static final int POPUP_TEXT_LAYOUT =
3083 com.android.internal.R.layout.text_edit_action_popup_text;
3084 private TextView mDeleteTextView;
3085 private EasyEditSpan mEasyEditSpan;
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003086 private EasyEditDeleteListener mOnDeleteListener;
Gilles Debunned88876a2012-03-16 17:34:04 -07003087
3088 @Override
3089 protected void createPopupWindow() {
3090 mPopupWindow = new PopupWindow(mTextView.getContext(), null,
3091 com.android.internal.R.attr.textSelectHandleWindowStyle);
3092 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
3093 mPopupWindow.setClippingEnabled(true);
3094 }
3095
3096 @Override
3097 protected void initContentView() {
3098 LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
3099 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
3100 mContentView = linearLayout;
3101 mContentView.setBackgroundResource(
3102 com.android.internal.R.drawable.text_edit_side_paste_window);
3103
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003104 LayoutInflater inflater = (LayoutInflater) mTextView.getContext()
3105 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003106
3107 LayoutParams wrapContent = new LayoutParams(
3108 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
3109
3110 mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
3111 mDeleteTextView.setLayoutParams(wrapContent);
3112 mDeleteTextView.setText(com.android.internal.R.string.delete);
3113 mDeleteTextView.setOnClickListener(this);
3114 mContentView.addView(mDeleteTextView);
3115 }
3116
Gilles Debunnec62589c2012-04-12 14:50:23 -07003117 public void setEasyEditSpan(EasyEditSpan easyEditSpan) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003118 mEasyEditSpan = easyEditSpan;
Gilles Debunned88876a2012-03-16 17:34:04 -07003119 }
3120
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003121 private void setOnDeleteListener(EasyEditDeleteListener listener) {
3122 mOnDeleteListener = listener;
3123 }
3124
Gilles Debunned88876a2012-03-16 17:34:04 -07003125 @Override
3126 public void onClick(View view) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003127 if (view == mDeleteTextView
3128 && mEasyEditSpan != null && mEasyEditSpan.isDeleteEnabled()
3129 && mOnDeleteListener != null) {
3130 mOnDeleteListener.onDeleteClick(mEasyEditSpan);
Gilles Debunned88876a2012-03-16 17:34:04 -07003131 }
3132 }
3133
3134 @Override
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003135 public void hide() {
3136 if (mEasyEditSpan != null) {
3137 mEasyEditSpan.setDeleteEnabled(false);
3138 }
3139 mOnDeleteListener = null;
3140 super.hide();
3141 }
3142
3143 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07003144 protected int getTextOffset() {
3145 // Place the pop-up at the end of the span
3146 Editable editable = (Editable) mTextView.getText();
3147 return editable.getSpanEnd(mEasyEditSpan);
3148 }
3149
3150 @Override
3151 protected int getVerticalLocalPosition(int line) {
Siyamed Sinira60b59d2017-07-26 09:26:41 -07003152 final Layout layout = mTextView.getLayout();
3153 return layout.getLineBottomWithoutSpacing(line);
Gilles Debunned88876a2012-03-16 17:34:04 -07003154 }
3155
3156 @Override
3157 protected int clipVertically(int positionY) {
3158 // As we display the pop-up below the span, no vertical clipping is required.
3159 return positionY;
3160 }
3161 }
3162
3163 private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
3164 // 3 handles
3165 // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003166 // 1 CursorAnchorInfoNotifier
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003167 private static final int MAXIMUM_NUMBER_OF_LISTENERS = 7;
Gilles Debunned88876a2012-03-16 17:34:04 -07003168 private TextViewPositionListener[] mPositionListeners =
3169 new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003170 private boolean[] mCanMove = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
Gilles Debunned88876a2012-03-16 17:34:04 -07003171 private boolean mPositionHasChanged = true;
3172 // Absolute position of the TextView with respect to its parent window
3173 private int mPositionX, mPositionY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003174 private int mPositionXOnScreen, mPositionYOnScreen;
Gilles Debunned88876a2012-03-16 17:34:04 -07003175 private int mNumberOfListeners;
3176 private boolean mScrollHasChanged;
3177 final int[] mTempCoords = new int[2];
3178
3179 public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
3180 if (mNumberOfListeners == 0) {
3181 updatePosition();
3182 ViewTreeObserver vto = mTextView.getViewTreeObserver();
3183 vto.addOnPreDrawListener(this);
3184 }
3185
3186 int emptySlotIndex = -1;
3187 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3188 TextViewPositionListener listener = mPositionListeners[i];
3189 if (listener == positionListener) {
3190 return;
3191 } else if (emptySlotIndex < 0 && listener == null) {
3192 emptySlotIndex = i;
3193 }
3194 }
3195
3196 mPositionListeners[emptySlotIndex] = positionListener;
3197 mCanMove[emptySlotIndex] = canMove;
3198 mNumberOfListeners++;
3199 }
3200
3201 public void removeSubscriber(TextViewPositionListener positionListener) {
3202 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3203 if (mPositionListeners[i] == positionListener) {
3204 mPositionListeners[i] = null;
3205 mNumberOfListeners--;
3206 break;
3207 }
3208 }
3209
3210 if (mNumberOfListeners == 0) {
3211 ViewTreeObserver vto = mTextView.getViewTreeObserver();
3212 vto.removeOnPreDrawListener(this);
3213 }
3214 }
3215
3216 public int getPositionX() {
3217 return mPositionX;
3218 }
3219
3220 public int getPositionY() {
3221 return mPositionY;
3222 }
3223
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003224 public int getPositionXOnScreen() {
3225 return mPositionXOnScreen;
3226 }
3227
3228 public int getPositionYOnScreen() {
3229 return mPositionYOnScreen;
3230 }
3231
Gilles Debunned88876a2012-03-16 17:34:04 -07003232 @Override
3233 public boolean onPreDraw() {
3234 updatePosition();
3235
3236 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3237 if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
3238 TextViewPositionListener positionListener = mPositionListeners[i];
3239 if (positionListener != null) {
3240 positionListener.updatePosition(mPositionX, mPositionY,
3241 mPositionHasChanged, mScrollHasChanged);
3242 }
3243 }
3244 }
3245
3246 mScrollHasChanged = false;
3247 return true;
3248 }
3249
3250 private void updatePosition() {
3251 mTextView.getLocationInWindow(mTempCoords);
3252
3253 mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
3254
3255 mPositionX = mTempCoords[0];
3256 mPositionY = mTempCoords[1];
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003257
3258 mTextView.getLocationOnScreen(mTempCoords);
3259
3260 mPositionXOnScreen = mTempCoords[0];
3261 mPositionYOnScreen = mTempCoords[1];
Gilles Debunned88876a2012-03-16 17:34:04 -07003262 }
3263
3264 public void onScrollChanged() {
3265 mScrollHasChanged = true;
3266 }
3267 }
3268
3269 private abstract class PinnedPopupWindow implements TextViewPositionListener {
3270 protected PopupWindow mPopupWindow;
3271 protected ViewGroup mContentView;
3272 int mPositionX, mPositionY;
Seigo Nonaka60490d12016-01-28 17:25:18 +09003273 int mClippingLimitLeft, mClippingLimitRight;
Gilles Debunned88876a2012-03-16 17:34:04 -07003274
3275 protected abstract void createPopupWindow();
3276 protected abstract void initContentView();
3277 protected abstract int getTextOffset();
3278 protected abstract int getVerticalLocalPosition(int line);
3279 protected abstract int clipVertically(int positionY);
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003280 protected void setUp() {
3281 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003282
3283 public PinnedPopupWindow() {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003284 // Due to calling subclass methods in base constructor, subclass constructor is not
3285 // called before subclass methods, e.g. createPopupWindow or initContentView. To give
3286 // a chance to initialize subclasses, call setUp() method here.
3287 // TODO: It is good to extract non trivial initialization code from constructor.
3288 setUp();
3289
Gilles Debunned88876a2012-03-16 17:34:04 -07003290 createPopupWindow();
3291
Alan Viverette80ebe0d2015-04-30 15:53:11 -07003292 mPopupWindow.setWindowLayoutType(
3293 WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
Gilles Debunned88876a2012-03-16 17:34:04 -07003294 mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
3295 mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
3296
3297 initContentView();
3298
3299 LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
3300 ViewGroup.LayoutParams.WRAP_CONTENT);
3301 mContentView.setLayoutParams(wrapContent);
3302
3303 mPopupWindow.setContentView(mContentView);
3304 }
3305
3306 public void show() {
3307 getPositionListener().addSubscriber(this, false /* offset is fixed */);
3308
3309 computeLocalPosition();
3310
3311 final PositionListener positionListener = getPositionListener();
3312 updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
3313 }
3314
3315 protected void measureContent() {
3316 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3317 mContentView.measure(
3318 View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
3319 View.MeasureSpec.AT_MOST),
3320 View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
3321 View.MeasureSpec.AT_MOST));
3322 }
3323
3324 /* The popup window will be horizontally centered on the getTextOffset() and vertically
3325 * positioned according to viewportToContentHorizontalOffset.
3326 *
3327 * This method assumes that mContentView has properly been measured from its content. */
3328 private void computeLocalPosition() {
3329 measureContent();
3330 final int width = mContentView.getMeasuredWidth();
3331 final int offset = getTextOffset();
3332 mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f);
3333 mPositionX += mTextView.viewportToContentHorizontalOffset();
3334
3335 final int line = mTextView.getLayout().getLineForOffset(offset);
3336 mPositionY = getVerticalLocalPosition(line);
3337 mPositionY += mTextView.viewportToContentVerticalOffset();
3338 }
3339
3340 private void updatePosition(int parentPositionX, int parentPositionY) {
3341 int positionX = parentPositionX + mPositionX;
3342 int positionY = parentPositionY + mPositionY;
3343
3344 positionY = clipVertically(positionY);
3345
3346 // Horizontal clipping
3347 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3348 final int width = mContentView.getMeasuredWidth();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003349 positionX = Math.min(
3350 displayMetrics.widthPixels - width + mClippingLimitRight, positionX);
3351 positionX = Math.max(-mClippingLimitLeft, positionX);
Gilles Debunned88876a2012-03-16 17:34:04 -07003352
3353 if (isShowing()) {
3354 mPopupWindow.update(positionX, positionY, -1, -1);
3355 } else {
3356 mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
3357 positionX, positionY);
3358 }
3359 }
3360
3361 public void hide() {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09003362 if (!isShowing()) {
3363 return;
3364 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003365 mPopupWindow.dismiss();
3366 getPositionListener().removeSubscriber(this);
3367 }
3368
3369 @Override
3370 public void updatePosition(int parentPositionX, int parentPositionY,
3371 boolean parentPositionChanged, boolean parentScrolled) {
3372 // Either parentPositionChanged or parentScrolled is true, check if still visible
3373 if (isShowing() && isOffsetVisible(getTextOffset())) {
3374 if (parentScrolled) computeLocalPosition();
3375 updatePosition(parentPositionX, parentPositionY);
3376 } else {
3377 hide();
3378 }
3379 }
3380
3381 public boolean isShowing() {
3382 return mPopupWindow.isShowing();
3383 }
3384 }
3385
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003386 private static final class SuggestionInfo {
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003387 // Range of actual suggestion within mText
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003388 int mSuggestionStart, mSuggestionEnd;
3389
3390 // The SuggestionSpan that this TextView represents
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003391 final SuggestionSpanInfo mSuggestionSpanInfo = new SuggestionSpanInfo();
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003392
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003393 // The index of this suggestion inside suggestionSpan
3394 int mSuggestionIndex;
3395
3396 final SpannableStringBuilder mText = new SpannableStringBuilder();
3397
3398 void clear() {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003399 mSuggestionSpanInfo.clear();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003400 mText.clear();
3401 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003402
3403 // Utility method to set attributes about a SuggestionSpan.
3404 void setSpanInfo(SuggestionSpan span, int spanStart, int spanEnd) {
3405 mSuggestionSpanInfo.mSuggestionSpan = span;
3406 mSuggestionSpanInfo.mSpanStart = spanStart;
3407 mSuggestionSpanInfo.mSpanEnd = spanEnd;
3408 }
3409 }
3410
3411 private static final class SuggestionSpanInfo {
3412 // The SuggestionSpan;
3413 @Nullable
3414 SuggestionSpan mSuggestionSpan;
3415
3416 // The SuggestionSpan start position
3417 int mSpanStart;
3418
3419 // The SuggestionSpan end position
3420 int mSpanEnd;
3421
3422 void clear() {
3423 mSuggestionSpan = null;
3424 }
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003425 }
3426
3427 private class SuggestionHelper {
3428 private final Comparator<SuggestionSpan> mSuggestionSpanComparator =
3429 new SuggestionSpanComparator();
3430 private final HashMap<SuggestionSpan, Integer> mSpansLengths =
3431 new HashMap<SuggestionSpan, Integer>();
3432
3433 private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
3434 public int compare(SuggestionSpan span1, SuggestionSpan span2) {
3435 final int flag1 = span1.getFlags();
3436 final int flag2 = span2.getFlags();
3437 if (flag1 != flag2) {
3438 // The order here should match what is used in updateDrawState
3439 final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
3440 final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
3441 final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
3442 final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
3443 if (easy1 && !misspelled1) return -1;
3444 if (easy2 && !misspelled2) return 1;
3445 if (misspelled1) return -1;
3446 if (misspelled2) return 1;
3447 }
3448
3449 return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
3450 }
3451 }
3452
3453 /**
3454 * Returns the suggestion spans that cover the current cursor position. The suggestion
3455 * spans are sorted according to the length of text that they are attached to.
3456 */
3457 private SuggestionSpan[] getSortedSuggestionSpans() {
3458 int pos = mTextView.getSelectionStart();
3459 Spannable spannable = (Spannable) mTextView.getText();
3460 SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
3461
3462 mSpansLengths.clear();
3463 for (SuggestionSpan suggestionSpan : suggestionSpans) {
3464 int start = spannable.getSpanStart(suggestionSpan);
3465 int end = spannable.getSpanEnd(suggestionSpan);
3466 mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
3467 }
3468
3469 // The suggestions are sorted according to their types (easy correction first, then
3470 // misspelled) and to the length of the text that they cover (shorter first).
3471 Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
3472 mSpansLengths.clear();
3473
3474 return suggestionSpans;
3475 }
3476
3477 /**
3478 * Gets the SuggestionInfo list that contains suggestion information at the current cursor
3479 * position.
3480 *
3481 * @param suggestionInfos SuggestionInfo array the results will be set.
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003482 * @param misspelledSpanInfo a struct the misspelled SuggestionSpan info will be set.
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003483 * @return the number of suggestions actually fetched.
3484 */
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003485 public int getSuggestionInfo(SuggestionInfo[] suggestionInfos,
3486 @Nullable SuggestionSpanInfo misspelledSpanInfo) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003487 final Spannable spannable = (Spannable) mTextView.getText();
3488 final SuggestionSpan[] suggestionSpans = getSortedSuggestionSpans();
3489 final int nbSpans = suggestionSpans.length;
3490 if (nbSpans == 0) return 0;
3491
3492 int numberOfSuggestions = 0;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003493 for (final SuggestionSpan suggestionSpan : suggestionSpans) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003494 final int spanStart = spannable.getSpanStart(suggestionSpan);
3495 final int spanEnd = spannable.getSpanEnd(suggestionSpan);
3496
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003497 if (misspelledSpanInfo != null
3498 && (suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
3499 misspelledSpanInfo.mSuggestionSpan = suggestionSpan;
3500 misspelledSpanInfo.mSpanStart = spanStart;
3501 misspelledSpanInfo.mSpanEnd = spanEnd;
3502 }
3503
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003504 final String[] suggestions = suggestionSpan.getSuggestions();
3505 final int nbSuggestions = suggestions.length;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003506 suggestionLoop:
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003507 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
3508 final String suggestion = suggestions[suggestionIndex];
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003509 for (int i = 0; i < numberOfSuggestions; i++) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003510 final SuggestionInfo otherSuggestionInfo = suggestionInfos[i];
3511 if (otherSuggestionInfo.mText.toString().equals(suggestion)) {
3512 final int otherSpanStart =
3513 otherSuggestionInfo.mSuggestionSpanInfo.mSpanStart;
3514 final int otherSpanEnd =
3515 otherSuggestionInfo.mSuggestionSpanInfo.mSpanEnd;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003516 if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003517 continue suggestionLoop;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003518 }
3519 }
3520 }
3521
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003522 SuggestionInfo suggestionInfo = suggestionInfos[numberOfSuggestions];
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003523 suggestionInfo.setSpanInfo(suggestionSpan, spanStart, spanEnd);
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003524 suggestionInfo.mSuggestionIndex = suggestionIndex;
3525 suggestionInfo.mSuggestionStart = 0;
3526 suggestionInfo.mSuggestionEnd = suggestion.length();
3527 suggestionInfo.mText.replace(0, suggestionInfo.mText.length(), suggestion);
3528 numberOfSuggestions++;
3529 if (numberOfSuggestions >= suggestionInfos.length) {
3530 return numberOfSuggestions;
3531 }
3532 }
3533 }
3534 return numberOfSuggestions;
3535 }
3536 }
3537
Yohei Yukawaca9376c2019-02-01 23:38:30 -08003538 private final class SuggestionsPopupWindow extends PinnedPopupWindow
3539 implements OnItemClickListener {
Gilles Debunned88876a2012-03-16 17:34:04 -07003540 private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003541
3542 // Key of intent extras for inserting new word into user dictionary.
3543 private static final String USER_DICTIONARY_EXTRA_WORD = "word";
3544 private static final String USER_DICTIONARY_EXTRA_LOCALE = "locale";
3545
Gilles Debunned88876a2012-03-16 17:34:04 -07003546 private SuggestionInfo[] mSuggestionInfos;
3547 private int mNumberOfSuggestions;
3548 private boolean mCursorWasVisibleBeforeSuggestions;
3549 private boolean mIsShowingUp = false;
3550 private SuggestionAdapter mSuggestionsAdapter;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003551 private TextAppearanceSpan mHighlightSpan; // TODO: Make mHighlightSpan final.
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003552 private TextView mAddToDictionaryButton;
3553 private TextView mDeleteButton;
Seigo Nonakaf47976e2016-03-01 09:17:37 -08003554 private ListView mSuggestionListView;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003555 private final SuggestionSpanInfo mMisspelledSpanInfo = new SuggestionSpanInfo();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003556 private int mContainerMarginWidth;
3557 private int mContainerMarginTop;
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003558 private LinearLayout mContainerView;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003559 private Context mContext; // TODO: Make mContext final.
Gilles Debunned88876a2012-03-16 17:34:04 -07003560
3561 private class CustomPopupWindow extends PopupWindow {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003562
Gilles Debunned88876a2012-03-16 17:34:04 -07003563 @Override
3564 public void dismiss() {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09003565 if (!isShowing()) {
3566 return;
3567 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003568 super.dismiss();
Gilles Debunned88876a2012-03-16 17:34:04 -07003569 getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
3570
3571 // Safe cast since show() checks that mTextView.getText() is an Editable
3572 ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan);
3573
3574 mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions);
Keisuke Kuroyanagi4a696ac2016-02-23 11:02:07 -08003575 if (hasInsertionController() && !extractedTextModeWillBeStarted()) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003576 getInsertionController().show();
3577 }
3578 }
3579 }
3580
3581 public SuggestionsPopupWindow() {
3582 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
Gilles Debunned88876a2012-03-16 17:34:04 -07003583 }
3584
3585 @Override
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003586 protected void setUp() {
3587 mContext = applyDefaultTheme(mTextView.getContext());
3588 mHighlightSpan = new TextAppearanceSpan(mContext,
3589 mTextView.mTextEditSuggestionHighlightStyle);
3590 }
3591
3592 private Context applyDefaultTheme(Context originalContext) {
3593 TypedArray a = originalContext.obtainStyledAttributes(
3594 new int[]{com.android.internal.R.attr.isLightTheme});
3595 boolean isLightTheme = a.getBoolean(0, true);
3596 int themeId = isLightTheme ? R.style.ThemeOverlay_Material_Light
3597 : R.style.ThemeOverlay_Material_Dark;
3598 a.recycle();
3599 return new ContextThemeWrapper(originalContext, themeId);
3600 }
3601
3602 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07003603 protected void createPopupWindow() {
Seigo Nonaka3ed1b392016-01-19 13:54:59 +09003604 mPopupWindow = new CustomPopupWindow();
Gilles Debunned88876a2012-03-16 17:34:04 -07003605 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
Seigo Nonaka3ed1b392016-01-19 13:54:59 +09003606 mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
Gilles Debunned88876a2012-03-16 17:34:04 -07003607 mPopupWindow.setFocusable(true);
3608 mPopupWindow.setClippingEnabled(false);
3609 }
3610
3611 @Override
3612 protected void initContentView() {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003613 final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
3614 Context.LAYOUT_INFLATER_SERVICE);
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003615 mContentView = (ViewGroup) inflater.inflate(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003616 mTextView.mTextEditSuggestionContainerLayout, null);
Gilles Debunned88876a2012-03-16 17:34:04 -07003617
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003618 mContainerView = (LinearLayout) mContentView.findViewById(
3619 com.android.internal.R.id.suggestionWindowContainer);
Seigo Nonaka60490d12016-01-28 17:25:18 +09003620 ViewGroup.MarginLayoutParams lp =
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003621 (ViewGroup.MarginLayoutParams) mContainerView.getLayoutParams();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003622 mContainerMarginWidth = lp.leftMargin + lp.rightMargin;
3623 mContainerMarginTop = lp.topMargin;
3624 mClippingLimitLeft = lp.leftMargin;
3625 mClippingLimitRight = lp.rightMargin;
3626
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003627 mSuggestionListView = (ListView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003628 com.android.internal.R.id.suggestionContainer);
3629
3630 mSuggestionsAdapter = new SuggestionAdapter();
Seigo Nonakaf47976e2016-03-01 09:17:37 -08003631 mSuggestionListView.setAdapter(mSuggestionsAdapter);
3632 mSuggestionListView.setOnItemClickListener(this);
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003633
3634 // Inflate the suggestion items once and for all.
3635 mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS];
Gilles Debunned88876a2012-03-16 17:34:04 -07003636 for (int i = 0; i < mSuggestionInfos.length; i++) {
3637 mSuggestionInfos[i] = new SuggestionInfo();
3638 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003639
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003640 mAddToDictionaryButton = (TextView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003641 com.android.internal.R.id.addToDictionaryButton);
3642 mAddToDictionaryButton.setOnClickListener(new View.OnClickListener() {
3643 public void onClick(View v) {
Keisuke Kuroyanagi6e0860d2016-03-15 15:40:43 +09003644 final SuggestionSpan misspelledSpan =
3645 findEquivalentSuggestionSpan(mMisspelledSpanInfo);
3646 if (misspelledSpan == null) {
3647 // Span has been removed.
3648 return;
3649 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003650 final Editable editable = (Editable) mTextView.getText();
Keisuke Kuroyanagi6e0860d2016-03-15 15:40:43 +09003651 final int spanStart = editable.getSpanStart(misspelledSpan);
3652 final int spanEnd = editable.getSpanEnd(misspelledSpan);
3653 if (spanStart < 0 || spanEnd <= spanStart) {
3654 return;
3655 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003656 final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
3657
3658 final Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
3659 intent.putExtra(USER_DICTIONARY_EXTRA_WORD, originalText);
3660 intent.putExtra(USER_DICTIONARY_EXTRA_LOCALE,
3661 mTextView.getTextServicesLocale().toString());
3662 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
Yohei Yukawa0115ac12019-02-05 22:27:20 -08003663 mTextView.startActivityAsTextOperationUserIfNecessary(intent);
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003664 // There is no way to know if the word was indeed added. Re-check.
3665 // TODO The ExtractEditText should remove the span in the original text instead
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003666 editable.removeSpan(mMisspelledSpanInfo.mSuggestionSpan);
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003667 Selection.setSelection(editable, spanEnd);
3668 updateSpellCheckSpans(spanStart, spanEnd, false);
3669 hideWithCleanUp();
3670 }
3671 });
3672
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003673 mDeleteButton = (TextView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003674 com.android.internal.R.id.deleteButton);
3675 mDeleteButton.setOnClickListener(new View.OnClickListener() {
3676 public void onClick(View v) {
3677 final Editable editable = (Editable) mTextView.getText();
3678
3679 final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
3680 int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
3681 if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
3682 // Do not leave two adjacent spaces after deletion, or one at beginning of
3683 // text
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003684 if (spanUnionEnd < editable.length()
3685 && Character.isSpaceChar(editable.charAt(spanUnionEnd))
3686 && (spanUnionStart == 0
3687 || Character.isSpaceChar(
3688 editable.charAt(spanUnionStart - 1)))) {
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003689 spanUnionEnd = spanUnionEnd + 1;
3690 }
3691 mTextView.deleteText_internal(spanUnionStart, spanUnionEnd);
3692 }
3693 hideWithCleanUp();
3694 }
3695 });
3696
Gilles Debunned88876a2012-03-16 17:34:04 -07003697 }
3698
3699 public boolean isShowingUp() {
3700 return mIsShowingUp;
3701 }
3702
3703 public void onParentLostFocus() {
3704 mIsShowingUp = false;
3705 }
3706
Gilles Debunned88876a2012-03-16 17:34:04 -07003707 private class SuggestionAdapter extends BaseAdapter {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003708 private LayoutInflater mInflater = (LayoutInflater) mContext.getSystemService(
3709 Context.LAYOUT_INFLATER_SERVICE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003710
3711 @Override
3712 public int getCount() {
3713 return mNumberOfSuggestions;
3714 }
3715
3716 @Override
3717 public Object getItem(int position) {
3718 return mSuggestionInfos[position];
3719 }
3720
3721 @Override
3722 public long getItemId(int position) {
3723 return position;
3724 }
3725
3726 @Override
3727 public View getView(int position, View convertView, ViewGroup parent) {
3728 TextView textView = (TextView) convertView;
3729
3730 if (textView == null) {
3731 textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout,
3732 parent, false);
3733 }
3734
3735 final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003736 textView.setText(suggestionInfo.mText);
Gilles Debunned88876a2012-03-16 17:34:04 -07003737 return textView;
3738 }
3739 }
3740
Gilles Debunned88876a2012-03-16 17:34:04 -07003741 @Override
3742 public void show() {
3743 if (!(mTextView.getText() instanceof Editable)) return;
Keisuke Kuroyanagi4a696ac2016-02-23 11:02:07 -08003744 if (extractedTextModeWillBeStarted()) {
3745 return;
3746 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003747
3748 if (updateSuggestions()) {
3749 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
3750 mTextView.setCursorVisible(false);
3751 mIsShowingUp = true;
3752 super.show();
3753 }
Clara Bayarri428e5232017-07-18 16:42:16 +01003754
3755 mSuggestionListView.setVisibility(mNumberOfSuggestions == 0 ? View.GONE : View.VISIBLE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003756 }
3757
3758 @Override
3759 protected void measureContent() {
3760 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3761 final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
3762 displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
3763 final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
3764 displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
3765
3766 int width = 0;
3767 View view = null;
3768 for (int i = 0; i < mNumberOfSuggestions; i++) {
3769 view = mSuggestionsAdapter.getView(i, view, mContentView);
3770 view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
3771 view.measure(horizontalMeasure, verticalMeasure);
3772 width = Math.max(width, view.getMeasuredWidth());
3773 }
3774
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003775 if (mAddToDictionaryButton.getVisibility() != View.GONE) {
3776 mAddToDictionaryButton.measure(horizontalMeasure, verticalMeasure);
3777 width = Math.max(width, mAddToDictionaryButton.getMeasuredWidth());
3778 }
3779
3780 mDeleteButton.measure(horizontalMeasure, verticalMeasure);
3781 width = Math.max(width, mDeleteButton.getMeasuredWidth());
3782
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003783 width += mContainerView.getPaddingLeft() + mContainerView.getPaddingRight()
3784 + mContainerMarginWidth;
Seigo Nonaka60490d12016-01-28 17:25:18 +09003785
Gilles Debunned88876a2012-03-16 17:34:04 -07003786 // Enforce the width based on actual text widths
3787 mContentView.measure(
3788 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
3789 verticalMeasure);
3790
3791 Drawable popupBackground = mPopupWindow.getBackground();
3792 if (popupBackground != null) {
3793 if (mTempRect == null) mTempRect = new Rect();
3794 popupBackground.getPadding(mTempRect);
3795 width += mTempRect.left + mTempRect.right;
3796 }
3797 mPopupWindow.setWidth(width);
3798 }
3799
3800 @Override
3801 protected int getTextOffset() {
Keisuke Kuroyanagi713be062016-02-29 16:07:54 -08003802 return (mTextView.getSelectionStart() + mTextView.getSelectionStart()) / 2;
Gilles Debunned88876a2012-03-16 17:34:04 -07003803 }
3804
3805 @Override
3806 protected int getVerticalLocalPosition(int line) {
Siyamed Sinira60b59d2017-07-26 09:26:41 -07003807 final Layout layout = mTextView.getLayout();
3808 return layout.getLineBottomWithoutSpacing(line) - mContainerMarginTop;
Gilles Debunned88876a2012-03-16 17:34:04 -07003809 }
3810
3811 @Override
3812 protected int clipVertically(int positionY) {
3813 final int height = mContentView.getMeasuredHeight();
3814 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3815 return Math.min(positionY, displayMetrics.heightPixels - height);
3816 }
3817
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003818 private void hideWithCleanUp() {
3819 for (final SuggestionInfo info : mSuggestionInfos) {
3820 info.clear();
3821 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003822 mMisspelledSpanInfo.clear();
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003823 hide();
Gilles Debunned88876a2012-03-16 17:34:04 -07003824 }
3825
3826 private boolean updateSuggestions() {
3827 Spannable spannable = (Spannable) mTextView.getText();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003828 mNumberOfSuggestions =
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003829 mSuggestionHelper.getSuggestionInfo(mSuggestionInfos, mMisspelledSpanInfo);
3830 if (mNumberOfSuggestions == 0 && mMisspelledSpanInfo.mSuggestionSpan == null) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003831 return false;
3832 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003833
Gilles Debunned88876a2012-03-16 17:34:04 -07003834 int spanUnionStart = mTextView.getText().length();
3835 int spanUnionEnd = 0;
3836
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003837 for (int i = 0; i < mNumberOfSuggestions; i++) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003838 final SuggestionSpanInfo spanInfo = mSuggestionInfos[i].mSuggestionSpanInfo;
3839 spanUnionStart = Math.min(spanUnionStart, spanInfo.mSpanStart);
3840 spanUnionEnd = Math.max(spanUnionEnd, spanInfo.mSpanEnd);
3841 }
3842 if (mMisspelledSpanInfo.mSuggestionSpan != null) {
3843 spanUnionStart = Math.min(spanUnionStart, mMisspelledSpanInfo.mSpanStart);
3844 spanUnionEnd = Math.max(spanUnionEnd, mMisspelledSpanInfo.mSpanEnd);
Gilles Debunned88876a2012-03-16 17:34:04 -07003845 }
3846
3847 for (int i = 0; i < mNumberOfSuggestions; i++) {
3848 highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
3849 }
3850
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003851 // Make "Add to dictionary" item visible if there is a span with the misspelled flag
3852 int addToDictionaryButtonVisibility = View.GONE;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003853 if (mMisspelledSpanInfo.mSuggestionSpan != null) {
3854 if (mMisspelledSpanInfo.mSpanStart >= 0
3855 && mMisspelledSpanInfo.mSpanEnd > mMisspelledSpanInfo.mSpanStart) {
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003856 addToDictionaryButtonVisibility = View.VISIBLE;
Gilles Debunned88876a2012-03-16 17:34:04 -07003857 }
3858 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003859 mAddToDictionaryButton.setVisibility(addToDictionaryButtonVisibility);
Gilles Debunned88876a2012-03-16 17:34:04 -07003860
3861 if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003862 final int underlineColor;
3863 if (mNumberOfSuggestions != 0) {
3864 underlineColor =
3865 mSuggestionInfos[0].mSuggestionSpanInfo.mSuggestionSpan.getUnderlineColor();
3866 } else {
3867 underlineColor = mMisspelledSpanInfo.mSuggestionSpan.getUnderlineColor();
3868 }
3869
Gilles Debunned88876a2012-03-16 17:34:04 -07003870 if (underlineColor == 0) {
3871 // Fallback on the default highlight color when the first span does not provide one
3872 mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor);
3873 } else {
3874 final float BACKGROUND_TRANSPARENCY = 0.4f;
3875 final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
3876 mSuggestionRangeSpan.setBackgroundColor(
3877 (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
3878 }
3879 spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
3880 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
3881
3882 mSuggestionsAdapter.notifyDataSetChanged();
3883 return true;
3884 }
3885
3886 private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
3887 int unionEnd) {
3888 final Spannable text = (Spannable) mTextView.getText();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003889 final int spanStart = suggestionInfo.mSuggestionSpanInfo.mSpanStart;
3890 final int spanEnd = suggestionInfo.mSuggestionSpanInfo.mSpanEnd;
Gilles Debunned88876a2012-03-16 17:34:04 -07003891
3892 // Adjust the start/end of the suggestion span
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003893 suggestionInfo.mSuggestionStart = spanStart - unionStart;
3894 suggestionInfo.mSuggestionEnd = suggestionInfo.mSuggestionStart
3895 + suggestionInfo.mText.length();
Gilles Debunned88876a2012-03-16 17:34:04 -07003896
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003897 suggestionInfo.mText.setSpan(mHighlightSpan, 0, suggestionInfo.mText.length(),
Seigo Nonakabffbd302015-08-18 18:27:56 -07003898 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003899
3900 // Add the text before and after the span.
3901 final String textAsString = text.toString();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003902 suggestionInfo.mText.insert(0, textAsString.substring(unionStart, spanStart));
3903 suggestionInfo.mText.append(textAsString.substring(spanEnd, unionEnd));
Gilles Debunned88876a2012-03-16 17:34:04 -07003904 }
3905
3906 @Override
3907 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003908 SuggestionInfo suggestionInfo = mSuggestionInfos[position];
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003909 replaceWithSuggestion(suggestionInfo);
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003910 hideWithCleanUp();
Gilles Debunned88876a2012-03-16 17:34:04 -07003911 }
3912 }
3913
3914 /**
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003915 * An ActionMode Callback class that is used to provide actions while in text insertion or
3916 * selection mode.
Gilles Debunned88876a2012-03-16 17:34:04 -07003917 *
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003918 * The default callback provides a subset of Select All, Cut, Copy, Paste, Share and Replace
3919 * actions, depending on which of these this TextView supports and the current selection.
Gilles Debunned88876a2012-03-16 17:34:04 -07003920 */
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003921 private class TextActionModeCallback extends ActionMode.Callback2 {
Clara Bayarriea4f1502015-03-18 00:25:01 +00003922 private final Path mSelectionPath = new Path();
3923 private final RectF mSelectionBounds = new RectF();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003924 private final boolean mHasSelection;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003925 private final int mHandleHeight;
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01003926 private final Map<MenuItem, OnClickListener> mAssistClickHandlers = new HashMap<>();
Clara Bayarriea4f1502015-03-18 00:25:01 +00003927
Richard Ledley26b87222017-11-30 10:54:08 +00003928 TextActionModeCallback(@TextActionMode int mode) {
3929 mHasSelection = mode == TextActionMode.SELECTION
3930 || (mTextIsSelectable && mode == TextActionMode.TEXT_LINK);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003931 if (mHasSelection) {
3932 SelectionModifierCursorController selectionController = getSelectionController();
3933 if (selectionController.mStartHandle == null) {
3934 // As these are for initializing selectionController, hide() must be called.
Mihai Popadb68c542018-11-08 15:23:01 +00003935 loadHandleDrawables(false /* overwrite */);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003936 selectionController.initHandles();
3937 selectionController.hide();
3938 }
3939 mHandleHeight = Math.max(
3940 mSelectHandleLeft.getMinimumHeight(),
3941 mSelectHandleRight.getMinimumHeight());
3942 } else {
3943 InsertionPointCursorController insertionController = getInsertionController();
3944 if (insertionController != null) {
3945 insertionController.getHandle();
3946 mHandleHeight = mSelectHandleCenter.getMinimumHeight();
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003947 } else {
3948 mHandleHeight = 0;
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003949 }
Clara Bayarri7fc946e2015-03-31 14:48:33 +01003950 }
Clara Bayarriea4f1502015-03-18 00:25:01 +00003951 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003952
3953 @Override
3954 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01003955 mAssistClickHandlers.clear();
3956
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003957 mode.setTitle(null);
Clara Bayarri13152d12015-04-09 12:02:04 +01003958 mode.setSubtitle(null);
3959 mode.setTitleOptionalHint(true);
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003960 populateMenuWithItems(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01003961
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003962 Callback customCallback = getCustomCallback();
3963 if (customCallback != null) {
3964 if (!customCallback.onCreateActionMode(mode, menu)) {
Clara Bayarri01243ac2015-06-03 00:46:29 +01003965 // The custom mode can choose to cancel the action mode, dismiss selection.
3966 Selection.setSelection((Spannable) mTextView.getText(),
3967 mTextView.getSelectionEnd());
Clara Bayarri13152d12015-04-09 12:02:04 +01003968 return false;
3969 }
3970 }
3971
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07003972 if (mTextView.canProcessText()) {
3973 mProcessTextIntentActionsHandler.onInitializeMenu(menu);
3974 }
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +00003975
Abodunrinwa Tokideb2f492017-11-06 18:55:17 +00003976 if (mHasSelection && !mTextView.hasTransientState()) {
3977 mTextView.setHasTransientState(true);
Clara Bayarri13152d12015-04-09 12:02:04 +01003978 }
Abodunrinwa Tokideb2f492017-11-06 18:55:17 +00003979 return true;
Clara Bayarri13152d12015-04-09 12:02:04 +01003980 }
3981
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003982 private Callback getCustomCallback() {
3983 return mHasSelection
3984 ? mCustomSelectionActionModeCallback
3985 : mCustomInsertionActionModeCallback;
3986 }
3987
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003988 private void populateMenuWithItems(Menu menu) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003989 if (mTextView.canCut()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003990 menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003991 com.android.internal.R.string.cut)
3992 .setAlphabeticShortcut('x')
3993 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07003994 }
3995
3996 if (mTextView.canCopy()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003997 menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003998 com.android.internal.R.string.copy)
3999 .setAlphabeticShortcut('c')
4000 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07004001 }
4002
4003 if (mTextView.canPaste()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01004004 menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004005 com.android.internal.R.string.paste)
4006 .setAlphabeticShortcut('v')
4007 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07004008 }
4009
Andrei Stingaceanu7f0c5bd2015-04-14 17:12:08 +01004010 if (mTextView.canShare()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01004011 menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004012 com.android.internal.R.string.share)
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +00004013 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
Andrei Stingaceanu7f0c5bd2015-04-14 17:12:08 +01004014 }
4015
Felipe Leme2ac463e2017-03-13 14:06:25 -07004016 if (mTextView.canRequestAutofill()) {
Felipe Leme1c1626e2017-06-02 10:53:13 -07004017 final String selected = mTextView.getSelectedText();
4018 if (selected == null || selected.isEmpty()) {
4019 menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
4020 com.android.internal.R.string.autofill)
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01004021 .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
Felipe Leme1c1626e2017-06-02 10:53:13 -07004022 }
Felipe Leme2ac463e2017-03-13 14:06:25 -07004023 }
4024
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01004025 if (mTextView.canPasteAsPlainText()) {
4026 menu.add(
4027 Menu.NONE,
4028 TextView.ID_PASTE_AS_PLAIN_TEXT,
4029 MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
4030 com.android.internal.R.string.paste_as_plain_text)
4031 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
4032 }
4033
Clara Bayarri3b69fd82015-06-03 21:52:02 +01004034 updateSelectAllItem(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01004035 updateReplaceItem(menu);
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004036 updateAssistMenuItems(menu);
Gilles Debunned88876a2012-03-16 17:34:04 -07004037 }
4038
4039 @Override
4040 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01004041 updateSelectAllItem(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01004042 updateReplaceItem(menu);
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004043 updateAssistMenuItems(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01004044
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004045 Callback customCallback = getCustomCallback();
4046 if (customCallback != null) {
4047 return customCallback.onPrepareActionMode(mode, menu);
Gilles Debunned88876a2012-03-16 17:34:04 -07004048 }
4049 return true;
4050 }
4051
Clara Bayarri3b69fd82015-06-03 21:52:02 +01004052 private void updateSelectAllItem(Menu menu) {
4053 boolean canSelectAll = mTextView.canSelectAllText();
4054 boolean selectAllItemExists = menu.findItem(TextView.ID_SELECT_ALL) != null;
4055 if (canSelectAll && !selectAllItemExists) {
4056 menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
4057 com.android.internal.R.string.selectAll)
4058 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
4059 } else if (!canSelectAll && selectAllItemExists) {
4060 menu.removeItem(TextView.ID_SELECT_ALL);
4061 }
4062 }
4063
Clara Bayarri13152d12015-04-09 12:02:04 +01004064 private void updateReplaceItem(Menu menu) {
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08004065 boolean canReplace = mTextView.isSuggestionsEnabled() && shouldOfferToShowSuggestions();
Clara Bayarri13152d12015-04-09 12:02:04 +01004066 boolean replaceItemExists = menu.findItem(TextView.ID_REPLACE) != null;
4067 if (canReplace && !replaceItemExists) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01004068 menu.add(Menu.NONE, TextView.ID_REPLACE, MENU_ITEM_ORDER_REPLACE,
4069 com.android.internal.R.string.replace)
4070 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
Clara Bayarri13152d12015-04-09 12:02:04 +01004071 } else if (!canReplace && replaceItemExists) {
4072 menu.removeItem(TextView.ID_REPLACE);
4073 }
4074 }
4075
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004076 private void updateAssistMenuItems(Menu menu) {
4077 clearAssistMenuItems(menu);
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +00004078 if (!shouldEnableAssistMenuItems()) {
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004079 return;
4080 }
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +01004081 final TextClassification textClassification =
4082 getSelectionActionModeHelper().getTextClassification();
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00004083 if (textClassification == null) {
4084 return;
4085 }
Jan Althaus20d346e2018-03-23 14:03:52 +01004086 if (!textClassification.getActions().isEmpty()) {
4087 // Primary assist action (Always shown).
4088 final MenuItem item = addAssistMenuItem(menu,
4089 textClassification.getActions().get(0), TextView.ID_ASSIST,
4090 MENU_ITEM_ORDER_ASSIST, MenuItem.SHOW_AS_ACTION_ALWAYS);
4091 item.setIntent(textClassification.getIntent());
4092 } else if (hasLegacyAssistItem(textClassification)) {
4093 // Legacy primary assist action (Always shown).
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00004094 final MenuItem item = menu.add(
4095 TextView.ID_ASSIST, TextView.ID_ASSIST, MENU_ITEM_ORDER_ASSIST,
4096 textClassification.getLabel())
4097 .setIcon(textClassification.getIcon())
4098 .setIntent(textClassification.getIntent());
4099 item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Jan Althaus20d346e2018-03-23 14:03:52 +01004100 mAssistClickHandlers.put(item, TextClassification.createIntentOnClickListener(
4101 TextClassification.createPendingIntent(mTextView.getContext(),
Abodunrinwa Toki904a9312018-04-18 21:21:27 +01004102 textClassification.getIntent(),
4103 createAssistMenuItemPendingIntentRequestCode())));
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00004104 }
Jan Althaus20d346e2018-03-23 14:03:52 +01004105 final int count = textClassification.getActions().size();
4106 for (int i = 1; i < count; i++) {
4107 // Secondary assist action (Never shown).
4108 addAssistMenuItem(menu, textClassification.getActions().get(i), Menu.NONE,
4109 MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START + i - 1,
4110 MenuItem.SHOW_AS_ACTION_NEVER);
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00004111 }
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +00004112 }
4113
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +00004114 private MenuItem addAssistMenuItem(Menu menu, RemoteAction action, int itemId, int order,
Jan Althaus20d346e2018-03-23 14:03:52 +01004115 int showAsAction) {
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +00004116 final MenuItem item = menu.add(TextView.ID_ASSIST, itemId, order, action.getTitle())
Jan Althaus20d346e2018-03-23 14:03:52 +01004117 .setContentDescription(action.getContentDescription());
4118 if (action.shouldShowIcon()) {
4119 item.setIcon(action.getIcon().loadDrawable(mTextView.getContext()));
4120 }
4121 item.setShowAsAction(showAsAction);
4122 mAssistClickHandlers.put(item,
4123 TextClassification.createIntentOnClickListener(action.getActionIntent()));
4124 return item;
4125 }
4126
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004127 private void clearAssistMenuItems(Menu menu) {
4128 int i = 0;
4129 while (i < menu.size()) {
4130 final MenuItem menuItem = menu.getItem(i);
4131 if (menuItem.getGroupId() == TextView.ID_ASSIST) {
4132 menu.removeItem(menuItem.getItemId());
4133 continue;
4134 }
4135 i++;
4136 }
4137 }
4138
Jan Althaus20d346e2018-03-23 14:03:52 +01004139 private boolean hasLegacyAssistItem(TextClassification classification) {
4140 // Check whether we have the UI data and and action.
4141 return (classification.getIcon() != null || !TextUtils.isEmpty(
4142 classification.getLabel())) && (classification.getIntent() != null
4143 || classification.getOnClickListener() != null);
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004144 }
4145
4146 private boolean onAssistMenuItemClicked(MenuItem assistMenuItem) {
4147 Preconditions.checkArgument(assistMenuItem.getGroupId() == TextView.ID_ASSIST);
4148
4149 final TextClassification textClassification =
4150 getSelectionActionModeHelper().getTextClassification();
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +00004151 if (!shouldEnableAssistMenuItems() || textClassification == null) {
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004152 // No textClassification result to handle the click. Eat the click.
4153 return true;
4154 }
4155
4156 OnClickListener onClickListener = mAssistClickHandlers.get(assistMenuItem);
4157 if (onClickListener == null) {
4158 final Intent intent = assistMenuItem.getIntent();
4159 if (intent != null) {
Abodunrinwa Toki2f19b922018-02-12 19:59:28 +00004160 onClickListener = TextClassification.createIntentOnClickListener(
Abodunrinwa Toki904a9312018-04-18 21:21:27 +01004161 TextClassification.createPendingIntent(
4162 mTextView.getContext(), intent,
4163 createAssistMenuItemPendingIntentRequestCode()));
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004164 }
4165 }
4166 if (onClickListener != null) {
4167 onClickListener.onClick(mTextView);
4168 stopTextActionMode();
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004169 }
4170 // We tried our best.
4171 return true;
Abodunrinwa Toki9796a1b2017-06-28 02:49:07 +01004172 }
4173
Abodunrinwa Toki904a9312018-04-18 21:21:27 +01004174 private int createAssistMenuItemPendingIntentRequestCode() {
4175 return mTextView.hasSelection()
4176 ? mTextView.getText().subSequence(
4177 mTextView.getSelectionStart(), mTextView.getSelectionEnd())
4178 .hashCode()
4179 : 0;
4180 }
4181
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +00004182 private boolean shouldEnableAssistMenuItems() {
4183 return mTextView.isDeviceProvisioned()
4184 && TextClassificationManager.getSettings(mTextView.getContext())
4185 .isSmartTextShareEnabled();
4186 }
4187
Gilles Debunned88876a2012-03-16 17:34:04 -07004188 @Override
4189 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +00004190 getSelectionActionModeHelper()
4191 .onSelectionAction(item.getItemId(), item.getTitle().toString());
Abodunrinwa Toki1d775572017-05-08 16:03:01 +01004192
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07004193 if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +00004194 return true;
4195 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004196 Callback customCallback = getCustomCallback();
4197 if (customCallback != null && customCallback.onActionItemClicked(mode, item)) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004198 return true;
4199 }
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004200 if (item.getGroupId() == TextView.ID_ASSIST && onAssistMenuItemClicked(item)) {
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +00004201 return true;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00004202 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004203 return mTextView.onTextContextMenuItem(item.getItemId());
4204 }
4205
4206 @Override
4207 public void onDestroyActionMode(ActionMode mode) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09004208 // Clear mTextActionMode not to recursively destroy action mode by clearing selection.
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +00004209 getSelectionActionModeHelper().onDestroyActionMode();
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09004210 mTextActionMode = null;
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004211 Callback customCallback = getCustomCallback();
4212 if (customCallback != null) {
4213 customCallback.onDestroyActionMode(mode);
Gilles Debunned88876a2012-03-16 17:34:04 -07004214 }
Adam Powell057a5852012-05-11 10:28:38 -07004215
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08004216 if (!mPreserveSelection) {
4217 /*
4218 * Leave current selection when we tentatively destroy action mode for the
4219 * selection. If we're detaching from a window, we'll bring back the selection
4220 * mode when (if) we get reattached.
4221 */
Adam Powell057a5852012-05-11 10:28:38 -07004222 Selection.setSelection((Spannable) mTextView.getText(),
4223 mTextView.getSelectionEnd());
Adam Powell057a5852012-05-11 10:28:38 -07004224 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004225
4226 if (mSelectionModifierCursorController != null) {
4227 mSelectionModifierCursorController.hide();
4228 }
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004229
4230 mAssistClickHandlers.clear();
Abodunrinwa Toki52096912018-03-21 23:14:42 +00004231 mRequestingLinkActionMode = false;
Gilles Debunned88876a2012-03-16 17:34:04 -07004232 }
Clara Bayarriea4f1502015-03-18 00:25:01 +00004233
4234 @Override
4235 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
4236 if (!view.equals(mTextView) || mTextView.getLayout() == null) {
4237 super.onGetContentRect(mode, view, outRect);
4238 return;
4239 }
4240 if (mTextView.getSelectionStart() != mTextView.getSelectionEnd()) {
4241 // We have a selection.
4242 mSelectionPath.reset();
4243 mTextView.getLayout().getSelectionPath(
4244 mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mSelectionPath);
4245 mSelectionPath.computeBounds(mSelectionBounds, true);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004246 mSelectionBounds.bottom += mHandleHeight;
Clara Bayarriea4f1502015-03-18 00:25:01 +00004247 } else {
Roozbeh Pournader9c133072017-07-26 22:36:27 -07004248 // We have a cursor.
Siyamed Sinir987ec652016-02-17 19:44:41 -08004249 Layout layout = mTextView.getLayout();
Mady Mellorff66ca52015-07-08 12:31:45 -07004250 int line = layout.getLineForOffset(mTextView.getSelectionStart());
Siyamed Sinir987ec652016-02-17 19:44:41 -08004251 float primaryHorizontal = clampHorizontalPosition(null,
4252 layout.getPrimaryHorizontal(mTextView.getSelectionStart()));
Clara Bayarriea4f1502015-03-18 00:25:01 +00004253 mSelectionBounds.set(
4254 primaryHorizontal,
Mady Mellorff66ca52015-07-08 12:31:45 -07004255 layout.getLineTop(line),
Clara Bayarrif95ed102015-08-12 19:46:47 +01004256 primaryHorizontal,
Siyamed Sinirfdbc5ee2018-02-09 11:24:16 -08004257 layout.getLineBottom(line) + mHandleHeight);
Clara Bayarriea4f1502015-03-18 00:25:01 +00004258 }
4259 // Take TextView's padding and scroll into account.
4260 int textHorizontalOffset = mTextView.viewportToContentHorizontalOffset();
4261 int textVerticalOffset = mTextView.viewportToContentVerticalOffset();
4262 outRect.set(
4263 (int) Math.floor(mSelectionBounds.left + textHorizontalOffset),
4264 (int) Math.floor(mSelectionBounds.top + textVerticalOffset),
4265 (int) Math.ceil(mSelectionBounds.right + textHorizontalOffset),
4266 (int) Math.ceil(mSelectionBounds.bottom + textVerticalOffset));
4267 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004268 }
4269
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004270 /**
4271 * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}
4272 * while the input method is requesting the cursor/anchor position. Does nothing as long as
4273 * {@link InputMethodManager#isWatchingCursor(View)} returns false.
4274 */
4275 private final class CursorAnchorInfoNotifier implements TextViewPositionListener {
Yohei Yukawac46b5f02014-06-10 12:26:34 +09004276 final CursorAnchorInfo.Builder mSelectionInfoBuilder = new CursorAnchorInfo.Builder();
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004277 final int[] mTmpIntOffset = new int[2];
4278 final Matrix mViewToScreenMatrix = new Matrix();
4279
4280 @Override
4281 public void updatePosition(int parentPositionX, int parentPositionY,
4282 boolean parentPositionChanged, boolean parentScrolled) {
4283 final InputMethodState ims = mInputMethodState;
4284 if (ims == null || ims.mBatchEditNesting > 0) {
4285 return;
4286 }
Yohei Yukawa484d4af2018-09-17 16:47:08 -07004287 final InputMethodManager imm = getInputMethodManager();
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004288 if (null == imm) {
4289 return;
4290 }
Yohei Yukawa0023d0e2014-07-11 04:13:03 +09004291 if (!imm.isActive(mTextView)) {
4292 return;
4293 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004294 // Skip if the IME has not requested the cursor/anchor position.
Yohei Yukawa0023d0e2014-07-11 04:13:03 +09004295 if (!imm.isCursorAnchorInfoEnabled()) {
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004296 return;
4297 }
4298 Layout layout = mTextView.getLayout();
4299 if (layout == null) {
4300 return;
4301 }
4302
Yohei Yukawac46b5f02014-06-10 12:26:34 +09004303 final CursorAnchorInfo.Builder builder = mSelectionInfoBuilder;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004304 builder.reset();
4305
4306 final int selectionStart = mTextView.getSelectionStart();
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004307 builder.setSelectionRange(selectionStart, mTextView.getSelectionEnd());
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004308
4309 // Construct transformation matrix from view local coordinates to screen coordinates.
4310 mViewToScreenMatrix.set(mTextView.getMatrix());
4311 mTextView.getLocationOnScreen(mTmpIntOffset);
4312 mViewToScreenMatrix.postTranslate(mTmpIntOffset[0], mTmpIntOffset[1]);
4313 builder.setMatrix(mViewToScreenMatrix);
4314
4315 final float viewportToContentHorizontalOffset =
4316 mTextView.viewportToContentHorizontalOffset();
4317 final float viewportToContentVerticalOffset =
4318 mTextView.viewportToContentVerticalOffset();
4319
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004320 final CharSequence text = mTextView.getText();
4321 if (text instanceof Spannable) {
4322 final Spannable sp = (Spannable) text;
4323 int composingTextStart = EditableInputConnection.getComposingSpanStart(sp);
4324 int composingTextEnd = EditableInputConnection.getComposingSpanEnd(sp);
4325 if (composingTextEnd < composingTextStart) {
4326 final int temp = composingTextEnd;
4327 composingTextEnd = composingTextStart;
4328 composingTextStart = temp;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004329 }
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004330 final boolean hasComposingText =
4331 (0 <= composingTextStart) && (composingTextStart < composingTextEnd);
4332 if (hasComposingText) {
4333 final CharSequence composingText = text.subSequence(composingTextStart,
4334 composingTextEnd);
4335 builder.setComposingText(composingTextStart, composingText);
Phil Weaverc2e28932016-12-08 12:29:25 -08004336 mTextView.populateCharacterBounds(builder, composingTextStart,
4337 composingTextEnd, viewportToContentHorizontalOffset,
4338 viewportToContentVerticalOffset);
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004339 }
4340 }
4341
4342 // Treat selectionStart as the insertion point.
4343 if (0 <= selectionStart) {
4344 final int offset = selectionStart;
4345 final int line = layout.getLineForOffset(offset);
4346 final float insertionMarkerX = layout.getPrimaryHorizontal(offset)
4347 + viewportToContentHorizontalOffset;
4348 final float insertionMarkerTop = layout.getLineTop(line)
4349 + viewportToContentVerticalOffset;
4350 final float insertionMarkerBaseline = layout.getLineBaseline(line)
4351 + viewportToContentVerticalOffset;
Siyamed Sinira60b59d2017-07-26 09:26:41 -07004352 final float insertionMarkerBottom = layout.getLineBottomWithoutSpacing(line)
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004353 + viewportToContentVerticalOffset;
Phil Weaverc2e28932016-12-08 12:29:25 -08004354 final boolean isTopVisible = mTextView
4355 .isPositionVisible(insertionMarkerX, insertionMarkerTop);
4356 final boolean isBottomVisible = mTextView
4357 .isPositionVisible(insertionMarkerX, insertionMarkerBottom);
Yohei Yukawacc24e2b2014-08-29 20:21:10 -07004358 int insertionMarkerFlags = 0;
4359 if (isTopVisible || isBottomVisible) {
4360 insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
4361 }
4362 if (!isTopVisible || !isBottomVisible) {
4363 insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
4364 }
Yohei Yukawa5f183f02014-09-02 14:18:40 -07004365 if (layout.isRtlCharAt(offset)) {
4366 insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL;
4367 }
Yohei Yukawa0b01e7f2014-07-08 15:29:51 +09004368 builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop,
Yohei Yukawacc24e2b2014-08-29 20:21:10 -07004369 insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags);
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004370 }
4371
4372 imm.updateCursorAnchorInfo(mTextView, builder.build());
4373 }
4374 }
4375
Mihai Popa38722382018-03-07 19:56:21 +00004376 private static class MagnifierMotionAnimator {
4377 private static final long DURATION = 100 /* miliseconds */;
4378
4379 // The magnifier being animated.
4380 private final Magnifier mMagnifier;
4381 // A value animator used to animate the magnifier.
4382 private final ValueAnimator mAnimator;
4383
4384 // Whether the magnifier is currently visible.
4385 private boolean mMagnifierIsShowing;
4386 // The coordinates of the magnifier when the currently running animation started.
4387 private float mAnimationStartX;
4388 private float mAnimationStartY;
4389 // The coordinates of the magnifier in the latest animation frame.
4390 private float mAnimationCurrentX;
4391 private float mAnimationCurrentY;
4392 // The latest coordinates the motion animator was asked to #show() the magnifier at.
4393 private float mLastX;
4394 private float mLastY;
4395
4396 private MagnifierMotionAnimator(final Magnifier magnifier) {
4397 mMagnifier = magnifier;
4398 // Prepare the animator used to run the motion animation.
4399 mAnimator = ValueAnimator.ofFloat(0, 1);
4400 mAnimator.setDuration(DURATION);
4401 mAnimator.setInterpolator(new LinearInterpolator());
4402 mAnimator.addUpdateListener((animation) -> {
4403 // Interpolate to find the current position of the magnifier.
4404 mAnimationCurrentX = mAnimationStartX
4405 + (mLastX - mAnimationStartX) * animation.getAnimatedFraction();
4406 mAnimationCurrentY = mAnimationStartY
4407 + (mLastY - mAnimationStartY) * animation.getAnimatedFraction();
4408 mMagnifier.show(mAnimationCurrentX, mAnimationCurrentY);
4409 });
4410 }
4411
4412 /**
4413 * Shows the magnifier at a new position.
4414 * If the y coordinate is different from the previous y coordinate
4415 * (probably corresponding to a line jump in the text), a short
4416 * animation is added to the jump.
4417 */
4418 private void show(final float x, final float y) {
4419 final boolean startNewAnimation = mMagnifierIsShowing && y != mLastY;
4420
4421 if (startNewAnimation) {
4422 if (mAnimator.isRunning()) {
4423 mAnimator.cancel();
4424 mAnimationStartX = mAnimationCurrentX;
4425 mAnimationStartY = mAnimationCurrentY;
4426 } else {
4427 mAnimationStartX = mLastX;
4428 mAnimationStartY = mLastY;
4429 }
4430 mAnimator.start();
4431 } else {
4432 if (!mAnimator.isRunning()) {
4433 mMagnifier.show(x, y);
4434 }
4435 }
4436 mLastX = x;
4437 mLastY = y;
4438 mMagnifierIsShowing = true;
4439 }
4440
4441 /**
4442 * Updates the content of the magnifier.
4443 */
4444 private void update() {
4445 mMagnifier.update();
4446 }
4447
4448 /**
4449 * Dismisses the magnifier, or does nothing if it is already dismissed.
4450 */
4451 private void dismiss() {
4452 mMagnifier.dismiss();
4453 mAnimator.cancel();
4454 mMagnifierIsShowing = false;
4455 }
4456 }
4457
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004458 @VisibleForTesting
4459 public abstract class HandleView extends View implements TextViewPositionListener {
Gilles Debunned88876a2012-03-16 17:34:04 -07004460 protected Drawable mDrawable;
4461 protected Drawable mDrawableLtr;
4462 protected Drawable mDrawableRtl;
4463 private final PopupWindow mContainer;
4464 // Position with respect to the parent TextView
4465 private int mPositionX, mPositionY;
4466 private boolean mIsDragging;
4467 // Offset from touch position to mPosition
4468 private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
4469 protected int mHotspotX;
Adam Powell3fceabd2014-08-19 18:28:04 -07004470 protected int mHorizontalGravity;
Gilles Debunned88876a2012-03-16 17:34:04 -07004471 // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
4472 private float mTouchOffsetY;
4473 // Where the touch position should be on the handle to ensure a maximum cursor visibility
4474 private float mIdealVerticalOffset;
4475 // Parent's (TextView) previous position in window
4476 private int mLastParentX, mLastParentY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004477 // Parent's (TextView) previous position on screen
4478 private int mLastParentXOnScreen, mLastParentYOnScreen;
Gilles Debunned88876a2012-03-16 17:34:04 -07004479 // Previous text character offset
Mady Mellorc2225b92015-04-01 15:59:20 -07004480 protected int mPreviousOffset = -1;
Gilles Debunned88876a2012-03-16 17:34:04 -07004481 // Previous text character offset
4482 private boolean mPositionHasChanged = true;
Adam Powell3fceabd2014-08-19 18:28:04 -07004483 // Minimum touch target size for handles
4484 private int mMinSize;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004485 // Indicates the line of text that the handle is on.
Mady Mellora6a0f782015-07-10 16:43:32 -07004486 protected int mPrevLine = UNSET_LINE;
4487 // Indicates the line of text that the user was touching. This can differ from mPrevLine
4488 // when selecting text when the handles jump to the end / start of words which may be on
4489 // a different line.
4490 protected int mPreviousLineTouched = UNSET_LINE;
Mihai Popa6d26d152019-01-30 15:36:47 +00004491 // The raw x coordinate of the motion down event which started the current dragging session.
4492 // Only used and stored when magnifier is used.
4493 private float mCurrentDragInitialTouchRawX = UNSET_X_VALUE;
4494 // The scale transform applied by containers to the TextView. Only used and computed
4495 // when magnifier is used.
4496 private float mTextViewScaleX;
4497 private float mTextViewScaleY;
Gilles Debunned88876a2012-03-16 17:34:04 -07004498
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004499 private HandleView(Drawable drawableLtr, Drawable drawableRtl, final int id) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004500 super(mTextView.getContext());
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004501 setId(id);
Gilles Debunned88876a2012-03-16 17:34:04 -07004502 mContainer = new PopupWindow(mTextView.getContext(), null,
4503 com.android.internal.R.attr.textSelectHandleWindowStyle);
4504 mContainer.setSplitTouchEnabled(true);
4505 mContainer.setClippingEnabled(false);
4506 mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
Keisuke Kuroyanagi7340be72015-02-27 17:57:49 +09004507 mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
4508 mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
Gilles Debunned88876a2012-03-16 17:34:04 -07004509 mContainer.setContentView(this);
4510
Mihai Popa6315a322018-10-17 17:39:57 +01004511 setDrawables(drawableLtr, drawableRtl);
4512
Adam Powell3fceabd2014-08-19 18:28:04 -07004513 mMinSize = mTextView.getContext().getResources().getDimensionPixelSize(
4514 com.android.internal.R.dimen.text_handle_min_size);
Gilles Debunned88876a2012-03-16 17:34:04 -07004515
Adam Powell3fceabd2014-08-19 18:28:04 -07004516 final int handleHeight = getPreferredHeight();
Gilles Debunned88876a2012-03-16 17:34:04 -07004517 mTouchOffsetY = -0.3f * handleHeight;
4518 mIdealVerticalOffset = 0.7f * handleHeight;
4519 }
4520
Mady Mellor7a936442015-05-20 10:05:52 -07004521 public float getIdealVerticalOffset() {
4522 return mIdealVerticalOffset;
4523 }
4524
Mihai Popa6315a322018-10-17 17:39:57 +01004525 void setDrawables(final Drawable drawableLtr, final Drawable drawableRtl) {
4526 mDrawableLtr = drawableLtr;
4527 mDrawableRtl = drawableRtl;
4528 updateDrawable(true /* updateDrawableWhenDragging */);
4529 }
4530
4531 protected void updateDrawable(final boolean updateDrawableWhenDragging) {
4532 if (!updateDrawableWhenDragging && mIsDragging) {
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004533 return;
4534 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004535 final Layout layout = mTextView.getLayout();
4536 if (layout == null) {
4537 return;
4538 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004539 final int offset = getCurrentCursorOffset();
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004540 final boolean isRtlCharAtOffset = isAtRtlRun(layout, offset);
Keisuke Kuroyanagi33f81ac2015-05-14 20:10:57 +09004541 final Drawable oldDrawable = mDrawable;
Gilles Debunned88876a2012-03-16 17:34:04 -07004542 mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
4543 mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
Adam Powell3fceabd2014-08-19 18:28:04 -07004544 mHorizontalGravity = getHorizontalGravity(isRtlCharAtOffset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004545 if (oldDrawable != mDrawable && isShowing()) {
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004546 // Update popup window position.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004547 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
4548 - getHorizontalOffset() + getCursorOffset();
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004549 mPositionX += mTextView.viewportToContentHorizontalOffset();
4550 mPositionHasChanged = true;
4551 updatePosition(mLastParentX, mLastParentY, false, false);
Keisuke Kuroyanagi33f81ac2015-05-14 20:10:57 +09004552 postInvalidate();
4553 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004554 }
4555
4556 protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
Adam Powell3fceabd2014-08-19 18:28:04 -07004557 protected abstract int getHorizontalGravity(boolean isRtlRun);
Gilles Debunned88876a2012-03-16 17:34:04 -07004558
4559 // Touch-up filter: number of previous positions remembered
4560 private static final int HISTORY_SIZE = 5;
4561 private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
4562 private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
4563 private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
4564 private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
4565 private int mPreviousOffsetIndex = 0;
4566 private int mNumberPreviousOffsets = 0;
4567
4568 private void startTouchUpFilter(int offset) {
4569 mNumberPreviousOffsets = 0;
4570 addPositionToTouchUpFilter(offset);
4571 }
4572
4573 private void addPositionToTouchUpFilter(int offset) {
4574 mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
4575 mPreviousOffsets[mPreviousOffsetIndex] = offset;
4576 mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
4577 mNumberPreviousOffsets++;
4578 }
4579
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004580 private void filterOnTouchUp(boolean fromTouchScreen) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004581 final long now = SystemClock.uptimeMillis();
4582 int i = 0;
4583 int index = mPreviousOffsetIndex;
4584 final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
4585 while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
4586 i++;
4587 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
4588 }
4589
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004590 if (i > 0 && i < iMax
4591 && (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004592 positionAtCursorOffset(mPreviousOffsets[index], false, fromTouchScreen);
Gilles Debunned88876a2012-03-16 17:34:04 -07004593 }
4594 }
4595
4596 public boolean offsetHasBeenChanged() {
4597 return mNumberPreviousOffsets > 1;
4598 }
4599
4600 @Override
4601 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Adam Powell3fceabd2014-08-19 18:28:04 -07004602 setMeasuredDimension(getPreferredWidth(), getPreferredHeight());
4603 }
4604
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004605 @Override
4606 public void invalidate() {
4607 super.invalidate();
4608 if (isShowing()) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004609 positionAtCursorOffset(getCurrentCursorOffset(), true, false);
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004610 }
4611 };
4612
Adam Powell3fceabd2014-08-19 18:28:04 -07004613 private int getPreferredWidth() {
4614 return Math.max(mDrawable.getIntrinsicWidth(), mMinSize);
4615 }
4616
4617 private int getPreferredHeight() {
4618 return Math.max(mDrawable.getIntrinsicHeight(), mMinSize);
Gilles Debunned88876a2012-03-16 17:34:04 -07004619 }
4620
4621 public void show() {
4622 if (isShowing()) return;
4623
4624 getPositionListener().addSubscriber(this, true /* local position may change */);
4625
4626 // Make sure the offset is always considered new, even when focusing at same position
4627 mPreviousOffset = -1;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004628 positionAtCursorOffset(getCurrentCursorOffset(), false, false);
Gilles Debunned88876a2012-03-16 17:34:04 -07004629 }
4630
4631 protected void dismiss() {
4632 mIsDragging = false;
4633 mContainer.dismiss();
4634 onDetached();
4635 }
4636
4637 public void hide() {
4638 dismiss();
4639
4640 getPositionListener().removeSubscriber(this);
4641 }
4642
Gilles Debunned88876a2012-03-16 17:34:04 -07004643 public boolean isShowing() {
4644 return mContainer.isShowing();
4645 }
4646
Mihai Popab1b423a2018-03-27 19:03:09 +01004647 private boolean shouldShow() {
4648 // A dragging handle should always be shown.
Gilles Debunned88876a2012-03-16 17:34:04 -07004649 if (mIsDragging) {
4650 return true;
4651 }
4652
4653 if (mTextView.isInBatchEditMode()) {
4654 return false;
4655 }
4656
Phil Weaverc2e28932016-12-08 12:29:25 -08004657 return mTextView.isPositionVisible(
4658 mPositionX + mHotspotX + getHorizontalOffset(), mPositionY);
Gilles Debunned88876a2012-03-16 17:34:04 -07004659 }
4660
Mihai Popab1b423a2018-03-27 19:03:09 +01004661 private void setVisible(final boolean visible) {
4662 mContainer.getContentView().setVisibility(visible ? VISIBLE : INVISIBLE);
4663 }
4664
Gilles Debunned88876a2012-03-16 17:34:04 -07004665 public abstract int getCurrentCursorOffset();
4666
4667 protected abstract void updateSelection(int offset);
4668
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004669 protected abstract void updatePosition(float x, float y, boolean fromTouchScreen);
Gilles Debunned88876a2012-03-16 17:34:04 -07004670
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004671 @MagnifierHandleTrigger
4672 protected abstract int getMagnifierHandleTrigger();
4673
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004674 protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
4675 return layout.isRtlCharAt(offset);
4676 }
4677
4678 @VisibleForTesting
4679 public float getHorizontal(@NonNull Layout layout, int offset) {
4680 return layout.getPrimaryHorizontal(offset);
4681 }
4682
4683 protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
4684 return mTextView.getOffsetAtCoordinate(line, x);
4685 }
4686
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004687 /**
4688 * @param offset Cursor offset. Must be in [-1, length].
4689 * @param forceUpdatePosition whether to force update the position. This should be true
4690 * when If the parent has been scrolled, for example.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004691 * @param fromTouchScreen {@code true} if the cursor is moved with motion events from the
4692 * touch screen.
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004693 */
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004694 protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
4695 boolean fromTouchScreen) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004696 // A HandleView relies on the layout, which may be nulled by external methods
4697 Layout layout = mTextView.getLayout();
4698 if (layout == null) {
4699 // Will update controllers' state, hiding them and stopping selection mode if needed
4700 prepareCursorControllers();
4701 return;
4702 }
Siyamed Sinir987ec652016-02-17 19:44:41 -08004703 layout = mTextView.getLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -07004704
4705 boolean offsetChanged = offset != mPreviousOffset;
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004706 if (offsetChanged || forceUpdatePosition) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004707 if (offsetChanged) {
4708 updateSelection(offset);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004709 if (fromTouchScreen && mHapticTextHandleEnabled) {
4710 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
4711 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004712 addPositionToTouchUpFilter(offset);
4713 }
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07004714 final int line = layout.getLineForOffset(offset);
Mady Mellorb9bbbb12015-03-23 11:50:46 -07004715 mPrevLine = line;
Gilles Debunned88876a2012-03-16 17:34:04 -07004716
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004717 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
4718 - getHorizontalOffset() + getCursorOffset();
Siyamed Sinira60b59d2017-07-26 09:26:41 -07004719 mPositionY = layout.getLineBottomWithoutSpacing(line);
Gilles Debunned88876a2012-03-16 17:34:04 -07004720
4721 // Take TextView's padding and scroll into account.
4722 mPositionX += mTextView.viewportToContentHorizontalOffset();
4723 mPositionY += mTextView.viewportToContentVerticalOffset();
4724
4725 mPreviousOffset = offset;
4726 mPositionHasChanged = true;
4727 }
4728 }
4729
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004730 /**
Roozbeh Pournader9c133072017-07-26 22:36:27 -07004731 * Return the clamped horizontal position for the cursor.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004732 *
4733 * @param layout Text layout.
4734 * @param offset Character offset for the cursor.
4735 * @return The clamped horizontal position for the cursor.
4736 */
4737 int getCursorHorizontalPosition(Layout layout, int offset) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004738 return (int) (getHorizontal(layout, offset) - 0.5f);
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004739 }
4740
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004741 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07004742 public void updatePosition(int parentPositionX, int parentPositionY,
4743 boolean parentPositionChanged, boolean parentScrolled) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004744 positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled, false);
Gilles Debunned88876a2012-03-16 17:34:04 -07004745 if (parentPositionChanged || mPositionHasChanged) {
4746 if (mIsDragging) {
4747 // Update touchToWindow offset in case of parent scrolling while dragging
4748 if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
4749 mTouchToWindowOffsetX += parentPositionX - mLastParentX;
4750 mTouchToWindowOffsetY += parentPositionY - mLastParentY;
4751 mLastParentX = parentPositionX;
4752 mLastParentY = parentPositionY;
4753 }
4754
4755 onHandleMoved();
4756 }
4757
Mihai Popab1b423a2018-03-27 19:03:09 +01004758 if (shouldShow()) {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004759 // Transform to the window coordinates to follow the view tranformation.
4760 final int[] pts = { mPositionX + mHotspotX + getHorizontalOffset(), mPositionY};
4761 mTextView.transformFromViewToWindowSpace(pts);
4762 pts[0] -= mHotspotX + getHorizontalOffset();
4763
Gilles Debunned88876a2012-03-16 17:34:04 -07004764 if (isShowing()) {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004765 mContainer.update(pts[0], pts[1], -1, -1);
Gilles Debunned88876a2012-03-16 17:34:04 -07004766 } else {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004767 mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY, pts[0], pts[1]);
Gilles Debunned88876a2012-03-16 17:34:04 -07004768 }
4769 } else {
4770 if (isShowing()) {
4771 dismiss();
4772 }
4773 }
4774
4775 mPositionHasChanged = false;
4776 }
4777 }
4778
4779 @Override
4780 protected void onDraw(Canvas c) {
Adam Powell3fceabd2014-08-19 18:28:04 -07004781 final int drawWidth = mDrawable.getIntrinsicWidth();
4782 final int left = getHorizontalOffset();
4783
4784 mDrawable.setBounds(left, 0, left + drawWidth, mDrawable.getIntrinsicHeight());
Gilles Debunned88876a2012-03-16 17:34:04 -07004785 mDrawable.draw(c);
4786 }
4787
Adam Powell3fceabd2014-08-19 18:28:04 -07004788 private int getHorizontalOffset() {
4789 final int width = getPreferredWidth();
4790 final int drawWidth = mDrawable.getIntrinsicWidth();
4791 final int left;
4792 switch (mHorizontalGravity) {
4793 case Gravity.LEFT:
4794 left = 0;
4795 break;
4796 default:
4797 case Gravity.CENTER:
4798 left = (width - drawWidth) / 2;
4799 break;
4800 case Gravity.RIGHT:
4801 left = width - drawWidth;
4802 break;
4803 }
4804 return left;
4805 }
4806
4807 protected int getCursorOffset() {
4808 return 0;
4809 }
4810
Mihai Popab1b423a2018-03-27 19:03:09 +01004811 private boolean tooLargeTextForMagnifier() {
4812 final float magnifierContentHeight = Math.round(
4813 mMagnifierAnimator.mMagnifier.getHeight()
4814 / mMagnifierAnimator.mMagnifier.getZoom());
4815 final Paint.FontMetrics fontMetrics = mTextView.getPaint().getFontMetrics();
4816 final float glyphHeight = fontMetrics.descent - fontMetrics.ascent;
Mihai Popa6d26d152019-01-30 15:36:47 +00004817 return glyphHeight * mTextViewScaleY > magnifierContentHeight;
Mihai Popab1b423a2018-03-27 19:03:09 +01004818 }
4819
Mihai Popa6d26d152019-01-30 15:36:47 +00004820 /**
4821 * Traverses the hierarchy above the text view, and computes the total scale applied
4822 * to it. If a rotation is encountered, the method returns {@code false}, indicating
4823 * that the magnifier should not be shown anyways. It would be nice to keep these two
4824 * pieces of logic separate (the rotation check and the total scale calculation),
4825 * but for efficiency we can do them in a single go.
4826 * @return whether the text view is rotated
4827 */
4828 private boolean checkForTransforms() {
Mihai Popaddf9fe02018-09-28 13:54:19 +01004829 if (mMagnifierAnimator.mMagnifierIsShowing) {
4830 // Do not check again when the magnifier is currently showing.
Mihai Popaddf9fe02018-09-28 13:54:19 +01004831 return true;
4832 }
Mihai Popa6d26d152019-01-30 15:36:47 +00004833
4834 if (mTextView.getRotation() != 0f || mTextView.getRotationX() != 0f
4835 || mTextView.getRotationY() != 0f) {
4836 return false;
4837 }
4838 mTextViewScaleX = mTextView.getScaleX();
4839 mTextViewScaleY = mTextView.getScaleY();
4840
Mihai Popaddf9fe02018-09-28 13:54:19 +01004841 ViewParent viewParent = mTextView.getParent();
4842 while (viewParent != null) {
Mihai Popa6d26d152019-01-30 15:36:47 +00004843 if (viewParent instanceof View) {
4844 final View view = (View) viewParent;
4845 if (view.getRotation() != 0f || view.getRotationX() != 0f
4846 || view.getRotationY() != 0f) {
4847 return false;
4848 }
4849 mTextViewScaleX *= view.getScaleX();
4850 mTextViewScaleY *= view.getScaleY();
Mihai Popaddf9fe02018-09-28 13:54:19 +01004851 }
4852 viewParent = viewParent.getParent();
4853 }
Mihai Popa6d26d152019-01-30 15:36:47 +00004854 return true;
Mihai Popaddf9fe02018-09-28 13:54:19 +01004855 }
4856
Mihai Popae3017462018-03-07 12:25:21 +00004857 /**
4858 * Computes the position where the magnifier should be shown, relative to
4859 * {@code mTextView}, and writes them to {@code showPosInView}. Also decides
4860 * whether the magnifier should be shown or dismissed after this touch event.
4861 * @return Whether the magnifier should be shown at the computed coordinates or dismissed.
4862 */
4863 private boolean obtainMagnifierShowCoordinates(@NonNull final MotionEvent event,
4864 final PointF showPosInView) {
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004865
4866 final int trigger = getMagnifierHandleTrigger();
4867 final int offset;
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004868 final int otherHandleOffset;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004869 switch (trigger) {
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004870 case MagnifierHandleTrigger.INSERTION:
4871 offset = mTextView.getSelectionStart();
4872 otherHandleOffset = -1;
4873 break;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004874 case MagnifierHandleTrigger.SELECTION_START:
4875 offset = mTextView.getSelectionStart();
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004876 otherHandleOffset = mTextView.getSelectionEnd();
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004877 break;
4878 case MagnifierHandleTrigger.SELECTION_END:
4879 offset = mTextView.getSelectionEnd();
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004880 otherHandleOffset = mTextView.getSelectionStart();
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004881 break;
4882 default:
4883 offset = -1;
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004884 otherHandleOffset = -1;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004885 break;
4886 }
4887
4888 if (offset == -1) {
Mihai Popae3017462018-03-07 12:25:21 +00004889 return false;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004890 }
4891
Mihai Popa6d26d152019-01-30 15:36:47 +00004892 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
4893 mCurrentDragInitialTouchRawX = event.getRawX();
4894 } else if (event.getActionMasked() == MotionEvent.ACTION_UP) {
4895 mCurrentDragInitialTouchRawX = UNSET_X_VALUE;
4896 }
4897
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004898 final Layout layout = mTextView.getLayout();
4899 final int lineNumber = layout.getLineForOffset(offset);
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004900 // Compute whether the selection handles are currently on the same line, and,
4901 // in this particular case, whether the selected text is right to left.
4902 final boolean sameLineSelection = otherHandleOffset != -1
4903 && lineNumber == layout.getLineForOffset(otherHandleOffset);
4904 final boolean rtl = sameLineSelection
4905 && (offset < otherHandleOffset)
4906 != (getHorizontal(mTextView.getLayout(), offset)
4907 < getHorizontal(mTextView.getLayout(), otherHandleOffset));
Mihai Popae3017462018-03-07 12:25:21 +00004908
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004909 // Horizontally move the magnifier smoothly, clamp inside the current line / selection.
Mihai Popa1d1ed0c2018-01-12 12:38:12 +00004910 final int[] textViewLocationOnScreen = new int[2];
4911 mTextView.getLocationOnScreen(textViewLocationOnScreen);
Mihai Popae3017462018-03-07 12:25:21 +00004912 final float touchXInView = event.getRawX() - textViewLocationOnScreen[0];
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004913 float leftBound = mTextView.getTotalPaddingLeft() - mTextView.getScrollX();
4914 float rightBound = mTextView.getTotalPaddingLeft() - mTextView.getScrollX();
4915 if (sameLineSelection && ((trigger == MagnifierHandleTrigger.SELECTION_END) ^ rtl)) {
4916 leftBound += getHorizontal(mTextView.getLayout(), otherHandleOffset);
4917 } else {
4918 leftBound += mTextView.getLayout().getLineLeft(lineNumber);
4919 }
4920 if (sameLineSelection && ((trigger == MagnifierHandleTrigger.SELECTION_START) ^ rtl)) {
4921 rightBound += getHorizontal(mTextView.getLayout(), otherHandleOffset);
4922 } else {
4923 rightBound += mTextView.getLayout().getLineRight(lineNumber);
4924 }
Mihai Popa6d26d152019-01-30 15:36:47 +00004925 leftBound *= mTextViewScaleX;
4926 rightBound *= mTextViewScaleX;
Mihai Popa38722382018-03-07 19:56:21 +00004927 final float contentWidth = Math.round(mMagnifierAnimator.mMagnifier.getWidth()
4928 / mMagnifierAnimator.mMagnifier.getZoom());
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004929 if (touchXInView < leftBound - contentWidth / 2
4930 || touchXInView > rightBound + contentWidth / 2) {
4931 // The touch is too far from the current line / selection, so hide the magnifier.
Mihai Popae3017462018-03-07 12:25:21 +00004932 return false;
4933 }
Mihai Popa6d26d152019-01-30 15:36:47 +00004934
4935 final float scaledTouchXInView;
4936 if (mTextViewScaleX == 1f) {
4937 // In the common case, do not use mCurrentDragInitialTouchRawX to compute this
4938 // coordinate, although the formula on the else branch should be equivalent.
4939 // Since the formula relies on mCurrentDragInitialTouchRawX being set on
4940 // MotionEvent.ACTION_DOWN, this makes us more defensive against cases when
4941 // the sequence of events might not look as expected: for example, a sequence of
4942 // ACTION_MOVE not preceded by ACTION_DOWN.
4943 scaledTouchXInView = touchXInView;
4944 } else {
4945 scaledTouchXInView = (event.getRawX() - mCurrentDragInitialTouchRawX)
4946 * mTextViewScaleX + mCurrentDragInitialTouchRawX
4947 - textViewLocationOnScreen[0];
4948 }
4949 showPosInView.x = Math.max(leftBound, Math.min(rightBound, scaledTouchXInView));
Mihai Popae3017462018-03-07 12:25:21 +00004950
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004951 // Vertically snap to middle of current line.
Mihai Popa6d26d152019-01-30 15:36:47 +00004952 showPosInView.y = ((mTextView.getLayout().getLineTop(lineNumber)
Andrei Stingaceanuca189fe2017-10-19 17:02:22 +01004953 + mTextView.getLayout().getLineBottom(lineNumber)) / 2.0f
Mihai Popa6d26d152019-01-30 15:36:47 +00004954 + mTextView.getTotalPaddingTop() - mTextView.getScrollY()) * mTextViewScaleY;
Mihai Popae3017462018-03-07 12:25:21 +00004955 return true;
4956 }
Mihai Popaa4e39c42018-02-20 15:31:11 +00004957
Mihai Popa63ee7f12018-04-05 12:01:53 +01004958 private boolean handleOverlapsMagnifier(@NonNull final HandleView handle,
4959 @NonNull final Rect magnifierRect) {
4960 final PopupWindow window = handle.mContainer;
4961 if (!window.hasDecorView()) {
4962 return false;
4963 }
4964 final Rect handleRect = new Rect(
4965 window.getDecorViewLayoutParams().x,
4966 window.getDecorViewLayoutParams().y,
4967 window.getDecorViewLayoutParams().x + window.getContentView().getWidth(),
4968 window.getDecorViewLayoutParams().y + window.getContentView().getHeight());
4969 return Rect.intersects(handleRect, magnifierRect);
Mihai Popa894469c2018-03-21 19:45:06 +00004970 }
4971
Mihai Popa63ee7f12018-04-05 12:01:53 +01004972 private @Nullable HandleView getOtherSelectionHandle() {
4973 final SelectionModifierCursorController controller = getSelectionController();
4974 if (controller == null || !controller.isActive()) {
4975 return null;
4976 }
4977 return controller.mStartHandle != this
4978 ? controller.mStartHandle
4979 : controller.mEndHandle;
4980 }
4981
Mihai Popac2e0bee2018-07-19 12:18:30 +01004982 private void updateHandlesVisibility() {
4983 final Point magnifierTopLeft = mMagnifierAnimator.mMagnifier.getPosition();
4984 if (magnifierTopLeft == null) {
4985 return;
Mihai Popa63ee7f12018-04-05 12:01:53 +01004986 }
Mihai Popac2e0bee2018-07-19 12:18:30 +01004987 final Rect surfaceInsets =
4988 mTextView.getViewRootImpl().mWindowAttributes.surfaceInsets;
4989 magnifierTopLeft.offset(-surfaceInsets.left, -surfaceInsets.top);
4990 final Rect magnifierRect = new Rect(magnifierTopLeft.x, magnifierTopLeft.y,
4991 magnifierTopLeft.x + mMagnifierAnimator.mMagnifier.getWidth(),
4992 magnifierTopLeft.y + mMagnifierAnimator.mMagnifier.getHeight());
4993 setVisible(!handleOverlapsMagnifier(HandleView.this, magnifierRect));
4994 final HandleView otherHandle = getOtherSelectionHandle();
4995 if (otherHandle != null) {
4996 otherHandle.setVisible(!handleOverlapsMagnifier(otherHandle, magnifierRect));
4997 }
4998 }
Mihai Popa63ee7f12018-04-05 12:01:53 +01004999
Mihai Popae3017462018-03-07 12:25:21 +00005000 protected final void updateMagnifier(@NonNull final MotionEvent event) {
Mihai Popa38722382018-03-07 19:56:21 +00005001 if (mMagnifierAnimator == null) {
Mihai Popae3017462018-03-07 12:25:21 +00005002 return;
5003 }
5004
5005 final PointF showPosInView = new PointF();
Mihai Popa6d26d152019-01-30 15:36:47 +00005006 final boolean shouldShow = checkForTransforms() /*check not rotated and compute scale*/
5007 && !tooLargeTextForMagnifier()
Mihai Popa894469c2018-03-21 19:45:06 +00005008 && obtainMagnifierShowCoordinates(event, showPosInView);
Mihai Popae3017462018-03-07 12:25:21 +00005009 if (shouldShow) {
5010 // Make the cursor visible and stop blinking.
5011 mRenderCursorRegardlessTiming = true;
5012 mTextView.invalidateCursorPath();
5013 suspendBlink();
Mihai Popab1b423a2018-03-27 19:03:09 +01005014
Mihai Popa38722382018-03-07 19:56:21 +00005015 mMagnifierAnimator.show(showPosInView.x, showPosInView.y);
Mihai Popac2e0bee2018-07-19 12:18:30 +01005016 updateHandlesVisibility();
Mihai Popae3017462018-03-07 12:25:21 +00005017 } else {
5018 dismissMagnifier();
5019 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005020 }
5021
5022 protected final void dismissMagnifier() {
Mihai Popa38722382018-03-07 19:56:21 +00005023 if (mMagnifierAnimator != null) {
5024 mMagnifierAnimator.dismiss();
Mihai Popaa4e39c42018-02-20 15:31:11 +00005025 mRenderCursorRegardlessTiming = false;
Andrei Stingaceanu451f9472017-10-13 16:41:28 +01005026 resumeBlink();
Mihai Popab1b423a2018-03-27 19:03:09 +01005027 setVisible(true);
Mihai Popa63ee7f12018-04-05 12:01:53 +01005028 final HandleView otherHandle = getOtherSelectionHandle();
5029 if (otherHandle != null) {
5030 otherHandle.setVisible(true);
5031 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005032 }
5033 }
5034
Gilles Debunned88876a2012-03-16 17:34:04 -07005035 @Override
5036 public boolean onTouchEvent(MotionEvent ev) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01005037 updateFloatingToolbarVisibility(ev);
5038
Gilles Debunned88876a2012-03-16 17:34:04 -07005039 switch (ev.getActionMasked()) {
5040 case MotionEvent.ACTION_DOWN: {
5041 startTouchUpFilter(getCurrentCursorOffset());
Gilles Debunned88876a2012-03-16 17:34:04 -07005042
5043 final PositionListener positionListener = getPositionListener();
5044 mLastParentX = positionListener.getPositionX();
5045 mLastParentY = positionListener.getPositionY();
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09005046 mLastParentXOnScreen = positionListener.getPositionXOnScreen();
5047 mLastParentYOnScreen = positionListener.getPositionYOnScreen();
5048
5049 final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
5050 final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
5051 mTouchToWindowOffsetX = xInWindow - mPositionX;
5052 mTouchToWindowOffsetY = yInWindow - mPositionY;
5053
Gilles Debunned88876a2012-03-16 17:34:04 -07005054 mIsDragging = true;
Mady Mellora6a0f782015-07-10 16:43:32 -07005055 mPreviousLineTouched = UNSET_LINE;
Gilles Debunned88876a2012-03-16 17:34:04 -07005056 break;
5057 }
5058
5059 case MotionEvent.ACTION_MOVE: {
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09005060 final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
5061 final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005062
5063 // Vertical hysteresis: vertical down movement tends to snap to ideal offset
5064 final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09005065 final float currentVerticalOffset = yInWindow - mPositionY - mLastParentY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005066 float newVerticalOffset;
5067 if (previousVerticalOffset < mIdealVerticalOffset) {
5068 newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
5069 newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
5070 } else {
5071 newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
5072 newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
5073 }
5074 mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
5075
Keisuke Kuroyanagibc89a5c2015-05-18 14:49:29 +09005076 final float newPosX =
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09005077 xInWindow - mTouchToWindowOffsetX + mHotspotX + getHorizontalOffset();
5078 final float newPosY = yInWindow - mTouchToWindowOffsetY + mTouchOffsetY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005079
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005080 updatePosition(newPosX, newPosY,
5081 ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Gilles Debunned88876a2012-03-16 17:34:04 -07005082 break;
5083 }
5084
5085 case MotionEvent.ACTION_UP:
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005086 filterOnTouchUp(ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005087 // Fall through.
Gilles Debunned88876a2012-03-16 17:34:04 -07005088 case MotionEvent.ACTION_CANCEL:
5089 mIsDragging = false;
Mihai Popa6315a322018-10-17 17:39:57 +01005090 updateDrawable(false /* updateDrawableWhenDragging */);
Gilles Debunned88876a2012-03-16 17:34:04 -07005091 break;
5092 }
5093 return true;
5094 }
5095
5096 public boolean isDragging() {
5097 return mIsDragging;
5098 }
5099
Clara Bayarri6351e662015-03-16 23:17:59 +00005100 void onHandleMoved() {}
Gilles Debunned88876a2012-03-16 17:34:04 -07005101
Clara Bayarri6351e662015-03-16 23:17:59 +00005102 public void onDetached() {}
Gilles Debunned88876a2012-03-16 17:34:04 -07005103 }
5104
5105 private class InsertionHandleView extends HandleView {
5106 private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
5107 private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
5108
Clara Bayarrib71dddd2015-06-04 23:17:30 +01005109 // Used to detect taps on the insertion handle, which will affect the insertion action mode
Gilles Debunned88876a2012-03-16 17:34:04 -07005110 private float mDownPositionX, mDownPositionY;
5111 private Runnable mHider;
5112
5113 public InsertionHandleView(Drawable drawable) {
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09005114 super(drawable, drawable, com.android.internal.R.id.insertion_handle);
Gilles Debunned88876a2012-03-16 17:34:04 -07005115 }
5116
5117 @Override
5118 public void show() {
5119 super.show();
5120
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01005121 final long durationSinceCutOrCopy =
Andrei Stingaceanu77b9c382015-05-06 13:25:19 +01005122 SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime;
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01005123
5124 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005125 if (mInsertionActionModeRunnable != null
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09005126 && ((mTapState == TAP_STATE_DOUBLE_TAP)
5127 || (mTapState == TAP_STATE_TRIPLE_CLICK)
5128 || isCursorInsideEasyCorrectionSpan())) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005129 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01005130 }
5131
5132 // Prepare and schedule the single tap runnable to run exactly after the double tap
5133 // timeout has passed.
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09005134 if ((mTapState != TAP_STATE_DOUBLE_TAP) && (mTapState != TAP_STATE_TRIPLE_CLICK)
5135 && !isCursorInsideEasyCorrectionSpan()
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01005136 && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION)) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01005137 if (mTextActionMode == null) {
5138 if (mInsertionActionModeRunnable == null) {
5139 mInsertionActionModeRunnable = new Runnable() {
5140 @Override
5141 public void run() {
5142 startInsertionActionMode();
5143 }
5144 };
5145 }
5146 mTextView.postDelayed(
5147 mInsertionActionModeRunnable,
5148 ViewConfiguration.getDoubleTapTimeout() + 1);
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01005149 }
5150
Gilles Debunned88876a2012-03-16 17:34:04 -07005151 }
5152
5153 hideAfterDelay();
5154 }
5155
Gilles Debunned88876a2012-03-16 17:34:04 -07005156 private void hideAfterDelay() {
5157 if (mHider == null) {
5158 mHider = new Runnable() {
5159 public void run() {
5160 hide();
5161 }
5162 };
5163 } else {
5164 removeHiderCallback();
5165 }
5166 mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
5167 }
5168
5169 private void removeHiderCallback() {
5170 if (mHider != null) {
5171 mTextView.removeCallbacks(mHider);
5172 }
5173 }
5174
5175 @Override
5176 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
5177 return drawable.getIntrinsicWidth() / 2;
5178 }
5179
5180 @Override
Adam Powell3fceabd2014-08-19 18:28:04 -07005181 protected int getHorizontalGravity(boolean isRtlRun) {
5182 return Gravity.CENTER_HORIZONTAL;
5183 }
5184
5185 @Override
5186 protected int getCursorOffset() {
5187 int offset = super.getCursorOffset();
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07005188 if (mDrawableForCursor != null) {
5189 mDrawableForCursor.getPadding(mTempRect);
5190 offset += (mDrawableForCursor.getIntrinsicWidth()
Roozbeh Pournader9c133072017-07-26 22:36:27 -07005191 - mTempRect.left - mTempRect.right) / 2;
Adam Powell3fceabd2014-08-19 18:28:04 -07005192 }
5193 return offset;
5194 }
5195
5196 @Override
Siyamed Sinir217c0f72016-02-01 18:30:02 -08005197 int getCursorHorizontalPosition(Layout layout, int offset) {
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07005198 if (mDrawableForCursor != null) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005199 final float horizontal = getHorizontal(layout, offset);
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07005200 return clampHorizontalPosition(mDrawableForCursor, horizontal) + mTempRect.left;
Siyamed Sinir217c0f72016-02-01 18:30:02 -08005201 }
5202 return super.getCursorHorizontalPosition(layout, offset);
5203 }
5204
5205 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07005206 public boolean onTouchEvent(MotionEvent ev) {
5207 final boolean result = super.onTouchEvent(ev);
5208
5209 switch (ev.getActionMasked()) {
5210 case MotionEvent.ACTION_DOWN:
5211 mDownPositionX = ev.getRawX();
5212 mDownPositionY = ev.getRawY();
Mihai Popae3017462018-03-07 12:25:21 +00005213 updateMagnifier(ev);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005214 break;
5215
5216 case MotionEvent.ACTION_MOVE:
Mihai Popae3017462018-03-07 12:25:21 +00005217 updateMagnifier(ev);
Gilles Debunned88876a2012-03-16 17:34:04 -07005218 break;
5219
5220 case MotionEvent.ACTION_UP:
5221 if (!offsetHasBeenChanged()) {
5222 final float deltaX = mDownPositionX - ev.getRawX();
5223 final float deltaY = mDownPositionY - ev.getRawY();
5224 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
5225
5226 final ViewConfiguration viewConfiguration = ViewConfiguration.get(
5227 mTextView.getContext());
5228 final int touchSlop = viewConfiguration.getScaledTouchSlop();
5229
5230 if (distanceSquared < touchSlop * touchSlop) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01005231 // Tapping on the handle toggles the insertion action mode.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005232 if (mTextActionMode != null) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005233 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07005234 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005235 startInsertionActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07005236 }
5237 }
Abodunrinwa Tokibcdf0ab2015-04-25 00:11:25 +01005238 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005239 if (mTextActionMode != null) {
5240 mTextActionMode.invalidateContentRect();
Abodunrinwa Tokibcdf0ab2015-04-25 00:11:25 +01005241 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005242 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005243 // Fall through.
Gilles Debunned88876a2012-03-16 17:34:04 -07005244 case MotionEvent.ACTION_CANCEL:
5245 hideAfterDelay();
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005246 dismissMagnifier();
Gilles Debunned88876a2012-03-16 17:34:04 -07005247 break;
5248
5249 default:
5250 break;
5251 }
5252
5253 return result;
5254 }
5255
5256 @Override
5257 public int getCurrentCursorOffset() {
5258 return mTextView.getSelectionStart();
5259 }
5260
5261 @Override
5262 public void updateSelection(int offset) {
5263 Selection.setSelection((Spannable) mTextView.getText(), offset);
5264 }
5265
5266 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005267 protected void updatePosition(float x, float y, boolean fromTouchScreen) {
Mady Melloree3821e2015-06-05 11:12:01 -07005268 Layout layout = mTextView.getLayout();
5269 int offset;
5270 if (layout != null) {
Mady Mellora6a0f782015-07-10 16:43:32 -07005271 if (mPreviousLineTouched == UNSET_LINE) {
5272 mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
5273 }
5274 int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005275 offset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellora6a0f782015-07-10 16:43:32 -07005276 mPreviousLineTouched = currLine;
Mady Melloree3821e2015-06-05 11:12:01 -07005277 } else {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005278 offset = -1;
Mady Melloree3821e2015-06-05 11:12:01 -07005279 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005280 positionAtCursorOffset(offset, false, fromTouchScreen);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005281 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01005282 invalidateActionMode();
Clara Bayarri1baed512015-05-11 15:29:16 +01005283 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005284 }
5285
5286 @Override
5287 void onHandleMoved() {
5288 super.onHandleMoved();
5289 removeHiderCallback();
5290 }
5291
5292 @Override
5293 public void onDetached() {
5294 super.onDetached();
5295 removeHiderCallback();
5296 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005297
5298 @Override
5299 @MagnifierHandleTrigger
5300 protected int getMagnifierHandleTrigger() {
5301 return MagnifierHandleTrigger.INSERTION;
5302 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005303 }
5304
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005305 @Retention(RetentionPolicy.SOURCE)
Jeff Sharkeyce8db992017-12-13 20:05:05 -07005306 @IntDef(prefix = { "HANDLE_TYPE_" }, value = {
5307 HANDLE_TYPE_SELECTION_START,
5308 HANDLE_TYPE_SELECTION_END
5309 })
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005310 public @interface HandleType {}
5311 public static final int HANDLE_TYPE_SELECTION_START = 0;
5312 public static final int HANDLE_TYPE_SELECTION_END = 1;
5313
Abodunrinwa Toki4a056a52017-08-05 01:56:40 +01005314 /** For selection handles */
5315 @VisibleForTesting
5316 public final class SelectionHandleView extends HandleView {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005317 // Indicates the handle type, selection start (HANDLE_TYPE_SELECTION_START) or selection
5318 // end (HANDLE_TYPE_SELECTION_END).
5319 @HandleType
5320 private final int mHandleType;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005321 // Indicates whether the cursor is making adjustments within a word.
5322 private boolean mInWord = false;
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005323 // Difference between touch position and word boundary position.
5324 private float mTouchWordDelta;
Mady Mellore264ac32015-06-22 16:46:29 -07005325 // X value of the previous updatePosition call.
5326 private float mPrevX;
5327 // Indicates if the handle has moved a boundary between LTR and RTL text.
5328 private boolean mLanguageDirectionChanged = false;
Mady Mellor42390aa2015-07-24 13:08:42 -07005329 // Distance from edge of horizontally scrolling text view
5330 // to use to switch to character mode.
5331 private final float mTextViewEdgeSlop;
5332 // Used to save text view location.
5333 private final int[] mTextViewLocation = new int[2];
Gilles Debunned88876a2012-03-16 17:34:04 -07005334
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005335 public SelectionHandleView(Drawable drawableLtr, Drawable drawableRtl, int id,
5336 @HandleType int handleType) {
5337 super(drawableLtr, drawableRtl, id);
5338 mHandleType = handleType;
5339 ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext());
Mady Mellor42390aa2015-07-24 13:08:42 -07005340 mTextViewEdgeSlop = viewConfiguration.getScaledTouchSlop() * 4;
Gilles Debunned88876a2012-03-16 17:34:04 -07005341 }
5342
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005343 private boolean isStartHandle() {
5344 return mHandleType == HANDLE_TYPE_SELECTION_START;
5345 }
5346
Gilles Debunned88876a2012-03-16 17:34:04 -07005347 @Override
5348 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005349 if (isRtlRun == isStartHandle()) {
Mady Mellor709386f2015-05-14 12:41:18 -07005350 return drawable.getIntrinsicWidth() / 4;
5351 } else {
5352 return (drawable.getIntrinsicWidth() * 3) / 4;
5353 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005354 }
5355
5356 @Override
Adam Powell3fceabd2014-08-19 18:28:04 -07005357 protected int getHorizontalGravity(boolean isRtlRun) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005358 return (isRtlRun == isStartHandle()) ? Gravity.LEFT : Gravity.RIGHT;
Adam Powell3fceabd2014-08-19 18:28:04 -07005359 }
5360
5361 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07005362 public int getCurrentCursorOffset() {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005363 return isStartHandle() ? mTextView.getSelectionStart() : mTextView.getSelectionEnd();
Gilles Debunned88876a2012-03-16 17:34:04 -07005364 }
5365
5366 @Override
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005367 protected void updateSelection(int offset) {
5368 if (isStartHandle()) {
5369 Selection.setSelection((Spannable) mTextView.getText(), offset,
5370 mTextView.getSelectionEnd());
5371 } else {
5372 Selection.setSelection((Spannable) mTextView.getText(),
5373 mTextView.getSelectionStart(), offset);
5374 }
Mihai Popa6315a322018-10-17 17:39:57 +01005375 updateDrawable(false /* updateDrawableWhenDragging */);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005376 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01005377 invalidateActionMode();
Clara Bayarri13152d12015-04-09 12:02:04 +01005378 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005379 }
5380
5381 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005382 protected void updatePosition(float x, float y, boolean fromTouchScreen) {
Mady Mellor81fa3e82015-05-14 09:17:41 -07005383 final Layout layout = mTextView.getLayout();
Mady Mellorcc65c372015-06-17 09:25:19 -07005384 if (layout == null) {
5385 // HandleView will deal appropriately in positionAtCursorOffset when
5386 // layout is null.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005387 positionAndAdjustForCrossingHandles(mTextView.getOffsetForPosition(x, y),
5388 fromTouchScreen);
Mady Mellorcc65c372015-06-17 09:25:19 -07005389 return;
5390 }
5391
Mady Mellora6a0f782015-07-10 16:43:32 -07005392 if (mPreviousLineTouched == UNSET_LINE) {
5393 mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
5394 }
5395
Mady Mellorb9bbbb12015-03-23 11:50:46 -07005396 boolean positionCursor = false;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005397 final int anotherHandleOffset =
5398 isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
Mady Mellora6a0f782015-07-10 16:43:32 -07005399 int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005400 int initialOffset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellor81fa3e82015-05-14 09:17:41 -07005401
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005402 if (isStartHandle() && initialOffset >= anotherHandleOffset
5403 || !isStartHandle() && initialOffset <= anotherHandleOffset) {
5404 // Handles have crossed, bound it to the first selected line and
Mady Mellor81fa3e82015-05-14 09:17:41 -07005405 // adjust by word / char as normal.
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005406 currLine = layout.getLineForOffset(anotherHandleOffset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005407 initialOffset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellor81fa3e82015-05-14 09:17:41 -07005408 }
5409
5410 int offset = initialOffset;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005411 final int wordEnd = getWordEnd(offset);
5412 final int wordStart = getWordStart(offset);
Gilles Debunned88876a2012-03-16 17:34:04 -07005413
Mady Mellore264ac32015-06-22 16:46:29 -07005414 if (mPrevX == UNSET_X_VALUE) {
5415 mPrevX = x;
5416 }
5417
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005418 final int currentOffset = getCurrentCursorOffset();
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005419 final boolean rtlAtCurrentOffset = isAtRtlRun(layout, currentOffset);
5420 final boolean atRtl = isAtRtlRun(layout, offset);
Mady Mellore264ac32015-06-22 16:46:29 -07005421 final boolean isLvlBoundary = layout.isLevelBoundary(offset);
Mady Mellore264ac32015-06-22 16:46:29 -07005422
5423 // We can't determine if the user is expanding or shrinking the selection if they're
5424 // on a bi-di boundary, so until they've moved past the boundary we'll just place
5425 // the cursor at the current position.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005426 if (isLvlBoundary || (rtlAtCurrentOffset && !atRtl) || (!rtlAtCurrentOffset && atRtl)) {
Mady Mellore264ac32015-06-22 16:46:29 -07005427 // We're on a boundary or this is the first direction change -- just update
5428 // to the current position.
5429 mLanguageDirectionChanged = true;
5430 mTouchWordDelta = 0.0f;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005431 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellore264ac32015-06-22 16:46:29 -07005432 return;
5433 } else if (mLanguageDirectionChanged && !isLvlBoundary) {
5434 // We've just moved past the boundary so update the position. After this we can
5435 // figure out if the user is expanding or shrinking to go by word or character.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005436 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellore264ac32015-06-22 16:46:29 -07005437 mTouchWordDelta = 0.0f;
5438 mLanguageDirectionChanged = false;
5439 return;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005440 }
5441
5442 boolean isExpanding;
5443 final float xDiff = x - mPrevX;
Keisuke Kuroyanagi26454142015-12-02 15:04:57 -08005444 if (isStartHandle()) {
5445 isExpanding = currLine < mPreviousLineTouched;
Mady Mellore264ac32015-06-22 16:46:29 -07005446 } else {
Keisuke Kuroyanagi26454142015-12-02 15:04:57 -08005447 isExpanding = currLine > mPreviousLineTouched;
5448 }
5449 if (atRtl == isStartHandle()) {
5450 isExpanding |= xDiff > 0;
5451 } else {
5452 isExpanding |= xDiff < 0;
Mady Mellore264ac32015-06-22 16:46:29 -07005453 }
5454
Mady Mellor42390aa2015-07-24 13:08:42 -07005455 if (mTextView.getHorizontallyScrolling()) {
5456 if (positionNearEdgeOfScrollingView(x, atRtl)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005457 && ((isStartHandle() && mTextView.getScrollX() != 0)
5458 || (!isStartHandle()
5459 && mTextView.canScrollHorizontally(atRtl ? -1 : 1)))
5460 && ((isExpanding && ((isStartHandle() && offset < currentOffset)
5461 || (!isStartHandle() && offset > currentOffset)))
5462 || !isExpanding)) {
5463 // If we're expanding ensure that the offset is actually expanding compared to
5464 // the current offset, if the handle snapped to the word, the finger position
Mady Mellor42390aa2015-07-24 13:08:42 -07005465 // may be out of sync and we don't want the selection to jump back.
5466 mTouchWordDelta = 0.0f;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005467 final int nextOffset = (atRtl == isStartHandle())
5468 ? layout.getOffsetToRightOf(mPreviousOffset)
Mady Mellor42390aa2015-07-24 13:08:42 -07005469 : layout.getOffsetToLeftOf(mPreviousOffset);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005470 positionAndAdjustForCrossingHandles(nextOffset, fromTouchScreen);
Mady Mellor42390aa2015-07-24 13:08:42 -07005471 return;
5472 }
5473 }
5474
Mady Mellore264ac32015-06-22 16:46:29 -07005475 if (isExpanding) {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005476 // User is increasing the selection.
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005477 int wordBoundary = isStartHandle() ? wordStart : wordEnd;
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005478 final boolean snapToWord = (!mInWord
5479 || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine))
5480 && atRtl == isAtRtlRun(layout, wordBoundary);
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005481 if (snapToWord) {
Mady Mellora5266832015-06-26 14:28:12 -07005482 // Sometimes words can be broken across lines (Chinese, hyphenation).
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005483 // We still snap to the word boundary but we only use the letters on the
Mady Mellora5266832015-06-26 14:28:12 -07005484 // current line to determine if the user is far enough into the word to snap.
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005485 if (layout.getLineForOffset(wordBoundary) != currLine) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005486 wordBoundary = isStartHandle()
5487 ? layout.getLineStart(currLine) : layout.getLineEnd(currLine);
Mady Mellora5266832015-06-26 14:28:12 -07005488 }
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005489 final int offsetThresholdToSnap = isStartHandle()
5490 ? wordEnd - ((wordEnd - wordBoundary) / 2)
5491 : wordStart + ((wordBoundary - wordStart) / 2);
5492 if (isStartHandle()
5493 && (offset <= offsetThresholdToSnap || currLine < mPrevLine)) {
5494 // User is far enough into the word or on a different line so we expand by
5495 // word.
5496 offset = wordStart;
5497 } else if (!isStartHandle()
5498 && (offset >= offsetThresholdToSnap || currLine > mPrevLine)) {
5499 // User is far enough into the word or on a different line so we expand by
5500 // word.
5501 offset = wordEnd;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005502 } else {
Mady Mellorc2225b92015-04-01 15:59:20 -07005503 offset = mPreviousOffset;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005504 }
5505 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005506 if ((isStartHandle() && offset < initialOffset)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005507 || (!isStartHandle() && offset > initialOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005508 final float adjustedX = getHorizontal(layout, offset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005509 mTouchWordDelta =
5510 mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
Keisuke Kuroyanagi50a927c2015-05-07 17:34:21 +09005511 } else {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005512 mTouchWordDelta = 0.0f;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005513 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005514 positionCursor = true;
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005515 } else {
5516 final int adjustedOffset =
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005517 getOffsetAtCoordinate(layout, currLine, x - mTouchWordDelta);
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005518 final boolean shrinking = isStartHandle()
5519 ? adjustedOffset > mPreviousOffset || currLine > mPrevLine
5520 : adjustedOffset < mPreviousOffset || currLine < mPrevLine;
5521 if (shrinking) {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005522 // User is shrinking the selection.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005523 if (currLine != mPrevLine) {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005524 // We're on a different line, so we'll snap to word boundaries.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005525 offset = isStartHandle() ? wordStart : wordEnd;
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005526 if ((isStartHandle() && offset < initialOffset)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005527 || (!isStartHandle() && offset > initialOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005528 final float adjustedX = getHorizontal(layout, offset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005529 mTouchWordDelta =
5530 mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
5531 } else {
5532 mTouchWordDelta = 0.0f;
5533 }
5534 } else {
5535 offset = adjustedOffset;
5536 }
5537 positionCursor = true;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005538 } else if ((isStartHandle() && adjustedOffset < mPreviousOffset)
5539 || (!isStartHandle() && adjustedOffset > mPreviousOffset)) {
5540 // Handle has jumped to the word boundary, and the user is moving
Mady Mellor43fd2f42015-06-08 14:03:34 -07005541 // their finger towards the handle, the delta should be updated.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005542 mTouchWordDelta = mTextView.convertToLocalHorizontalCoordinate(x)
5543 - getHorizontal(layout, mPreviousOffset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005544 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005545 }
5546
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005547 if (positionCursor) {
Mady Mellora6a0f782015-07-10 16:43:32 -07005548 mPreviousLineTouched = currLine;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005549 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005550 }
Mady Mellore264ac32015-06-22 16:46:29 -07005551 mPrevX = x;
Gilles Debunned88876a2012-03-16 17:34:04 -07005552 }
5553
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005554 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005555 protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
5556 boolean fromTouchScreen) {
5557 super.positionAtCursorOffset(offset, forceUpdatePosition, fromTouchScreen);
Yoshiki Iguchi9582e152015-10-15 13:34:41 +09005558 mInWord = (offset != -1) && !getWordIteratorWithText().isBoundary(offset);
Mady Mellor36d5a7b2015-05-22 10:31:12 -07005559 }
5560
5561 @Override
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005562 public boolean onTouchEvent(MotionEvent event) {
5563 boolean superResult = super.onTouchEvent(event);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005564
5565 switch (event.getActionMasked()) {
5566 case MotionEvent.ACTION_DOWN:
5567 // Reset the touch word offset and x value when the user
5568 // re-engages the handle.
5569 mTouchWordDelta = 0.0f;
5570 mPrevX = UNSET_X_VALUE;
Mihai Popae3017462018-03-07 12:25:21 +00005571 updateMagnifier(event);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005572 break;
5573
5574 case MotionEvent.ACTION_MOVE:
Mihai Popae3017462018-03-07 12:25:21 +00005575 updateMagnifier(event);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005576 break;
5577
5578 case MotionEvent.ACTION_UP:
5579 case MotionEvent.ACTION_CANCEL:
5580 dismissMagnifier();
5581 break;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005582 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005583
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005584 return superResult;
5585 }
Mady Mellor42390aa2015-07-24 13:08:42 -07005586
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005587 private void positionAndAdjustForCrossingHandles(int offset, boolean fromTouchScreen) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005588 final int anotherHandleOffset =
5589 isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
5590 if ((isStartHandle() && offset >= anotherHandleOffset)
5591 || (!isStartHandle() && offset <= anotherHandleOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005592 mTouchWordDelta = 0.0f;
5593 final Layout layout = mTextView.getLayout();
5594 if (layout != null && offset != anotherHandleOffset) {
5595 final float horiz = getHorizontal(layout, offset);
5596 final float anotherHandleHoriz = getHorizontal(layout, anotherHandleOffset,
5597 !isStartHandle());
5598 final float currentHoriz = getHorizontal(layout, mPreviousOffset);
5599 if (currentHoriz < anotherHandleHoriz && horiz < anotherHandleHoriz
5600 || currentHoriz > anotherHandleHoriz && horiz > anotherHandleHoriz) {
5601 // This handle passes another one as it crossed a direction boundary.
5602 // Don't minimize the selection, but keep the handle at the run boundary.
5603 final int currentOffset = getCurrentCursorOffset();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005604 final int offsetToGetRunRange = isStartHandle()
5605 ? currentOffset : Math.max(currentOffset - 1, 0);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005606 final long range = layout.getRunRange(offsetToGetRunRange);
5607 if (isStartHandle()) {
5608 offset = TextUtils.unpackRangeStartFromLong(range);
5609 } else {
5610 offset = TextUtils.unpackRangeEndFromLong(range);
5611 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005612 positionAtCursorOffset(offset, false, fromTouchScreen);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005613 return;
5614 }
5615 }
Mady Mellor42390aa2015-07-24 13:08:42 -07005616 // Handles can not cross and selection is at least one character.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005617 offset = getNextCursorOffset(anotherHandleOffset, !isStartHandle());
Mady Mellor42390aa2015-07-24 13:08:42 -07005618 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005619 positionAtCursorOffset(offset, false, fromTouchScreen);
Mady Mellor42390aa2015-07-24 13:08:42 -07005620 }
5621
Mady Mellor42390aa2015-07-24 13:08:42 -07005622 private boolean positionNearEdgeOfScrollingView(float x, boolean atRtl) {
5623 mTextView.getLocationOnScreen(mTextViewLocation);
5624 boolean nearEdge;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005625 if (atRtl == isStartHandle()) {
Mady Mellor42390aa2015-07-24 13:08:42 -07005626 int rightEdge = mTextViewLocation[0] + mTextView.getWidth()
5627 - mTextView.getPaddingRight();
5628 nearEdge = x > rightEdge - mTextViewEdgeSlop;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005629 } else {
5630 int leftEdge = mTextViewLocation[0] + mTextView.getPaddingLeft();
5631 nearEdge = x < leftEdge + mTextViewEdgeSlop;
Mady Mellor42390aa2015-07-24 13:08:42 -07005632 }
5633 return nearEdge;
5634 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005635
5636 @Override
5637 protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
5638 final int offsetToCheck = isStartHandle() ? offset : Math.max(offset - 1, 0);
5639 return layout.isRtlCharAt(offsetToCheck);
5640 }
5641
5642 @Override
5643 public float getHorizontal(@NonNull Layout layout, int offset) {
5644 return getHorizontal(layout, offset, isStartHandle());
5645 }
5646
5647 private float getHorizontal(@NonNull Layout layout, int offset, boolean startHandle) {
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005648 final int line = layout.getLineForOffset(offset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005649 final int offsetToCheck = startHandle ? offset : Math.max(offset - 1, 0);
5650 final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
5651 final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005652 return (isRtlChar == isRtlParagraph)
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005653 ? layout.getPrimaryHorizontal(offset) : layout.getSecondaryHorizontal(offset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005654 }
5655
5656 @Override
5657 protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
Keisuke Kuroyanagib1b88652016-04-05 16:26:16 +09005658 final float localX = mTextView.convertToLocalHorizontalCoordinate(x);
5659 final int primaryOffset = layout.getOffsetForHorizontal(line, localX, true);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005660 if (!layout.isLevelBoundary(primaryOffset)) {
5661 return primaryOffset;
5662 }
Keisuke Kuroyanagib1b88652016-04-05 16:26:16 +09005663 final int secondaryOffset = layout.getOffsetForHorizontal(line, localX, false);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005664 final int currentOffset = getCurrentCursorOffset();
5665 final int primaryDiff = Math.abs(primaryOffset - currentOffset);
5666 final int secondaryDiff = Math.abs(secondaryOffset - currentOffset);
5667 if (primaryDiff < secondaryDiff) {
5668 return primaryOffset;
5669 } else if (primaryDiff > secondaryDiff) {
5670 return secondaryOffset;
5671 } else {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005672 final int offsetToCheck = isStartHandle()
5673 ? currentOffset : Math.max(currentOffset - 1, 0);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005674 final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
5675 final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
5676 return isRtlChar == isRtlParagraph ? primaryOffset : secondaryOffset;
5677 }
5678 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005679
5680 @MagnifierHandleTrigger
5681 protected int getMagnifierHandleTrigger() {
5682 return isStartHandle()
5683 ? MagnifierHandleTrigger.SELECTION_START
5684 : MagnifierHandleTrigger.SELECTION_END;
5685 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005686 }
5687
Mady Mellorcc65c372015-06-17 09:25:19 -07005688 private int getCurrentLineAdjustedForSlop(Layout layout, int prevLine, float y) {
Mady Mellor80679072015-07-09 16:05:36 -07005689 final int trueLine = mTextView.getLineAtCoordinate(y);
Mady Mellorcc65c372015-06-17 09:25:19 -07005690 if (layout == null || prevLine > layout.getLineCount()
5691 || layout.getLineCount() <= 0 || prevLine < 0) {
5692 // Invalid parameters, just return whatever line is at y.
Mady Mellor80679072015-07-09 16:05:36 -07005693 return trueLine;
5694 }
5695
5696 if (Math.abs(trueLine - prevLine) >= 2) {
5697 // Only stick to lines if we're within a line of the previous selection.
5698 return trueLine;
Mady Mellorcc65c372015-06-17 09:25:19 -07005699 }
5700
5701 final float verticalOffset = mTextView.viewportToContentVerticalOffset();
5702 final int lineCount = layout.getLineCount();
5703 final float slop = mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS;
5704
5705 final float firstLineTop = layout.getLineTop(0) + verticalOffset;
5706 final float prevLineTop = layout.getLineTop(prevLine) + verticalOffset;
5707 final float yTopBound = Math.max(prevLineTop - slop, firstLineTop + slop);
5708
5709 final float lastLineBottom = layout.getLineBottom(lineCount - 1) + verticalOffset;
5710 final float prevLineBottom = layout.getLineBottom(prevLine) + verticalOffset;
5711 final float yBottomBound = Math.min(prevLineBottom + slop, lastLineBottom - slop);
5712
5713 // Determine if we've moved lines based on y position and previous line.
5714 int currLine;
5715 if (y <= yTopBound) {
5716 currLine = Math.max(prevLine - 1, 0);
5717 } else if (y >= yBottomBound) {
5718 currLine = Math.min(prevLine + 1, lineCount - 1);
5719 } else {
5720 currLine = prevLine;
5721 }
5722 return currLine;
5723 }
5724
Gilles Debunned88876a2012-03-16 17:34:04 -07005725 /**
5726 * A CursorController instance can be used to control a cursor in the text.
5727 */
5728 private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
5729 /**
5730 * Makes the cursor controller visible on screen.
5731 * See also {@link #hide()}.
5732 */
5733 public void show();
5734
5735 /**
5736 * Hide the cursor controller from screen.
5737 * See also {@link #show()}.
5738 */
5739 public void hide();
5740
5741 /**
5742 * Called when the view is detached from window. Perform house keeping task, such as
5743 * stopping Runnable thread that would otherwise keep a reference on the context, thus
5744 * preventing the activity from being recycled.
5745 */
5746 public void onDetached();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005747
5748 public boolean isCursorBeingModified();
5749
5750 public boolean isActive();
Gilles Debunned88876a2012-03-16 17:34:04 -07005751 }
5752
Mihai Popa6c7ad1d2018-12-04 15:45:00 +00005753 void loadCursorDrawable() {
5754 if (mDrawableForCursor == null) {
5755 mDrawableForCursor = mTextView.getTextCursorDrawable();
5756 }
5757 }
5758
Gilles Debunned88876a2012-03-16 17:34:04 -07005759 private class InsertionPointCursorController implements CursorController {
5760 private InsertionHandleView mHandle;
5761
5762 public void show() {
5763 getHandle().show();
Andrei Stingaceanu35c550c2015-05-07 16:49:49 +01005764
5765 if (mSelectionModifierCursorController != null) {
5766 mSelectionModifierCursorController.hide();
5767 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005768 }
5769
Gilles Debunned88876a2012-03-16 17:34:04 -07005770 public void hide() {
5771 if (mHandle != null) {
5772 mHandle.hide();
5773 }
5774 }
5775
5776 public void onTouchModeChanged(boolean isInTouchMode) {
5777 if (!isInTouchMode) {
5778 hide();
5779 }
5780 }
5781
5782 private InsertionHandleView getHandle() {
Gilles Debunned88876a2012-03-16 17:34:04 -07005783 if (mHandle == null) {
Mihai Popa6315a322018-10-17 17:39:57 +01005784 loadHandleDrawables(false /* overwrite */);
Gilles Debunned88876a2012-03-16 17:34:04 -07005785 mHandle = new InsertionHandleView(mSelectHandleCenter);
5786 }
5787 return mHandle;
5788 }
5789
Mihai Popa6315a322018-10-17 17:39:57 +01005790 private void reloadHandleDrawable() {
5791 if (mHandle == null) {
5792 // No need to reload, the potentially new drawable will
5793 // be used when the handle is created.
5794 return;
5795 }
5796 mHandle.setDrawables(mSelectHandleCenter, mSelectHandleCenter);
5797 }
5798
Gilles Debunned88876a2012-03-16 17:34:04 -07005799 @Override
5800 public void onDetached() {
5801 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
5802 observer.removeOnTouchModeChangeListener(this);
5803
5804 if (mHandle != null) mHandle.onDetached();
5805 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005806
5807 @Override
5808 public boolean isCursorBeingModified() {
5809 return mHandle != null && mHandle.isDragging();
5810 }
5811
5812 @Override
5813 public boolean isActive() {
5814 return mHandle != null && mHandle.isShowing();
5815 }
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09005816
5817 public void invalidateHandle() {
5818 if (mHandle != null) {
5819 mHandle.invalidate();
5820 }
5821 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005822 }
5823
5824 class SelectionModifierCursorController implements CursorController {
Gilles Debunned88876a2012-03-16 17:34:04 -07005825 // The cursor controller handles, lazily created when shown.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005826 private SelectionHandleView mStartHandle;
5827 private SelectionHandleView mEndHandle;
Gilles Debunned88876a2012-03-16 17:34:04 -07005828 // The offsets of that last touch down event. Remembered to start selection there.
5829 private int mMinTouchOffset, mMaxTouchOffset;
5830
Gilles Debunned88876a2012-03-16 17:34:04 -07005831 private float mDownPositionX, mDownPositionY;
5832 private boolean mGestureStayedInTapRegion;
5833
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005834 // Where the user first starts the drag motion.
5835 private int mStartOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005836
Mady Mellor7a936442015-05-20 10:05:52 -07005837 private boolean mHaventMovedEnoughToStartDrag;
Mady Mellorf9f8aeb2015-06-17 09:46:01 -07005838 // The line that a selection happened most recently with the drag accelerator.
5839 private int mLineSelectionIsOn = -1;
5840 // Whether the drag accelerator has selected past the initial line.
5841 private boolean mSwitchedLines = false;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005842
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005843 // Indicates the drag accelerator mode that the user is currently using.
5844 private int mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
5845 // Drag accelerator is inactive.
5846 private static final int DRAG_ACCELERATOR_MODE_INACTIVE = 0;
5847 // Character based selection by dragging. Only for mouse.
5848 private static final int DRAG_ACCELERATOR_MODE_CHARACTER = 1;
5849 // Word based selection by dragging. Enabled after long pressing or double tapping.
5850 private static final int DRAG_ACCELERATOR_MODE_WORD = 2;
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005851 // Paragraph based selection by dragging. Enabled after mouse triple click.
5852 private static final int DRAG_ACCELERATOR_MODE_PARAGRAPH = 3;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005853
Gilles Debunned88876a2012-03-16 17:34:04 -07005854 SelectionModifierCursorController() {
5855 resetTouchOffsets();
5856 }
5857
5858 public void show() {
5859 if (mTextView.isInBatchEditMode()) {
5860 return;
5861 }
Mihai Popa6315a322018-10-17 17:39:57 +01005862 loadHandleDrawables(false /* overwrite */);
Gilles Debunned88876a2012-03-16 17:34:04 -07005863 initHandles();
Gilles Debunned88876a2012-03-16 17:34:04 -07005864 }
5865
Gilles Debunned88876a2012-03-16 17:34:04 -07005866 private void initHandles() {
5867 // Lazy object creation has to be done before updatePosition() is called.
5868 if (mStartHandle == null) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005869 mStartHandle = new SelectionHandleView(mSelectHandleLeft, mSelectHandleRight,
5870 com.android.internal.R.id.selection_start_handle,
5871 HANDLE_TYPE_SELECTION_START);
Gilles Debunned88876a2012-03-16 17:34:04 -07005872 }
5873 if (mEndHandle == null) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005874 mEndHandle = new SelectionHandleView(mSelectHandleRight, mSelectHandleLeft,
5875 com.android.internal.R.id.selection_end_handle,
5876 HANDLE_TYPE_SELECTION_END);
Gilles Debunned88876a2012-03-16 17:34:04 -07005877 }
5878
5879 mStartHandle.show();
5880 mEndHandle.show();
5881
Gilles Debunned88876a2012-03-16 17:34:04 -07005882 hideInsertionPointCursorController();
5883 }
5884
Mihai Popa6315a322018-10-17 17:39:57 +01005885 private void reloadHandleDrawables() {
5886 if (mStartHandle == null) {
5887 // No need to reload, the potentially new drawables will
5888 // be used when the handles are created.
5889 return;
5890 }
5891 mStartHandle.setDrawables(mSelectHandleLeft, mSelectHandleRight);
5892 mEndHandle.setDrawables(mSelectHandleRight, mSelectHandleLeft);
5893 }
5894
Gilles Debunned88876a2012-03-16 17:34:04 -07005895 public void hide() {
5896 if (mStartHandle != null) mStartHandle.hide();
5897 if (mEndHandle != null) mEndHandle.hide();
5898 }
5899
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005900 public void enterDrag(int dragAcceleratorMode) {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005901 // Just need to init the handles / hide insertion cursor.
5902 show();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005903 mDragAcceleratorMode = dragAcceleratorMode;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005904 // Start location of selection.
5905 mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX,
5906 mLastDownPositionY);
Mady Mellorf9f8aeb2015-06-17 09:46:01 -07005907 mLineSelectionIsOn = mTextView.getLineAtCoordinate(mLastDownPositionY);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005908 // Don't show the handles until user has lifted finger.
5909 hide();
5910
5911 // This stops scrolling parents from intercepting the touch event, allowing
5912 // the user to continue dragging across the screen to select text; TextView will
5913 // scroll as necessary.
5914 mTextView.getParent().requestDisallowInterceptTouchEvent(true);
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005915 mTextView.cancelLongPress();
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005916 }
5917
Gilles Debunned88876a2012-03-16 17:34:04 -07005918 public void onTouchEvent(MotionEvent event) {
5919 // This is done even when the View does not have focus, so that long presses can start
5920 // selection and tap can move cursor from this tap position.
Mady Mellor7a936442015-05-20 10:05:52 -07005921 final float eventX = event.getX();
5922 final float eventY = event.getY();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005923 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
Gilles Debunned88876a2012-03-16 17:34:04 -07005924 switch (event.getActionMasked()) {
5925 case MotionEvent.ACTION_DOWN:
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005926 if (extractedTextModeWillBeStarted()) {
5927 // Prevent duplicating the selection handles until the mode starts.
5928 hide();
5929 } else {
5930 // Remember finger down position, to be able to start selection from there.
5931 mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(
5932 eventX, eventY);
Gilles Debunned88876a2012-03-16 17:34:04 -07005933
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005934 // Double tap detection
5935 if (mGestureStayedInTapRegion) {
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09005936 if (mTapState == TAP_STATE_DOUBLE_TAP
5937 || mTapState == TAP_STATE_TRIPLE_CLICK) {
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005938 final float deltaX = eventX - mDownPositionX;
5939 final float deltaY = eventY - mDownPositionY;
5940 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005941
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005942 ViewConfiguration viewConfiguration = ViewConfiguration.get(
5943 mTextView.getContext());
5944 int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
5945 boolean stayedInArea =
5946 distanceSquared < doubleTapSlop * doubleTapSlop;
Gilles Debunned88876a2012-03-16 17:34:04 -07005947
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005948 if (stayedInArea && (isMouse || isPositionOnText(eventX, eventY))) {
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09005949 if (mTapState == TAP_STATE_DOUBLE_TAP) {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005950 selectCurrentWordAndStartDrag();
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09005951 } else if (mTapState == TAP_STATE_TRIPLE_CLICK) {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005952 selectCurrentParagraphAndStartDrag();
5953 }
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005954 mDiscardNextActionUp = true;
5955 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005956 }
5957 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005958
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005959 mDownPositionX = eventX;
5960 mDownPositionY = eventY;
5961 mGestureStayedInTapRegion = true;
5962 mHaventMovedEnoughToStartDrag = true;
5963 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005964 break;
5965
5966 case MotionEvent.ACTION_POINTER_DOWN:
5967 case MotionEvent.ACTION_POINTER_UP:
5968 // Handle multi-point gestures. Keep min and max offset positions.
5969 // Only activated for devices that correctly handle multi-touch.
5970 if (mTextView.getContext().getPackageManager().hasSystemFeature(
5971 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
5972 updateMinAndMaxOffsets(event);
5973 }
5974 break;
5975
5976 case MotionEvent.ACTION_MOVE:
Mady Mellor7a936442015-05-20 10:05:52 -07005977 final ViewConfiguration viewConfig = ViewConfiguration.get(
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005978 mTextView.getContext());
Mady Mellor7a936442015-05-20 10:05:52 -07005979 final int touchSlop = viewConfig.getScaledTouchSlop();
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005980
Mady Mellor7a936442015-05-20 10:05:52 -07005981 if (mGestureStayedInTapRegion || mHaventMovedEnoughToStartDrag) {
5982 final float deltaX = eventX - mDownPositionX;
5983 final float deltaY = eventY - mDownPositionY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005984 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
5985
Mady Mellor7a936442015-05-20 10:05:52 -07005986 if (mGestureStayedInTapRegion) {
5987 int doubleTapTouchSlop = viewConfig.getScaledDoubleTapTouchSlop();
5988 mGestureStayedInTapRegion =
5989 distanceSquared <= doubleTapTouchSlop * doubleTapTouchSlop;
5990 }
5991 if (mHaventMovedEnoughToStartDrag) {
5992 // We don't start dragging until the user has moved enough.
5993 mHaventMovedEnoughToStartDrag =
5994 distanceSquared <= touchSlop * touchSlop;
Gilles Debunned88876a2012-03-16 17:34:04 -07005995 }
5996 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005997
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005998 if (isMouse && !isDragAcceleratorActive()) {
5999 final int offset = mTextView.getOffsetForPosition(eventX, eventY);
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09006000 if (mTextView.hasSelection()
6001 && (!mHaventMovedEnoughToStartDrag || mStartOffset != offset)
6002 && offset >= mTextView.getSelectionStart()
6003 && offset <= mTextView.getSelectionEnd()) {
6004 startDragAndDrop();
6005 break;
6006 }
6007
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006008 if (mStartOffset != offset) {
6009 // Start character based drag accelerator.
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006010 stopTextActionMode();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006011 enterDrag(DRAG_ACCELERATOR_MODE_CHARACTER);
6012 mDiscardNextActionUp = true;
6013 mHaventMovedEnoughToStartDrag = false;
6014 }
6015 }
6016
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006017 if (mStartHandle != null && mStartHandle.isShowing()) {
6018 // Don't do the drag if the handles are showing already.
6019 break;
6020 }
6021
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006022 updateSelection(event);
Gilles Debunned88876a2012-03-16 17:34:04 -07006023 break;
6024
6025 case MotionEvent.ACTION_UP:
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006026 if (!isDragAcceleratorActive()) {
6027 break;
6028 }
6029 updateSelection(event);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006030
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006031 // No longer dragging to select text, let the parent intercept events.
6032 mTextView.getParent().requestDisallowInterceptTouchEvent(false);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006033
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006034 // No longer the first dragging motion, reset.
6035 resetDragAcceleratorState();
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09006036
6037 if (mTextView.hasSelection()) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01006038 // Drag selection should not be adjusted by the text classifier.
6039 startSelectionActionModeAsync(mHaventMovedEnoughToStartDrag);
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09006040 }
Gilles Debunned88876a2012-03-16 17:34:04 -07006041 break;
6042 }
6043 }
6044
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006045 private void updateSelection(MotionEvent event) {
6046 if (mTextView.getLayout() != null) {
6047 switch (mDragAcceleratorMode) {
6048 case DRAG_ACCELERATOR_MODE_CHARACTER:
6049 updateCharacterBasedSelection(event);
6050 break;
6051 case DRAG_ACCELERATOR_MODE_WORD:
6052 updateWordBasedSelection(event);
6053 break;
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09006054 case DRAG_ACCELERATOR_MODE_PARAGRAPH:
6055 updateParagraphBasedSelection(event);
6056 break;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006057 }
6058 }
6059 }
6060
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09006061 /**
6062 * If the TextView allows text selection, selects the current paragraph and starts a drag.
6063 *
6064 * @return true if the drag was started.
6065 */
6066 private boolean selectCurrentParagraphAndStartDrag() {
6067 if (mInsertionActionModeRunnable != null) {
6068 mTextView.removeCallbacks(mInsertionActionModeRunnable);
6069 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006070 stopTextActionMode();
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09006071 if (!selectCurrentParagraph()) {
6072 return false;
6073 }
6074 enterDrag(SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_PARAGRAPH);
6075 return true;
6076 }
6077
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006078 private void updateCharacterBasedSelection(MotionEvent event) {
6079 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07006080 updateSelectionInternal(mStartOffset, offset,
6081 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006082 }
6083
6084 private void updateWordBasedSelection(MotionEvent event) {
6085 if (mHaventMovedEnoughToStartDrag) {
6086 return;
6087 }
6088 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
6089 final ViewConfiguration viewConfig = ViewConfiguration.get(
6090 mTextView.getContext());
6091 final float eventX = event.getX();
6092 final float eventY = event.getY();
6093 final int currLine;
6094 if (isMouse) {
6095 // No need to offset the y coordinate for mouse input.
6096 currLine = mTextView.getLineAtCoordinate(eventY);
6097 } else {
6098 float y = eventY;
6099 if (mSwitchedLines) {
6100 // Offset the finger by the same vertical offset as the handles.
6101 // This improves visibility of the content being selected by
6102 // shifting the finger below the content, this is applied once
6103 // the user has switched lines.
6104 final int touchSlop = viewConfig.getScaledTouchSlop();
6105 final float fingerOffset = (mStartHandle != null)
6106 ? mStartHandle.getIdealVerticalOffset()
6107 : touchSlop;
6108 y = eventY - fingerOffset;
6109 }
6110
6111 currLine = getCurrentLineAdjustedForSlop(mTextView.getLayout(), mLineSelectionIsOn,
6112 y);
6113 if (!mSwitchedLines && currLine != mLineSelectionIsOn) {
6114 // Break early here, we want to offset the finger position from
6115 // the selection highlight, once the user moved their finger
6116 // to a different line we should apply the offset and *not* switch
6117 // lines until recomputing the position with the finger offset.
6118 mSwitchedLines = true;
6119 return;
6120 }
6121 }
6122
6123 int startOffset;
6124 int offset = mTextView.getOffsetAtCoordinate(currLine, eventX);
6125 // Snap to word boundaries.
6126 if (mStartOffset < offset) {
6127 // Expanding with end handle.
6128 offset = getWordEnd(offset);
6129 startOffset = getWordStart(mStartOffset);
6130 } else {
6131 // Expanding with start handle.
6132 offset = getWordStart(offset);
6133 startOffset = getWordEnd(mStartOffset);
Keisuke Kuroyanagi133dfc02016-07-21 18:07:23 +09006134 if (startOffset == offset) {
6135 offset = getNextCursorOffset(offset, false);
6136 }
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006137 }
6138 mLineSelectionIsOn = currLine;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07006139 updateSelectionInternal(startOffset, offset,
6140 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006141 }
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09006142
6143 private void updateParagraphBasedSelection(MotionEvent event) {
6144 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
6145
6146 final int start = Math.min(offset, mStartOffset);
6147 final int end = Math.max(offset, mStartOffset);
6148 final long paragraphsRange = getParagraphsRange(start, end);
6149 final int selectionStart = TextUtils.unpackRangeStartFromLong(paragraphsRange);
6150 final int selectionEnd = TextUtils.unpackRangeEndFromLong(paragraphsRange);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07006151 updateSelectionInternal(selectionStart, selectionEnd,
6152 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
6153 }
6154
6155 private void updateSelectionInternal(int selectionStart, int selectionEnd,
6156 boolean fromTouchScreen) {
6157 final boolean performHapticFeedback = fromTouchScreen && mHapticTextHandleEnabled
6158 && ((mTextView.getSelectionStart() != selectionStart)
6159 || (mTextView.getSelectionEnd() != selectionEnd));
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09006160 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07006161 if (performHapticFeedback) {
6162 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
6163 }
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09006164 }
6165
Gilles Debunned88876a2012-03-16 17:34:04 -07006166 /**
6167 * @param event
6168 */
6169 private void updateMinAndMaxOffsets(MotionEvent event) {
6170 int pointerCount = event.getPointerCount();
6171 for (int index = 0; index < pointerCount; index++) {
6172 int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
6173 if (offset < mMinTouchOffset) mMinTouchOffset = offset;
6174 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
6175 }
6176 }
6177
6178 public int getMinTouchOffset() {
6179 return mMinTouchOffset;
6180 }
6181
6182 public int getMaxTouchOffset() {
6183 return mMaxTouchOffset;
6184 }
6185
6186 public void resetTouchOffsets() {
6187 mMinTouchOffset = mMaxTouchOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006188 resetDragAcceleratorState();
6189 }
6190
6191 private void resetDragAcceleratorState() {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006192 mStartOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006193 mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
Mady Mellorf9f8aeb2015-06-17 09:46:01 -07006194 mSwitchedLines = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006195 final int selectionStart = mTextView.getSelectionStart();
6196 final int selectionEnd = mTextView.getSelectionEnd();
Clara Bayarri4e518772018-03-27 14:25:33 +01006197 if (selectionStart < 0 || selectionEnd < 0) {
6198 Selection.removeSelection((Spannable) mTextView.getText());
6199 } else if (selectionStart > selectionEnd) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006200 Selection.setSelection((Spannable) mTextView.getText(),
6201 selectionEnd, selectionStart);
6202 }
Gilles Debunned88876a2012-03-16 17:34:04 -07006203 }
6204
6205 /**
6206 * @return true iff this controller is currently used to move the selection start.
6207 */
6208 public boolean isSelectionStartDragged() {
6209 return mStartHandle != null && mStartHandle.isDragging();
6210 }
6211
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006212 @Override
6213 public boolean isCursorBeingModified() {
6214 return isDragAcceleratorActive() || isSelectionStartDragged()
6215 || (mEndHandle != null && mEndHandle.isDragging());
6216 }
6217
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006218 /**
6219 * @return true if the user is selecting text using the drag accelerator.
6220 */
6221 public boolean isDragAcceleratorActive() {
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006222 return mDragAcceleratorMode != DRAG_ACCELERATOR_MODE_INACTIVE;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006223 }
6224
Gilles Debunned88876a2012-03-16 17:34:04 -07006225 public void onTouchModeChanged(boolean isInTouchMode) {
6226 if (!isInTouchMode) {
6227 hide();
6228 }
6229 }
6230
6231 @Override
6232 public void onDetached() {
6233 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
6234 observer.removeOnTouchModeChangeListener(this);
6235
6236 if (mStartHandle != null) mStartHandle.onDetached();
6237 if (mEndHandle != null) mEndHandle.onDetached();
6238 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006239
6240 @Override
6241 public boolean isActive() {
6242 return mStartHandle != null && mStartHandle.isShowing();
6243 }
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09006244
6245 public void invalidateHandles() {
6246 if (mStartHandle != null) {
6247 mStartHandle.invalidate();
6248 }
6249 if (mEndHandle != null) {
6250 mEndHandle.invalidate();
6251 }
6252 }
Gilles Debunned88876a2012-03-16 17:34:04 -07006253 }
6254
Mihai Popa6315a322018-10-17 17:39:57 +01006255 /**
6256 * Loads the insertion and selection handle Drawables from TextView. If the handle
6257 * drawables are already loaded, do not overwrite them unless the method parameter
6258 * is set to true. This logic is required to avoid overwriting Drawables assigned
6259 * to mSelectHandle[Center/Left/Right] by developers using reflection, unless they
6260 * explicitly call the setters in TextView.
6261 *
6262 * @param overwrite whether to overwrite already existing nonnull Drawables
6263 */
6264 void loadHandleDrawables(final boolean overwrite) {
6265 if (mSelectHandleCenter == null || overwrite) {
6266 mSelectHandleCenter = mTextView.getTextSelectHandle();
6267 if (hasInsertionController()) {
6268 getInsertionController().reloadHandleDrawable();
6269 }
6270 }
6271
6272 if (mSelectHandleLeft == null || mSelectHandleRight == null || overwrite) {
6273 mSelectHandleLeft = mTextView.getTextSelectHandleLeft();
6274 mSelectHandleRight = mTextView.getTextSelectHandleRight();
6275 if (hasSelectionController()) {
6276 getSelectionController().reloadHandleDrawables();
6277 }
6278 }
6279 }
6280
Gilles Debunned88876a2012-03-16 17:34:04 -07006281 private class CorrectionHighlighter {
6282 private final Path mPath = new Path();
Chris Craik6a49dde2015-05-12 10:28:14 -07006283 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
Gilles Debunned88876a2012-03-16 17:34:04 -07006284 private int mStart, mEnd;
6285 private long mFadingStartTime;
6286 private RectF mTempRectF;
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006287 private static final int FADE_OUT_DURATION = 400;
Gilles Debunned88876a2012-03-16 17:34:04 -07006288
6289 public CorrectionHighlighter() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006290 mPaint.setCompatibilityScaling(
6291 mTextView.getResources().getCompatibilityInfo().applicationScale);
Gilles Debunned88876a2012-03-16 17:34:04 -07006292 mPaint.setStyle(Paint.Style.FILL);
6293 }
6294
6295 public void highlight(CorrectionInfo info) {
6296 mStart = info.getOffset();
6297 mEnd = mStart + info.getNewText().length();
6298 mFadingStartTime = SystemClock.uptimeMillis();
6299
6300 if (mStart < 0 || mEnd < 0) {
6301 stopAnimation();
6302 }
6303 }
6304
6305 public void draw(Canvas canvas, int cursorOffsetVertical) {
6306 if (updatePath() && updatePaint()) {
6307 if (cursorOffsetVertical != 0) {
6308 canvas.translate(0, cursorOffsetVertical);
6309 }
6310
6311 canvas.drawPath(mPath, mPaint);
6312
6313 if (cursorOffsetVertical != 0) {
6314 canvas.translate(0, -cursorOffsetVertical);
6315 }
6316 invalidate(true); // TODO invalidate cursor region only
6317 } else {
6318 stopAnimation();
6319 invalidate(false); // TODO invalidate cursor region only
6320 }
6321 }
6322
6323 private boolean updatePaint() {
6324 final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
6325 if (duration > FADE_OUT_DURATION) return false;
6326
6327 final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
6328 final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006329 final int color = (mTextView.mHighlightColor & 0x00FFFFFF)
6330 + ((int) (highlightColorAlpha * coef) << 24);
Gilles Debunned88876a2012-03-16 17:34:04 -07006331 mPaint.setColor(color);
6332 return true;
6333 }
6334
6335 private boolean updatePath() {
6336 final Layout layout = mTextView.getLayout();
6337 if (layout == null) return false;
6338
6339 // Update in case text is edited while the animation is run
6340 final int length = mTextView.getText().length();
6341 int start = Math.min(length, mStart);
6342 int end = Math.min(length, mEnd);
6343
6344 mPath.reset();
6345 layout.getSelectionPath(start, end, mPath);
6346 return true;
6347 }
6348
6349 private void invalidate(boolean delayed) {
6350 if (mTextView.getLayout() == null) return;
6351
6352 if (mTempRectF == null) mTempRectF = new RectF();
6353 mPath.computeBounds(mTempRectF, false);
6354
6355 int left = mTextView.getCompoundPaddingLeft();
6356 int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
6357
6358 if (delayed) {
6359 mTextView.postInvalidateOnAnimation(
6360 left + (int) mTempRectF.left, top + (int) mTempRectF.top,
6361 left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
6362 } else {
6363 mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
6364 (int) mTempRectF.right, (int) mTempRectF.bottom);
6365 }
6366 }
6367
6368 private void stopAnimation() {
6369 Editor.this.mCorrectionHighlighter = null;
6370 }
6371 }
6372
6373 private static class ErrorPopup extends PopupWindow {
6374 private boolean mAbove = false;
6375 private final TextView mView;
6376 private int mPopupInlineErrorBackgroundId = 0;
6377 private int mPopupInlineErrorAboveBackgroundId = 0;
6378
6379 ErrorPopup(TextView v, int width, int height) {
6380 super(v, width, height);
6381 mView = v;
6382 // Make sure the TextView has a background set as it will be used the first time it is
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -08006383 // shown and positioned. Initialized with below background, which should have
Gilles Debunned88876a2012-03-16 17:34:04 -07006384 // dimensions identical to the above version for this to work (and is more likely).
6385 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
6386 com.android.internal.R.styleable.Theme_errorMessageBackground);
6387 mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
6388 }
6389
6390 void fixDirection(boolean above) {
6391 mAbove = above;
6392
6393 if (above) {
6394 mPopupInlineErrorAboveBackgroundId =
6395 getResourceId(mPopupInlineErrorAboveBackgroundId,
6396 com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
6397 } else {
6398 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
6399 com.android.internal.R.styleable.Theme_errorMessageBackground);
6400 }
6401
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006402 mView.setBackgroundResource(
6403 above ? mPopupInlineErrorAboveBackgroundId : mPopupInlineErrorBackgroundId);
Gilles Debunned88876a2012-03-16 17:34:04 -07006404 }
6405
6406 private int getResourceId(int currentId, int index) {
6407 if (currentId == 0) {
6408 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
6409 R.styleable.Theme);
6410 currentId = styledAttributes.getResourceId(index, 0);
6411 styledAttributes.recycle();
6412 }
6413 return currentId;
6414 }
6415
6416 @Override
6417 public void update(int x, int y, int w, int h, boolean force) {
6418 super.update(x, y, w, h, force);
6419
6420 boolean above = isAboveAnchor();
6421 if (above != mAbove) {
6422 fixDirection(above);
6423 }
6424 }
6425 }
6426
6427 static class InputContentType {
6428 int imeOptions = EditorInfo.IME_NULL;
Mathew Inwood978c6e22018-08-21 15:58:55 +01006429 @UnsupportedAppUsage
Gilles Debunned88876a2012-03-16 17:34:04 -07006430 String privateImeOptions;
6431 CharSequence imeActionLabel;
6432 int imeActionId;
6433 Bundle extras;
6434 OnEditorActionListener onEditorActionListener;
6435 boolean enterDown;
Yohei Yukawad469f212016-01-21 12:38:09 -08006436 LocaleList imeHintLocales;
Gilles Debunned88876a2012-03-16 17:34:04 -07006437 }
6438
6439 static class InputMethodState {
Gilles Debunnec62589c2012-04-12 14:50:23 -07006440 ExtractedTextRequest mExtractedTextRequest;
6441 final ExtractedText mExtractedText = new ExtractedText();
Gilles Debunned88876a2012-03-16 17:34:04 -07006442 int mBatchEditNesting;
6443 boolean mCursorChanged;
6444 boolean mSelectionModeChanged;
6445 boolean mContentChanged;
6446 int mChangedStart, mChangedEnd, mChangedDelta;
6447 }
Satoshi Kataoka0e3849a2012-12-13 14:37:19 +09006448
James Cookf59152c2015-02-26 18:03:58 -08006449 /**
James Cook471559f2015-02-27 10:31:20 -08006450 * @return True iff (start, end) is a valid range within the text.
6451 */
6452 private static boolean isValidRange(CharSequence text, int start, int end) {
6453 return 0 <= start && start <= end && end <= text.length();
6454 }
6455
6456 /**
James Cookf59152c2015-02-26 18:03:58 -08006457 * An InputFilter that monitors text input to maintain undo history. It does not modify the
6458 * text being typed (and hence always returns null from the filter() method).
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006459 *
6460 * TODO: Make this span aware.
James Cookf59152c2015-02-26 18:03:58 -08006461 */
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006462 public static class UndoInputFilter implements InputFilter {
James Cookf59152c2015-02-26 18:03:58 -08006463 private final Editor mEditor;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006464
James Cook48e0fac2015-02-25 15:44:51 -08006465 // Whether the current filter pass is directly caused by an end-user text edit.
6466 private boolean mIsUserEdit;
6467
James Cookd2026682015-03-03 14:40:14 -08006468 // Whether the text field is handling an IME composition. Must be parceled in case the user
6469 // rotates the screen during composition.
6470 private boolean mHasComposition;
James Cook48e0fac2015-02-25 15:44:51 -08006471
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006472 // Whether the user is expanding or shortening the text
6473 private boolean mExpanding;
6474
6475 // Whether the previous edit operation was in the current batch edit.
6476 private boolean mPreviousOperationWasInSameBatchEdit;
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08006477
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006478 public UndoInputFilter(Editor editor) {
6479 mEditor = editor;
6480 }
6481
James Cookd2026682015-03-03 14:40:14 -08006482 public void saveInstanceState(Parcel parcel) {
6483 parcel.writeInt(mIsUserEdit ? 1 : 0);
6484 parcel.writeInt(mHasComposition ? 1 : 0);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006485 parcel.writeInt(mExpanding ? 1 : 0);
6486 parcel.writeInt(mPreviousOperationWasInSameBatchEdit ? 1 : 0);
James Cookd2026682015-03-03 14:40:14 -08006487 }
6488
6489 public void restoreInstanceState(Parcel parcel) {
6490 mIsUserEdit = parcel.readInt() != 0;
6491 mHasComposition = parcel.readInt() != 0;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006492 mExpanding = parcel.readInt() != 0;
6493 mPreviousOperationWasInSameBatchEdit = parcel.readInt() != 0;
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08006494 }
6495
James Cook48e0fac2015-02-25 15:44:51 -08006496 /**
6497 * Signals that a user-triggered edit is starting.
6498 */
6499 public void beginBatchEdit() {
6500 if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit");
6501 mIsUserEdit = true;
James Cook48e0fac2015-02-25 15:44:51 -08006502 }
6503
6504 public void endBatchEdit() {
6505 if (DEBUG_UNDO) Log.d(TAG, "endBatchEdit");
6506 mIsUserEdit = false;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006507 mPreviousOperationWasInSameBatchEdit = false;
James Cook48e0fac2015-02-25 15:44:51 -08006508 }
6509
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006510 @Override
6511 public CharSequence filter(CharSequence source, int start, int end,
6512 Spanned dest, int dstart, int dend) {
6513 if (DEBUG_UNDO) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006514 Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ") "
6515 + "dest=" + dest + " (" + dstart + "-" + dend + ")");
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006516 }
James Cookf1dad1e2015-02-27 11:00:01 -08006517
James Cook48e0fac2015-02-25 15:44:51 -08006518 // Check to see if this edit should be tracked for undo.
6519 if (!canUndoEdit(source, start, end, dest, dstart, dend)) {
James Cookf1dad1e2015-02-27 11:00:01 -08006520 return null;
6521 }
6522
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006523 final boolean hadComposition = mHasComposition;
6524 mHasComposition = isComposition(source);
6525 final boolean wasExpanding = mExpanding;
6526 boolean shouldCreateSeparateState = false;
6527 if ((end - start) != (dend - dstart)) {
6528 mExpanding = (end - start) > (dend - dstart);
6529 if (hadComposition && mExpanding != wasExpanding) {
6530 shouldCreateSeparateState = true;
6531 }
James Cookd2026682015-03-03 14:40:14 -08006532 }
6533
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006534 // Handle edit.
6535 handleEdit(source, start, end, dest, dstart, dend, shouldCreateSeparateState);
James Cookd2026682015-03-03 14:40:14 -08006536 return null;
6537 }
6538
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09006539 void freezeLastEdit() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006540 mEditor.mUndoManager.beginUpdate("Edit text");
6541 EditOperation lastEdit = getLastEdit();
6542 if (lastEdit != null) {
6543 lastEdit.mFrozen = true;
James Cookd2026682015-03-03 14:40:14 -08006544 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006545 mEditor.mUndoManager.endUpdate();
James Cookd2026682015-03-03 14:40:14 -08006546 }
6547
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006548 @Retention(RetentionPolicy.SOURCE)
Jeff Sharkeyce8db992017-12-13 20:05:05 -07006549 @IntDef(prefix = { "MERGE_EDIT_MODE_" }, value = {
6550 MERGE_EDIT_MODE_FORCE_MERGE,
6551 MERGE_EDIT_MODE_NEVER_MERGE,
6552 MERGE_EDIT_MODE_NORMAL
6553 })
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006554 private @interface MergeMode {}
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006555 private static final int MERGE_EDIT_MODE_FORCE_MERGE = 0;
6556 private static final int MERGE_EDIT_MODE_NEVER_MERGE = 1;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006557 /** Use {@link EditOperation#mergeWith} to merge */
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006558 private static final int MERGE_EDIT_MODE_NORMAL = 2;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006559
6560 private void handleEdit(CharSequence source, int start, int end,
6561 Spanned dest, int dstart, int dend, boolean shouldCreateSeparateState) {
James Cook48e0fac2015-02-25 15:44:51 -08006562 // An application may install a TextWatcher to provide additional modifications after
6563 // the initial input filters run (e.g. a credit card formatter that adds spaces to a
6564 // string). This results in multiple filter() calls for what the user considers to be
6565 // a single operation. Always undo the whole set of changes in one step.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006566 @MergeMode
6567 final int mergeMode;
6568 if (isInTextWatcher() || mPreviousOperationWasInSameBatchEdit) {
6569 mergeMode = MERGE_EDIT_MODE_FORCE_MERGE;
6570 } else if (shouldCreateSeparateState) {
6571 mergeMode = MERGE_EDIT_MODE_NEVER_MERGE;
6572 } else {
6573 mergeMode = MERGE_EDIT_MODE_NORMAL;
6574 }
James Cook471559f2015-02-27 10:31:20 -08006575 // Build a new operation with all the information from this edit.
James Cookd2026682015-03-03 14:40:14 -08006576 String newText = TextUtils.substring(source, start, end);
6577 String oldText = TextUtils.substring(dest, dstart, dend);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006578 EditOperation edit = new EditOperation(mEditor, oldText, dstart, newText,
6579 mHasComposition);
6580 if (mHasComposition && TextUtils.equals(edit.mNewText, edit.mOldText)) {
6581 return;
6582 }
6583 recordEdit(edit, mergeMode);
James Cookd2026682015-03-03 14:40:14 -08006584 }
James Cook471559f2015-02-27 10:31:20 -08006585
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006586 private EditOperation getLastEdit() {
6587 final UndoManager um = mEditor.mUndoManager;
6588 return um.getLastOperation(
6589 EditOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE);
6590 }
James Cook22054252015-03-25 14:04:01 -07006591 /**
6592 * Fetches the last undo operation and checks to see if a new edit should be merged into it.
6593 * If forceMerge is true then the new edit is always merged.
6594 */
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006595 private void recordEdit(EditOperation edit, @MergeMode int mergeMode) {
James Cook471559f2015-02-27 10:31:20 -08006596 // Fetch the last edit operation and attempt to merge in the new edit.
James Cook48e0fac2015-02-25 15:44:51 -08006597 final UndoManager um = mEditor.mUndoManager;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006598 um.beginUpdate("Edit text");
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006599 EditOperation lastEdit = getLastEdit();
James Cook471559f2015-02-27 10:31:20 -08006600 if (lastEdit == null) {
6601 // Add this as the first edit.
6602 if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit);
6603 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006604 } else if (mergeMode == MERGE_EDIT_MODE_FORCE_MERGE) {
James Cook22054252015-03-25 14:04:01 -07006605 // Forced merges take priority because they could be the result of a non-user-edit
6606 // change and this case should not create a new undo operation.
6607 if (DEBUG_UNDO) Log.d(TAG, "filter: force merge " + edit);
6608 lastEdit.forceMergeWith(edit);
James Cook48e0fac2015-02-25 15:44:51 -08006609 } else if (!mIsUserEdit) {
6610 // An application directly modified the Editable outside of a text edit. Treat this
6611 // as a new change and don't attempt to merge.
6612 if (DEBUG_UNDO) Log.d(TAG, "non-user edit, new op " + edit);
6613 um.commitState(mEditor.mUndoOwner);
6614 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006615 } else if (mergeMode == MERGE_EDIT_MODE_NORMAL && lastEdit.mergeWith(edit)) {
James Cook471559f2015-02-27 10:31:20 -08006616 // Merge succeeded, nothing else to do.
6617 if (DEBUG_UNDO) Log.d(TAG, "filter: merge succeeded, created " + lastEdit);
James Cook3ac0bcb2015-02-26 10:53:41 -08006618 } else {
James Cook471559f2015-02-27 10:31:20 -08006619 // Could not merge with the last edit, so commit the last edit and add this edit.
6620 if (DEBUG_UNDO) Log.d(TAG, "filter: merge failed, adding " + edit);
6621 um.commitState(mEditor.mUndoOwner);
6622 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
James Cook3ac0bcb2015-02-26 10:53:41 -08006623 }
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09006624 mPreviousOperationWasInSameBatchEdit = mIsUserEdit;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006625 um.endUpdate();
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006626 }
James Cook48e0fac2015-02-25 15:44:51 -08006627
6628 private boolean canUndoEdit(CharSequence source, int start, int end,
6629 Spanned dest, int dstart, int dend) {
6630 if (!mEditor.mAllowUndo) {
6631 if (DEBUG_UNDO) Log.d(TAG, "filter: undo is disabled");
6632 return false;
6633 }
6634
6635 if (mEditor.mUndoManager.isInUndo()) {
6636 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping, currently performing undo/redo");
6637 return false;
6638 }
6639
6640 // Text filters run before input operations are applied. However, some input operations
6641 // are invalid and will throw exceptions when applied. This is common in tests. Don't
6642 // attempt to undo invalid operations.
6643 if (!isValidRange(source, start, end) || !isValidRange(dest, dstart, dend)) {
6644 if (DEBUG_UNDO) Log.d(TAG, "filter: invalid op");
6645 return false;
6646 }
6647
6648 // Earlier filters can rewrite input to be a no-op, for example due to a length limit
6649 // on an input field. Skip no-op changes.
6650 if (start == end && dstart == dend) {
6651 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping no-op");
6652 return false;
6653 }
6654
6655 return true;
6656 }
James Cookd2026682015-03-03 14:40:14 -08006657
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006658 private static boolean isComposition(CharSequence source) {
James Cookd2026682015-03-03 14:40:14 -08006659 if (!(source instanceof Spannable)) {
6660 return false;
6661 }
6662 // This is a composition edit if the source has a non-zero-length composing span.
6663 Spannable text = (Spannable) source;
6664 int composeBegin = EditableInputConnection.getComposingSpanStart(text);
6665 int composeEnd = EditableInputConnection.getComposingSpanEnd(text);
6666 return composeBegin < composeEnd;
6667 }
6668
6669 private boolean isInTextWatcher() {
6670 CharSequence text = mEditor.mTextView.getText();
6671 return (text instanceof SpannableStringBuilder)
6672 && ((SpannableStringBuilder) text).getTextWatcherDepth() > 0;
6673 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006674 }
6675
James Cookf59152c2015-02-26 18:03:58 -08006676 /**
6677 * An operation to undo a single "edit" to a text view.
6678 */
James Cook471559f2015-02-27 10:31:20 -08006679 public static class EditOperation extends UndoOperation<Editor> {
6680 private static final int TYPE_INSERT = 0;
6681 private static final int TYPE_DELETE = 1;
6682 private static final int TYPE_REPLACE = 2;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006683
James Cook471559f2015-02-27 10:31:20 -08006684 private int mType;
6685 private String mOldText;
James Cook471559f2015-02-27 10:31:20 -08006686 private String mNewText;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006687 private int mStart;
James Cook471559f2015-02-27 10:31:20 -08006688
6689 private int mOldCursorPos;
6690 private int mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006691 private boolean mFrozen;
6692 private boolean mIsComposition;
James Cook471559f2015-02-27 10:31:20 -08006693
6694 /**
James Cookd2026682015-03-03 14:40:14 -08006695 * Constructs an edit operation from a text input operation on editor that replaces the
James Cook22054252015-03-25 14:04:01 -07006696 * oldText starting at dstart with newText.
James Cook471559f2015-02-27 10:31:20 -08006697 */
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006698 public EditOperation(Editor editor, String oldText, int dstart, String newText,
6699 boolean isComposition) {
James Cook471559f2015-02-27 10:31:20 -08006700 super(editor.mUndoOwner);
James Cookd2026682015-03-03 14:40:14 -08006701 mOldText = oldText;
6702 mNewText = newText;
James Cook471559f2015-02-27 10:31:20 -08006703
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006704 // Determine the type of the edit.
James Cook471559f2015-02-27 10:31:20 -08006705 if (mNewText.length() > 0 && mOldText.length() == 0) {
6706 mType = TYPE_INSERT;
James Cook471559f2015-02-27 10:31:20 -08006707 } else if (mNewText.length() == 0 && mOldText.length() > 0) {
6708 mType = TYPE_DELETE;
James Cook471559f2015-02-27 10:31:20 -08006709 } else {
6710 mType = TYPE_REPLACE;
James Cook471559f2015-02-27 10:31:20 -08006711 }
6712
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006713 mStart = dstart;
James Cook471559f2015-02-27 10:31:20 -08006714 // Store cursor data.
6715 mOldCursorPos = editor.mTextView.getSelectionStart();
James Cookd2026682015-03-03 14:40:14 -08006716 mNewCursorPos = dstart + mNewText.length();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006717 mIsComposition = isComposition;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006718 }
6719
James Cook471559f2015-02-27 10:31:20 -08006720 public EditOperation(Parcel src, ClassLoader loader) {
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006721 super(src, loader);
James Cook471559f2015-02-27 10:31:20 -08006722 mType = src.readInt();
6723 mOldText = src.readString();
James Cook471559f2015-02-27 10:31:20 -08006724 mNewText = src.readString();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006725 mStart = src.readInt();
James Cook471559f2015-02-27 10:31:20 -08006726 mOldCursorPos = src.readInt();
6727 mNewCursorPos = src.readInt();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006728 mFrozen = src.readInt() == 1;
6729 mIsComposition = src.readInt() == 1;
James Cook471559f2015-02-27 10:31:20 -08006730 }
6731
6732 @Override
6733 public void writeToParcel(Parcel dest, int flags) {
6734 dest.writeInt(mType);
6735 dest.writeString(mOldText);
James Cook471559f2015-02-27 10:31:20 -08006736 dest.writeString(mNewText);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006737 dest.writeInt(mStart);
James Cook471559f2015-02-27 10:31:20 -08006738 dest.writeInt(mOldCursorPos);
6739 dest.writeInt(mNewCursorPos);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006740 dest.writeInt(mFrozen ? 1 : 0);
6741 dest.writeInt(mIsComposition ? 1 : 0);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006742 }
6743
James Cook48e0fac2015-02-25 15:44:51 -08006744 private int getNewTextEnd() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006745 return mStart + mNewText.length();
James Cook48e0fac2015-02-25 15:44:51 -08006746 }
6747
6748 private int getOldTextEnd() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006749 return mStart + mOldText.length();
James Cook48e0fac2015-02-25 15:44:51 -08006750 }
6751
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006752 @Override
6753 public void commit() {
6754 }
6755
6756 @Override
6757 public void undo() {
James Cook471559f2015-02-27 10:31:20 -08006758 if (DEBUG_UNDO) Log.d(TAG, "undo");
6759 // Remove the new text and insert the old.
James Cook48e0fac2015-02-25 15:44:51 -08006760 Editor editor = getOwnerData();
6761 Editable text = (Editable) editor.mTextView.getText();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006762 modifyText(text, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006763 }
6764
6765 @Override
6766 public void redo() {
James Cook471559f2015-02-27 10:31:20 -08006767 if (DEBUG_UNDO) Log.d(TAG, "redo");
6768 // Remove the old text and insert the new.
James Cook48e0fac2015-02-25 15:44:51 -08006769 Editor editor = getOwnerData();
6770 Editable text = (Editable) editor.mTextView.getText();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006771 modifyText(text, mStart, getOldTextEnd(), mNewText, mStart, mNewCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006772 }
6773
James Cook471559f2015-02-27 10:31:20 -08006774 /**
6775 * Attempts to merge this existing operation with a new edit.
6776 * @param edit The new edit operation.
6777 * @return If the merge succeeded, returns true. Otherwise returns false and leaves this
6778 * object unchanged.
6779 */
6780 private boolean mergeWith(EditOperation edit) {
James Cook48e0fac2015-02-25 15:44:51 -08006781 if (DEBUG_UNDO) {
6782 Log.d(TAG, "mergeWith old " + this);
6783 Log.d(TAG, "mergeWith new " + edit);
6784 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006785
6786 if (mFrozen) {
6787 return false;
6788 }
6789
James Cook471559f2015-02-27 10:31:20 -08006790 switch (mType) {
6791 case TYPE_INSERT:
6792 return mergeInsertWith(edit);
6793 case TYPE_DELETE:
6794 return mergeDeleteWith(edit);
6795 case TYPE_REPLACE:
6796 return mergeReplaceWith(edit);
6797 default:
6798 return false;
6799 }
6800 }
6801
6802 private boolean mergeInsertWith(EditOperation edit) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006803 if (edit.mType == TYPE_INSERT) {
6804 // Merge insertions that are contiguous even when it's frozen.
6805 if (getNewTextEnd() != edit.mStart) {
6806 return false;
6807 }
6808 mNewText += edit.mNewText;
6809 mNewCursorPos = edit.mNewCursorPos;
6810 mFrozen = edit.mFrozen;
6811 mIsComposition = edit.mIsComposition;
6812 return true;
James Cook471559f2015-02-27 10:31:20 -08006813 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006814 if (mIsComposition && edit.mType == TYPE_REPLACE
6815 && mStart <= edit.mStart && getNewTextEnd() >= edit.getOldTextEnd()) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006816 // Merge insertion with replace as they can be single insertion.
6817 mNewText = mNewText.substring(0, edit.mStart - mStart) + edit.mNewText
6818 + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
6819 mNewCursorPos = edit.mNewCursorPos;
6820 mIsComposition = edit.mIsComposition;
6821 return true;
James Cook471559f2015-02-27 10:31:20 -08006822 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006823 return false;
James Cook471559f2015-02-27 10:31:20 -08006824 }
6825
6826 // TODO: Support forward delete.
6827 private boolean mergeDeleteWith(EditOperation edit) {
James Cook471559f2015-02-27 10:31:20 -08006828 // Only merge continuous deletes.
6829 if (edit.mType != TYPE_DELETE) {
6830 return false;
6831 }
6832 // Only merge deletions that are contiguous.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006833 if (mStart != edit.getOldTextEnd()) {
James Cook471559f2015-02-27 10:31:20 -08006834 return false;
6835 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006836 mStart = edit.mStart;
James Cook471559f2015-02-27 10:31:20 -08006837 mOldText = edit.mOldText + mOldText;
6838 mNewCursorPos = edit.mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006839 mIsComposition = edit.mIsComposition;
James Cook471559f2015-02-27 10:31:20 -08006840 return true;
6841 }
6842
6843 private boolean mergeReplaceWith(EditOperation edit) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006844 if (edit.mType == TYPE_INSERT && getNewTextEnd() == edit.mStart) {
6845 // Merge with adjacent insert.
6846 mNewText += edit.mNewText;
6847 mNewCursorPos = edit.mNewCursorPos;
6848 return true;
6849 }
6850 if (!mIsComposition) {
James Cook471559f2015-02-27 10:31:20 -08006851 return false;
6852 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006853 if (edit.mType == TYPE_DELETE && mStart <= edit.mStart
6854 && getNewTextEnd() >= edit.getOldTextEnd()) {
6855 // Merge with delete as they can be single operation.
6856 mNewText = mNewText.substring(0, edit.mStart - mStart)
6857 + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
6858 if (mNewText.isEmpty()) {
6859 mType = TYPE_DELETE;
6860 }
6861 mNewCursorPos = edit.mNewCursorPos;
6862 mIsComposition = edit.mIsComposition;
6863 return true;
6864 }
6865 if (edit.mType == TYPE_REPLACE && mStart == edit.mStart
6866 && TextUtils.equals(mNewText, edit.mOldText)) {
6867 // Merge with the replace that replaces the same region.
6868 mNewText = edit.mNewText;
6869 mNewCursorPos = edit.mNewCursorPos;
6870 mIsComposition = edit.mIsComposition;
6871 return true;
6872 }
6873 return false;
James Cook471559f2015-02-27 10:31:20 -08006874 }
6875
James Cook48e0fac2015-02-25 15:44:51 -08006876 /**
6877 * Forcibly creates a single merged edit operation by simulating the entire text
6878 * contents being replaced.
6879 */
James Cook22054252015-03-25 14:04:01 -07006880 public void forceMergeWith(EditOperation edit) {
James Cook48e0fac2015-02-25 15:44:51 -08006881 if (DEBUG_UNDO) Log.d(TAG, "forceMerge");
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006882 if (mergeWith(edit)) {
6883 return;
6884 }
James Cookf59152c2015-02-26 18:03:58 -08006885 Editor editor = getOwnerData();
James Cook48e0fac2015-02-25 15:44:51 -08006886
6887 // Copy the text of the current field.
6888 // NOTE: Using StringBuilder instead of SpannableStringBuilder would be somewhat faster,
6889 // but would require two parallel implementations of modifyText() because Editable and
6890 // StringBuilder do not share an interface for replace/delete/insert.
6891 Editable editable = (Editable) editor.mTextView.getText();
6892 Editable originalText = new SpannableStringBuilder(editable.toString());
6893
6894 // Roll back the last operation.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006895 modifyText(originalText, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
James Cook48e0fac2015-02-25 15:44:51 -08006896
6897 // Clone the text again and apply the new operation.
6898 Editable finalText = new SpannableStringBuilder(editable.toString());
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006899 modifyText(finalText, edit.mStart, edit.getOldTextEnd(),
6900 edit.mNewText, edit.mStart, edit.mNewCursorPos);
James Cook48e0fac2015-02-25 15:44:51 -08006901
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006902 // Convert this operation into a replace operation.
James Cook48e0fac2015-02-25 15:44:51 -08006903 mType = TYPE_REPLACE;
6904 mNewText = finalText.toString();
James Cook48e0fac2015-02-25 15:44:51 -08006905 mOldText = originalText.toString();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006906 mStart = 0;
James Cook48e0fac2015-02-25 15:44:51 -08006907 mNewCursorPos = edit.mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006908 mIsComposition = edit.mIsComposition;
James Cook48e0fac2015-02-25 15:44:51 -08006909 // mOldCursorPos is unchanged.
6910 }
6911
6912 private static void modifyText(Editable text, int deleteFrom, int deleteTo,
6913 CharSequence newText, int newTextInsertAt, int newCursorPos) {
James Cook471559f2015-02-27 10:31:20 -08006914 // Apply the edit if it is still valid.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006915 if (isValidRange(text, deleteFrom, deleteTo)
6916 && newTextInsertAt <= text.length() - (deleteTo - deleteFrom)) {
James Cook471559f2015-02-27 10:31:20 -08006917 if (deleteFrom != deleteTo) {
6918 text.delete(deleteFrom, deleteTo);
6919 }
6920 if (newText.length() != 0) {
6921 text.insert(newTextInsertAt, newText);
6922 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006923 }
James Cook900185d2015-03-10 09:48:11 -07006924 // Restore the cursor position. If there wasn't an old cursor (newCursorPos == -1) then
6925 // don't explicitly set it and rely on SpannableStringBuilder to position it.
James Cook471559f2015-02-27 10:31:20 -08006926 // TODO: Select all the text that was undone.
James Cook900185d2015-03-10 09:48:11 -07006927 if (0 <= newCursorPos && newCursorPos <= text.length()) {
James Cook471559f2015-02-27 10:31:20 -08006928 Selection.setSelection(text, newCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006929 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006930 }
6931
James Cook48e0fac2015-02-25 15:44:51 -08006932 private String getTypeString() {
6933 switch (mType) {
6934 case TYPE_INSERT:
6935 return "insert";
6936 case TYPE_DELETE:
6937 return "delete";
6938 case TYPE_REPLACE:
6939 return "replace";
6940 default:
6941 return "";
6942 }
6943 }
6944
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006945 @Override
James Cook471559f2015-02-27 10:31:20 -08006946 public String toString() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006947 return "[mType=" + getTypeString() + ", "
6948 + "mOldText=" + mOldText + ", "
6949 + "mNewText=" + mNewText + ", "
6950 + "mStart=" + mStart + ", "
6951 + "mOldCursorPos=" + mOldCursorPos + ", "
6952 + "mNewCursorPos=" + mNewCursorPos + ", "
6953 + "mFrozen=" + mFrozen + ", "
6954 + "mIsComposition=" + mIsComposition + "]";
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006955 }
6956
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006957 public static final Parcelable.ClassLoaderCreator<EditOperation> CREATOR =
6958 new Parcelable.ClassLoaderCreator<EditOperation>() {
James Cookf59152c2015-02-26 18:03:58 -08006959 @Override
James Cook471559f2015-02-27 10:31:20 -08006960 public EditOperation createFromParcel(Parcel in) {
6961 return new EditOperation(in, null);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006962 }
6963
James Cookf59152c2015-02-26 18:03:58 -08006964 @Override
James Cook471559f2015-02-27 10:31:20 -08006965 public EditOperation createFromParcel(Parcel in, ClassLoader loader) {
6966 return new EditOperation(in, loader);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006967 }
6968
James Cookf59152c2015-02-26 18:03:58 -08006969 @Override
James Cook471559f2015-02-27 10:31:20 -08006970 public EditOperation[] newArray(int size) {
6971 return new EditOperation[size];
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006972 }
6973 };
6974 }
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006975
6976 /**
6977 * A helper for enabling and handling "PROCESS_TEXT" menu actions.
6978 * These allow external applications to plug into currently selected text.
6979 */
6980 static final class ProcessTextIntentActionsHandler {
6981
6982 private final Editor mEditor;
6983 private final TextView mTextView;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006984 private final Context mContext;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006985 private final PackageManager mPackageManager;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006986 private final String mPackageName;
6987 private final SparseArray<Intent> mAccessibilityIntents = new SparseArray<>();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006988 private final SparseArray<AccessibilityNodeInfo.AccessibilityAction> mAccessibilityActions =
6989 new SparseArray<>();
6990 private final List<ResolveInfo> mSupportedActivities = new ArrayList<>();
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006991
6992 private ProcessTextIntentActionsHandler(Editor editor) {
6993 mEditor = Preconditions.checkNotNull(editor);
6994 mTextView = Preconditions.checkNotNull(mEditor.mTextView);
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006995 mContext = Preconditions.checkNotNull(mTextView.getContext());
6996 mPackageManager = Preconditions.checkNotNull(mContext.getPackageManager());
6997 mPackageName = Preconditions.checkNotNull(mContext.getPackageName());
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006998 }
6999
7000 /**
7001 * Adds "PROCESS_TEXT" menu items to the specified menu.
7002 */
7003 public void onInitializeMenu(Menu menu) {
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01007004 loadSupportedActivities();
Abodunrinwa Tokic28be382017-11-07 18:46:50 +00007005 final int size = mSupportedActivities.size();
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +01007006 for (int i = 0; i < size; i++) {
7007 final ResolveInfo resolveInfo = mSupportedActivities.get(i);
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007008 menu.add(Menu.NONE, Menu.NONE,
Abodunrinwa Tokic28be382017-11-07 18:46:50 +00007009 Editor.MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i,
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007010 getLabel(resolveInfo))
7011 .setIntent(createProcessTextIntentForResolveInfo(resolveInfo))
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00007012 .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007013 }
7014 }
7015
7016 /**
7017 * Performs a "PROCESS_TEXT" action if there is one associated with the specified
7018 * menu item.
7019 *
7020 * @return True if the action was performed, false otherwise.
7021 */
7022 public boolean performMenuItemAction(MenuItem item) {
7023 return fireIntent(item.getIntent());
7024 }
7025
7026 /**
7027 * Initializes and caches "PROCESS_TEXT" accessibility actions.
7028 */
7029 public void initializeAccessibilityActions() {
7030 mAccessibilityIntents.clear();
7031 mAccessibilityActions.clear();
7032 int i = 0;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01007033 loadSupportedActivities();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007034 for (ResolveInfo resolveInfo : mSupportedActivities) {
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007035 int actionId = TextView.ACCESSIBILITY_ACTION_PROCESS_TEXT_START_ID + i++;
7036 mAccessibilityActions.put(
7037 actionId,
7038 new AccessibilityNodeInfo.AccessibilityAction(
7039 actionId, getLabel(resolveInfo)));
7040 mAccessibilityIntents.put(
7041 actionId, createProcessTextIntentForResolveInfo(resolveInfo));
7042 }
7043 }
7044
7045 /**
7046 * Adds "PROCESS_TEXT" accessibility actions to the specified accessibility node info.
7047 * NOTE: This needs a prior call to {@link #initializeAccessibilityActions()} to make the
7048 * latest accessibility actions available for this call.
7049 */
7050 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) {
7051 for (int i = 0; i < mAccessibilityActions.size(); i++) {
7052 nodeInfo.addAction(mAccessibilityActions.valueAt(i));
7053 }
7054 }
7055
7056 /**
7057 * Performs a "PROCESS_TEXT" action if there is one associated with the specified
7058 * accessibility action id.
7059 *
7060 * @return True if the action was performed, false otherwise.
7061 */
7062 public boolean performAccessibilityAction(int actionId) {
7063 return fireIntent(mAccessibilityIntents.get(actionId));
7064 }
7065
7066 private boolean fireIntent(Intent intent) {
7067 if (intent != null && Intent.ACTION_PROCESS_TEXT.equals(intent.getAction())) {
Siyamed Sinirce3b05a2017-07-18 18:54:31 -07007068 String selectedText = mTextView.getSelectedText();
7069 selectedText = TextUtils.trimToParcelableSize(selectedText);
7070 intent.putExtra(Intent.EXTRA_PROCESS_TEXT, selectedText);
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08007071 mEditor.mPreserveSelection = true;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007072 mTextView.startActivityForResult(intent, TextView.PROCESS_TEXT_REQUEST_CODE);
7073 return true;
7074 }
7075 return false;
7076 }
7077
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01007078 private void loadSupportedActivities() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007079 mSupportedActivities.clear();
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01007080 if (!mContext.canStartActivityForResult()) {
7081 return;
7082 }
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007083 PackageManager packageManager = mTextView.getContext().getPackageManager();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007084 List<ResolveInfo> unfiltered =
7085 packageManager.queryIntentActivities(createProcessTextIntent(), 0);
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01007086 for (ResolveInfo info : unfiltered) {
7087 if (isSupportedActivity(info)) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007088 mSupportedActivities.add(info);
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01007089 }
7090 }
7091 }
7092
7093 private boolean isSupportedActivity(ResolveInfo info) {
7094 return mPackageName.equals(info.activityInfo.packageName)
7095 || info.activityInfo.exported
7096 && (info.activityInfo.permission == null
7097 || mContext.checkSelfPermission(info.activityInfo.permission)
7098 == PackageManager.PERMISSION_GRANTED);
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007099 }
7100
7101 private Intent createProcessTextIntentForResolveInfo(ResolveInfo info) {
7102 return createProcessTextIntent()
7103 .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !mTextView.isTextEditable())
7104 .setClassName(info.activityInfo.packageName, info.activityInfo.name);
7105 }
7106
7107 private Intent createProcessTextIntent() {
7108 return new Intent()
7109 .setAction(Intent.ACTION_PROCESS_TEXT)
7110 .setType("text/plain");
7111 }
7112
7113 private CharSequence getLabel(ResolveInfo resolveInfo) {
7114 return resolveInfo.loadLabel(mPackageManager);
7115 }
7116 }
Gilles Debunned88876a2012-03-16 17:34:04 -07007117}