blob: 07a721f5a9c9134af9d8bc109cf52efd64bc3e19 [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;
Shu Chen36ae40e2020-01-17 11:46:19 +080024import android.app.AppGlobals;
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;
Artur Satayeved5a6ae2019-12-10 17:47:54 +000028import android.compat.annotation.UnsupportedAppUsage;
Gilles Debunned88876a2012-03-16 17:34:04 -070029import android.content.ClipData;
30import android.content.ClipData.Item;
31import android.content.Context;
32import android.content.Intent;
Raph Levien26d443a2015-03-30 14:18:32 -070033import android.content.UndoManager;
34import android.content.UndoOperation;
35import android.content.UndoOwner;
Gilles Debunned88876a2012-03-16 17:34:04 -070036import android.content.pm.PackageManager;
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +000037import android.content.pm.ResolveInfo;
Gilles Debunned88876a2012-03-16 17:34:04 -070038import android.content.res.TypedArray;
39import android.graphics.Canvas;
40import android.graphics.Color;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +090041import android.graphics.Matrix;
Gilles Debunned88876a2012-03-16 17:34:04 -070042import android.graphics.Paint;
43import android.graphics.Path;
Mihai Popa63ee7f12018-04-05 12:01:53 +010044import android.graphics.Point;
Mihai Popae3017462018-03-07 12:25:21 +000045import android.graphics.PointF;
John Reck32f140aa62018-10-04 15:08:24 -070046import android.graphics.RecordingCanvas;
Gilles Debunned88876a2012-03-16 17:34:04 -070047import android.graphics.Rect;
48import android.graphics.RectF;
John Reck32f140aa62018-10-04 15:08:24 -070049import android.graphics.RenderNode;
Seigo Nonaka3ed1b392016-01-19 13:54:59 +090050import android.graphics.drawable.ColorDrawable;
Gilles Debunned88876a2012-03-16 17:34:04 -070051import android.graphics.drawable.Drawable;
Mihai Popa6315a322018-10-17 17:39:57 +010052import android.os.Build;
Gilles Debunned88876a2012-03-16 17:34:04 -070053import android.os.Bundle;
Yohei Yukawa23cbe852016-05-17 16:42:58 -070054import android.os.LocaleList;
Raph Levien26d443a2015-03-30 14:18:32 -070055import android.os.Parcel;
56import android.os.Parcelable;
James Cookf59152c2015-02-26 18:03:58 -080057import android.os.ParcelableParcel;
Gilles Debunned88876a2012-03-16 17:34:04 -070058import android.os.SystemClock;
59import android.provider.Settings;
60import android.text.DynamicLayout;
61import android.text.Editable;
Raph Levien26d443a2015-03-30 14:18:32 -070062import android.text.InputFilter;
Gilles Debunned88876a2012-03-16 17:34:04 -070063import android.text.InputType;
64import android.text.Layout;
65import android.text.ParcelableSpan;
66import android.text.Selection;
67import android.text.SpanWatcher;
68import android.text.Spannable;
69import android.text.SpannableStringBuilder;
70import android.text.Spanned;
71import android.text.StaticLayout;
72import android.text.TextUtils;
Gilles Debunned88876a2012-03-16 17:34:04 -070073import android.text.method.KeyListener;
74import android.text.method.MetaKeyKeyListener;
75import android.text.method.MovementMethod;
Gilles Debunned88876a2012-03-16 17:34:04 -070076import android.text.method.WordIterator;
77import android.text.style.EasyEditSpan;
78import android.text.style.SuggestionRangeSpan;
79import android.text.style.SuggestionSpan;
80import android.text.style.TextAppearanceSpan;
81import android.text.style.URLSpan;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +090082import android.util.ArraySet;
Gilles Debunned88876a2012-03-16 17:34:04 -070083import android.util.DisplayMetrics;
84import android.util.Log;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -070085import android.util.SparseArray;
Shu Chend931a472020-02-14 14:25:14 +080086import android.util.TypedValue;
Gilles Debunned88876a2012-03-16 17:34:04 -070087import android.view.ActionMode;
88import android.view.ActionMode.Callback;
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +090089import android.view.ContextMenu;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +090090import android.view.ContextThemeWrapper;
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -070091import android.view.DragAndDropPermissions;
Gilles Debunned88876a2012-03-16 17:34:04 -070092import android.view.DragEvent;
93import android.view.Gravity;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -070094import android.view.HapticFeedbackConstants;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -080095import android.view.InputDevice;
Gilles Debunned88876a2012-03-16 17:34:04 -070096import android.view.LayoutInflater;
97import android.view.Menu;
98import android.view.MenuItem;
99import android.view.MotionEvent;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +0900100import android.view.SubMenu;
Gilles Debunned88876a2012-03-16 17:34:04 -0700101import android.view.View;
Gilles Debunned88876a2012-03-16 17:34:04 -0700102import android.view.View.DragShadowBuilder;
103import android.view.View.OnClickListener;
Adam Powell057a5852012-05-11 10:28:38 -0700104import android.view.ViewConfiguration;
105import android.view.ViewGroup;
Gilles Debunned88876a2012-03-16 17:34:04 -0700106import android.view.ViewGroup.LayoutParams;
Mihai Popaddf9fe02018-09-28 13:54:19 +0100107import android.view.ViewParent;
Gilles Debunned88876a2012-03-16 17:34:04 -0700108import android.view.ViewTreeObserver;
109import android.view.WindowManager;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700110import android.view.accessibility.AccessibilityNodeInfo;
Mihai Popa38722382018-03-07 19:56:21 +0000111import android.view.animation.LinearInterpolator;
Gilles Debunned88876a2012-03-16 17:34:04 -0700112import android.view.inputmethod.CorrectionInfo;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900113import android.view.inputmethod.CursorAnchorInfo;
Gilles Debunned88876a2012-03-16 17:34:04 -0700114import android.view.inputmethod.EditorInfo;
115import android.view.inputmethod.ExtractedText;
116import android.view.inputmethod.ExtractedTextRequest;
117import android.view.inputmethod.InputConnection;
118import android.view.inputmethod.InputMethodManager;
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100119import android.view.textclassifier.TextClassification;
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000120import android.view.textclassifier.TextClassificationManager;
Gilles Debunned88876a2012-03-16 17:34:04 -0700121import android.widget.AdapterView.OnItemClickListener;
122import android.widget.TextView.Drawables;
123import android.widget.TextView.OnEditorActionListener;
124
Seigo Nonakaa60160b2015-08-19 12:38:35 -0700125import com.android.internal.annotations.VisibleForTesting;
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +0000126import com.android.internal.logging.MetricsLogger;
127import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
Raph Levien26d443a2015-03-30 14:18:32 -0700128import com.android.internal.util.ArrayUtils;
129import com.android.internal.util.GrowingArrayUtils;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700130import com.android.internal.util.Preconditions;
Abodunrinwa Toki29cb7682018-04-11 21:24:20 +0100131import com.android.internal.view.FloatingActionMode;
Raph Levien26d443a2015-03-30 14:18:32 -0700132import com.android.internal.widget.EditableInputConnection;
133
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +0900134import java.lang.annotation.Retention;
135import java.lang.annotation.RetentionPolicy;
Andrei Stingaceanu2aaeefe2015-10-20 19:11:23 +0100136import java.text.BreakIterator;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +0100137import java.util.ArrayList;
Andrei Stingaceanu2aaeefe2015-10-20 19:11:23 +0100138import java.util.Arrays;
Adam Powell86241212019-06-10 08:38:49 -0700139import java.util.Collections;
Andrei Stingaceanu2aaeefe2015-10-20 19:11:23 +0100140import java.util.Comparator;
141import java.util.HashMap;
142import java.util.List;
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +0100143import java.util.Map;
Daulet Zhanguzincb0d19b2019-12-18 15:08:09 +0000144import java.util.Objects;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700145
Gilles Debunned88876a2012-03-16 17:34:04 -0700146/**
147 * Helper class used by TextView to handle editable text views.
148 *
149 * @hide
150 */
151public class Editor {
Adam Powell057a5852012-05-11 10:28:38 -0700152 private static final String TAG = "Editor";
James Cookf59152c2015-02-26 18:03:58 -0800153 private static final boolean DEBUG_UNDO = false;
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800154
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800155 // Specifies whether to use the magnifier when pressing the insertion or selection handles.
Andrei Stingaceanu060b3d72017-10-04 11:27:08 +0100156 private static final boolean FLAG_USE_MAGNIFIER = true;
Adam Powell057a5852012-05-11 10:28:38 -0700157
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -0800158 private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
159 private static final int RECENT_CUT_COPY_DURATION_MS = 15 * 1000; // 15 seconds in millis
160
Gilles Debunned88876a2012-03-16 17:34:04 -0700161 static final int BLINK = 500;
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700162 private static final int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
Mady Mellorcc65c372015-06-17 09:25:19 -0700163 private static final float LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS = 0.5f;
Mady Mellore264ac32015-06-22 16:46:29 -0700164 private static final int UNSET_X_VALUE = -1;
Mady Mellora6a0f782015-07-10 16:43:32 -0700165 private static final int UNSET_LINE = -1;
James Cookf59152c2015-02-26 18:03:58 -0800166 // Tag used when the Editor maintains its own separate UndoManager.
167 private static final String UNDO_OWNER_TAG = "Editor";
Gilles Debunned88876a2012-03-16 17:34:04 -0700168
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900169 // Ordering constants used to place the Action Mode or context menu items in their menu.
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +0100170 private static final int MENU_ITEM_ORDER_ASSIST = 0;
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +0000171 private static final int MENU_ITEM_ORDER_UNDO = 2;
172 private static final int MENU_ITEM_ORDER_REDO = 3;
Abodunrinwa Toki5fedfb82017-02-06 19:34:00 +0000173 private static final int MENU_ITEM_ORDER_CUT = 4;
174 private static final int MENU_ITEM_ORDER_COPY = 5;
175 private static final int MENU_ITEM_ORDER_PASTE = 6;
176 private static final int MENU_ITEM_ORDER_SHARE = 7;
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +0100177 private static final int MENU_ITEM_ORDER_SELECT_ALL = 8;
178 private static final int MENU_ITEM_ORDER_REPLACE = 9;
179 private static final int MENU_ITEM_ORDER_AUTOFILL = 10;
180 private static final int MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT = 11;
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +0100181 private static final int MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START = 50;
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +0100182 private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 100;
Clara Bayarri3b69fd82015-06-03 21:52:02 +0100183
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100184 @IntDef({MagnifierHandleTrigger.SELECTION_START,
185 MagnifierHandleTrigger.SELECTION_END,
186 MagnifierHandleTrigger.INSERTION})
187 @Retention(RetentionPolicy.SOURCE)
188 private @interface MagnifierHandleTrigger {
189 int INSERTION = 0;
190 int SELECTION_START = 1;
191 int SELECTION_END = 2;
192 }
193
Richard Ledley26b87222017-11-30 10:54:08 +0000194 @IntDef({TextActionMode.SELECTION, TextActionMode.INSERTION, TextActionMode.TEXT_LINK})
195 @interface TextActionMode {
196 int SELECTION = 0;
197 int INSERTION = 1;
198 int TEXT_LINK = 2;
199 }
200
James Cookf59152c2015-02-26 18:03:58 -0800201 // Each Editor manages its own undo stack.
202 private final UndoManager mUndoManager = new UndoManager();
203 private UndoOwner mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
James Cook48e0fac2015-02-25 15:44:51 -0800204 final UndoInputFilter mUndoInputFilter = new UndoInputFilter(this);
James Cookf1dad1e2015-02-27 11:00:01 -0800205 boolean mAllowUndo = true;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700206
Abodunrinwa Toki54486c12017-04-19 21:02:36 +0100207 private final MetricsLogger mMetricsLogger = new MetricsLogger();
208
Gilles Debunned88876a2012-03-16 17:34:04 -0700209 // Cursor Controllers.
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800210 InsertionPointCursorController mInsertionPointCursorController;
Gilles Debunned88876a2012-03-16 17:34:04 -0700211 SelectionModifierCursorController mSelectionModifierCursorController;
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100212 // Action mode used when text is selected or when actions on an insertion cursor are triggered.
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800213 private ActionMode mTextActionMode;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100214 @UnsupportedAppUsage
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900215 private boolean mInsertionControllerEnabled;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100216 @UnsupportedAppUsage
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900217 private boolean mSelectionControllerEnabled;
Gilles Debunned88876a2012-03-16 17:34:04 -0700218
Yohei Yukawac9cd9db2017-06-19 18:27:34 -0700219 private final boolean mHapticTextHandleEnabled;
220
Shu Chen847583c2020-01-22 09:18:40 +0800221 @Nullable
222 private MagnifierMotionAnimator mMagnifierAnimator;
223
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000224 private final Runnable mUpdateMagnifierRunnable = new Runnable() {
225 @Override
226 public void run() {
Mihai Popa38722382018-03-07 19:56:21 +0000227 mMagnifierAnimator.update();
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000228 }
229 };
230 // Update the magnifier contents whenever anything in the view hierarchy is updated.
231 // Note: this only captures UI thread-visible changes, so it's a known issue that an animating
232 // VectorDrawable or Ripple animation will not trigger capture, since they're owned by
233 // RenderThread.
234 private final ViewTreeObserver.OnDrawListener mMagnifierOnDrawListener =
235 new ViewTreeObserver.OnDrawListener() {
236 @Override
237 public void onDraw() {
Mihai Popa38722382018-03-07 19:56:21 +0000238 if (mMagnifierAnimator != null) {
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000239 // Posting the method will ensure that updating the magnifier contents will
240 // happen right after the rendering of the current frame.
241 mTextView.post(mUpdateMagnifierRunnable);
242 }
243 }
244 };
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100245
Gilles Debunned88876a2012-03-16 17:34:04 -0700246 // Used to highlight a word when it is corrected by the IME
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900247 private CorrectionHighlighter mCorrectionHighlighter;
Gilles Debunned88876a2012-03-16 17:34:04 -0700248
249 InputContentType mInputContentType;
250 InputMethodState mInputMethodState;
251
Chris Craik956f3402015-04-27 16:41:00 -0700252 private static class TextRenderNode {
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +0900253 // Render node has 3 recording states:
254 // 1. Recorded operations are valid.
255 // #needsRecord() returns false, but needsToBeShifted is false.
256 // 2. Recorded operations are not valid, but just the position needed to be updated.
257 // #needsRecord() returns false, but needsToBeShifted is true.
258 // 3. Recorded operations are not valid. Need to record operations. #needsRecord() returns
259 // true.
Chris Craik956f3402015-04-27 16:41:00 -0700260 RenderNode renderNode;
John Reck7558aa72014-03-05 14:59:59 -0800261 boolean isDirty;
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +0900262 // Becomes true when recorded operations can be reused, but the position has to be updated.
263 boolean needsToBeShifted;
Chris Craik956f3402015-04-27 16:41:00 -0700264 public TextRenderNode(String name) {
Chris Craik956f3402015-04-27 16:41:00 -0700265 renderNode = RenderNode.create(name, null);
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +0900266 isDirty = true;
267 needsToBeShifted = true;
John Reck7558aa72014-03-05 14:59:59 -0800268 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700269 boolean needsRecord() {
John Reckc7ddcf32018-10-25 13:56:17 -0700270 return isDirty || !renderNode.hasDisplayList();
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700271 }
John Reck7558aa72014-03-05 14:59:59 -0800272 }
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900273 private TextRenderNode[] mTextRenderNodes;
Gilles Debunned88876a2012-03-16 17:34:04 -0700274
275 boolean mFrozenWithFocus;
276 boolean mSelectionMoved;
277 boolean mTouchFocusSelected;
278
279 KeyListener mKeyListener;
280 int mInputType = EditorInfo.TYPE_NULL;
281
282 boolean mDiscardNextActionUp;
283 boolean mIgnoreActionUpEvent;
284
Louis Pullen-Freilich1c400a32019-02-05 14:35:20 +0000285 /**
286 * To set a custom cursor, you should use {@link TextView#setTextCursorDrawable(Drawable)}
287 * or {@link TextView#setTextCursorDrawable(int)}.
288 */
289 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
Mihai Popaa4e39c42018-02-20 15:31:11 +0000290 private long mShowCursor;
291 private boolean mRenderCursorRegardlessTiming;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900292 private Blink mBlink;
Gilles Debunned88876a2012-03-16 17:34:04 -0700293
294 boolean mCursorVisible = true;
295 boolean mSelectAllOnFocus;
296 boolean mTextIsSelectable;
297
298 CharSequence mError;
299 boolean mErrorWasChanged;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900300 private ErrorPopup mErrorPopup;
Fabrice Di Meglio1957d282012-10-25 17:42:39 -0700301
Gilles Debunned88876a2012-03-16 17:34:04 -0700302 /**
303 * This flag is set if the TextView tries to display an error before it
304 * is attached to the window (so its position is still unknown).
305 * It causes the error to be shown later, when onAttachedToWindow()
306 * is called.
307 */
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900308 private boolean mShowErrorAfterAttach;
Gilles Debunned88876a2012-03-16 17:34:04 -0700309
310 boolean mInBatchEditControllers;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100311 @UnsupportedAppUsage
Gilles Debunne3473b2b2012-04-20 16:21:10 -0700312 boolean mShowSoftInputOnFocus = true;
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -0800313 private boolean mPreserveSelection;
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +0900314 private boolean mRestartActionModeOnNextRefresh;
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000315 private boolean mRequestingLinkActionMode;
Gilles Debunned88876a2012-03-16 17:34:04 -0700316
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800317 private SelectionActionModeHelper mSelectionActionModeHelper;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +0000318
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900319 boolean mIsBeingLongClicked;
sallyyuencc02ea32020-02-10 10:45:48 -0800320 boolean mIsBeingLongClickedByAccessibility;
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900321
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900322 private SuggestionsPopupWindow mSuggestionsPopupWindow;
Gilles Debunned88876a2012-03-16 17:34:04 -0700323 SuggestionRangeSpan mSuggestionRangeSpan;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900324 private Runnable mShowSuggestionRunnable;
Gilles Debunned88876a2012-03-16 17:34:04 -0700325
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -0700326 Drawable mDrawableForCursor = null;
Gilles Debunned88876a2012-03-16 17:34:04 -0700327
Mihai Popa6315a322018-10-17 17:39:57 +0100328 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
329 Drawable mSelectHandleLeft;
330 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
331 Drawable mSelectHandleRight;
332 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
333 Drawable mSelectHandleCenter;
Gilles Debunned88876a2012-03-16 17:34:04 -0700334
335 // Global listener that detects changes in the global position of the TextView
336 private PositionListener mPositionListener;
337
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900338 private float mContextMenuAnchorX, mContextMenuAnchorY;
Gilles Debunned88876a2012-03-16 17:34:04 -0700339 Callback mCustomSelectionActionModeCallback;
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100340 Callback mCustomInsertionActionModeCallback;
Gilles Debunned88876a2012-03-16 17:34:04 -0700341
342 // Set when this TextView gained focus with some text selected. Will start selection mode.
Mathew Inwood978c6e22018-08-21 15:58:55 +0100343 @UnsupportedAppUsage
Gilles Debunned88876a2012-03-16 17:34:04 -0700344 boolean mCreatedWithASelection;
345
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900346 // The button state as of the last time #onTouchEvent is called.
347 private int mLastButtonState;
348
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -0800349 private final EditorTouchState mTouchState = new EditorTouchState();
350
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100351 private Runnable mInsertionActionModeRunnable;
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100352
Jean Chalardbaf30942013-02-28 16:01:51 -0800353 // The span controller helps monitoring the changes to which the Editor needs to react:
354 // - EasyEditSpans, for which we have some UI to display on attach and on hide
355 // - SelectionSpans, for which we need to call updateSelection if an IME is attached
356 private SpanController mSpanController;
Gilles Debunned88876a2012-03-16 17:34:04 -0700357
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900358 private WordIterator mWordIterator;
Gilles Debunned88876a2012-03-16 17:34:04 -0700359 SpellChecker mSpellChecker;
360
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800361 // This word iterator is set with text and used to determine word boundaries
362 // when a user is selecting text.
363 private WordIterator mWordIteratorWithText;
364 // Indicate that the text in the word iterator needs to be updated.
365 private boolean mUpdateWordIteratorText;
366
Gilles Debunned88876a2012-03-16 17:34:04 -0700367 private Rect mTempRect;
368
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800369 private final TextView mTextView;
Gilles Debunned88876a2012-03-16 17:34:04 -0700370
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700371 final ProcessTextIntentActionsHandler mProcessTextIntentActionsHandler;
372
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700373 private final CursorAnchorInfoNotifier mCursorAnchorInfoNotifier =
374 new CursorAnchorInfoNotifier();
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900375
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100376 private final Runnable mShowFloatingToolbar = new Runnable() {
377 @Override
378 public void run() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100379 if (mTextActionMode != null) {
Abodunrinwa Toki9e211282015-06-05 02:46:57 +0100380 mTextActionMode.hide(0); // hide off.
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100381 }
382 }
383 };
384
Clara Bayarrib71dddd2015-06-04 23:17:30 +0100385 boolean mIsInsertionActionModeStartPending = false;
386
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +0900387 private final SuggestionHelper mSuggestionHelper = new SuggestionHelper();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +0900388
Nikita Dubrovskyac919b02020-02-18 09:39:20 -0800389 private boolean mFlagCursorDragFromAnywhereEnabled;
390 private boolean mFlagInsertionHandleGesturesEnabled;
Shu Chen36ae40e2020-01-17 11:46:19 +0800391
Shu Chen9d744e52020-01-22 14:28:08 +0800392 // Specifies whether the new magnifier (with fish-eye effect) is enabled.
393 private final boolean mNewMagnifierEnabled;
394
Shu Chend931a472020-02-14 14:25:14 +0800395 // Line height range in DP for the new magnifier.
396 static private final int MIN_LINE_HEIGHT_FOR_MAGNIFIER = 20;
397 static private final int MAX_LINE_HEIGHT_FOR_MAGNIFIER = 32;
398 // Line height range in pixels for the new magnifier.
399 // - If the line height is bigger than the max, magnifier should be dismissed.
400 // - If the line height is smaller than the min, magnifier should apply a bigger zoom factor
401 // to make sure the text can be seen clearly.
402 private int mMinLineHeightForMagnifier;
403 private int mMaxLineHeightForMagnifier;
404 // The zoom factor initially configured.
405 // The actual zoom value may changes based on this initial zoom value.
406 private float mInitialZoom = 1f;
407
Shu Chenafbcf852020-03-10 08:19:07 +0800408 // For calculating the line change slops while moving cursor/selection.
409 // The slop max/min value include line height and the slop on the upper/lower line.
410 private static final int LINE_CHANGE_SLOP_MAX_DP = 45;
411 private static final int LINE_CHANGE_SLOP_MIN_DP = 12;
412 private int mLineChangeSlopMax;
413 private int mLineChangeSlopMin;
414
Gilles Debunned88876a2012-03-16 17:34:04 -0700415 Editor(TextView textView) {
416 mTextView = textView;
James Cookf59152c2015-02-26 18:03:58 -0800417 // Synchronize the filter list, which places the undo input filter at the end.
418 mTextView.setFilters(mTextView.getFilters());
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700419 mProcessTextIntentActionsHandler = new ProcessTextIntentActionsHandler(this);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -0700420 mHapticTextHandleEnabled = mTextView.getContext().getResources().getBoolean(
421 com.android.internal.R.bool.config_enableHapticTextHandle);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100422
Nikita Dubrovskyac919b02020-02-18 09:39:20 -0800423 mFlagCursorDragFromAnywhereEnabled = AppGlobals.getIntCoreSetting(
Nikita Dubrovsky9e139172020-02-18 16:23:17 -0800424 WidgetFlags.KEY_ENABLE_CURSOR_DRAG_FROM_ANYWHERE,
425 WidgetFlags.ENABLE_CURSOR_DRAG_FROM_ANYWHERE_DEFAULT ? 1 : 0) != 0;
Nikita Dubrovskyac919b02020-02-18 09:39:20 -0800426 mFlagInsertionHandleGesturesEnabled = AppGlobals.getIntCoreSetting(
Nikita Dubrovsky9e139172020-02-18 16:23:17 -0800427 WidgetFlags.KEY_ENABLE_INSERTION_HANDLE_GESTURES,
428 WidgetFlags.ENABLE_INSERTION_HANDLE_GESTURES_DEFAULT ? 1 : 0) != 0;
Shu Chen9d744e52020-01-22 14:28:08 +0800429 mNewMagnifierEnabled = AppGlobals.getIntCoreSetting(
Nikita Dubrovsky9e139172020-02-18 16:23:17 -0800430 WidgetFlags.KEY_ENABLE_NEW_MAGNIFIER,
431 WidgetFlags.ENABLE_NEW_MAGNIFIER_DEFAULT ? 1 : 0) != 0;
Shu Chen36ae40e2020-01-17 11:46:19 +0800432 if (TextView.DEBUG_CURSOR) {
Nikita Dubrovskyac919b02020-02-18 09:39:20 -0800433 logCursor("Editor", "Cursor drag from anywhere is %s.",
434 mFlagCursorDragFromAnywhereEnabled ? "enabled" : "disabled");
435 logCursor("Editor", "Insertion handle gestures is %s.",
436 mFlagInsertionHandleGesturesEnabled ? "enabled" : "disabled");
Shu Chen9d744e52020-01-22 14:28:08 +0800437 logCursor("Editor", "New magnifier is %s.",
438 mNewMagnifierEnabled ? "enabled" : "disabled");
Shu Chen36ae40e2020-01-17 11:46:19 +0800439 }
Shu Chenafbcf852020-03-10 08:19:07 +0800440
441 mLineChangeSlopMax = (int) TypedValue.applyDimension(
442 TypedValue.COMPLEX_UNIT_DIP, LINE_CHANGE_SLOP_MAX_DP,
443 mTextView.getContext().getResources().getDisplayMetrics());
444 mLineChangeSlopMin = (int) TypedValue.applyDimension(
445 TypedValue.COMPLEX_UNIT_DIP, LINE_CHANGE_SLOP_MIN_DP,
446 mTextView.getContext().getResources().getDisplayMetrics());
447
James Cookf59152c2015-02-26 18:03:58 -0800448 }
449
Shu Chen38451192020-01-21 14:32:32 +0800450 @VisibleForTesting
Nikita Dubrovskyac919b02020-02-18 09:39:20 -0800451 public boolean getFlagCursorDragFromAnywhereEnabled() {
452 return mFlagCursorDragFromAnywhereEnabled;
Shu Chen38451192020-01-21 14:32:32 +0800453 }
454
455 @VisibleForTesting
Nikita Dubrovskyac919b02020-02-18 09:39:20 -0800456 public void setFlagCursorDragFromAnywhereEnabled(boolean enabled) {
457 mFlagCursorDragFromAnywhereEnabled = enabled;
458 }
459
460 @VisibleForTesting
461 public boolean getFlagInsertionHandleGesturesEnabled() {
462 return mFlagInsertionHandleGesturesEnabled;
463 }
464
465 @VisibleForTesting
466 public void setFlagInsertionHandleGesturesEnabled(boolean enabled) {
467 mFlagInsertionHandleGesturesEnabled = enabled;
Shu Chen38451192020-01-21 14:32:32 +0800468 }
469
Shu Chen847583c2020-01-22 09:18:40 +0800470 // Lazy creates the magnifier animator.
471 private MagnifierMotionAnimator getMagnifierAnimator() {
472 if (FLAG_USE_MAGNIFIER && mMagnifierAnimator == null) {
473 // Lazy creates the magnifier instance because it requires the text height which cannot
474 // be measured at the time of Editor instance being created.
Shu Chen9d744e52020-01-22 14:28:08 +0800475 final Magnifier.Builder builder = mNewMagnifierEnabled
Shu Chen847583c2020-01-22 09:18:40 +0800476 ? createBuilderWithInlineMagnifierDefaults()
477 : Magnifier.createBuilderWithOldMagnifierDefaults(mTextView);
478 mMagnifierAnimator = new MagnifierMotionAnimator(builder.build());
479 }
480 return mMagnifierAnimator;
481 }
482
Shu Chen847583c2020-01-22 09:18:40 +0800483 private Magnifier.Builder createBuilderWithInlineMagnifierDefaults() {
484 final Magnifier.Builder params = new Magnifier.Builder(mTextView);
485
Shu Chen9d744e52020-01-22 14:28:08 +0800486 float zoom = AppGlobals.getFloatCoreSetting(
Nikita Dubrovsky9e139172020-02-18 16:23:17 -0800487 WidgetFlags.KEY_MAGNIFIER_ZOOM_FACTOR,
488 WidgetFlags.MAGNIFIER_ZOOM_FACTOR_DEFAULT);
Shu Chen9d744e52020-01-22 14:28:08 +0800489 float aspectRatio = AppGlobals.getFloatCoreSetting(
Nikita Dubrovsky9e139172020-02-18 16:23:17 -0800490 WidgetFlags.KEY_MAGNIFIER_ASPECT_RATIO,
491 WidgetFlags.MAGNIFIER_ASPECT_RATIO_DEFAULT);
Shu Chen9d744e52020-01-22 14:28:08 +0800492 // Avoid invalid/unsupported values.
493 if (zoom < 1.2f || zoom > 1.8f) {
494 zoom = 1.5f;
495 }
496 if (aspectRatio < 3 || aspectRatio > 8) {
497 aspectRatio = 5.5f;
498 }
499
Shu Chend931a472020-02-14 14:25:14 +0800500 mInitialZoom = zoom;
501 mMinLineHeightForMagnifier = (int) TypedValue.applyDimension(
502 TypedValue.COMPLEX_UNIT_DIP, MIN_LINE_HEIGHT_FOR_MAGNIFIER,
503 mTextView.getContext().getResources().getDisplayMetrics());
504 mMaxLineHeightForMagnifier = (int) TypedValue.applyDimension(
505 TypedValue.COMPLEX_UNIT_DIP, MAX_LINE_HEIGHT_FOR_MAGNIFIER,
506 mTextView.getContext().getResources().getDisplayMetrics());
507
Shu Chen09ce0f12020-02-04 15:27:47 +0800508 final Layout layout = mTextView.getLayout();
509 final int line = layout.getLineForOffset(mTextView.getSelectionStart());
510 final int sourceHeight =
511 layout.getLineBottomWithoutSpacing(line) - layout.getLineTop(line);
Shu Chend931a472020-02-14 14:25:14 +0800512 final int height = (int)(sourceHeight * zoom);
513 final int width = (int)(aspectRatio * Math.max(sourceHeight, mMinLineHeightForMagnifier));
Shu Chen847583c2020-01-22 09:18:40 +0800514
515 params.setFishEyeStyle()
516 .setSize(width, height)
Shu Chen09ce0f12020-02-04 15:27:47 +0800517 .setSourceSize(width, sourceHeight)
Shu Chen847583c2020-01-22 09:18:40 +0800518 .setElevation(0)
519 .setInitialZoom(zoom)
520 .setClippingEnabled(false);
521
522 final Context context = mTextView.getContext();
523 final TypedArray a = context.obtainStyledAttributes(
524 null, com.android.internal.R.styleable.Magnifier,
525 com.android.internal.R.attr.magnifierStyle, 0);
526 params.setDefaultSourceToMagnifierOffset(
527 a.getDimensionPixelSize(
528 com.android.internal.R.styleable.Magnifier_magnifierHorizontalOffset, 0),
529 a.getDimensionPixelSize(
530 com.android.internal.R.styleable.Magnifier_magnifierVerticalOffset, 0));
531 a.recycle();
532
533 return params.setSourceBounds(
534 Magnifier.SOURCE_BOUND_MAX_VISIBLE,
535 Magnifier.SOURCE_BOUND_MAX_IN_SURFACE,
536 Magnifier.SOURCE_BOUND_MAX_VISIBLE,
537 Magnifier.SOURCE_BOUND_MAX_IN_SURFACE);
538 }
539
James Cookf59152c2015-02-26 18:03:58 -0800540 ParcelableParcel saveInstanceState() {
James Cookd2026682015-03-03 14:40:14 -0800541 ParcelableParcel state = new ParcelableParcel(getClass().getClassLoader());
542 Parcel parcel = state.getParcel();
543 mUndoManager.saveInstanceState(parcel);
544 mUndoInputFilter.saveInstanceState(parcel);
545 return state;
James Cookf59152c2015-02-26 18:03:58 -0800546 }
547
548 void restoreInstanceState(ParcelableParcel state) {
James Cookd2026682015-03-03 14:40:14 -0800549 Parcel parcel = state.getParcel();
550 mUndoManager.restoreInstanceState(parcel, state.getClassLoader());
551 mUndoInputFilter.restoreInstanceState(parcel);
James Cookf59152c2015-02-26 18:03:58 -0800552 // Re-associate this object as the owner of undo state.
553 mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
554 }
555
James Cook48e0fac2015-02-25 15:44:51 -0800556 /**
557 * Forgets all undo and redo operations for this Editor.
558 */
559 void forgetUndoRedo() {
560 UndoOwner[] owners = { mUndoOwner };
561 mUndoManager.forgetUndos(owners, -1 /* all */);
562 mUndoManager.forgetRedos(owners, -1 /* all */);
563 }
564
James Cookf59152c2015-02-26 18:03:58 -0800565 boolean canUndo() {
566 UndoOwner[] owners = { mUndoOwner };
James Cookf1dad1e2015-02-27 11:00:01 -0800567 return mAllowUndo && mUndoManager.countUndos(owners) > 0;
James Cookf59152c2015-02-26 18:03:58 -0800568 }
569
570 boolean canRedo() {
571 UndoOwner[] owners = { mUndoOwner };
James Cookf1dad1e2015-02-27 11:00:01 -0800572 return mAllowUndo && mUndoManager.countRedos(owners) > 0;
James Cookf59152c2015-02-26 18:03:58 -0800573 }
574
575 void undo() {
James Cookf1dad1e2015-02-27 11:00:01 -0800576 if (!mAllowUndo) {
577 return;
578 }
James Cookf59152c2015-02-26 18:03:58 -0800579 UndoOwner[] owners = { mUndoOwner };
580 mUndoManager.undo(owners, 1); // Undo 1 action.
581 }
582
583 void redo() {
James Cookf1dad1e2015-02-27 11:00:01 -0800584 if (!mAllowUndo) {
585 return;
586 }
James Cookf59152c2015-02-26 18:03:58 -0800587 UndoOwner[] owners = { mUndoOwner };
588 mUndoManager.redo(owners, 1); // Redo 1 action.
Gilles Debunned88876a2012-03-16 17:34:04 -0700589 }
590
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100591 void replace() {
Keisuke Kuroyanagi713be062016-02-29 16:07:54 -0800592 if (mSuggestionsPopupWindow == null) {
593 mSuggestionsPopupWindow = new SuggestionsPopupWindow();
594 }
595 hideCursorAndSpanControllers();
596 mSuggestionsPopupWindow.show();
597
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100598 int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100599 Selection.setSelection((Spannable) mTextView.getText(), middle);
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100600 }
601
Gilles Debunned88876a2012-03-16 17:34:04 -0700602 void onAttachedToWindow() {
603 if (mShowErrorAfterAttach) {
604 showError();
605 mShowErrorAfterAttach = false;
606 }
607
608 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000609 if (observer.isAlive()) {
610 // No need to create the controller.
611 // The get method will add the listener on controller creation.
612 if (mInsertionPointCursorController != null) {
613 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
614 }
615 if (mSelectionModifierCursorController != null) {
616 mSelectionModifierCursorController.resetTouchOffsets();
617 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
618 }
619 if (FLAG_USE_MAGNIFIER) {
620 observer.addOnDrawListener(mMagnifierOnDrawListener);
621 }
Gilles Debunned88876a2012-03-16 17:34:04 -0700622 }
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000623
Gilles Debunned88876a2012-03-16 17:34:04 -0700624 updateSpellCheckSpans(0, mTextView.getText().length(),
625 true /* create the spell checker if needed */);
Adam Powell057a5852012-05-11 10:28:38 -0700626
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +0900627 if (mTextView.hasSelection()) {
628 refreshTextActionMode();
Adam Powell057a5852012-05-11 10:28:38 -0700629 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900630
631 getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true);
Mikael Gullstrand5b734f22013-07-09 14:41:28 +0200632 resumeBlink();
Gilles Debunned88876a2012-03-16 17:34:04 -0700633 }
634
635 void onDetachedFromWindow() {
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900636 getPositionListener().removeSubscriber(mCursorAnchorInfoNotifier);
637
Gilles Debunned88876a2012-03-16 17:34:04 -0700638 if (mError != null) {
639 hideError();
640 }
641
Mikael Gullstrand5b734f22013-07-09 14:41:28 +0200642 suspendBlink();
Gilles Debunned88876a2012-03-16 17:34:04 -0700643
644 if (mInsertionPointCursorController != null) {
645 mInsertionPointCursorController.onDetached();
646 }
647
648 if (mSelectionModifierCursorController != null) {
649 mSelectionModifierCursorController.onDetached();
650 }
651
652 if (mShowSuggestionRunnable != null) {
653 mTextView.removeCallbacks(mShowSuggestionRunnable);
654 }
655
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100656 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100657 if (mInsertionActionModeRunnable != null) {
658 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100659 }
660
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100661 mTextView.removeCallbacks(mShowFloatingToolbar);
662
Chris Craik003cc3d2015-10-16 10:24:55 -0700663 discardTextDisplayLists();
Gilles Debunned88876a2012-03-16 17:34:04 -0700664
665 if (mSpellChecker != null) {
666 mSpellChecker.closeSession();
667 // Forces the creation of a new SpellChecker next time this window is created.
668 // Will handle the cases where the settings has been changed in the meantime.
669 mSpellChecker = null;
670 }
671
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000672 if (FLAG_USE_MAGNIFIER) {
673 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
674 if (observer.isAlive()) {
675 observer.removeOnDrawListener(mMagnifierOnDrawListener);
676 }
677 }
678
Mady Mellora2861452015-06-25 08:40:27 -0700679 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -0800680 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -0700681 }
682
Chris Craik003cc3d2015-10-16 10:24:55 -0700683 private void discardTextDisplayLists() {
Chris Craik956f3402015-04-27 16:41:00 -0700684 if (mTextRenderNodes != null) {
685 for (int i = 0; i < mTextRenderNodes.length; i++) {
686 RenderNode displayList = mTextRenderNodes[i] != null
687 ? mTextRenderNodes[i].renderNode : null;
John Reckc7ddcf32018-10-25 13:56:17 -0700688 if (displayList != null && displayList.hasDisplayList()) {
Chris Craik003cc3d2015-10-16 10:24:55 -0700689 displayList.discardDisplayList();
John Reck7558aa72014-03-05 14:59:59 -0800690 }
691 }
692 }
693 }
694
Gilles Debunned88876a2012-03-16 17:34:04 -0700695 private void showError() {
696 if (mTextView.getWindowToken() == null) {
697 mShowErrorAfterAttach = true;
698 return;
699 }
700
701 if (mErrorPopup == null) {
702 LayoutInflater inflater = LayoutInflater.from(mTextView.getContext());
703 final TextView err = (TextView) inflater.inflate(
704 com.android.internal.R.layout.textview_hint, null);
705
706 final float scale = mTextView.getResources().getDisplayMetrics().density;
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700707 mErrorPopup =
708 new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f));
Gilles Debunned88876a2012-03-16 17:34:04 -0700709 mErrorPopup.setFocusable(false);
710 // The user is entering text, so the input method is needed. We
711 // don't want the popup to be displayed on top of it.
712 mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
713 }
714
715 TextView tv = (TextView) mErrorPopup.getContentView();
716 chooseSize(mErrorPopup, mError, tv);
717 tv.setText(mError);
718
Hidehiko Tsuchiyaa0c8c1c2017-11-13 10:52:23 +0900719 mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY(),
720 Gravity.TOP | Gravity.LEFT);
Gilles Debunned88876a2012-03-16 17:34:04 -0700721 mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor());
722 }
723
724 public void setError(CharSequence error, Drawable icon) {
725 mError = TextUtils.stringOrSpannedString(error);
726 mErrorWasChanged = true;
Romain Guyd1cc1872012-11-05 17:43:25 -0800727
Gilles Debunned88876a2012-03-16 17:34:04 -0700728 if (mError == null) {
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800729 setErrorIcon(null);
Gilles Debunned88876a2012-03-16 17:34:04 -0700730 if (mErrorPopup != null) {
731 if (mErrorPopup.isShowing()) {
732 mErrorPopup.dismiss();
733 }
734
735 mErrorPopup = null;
736 }
Daniel 2 Olofssonf4ecc552013-08-13 10:30:26 +0200737 mShowErrorAfterAttach = false;
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800738 } else {
Romain Guyd1cc1872012-11-05 17:43:25 -0800739 setErrorIcon(icon);
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800740 if (mTextView.isFocused()) {
741 showError();
742 }
Romain Guyd1cc1872012-11-05 17:43:25 -0800743 }
744 }
745
746 private void setErrorIcon(Drawable icon) {
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800747 Drawables dr = mTextView.mDrawables;
748 if (dr == null) {
Fabrice Di Megliof7a5cdf2013-03-15 15:36:51 -0700749 mTextView.mDrawables = dr = new Drawables(mTextView.getContext());
Gilles Debunned88876a2012-03-16 17:34:04 -0700750 }
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800751 dr.setErrorDrawable(icon, mTextView);
752
753 mTextView.resetResolvedDrawables();
754 mTextView.invalidate();
755 mTextView.requestLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -0700756 }
757
758 private void hideError() {
759 if (mErrorPopup != null) {
760 if (mErrorPopup.isShowing()) {
761 mErrorPopup.dismiss();
762 }
763 }
764
765 mShowErrorAfterAttach = false;
766 }
767
768 /**
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800769 * Returns the X offset to make the pointy top of the error point
Gilles Debunned88876a2012-03-16 17:34:04 -0700770 * at the middle of the error icon.
771 */
772 private int getErrorX() {
773 /*
774 * The "25" is the distance between the point and the right edge
775 * of the background
776 */
777 final float scale = mTextView.getResources().getDisplayMetrics().density;
778
779 final Drawables dr = mTextView.mDrawables;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800780
781 final int layoutDirection = mTextView.getLayoutDirection();
782 int errorX;
783 int offset;
784 switch (layoutDirection) {
785 default:
786 case View.LAYOUT_DIRECTION_LTR:
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700787 offset = -(dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
788 errorX = mTextView.getWidth() - mErrorPopup.getWidth()
789 - mTextView.getPaddingRight() + offset;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800790 break;
791 case View.LAYOUT_DIRECTION_RTL:
792 offset = (dr != null ? dr.mDrawableSizeLeft : 0) / 2 - (int) (25 * scale + 0.5f);
793 errorX = mTextView.getPaddingLeft() + offset;
794 break;
795 }
796 return errorX;
Gilles Debunned88876a2012-03-16 17:34:04 -0700797 }
798
799 /**
800 * Returns the Y offset to make the pointy top of the error point
801 * at the bottom of the error icon.
802 */
803 private int getErrorY() {
804 /*
805 * Compound, not extended, because the icon is not clipped
806 * if the text height is smaller.
807 */
808 final int compoundPaddingTop = mTextView.getCompoundPaddingTop();
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700809 int vspace = mTextView.getBottom() - mTextView.getTop()
810 - mTextView.getCompoundPaddingBottom() - compoundPaddingTop;
Gilles Debunned88876a2012-03-16 17:34:04 -0700811
812 final Drawables dr = mTextView.mDrawables;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800813
814 final int layoutDirection = mTextView.getLayoutDirection();
815 int height;
816 switch (layoutDirection) {
817 default:
818 case View.LAYOUT_DIRECTION_LTR:
819 height = (dr != null ? dr.mDrawableHeightRight : 0);
820 break;
821 case View.LAYOUT_DIRECTION_RTL:
822 height = (dr != null ? dr.mDrawableHeightLeft : 0);
823 break;
824 }
825
826 int icontop = compoundPaddingTop + (vspace - height) / 2;
Gilles Debunned88876a2012-03-16 17:34:04 -0700827
828 /*
829 * The "2" is the distance between the point and the top edge
830 * of the background.
831 */
832 final float scale = mTextView.getResources().getDisplayMetrics().density;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800833 return icontop + height - mTextView.getHeight() - (int) (2 * scale + 0.5f);
Gilles Debunned88876a2012-03-16 17:34:04 -0700834 }
835
836 void createInputContentTypeIfNeeded() {
837 if (mInputContentType == null) {
838 mInputContentType = new InputContentType();
839 }
840 }
841
842 void createInputMethodStateIfNeeded() {
843 if (mInputMethodState == null) {
844 mInputMethodState = new InputMethodState();
845 }
846 }
847
Mihai Popaa4e39c42018-02-20 15:31:11 +0000848 private boolean isCursorVisible() {
Gilles Debunned88876a2012-03-16 17:34:04 -0700849 // The default value is true, even when there is no associated Editor
850 return mCursorVisible && mTextView.isTextEditable();
851 }
852
Mihai Popaa4e39c42018-02-20 15:31:11 +0000853 boolean shouldRenderCursor() {
854 if (!isCursorVisible()) {
855 return false;
856 }
857 if (mRenderCursorRegardlessTiming) {
858 return true;
859 }
860 final long showCursorDelta = SystemClock.uptimeMillis() - mShowCursor;
861 return showCursorDelta % (2 * BLINK) < BLINK;
862 }
863
Gilles Debunned88876a2012-03-16 17:34:04 -0700864 void prepareCursorControllers() {
865 boolean windowSupportsHandles = false;
866
867 ViewGroup.LayoutParams params = mTextView.getRootView().getLayoutParams();
868 if (params instanceof WindowManager.LayoutParams) {
869 WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
870 windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
871 || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
872 }
873
874 boolean enabled = windowSupportsHandles && mTextView.getLayout() != null;
875 mInsertionControllerEnabled = enabled && isCursorVisible();
876 mSelectionControllerEnabled = enabled && mTextView.textCanBeSelected();
877
878 if (!mInsertionControllerEnabled) {
879 hideInsertionPointCursorController();
880 if (mInsertionPointCursorController != null) {
881 mInsertionPointCursorController.onDetached();
882 mInsertionPointCursorController = null;
883 }
884 }
885
886 if (!mSelectionControllerEnabled) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100887 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -0700888 if (mSelectionModifierCursorController != null) {
889 mSelectionModifierCursorController.onDetached();
890 mSelectionModifierCursorController = null;
891 }
892 }
893 }
894
Seigo Nonakabb6a62c2015-03-31 21:59:30 +0900895 void hideInsertionPointCursorController() {
Gilles Debunned88876a2012-03-16 17:34:04 -0700896 if (mInsertionPointCursorController != null) {
897 mInsertionPointCursorController.hide();
898 }
899 }
900
901 /**
Mady Mellora2861452015-06-25 08:40:27 -0700902 * Hides the insertion and span controllers.
Gilles Debunned88876a2012-03-16 17:34:04 -0700903 */
Mady Mellora2861452015-06-25 08:40:27 -0700904 void hideCursorAndSpanControllers() {
Gilles Debunned88876a2012-03-16 17:34:04 -0700905 hideCursorControllers();
906 hideSpanControllers();
907 }
908
909 private void hideSpanControllers() {
Jean Chalardbaf30942013-02-28 16:01:51 -0800910 if (mSpanController != null) {
911 mSpanController.hide();
Gilles Debunned88876a2012-03-16 17:34:04 -0700912 }
913 }
914
915 private void hideCursorControllers() {
Yohei Yukawa85d08f12015-04-29 20:12:37 -0700916 // When mTextView is not ExtractEditText, we need to distinguish two kinds of focus-lost.
917 // One is the true focus lost where suggestions pop-up (if any) should be dismissed, and the
918 // other is an side effect of showing the suggestions pop-up itself. We use isShowingUp()
919 // to distinguish one from the other.
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700920 if (mSuggestionsPopupWindow != null && ((mTextView.isInExtractedMode())
921 || !mSuggestionsPopupWindow.isShowingUp())) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700922 // Should be done before hide insertion point controller since it triggers a show of it
923 mSuggestionsPopupWindow.hide();
924 }
925 hideInsertionPointCursorController();
Gilles Debunned88876a2012-03-16 17:34:04 -0700926 }
927
928 /**
929 * Create new SpellCheckSpans on the modified region.
930 */
931 private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
Satoshi Kataokad7429c12013-06-05 16:30:23 +0900932 // Remove spans whose adjacent characters are text not punctuation
933 mTextView.removeAdjacentSuggestionSpans(start);
934 mTextView.removeAdjacentSuggestionSpans(end);
935
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700936 if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled()
937 && !(mTextView.isInExtractedMode())) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700938 if (mSpellChecker == null && createSpellChecker) {
939 mSpellChecker = new SpellChecker(mTextView);
940 }
941 if (mSpellChecker != null) {
942 mSpellChecker.spellCheck(start, end);
943 }
944 }
945 }
946
947 void onScreenStateChanged(int screenState) {
948 switch (screenState) {
949 case View.SCREEN_STATE_ON:
950 resumeBlink();
951 break;
952 case View.SCREEN_STATE_OFF:
953 suspendBlink();
954 break;
955 }
956 }
957
958 private void suspendBlink() {
959 if (mBlink != null) {
960 mBlink.cancel();
961 }
962 }
963
964 private void resumeBlink() {
965 if (mBlink != null) {
966 mBlink.uncancel();
967 makeBlink();
968 }
969 }
970
971 void adjustInputType(boolean password, boolean passwordInputType,
972 boolean webPasswordInputType, boolean numberPasswordInputType) {
973 // mInputType has been set from inputType, possibly modified by mInputMethod.
974 // Specialize mInputType to [web]password if we have a text class and the original input
975 // type was a password.
976 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
977 if (password || passwordInputType) {
978 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
979 | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
980 }
981 if (webPasswordInputType) {
982 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
983 | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
984 }
985 } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
986 if (numberPasswordInputType) {
987 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
988 | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
989 }
990 }
991 }
992
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700993 private void chooseSize(@NonNull PopupWindow pop, @NonNull CharSequence text,
994 @NonNull TextView tv) {
995 final int wid = tv.getPaddingLeft() + tv.getPaddingRight();
996 final int ht = tv.getPaddingTop() + tv.getPaddingBottom();
Gilles Debunned88876a2012-03-16 17:34:04 -0700997
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700998 final int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize(
Gilles Debunned88876a2012-03-16 17:34:04 -0700999 com.android.internal.R.dimen.textview_error_popup_default_width);
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -07001000 final StaticLayout l = StaticLayout.Builder.obtain(text, 0, text.length(), tv.getPaint(),
1001 defaultWidthInPixels)
1002 .setUseLineSpacingFromFallbacks(tv.mUseFallbackLineSpacing)
1003 .build();
1004
Gilles Debunned88876a2012-03-16 17:34:04 -07001005 float max = 0;
1006 for (int i = 0; i < l.getLineCount(); i++) {
1007 max = Math.max(max, l.getLineWidth(i));
1008 }
1009
1010 /*
1011 * Now set the popup size to be big enough for the text plus the border capped
1012 * to DEFAULT_MAX_POPUP_WIDTH
1013 */
1014 pop.setWidth(wid + (int) Math.ceil(max));
1015 pop.setHeight(ht + l.getHeight());
1016 }
1017
1018 void setFrame() {
1019 if (mErrorPopup != null) {
1020 TextView tv = (TextView) mErrorPopup.getContentView();
1021 chooseSize(mErrorPopup, mError, tv);
1022 mErrorPopup.update(mTextView, getErrorX(), getErrorY(),
1023 mErrorPopup.getWidth(), mErrorPopup.getHeight());
1024 }
1025 }
1026
Mady Mellor2ff2cd82015-03-02 10:37:01 -08001027 private int getWordStart(int offset) {
1028 // FIXME - For this and similar methods we're not doing anything to check if there's
1029 // a LocaleSpan in the text, this may be something we should try handling or checking for.
Mady Mellor6c7b4ad2015-04-15 14:23:26 -07001030 int retOffset = getWordIteratorWithText().prevBoundary(offset);
Mady Mellor58c90872015-05-12 11:09:37 -07001031 if (getWordIteratorWithText().isOnPunctuation(retOffset)) {
1032 // On punctuation boundary or within group of punctuation, find punctuation start.
1033 retOffset = getWordIteratorWithText().getPunctuationBeginning(offset);
1034 } else {
1035 // Not on a punctuation boundary, find the word start.
Mady Mellore264ac32015-06-22 16:46:29 -07001036 retOffset = getWordIteratorWithText().getPrevWordBeginningOnTwoWordsBoundary(offset);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08001037 }
Mady Mellor6c7b4ad2015-04-15 14:23:26 -07001038 if (retOffset == BreakIterator.DONE) {
1039 return offset;
1040 }
1041 return retOffset;
1042 }
1043
1044 private int getWordEnd(int offset) {
1045 int retOffset = getWordIteratorWithText().nextBoundary(offset);
Mady Mellor58c90872015-05-12 11:09:37 -07001046 if (getWordIteratorWithText().isAfterPunctuation(retOffset)) {
1047 // On punctuation boundary or within group of punctuation, find punctuation end.
1048 retOffset = getWordIteratorWithText().getPunctuationEnd(offset);
1049 } else {
1050 // Not on a punctuation boundary, find the word end.
Mady Mellore264ac32015-06-22 16:46:29 -07001051 retOffset = getWordIteratorWithText().getNextWordEndOnTwoWordBoundary(offset);
Mady Mellor6c7b4ad2015-04-15 14:23:26 -07001052 }
1053 if (retOffset == BreakIterator.DONE) {
1054 return offset;
1055 }
1056 return retOffset;
1057 }
1058
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09001059 private boolean needsToSelectAllToSelectWordOrParagraph() {
Andrei Stingaceanu47f82ae2015-04-28 17:43:54 +01001060 if (mTextView.hasPasswordTransformationMethod()) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001061 // Always select all on a password field.
1062 // Cut/copy menu entries are not available for passwords, but being able to select all
1063 // is however useful to delete or paste to replace the entire content.
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09001064 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001065 }
1066
1067 int inputType = mTextView.getInputType();
1068 int klass = inputType & InputType.TYPE_MASK_CLASS;
1069 int variation = inputType & InputType.TYPE_MASK_VARIATION;
1070
1071 // Specific text field types: select the entire text for these
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001072 if (klass == InputType.TYPE_CLASS_NUMBER
1073 || klass == InputType.TYPE_CLASS_PHONE
1074 || klass == InputType.TYPE_CLASS_DATETIME
1075 || variation == InputType.TYPE_TEXT_VARIATION_URI
1076 || variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
1077 || variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS
1078 || variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09001079 return true;
1080 }
1081 return false;
1082 }
1083
1084 /**
1085 * Adjusts selection to the word under last touch offset. Return true if the operation was
1086 * successfully performed.
1087 */
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +01001088 boolean selectCurrentWord() {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09001089 if (!mTextView.canSelectText()) {
1090 return false;
1091 }
1092
1093 if (needsToSelectAllToSelectWordOrParagraph()) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001094 return mTextView.selectAllText();
1095 }
1096
1097 long lastTouchOffsets = getLastTouchOffsets();
1098 final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
1099 final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
1100
1101 // Safety check in case standard touch event handling has been bypassed
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08001102 if (minOffset < 0 || minOffset > mTextView.getText().length()) return false;
1103 if (maxOffset < 0 || maxOffset > mTextView.getText().length()) return false;
Gilles Debunned88876a2012-03-16 17:34:04 -07001104
1105 int selectionStart, selectionEnd;
1106
1107 // If a URLSpan (web address, email, phone...) is found at that position, select it.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001108 URLSpan[] urlSpans =
1109 ((Spanned) mTextView.getText()).getSpans(minOffset, maxOffset, URLSpan.class);
Gilles Debunned88876a2012-03-16 17:34:04 -07001110 if (urlSpans.length >= 1) {
1111 URLSpan urlSpan = urlSpans[0];
1112 selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan);
1113 selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan);
1114 } else {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08001115 // FIXME - We should check if there's a LocaleSpan in the text, this may be
1116 // something we should try handling or checking for.
Gilles Debunned88876a2012-03-16 17:34:04 -07001117 final WordIterator wordIterator = getWordIterator();
1118 wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset);
1119
1120 selectionStart = wordIterator.getBeginning(minOffset);
1121 selectionEnd = wordIterator.getEnd(maxOffset);
1122
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001123 if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE
1124 || selectionStart == selectionEnd) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001125 // Possible when the word iterator does not properly handle the text's language
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +09001126 long range = getCharClusterRange(minOffset);
Gilles Debunned88876a2012-03-16 17:34:04 -07001127 selectionStart = TextUtils.unpackRangeStartFromLong(range);
1128 selectionEnd = TextUtils.unpackRangeEndFromLong(range);
1129 }
1130 }
1131
1132 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
1133 return selectionEnd > selectionStart;
1134 }
1135
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09001136 /**
1137 * Adjusts selection to the paragraph under last touch offset. Return true if the operation was
1138 * successfully performed.
1139 */
1140 private boolean selectCurrentParagraph() {
1141 if (!mTextView.canSelectText()) {
1142 return false;
1143 }
1144
1145 if (needsToSelectAllToSelectWordOrParagraph()) {
1146 return mTextView.selectAllText();
1147 }
1148
1149 long lastTouchOffsets = getLastTouchOffsets();
1150 final int minLastTouchOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
1151 final int maxLastTouchOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
1152
1153 final long paragraphsRange = getParagraphsRange(minLastTouchOffset, maxLastTouchOffset);
1154 final int start = TextUtils.unpackRangeStartFromLong(paragraphsRange);
1155 final int end = TextUtils.unpackRangeEndFromLong(paragraphsRange);
1156 if (start < end) {
1157 Selection.setSelection((Spannable) mTextView.getText(), start, end);
1158 return true;
1159 }
1160 return false;
1161 }
1162
1163 /**
1164 * Get the minimum range of paragraphs that contains startOffset and endOffset.
1165 */
1166 private long getParagraphsRange(int startOffset, int endOffset) {
1167 final Layout layout = mTextView.getLayout();
1168 if (layout == null) {
1169 return TextUtils.packRangeInLong(-1, -1);
1170 }
1171 final CharSequence text = mTextView.getText();
1172 int minLine = layout.getLineForOffset(startOffset);
1173 // Search paragraph start.
1174 while (minLine > 0) {
1175 final int prevLineEndOffset = layout.getLineEnd(minLine - 1);
1176 if (text.charAt(prevLineEndOffset - 1) == '\n') {
1177 break;
1178 }
1179 minLine--;
1180 }
1181 int maxLine = layout.getLineForOffset(endOffset);
1182 // Search paragraph end.
1183 while (maxLine < layout.getLineCount() - 1) {
1184 final int lineEndOffset = layout.getLineEnd(maxLine);
1185 if (text.charAt(lineEndOffset - 1) == '\n') {
1186 break;
1187 }
1188 maxLine++;
1189 }
1190 return TextUtils.packRangeInLong(layout.getLineStart(minLine), layout.getLineEnd(maxLine));
1191 }
1192
Gilles Debunned88876a2012-03-16 17:34:04 -07001193 void onLocaleChanged() {
Keisuke Kuroyanagie0ac5ac2016-03-09 15:33:30 +09001194 // Will be re-created on demand in getWordIterator and getWordIteratorWithText with the
1195 // proper new locale
Gilles Debunned88876a2012-03-16 17:34:04 -07001196 mWordIterator = null;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08001197 mWordIteratorWithText = null;
Gilles Debunned88876a2012-03-16 17:34:04 -07001198 }
1199
Gilles Debunned88876a2012-03-16 17:34:04 -07001200 public WordIterator getWordIterator() {
1201 if (mWordIterator == null) {
1202 mWordIterator = new WordIterator(mTextView.getTextServicesLocale());
1203 }
1204 return mWordIterator;
1205 }
1206
Mady Mellor2ff2cd82015-03-02 10:37:01 -08001207 private WordIterator getWordIteratorWithText() {
1208 if (mWordIteratorWithText == null) {
1209 mWordIteratorWithText = new WordIterator(mTextView.getTextServicesLocale());
1210 mUpdateWordIteratorText = true;
1211 }
1212 if (mUpdateWordIteratorText) {
1213 // FIXME - Shouldn't copy all of the text as only the area of the text relevant
1214 // to the user's selection is needed. A possible solution would be to
1215 // copy some number N of characters near the selection and then when the
1216 // user approaches N then we'd do another copy of the next N characters.
1217 CharSequence text = mTextView.getText();
1218 mWordIteratorWithText.setCharSequence(text, 0, text.length());
1219 mUpdateWordIteratorText = false;
1220 }
1221 return mWordIteratorWithText;
1222 }
1223
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +09001224 private int getNextCursorOffset(int offset, boolean findAfterGivenOffset) {
1225 final Layout layout = mTextView.getLayout();
1226 if (layout == null) return offset;
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001227 return findAfterGivenOffset == layout.isRtlCharAt(offset)
1228 ? layout.getOffsetToLeftOf(offset) : layout.getOffsetToRightOf(offset);
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +09001229 }
1230
1231 private long getCharClusterRange(int offset) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001232 final int textLength = mTextView.getText().length();
Gilles Debunned88876a2012-03-16 17:34:04 -07001233 if (offset < textLength) {
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08001234 final int clusterEndOffset = getNextCursorOffset(offset, true);
1235 return TextUtils.packRangeInLong(
1236 getNextCursorOffset(clusterEndOffset, false), clusterEndOffset);
Gilles Debunned88876a2012-03-16 17:34:04 -07001237 }
1238 if (offset - 1 >= 0) {
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08001239 final int clusterStartOffset = getNextCursorOffset(offset, false);
1240 return TextUtils.packRangeInLong(clusterStartOffset,
1241 getNextCursorOffset(clusterStartOffset, true));
Gilles Debunned88876a2012-03-16 17:34:04 -07001242 }
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +09001243 return TextUtils.packRangeInLong(offset, offset);
Gilles Debunned88876a2012-03-16 17:34:04 -07001244 }
1245
1246 private boolean touchPositionIsInSelection() {
1247 int selectionStart = mTextView.getSelectionStart();
1248 int selectionEnd = mTextView.getSelectionEnd();
1249
1250 if (selectionStart == selectionEnd) {
1251 return false;
1252 }
1253
1254 if (selectionStart > selectionEnd) {
1255 int tmp = selectionStart;
1256 selectionStart = selectionEnd;
1257 selectionEnd = tmp;
1258 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
1259 }
1260
1261 SelectionModifierCursorController selectionController = getSelectionController();
1262 int minOffset = selectionController.getMinTouchOffset();
1263 int maxOffset = selectionController.getMaxTouchOffset();
1264
1265 return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
1266 }
1267
1268 private PositionListener getPositionListener() {
1269 if (mPositionListener == null) {
1270 mPositionListener = new PositionListener();
1271 }
1272 return mPositionListener;
1273 }
1274
1275 private interface TextViewPositionListener {
1276 public void updatePosition(int parentPositionX, int parentPositionY,
1277 boolean parentPositionChanged, boolean parentScrolled);
1278 }
1279
Gilles Debunned88876a2012-03-16 17:34:04 -07001280 private boolean isOffsetVisible(int offset) {
1281 Layout layout = mTextView.getLayout();
Victoria Leaseb9b77ae2013-10-13 15:12:52 -07001282 if (layout == null) return false;
1283
Gilles Debunned88876a2012-03-16 17:34:04 -07001284 final int line = layout.getLineForOffset(offset);
1285 final int lineBottom = layout.getLineBottom(line);
1286 final int primaryHorizontal = (int) layout.getPrimaryHorizontal(offset);
Phil Weaverc2e28932016-12-08 12:29:25 -08001287 return mTextView.isPositionVisible(
1288 primaryHorizontal + mTextView.viewportToContentHorizontalOffset(),
Gilles Debunned88876a2012-03-16 17:34:04 -07001289 lineBottom + mTextView.viewportToContentVerticalOffset());
1290 }
1291
1292 /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
1293 * in the view. Returns false when the position is in the empty space of left/right of text.
1294 */
1295 private boolean isPositionOnText(float x, float y) {
1296 Layout layout = mTextView.getLayout();
1297 if (layout == null) return false;
1298
1299 final int line = mTextView.getLineAtCoordinate(y);
1300 x = mTextView.convertToLocalHorizontalCoordinate(x);
1301
1302 if (x < layout.getLineLeft(line)) return false;
1303 if (x > layout.getLineRight(line)) return false;
1304 return true;
1305 }
1306
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001307 private void startDragAndDrop() {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001308 getSelectionActionModeHelper().onSelectionDrag();
1309
Keisuke Kuroyanagifdfc93d2016-03-15 14:47:08 +09001310 // TODO: Fix drag and drop in full screen extracted mode.
1311 if (mTextView.isInExtractedMode()) {
1312 return;
1313 }
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001314 final int start = mTextView.getSelectionStart();
1315 final int end = mTextView.getSelectionEnd();
1316 CharSequence selectedText = mTextView.getTransformedText(start, end);
1317 ClipData data = ClipData.newPlainText(null, selectedText);
1318 DragLocalState localState = new DragLocalState(mTextView, start, end);
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08001319 mTextView.startDragAndDrop(data, getTextThumbnailBuilder(start, end), localState,
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001320 View.DRAG_FLAG_GLOBAL);
1321 stopTextActionMode();
1322 if (hasSelectionController()) {
1323 getSelectionController().resetTouchOffsets();
1324 }
1325 }
1326
Gilles Debunned88876a2012-03-16 17:34:04 -07001327 public boolean performLongClick(boolean handled) {
Nikita Dubrovsky05cfcc82019-10-24 08:57:32 -07001328 if (TextView.DEBUG_CURSOR) {
1329 logCursor("performLongClick", "handled=%s", handled);
1330 }
sallyyuencc02ea32020-02-10 10:45:48 -08001331 if (mIsBeingLongClickedByAccessibility) {
1332 if (!handled) {
1333 toggleInsertionActionMode();
1334 }
1335 return true;
1336 }
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001337 // Long press in empty space moves cursor and starts the insertion action mode.
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08001338 if (!handled && !isPositionOnText(mTouchState.getLastDownX(), mTouchState.getLastDownY())
Shu Chen38451192020-01-21 14:32:32 +08001339 && !mTouchState.isOnHandle() && mInsertionControllerEnabled) {
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08001340 final int offset = mTextView.getOffsetForPosition(mTouchState.getLastDownX(),
1341 mTouchState.getLastDownY());
Gilles Debunned88876a2012-03-16 17:34:04 -07001342 Selection.setSelection((Spannable) mTextView.getText(), offset);
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00001343 getInsertionController().show();
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001344 mIsInsertionActionModeStartPending = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001345 handled = true;
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001346 MetricsLogger.action(
1347 mTextView.getContext(),
1348 MetricsEvent.TEXT_LONGPRESS,
1349 TextViewMetrics.SUBTYPE_LONG_PRESS_OTHER);
Gilles Debunned88876a2012-03-16 17:34:04 -07001350 }
1351
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001352 if (!handled && mTextActionMode != null) {
Andrei Stingaceanu2aaeefe2015-10-20 19:11:23 +01001353 if (touchPositionIsInSelection()) {
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001354 startDragAndDrop();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001355 MetricsLogger.action(
1356 mTextView.getContext(),
1357 MetricsEvent.TEXT_LONGPRESS,
1358 TextViewMetrics.SUBTYPE_LONG_PRESS_DRAG_AND_DROP);
Gilles Debunned88876a2012-03-16 17:34:04 -07001359 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001360 stopTextActionMode();
Clara Bayarridfac4432015-05-15 12:18:24 +01001361 selectCurrentWordAndStartDrag();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001362 MetricsLogger.action(
1363 mTextView.getContext(),
1364 MetricsEvent.TEXT_LONGPRESS,
1365 TextViewMetrics.SUBTYPE_LONG_PRESS_SELECTION);
Gilles Debunned88876a2012-03-16 17:34:04 -07001366 }
1367 handled = true;
1368 }
1369
1370 // Start a new selection
1371 if (!handled) {
Clara Bayarridfac4432015-05-15 12:18:24 +01001372 handled = selectCurrentWordAndStartDrag();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001373 if (handled) {
1374 MetricsLogger.action(
1375 mTextView.getContext(),
1376 MetricsEvent.TEXT_LONGPRESS,
1377 TextViewMetrics.SUBTYPE_LONG_PRESS_SELECTION);
1378 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001379 }
1380
1381 return handled;
1382 }
1383
sallyyuencc02ea32020-02-10 10:45:48 -08001384 private void toggleInsertionActionMode() {
1385 if (mTextActionMode != null) {
1386 stopTextActionMode();
1387 } else {
1388 startInsertionActionMode();
1389 }
1390 }
1391
Petar Å egina91df3f92017-08-15 16:20:43 +01001392 float getLastUpPositionX() {
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08001393 return mTouchState.getLastUpX();
Petar Å egina91df3f92017-08-15 16:20:43 +01001394 }
1395
1396 float getLastUpPositionY() {
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08001397 return mTouchState.getLastUpY();
Petar Å egina91df3f92017-08-15 16:20:43 +01001398 }
1399
Gilles Debunned88876a2012-03-16 17:34:04 -07001400 private long getLastTouchOffsets() {
1401 SelectionModifierCursorController selectionController = getSelectionController();
1402 final int minOffset = selectionController.getMinTouchOffset();
1403 final int maxOffset = selectionController.getMaxTouchOffset();
1404 return TextUtils.packRangeInLong(minOffset, maxOffset);
1405 }
1406
1407 void onFocusChanged(boolean focused, int direction) {
Nikita Dubrovsky05cfcc82019-10-24 08:57:32 -07001408 if (TextView.DEBUG_CURSOR) {
1409 logCursor("onFocusChanged", "focused=%s", focused);
1410 }
1411
Gilles Debunned88876a2012-03-16 17:34:04 -07001412 mShowCursor = SystemClock.uptimeMillis();
1413 ensureEndedBatchEdit();
1414
1415 if (focused) {
1416 int selStart = mTextView.getSelectionStart();
1417 int selEnd = mTextView.getSelectionEnd();
1418
1419 // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
1420 // mode for these, unless there was a specific selection already started.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001421 final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0
1422 && selEnd == mTextView.getText().length();
Gilles Debunned88876a2012-03-16 17:34:04 -07001423
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001424 mCreatedWithASelection = mFrozenWithFocus && mTextView.hasSelection()
1425 && !isFocusHighlighted;
Gilles Debunned88876a2012-03-16 17:34:04 -07001426
1427 if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
1428 // If a tap was used to give focus to that view, move cursor at tap position.
1429 // Has to be done before onTakeFocus, which can be overloaded.
1430 final int lastTapPosition = getLastTapPosition();
1431 if (lastTapPosition >= 0) {
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08001432 if (TextView.DEBUG_CURSOR) {
1433 logCursor("onFocusChanged", "setting cursor position: %d", lastTapPosition);
1434 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001435 Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition);
1436 }
1437
1438 // Note this may have to be moved out of the Editor class
1439 MovementMethod mMovement = mTextView.getMovementMethod();
1440 if (mMovement != null) {
1441 mMovement.onTakeFocus(mTextView, (Spannable) mTextView.getText(), direction);
1442 }
1443
1444 // The DecorView does not have focus when the 'Done' ExtractEditText button is
1445 // pressed. Since it is the ViewAncestor's mView, it requests focus before
1446 // ExtractEditText clears focus, which gives focus to the ExtractEditText.
1447 // This special case ensure that we keep current selection in that case.
1448 // It would be better to know why the DecorView does not have focus at that time.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001449 if (((mTextView.isInExtractedMode()) || mSelectionMoved)
1450 && selStart >= 0 && selEnd >= 0) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001451 /*
1452 * Someone intentionally set the selection, so let them
1453 * do whatever it is that they wanted to do instead of
1454 * the default on-focus behavior. We reset the selection
1455 * here instead of just skipping the onTakeFocus() call
1456 * because some movement methods do something other than
1457 * just setting the selection in theirs and we still
1458 * need to go through that path.
1459 */
1460 Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
1461 }
1462
1463 if (mSelectAllOnFocus) {
1464 mTextView.selectAllText();
1465 }
1466
1467 mTouchFocusSelected = true;
1468 }
1469
1470 mFrozenWithFocus = false;
1471 mSelectionMoved = false;
1472
1473 if (mError != null) {
1474 showError();
1475 }
1476
1477 makeBlink();
1478 } else {
1479 if (mError != null) {
1480 hideError();
1481 }
1482 // Don't leave us in the middle of a batch edit.
1483 mTextView.onEndBatchEdit();
1484
Andrei Stingaceanub1891b32015-06-19 16:44:37 +01001485 if (mTextView.isInExtractedMode()) {
Mady Mellora2861452015-06-25 08:40:27 -07001486 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001487 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -07001488 } else {
Mady Mellora2861452015-06-25 08:40:27 -07001489 hideCursorAndSpanControllers();
Yohei Yukawa24df9312016-03-31 17:15:23 -07001490 if (mTextView.isTemporarilyDetached()) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001491 stopTextActionModeWithPreservingSelection();
1492 } else {
1493 stopTextActionMode();
1494 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001495 downgradeEasyCorrectionSpans();
1496 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001497 // No need to create the controller
1498 if (mSelectionModifierCursorController != null) {
1499 mSelectionModifierCursorController.resetTouchOffsets();
1500 }
Richard Ledley5f2f8202018-02-05 14:55:47 +00001501
1502 ensureNoSelectionIfNonSelectable();
1503 }
1504 }
1505
1506 private void ensureNoSelectionIfNonSelectable() {
1507 // This could be the case if a TextLink has been tapped.
1508 if (!mTextView.textCanBeSelected() && mTextView.hasSelection()) {
1509 Selection.setSelection((Spannable) mTextView.getText(),
1510 mTextView.length(), mTextView.length());
Gilles Debunned88876a2012-03-16 17:34:04 -07001511 }
1512 }
1513
1514 /**
1515 * Downgrades to simple suggestions all the easy correction spans that are not a spell check
1516 * span.
1517 */
1518 private void downgradeEasyCorrectionSpans() {
1519 CharSequence text = mTextView.getText();
1520 if (text instanceof Spannable) {
1521 Spannable spannable = (Spannable) text;
1522 SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
1523 spannable.length(), SuggestionSpan.class);
1524 for (int i = 0; i < suggestionSpans.length; i++) {
1525 int flags = suggestionSpans[i].getFlags();
1526 if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
1527 && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
1528 flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
1529 suggestionSpans[i].setFlags(flags);
1530 }
1531 }
1532 }
1533 }
1534
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +01001535 void sendOnTextChanged(int start, int before, int after) {
1536 getSelectionActionModeHelper().onTextChanged(start, start + before);
Gilles Debunned88876a2012-03-16 17:34:04 -07001537 updateSpellCheckSpans(start, start + after, false);
1538
Mady Mellor2ff2cd82015-03-02 10:37:01 -08001539 // Flip flag to indicate the word iterator needs to have the text reset.
1540 mUpdateWordIteratorText = true;
1541
Gilles Debunned88876a2012-03-16 17:34:04 -07001542 // Hide the controllers as soon as text is modified (typing, procedural...)
1543 // We do not hide the span controllers, since they can be added when a new text is
1544 // inserted into the text view (voice IME).
1545 hideCursorControllers();
Keisuke Kuroyanagif4e347d2015-06-11 17:41:00 +09001546 // Reset drag accelerator.
1547 if (mSelectionModifierCursorController != null) {
1548 mSelectionModifierCursorController.resetTouchOffsets();
1549 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001550 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07001551 }
1552
1553 private int getLastTapPosition() {
1554 // No need to create the controller at that point, no last tap position saved
1555 if (mSelectionModifierCursorController != null) {
1556 int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
1557 if (lastTapPosition >= 0) {
1558 // Safety check, should not be possible.
1559 if (lastTapPosition > mTextView.getText().length()) {
1560 lastTapPosition = mTextView.getText().length();
1561 }
1562 return lastTapPosition;
1563 }
1564 }
1565
1566 return -1;
1567 }
1568
1569 void onWindowFocusChanged(boolean hasWindowFocus) {
1570 if (hasWindowFocus) {
1571 if (mBlink != null) {
1572 mBlink.uncancel();
1573 makeBlink();
1574 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001575 if (mTextView.hasSelection() && !extractedTextModeWillBeStarted()) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001576 refreshTextActionMode();
Mady Mellora2861452015-06-25 08:40:27 -07001577 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001578 } else {
1579 if (mBlink != null) {
1580 mBlink.cancel();
1581 }
1582 if (mInputContentType != null) {
1583 mInputContentType.enterDown = false;
1584 }
1585 // Order matters! Must be done before onParentLostFocus to rely on isShowingUp
Mady Mellora2861452015-06-25 08:40:27 -07001586 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001587 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -07001588 if (mSuggestionsPopupWindow != null) {
1589 mSuggestionsPopupWindow.onParentLostFocus();
1590 }
1591
Gilles Debunnec72fba82012-06-26 14:47:07 -07001592 // Don't leave us in the middle of a batch edit. Same as in onFocusChanged
1593 ensureEndedBatchEdit();
Richard Ledley5f2f8202018-02-05 14:55:47 +00001594
1595 ensureNoSelectionIfNonSelectable();
Gilles Debunned88876a2012-03-16 17:34:04 -07001596 }
1597 }
1598
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09001599 private boolean shouldFilterOutTouchEvent(MotionEvent event) {
1600 if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) {
1601 return false;
1602 }
1603 final boolean primaryButtonStateChanged =
1604 ((mLastButtonState ^ event.getButtonState()) & MotionEvent.BUTTON_PRIMARY) != 0;
1605 final int action = event.getActionMasked();
1606 if ((action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_UP)
1607 && !primaryButtonStateChanged) {
1608 return true;
1609 }
1610 if (action == MotionEvent.ACTION_MOVE
1611 && !event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
1612 return true;
1613 }
1614 return false;
1615 }
1616
Nikita Dubrovsky21c6a962019-12-27 08:48:02 -08001617 /**
1618 * Handles touch events on an editable text view, implementing cursor movement, selection, etc.
1619 */
1620 @VisibleForTesting
1621 public void onTouchEvent(MotionEvent event) {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09001622 final boolean filterOutEvent = shouldFilterOutTouchEvent(event);
1623 mLastButtonState = event.getButtonState();
1624 if (filterOutEvent) {
1625 if (event.getActionMasked() == MotionEvent.ACTION_UP) {
1626 mDiscardNextActionUp = true;
1627 }
1628 return;
1629 }
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08001630 ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext());
1631 mTouchState.update(event, viewConfiguration);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001632 updateFloatingToolbarVisibility(event);
1633
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08001634 if (hasInsertionController()) {
1635 getInsertionController().onTouchEvent(event);
1636 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001637 if (hasSelectionController()) {
1638 getSelectionController().onTouchEvent(event);
1639 }
1640
1641 if (mShowSuggestionRunnable != null) {
1642 mTextView.removeCallbacks(mShowSuggestionRunnable);
1643 mShowSuggestionRunnable = null;
1644 }
1645
1646 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001647 // Reset this state; it will be re-set if super.onTouchEvent
1648 // causes focus to move to the view.
1649 mTouchFocusSelected = false;
1650 mIgnoreActionUpEvent = false;
1651 }
1652 }
1653
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001654 private void updateFloatingToolbarVisibility(MotionEvent event) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001655 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001656 switch (event.getActionMasked()) {
1657 case MotionEvent.ACTION_MOVE:
Abodunrinwa Toki4a7aeb32017-07-13 02:06:56 +01001658 hideFloatingToolbar(ActionMode.DEFAULT_HIDE_DURATION);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001659 break;
1660 case MotionEvent.ACTION_UP: // fall through
1661 case MotionEvent.ACTION_CANCEL:
1662 showFloatingToolbar();
1663 }
1664 }
1665 }
1666
Abodunrinwa Toki4a7aeb32017-07-13 02:06:56 +01001667 void hideFloatingToolbar(int duration) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001668 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001669 mTextView.removeCallbacks(mShowFloatingToolbar);
Abodunrinwa Toki4a7aeb32017-07-13 02:06:56 +01001670 mTextActionMode.hide(duration);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001671 }
1672 }
1673
1674 private void showFloatingToolbar() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001675 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001676 // Delay "show" so it doesn't interfere with click confirmations
1677 // or double-clicks that could "dismiss" the floating toolbar.
1678 int delay = ViewConfiguration.getDoubleTapTimeout();
1679 mTextView.postDelayed(mShowFloatingToolbar, delay);
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01001680
1681 // This classifies the text and most likely returns before the toolbar is actually
1682 // shown. If not, it will update the toolbar with the result when classification
1683 // returns. We would rather not wait for a long running classification process.
1684 invalidateActionModeAsync();
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001685 }
1686 }
1687
Yohei Yukawa484d4af2018-09-17 16:47:08 -07001688 private InputMethodManager getInputMethodManager() {
1689 return mTextView.getContext().getSystemService(InputMethodManager.class);
1690 }
1691
Gilles Debunned88876a2012-03-16 17:34:04 -07001692 public void beginBatchEdit() {
1693 mInBatchEditControllers = true;
1694 final InputMethodState ims = mInputMethodState;
1695 if (ims != null) {
1696 int nesting = ++ims.mBatchEditNesting;
1697 if (nesting == 1) {
1698 ims.mCursorChanged = false;
1699 ims.mChangedDelta = 0;
1700 if (ims.mContentChanged) {
1701 // We already have a pending change from somewhere else,
1702 // so turn this into a full update.
1703 ims.mChangedStart = 0;
1704 ims.mChangedEnd = mTextView.getText().length();
1705 } else {
1706 ims.mChangedStart = EXTRACT_UNKNOWN;
1707 ims.mChangedEnd = EXTRACT_UNKNOWN;
1708 ims.mContentChanged = false;
1709 }
James Cook48e0fac2015-02-25 15:44:51 -08001710 mUndoInputFilter.beginBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001711 mTextView.onBeginBatchEdit();
1712 }
1713 }
1714 }
1715
1716 public void endBatchEdit() {
1717 mInBatchEditControllers = false;
1718 final InputMethodState ims = mInputMethodState;
1719 if (ims != null) {
1720 int nesting = --ims.mBatchEditNesting;
1721 if (nesting == 0) {
1722 finishBatchEdit(ims);
1723 }
1724 }
1725 }
1726
1727 void ensureEndedBatchEdit() {
1728 final InputMethodState ims = mInputMethodState;
1729 if (ims != null && ims.mBatchEditNesting != 0) {
1730 ims.mBatchEditNesting = 0;
1731 finishBatchEdit(ims);
1732 }
1733 }
1734
1735 void finishBatchEdit(final InputMethodState ims) {
1736 mTextView.onEndBatchEdit();
James Cook48e0fac2015-02-25 15:44:51 -08001737 mUndoInputFilter.endBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001738
1739 if (ims.mContentChanged || ims.mSelectionModeChanged) {
1740 mTextView.updateAfterEdit();
1741 reportExtractedText();
1742 } else if (ims.mCursorChanged) {
Jean Chalardc99d33f2013-02-28 16:39:47 -08001743 // Cheesy way to get us to report the current cursor location.
Gilles Debunned88876a2012-03-16 17:34:04 -07001744 mTextView.invalidateCursor();
1745 }
Jean Chalardc99d33f2013-02-28 16:39:47 -08001746 // sendUpdateSelection knows to avoid sending if the selection did
1747 // not actually change.
1748 sendUpdateSelection();
Keisuke Kuroyanagic6fad962016-05-02 15:11:41 +09001749
1750 // Show drag handles if they were blocked by batch edit mode.
1751 if (mTextActionMode != null) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001752 final CursorController cursorController = mTextView.hasSelection()
1753 ? getSelectionController() : getInsertionController();
Keisuke Kuroyanagic6fad962016-05-02 15:11:41 +09001754 if (cursorController != null && !cursorController.isActive()
1755 && !cursorController.isCursorBeingModified()) {
1756 cursorController.show();
1757 }
1758 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001759 }
1760
1761 static final int EXTRACT_NOTHING = -2;
1762 static final int EXTRACT_UNKNOWN = -1;
1763
1764 boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
1765 return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
1766 EXTRACT_UNKNOWN, outText);
1767 }
1768
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001769 private boolean extractTextInternal(@Nullable ExtractedTextRequest request,
Gilles Debunned88876a2012-03-16 17:34:04 -07001770 int partialStartOffset, int partialEndOffset, int delta,
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001771 @Nullable ExtractedText outText) {
1772 if (request == null || outText == null) {
1773 return false;
Gilles Debunned88876a2012-03-16 17:34:04 -07001774 }
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001775
1776 final CharSequence content = mTextView.getText();
1777 if (content == null) {
1778 return false;
1779 }
1780
1781 if (partialStartOffset != EXTRACT_NOTHING) {
1782 final int N = content.length();
1783 if (partialStartOffset < 0) {
1784 outText.partialStartOffset = outText.partialEndOffset = -1;
1785 partialStartOffset = 0;
1786 partialEndOffset = N;
1787 } else {
1788 // Now use the delta to determine the actual amount of text
1789 // we need.
1790 partialEndOffset += delta;
1791 // Adjust offsets to ensure we contain full spans.
1792 if (content instanceof Spanned) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001793 Spanned spanned = (Spanned) content;
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001794 Object[] spans = spanned.getSpans(partialStartOffset,
1795 partialEndOffset, ParcelableSpan.class);
1796 int i = spans.length;
1797 while (i > 0) {
1798 i--;
1799 int j = spanned.getSpanStart(spans[i]);
1800 if (j < partialStartOffset) partialStartOffset = j;
1801 j = spanned.getSpanEnd(spans[i]);
1802 if (j > partialEndOffset) partialEndOffset = j;
1803 }
1804 }
1805 outText.partialStartOffset = partialStartOffset;
1806 outText.partialEndOffset = partialEndOffset - delta;
1807
1808 if (partialStartOffset > N) {
1809 partialStartOffset = N;
1810 } else if (partialStartOffset < 0) {
1811 partialStartOffset = 0;
1812 }
1813 if (partialEndOffset > N) {
1814 partialEndOffset = N;
1815 } else if (partialEndOffset < 0) {
1816 partialEndOffset = 0;
1817 }
1818 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001819 if ((request.flags & InputConnection.GET_TEXT_WITH_STYLES) != 0) {
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001820 outText.text = content.subSequence(partialStartOffset,
1821 partialEndOffset);
1822 } else {
1823 outText.text = TextUtils.substring(content, partialStartOffset,
1824 partialEndOffset);
1825 }
1826 } else {
1827 outText.partialStartOffset = 0;
1828 outText.partialEndOffset = 0;
1829 outText.text = "";
1830 }
1831 outText.flags = 0;
1832 if (MetaKeyKeyListener.getMetaState(content, MetaKeyKeyListener.META_SELECTING) != 0) {
1833 outText.flags |= ExtractedText.FLAG_SELECTING;
1834 }
1835 if (mTextView.isSingleLine()) {
1836 outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
1837 }
1838 outText.startOffset = 0;
1839 outText.selectionStart = mTextView.getSelectionStart();
1840 outText.selectionEnd = mTextView.getSelectionEnd();
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001841 outText.hint = mTextView.getHint();
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001842 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001843 }
1844
1845 boolean reportExtractedText() {
1846 final Editor.InputMethodState ims = mInputMethodState;
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001847 if (ims == null) {
1848 return false;
1849 }
Clara Bayarri038f7a82018-06-04 15:00:07 +01001850 final boolean wasContentChanged = ims.mContentChanged;
1851 if (!wasContentChanged && !ims.mSelectionModeChanged) {
1852 return false;
1853 }
1854 ims.mContentChanged = false;
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001855 ims.mSelectionModeChanged = false;
1856 final ExtractedTextRequest req = ims.mExtractedTextRequest;
1857 if (req == null) {
1858 return false;
1859 }
Yohei Yukawa484d4af2018-09-17 16:47:08 -07001860 final InputMethodManager imm = getInputMethodManager();
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001861 if (imm == null) {
1862 return false;
1863 }
1864 if (TextView.DEBUG_EXTRACT) {
1865 Log.v(TextView.LOG_TAG, "Retrieving extracted start="
1866 + ims.mChangedStart
1867 + " end=" + ims.mChangedEnd
1868 + " delta=" + ims.mChangedDelta);
1869 }
Clara Bayarri038f7a82018-06-04 15:00:07 +01001870 if (ims.mChangedStart < 0 && !wasContentChanged) {
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001871 ims.mChangedStart = EXTRACT_NOTHING;
1872 }
1873 if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
1874 ims.mChangedDelta, ims.mExtractedText)) {
1875 if (TextView.DEBUG_EXTRACT) {
1876 Log.v(TextView.LOG_TAG,
1877 "Reporting extracted start="
1878 + ims.mExtractedText.partialStartOffset
1879 + " end=" + ims.mExtractedText.partialEndOffset
1880 + ": " + ims.mExtractedText.text);
Gilles Debunned88876a2012-03-16 17:34:04 -07001881 }
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001882
1883 imm.updateExtractedText(mTextView, req.token, ims.mExtractedText);
1884 ims.mChangedStart = EXTRACT_UNKNOWN;
1885 ims.mChangedEnd = EXTRACT_UNKNOWN;
1886 ims.mChangedDelta = 0;
1887 ims.mContentChanged = false;
1888 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001889 }
1890 return false;
1891 }
1892
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001893 private void sendUpdateSelection() {
1894 if (null != mInputMethodState && mInputMethodState.mBatchEditNesting <= 0) {
Yohei Yukawa484d4af2018-09-17 16:47:08 -07001895 final InputMethodManager imm = getInputMethodManager();
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001896 if (null != imm) {
1897 final int selectionStart = mTextView.getSelectionStart();
1898 final int selectionEnd = mTextView.getSelectionEnd();
1899 int candStart = -1;
1900 int candEnd = -1;
1901 if (mTextView.getText() instanceof Spannable) {
1902 final Spannable sp = (Spannable) mTextView.getText();
1903 candStart = EditableInputConnection.getComposingSpanStart(sp);
1904 candEnd = EditableInputConnection.getComposingSpanEnd(sp);
1905 }
Jean Chalardc99d33f2013-02-28 16:39:47 -08001906 // InputMethodManager#updateSelection skips sending the message if
1907 // none of the parameters have changed since the last time we called it.
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001908 imm.updateSelection(mTextView,
1909 selectionStart, selectionEnd, candStart, candEnd);
1910 }
1911 }
1912 }
1913
Gilles Debunned88876a2012-03-16 17:34:04 -07001914 void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint,
1915 int cursorOffsetVertical) {
1916 final int selectionStart = mTextView.getSelectionStart();
1917 final int selectionEnd = mTextView.getSelectionEnd();
1918
1919 final InputMethodState ims = mInputMethodState;
1920 if (ims != null && ims.mBatchEditNesting == 0) {
Yohei Yukawa484d4af2018-09-17 16:47:08 -07001921 InputMethodManager imm = getInputMethodManager();
Gilles Debunned88876a2012-03-16 17:34:04 -07001922 if (imm != null) {
1923 if (imm.isActive(mTextView)) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001924 if (ims.mContentChanged || ims.mSelectionModeChanged) {
1925 // We are in extract mode and the content has changed
1926 // in some way... just report complete new text to the
1927 // input method.
Yohei Yukawab6bec1a2015-05-01 16:18:25 -07001928 reportExtractedText();
Gilles Debunned88876a2012-03-16 17:34:04 -07001929 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001930 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001931 }
1932 }
1933
1934 if (mCorrectionHighlighter != null) {
1935 mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
1936 }
1937
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07001938 if (highlight != null && selectionStart == selectionEnd && mDrawableForCursor != null) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001939 drawCursor(canvas, cursorOffsetVertical);
1940 // Rely on the drawable entirely, do not draw the cursor line.
1941 // Has to be done after the IMM related code above which relies on the highlight.
1942 highlight = null;
1943 }
1944
Jan Althaus80620c52018-02-02 17:39:22 +01001945 if (mSelectionActionModeHelper != null) {
1946 mSelectionActionModeHelper.onDraw(canvas);
1947 if (mSelectionActionModeHelper.isDrawingHighlight()) {
1948 highlight = null;
1949 }
1950 }
1951
Gilles Debunned88876a2012-03-16 17:34:04 -07001952 if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
1953 drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
1954 cursorOffsetVertical);
1955 } else {
1956 layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
1957 }
1958 }
1959
1960 private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
1961 Paint highlightPaint, int cursorOffsetVertical) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001962 final long lineRange = layout.getLineRangeForDraw(canvas);
1963 int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
1964 int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
1965 if (lastLine < 0) return;
1966
1967 layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
1968 firstLine, lastLine);
1969
1970 if (layout instanceof DynamicLayout) {
Chris Craik956f3402015-04-27 16:41:00 -07001971 if (mTextRenderNodes == null) {
1972 mTextRenderNodes = ArrayUtils.emptyArray(TextRenderNode.class);
Gilles Debunned88876a2012-03-16 17:34:04 -07001973 }
1974
1975 DynamicLayout dynamicLayout = (DynamicLayout) layout;
Gilles Debunne157aafc2012-04-19 17:21:57 -07001976 int[] blockEndLines = dynamicLayout.getBlockEndLines();
Gilles Debunned88876a2012-03-16 17:34:04 -07001977 int[] blockIndices = dynamicLayout.getBlockIndices();
1978 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
Sangkyu Lee955beb22012-12-10 15:47:00 +09001979 final int indexFirstChangedBlock = dynamicLayout.getIndexFirstChangedBlock();
Gilles Debunned88876a2012-03-16 17:34:04 -07001980
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001981 final ArraySet<Integer> blockSet = dynamicLayout.getBlocksAlwaysNeedToBeRedrawn();
1982 if (blockSet != null) {
1983 for (int i = 0; i < blockSet.size(); i++) {
1984 final int blockIndex = dynamicLayout.getBlockIndex(blockSet.valueAt(i));
1985 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
1986 && mTextRenderNodes[blockIndex] != null) {
1987 mTextRenderNodes[blockIndex].needsToBeShifted = true;
1988 }
1989 }
1990 }
1991
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001992 int startBlock = Arrays.binarySearch(blockEndLines, 0, numberOfBlocks, firstLine);
1993 if (startBlock < 0) {
1994 startBlock = -(startBlock + 1);
1995 }
1996 startBlock = Math.min(indexFirstChangedBlock, startBlock);
Gilles Debunned88876a2012-03-16 17:34:04 -07001997
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001998 int startIndexToFindAvailableRenderNode = 0;
1999 int lastIndex = numberOfBlocks;
2000
2001 for (int i = startBlock; i < numberOfBlocks; i++) {
2002 final int blockIndex = blockIndices[i];
2003 if (i >= indexFirstChangedBlock
2004 && blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
2005 && mTextRenderNodes[blockIndex] != null) {
2006 mTextRenderNodes[blockIndex].needsToBeShifted = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07002007 }
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09002008 if (blockEndLines[i] < firstLine) {
2009 // Blocks in [indexFirstChangedBlock, firstLine) are not redrawn here. They will
2010 // be redrawn after they get scrolled into drawing range.
2011 continue;
Gilles Debunned88876a2012-03-16 17:34:04 -07002012 }
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09002013 startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas, layout,
2014 highlight, highlightPaint, cursorOffsetVertical, blockEndLines,
2015 blockIndices, i, numberOfBlocks, startIndexToFindAvailableRenderNode);
2016 if (blockEndLines[i] >= lastLine) {
2017 lastIndex = Math.max(indexFirstChangedBlock, i + 1);
2018 break;
Gilles Debunned88876a2012-03-16 17:34:04 -07002019 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002020 }
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09002021 if (blockSet != null) {
2022 for (int i = 0; i < blockSet.size(); i++) {
2023 final int block = blockSet.valueAt(i);
2024 final int blockIndex = dynamicLayout.getBlockIndex(block);
2025 if (blockIndex == DynamicLayout.INVALID_BLOCK_INDEX
2026 || mTextRenderNodes[blockIndex] == null
2027 || mTextRenderNodes[blockIndex].needsToBeShifted) {
2028 startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas,
2029 layout, highlight, highlightPaint, cursorOffsetVertical,
2030 blockEndLines, blockIndices, block, numberOfBlocks,
2031 startIndexToFindAvailableRenderNode);
2032 }
2033 }
2034 }
Sangkyu Lee955beb22012-12-10 15:47:00 +09002035
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09002036 dynamicLayout.setIndexFirstChangedBlock(lastIndex);
Gilles Debunned88876a2012-03-16 17:34:04 -07002037 } else {
2038 // Boring layout is used for empty and hint text
2039 layout.drawText(canvas, firstLine, lastLine);
2040 }
2041 }
2042
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09002043 private int drawHardwareAcceleratedInner(Canvas canvas, Layout layout, Path highlight,
2044 Paint highlightPaint, int cursorOffsetVertical, int[] blockEndLines,
2045 int[] blockIndices, int blockInfoIndex, int numberOfBlocks,
2046 int startIndexToFindAvailableRenderNode) {
2047 final int blockEndLine = blockEndLines[blockInfoIndex];
2048 int blockIndex = blockIndices[blockInfoIndex];
2049
2050 final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
2051 if (blockIsInvalid) {
2052 blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
2053 startIndexToFindAvailableRenderNode);
2054 // Note how dynamic layout's internal block indices get updated from Editor
2055 blockIndices[blockInfoIndex] = blockIndex;
2056 if (mTextRenderNodes[blockIndex] != null) {
2057 mTextRenderNodes[blockIndex].isDirty = true;
2058 }
2059 startIndexToFindAvailableRenderNode = blockIndex + 1;
2060 }
2061
2062 if (mTextRenderNodes[blockIndex] == null) {
2063 mTextRenderNodes[blockIndex] = new TextRenderNode("Text " + blockIndex);
2064 }
2065
2066 final boolean blockDisplayListIsInvalid = mTextRenderNodes[blockIndex].needsRecord();
2067 RenderNode blockDisplayList = mTextRenderNodes[blockIndex].renderNode;
2068 if (mTextRenderNodes[blockIndex].needsToBeShifted || blockDisplayListIsInvalid) {
2069 final int blockBeginLine = blockInfoIndex == 0 ?
2070 0 : blockEndLines[blockInfoIndex - 1] + 1;
2071 final int top = layout.getLineTop(blockBeginLine);
2072 final int bottom = layout.getLineBottom(blockEndLine);
2073 int left = 0;
2074 int right = mTextView.getWidth();
2075 if (mTextView.getHorizontallyScrolling()) {
2076 float min = Float.MAX_VALUE;
2077 float max = Float.MIN_VALUE;
2078 for (int line = blockBeginLine; line <= blockEndLine; line++) {
2079 min = Math.min(min, layout.getLineLeft(line));
2080 max = Math.max(max, layout.getLineRight(line));
2081 }
2082 left = (int) min;
2083 right = (int) (max + 0.5f);
2084 }
2085
2086 // Rebuild display list if it is invalid
2087 if (blockDisplayListIsInvalid) {
John Recke57475e2019-02-20 17:39:52 -08002088 final RecordingCanvas recordingCanvas = blockDisplayList.beginRecording(
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09002089 right - left, bottom - top);
2090 try {
2091 // drawText is always relative to TextView's origin, this translation
2092 // brings this range of text back to the top left corner of the viewport
John Reck32f140aa62018-10-04 15:08:24 -07002093 recordingCanvas.translate(-left, -top);
2094 layout.drawText(recordingCanvas, blockBeginLine, blockEndLine);
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09002095 mTextRenderNodes[blockIndex].isDirty = false;
2096 // No need to untranslate, previous context is popped after
2097 // drawDisplayList
2098 } finally {
John Recke57475e2019-02-20 17:39:52 -08002099 blockDisplayList.endRecording();
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09002100 // Same as drawDisplayList below, handled by our TextView's parent
2101 blockDisplayList.setClipToBounds(false);
2102 }
2103 }
2104
2105 // Valid display list only needs to update its drawing location.
2106 blockDisplayList.setLeftTopRightBottom(left, top, right, bottom);
2107 mTextRenderNodes[blockIndex].needsToBeShifted = false;
2108 }
John Reck32f140aa62018-10-04 15:08:24 -07002109 ((RecordingCanvas) canvas).drawRenderNode(blockDisplayList);
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09002110 return startIndexToFindAvailableRenderNode;
2111 }
2112
Gilles Debunned88876a2012-03-16 17:34:04 -07002113 private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
2114 int searchStartIndex) {
Chris Craik956f3402015-04-27 16:41:00 -07002115 int length = mTextRenderNodes.length;
Gilles Debunned88876a2012-03-16 17:34:04 -07002116 for (int i = searchStartIndex; i < length; i++) {
2117 boolean blockIndexFound = false;
2118 for (int j = 0; j < numberOfBlocks; j++) {
2119 if (blockIndices[j] == i) {
2120 blockIndexFound = true;
2121 break;
2122 }
2123 }
2124 if (blockIndexFound) continue;
2125 return i;
2126 }
2127
2128 // No available index found, the pool has to grow
Chris Craik956f3402015-04-27 16:41:00 -07002129 mTextRenderNodes = GrowingArrayUtils.append(mTextRenderNodes, length, null);
Gilles Debunned88876a2012-03-16 17:34:04 -07002130 return length;
2131 }
2132
2133 private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
2134 final boolean translate = cursorOffsetVertical != 0;
2135 if (translate) canvas.translate(0, cursorOffsetVertical);
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07002136 if (mDrawableForCursor != null) {
2137 mDrawableForCursor.draw(canvas);
Gilles Debunned88876a2012-03-16 17:34:04 -07002138 }
2139 if (translate) canvas.translate(0, -cursorOffsetVertical);
2140 }
2141
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09002142 void invalidateHandlesAndActionMode() {
2143 if (mSelectionModifierCursorController != null) {
2144 mSelectionModifierCursorController.invalidateHandles();
2145 }
2146 if (mInsertionPointCursorController != null) {
2147 mInsertionPointCursorController.invalidateHandle();
2148 }
2149 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002150 invalidateActionMode();
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09002151 }
2152 }
2153
Gilles Debunneebc86af2012-04-20 15:10:47 -07002154 /**
2155 * Invalidates all the sub-display lists that overlap the specified character range
2156 */
2157 void invalidateTextDisplayList(Layout layout, int start, int end) {
Chris Craik956f3402015-04-27 16:41:00 -07002158 if (mTextRenderNodes != null && layout instanceof DynamicLayout) {
Gilles Debunneebc86af2012-04-20 15:10:47 -07002159 final int firstLine = layout.getLineForOffset(start);
2160 final int lastLine = layout.getLineForOffset(end);
2161
2162 DynamicLayout dynamicLayout = (DynamicLayout) layout;
2163 int[] blockEndLines = dynamicLayout.getBlockEndLines();
2164 int[] blockIndices = dynamicLayout.getBlockIndices();
2165 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
2166
2167 int i = 0;
2168 // Skip the blocks before firstLine
2169 while (i < numberOfBlocks) {
2170 if (blockEndLines[i] >= firstLine) break;
2171 i++;
2172 }
2173
2174 // Invalidate all subsequent blocks until lastLine is passed
2175 while (i < numberOfBlocks) {
2176 final int blockIndex = blockIndices[i];
2177 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX) {
Chris Craik956f3402015-04-27 16:41:00 -07002178 mTextRenderNodes[blockIndex].isDirty = true;
Gilles Debunneebc86af2012-04-20 15:10:47 -07002179 }
2180 if (blockEndLines[i] >= lastLine) break;
2181 i++;
2182 }
2183 }
2184 }
2185
Mathew Inwood978c6e22018-08-21 15:58:55 +01002186 @UnsupportedAppUsage
Gilles Debunned88876a2012-03-16 17:34:04 -07002187 void invalidateTextDisplayList() {
Chris Craik956f3402015-04-27 16:41:00 -07002188 if (mTextRenderNodes != null) {
2189 for (int i = 0; i < mTextRenderNodes.length; i++) {
2190 if (mTextRenderNodes[i] != null) mTextRenderNodes[i].isDirty = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07002191 }
2192 }
2193 }
2194
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002195 void updateCursorPosition() {
Mihai Popa6c7ad1d2018-12-04 15:45:00 +00002196 loadCursorDrawable();
2197 if (mDrawableForCursor == null) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002198 return;
2199 }
2200
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002201 final Layout layout = mTextView.getLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -07002202 final int offset = mTextView.getSelectionStart();
2203 final int line = layout.getLineForOffset(offset);
2204 final int top = layout.getLineTop(line);
Siyamed Sinira60b59d2017-07-26 09:26:41 -07002205 final int bottom = layout.getLineBottomWithoutSpacing(line);
Gilles Debunned88876a2012-03-16 17:34:04 -07002206
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002207 final boolean clamped = layout.shouldClampCursor(line);
2208 updateCursorPosition(top, bottom, layout.getPrimaryHorizontal(offset, clamped));
Gilles Debunned88876a2012-03-16 17:34:04 -07002209 }
2210
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002211 void refreshTextActionMode() {
2212 if (extractedTextModeWillBeStarted()) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002213 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002214 return;
2215 }
2216 final boolean hasSelection = mTextView.hasSelection();
2217 final SelectionModifierCursorController selectionController = getSelectionController();
2218 final InsertionPointCursorController insertionController = getInsertionController();
2219 if ((selectionController != null && selectionController.isCursorBeingModified())
2220 || (insertionController != null && insertionController.isCursorBeingModified())) {
2221 // ActionMode should be managed by the currently active cursor controller.
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002222 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002223 return;
2224 }
2225 if (hasSelection) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002226 hideInsertionPointCursorController();
2227 if (mTextActionMode == null) {
Keisuke Kuroyanagi0fd28c92016-04-04 17:43:06 +09002228 if (mRestartActionModeOnNextRefresh) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002229 // To avoid distraction, newly start action mode only when selection action
Keisuke Kuroyanagi0fd28c92016-04-04 17:43:06 +09002230 // mode is being restarted.
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002231 startSelectionActionModeAsync(false);
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002232 }
2233 } else if (selectionController == null || !selectionController.isActive()) {
2234 // Insertion action mode is active. Avoid dismissing the selection.
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002235 stopTextActionModeWithPreservingSelection();
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002236 startSelectionActionModeAsync(false);
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002237 } else {
2238 mTextActionMode.invalidateContentRect();
2239 }
2240 } else {
2241 // Insertion action mode is started only when insertion controller is explicitly
2242 // activated.
2243 if (insertionController == null || !insertionController.isActive()) {
2244 stopTextActionMode();
2245 } else if (mTextActionMode != null) {
2246 mTextActionMode.invalidateContentRect();
2247 }
2248 }
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002249 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002250 }
2251
Gilles Debunned88876a2012-03-16 17:34:04 -07002252 /**
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002253 * Start an Insertion action mode.
Gilles Debunned88876a2012-03-16 17:34:04 -07002254 */
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002255 void startInsertionActionMode() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002256 if (mInsertionActionModeRunnable != null) {
2257 mTextView.removeCallbacks(mInsertionActionModeRunnable);
2258 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002259 if (extractedTextModeWillBeStarted()) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002260 return;
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002261 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002262 stopTextActionMode();
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002263
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002264 ActionMode.Callback actionModeCallback =
Richard Ledley26b87222017-11-30 10:54:08 +00002265 new TextActionModeCallback(TextActionMode.INSERTION);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002266 mTextActionMode = mTextView.startActionMode(
Clara Bayarrib8ed5b72015-04-09 15:26:41 +01002267 actionModeCallback, ActionMode.TYPE_FLOATING);
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002268 if (mTextActionMode != null && getInsertionController() != null) {
2269 getInsertionController().show();
2270 }
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00002271 }
2272
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002273 @NonNull
2274 TextView getTextView() {
2275 return mTextView;
2276 }
2277
2278 @Nullable
2279 ActionMode getTextActionMode() {
2280 return mTextActionMode;
2281 }
2282
2283 void setRestartActionModeOnNextRefresh(boolean value) {
2284 mRestartActionModeOnNextRefresh = value;
2285 }
2286
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00002287 /**
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002288 * Asynchronously starts a selection action mode using the TextClassifier.
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00002289 */
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002290 void startSelectionActionModeAsync(boolean adjustSelection) {
Richard Ledley26b87222017-11-30 10:54:08 +00002291 getSelectionActionModeHelper().startSelectionActionModeAsync(adjustSelection);
2292 }
2293
Richard Ledley27db81b2018-03-01 12:34:55 +00002294 void startLinkActionModeAsync(int start, int end) {
Richard Ledley26b87222017-11-30 10:54:08 +00002295 if (!(mTextView.getText() instanceof Spannable)) {
2296 return;
2297 }
Richard Ledley26b87222017-11-30 10:54:08 +00002298 stopTextActionMode();
Abodunrinwa Toki52096912018-03-21 23:14:42 +00002299 mRequestingLinkActionMode = true;
Richard Ledley27db81b2018-03-01 12:34:55 +00002300 getSelectionActionModeHelper().startLinkActionModeAsync(start, end);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002301 }
2302
2303 /**
2304 * Asynchronously invalidates an action mode using the TextClassifier.
2305 */
Abodunrinwa Toki4ce651e2017-05-12 15:37:29 +01002306 void invalidateActionModeAsync() {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002307 getSelectionActionModeHelper().invalidateActionModeAsync();
2308 }
2309
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002310 /**
2311 * Synchronously invalidates an action mode without the TextClassifier.
2312 */
2313 private void invalidateActionMode() {
2314 if (mTextActionMode != null) {
2315 mTextActionMode.invalidate();
2316 }
2317 }
2318
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002319 private SelectionActionModeHelper getSelectionActionModeHelper() {
2320 if (mSelectionActionModeHelper == null) {
2321 mSelectionActionModeHelper = new SelectionActionModeHelper(this);
Clara Bayarri578286f2015-04-10 15:35:31 +01002322 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002323 return mSelectionActionModeHelper;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00002324 }
2325
Clara Bayarridfac4432015-05-15 12:18:24 +01002326 /**
2327 * If the TextView allows text selection, selects the current word when no existing selection
2328 * was available and starts a drag.
2329 *
2330 * @return true if the drag was started.
2331 */
2332 private boolean selectCurrentWordAndStartDrag() {
Clara Bayarri7184c8a2015-06-05 17:34:09 +01002333 if (mInsertionActionModeRunnable != null) {
2334 mTextView.removeCallbacks(mInsertionActionModeRunnable);
2335 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002336 if (extractedTextModeWillBeStarted()) {
Clara Bayarridfac4432015-05-15 12:18:24 +01002337 return false;
2338 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002339 if (!checkField()) {
Clara Bayarridfac4432015-05-15 12:18:24 +01002340 return false;
2341 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002342 if (!mTextView.hasSelection() && !selectCurrentWord()) {
2343 // No selection and cannot select a word.
2344 return false;
2345 }
2346 stopTextActionModeWithPreservingSelection();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08002347 getSelectionController().enterDrag(
2348 SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_WORD);
Clara Bayarridfac4432015-05-15 12:18:24 +01002349 return true;
2350 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002351
Clara Bayarridfac4432015-05-15 12:18:24 +01002352 /**
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002353 * Checks whether a selection can be performed on the current TextView.
Clara Bayarridfac4432015-05-15 12:18:24 +01002354 *
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002355 * @return true if a selection can be performed
Clara Bayarridfac4432015-05-15 12:18:24 +01002356 */
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002357 boolean checkField() {
Clara Bayarridfac4432015-05-15 12:18:24 +01002358 if (!mTextView.canSelectText() || !mTextView.requestFocus()) {
2359 Log.w(TextView.LOG_TAG,
2360 "TextView does not support text selection. Selection cancelled.");
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002361 return false;
2362 }
Clara Bayarridfac4432015-05-15 12:18:24 +01002363 return true;
2364 }
2365
Richard Ledley26b87222017-11-30 10:54:08 +00002366 boolean startActionModeInternal(@TextActionMode int actionMode) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002367 if (extractedTextModeWillBeStarted()) {
2368 return false;
2369 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002370 if (mTextActionMode != null) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002371 // Text action mode is already started
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002372 invalidateActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07002373 return false;
2374 }
2375
Richard Ledley724eff92017-12-21 10:11:34 +00002376 if (actionMode != TextActionMode.TEXT_LINK
2377 && (!checkField() || !mTextView.hasSelection())) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002378 return false;
2379 }
2380
Richard Ledley26b87222017-11-30 10:54:08 +00002381 ActionMode.Callback actionModeCallback = new TextActionModeCallback(actionMode);
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002382 mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING);
Gilles Debunned88876a2012-03-16 17:34:04 -07002383
Abodunrinwa Toki29cb7682018-04-11 21:24:20 +01002384 final boolean selectableText = mTextView.isTextEditable() || mTextView.isTextSelectable();
2385 if (actionMode == TextActionMode.TEXT_LINK && !selectableText
2386 && mTextActionMode instanceof FloatingActionMode) {
2387 // Make the toolbar outside-touchable so that it can be dismissed when the user clicks
2388 // outside of it.
2389 ((FloatingActionMode) mTextActionMode).setOutsideTouchable(true,
2390 () -> stopTextActionMode());
2391 }
2392
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002393 final boolean selectionStarted = mTextActionMode != null;
Abodunrinwa Toki52096912018-03-21 23:14:42 +00002394 if (selectionStarted
2395 && mTextView.isTextEditable() && !mTextView.isTextSelectable()
2396 && mShowSoftInputOnFocus) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002397 // Show the IME to be able to replace text, except when selecting non editable text.
Yohei Yukawa484d4af2018-09-17 16:47:08 -07002398 final InputMethodManager imm = getInputMethodManager();
Gilles Debunned88876a2012-03-16 17:34:04 -07002399 if (imm != null) {
2400 imm.showSoftInput(mTextView, 0, null);
2401 }
2402 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002403 return selectionStarted;
2404 }
2405
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09002406 private boolean extractedTextModeWillBeStarted() {
Andrei Stingaceanub1891b32015-06-19 16:44:37 +01002407 if (!(mTextView.isInExtractedMode())) {
Yohei Yukawa484d4af2018-09-17 16:47:08 -07002408 final InputMethodManager imm = getInputMethodManager();
Gilles Debunned88876a2012-03-16 17:34:04 -07002409 return imm != null && imm.isFullscreenMode();
2410 }
2411 return false;
2412 }
2413
2414 /**
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002415 * @return <code>true</code> if it's reasonable to offer to show suggestions depending on
2416 * the current cursor position or selection range. This method is consistent with the
2417 * method to show suggestions {@link SuggestionsPopupWindow#updateSuggestions}.
Gilles Debunned88876a2012-03-16 17:34:04 -07002418 */
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002419 private boolean shouldOfferToShowSuggestions() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002420 CharSequence text = mTextView.getText();
2421 if (!(text instanceof Spannable)) return false;
2422
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002423 final Spannable spannable = (Spannable) text;
2424 final int selectionStart = mTextView.getSelectionStart();
2425 final int selectionEnd = mTextView.getSelectionEnd();
2426 final SuggestionSpan[] suggestionSpans = spannable.getSpans(selectionStart, selectionEnd,
2427 SuggestionSpan.class);
2428 if (suggestionSpans.length == 0) {
2429 return false;
2430 }
2431 if (selectionStart == selectionEnd) {
2432 // Spans overlap the cursor.
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002433 for (int i = 0; i < suggestionSpans.length; i++) {
2434 if (suggestionSpans[i].getSuggestions().length > 0) {
2435 return true;
2436 }
2437 }
2438 return false;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002439 }
2440 int minSpanStart = mTextView.getText().length();
2441 int maxSpanEnd = 0;
2442 int unionOfSpansCoveringSelectionStartStart = mTextView.getText().length();
2443 int unionOfSpansCoveringSelectionStartEnd = 0;
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002444 boolean hasValidSuggestions = false;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002445 for (int i = 0; i < suggestionSpans.length; i++) {
2446 final int spanStart = spannable.getSpanStart(suggestionSpans[i]);
2447 final int spanEnd = spannable.getSpanEnd(suggestionSpans[i]);
2448 minSpanStart = Math.min(minSpanStart, spanStart);
2449 maxSpanEnd = Math.max(maxSpanEnd, spanEnd);
2450 if (selectionStart < spanStart || selectionStart > spanEnd) {
2451 // The span doesn't cover the current selection start point.
2452 continue;
2453 }
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002454 hasValidSuggestions =
2455 hasValidSuggestions || suggestionSpans[i].getSuggestions().length > 0;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002456 unionOfSpansCoveringSelectionStartStart =
2457 Math.min(unionOfSpansCoveringSelectionStartStart, spanStart);
2458 unionOfSpansCoveringSelectionStartEnd =
2459 Math.max(unionOfSpansCoveringSelectionStartEnd, spanEnd);
2460 }
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002461 if (!hasValidSuggestions) {
2462 return false;
2463 }
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002464 if (unionOfSpansCoveringSelectionStartStart >= unionOfSpansCoveringSelectionStartEnd) {
2465 // No spans cover the selection start point.
2466 return false;
2467 }
2468 if (minSpanStart < unionOfSpansCoveringSelectionStartStart
2469 || maxSpanEnd > unionOfSpansCoveringSelectionStartEnd) {
2470 // There is a span that is not covered by the union. In this case, we soouldn't offer
2471 // to show suggestions as it's confusing.
2472 return false;
2473 }
2474 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07002475 }
2476
2477 /**
2478 * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
2479 * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
2480 */
2481 private boolean isCursorInsideEasyCorrectionSpan() {
2482 Spannable spannable = (Spannable) mTextView.getText();
2483 SuggestionSpan[] suggestionSpans = spannable.getSpans(mTextView.getSelectionStart(),
2484 mTextView.getSelectionEnd(), SuggestionSpan.class);
2485 for (int i = 0; i < suggestionSpans.length; i++) {
2486 if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
2487 return true;
2488 }
2489 }
2490 return false;
2491 }
2492
2493 void onTouchUpEvent(MotionEvent event) {
Nikita Dubrovsky05cfcc82019-10-24 08:57:32 -07002494 if (TextView.DEBUG_CURSOR) {
2495 logCursor("onTouchUpEvent", null);
2496 }
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +01002497 if (getSelectionActionModeHelper().resetSelection(
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +00002498 getTextView().getOffsetForPosition(event.getX(), event.getY()))) {
2499 return;
2500 }
2501
Gilles Debunned88876a2012-03-16 17:34:04 -07002502 boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect();
Mady Mellora2861452015-06-25 08:40:27 -07002503 hideCursorAndSpanControllers();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002504 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07002505 CharSequence text = mTextView.getText();
2506 if (!selectAllGotFocus && text.length() > 0) {
2507 // Move cursor
2508 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
Abodunrinwa Toki52096912018-03-21 23:14:42 +00002509
2510 final boolean shouldInsertCursor = !mRequestingLinkActionMode;
2511 if (shouldInsertCursor) {
2512 Selection.setSelection((Spannable) text, offset);
2513 if (mSpellChecker != null) {
2514 // When the cursor moves, the word that was typed may need spell check
2515 mSpellChecker.onSelectionChanged();
2516 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002517 }
Andrei Stingaceanu35c550c2015-05-07 16:49:49 +01002518
Gilles Debunned88876a2012-03-16 17:34:04 -07002519 if (!extractedTextModeWillBeStarted()) {
2520 if (isCursorInsideEasyCorrectionSpan()) {
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01002521 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002522 if (mInsertionActionModeRunnable != null) {
2523 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01002524 }
2525
Abodunrinwa Toki52096912018-03-21 23:14:42 +00002526 mShowSuggestionRunnable = this::replace;
2527
Gilles Debunned88876a2012-03-16 17:34:04 -07002528 // removeCallbacks is performed on every touch
2529 mTextView.postDelayed(mShowSuggestionRunnable,
2530 ViewConfiguration.getDoubleTapTimeout());
2531 } else if (hasInsertionController()) {
Abodunrinwa Toki52096912018-03-21 23:14:42 +00002532 if (shouldInsertCursor) {
2533 getInsertionController().show();
2534 } else {
2535 getInsertionController().hide();
2536 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002537 }
2538 }
2539 }
2540 }
2541
Yohei Yukawa401e3d42019-01-19 11:49:37 -08002542 /**
2543 * Called when {@link TextView#mTextOperationUser} has changed.
2544 *
2545 * <p>Any user-specific resources need to be refreshed here.</p>
2546 */
2547 final void onTextOperationUserChanged() {
2548 if (mSpellChecker != null) {
2549 mSpellChecker.resetSession();
2550 }
2551 }
2552
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002553 protected void stopTextActionMode() {
2554 if (mTextActionMode != null) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002555 // This will hide the mSelectionModifierCursorController
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002556 mTextActionMode.finish();
Gilles Debunned88876a2012-03-16 17:34:04 -07002557 }
2558 }
2559
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002560 private void stopTextActionModeWithPreservingSelection() {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002561 if (mTextActionMode != null) {
2562 mRestartActionModeOnNextRefresh = true;
2563 }
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002564 mPreserveSelection = true;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002565 stopTextActionMode();
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002566 mPreserveSelection = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002567 }
2568
Gilles Debunned88876a2012-03-16 17:34:04 -07002569 /**
2570 * @return True if this view supports insertion handles.
2571 */
2572 boolean hasInsertionController() {
2573 return mInsertionControllerEnabled;
2574 }
2575
2576 /**
2577 * @return True if this view supports selection handles.
2578 */
2579 boolean hasSelectionController() {
2580 return mSelectionControllerEnabled;
2581 }
2582
Nikita Dubrovsky21c6a962019-12-27 08:48:02 -08002583 /** Returns the controller for the insertion cursor. */
2584 @VisibleForTesting
2585 public @Nullable InsertionPointCursorController getInsertionController() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002586 if (!mInsertionControllerEnabled) {
2587 return null;
2588 }
2589
2590 if (mInsertionPointCursorController == null) {
2591 mInsertionPointCursorController = new InsertionPointCursorController();
2592
2593 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
2594 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
2595 }
2596
2597 return mInsertionPointCursorController;
2598 }
2599
Nikita Dubrovsky21c6a962019-12-27 08:48:02 -08002600 /** Returns the controller for selection. */
2601 @VisibleForTesting
2602 public @Nullable SelectionModifierCursorController getSelectionController() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002603 if (!mSelectionControllerEnabled) {
2604 return null;
2605 }
2606
2607 if (mSelectionModifierCursorController == null) {
2608 mSelectionModifierCursorController = new SelectionModifierCursorController();
2609
2610 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
2611 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
2612 }
2613
2614 return mSelectionModifierCursorController;
2615 }
2616
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002617 @VisibleForTesting
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002618 @Nullable
2619 public Drawable getCursorDrawable() {
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07002620 return mDrawableForCursor;
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002621 }
2622
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002623 private void updateCursorPosition(int top, int bottom, float horizontal) {
Mihai Popa6c7ad1d2018-12-04 15:45:00 +00002624 loadCursorDrawable();
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07002625 final int left = clampHorizontalPosition(mDrawableForCursor, horizontal);
2626 final int width = mDrawableForCursor.getIntrinsicWidth();
Nikita Dubrovsky05cfcc82019-10-24 08:57:32 -07002627 if (TextView.DEBUG_CURSOR) {
2628 logCursor("updateCursorPosition", "left=%s, top=%s", left, (top - mTempRect.top));
2629 }
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07002630 mDrawableForCursor.setBounds(left, top - mTempRect.top, left + width,
Gilles Debunned88876a2012-03-16 17:34:04 -07002631 bottom + mTempRect.bottom);
2632 }
2633
2634 /**
Siyamed Sinir987ec652016-02-17 19:44:41 -08002635 * Return clamped position for the drawable. If the drawable is within the boundaries of the
2636 * 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 -08002637 * 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 -08002638 * the view boundary. If the drawable is null, horizontal parameter is aligned to left or right
2639 * of the view.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002640 *
Siyamed Sinir987ec652016-02-17 19:44:41 -08002641 * @param drawable Drawable. Can be null.
2642 * @param horizontal Horizontal position for the drawable.
2643 * @return The clamped horizontal position for the drawable.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002644 */
Siyamed Sinir987ec652016-02-17 19:44:41 -08002645 private int clampHorizontalPosition(@Nullable final Drawable drawable, float horizontal) {
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002646 horizontal = Math.max(0.5f, horizontal - 0.5f);
2647 if (mTempRect == null) mTempRect = new Rect();
Siyamed Sinir987ec652016-02-17 19:44:41 -08002648
2649 int drawableWidth = 0;
2650 if (drawable != null) {
2651 drawable.getPadding(mTempRect);
2652 drawableWidth = drawable.getIntrinsicWidth();
2653 } else {
2654 mTempRect.setEmpty();
2655 }
2656
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002657 int scrollX = mTextView.getScrollX();
2658 float horizontalDiff = horizontal - scrollX;
2659 int viewClippedWidth = mTextView.getWidth() - mTextView.getCompoundPaddingLeft()
2660 - mTextView.getCompoundPaddingRight();
2661
2662 final int left;
2663 if (horizontalDiff >= (viewClippedWidth - 1f)) {
2664 // at the rightmost position
Siyamed Sinir987ec652016-02-17 19:44:41 -08002665 left = viewClippedWidth + scrollX - (drawableWidth - mTempRect.right);
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002666 } else if (Math.abs(horizontalDiff) <= 1f
2667 || (TextUtils.isEmpty(mTextView.getText())
Siyamed Sinir987ec652016-02-17 19:44:41 -08002668 && (TextView.VERY_WIDE - scrollX) <= (viewClippedWidth + 1f)
2669 && horizontal <= 1f)) {
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002670 // at the leftmost position
2671 left = scrollX - mTempRect.left;
2672 } else {
2673 left = (int) horizontal - mTempRect.left;
2674 }
2675 return left;
2676 }
2677
2678 /**
Gilles Debunned88876a2012-03-16 17:34:04 -07002679 * 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 -08002680 * a dictionary) from the current input method, provided by it calling
Gilles Debunned88876a2012-03-16 17:34:04 -07002681 * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
2682 * implementation flashes the background of the corrected word to provide feedback to the user.
2683 *
2684 * @param info The auto correct info about the text that was corrected.
2685 */
2686 public void onCommitCorrection(CorrectionInfo info) {
2687 if (mCorrectionHighlighter == null) {
2688 mCorrectionHighlighter = new CorrectionHighlighter();
2689 } else {
2690 mCorrectionHighlighter.invalidate(false);
2691 }
2692
2693 mCorrectionHighlighter.highlight(info);
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002694 mUndoInputFilter.freezeLastEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07002695 }
2696
Gilles Debunned88876a2012-03-16 17:34:04 -07002697 void onScrollChanged() {
Gilles Debunne157aafc2012-04-19 17:21:57 -07002698 if (mPositionListener != null) {
2699 mPositionListener.onScrollChanged();
2700 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002701 if (mTextActionMode != null) {
2702 mTextActionMode.invalidateContentRect();
Abodunrinwa Toki56195db2015-04-22 06:46:54 +01002703 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002704 }
2705
2706 /**
2707 * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
2708 */
2709 private boolean shouldBlink() {
Long Ling0c27fbb2019-11-19 18:41:21 +00002710 if (!isCursorVisible() || !mTextView.isFocused()) return false;
Gilles Debunned88876a2012-03-16 17:34:04 -07002711
2712 final int start = mTextView.getSelectionStart();
2713 if (start < 0) return false;
2714
2715 final int end = mTextView.getSelectionEnd();
2716 if (end < 0) return false;
2717
2718 return start == end;
2719 }
2720
2721 void makeBlink() {
2722 if (shouldBlink()) {
2723 mShowCursor = SystemClock.uptimeMillis();
2724 if (mBlink == null) mBlink = new Blink();
John Reckd0374c62015-10-20 13:25:01 -07002725 mTextView.removeCallbacks(mBlink);
2726 mTextView.postDelayed(mBlink, BLINK);
Gilles Debunned88876a2012-03-16 17:34:04 -07002727 } else {
John Reckd0374c62015-10-20 13:25:01 -07002728 if (mBlink != null) mTextView.removeCallbacks(mBlink);
Gilles Debunned88876a2012-03-16 17:34:04 -07002729 }
2730 }
2731
John Reckd0374c62015-10-20 13:25:01 -07002732 private class Blink implements Runnable {
Gilles Debunned88876a2012-03-16 17:34:04 -07002733 private boolean mCancelled;
2734
2735 public void run() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002736 if (mCancelled) {
2737 return;
2738 }
2739
John Reckd0374c62015-10-20 13:25:01 -07002740 mTextView.removeCallbacks(this);
Gilles Debunned88876a2012-03-16 17:34:04 -07002741
2742 if (shouldBlink()) {
2743 if (mTextView.getLayout() != null) {
2744 mTextView.invalidateCursorPath();
2745 }
2746
John Reckd0374c62015-10-20 13:25:01 -07002747 mTextView.postDelayed(this, BLINK);
Gilles Debunned88876a2012-03-16 17:34:04 -07002748 }
2749 }
2750
2751 void cancel() {
2752 if (!mCancelled) {
John Reckd0374c62015-10-20 13:25:01 -07002753 mTextView.removeCallbacks(this);
Gilles Debunned88876a2012-03-16 17:34:04 -07002754 mCancelled = true;
2755 }
2756 }
2757
2758 void uncancel() {
2759 mCancelled = false;
2760 }
2761 }
2762
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002763 private DragShadowBuilder getTextThumbnailBuilder(int start, int end) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002764 TextView shadowView = (TextView) View.inflate(mTextView.getContext(),
2765 com.android.internal.R.layout.text_drag_thumbnail, null);
2766
2767 if (shadowView == null) {
2768 throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
2769 }
2770
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002771 if (end - start > DRAG_SHADOW_MAX_TEXT_LENGTH) {
2772 final long range = getCharClusterRange(start + DRAG_SHADOW_MAX_TEXT_LENGTH);
2773 end = TextUtils.unpackRangeEndFromLong(range);
Gilles Debunned88876a2012-03-16 17:34:04 -07002774 }
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002775 final CharSequence text = mTextView.getTransformedText(start, end);
Gilles Debunned88876a2012-03-16 17:34:04 -07002776 shadowView.setText(text);
2777 shadowView.setTextColor(mTextView.getTextColors());
2778
Alan Viverettebb98ebd2015-05-08 17:17:44 -07002779 shadowView.setTextAppearance(R.styleable.Theme_textAppearanceLarge);
Gilles Debunned88876a2012-03-16 17:34:04 -07002780 shadowView.setGravity(Gravity.CENTER);
2781
2782 shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
2783 ViewGroup.LayoutParams.WRAP_CONTENT));
2784
2785 final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
2786 shadowView.measure(size, size);
2787
2788 shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
2789 shadowView.invalidate();
2790 return new DragShadowBuilder(shadowView);
2791 }
2792
2793 private static class DragLocalState {
2794 public TextView sourceTextView;
2795 public int start, end;
2796
2797 public DragLocalState(TextView sourceTextView, int start, int end) {
2798 this.sourceTextView = sourceTextView;
2799 this.start = start;
2800 this.end = end;
2801 }
2802 }
2803
2804 void onDrop(DragEvent event) {
Ben Murdoch3dac4602017-01-17 11:27:37 +00002805 SpannableStringBuilder content = new SpannableStringBuilder();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002806
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -07002807 final DragAndDropPermissions permissions = DragAndDropPermissions.obtain(event);
2808 if (permissions != null) {
2809 permissions.takeTransient();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002810 }
2811
2812 try {
2813 ClipData clipData = event.getClipData();
2814 final int itemCount = clipData.getItemCount();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002815 for (int i = 0; i < itemCount; i++) {
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002816 Item item = clipData.getItemAt(i);
2817 content.append(item.coerceToStyledText(mTextView.getContext()));
2818 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002819 } finally {
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -07002820 if (permissions != null) {
2821 permissions.release();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002822 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002823 }
2824
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002825 mTextView.beginBatchEdit();
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002826 mUndoInputFilter.freezeLastEdit();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002827 try {
2828 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
2829 Object localState = event.getLocalState();
2830 DragLocalState dragLocalState = null;
2831 if (localState instanceof DragLocalState) {
2832 dragLocalState = (DragLocalState) localState;
Gilles Debunned88876a2012-03-16 17:34:04 -07002833 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002834 boolean dragDropIntoItself = dragLocalState != null
2835 && dragLocalState.sourceTextView == mTextView;
Gilles Debunned88876a2012-03-16 17:34:04 -07002836
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002837 if (dragDropIntoItself) {
2838 if (offset >= dragLocalState.start && offset < dragLocalState.end) {
2839 // A drop inside the original selection discards the drop.
2840 return;
2841 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002842 }
2843
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002844 final int originalLength = mTextView.getText().length();
2845 int min = offset;
2846 int max = offset;
2847
2848 Selection.setSelection((Spannable) mTextView.getText(), max);
2849 mTextView.replaceText_internal(min, max, content);
2850
2851 if (dragDropIntoItself) {
2852 int dragSourceStart = dragLocalState.start;
2853 int dragSourceEnd = dragLocalState.end;
2854 if (max <= dragSourceStart) {
2855 // Inserting text before selection has shifted positions
2856 final int shift = mTextView.getText().length() - originalLength;
2857 dragSourceStart += shift;
2858 dragSourceEnd += shift;
2859 }
2860
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08002861 // Delete original selection
2862 mTextView.deleteText_internal(dragSourceStart, dragSourceEnd);
Gilles Debunned88876a2012-03-16 17:34:04 -07002863
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08002864 // Make sure we do not leave two adjacent spaces.
2865 final int prevCharIdx = Math.max(0, dragSourceStart - 1);
2866 final int nextCharIdx = Math.min(mTextView.getText().length(), dragSourceStart + 1);
2867 if (nextCharIdx > prevCharIdx + 1) {
2868 CharSequence t = mTextView.getTransformedText(prevCharIdx, nextCharIdx);
2869 if (Character.isSpaceChar(t.charAt(0)) && Character.isSpaceChar(t.charAt(1))) {
2870 mTextView.deleteText_internal(prevCharIdx, prevCharIdx + 1);
2871 }
Victoria Lease91373202012-09-07 16:41:59 -07002872 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002873 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002874 } finally {
2875 mTextView.endBatchEdit();
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002876 mUndoInputFilter.freezeLastEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07002877 }
2878 }
2879
Gilles Debunnec62589c2012-04-12 14:50:23 -07002880 public void addSpanWatchers(Spannable text) {
2881 final int textLength = text.length();
2882
2883 if (mKeyListener != null) {
2884 text.setSpan(mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
2885 }
2886
Jean Chalardbaf30942013-02-28 16:01:51 -08002887 if (mSpanController == null) {
2888 mSpanController = new SpanController();
Gilles Debunnec62589c2012-04-12 14:50:23 -07002889 }
Jean Chalardbaf30942013-02-28 16:01:51 -08002890 text.setSpan(mSpanController, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002891 }
2892
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002893 void setContextMenuAnchor(float x, float y) {
2894 mContextMenuAnchorX = x;
2895 mContextMenuAnchorY = y;
2896 }
2897
2898 void onCreateContextMenu(ContextMenu menu) {
2899 if (mIsBeingLongClicked || Float.isNaN(mContextMenuAnchorX)
2900 || Float.isNaN(mContextMenuAnchorY)) {
2901 return;
2902 }
2903 final int offset = mTextView.getOffsetForPosition(mContextMenuAnchorX, mContextMenuAnchorY);
2904 if (offset == -1) {
2905 return;
2906 }
Siyamed Sinir532f3c92017-06-15 18:22:31 -07002907
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002908 stopTextActionModeWithPreservingSelection();
Siyamed Sinir532f3c92017-06-15 18:22:31 -07002909 if (mTextView.canSelectText()) {
2910 final boolean isOnSelection = mTextView.hasSelection()
2911 && offset >= mTextView.getSelectionStart()
2912 && offset <= mTextView.getSelectionEnd();
2913 if (!isOnSelection) {
2914 // Right clicked position is not on the selection. Remove the selection and move the
2915 // cursor to the right clicked position.
2916 Selection.setSelection((Spannable) mTextView.getText(), offset);
2917 stopTextActionMode();
2918 }
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002919 }
2920
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002921 if (shouldOfferToShowSuggestions()) {
Keisuke Kuroyanagi182f5fe2016-03-11 16:31:29 +09002922 final SuggestionInfo[] suggestionInfoArray =
2923 new SuggestionInfo[SuggestionSpan.SUGGESTIONS_MAX_SIZE];
2924 for (int i = 0; i < suggestionInfoArray.length; i++) {
2925 suggestionInfoArray[i] = new SuggestionInfo();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002926 }
2927 final SubMenu subMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, MENU_ITEM_ORDER_REPLACE,
2928 com.android.internal.R.string.replace);
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002929 final int numItems = mSuggestionHelper.getSuggestionInfo(suggestionInfoArray, null);
Keisuke Kuroyanagi182f5fe2016-03-11 16:31:29 +09002930 for (int i = 0; i < numItems; i++) {
2931 final SuggestionInfo info = suggestionInfoArray[i];
2932 subMenu.add(Menu.NONE, Menu.NONE, i, info.mText)
2933 .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
2934 @Override
2935 public boolean onMenuItemClick(MenuItem item) {
2936 replaceWithSuggestion(info);
2937 return true;
2938 }
2939 });
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002940 }
2941 }
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002942
2943 menu.add(Menu.NONE, TextView.ID_UNDO, MENU_ITEM_ORDER_UNDO,
2944 com.android.internal.R.string.undo)
2945 .setAlphabeticShortcut('z')
2946 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2947 .setEnabled(mTextView.canUndo());
2948 menu.add(Menu.NONE, TextView.ID_REDO, MENU_ITEM_ORDER_REDO,
2949 com.android.internal.R.string.redo)
2950 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2951 .setEnabled(mTextView.canRedo());
2952
2953 menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
2954 com.android.internal.R.string.cut)
2955 .setAlphabeticShortcut('x')
2956 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2957 .setEnabled(mTextView.canCut());
2958 menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
2959 com.android.internal.R.string.copy)
2960 .setAlphabeticShortcut('c')
2961 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2962 .setEnabled(mTextView.canCopy());
2963 menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
2964 com.android.internal.R.string.paste)
2965 .setAlphabeticShortcut('v')
2966 .setEnabled(mTextView.canPaste())
2967 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01002968 menu.add(Menu.NONE, TextView.ID_PASTE_AS_PLAIN_TEXT, MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002969 com.android.internal.R.string.paste_as_plain_text)
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01002970 .setEnabled(mTextView.canPasteAsPlainText())
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002971 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2972 menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
2973 com.android.internal.R.string.share)
2974 .setEnabled(mTextView.canShare())
2975 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2976 menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
2977 com.android.internal.R.string.selectAll)
2978 .setAlphabeticShortcut('a')
2979 .setEnabled(mTextView.canSelectAllText())
2980 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Felipe Leme2ac463e2017-03-13 14:06:25 -07002981 menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
Felipe Leme555bcac2017-06-26 12:53:56 -07002982 android.R.string.autofill)
Felipe Leme2ac463e2017-03-13 14:06:25 -07002983 .setEnabled(mTextView.canRequestAutofill())
2984 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002985
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002986 mPreserveSelection = true;
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002987 }
2988
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002989 @Nullable
2990 private SuggestionSpan findEquivalentSuggestionSpan(
2991 @NonNull SuggestionSpanInfo suggestionSpanInfo) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002992 final Editable editable = (Editable) mTextView.getText();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002993 if (editable.getSpanStart(suggestionSpanInfo.mSuggestionSpan) >= 0) {
2994 // Exactly same span is found.
2995 return suggestionSpanInfo.mSuggestionSpan;
2996 }
2997 // Suggestion span couldn't be found. Try to find a suggestion span that has the same
2998 // contents.
2999 final SuggestionSpan[] suggestionSpans = editable.getSpans(suggestionSpanInfo.mSpanStart,
3000 suggestionSpanInfo.mSpanEnd, SuggestionSpan.class);
3001 for (final SuggestionSpan suggestionSpan : suggestionSpans) {
3002 final int start = editable.getSpanStart(suggestionSpan);
3003 if (start != suggestionSpanInfo.mSpanStart) {
3004 continue;
3005 }
3006 final int end = editable.getSpanEnd(suggestionSpan);
3007 if (end != suggestionSpanInfo.mSpanEnd) {
3008 continue;
3009 }
3010 if (suggestionSpan.equals(suggestionSpanInfo.mSuggestionSpan)) {
3011 return suggestionSpan;
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003012 }
3013 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003014 return null;
3015 }
3016
3017 private void replaceWithSuggestion(@NonNull final SuggestionInfo suggestionInfo) {
3018 final SuggestionSpan targetSuggestionSpan = findEquivalentSuggestionSpan(
3019 suggestionInfo.mSuggestionSpanInfo);
3020 if (targetSuggestionSpan == null) {
3021 // Span has been removed
3022 return;
3023 }
3024 final Editable editable = (Editable) mTextView.getText();
3025 final int spanStart = editable.getSpanStart(targetSuggestionSpan);
3026 final int spanEnd = editable.getSpanEnd(targetSuggestionSpan);
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003027 if (spanStart < 0 || spanEnd <= spanStart) {
3028 // Span has been removed
3029 return;
3030 }
3031
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003032 final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
3033 // SuggestionSpans are removed by replace: save them before
3034 SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
3035 SuggestionSpan.class);
3036 final int length = suggestionSpans.length;
3037 int[] suggestionSpansStarts = new int[length];
3038 int[] suggestionSpansEnds = new int[length];
3039 int[] suggestionSpansFlags = new int[length];
3040 for (int i = 0; i < length; i++) {
3041 final SuggestionSpan suggestionSpan = suggestionSpans[i];
3042 suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
3043 suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
3044 suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
3045
3046 // Remove potential misspelled flags
3047 int suggestionSpanFlags = suggestionSpan.getFlags();
3048 if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
3049 suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
3050 suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
3051 suggestionSpan.setFlags(suggestionSpanFlags);
3052 }
3053 }
3054
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003055 // Swap text content between actual text and Suggestion span
3056 final int suggestionStart = suggestionInfo.mSuggestionStart;
3057 final int suggestionEnd = suggestionInfo.mSuggestionEnd;
3058 final String suggestion = suggestionInfo.mText.subSequence(
3059 suggestionStart, suggestionEnd).toString();
3060 mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
3061
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003062 String[] suggestions = targetSuggestionSpan.getSuggestions();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003063 suggestions[suggestionInfo.mSuggestionIndex] = originalText;
3064
3065 // Restore previous SuggestionSpans
3066 final int lengthDelta = suggestion.length() - (spanEnd - spanStart);
3067 for (int i = 0; i < length; i++) {
3068 // Only spans that include the modified region make sense after replacement
3069 // Spans partially included in the replaced region are removed, there is no
3070 // way to assign them a valid range after replacement
3071 if (suggestionSpansStarts[i] <= spanStart && suggestionSpansEnds[i] >= spanEnd) {
3072 mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
3073 suggestionSpansEnds[i] + lengthDelta, suggestionSpansFlags[i]);
3074 }
3075 }
3076 // Move cursor at the end of the replaced word
3077 final int newCursorPosition = spanEnd + lengthDelta;
3078 mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
3079 }
3080
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09003081 private final MenuItem.OnMenuItemClickListener mOnContextMenuItemClickListener =
3082 new MenuItem.OnMenuItemClickListener() {
3083 @Override
3084 public boolean onMenuItemClick(MenuItem item) {
3085 if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
3086 return true;
3087 }
3088 return mTextView.onTextContextMenuItem(item.getItemId());
3089 }
3090 };
3091
Gilles Debunned88876a2012-03-16 17:34:04 -07003092 /**
3093 * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
3094 * pop-up should be displayed.
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07003095 * Also monitors {@link Selection} to call back to the attached input method.
Gilles Debunned88876a2012-03-16 17:34:04 -07003096 */
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09003097 private class SpanController implements SpanWatcher {
Gilles Debunned88876a2012-03-16 17:34:04 -07003098
3099 private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
3100
3101 private EasyEditPopupWindow mPopupWindow;
3102
Gilles Debunned88876a2012-03-16 17:34:04 -07003103 private Runnable mHidePopup;
3104
Jean Chalardbaf30942013-02-28 16:01:51 -08003105 // This function is pure but inner classes can't have static functions
3106 private boolean isNonIntermediateSelectionSpan(final Spannable text,
3107 final Object span) {
3108 return (Selection.SELECTION_START == span || Selection.SELECTION_END == span)
3109 && (text.getSpanFlags(span) & Spanned.SPAN_INTERMEDIATE) == 0;
3110 }
3111
Gilles Debunnec62589c2012-04-12 14:50:23 -07003112 @Override
3113 public void onSpanAdded(Spannable text, Object span, int start, int end) {
Jean Chalardbaf30942013-02-28 16:01:51 -08003114 if (isNonIntermediateSelectionSpan(text, span)) {
3115 sendUpdateSelection();
3116 } else if (span instanceof EasyEditSpan) {
Gilles Debunnec62589c2012-04-12 14:50:23 -07003117 if (mPopupWindow == null) {
3118 mPopupWindow = new EasyEditPopupWindow();
3119 mHidePopup = new Runnable() {
3120 @Override
3121 public void run() {
3122 hide();
3123 }
3124 };
3125 }
3126
3127 // Make sure there is only at most one EasyEditSpan in the text
3128 if (mPopupWindow.mEasyEditSpan != null) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003129 mPopupWindow.mEasyEditSpan.setDeleteEnabled(false);
Gilles Debunnec62589c2012-04-12 14:50:23 -07003130 }
3131
3132 mPopupWindow.setEasyEditSpan((EasyEditSpan) span);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003133 mPopupWindow.setOnDeleteListener(new EasyEditDeleteListener() {
3134 @Override
3135 public void onDeleteClick(EasyEditSpan span) {
3136 Editable editable = (Editable) mTextView.getText();
3137 int start = editable.getSpanStart(span);
3138 int end = editable.getSpanEnd(span);
3139 if (start >= 0 && end >= 0) {
Jean Chalardbaf30942013-02-28 16:01:51 -08003140 sendEasySpanNotification(EasyEditSpan.TEXT_DELETED, span);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003141 mTextView.deleteText_internal(start, end);
3142 }
3143 editable.removeSpan(span);
3144 }
3145 });
Gilles Debunnec62589c2012-04-12 14:50:23 -07003146
3147 if (mTextView.getWindowVisibility() != View.VISIBLE) {
3148 // The window is not visible yet, ignore the text change.
3149 return;
3150 }
3151
3152 if (mTextView.getLayout() == null) {
3153 // The view has not been laid out yet, ignore the text change
3154 return;
3155 }
3156
3157 if (extractedTextModeWillBeStarted()) {
3158 // The input is in extract mode. Do not handle the easy edit in
3159 // the original TextView, as the ExtractEditText will do
3160 return;
3161 }
3162
3163 mPopupWindow.show();
3164 mTextView.removeCallbacks(mHidePopup);
3165 mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
3166 }
3167 }
3168
3169 @Override
3170 public void onSpanRemoved(Spannable text, Object span, int start, int end) {
Jean Chalardbaf30942013-02-28 16:01:51 -08003171 if (isNonIntermediateSelectionSpan(text, span)) {
3172 sendUpdateSelection();
3173 } else if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
Gilles Debunnec62589c2012-04-12 14:50:23 -07003174 hide();
3175 }
3176 }
3177
3178 @Override
3179 public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd,
3180 int newStart, int newEnd) {
Jean Chalardbaf30942013-02-28 16:01:51 -08003181 if (isNonIntermediateSelectionSpan(text, span)) {
3182 sendUpdateSelection();
3183 } else if (mPopupWindow != null && span instanceof EasyEditSpan) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003184 EasyEditSpan easyEditSpan = (EasyEditSpan) span;
Jean Chalardbaf30942013-02-28 16:01:51 -08003185 sendEasySpanNotification(EasyEditSpan.TEXT_MODIFIED, easyEditSpan);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003186 text.removeSpan(easyEditSpan);
Gilles Debunnec62589c2012-04-12 14:50:23 -07003187 }
3188 }
3189
Gilles Debunned88876a2012-03-16 17:34:04 -07003190 public void hide() {
3191 if (mPopupWindow != null) {
3192 mPopupWindow.hide();
3193 mTextView.removeCallbacks(mHidePopup);
3194 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003195 }
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003196
Jean Chalardbaf30942013-02-28 16:01:51 -08003197 private void sendEasySpanNotification(int textChangedType, EasyEditSpan span) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003198 try {
3199 PendingIntent pendingIntent = span.getPendingIntent();
3200 if (pendingIntent != null) {
3201 Intent intent = new Intent();
3202 intent.putExtra(EasyEditSpan.EXTRA_TEXT_CHANGED_TYPE, textChangedType);
3203 pendingIntent.send(mTextView.getContext(), 0, intent);
3204 }
3205 } catch (CanceledException e) {
3206 // This should not happen, as we should try to send the intent only once.
3207 Log.w(TAG, "PendingIntent for notification cannot be sent", e);
3208 }
3209 }
3210 }
3211
3212 /**
3213 * Listens for the delete event triggered by {@link EasyEditPopupWindow}.
3214 */
3215 private interface EasyEditDeleteListener {
3216
3217 /**
3218 * Clicks the delete pop-up.
3219 */
3220 void onDeleteClick(EasyEditSpan span);
Gilles Debunned88876a2012-03-16 17:34:04 -07003221 }
3222
3223 /**
3224 * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07003225 * by {@link SpanController}.
Gilles Debunned88876a2012-03-16 17:34:04 -07003226 */
3227 private class EasyEditPopupWindow extends PinnedPopupWindow
3228 implements OnClickListener {
3229 private static final int POPUP_TEXT_LAYOUT =
3230 com.android.internal.R.layout.text_edit_action_popup_text;
3231 private TextView mDeleteTextView;
3232 private EasyEditSpan mEasyEditSpan;
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003233 private EasyEditDeleteListener mOnDeleteListener;
Gilles Debunned88876a2012-03-16 17:34:04 -07003234
3235 @Override
3236 protected void createPopupWindow() {
3237 mPopupWindow = new PopupWindow(mTextView.getContext(), null,
3238 com.android.internal.R.attr.textSelectHandleWindowStyle);
3239 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
3240 mPopupWindow.setClippingEnabled(true);
3241 }
3242
3243 @Override
3244 protected void initContentView() {
3245 LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
3246 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
3247 mContentView = linearLayout;
3248 mContentView.setBackgroundResource(
3249 com.android.internal.R.drawable.text_edit_side_paste_window);
3250
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003251 LayoutInflater inflater = (LayoutInflater) mTextView.getContext()
3252 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003253
3254 LayoutParams wrapContent = new LayoutParams(
3255 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
3256
3257 mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
3258 mDeleteTextView.setLayoutParams(wrapContent);
3259 mDeleteTextView.setText(com.android.internal.R.string.delete);
3260 mDeleteTextView.setOnClickListener(this);
3261 mContentView.addView(mDeleteTextView);
3262 }
3263
Gilles Debunnec62589c2012-04-12 14:50:23 -07003264 public void setEasyEditSpan(EasyEditSpan easyEditSpan) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003265 mEasyEditSpan = easyEditSpan;
Gilles Debunned88876a2012-03-16 17:34:04 -07003266 }
3267
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003268 private void setOnDeleteListener(EasyEditDeleteListener listener) {
3269 mOnDeleteListener = listener;
3270 }
3271
Gilles Debunned88876a2012-03-16 17:34:04 -07003272 @Override
3273 public void onClick(View view) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003274 if (view == mDeleteTextView
3275 && mEasyEditSpan != null && mEasyEditSpan.isDeleteEnabled()
3276 && mOnDeleteListener != null) {
3277 mOnDeleteListener.onDeleteClick(mEasyEditSpan);
Gilles Debunned88876a2012-03-16 17:34:04 -07003278 }
3279 }
3280
3281 @Override
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003282 public void hide() {
3283 if (mEasyEditSpan != null) {
3284 mEasyEditSpan.setDeleteEnabled(false);
3285 }
3286 mOnDeleteListener = null;
3287 super.hide();
3288 }
3289
3290 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07003291 protected int getTextOffset() {
3292 // Place the pop-up at the end of the span
3293 Editable editable = (Editable) mTextView.getText();
3294 return editable.getSpanEnd(mEasyEditSpan);
3295 }
3296
3297 @Override
3298 protected int getVerticalLocalPosition(int line) {
Siyamed Sinira60b59d2017-07-26 09:26:41 -07003299 final Layout layout = mTextView.getLayout();
3300 return layout.getLineBottomWithoutSpacing(line);
Gilles Debunned88876a2012-03-16 17:34:04 -07003301 }
3302
3303 @Override
3304 protected int clipVertically(int positionY) {
3305 // As we display the pop-up below the span, no vertical clipping is required.
3306 return positionY;
3307 }
3308 }
3309
3310 private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
3311 // 3 handles
3312 // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003313 // 1 CursorAnchorInfoNotifier
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003314 private static final int MAXIMUM_NUMBER_OF_LISTENERS = 7;
Gilles Debunned88876a2012-03-16 17:34:04 -07003315 private TextViewPositionListener[] mPositionListeners =
3316 new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003317 private boolean[] mCanMove = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
Gilles Debunned88876a2012-03-16 17:34:04 -07003318 private boolean mPositionHasChanged = true;
3319 // Absolute position of the TextView with respect to its parent window
3320 private int mPositionX, mPositionY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003321 private int mPositionXOnScreen, mPositionYOnScreen;
Gilles Debunned88876a2012-03-16 17:34:04 -07003322 private int mNumberOfListeners;
3323 private boolean mScrollHasChanged;
3324 final int[] mTempCoords = new int[2];
3325
3326 public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
3327 if (mNumberOfListeners == 0) {
3328 updatePosition();
3329 ViewTreeObserver vto = mTextView.getViewTreeObserver();
3330 vto.addOnPreDrawListener(this);
3331 }
3332
3333 int emptySlotIndex = -1;
3334 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3335 TextViewPositionListener listener = mPositionListeners[i];
3336 if (listener == positionListener) {
3337 return;
3338 } else if (emptySlotIndex < 0 && listener == null) {
3339 emptySlotIndex = i;
3340 }
3341 }
3342
3343 mPositionListeners[emptySlotIndex] = positionListener;
3344 mCanMove[emptySlotIndex] = canMove;
3345 mNumberOfListeners++;
3346 }
3347
3348 public void removeSubscriber(TextViewPositionListener positionListener) {
3349 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3350 if (mPositionListeners[i] == positionListener) {
3351 mPositionListeners[i] = null;
3352 mNumberOfListeners--;
3353 break;
3354 }
3355 }
3356
3357 if (mNumberOfListeners == 0) {
3358 ViewTreeObserver vto = mTextView.getViewTreeObserver();
3359 vto.removeOnPreDrawListener(this);
3360 }
3361 }
3362
3363 public int getPositionX() {
3364 return mPositionX;
3365 }
3366
3367 public int getPositionY() {
3368 return mPositionY;
3369 }
3370
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003371 public int getPositionXOnScreen() {
3372 return mPositionXOnScreen;
3373 }
3374
3375 public int getPositionYOnScreen() {
3376 return mPositionYOnScreen;
3377 }
3378
Gilles Debunned88876a2012-03-16 17:34:04 -07003379 @Override
3380 public boolean onPreDraw() {
3381 updatePosition();
3382
3383 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3384 if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
3385 TextViewPositionListener positionListener = mPositionListeners[i];
3386 if (positionListener != null) {
3387 positionListener.updatePosition(mPositionX, mPositionY,
3388 mPositionHasChanged, mScrollHasChanged);
3389 }
3390 }
3391 }
3392
3393 mScrollHasChanged = false;
3394 return true;
3395 }
3396
3397 private void updatePosition() {
3398 mTextView.getLocationInWindow(mTempCoords);
3399
3400 mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
3401
3402 mPositionX = mTempCoords[0];
3403 mPositionY = mTempCoords[1];
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003404
3405 mTextView.getLocationOnScreen(mTempCoords);
3406
3407 mPositionXOnScreen = mTempCoords[0];
3408 mPositionYOnScreen = mTempCoords[1];
Gilles Debunned88876a2012-03-16 17:34:04 -07003409 }
3410
3411 public void onScrollChanged() {
3412 mScrollHasChanged = true;
3413 }
3414 }
3415
3416 private abstract class PinnedPopupWindow implements TextViewPositionListener {
3417 protected PopupWindow mPopupWindow;
3418 protected ViewGroup mContentView;
3419 int mPositionX, mPositionY;
Seigo Nonaka60490d12016-01-28 17:25:18 +09003420 int mClippingLimitLeft, mClippingLimitRight;
Gilles Debunned88876a2012-03-16 17:34:04 -07003421
3422 protected abstract void createPopupWindow();
3423 protected abstract void initContentView();
3424 protected abstract int getTextOffset();
3425 protected abstract int getVerticalLocalPosition(int line);
3426 protected abstract int clipVertically(int positionY);
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003427 protected void setUp() {
3428 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003429
3430 public PinnedPopupWindow() {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003431 // Due to calling subclass methods in base constructor, subclass constructor is not
3432 // called before subclass methods, e.g. createPopupWindow or initContentView. To give
3433 // a chance to initialize subclasses, call setUp() method here.
3434 // TODO: It is good to extract non trivial initialization code from constructor.
3435 setUp();
3436
Gilles Debunned88876a2012-03-16 17:34:04 -07003437 createPopupWindow();
3438
Alan Viverette80ebe0d2015-04-30 15:53:11 -07003439 mPopupWindow.setWindowLayoutType(
3440 WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
Gilles Debunned88876a2012-03-16 17:34:04 -07003441 mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
3442 mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
3443
3444 initContentView();
3445
3446 LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
3447 ViewGroup.LayoutParams.WRAP_CONTENT);
3448 mContentView.setLayoutParams(wrapContent);
3449
3450 mPopupWindow.setContentView(mContentView);
3451 }
3452
3453 public void show() {
3454 getPositionListener().addSubscriber(this, false /* offset is fixed */);
3455
3456 computeLocalPosition();
3457
3458 final PositionListener positionListener = getPositionListener();
3459 updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
3460 }
3461
3462 protected void measureContent() {
3463 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3464 mContentView.measure(
3465 View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
3466 View.MeasureSpec.AT_MOST),
3467 View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
3468 View.MeasureSpec.AT_MOST));
3469 }
3470
3471 /* The popup window will be horizontally centered on the getTextOffset() and vertically
3472 * positioned according to viewportToContentHorizontalOffset.
3473 *
3474 * This method assumes that mContentView has properly been measured from its content. */
3475 private void computeLocalPosition() {
3476 measureContent();
3477 final int width = mContentView.getMeasuredWidth();
3478 final int offset = getTextOffset();
3479 mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f);
3480 mPositionX += mTextView.viewportToContentHorizontalOffset();
3481
3482 final int line = mTextView.getLayout().getLineForOffset(offset);
3483 mPositionY = getVerticalLocalPosition(line);
3484 mPositionY += mTextView.viewportToContentVerticalOffset();
3485 }
3486
3487 private void updatePosition(int parentPositionX, int parentPositionY) {
3488 int positionX = parentPositionX + mPositionX;
3489 int positionY = parentPositionY + mPositionY;
3490
3491 positionY = clipVertically(positionY);
3492
3493 // Horizontal clipping
3494 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3495 final int width = mContentView.getMeasuredWidth();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003496 positionX = Math.min(
3497 displayMetrics.widthPixels - width + mClippingLimitRight, positionX);
3498 positionX = Math.max(-mClippingLimitLeft, positionX);
Gilles Debunned88876a2012-03-16 17:34:04 -07003499
3500 if (isShowing()) {
3501 mPopupWindow.update(positionX, positionY, -1, -1);
3502 } else {
3503 mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
3504 positionX, positionY);
3505 }
3506 }
3507
3508 public void hide() {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09003509 if (!isShowing()) {
3510 return;
3511 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003512 mPopupWindow.dismiss();
3513 getPositionListener().removeSubscriber(this);
3514 }
3515
3516 @Override
3517 public void updatePosition(int parentPositionX, int parentPositionY,
3518 boolean parentPositionChanged, boolean parentScrolled) {
3519 // Either parentPositionChanged or parentScrolled is true, check if still visible
3520 if (isShowing() && isOffsetVisible(getTextOffset())) {
3521 if (parentScrolled) computeLocalPosition();
3522 updatePosition(parentPositionX, parentPositionY);
3523 } else {
3524 hide();
3525 }
3526 }
3527
3528 public boolean isShowing() {
3529 return mPopupWindow.isShowing();
3530 }
3531 }
3532
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003533 private static final class SuggestionInfo {
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003534 // Range of actual suggestion within mText
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003535 int mSuggestionStart, mSuggestionEnd;
3536
3537 // The SuggestionSpan that this TextView represents
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003538 final SuggestionSpanInfo mSuggestionSpanInfo = new SuggestionSpanInfo();
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003539
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003540 // The index of this suggestion inside suggestionSpan
3541 int mSuggestionIndex;
3542
3543 final SpannableStringBuilder mText = new SpannableStringBuilder();
3544
3545 void clear() {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003546 mSuggestionSpanInfo.clear();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003547 mText.clear();
3548 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003549
3550 // Utility method to set attributes about a SuggestionSpan.
3551 void setSpanInfo(SuggestionSpan span, int spanStart, int spanEnd) {
3552 mSuggestionSpanInfo.mSuggestionSpan = span;
3553 mSuggestionSpanInfo.mSpanStart = spanStart;
3554 mSuggestionSpanInfo.mSpanEnd = spanEnd;
3555 }
3556 }
3557
3558 private static final class SuggestionSpanInfo {
3559 // The SuggestionSpan;
3560 @Nullable
3561 SuggestionSpan mSuggestionSpan;
3562
3563 // The SuggestionSpan start position
3564 int mSpanStart;
3565
3566 // The SuggestionSpan end position
3567 int mSpanEnd;
3568
3569 void clear() {
3570 mSuggestionSpan = null;
3571 }
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003572 }
3573
3574 private class SuggestionHelper {
3575 private final Comparator<SuggestionSpan> mSuggestionSpanComparator =
3576 new SuggestionSpanComparator();
3577 private final HashMap<SuggestionSpan, Integer> mSpansLengths =
3578 new HashMap<SuggestionSpan, Integer>();
3579
3580 private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
3581 public int compare(SuggestionSpan span1, SuggestionSpan span2) {
3582 final int flag1 = span1.getFlags();
3583 final int flag2 = span2.getFlags();
3584 if (flag1 != flag2) {
3585 // The order here should match what is used in updateDrawState
3586 final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
3587 final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
3588 final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
3589 final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
3590 if (easy1 && !misspelled1) return -1;
3591 if (easy2 && !misspelled2) return 1;
3592 if (misspelled1) return -1;
3593 if (misspelled2) return 1;
3594 }
3595
3596 return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
3597 }
3598 }
3599
3600 /**
3601 * Returns the suggestion spans that cover the current cursor position. The suggestion
3602 * spans are sorted according to the length of text that they are attached to.
3603 */
3604 private SuggestionSpan[] getSortedSuggestionSpans() {
3605 int pos = mTextView.getSelectionStart();
3606 Spannable spannable = (Spannable) mTextView.getText();
3607 SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
3608
3609 mSpansLengths.clear();
3610 for (SuggestionSpan suggestionSpan : suggestionSpans) {
3611 int start = spannable.getSpanStart(suggestionSpan);
3612 int end = spannable.getSpanEnd(suggestionSpan);
3613 mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
3614 }
3615
3616 // The suggestions are sorted according to their types (easy correction first, then
3617 // misspelled) and to the length of the text that they cover (shorter first).
3618 Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
3619 mSpansLengths.clear();
3620
3621 return suggestionSpans;
3622 }
3623
3624 /**
3625 * Gets the SuggestionInfo list that contains suggestion information at the current cursor
3626 * position.
3627 *
3628 * @param suggestionInfos SuggestionInfo array the results will be set.
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003629 * @param misspelledSpanInfo a struct the misspelled SuggestionSpan info will be set.
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003630 * @return the number of suggestions actually fetched.
3631 */
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003632 public int getSuggestionInfo(SuggestionInfo[] suggestionInfos,
3633 @Nullable SuggestionSpanInfo misspelledSpanInfo) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003634 final Spannable spannable = (Spannable) mTextView.getText();
3635 final SuggestionSpan[] suggestionSpans = getSortedSuggestionSpans();
3636 final int nbSpans = suggestionSpans.length;
3637 if (nbSpans == 0) return 0;
3638
3639 int numberOfSuggestions = 0;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003640 for (final SuggestionSpan suggestionSpan : suggestionSpans) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003641 final int spanStart = spannable.getSpanStart(suggestionSpan);
3642 final int spanEnd = spannable.getSpanEnd(suggestionSpan);
3643
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003644 if (misspelledSpanInfo != null
3645 && (suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
3646 misspelledSpanInfo.mSuggestionSpan = suggestionSpan;
3647 misspelledSpanInfo.mSpanStart = spanStart;
3648 misspelledSpanInfo.mSpanEnd = spanEnd;
3649 }
3650
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003651 final String[] suggestions = suggestionSpan.getSuggestions();
3652 final int nbSuggestions = suggestions.length;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003653 suggestionLoop:
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003654 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
3655 final String suggestion = suggestions[suggestionIndex];
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003656 for (int i = 0; i < numberOfSuggestions; i++) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003657 final SuggestionInfo otherSuggestionInfo = suggestionInfos[i];
3658 if (otherSuggestionInfo.mText.toString().equals(suggestion)) {
3659 final int otherSpanStart =
3660 otherSuggestionInfo.mSuggestionSpanInfo.mSpanStart;
3661 final int otherSpanEnd =
3662 otherSuggestionInfo.mSuggestionSpanInfo.mSpanEnd;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003663 if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003664 continue suggestionLoop;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003665 }
3666 }
3667 }
3668
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003669 SuggestionInfo suggestionInfo = suggestionInfos[numberOfSuggestions];
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003670 suggestionInfo.setSpanInfo(suggestionSpan, spanStart, spanEnd);
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003671 suggestionInfo.mSuggestionIndex = suggestionIndex;
3672 suggestionInfo.mSuggestionStart = 0;
3673 suggestionInfo.mSuggestionEnd = suggestion.length();
3674 suggestionInfo.mText.replace(0, suggestionInfo.mText.length(), suggestion);
3675 numberOfSuggestions++;
3676 if (numberOfSuggestions >= suggestionInfos.length) {
3677 return numberOfSuggestions;
3678 }
3679 }
3680 }
3681 return numberOfSuggestions;
3682 }
3683 }
3684
Yohei Yukawaca9376c2019-02-01 23:38:30 -08003685 private final class SuggestionsPopupWindow extends PinnedPopupWindow
3686 implements OnItemClickListener {
Gilles Debunned88876a2012-03-16 17:34:04 -07003687 private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003688
3689 // Key of intent extras for inserting new word into user dictionary.
3690 private static final String USER_DICTIONARY_EXTRA_WORD = "word";
3691 private static final String USER_DICTIONARY_EXTRA_LOCALE = "locale";
3692
Gilles Debunned88876a2012-03-16 17:34:04 -07003693 private SuggestionInfo[] mSuggestionInfos;
3694 private int mNumberOfSuggestions;
3695 private boolean mCursorWasVisibleBeforeSuggestions;
3696 private boolean mIsShowingUp = false;
3697 private SuggestionAdapter mSuggestionsAdapter;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003698 private TextAppearanceSpan mHighlightSpan; // TODO: Make mHighlightSpan final.
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003699 private TextView mAddToDictionaryButton;
3700 private TextView mDeleteButton;
Seigo Nonakaf47976e2016-03-01 09:17:37 -08003701 private ListView mSuggestionListView;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003702 private final SuggestionSpanInfo mMisspelledSpanInfo = new SuggestionSpanInfo();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003703 private int mContainerMarginWidth;
3704 private int mContainerMarginTop;
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003705 private LinearLayout mContainerView;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003706 private Context mContext; // TODO: Make mContext final.
Gilles Debunned88876a2012-03-16 17:34:04 -07003707
3708 private class CustomPopupWindow extends PopupWindow {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003709
Gilles Debunned88876a2012-03-16 17:34:04 -07003710 @Override
3711 public void dismiss() {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09003712 if (!isShowing()) {
3713 return;
3714 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003715 super.dismiss();
Gilles Debunned88876a2012-03-16 17:34:04 -07003716 getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
3717
3718 // Safe cast since show() checks that mTextView.getText() is an Editable
3719 ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan);
3720
3721 mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions);
Keisuke Kuroyanagi4a696ac2016-02-23 11:02:07 -08003722 if (hasInsertionController() && !extractedTextModeWillBeStarted()) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003723 getInsertionController().show();
3724 }
3725 }
3726 }
3727
3728 public SuggestionsPopupWindow() {
3729 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
Gilles Debunned88876a2012-03-16 17:34:04 -07003730 }
3731
3732 @Override
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003733 protected void setUp() {
3734 mContext = applyDefaultTheme(mTextView.getContext());
3735 mHighlightSpan = new TextAppearanceSpan(mContext,
3736 mTextView.mTextEditSuggestionHighlightStyle);
3737 }
3738
3739 private Context applyDefaultTheme(Context originalContext) {
3740 TypedArray a = originalContext.obtainStyledAttributes(
3741 new int[]{com.android.internal.R.attr.isLightTheme});
3742 boolean isLightTheme = a.getBoolean(0, true);
3743 int themeId = isLightTheme ? R.style.ThemeOverlay_Material_Light
3744 : R.style.ThemeOverlay_Material_Dark;
3745 a.recycle();
3746 return new ContextThemeWrapper(originalContext, themeId);
3747 }
3748
3749 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07003750 protected void createPopupWindow() {
Seigo Nonaka3ed1b392016-01-19 13:54:59 +09003751 mPopupWindow = new CustomPopupWindow();
Gilles Debunned88876a2012-03-16 17:34:04 -07003752 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
Seigo Nonaka3ed1b392016-01-19 13:54:59 +09003753 mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
Gilles Debunned88876a2012-03-16 17:34:04 -07003754 mPopupWindow.setFocusable(true);
3755 mPopupWindow.setClippingEnabled(false);
3756 }
3757
3758 @Override
3759 protected void initContentView() {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003760 final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
3761 Context.LAYOUT_INFLATER_SERVICE);
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003762 mContentView = (ViewGroup) inflater.inflate(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003763 mTextView.mTextEditSuggestionContainerLayout, null);
Gilles Debunned88876a2012-03-16 17:34:04 -07003764
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003765 mContainerView = (LinearLayout) mContentView.findViewById(
3766 com.android.internal.R.id.suggestionWindowContainer);
Seigo Nonaka60490d12016-01-28 17:25:18 +09003767 ViewGroup.MarginLayoutParams lp =
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003768 (ViewGroup.MarginLayoutParams) mContainerView.getLayoutParams();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003769 mContainerMarginWidth = lp.leftMargin + lp.rightMargin;
3770 mContainerMarginTop = lp.topMargin;
3771 mClippingLimitLeft = lp.leftMargin;
3772 mClippingLimitRight = lp.rightMargin;
3773
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003774 mSuggestionListView = (ListView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003775 com.android.internal.R.id.suggestionContainer);
3776
3777 mSuggestionsAdapter = new SuggestionAdapter();
Seigo Nonakaf47976e2016-03-01 09:17:37 -08003778 mSuggestionListView.setAdapter(mSuggestionsAdapter);
3779 mSuggestionListView.setOnItemClickListener(this);
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003780
3781 // Inflate the suggestion items once and for all.
3782 mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS];
Gilles Debunned88876a2012-03-16 17:34:04 -07003783 for (int i = 0; i < mSuggestionInfos.length; i++) {
3784 mSuggestionInfos[i] = new SuggestionInfo();
3785 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003786
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003787 mAddToDictionaryButton = (TextView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003788 com.android.internal.R.id.addToDictionaryButton);
3789 mAddToDictionaryButton.setOnClickListener(new View.OnClickListener() {
3790 public void onClick(View v) {
Keisuke Kuroyanagi6e0860d2016-03-15 15:40:43 +09003791 final SuggestionSpan misspelledSpan =
3792 findEquivalentSuggestionSpan(mMisspelledSpanInfo);
3793 if (misspelledSpan == null) {
3794 // Span has been removed.
3795 return;
3796 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003797 final Editable editable = (Editable) mTextView.getText();
Keisuke Kuroyanagi6e0860d2016-03-15 15:40:43 +09003798 final int spanStart = editable.getSpanStart(misspelledSpan);
3799 final int spanEnd = editable.getSpanEnd(misspelledSpan);
3800 if (spanStart < 0 || spanEnd <= spanStart) {
3801 return;
3802 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003803 final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
3804
3805 final Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
3806 intent.putExtra(USER_DICTIONARY_EXTRA_WORD, originalText);
3807 intent.putExtra(USER_DICTIONARY_EXTRA_LOCALE,
3808 mTextView.getTextServicesLocale().toString());
3809 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
Yohei Yukawa0115ac12019-02-05 22:27:20 -08003810 mTextView.startActivityAsTextOperationUserIfNecessary(intent);
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003811 // There is no way to know if the word was indeed added. Re-check.
3812 // TODO The ExtractEditText should remove the span in the original text instead
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003813 editable.removeSpan(mMisspelledSpanInfo.mSuggestionSpan);
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003814 Selection.setSelection(editable, spanEnd);
3815 updateSpellCheckSpans(spanStart, spanEnd, false);
3816 hideWithCleanUp();
3817 }
3818 });
3819
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003820 mDeleteButton = (TextView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003821 com.android.internal.R.id.deleteButton);
3822 mDeleteButton.setOnClickListener(new View.OnClickListener() {
3823 public void onClick(View v) {
3824 final Editable editable = (Editable) mTextView.getText();
3825
3826 final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
3827 int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
3828 if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
3829 // Do not leave two adjacent spaces after deletion, or one at beginning of
3830 // text
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003831 if (spanUnionEnd < editable.length()
3832 && Character.isSpaceChar(editable.charAt(spanUnionEnd))
3833 && (spanUnionStart == 0
3834 || Character.isSpaceChar(
3835 editable.charAt(spanUnionStart - 1)))) {
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003836 spanUnionEnd = spanUnionEnd + 1;
3837 }
3838 mTextView.deleteText_internal(spanUnionStart, spanUnionEnd);
3839 }
3840 hideWithCleanUp();
3841 }
3842 });
3843
Gilles Debunned88876a2012-03-16 17:34:04 -07003844 }
3845
3846 public boolean isShowingUp() {
3847 return mIsShowingUp;
3848 }
3849
3850 public void onParentLostFocus() {
3851 mIsShowingUp = false;
3852 }
3853
Gilles Debunned88876a2012-03-16 17:34:04 -07003854 private class SuggestionAdapter extends BaseAdapter {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003855 private LayoutInflater mInflater = (LayoutInflater) mContext.getSystemService(
3856 Context.LAYOUT_INFLATER_SERVICE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003857
3858 @Override
3859 public int getCount() {
3860 return mNumberOfSuggestions;
3861 }
3862
3863 @Override
3864 public Object getItem(int position) {
3865 return mSuggestionInfos[position];
3866 }
3867
3868 @Override
3869 public long getItemId(int position) {
3870 return position;
3871 }
3872
3873 @Override
3874 public View getView(int position, View convertView, ViewGroup parent) {
3875 TextView textView = (TextView) convertView;
3876
3877 if (textView == null) {
3878 textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout,
3879 parent, false);
3880 }
3881
3882 final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003883 textView.setText(suggestionInfo.mText);
Gilles Debunned88876a2012-03-16 17:34:04 -07003884 return textView;
3885 }
3886 }
3887
Gilles Debunned88876a2012-03-16 17:34:04 -07003888 @Override
3889 public void show() {
3890 if (!(mTextView.getText() instanceof Editable)) return;
Keisuke Kuroyanagi4a696ac2016-02-23 11:02:07 -08003891 if (extractedTextModeWillBeStarted()) {
3892 return;
3893 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003894
3895 if (updateSuggestions()) {
3896 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
3897 mTextView.setCursorVisible(false);
3898 mIsShowingUp = true;
3899 super.show();
3900 }
Clara Bayarri428e5232017-07-18 16:42:16 +01003901
3902 mSuggestionListView.setVisibility(mNumberOfSuggestions == 0 ? View.GONE : View.VISIBLE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003903 }
3904
3905 @Override
3906 protected void measureContent() {
3907 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3908 final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
3909 displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
3910 final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
3911 displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
3912
3913 int width = 0;
3914 View view = null;
3915 for (int i = 0; i < mNumberOfSuggestions; i++) {
3916 view = mSuggestionsAdapter.getView(i, view, mContentView);
3917 view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
3918 view.measure(horizontalMeasure, verticalMeasure);
3919 width = Math.max(width, view.getMeasuredWidth());
3920 }
3921
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003922 if (mAddToDictionaryButton.getVisibility() != View.GONE) {
3923 mAddToDictionaryButton.measure(horizontalMeasure, verticalMeasure);
3924 width = Math.max(width, mAddToDictionaryButton.getMeasuredWidth());
3925 }
3926
3927 mDeleteButton.measure(horizontalMeasure, verticalMeasure);
3928 width = Math.max(width, mDeleteButton.getMeasuredWidth());
3929
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003930 width += mContainerView.getPaddingLeft() + mContainerView.getPaddingRight()
3931 + mContainerMarginWidth;
Seigo Nonaka60490d12016-01-28 17:25:18 +09003932
Gilles Debunned88876a2012-03-16 17:34:04 -07003933 // Enforce the width based on actual text widths
3934 mContentView.measure(
3935 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
3936 verticalMeasure);
3937
3938 Drawable popupBackground = mPopupWindow.getBackground();
3939 if (popupBackground != null) {
3940 if (mTempRect == null) mTempRect = new Rect();
3941 popupBackground.getPadding(mTempRect);
3942 width += mTempRect.left + mTempRect.right;
3943 }
3944 mPopupWindow.setWidth(width);
3945 }
3946
3947 @Override
3948 protected int getTextOffset() {
Keisuke Kuroyanagi713be062016-02-29 16:07:54 -08003949 return (mTextView.getSelectionStart() + mTextView.getSelectionStart()) / 2;
Gilles Debunned88876a2012-03-16 17:34:04 -07003950 }
3951
3952 @Override
3953 protected int getVerticalLocalPosition(int line) {
Siyamed Sinira60b59d2017-07-26 09:26:41 -07003954 final Layout layout = mTextView.getLayout();
3955 return layout.getLineBottomWithoutSpacing(line) - mContainerMarginTop;
Gilles Debunned88876a2012-03-16 17:34:04 -07003956 }
3957
3958 @Override
3959 protected int clipVertically(int positionY) {
3960 final int height = mContentView.getMeasuredHeight();
3961 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3962 return Math.min(positionY, displayMetrics.heightPixels - height);
3963 }
3964
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003965 private void hideWithCleanUp() {
3966 for (final SuggestionInfo info : mSuggestionInfos) {
3967 info.clear();
3968 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003969 mMisspelledSpanInfo.clear();
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003970 hide();
Gilles Debunned88876a2012-03-16 17:34:04 -07003971 }
3972
3973 private boolean updateSuggestions() {
3974 Spannable spannable = (Spannable) mTextView.getText();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003975 mNumberOfSuggestions =
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003976 mSuggestionHelper.getSuggestionInfo(mSuggestionInfos, mMisspelledSpanInfo);
3977 if (mNumberOfSuggestions == 0 && mMisspelledSpanInfo.mSuggestionSpan == null) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003978 return false;
3979 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003980
Gilles Debunned88876a2012-03-16 17:34:04 -07003981 int spanUnionStart = mTextView.getText().length();
3982 int spanUnionEnd = 0;
3983
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003984 for (int i = 0; i < mNumberOfSuggestions; i++) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003985 final SuggestionSpanInfo spanInfo = mSuggestionInfos[i].mSuggestionSpanInfo;
3986 spanUnionStart = Math.min(spanUnionStart, spanInfo.mSpanStart);
3987 spanUnionEnd = Math.max(spanUnionEnd, spanInfo.mSpanEnd);
3988 }
3989 if (mMisspelledSpanInfo.mSuggestionSpan != null) {
3990 spanUnionStart = Math.min(spanUnionStart, mMisspelledSpanInfo.mSpanStart);
3991 spanUnionEnd = Math.max(spanUnionEnd, mMisspelledSpanInfo.mSpanEnd);
Gilles Debunned88876a2012-03-16 17:34:04 -07003992 }
3993
3994 for (int i = 0; i < mNumberOfSuggestions; i++) {
3995 highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
3996 }
3997
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003998 // Make "Add to dictionary" item visible if there is a span with the misspelled flag
3999 int addToDictionaryButtonVisibility = View.GONE;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09004000 if (mMisspelledSpanInfo.mSuggestionSpan != null) {
4001 if (mMisspelledSpanInfo.mSpanStart >= 0
4002 && mMisspelledSpanInfo.mSpanEnd > mMisspelledSpanInfo.mSpanStart) {
Seigo Nonakaa71a2442015-06-19 15:00:43 +09004003 addToDictionaryButtonVisibility = View.VISIBLE;
Gilles Debunned88876a2012-03-16 17:34:04 -07004004 }
4005 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09004006 mAddToDictionaryButton.setVisibility(addToDictionaryButtonVisibility);
Gilles Debunned88876a2012-03-16 17:34:04 -07004007
4008 if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09004009 final int underlineColor;
4010 if (mNumberOfSuggestions != 0) {
4011 underlineColor =
4012 mSuggestionInfos[0].mSuggestionSpanInfo.mSuggestionSpan.getUnderlineColor();
4013 } else {
4014 underlineColor = mMisspelledSpanInfo.mSuggestionSpan.getUnderlineColor();
4015 }
4016
Gilles Debunned88876a2012-03-16 17:34:04 -07004017 if (underlineColor == 0) {
4018 // Fallback on the default highlight color when the first span does not provide one
4019 mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor);
4020 } else {
4021 final float BACKGROUND_TRANSPARENCY = 0.4f;
4022 final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
4023 mSuggestionRangeSpan.setBackgroundColor(
4024 (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
4025 }
4026 spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
4027 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
4028
4029 mSuggestionsAdapter.notifyDataSetChanged();
4030 return true;
4031 }
4032
4033 private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
4034 int unionEnd) {
4035 final Spannable text = (Spannable) mTextView.getText();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09004036 final int spanStart = suggestionInfo.mSuggestionSpanInfo.mSpanStart;
4037 final int spanEnd = suggestionInfo.mSuggestionSpanInfo.mSpanEnd;
Gilles Debunned88876a2012-03-16 17:34:04 -07004038
4039 // Adjust the start/end of the suggestion span
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09004040 suggestionInfo.mSuggestionStart = spanStart - unionStart;
4041 suggestionInfo.mSuggestionEnd = suggestionInfo.mSuggestionStart
4042 + suggestionInfo.mText.length();
Gilles Debunned88876a2012-03-16 17:34:04 -07004043
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09004044 suggestionInfo.mText.setSpan(mHighlightSpan, 0, suggestionInfo.mText.length(),
Seigo Nonakabffbd302015-08-18 18:27:56 -07004045 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunned88876a2012-03-16 17:34:04 -07004046
4047 // Add the text before and after the span.
4048 final String textAsString = text.toString();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09004049 suggestionInfo.mText.insert(0, textAsString.substring(unionStart, spanStart));
4050 suggestionInfo.mText.append(textAsString.substring(spanEnd, unionEnd));
Gilles Debunned88876a2012-03-16 17:34:04 -07004051 }
4052
4053 @Override
4054 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004055 SuggestionInfo suggestionInfo = mSuggestionInfos[position];
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08004056 replaceWithSuggestion(suggestionInfo);
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09004057 hideWithCleanUp();
Gilles Debunned88876a2012-03-16 17:34:04 -07004058 }
4059 }
4060
4061 /**
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004062 * An ActionMode Callback class that is used to provide actions while in text insertion or
4063 * selection mode.
Gilles Debunned88876a2012-03-16 17:34:04 -07004064 *
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004065 * The default callback provides a subset of Select All, Cut, Copy, Paste, Share and Replace
4066 * actions, depending on which of these this TextView supports and the current selection.
Gilles Debunned88876a2012-03-16 17:34:04 -07004067 */
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004068 private class TextActionModeCallback extends ActionMode.Callback2 {
Clara Bayarriea4f1502015-03-18 00:25:01 +00004069 private final Path mSelectionPath = new Path();
4070 private final RectF mSelectionBounds = new RectF();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004071 private final boolean mHasSelection;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00004072 private final int mHandleHeight;
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004073 private final Map<MenuItem, OnClickListener> mAssistClickHandlers = new HashMap<>();
Clara Bayarriea4f1502015-03-18 00:25:01 +00004074
Richard Ledley26b87222017-11-30 10:54:08 +00004075 TextActionModeCallback(@TextActionMode int mode) {
4076 mHasSelection = mode == TextActionMode.SELECTION
4077 || (mTextIsSelectable && mode == TextActionMode.TEXT_LINK);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004078 if (mHasSelection) {
4079 SelectionModifierCursorController selectionController = getSelectionController();
4080 if (selectionController.mStartHandle == null) {
4081 // As these are for initializing selectionController, hide() must be called.
Mihai Popadb68c542018-11-08 15:23:01 +00004082 loadHandleDrawables(false /* overwrite */);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004083 selectionController.initHandles();
4084 selectionController.hide();
4085 }
4086 mHandleHeight = Math.max(
4087 mSelectHandleLeft.getMinimumHeight(),
4088 mSelectHandleRight.getMinimumHeight());
4089 } else {
4090 InsertionPointCursorController insertionController = getInsertionController();
4091 if (insertionController != null) {
4092 insertionController.getHandle();
4093 mHandleHeight = mSelectHandleCenter.getMinimumHeight();
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00004094 } else {
4095 mHandleHeight = 0;
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004096 }
Clara Bayarri7fc946e2015-03-31 14:48:33 +01004097 }
Clara Bayarriea4f1502015-03-18 00:25:01 +00004098 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004099
4100 @Override
4101 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004102 mAssistClickHandlers.clear();
4103
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004104 mode.setTitle(null);
Clara Bayarri13152d12015-04-09 12:02:04 +01004105 mode.setSubtitle(null);
4106 mode.setTitleOptionalHint(true);
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00004107 populateMenuWithItems(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01004108
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004109 Callback customCallback = getCustomCallback();
4110 if (customCallback != null) {
4111 if (!customCallback.onCreateActionMode(mode, menu)) {
Clara Bayarri01243ac2015-06-03 00:46:29 +01004112 // The custom mode can choose to cancel the action mode, dismiss selection.
4113 Selection.setSelection((Spannable) mTextView.getText(),
4114 mTextView.getSelectionEnd());
Clara Bayarri13152d12015-04-09 12:02:04 +01004115 return false;
4116 }
4117 }
4118
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07004119 if (mTextView.canProcessText()) {
4120 mProcessTextIntentActionsHandler.onInitializeMenu(menu);
4121 }
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +00004122
Abodunrinwa Tokideb2f492017-11-06 18:55:17 +00004123 if (mHasSelection && !mTextView.hasTransientState()) {
4124 mTextView.setHasTransientState(true);
Clara Bayarri13152d12015-04-09 12:02:04 +01004125 }
Abodunrinwa Tokideb2f492017-11-06 18:55:17 +00004126 return true;
Clara Bayarri13152d12015-04-09 12:02:04 +01004127 }
4128
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004129 private Callback getCustomCallback() {
4130 return mHasSelection
4131 ? mCustomSelectionActionModeCallback
4132 : mCustomInsertionActionModeCallback;
4133 }
4134
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00004135 private void populateMenuWithItems(Menu menu) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004136 if (mTextView.canCut()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01004137 menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004138 com.android.internal.R.string.cut)
4139 .setAlphabeticShortcut('x')
4140 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07004141 }
4142
4143 if (mTextView.canCopy()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01004144 menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004145 com.android.internal.R.string.copy)
4146 .setAlphabeticShortcut('c')
4147 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07004148 }
4149
4150 if (mTextView.canPaste()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01004151 menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004152 com.android.internal.R.string.paste)
4153 .setAlphabeticShortcut('v')
4154 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07004155 }
4156
Andrei Stingaceanu7f0c5bd2015-04-14 17:12:08 +01004157 if (mTextView.canShare()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01004158 menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004159 com.android.internal.R.string.share)
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +00004160 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
Andrei Stingaceanu7f0c5bd2015-04-14 17:12:08 +01004161 }
4162
Felipe Leme2ac463e2017-03-13 14:06:25 -07004163 if (mTextView.canRequestAutofill()) {
Felipe Leme1c1626e2017-06-02 10:53:13 -07004164 final String selected = mTextView.getSelectedText();
4165 if (selected == null || selected.isEmpty()) {
4166 menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
4167 com.android.internal.R.string.autofill)
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01004168 .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
Felipe Leme1c1626e2017-06-02 10:53:13 -07004169 }
Felipe Leme2ac463e2017-03-13 14:06:25 -07004170 }
4171
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01004172 if (mTextView.canPasteAsPlainText()) {
4173 menu.add(
4174 Menu.NONE,
4175 TextView.ID_PASTE_AS_PLAIN_TEXT,
4176 MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
4177 com.android.internal.R.string.paste_as_plain_text)
4178 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
4179 }
4180
Clara Bayarri3b69fd82015-06-03 21:52:02 +01004181 updateSelectAllItem(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01004182 updateReplaceItem(menu);
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004183 updateAssistMenuItems(menu);
Gilles Debunned88876a2012-03-16 17:34:04 -07004184 }
4185
4186 @Override
4187 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01004188 updateSelectAllItem(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01004189 updateReplaceItem(menu);
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004190 updateAssistMenuItems(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01004191
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004192 Callback customCallback = getCustomCallback();
4193 if (customCallback != null) {
4194 return customCallback.onPrepareActionMode(mode, menu);
Gilles Debunned88876a2012-03-16 17:34:04 -07004195 }
4196 return true;
4197 }
4198
Clara Bayarri3b69fd82015-06-03 21:52:02 +01004199 private void updateSelectAllItem(Menu menu) {
4200 boolean canSelectAll = mTextView.canSelectAllText();
4201 boolean selectAllItemExists = menu.findItem(TextView.ID_SELECT_ALL) != null;
4202 if (canSelectAll && !selectAllItemExists) {
4203 menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
4204 com.android.internal.R.string.selectAll)
4205 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
4206 } else if (!canSelectAll && selectAllItemExists) {
4207 menu.removeItem(TextView.ID_SELECT_ALL);
4208 }
4209 }
4210
Clara Bayarri13152d12015-04-09 12:02:04 +01004211 private void updateReplaceItem(Menu menu) {
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08004212 boolean canReplace = mTextView.isSuggestionsEnabled() && shouldOfferToShowSuggestions();
Clara Bayarri13152d12015-04-09 12:02:04 +01004213 boolean replaceItemExists = menu.findItem(TextView.ID_REPLACE) != null;
4214 if (canReplace && !replaceItemExists) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01004215 menu.add(Menu.NONE, TextView.ID_REPLACE, MENU_ITEM_ORDER_REPLACE,
4216 com.android.internal.R.string.replace)
4217 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
Clara Bayarri13152d12015-04-09 12:02:04 +01004218 } else if (!canReplace && replaceItemExists) {
4219 menu.removeItem(TextView.ID_REPLACE);
4220 }
4221 }
4222
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004223 private void updateAssistMenuItems(Menu menu) {
4224 clearAssistMenuItems(menu);
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +00004225 if (!shouldEnableAssistMenuItems()) {
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004226 return;
4227 }
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +01004228 final TextClassification textClassification =
4229 getSelectionActionModeHelper().getTextClassification();
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00004230 if (textClassification == null) {
4231 return;
4232 }
Jan Althaus20d346e2018-03-23 14:03:52 +01004233 if (!textClassification.getActions().isEmpty()) {
4234 // Primary assist action (Always shown).
4235 final MenuItem item = addAssistMenuItem(menu,
4236 textClassification.getActions().get(0), TextView.ID_ASSIST,
4237 MENU_ITEM_ORDER_ASSIST, MenuItem.SHOW_AS_ACTION_ALWAYS);
4238 item.setIntent(textClassification.getIntent());
4239 } else if (hasLegacyAssistItem(textClassification)) {
4240 // Legacy primary assist action (Always shown).
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00004241 final MenuItem item = menu.add(
4242 TextView.ID_ASSIST, TextView.ID_ASSIST, MENU_ITEM_ORDER_ASSIST,
4243 textClassification.getLabel())
4244 .setIcon(textClassification.getIcon())
4245 .setIntent(textClassification.getIntent());
4246 item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Jan Althaus20d346e2018-03-23 14:03:52 +01004247 mAssistClickHandlers.put(item, TextClassification.createIntentOnClickListener(
4248 TextClassification.createPendingIntent(mTextView.getContext(),
Abodunrinwa Toki904a9312018-04-18 21:21:27 +01004249 textClassification.getIntent(),
4250 createAssistMenuItemPendingIntentRequestCode())));
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00004251 }
Jan Althaus20d346e2018-03-23 14:03:52 +01004252 final int count = textClassification.getActions().size();
4253 for (int i = 1; i < count; i++) {
4254 // Secondary assist action (Never shown).
4255 addAssistMenuItem(menu, textClassification.getActions().get(i), Menu.NONE,
4256 MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START + i - 1,
4257 MenuItem.SHOW_AS_ACTION_NEVER);
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00004258 }
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +00004259 }
4260
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +00004261 private MenuItem addAssistMenuItem(Menu menu, RemoteAction action, int itemId, int order,
Jan Althaus20d346e2018-03-23 14:03:52 +01004262 int showAsAction) {
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +00004263 final MenuItem item = menu.add(TextView.ID_ASSIST, itemId, order, action.getTitle())
Jan Althaus20d346e2018-03-23 14:03:52 +01004264 .setContentDescription(action.getContentDescription());
4265 if (action.shouldShowIcon()) {
4266 item.setIcon(action.getIcon().loadDrawable(mTextView.getContext()));
4267 }
4268 item.setShowAsAction(showAsAction);
4269 mAssistClickHandlers.put(item,
4270 TextClassification.createIntentOnClickListener(action.getActionIntent()));
4271 return item;
4272 }
4273
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004274 private void clearAssistMenuItems(Menu menu) {
4275 int i = 0;
4276 while (i < menu.size()) {
4277 final MenuItem menuItem = menu.getItem(i);
4278 if (menuItem.getGroupId() == TextView.ID_ASSIST) {
4279 menu.removeItem(menuItem.getItemId());
4280 continue;
4281 }
4282 i++;
4283 }
4284 }
4285
Jan Althaus20d346e2018-03-23 14:03:52 +01004286 private boolean hasLegacyAssistItem(TextClassification classification) {
4287 // Check whether we have the UI data and and action.
4288 return (classification.getIcon() != null || !TextUtils.isEmpty(
4289 classification.getLabel())) && (classification.getIntent() != null
4290 || classification.getOnClickListener() != null);
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004291 }
4292
4293 private boolean onAssistMenuItemClicked(MenuItem assistMenuItem) {
4294 Preconditions.checkArgument(assistMenuItem.getGroupId() == TextView.ID_ASSIST);
4295
4296 final TextClassification textClassification =
4297 getSelectionActionModeHelper().getTextClassification();
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +00004298 if (!shouldEnableAssistMenuItems() || textClassification == null) {
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004299 // No textClassification result to handle the click. Eat the click.
4300 return true;
4301 }
4302
4303 OnClickListener onClickListener = mAssistClickHandlers.get(assistMenuItem);
4304 if (onClickListener == null) {
4305 final Intent intent = assistMenuItem.getIntent();
4306 if (intent != null) {
Abodunrinwa Toki2f19b922018-02-12 19:59:28 +00004307 onClickListener = TextClassification.createIntentOnClickListener(
Abodunrinwa Toki904a9312018-04-18 21:21:27 +01004308 TextClassification.createPendingIntent(
4309 mTextView.getContext(), intent,
4310 createAssistMenuItemPendingIntentRequestCode()));
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004311 }
4312 }
4313 if (onClickListener != null) {
4314 onClickListener.onClick(mTextView);
4315 stopTextActionMode();
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004316 }
4317 // We tried our best.
4318 return true;
Abodunrinwa Toki9796a1b2017-06-28 02:49:07 +01004319 }
4320
Abodunrinwa Toki904a9312018-04-18 21:21:27 +01004321 private int createAssistMenuItemPendingIntentRequestCode() {
4322 return mTextView.hasSelection()
4323 ? mTextView.getText().subSequence(
4324 mTextView.getSelectionStart(), mTextView.getSelectionEnd())
4325 .hashCode()
4326 : 0;
4327 }
4328
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +00004329 private boolean shouldEnableAssistMenuItems() {
4330 return mTextView.isDeviceProvisioned()
4331 && TextClassificationManager.getSettings(mTextView.getContext())
4332 .isSmartTextShareEnabled();
4333 }
4334
Gilles Debunned88876a2012-03-16 17:34:04 -07004335 @Override
4336 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +00004337 getSelectionActionModeHelper()
4338 .onSelectionAction(item.getItemId(), item.getTitle().toString());
Abodunrinwa Toki1d775572017-05-08 16:03:01 +01004339
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07004340 if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +00004341 return true;
4342 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004343 Callback customCallback = getCustomCallback();
4344 if (customCallback != null && customCallback.onActionItemClicked(mode, item)) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004345 return true;
4346 }
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004347 if (item.getGroupId() == TextView.ID_ASSIST && onAssistMenuItemClicked(item)) {
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +00004348 return true;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00004349 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004350 return mTextView.onTextContextMenuItem(item.getItemId());
4351 }
4352
4353 @Override
4354 public void onDestroyActionMode(ActionMode mode) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09004355 // Clear mTextActionMode not to recursively destroy action mode by clearing selection.
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +00004356 getSelectionActionModeHelper().onDestroyActionMode();
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09004357 mTextActionMode = null;
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004358 Callback customCallback = getCustomCallback();
4359 if (customCallback != null) {
4360 customCallback.onDestroyActionMode(mode);
Gilles Debunned88876a2012-03-16 17:34:04 -07004361 }
Adam Powell057a5852012-05-11 10:28:38 -07004362
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08004363 if (!mPreserveSelection) {
4364 /*
4365 * Leave current selection when we tentatively destroy action mode for the
4366 * selection. If we're detaching from a window, we'll bring back the selection
4367 * mode when (if) we get reattached.
4368 */
Adam Powell057a5852012-05-11 10:28:38 -07004369 Selection.setSelection((Spannable) mTextView.getText(),
4370 mTextView.getSelectionEnd());
Adam Powell057a5852012-05-11 10:28:38 -07004371 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004372
4373 if (mSelectionModifierCursorController != null) {
4374 mSelectionModifierCursorController.hide();
4375 }
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004376
4377 mAssistClickHandlers.clear();
Abodunrinwa Toki52096912018-03-21 23:14:42 +00004378 mRequestingLinkActionMode = false;
Gilles Debunned88876a2012-03-16 17:34:04 -07004379 }
Clara Bayarriea4f1502015-03-18 00:25:01 +00004380
4381 @Override
4382 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
4383 if (!view.equals(mTextView) || mTextView.getLayout() == null) {
4384 super.onGetContentRect(mode, view, outRect);
4385 return;
4386 }
4387 if (mTextView.getSelectionStart() != mTextView.getSelectionEnd()) {
4388 // We have a selection.
4389 mSelectionPath.reset();
4390 mTextView.getLayout().getSelectionPath(
4391 mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mSelectionPath);
4392 mSelectionPath.computeBounds(mSelectionBounds, true);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004393 mSelectionBounds.bottom += mHandleHeight;
Clara Bayarriea4f1502015-03-18 00:25:01 +00004394 } else {
Roozbeh Pournader9c133072017-07-26 22:36:27 -07004395 // We have a cursor.
Siyamed Sinir987ec652016-02-17 19:44:41 -08004396 Layout layout = mTextView.getLayout();
Mady Mellorff66ca52015-07-08 12:31:45 -07004397 int line = layout.getLineForOffset(mTextView.getSelectionStart());
Siyamed Sinir987ec652016-02-17 19:44:41 -08004398 float primaryHorizontal = clampHorizontalPosition(null,
4399 layout.getPrimaryHorizontal(mTextView.getSelectionStart()));
Clara Bayarriea4f1502015-03-18 00:25:01 +00004400 mSelectionBounds.set(
4401 primaryHorizontal,
Mady Mellorff66ca52015-07-08 12:31:45 -07004402 layout.getLineTop(line),
Clara Bayarrif95ed102015-08-12 19:46:47 +01004403 primaryHorizontal,
Siyamed Sinirfdbc5ee2018-02-09 11:24:16 -08004404 layout.getLineBottom(line) + mHandleHeight);
Clara Bayarriea4f1502015-03-18 00:25:01 +00004405 }
4406 // Take TextView's padding and scroll into account.
4407 int textHorizontalOffset = mTextView.viewportToContentHorizontalOffset();
4408 int textVerticalOffset = mTextView.viewportToContentVerticalOffset();
4409 outRect.set(
4410 (int) Math.floor(mSelectionBounds.left + textHorizontalOffset),
4411 (int) Math.floor(mSelectionBounds.top + textVerticalOffset),
4412 (int) Math.ceil(mSelectionBounds.right + textHorizontalOffset),
4413 (int) Math.ceil(mSelectionBounds.bottom + textVerticalOffset));
4414 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004415 }
4416
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004417 /**
4418 * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}
4419 * while the input method is requesting the cursor/anchor position. Does nothing as long as
4420 * {@link InputMethodManager#isWatchingCursor(View)} returns false.
4421 */
4422 private final class CursorAnchorInfoNotifier implements TextViewPositionListener {
Yohei Yukawac46b5f02014-06-10 12:26:34 +09004423 final CursorAnchorInfo.Builder mSelectionInfoBuilder = new CursorAnchorInfo.Builder();
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004424 final int[] mTmpIntOffset = new int[2];
4425 final Matrix mViewToScreenMatrix = new Matrix();
4426
4427 @Override
4428 public void updatePosition(int parentPositionX, int parentPositionY,
4429 boolean parentPositionChanged, boolean parentScrolled) {
4430 final InputMethodState ims = mInputMethodState;
4431 if (ims == null || ims.mBatchEditNesting > 0) {
4432 return;
4433 }
Yohei Yukawa484d4af2018-09-17 16:47:08 -07004434 final InputMethodManager imm = getInputMethodManager();
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004435 if (null == imm) {
4436 return;
4437 }
Yohei Yukawa0023d0e2014-07-11 04:13:03 +09004438 if (!imm.isActive(mTextView)) {
4439 return;
4440 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004441 // Skip if the IME has not requested the cursor/anchor position.
Yohei Yukawa0023d0e2014-07-11 04:13:03 +09004442 if (!imm.isCursorAnchorInfoEnabled()) {
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004443 return;
4444 }
4445 Layout layout = mTextView.getLayout();
4446 if (layout == null) {
4447 return;
4448 }
4449
Yohei Yukawac46b5f02014-06-10 12:26:34 +09004450 final CursorAnchorInfo.Builder builder = mSelectionInfoBuilder;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004451 builder.reset();
4452
4453 final int selectionStart = mTextView.getSelectionStart();
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004454 builder.setSelectionRange(selectionStart, mTextView.getSelectionEnd());
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004455
4456 // Construct transformation matrix from view local coordinates to screen coordinates.
4457 mViewToScreenMatrix.set(mTextView.getMatrix());
4458 mTextView.getLocationOnScreen(mTmpIntOffset);
4459 mViewToScreenMatrix.postTranslate(mTmpIntOffset[0], mTmpIntOffset[1]);
4460 builder.setMatrix(mViewToScreenMatrix);
4461
4462 final float viewportToContentHorizontalOffset =
4463 mTextView.viewportToContentHorizontalOffset();
4464 final float viewportToContentVerticalOffset =
4465 mTextView.viewportToContentVerticalOffset();
4466
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004467 final CharSequence text = mTextView.getText();
4468 if (text instanceof Spannable) {
4469 final Spannable sp = (Spannable) text;
4470 int composingTextStart = EditableInputConnection.getComposingSpanStart(sp);
4471 int composingTextEnd = EditableInputConnection.getComposingSpanEnd(sp);
4472 if (composingTextEnd < composingTextStart) {
4473 final int temp = composingTextEnd;
4474 composingTextEnd = composingTextStart;
4475 composingTextStart = temp;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004476 }
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004477 final boolean hasComposingText =
4478 (0 <= composingTextStart) && (composingTextStart < composingTextEnd);
4479 if (hasComposingText) {
4480 final CharSequence composingText = text.subSequence(composingTextStart,
4481 composingTextEnd);
4482 builder.setComposingText(composingTextStart, composingText);
Phil Weaverc2e28932016-12-08 12:29:25 -08004483 mTextView.populateCharacterBounds(builder, composingTextStart,
4484 composingTextEnd, viewportToContentHorizontalOffset,
4485 viewportToContentVerticalOffset);
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004486 }
4487 }
4488
4489 // Treat selectionStart as the insertion point.
4490 if (0 <= selectionStart) {
4491 final int offset = selectionStart;
4492 final int line = layout.getLineForOffset(offset);
4493 final float insertionMarkerX = layout.getPrimaryHorizontal(offset)
4494 + viewportToContentHorizontalOffset;
4495 final float insertionMarkerTop = layout.getLineTop(line)
4496 + viewportToContentVerticalOffset;
4497 final float insertionMarkerBaseline = layout.getLineBaseline(line)
4498 + viewportToContentVerticalOffset;
Siyamed Sinira60b59d2017-07-26 09:26:41 -07004499 final float insertionMarkerBottom = layout.getLineBottomWithoutSpacing(line)
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004500 + viewportToContentVerticalOffset;
Phil Weaverc2e28932016-12-08 12:29:25 -08004501 final boolean isTopVisible = mTextView
4502 .isPositionVisible(insertionMarkerX, insertionMarkerTop);
4503 final boolean isBottomVisible = mTextView
4504 .isPositionVisible(insertionMarkerX, insertionMarkerBottom);
Yohei Yukawacc24e2b2014-08-29 20:21:10 -07004505 int insertionMarkerFlags = 0;
4506 if (isTopVisible || isBottomVisible) {
4507 insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
4508 }
4509 if (!isTopVisible || !isBottomVisible) {
4510 insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
4511 }
Yohei Yukawa5f183f02014-09-02 14:18:40 -07004512 if (layout.isRtlCharAt(offset)) {
4513 insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL;
4514 }
Yohei Yukawa0b01e7f2014-07-08 15:29:51 +09004515 builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop,
Yohei Yukawacc24e2b2014-08-29 20:21:10 -07004516 insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags);
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004517 }
4518
4519 imm.updateCursorAnchorInfo(mTextView, builder.build());
4520 }
4521 }
4522
Mihai Popa38722382018-03-07 19:56:21 +00004523 private static class MagnifierMotionAnimator {
4524 private static final long DURATION = 100 /* miliseconds */;
4525
4526 // The magnifier being animated.
4527 private final Magnifier mMagnifier;
4528 // A value animator used to animate the magnifier.
4529 private final ValueAnimator mAnimator;
4530
4531 // Whether the magnifier is currently visible.
4532 private boolean mMagnifierIsShowing;
4533 // The coordinates of the magnifier when the currently running animation started.
4534 private float mAnimationStartX;
4535 private float mAnimationStartY;
4536 // The coordinates of the magnifier in the latest animation frame.
4537 private float mAnimationCurrentX;
4538 private float mAnimationCurrentY;
4539 // The latest coordinates the motion animator was asked to #show() the magnifier at.
4540 private float mLastX;
4541 private float mLastY;
4542
4543 private MagnifierMotionAnimator(final Magnifier magnifier) {
4544 mMagnifier = magnifier;
4545 // Prepare the animator used to run the motion animation.
4546 mAnimator = ValueAnimator.ofFloat(0, 1);
4547 mAnimator.setDuration(DURATION);
4548 mAnimator.setInterpolator(new LinearInterpolator());
4549 mAnimator.addUpdateListener((animation) -> {
4550 // Interpolate to find the current position of the magnifier.
4551 mAnimationCurrentX = mAnimationStartX
4552 + (mLastX - mAnimationStartX) * animation.getAnimatedFraction();
4553 mAnimationCurrentY = mAnimationStartY
4554 + (mLastY - mAnimationStartY) * animation.getAnimatedFraction();
4555 mMagnifier.show(mAnimationCurrentX, mAnimationCurrentY);
4556 });
4557 }
4558
4559 /**
4560 * Shows the magnifier at a new position.
4561 * If the y coordinate is different from the previous y coordinate
4562 * (probably corresponding to a line jump in the text), a short
4563 * animation is added to the jump.
4564 */
4565 private void show(final float x, final float y) {
4566 final boolean startNewAnimation = mMagnifierIsShowing && y != mLastY;
4567
4568 if (startNewAnimation) {
4569 if (mAnimator.isRunning()) {
4570 mAnimator.cancel();
4571 mAnimationStartX = mAnimationCurrentX;
4572 mAnimationStartY = mAnimationCurrentY;
4573 } else {
4574 mAnimationStartX = mLastX;
4575 mAnimationStartY = mLastY;
4576 }
4577 mAnimator.start();
4578 } else {
4579 if (!mAnimator.isRunning()) {
4580 mMagnifier.show(x, y);
4581 }
4582 }
4583 mLastX = x;
4584 mLastY = y;
4585 mMagnifierIsShowing = true;
4586 }
4587
4588 /**
4589 * Updates the content of the magnifier.
4590 */
4591 private void update() {
4592 mMagnifier.update();
4593 }
4594
4595 /**
4596 * Dismisses the magnifier, or does nothing if it is already dismissed.
4597 */
4598 private void dismiss() {
4599 mMagnifier.dismiss();
4600 mAnimator.cancel();
4601 mMagnifierIsShowing = false;
4602 }
4603 }
4604
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004605 @VisibleForTesting
4606 public abstract class HandleView extends View implements TextViewPositionListener {
Gilles Debunned88876a2012-03-16 17:34:04 -07004607 protected Drawable mDrawable;
4608 protected Drawable mDrawableLtr;
4609 protected Drawable mDrawableRtl;
4610 private final PopupWindow mContainer;
4611 // Position with respect to the parent TextView
4612 private int mPositionX, mPositionY;
4613 private boolean mIsDragging;
4614 // Offset from touch position to mPosition
4615 private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
4616 protected int mHotspotX;
Adam Powell3fceabd2014-08-19 18:28:04 -07004617 protected int mHorizontalGravity;
Gilles Debunned88876a2012-03-16 17:34:04 -07004618 // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
4619 private float mTouchOffsetY;
Shu Chen77003422020-03-05 13:38:05 +08004620 // Where the touch position should be on the handle to ensure a maximum cursor visibility.
4621 // This is the distance in pixels from the top of the handle view.
Shu Chenc3310322020-03-12 10:55:07 +08004622 private final float mIdealVerticalOffset;
Gilles Debunned88876a2012-03-16 17:34:04 -07004623 // Parent's (TextView) previous position in window
4624 private int mLastParentX, mLastParentY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004625 // Parent's (TextView) previous position on screen
4626 private int mLastParentXOnScreen, mLastParentYOnScreen;
Gilles Debunned88876a2012-03-16 17:34:04 -07004627 // Previous text character offset
Mady Mellorc2225b92015-04-01 15:59:20 -07004628 protected int mPreviousOffset = -1;
Gilles Debunned88876a2012-03-16 17:34:04 -07004629 // Previous text character offset
4630 private boolean mPositionHasChanged = true;
Adam Powell3fceabd2014-08-19 18:28:04 -07004631 // Minimum touch target size for handles
4632 private int mMinSize;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004633 // Indicates the line of text that the handle is on.
Mady Mellora6a0f782015-07-10 16:43:32 -07004634 protected int mPrevLine = UNSET_LINE;
4635 // Indicates the line of text that the user was touching. This can differ from mPrevLine
4636 // when selecting text when the handles jump to the end / start of words which may be on
4637 // a different line.
4638 protected int mPreviousLineTouched = UNSET_LINE;
Mihai Popa6d26d152019-01-30 15:36:47 +00004639 // The raw x coordinate of the motion down event which started the current dragging session.
4640 // Only used and stored when magnifier is used.
4641 private float mCurrentDragInitialTouchRawX = UNSET_X_VALUE;
4642 // The scale transform applied by containers to the TextView. Only used and computed
4643 // when magnifier is used.
4644 private float mTextViewScaleX;
4645 private float mTextViewScaleY;
Shu Chen77003422020-03-05 13:38:05 +08004646 /**
4647 * The vertical distance in pixels from finger to the cursor Y while dragging.
4648 * See {@link Editor.InsertionPointCursorController#getLineDuringDrag}.
4649 */
4650 private final int mIdealFingerToCursorOffset;
Gilles Debunned88876a2012-03-16 17:34:04 -07004651
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004652 private HandleView(Drawable drawableLtr, Drawable drawableRtl, final int id) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004653 super(mTextView.getContext());
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004654 setId(id);
Gilles Debunned88876a2012-03-16 17:34:04 -07004655 mContainer = new PopupWindow(mTextView.getContext(), null,
4656 com.android.internal.R.attr.textSelectHandleWindowStyle);
4657 mContainer.setSplitTouchEnabled(true);
4658 mContainer.setClippingEnabled(false);
4659 mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
Keisuke Kuroyanagi7340be72015-02-27 17:57:49 +09004660 mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
4661 mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
Gilles Debunned88876a2012-03-16 17:34:04 -07004662 mContainer.setContentView(this);
4663
Mihai Popa6315a322018-10-17 17:39:57 +01004664 setDrawables(drawableLtr, drawableRtl);
4665
Adam Powell3fceabd2014-08-19 18:28:04 -07004666 mMinSize = mTextView.getContext().getResources().getDimensionPixelSize(
4667 com.android.internal.R.dimen.text_handle_min_size);
Gilles Debunned88876a2012-03-16 17:34:04 -07004668
Adam Powell3fceabd2014-08-19 18:28:04 -07004669 final int handleHeight = getPreferredHeight();
Gilles Debunned88876a2012-03-16 17:34:04 -07004670 mTouchOffsetY = -0.3f * handleHeight;
Shu Chenc3310322020-03-12 10:55:07 +08004671 final int distance = AppGlobals.getIntCoreSetting(
4672 WidgetFlags.KEY_FINGER_TO_CURSOR_DISTANCE,
4673 WidgetFlags.FINGER_TO_CURSOR_DISTANCE_DEFAULT);
4674 if (distance < 0 || distance > 100) {
4675 mIdealVerticalOffset = 0.7f * handleHeight;
4676 mIdealFingerToCursorOffset = (int)(mIdealVerticalOffset - mTouchOffsetY);
4677 } else {
4678 mIdealFingerToCursorOffset = (int) TypedValue.applyDimension(
4679 TypedValue.COMPLEX_UNIT_DIP, distance,
4680 mTextView.getContext().getResources().getDisplayMetrics());
4681 mIdealVerticalOffset = mIdealFingerToCursorOffset + mTouchOffsetY;
4682 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004683 }
4684
Mady Mellor7a936442015-05-20 10:05:52 -07004685 public float getIdealVerticalOffset() {
4686 return mIdealVerticalOffset;
4687 }
4688
Shu Chen77003422020-03-05 13:38:05 +08004689 final int getIdealFingerToCursorOffset() {
4690 return mIdealFingerToCursorOffset;
4691 }
4692
Mihai Popa6315a322018-10-17 17:39:57 +01004693 void setDrawables(final Drawable drawableLtr, final Drawable drawableRtl) {
4694 mDrawableLtr = drawableLtr;
4695 mDrawableRtl = drawableRtl;
4696 updateDrawable(true /* updateDrawableWhenDragging */);
4697 }
4698
4699 protected void updateDrawable(final boolean updateDrawableWhenDragging) {
4700 if (!updateDrawableWhenDragging && mIsDragging) {
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004701 return;
4702 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004703 final Layout layout = mTextView.getLayout();
4704 if (layout == null) {
4705 return;
4706 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004707 final int offset = getCurrentCursorOffset();
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004708 final boolean isRtlCharAtOffset = isAtRtlRun(layout, offset);
Keisuke Kuroyanagi33f81ac2015-05-14 20:10:57 +09004709 final Drawable oldDrawable = mDrawable;
Gilles Debunned88876a2012-03-16 17:34:04 -07004710 mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
4711 mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
Adam Powell3fceabd2014-08-19 18:28:04 -07004712 mHorizontalGravity = getHorizontalGravity(isRtlCharAtOffset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004713 if (oldDrawable != mDrawable && isShowing()) {
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004714 // Update popup window position.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004715 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
4716 - getHorizontalOffset() + getCursorOffset();
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004717 mPositionX += mTextView.viewportToContentHorizontalOffset();
4718 mPositionHasChanged = true;
4719 updatePosition(mLastParentX, mLastParentY, false, false);
Keisuke Kuroyanagi33f81ac2015-05-14 20:10:57 +09004720 postInvalidate();
4721 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004722 }
4723
4724 protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
Adam Powell3fceabd2014-08-19 18:28:04 -07004725 protected abstract int getHorizontalGravity(boolean isRtlRun);
Gilles Debunned88876a2012-03-16 17:34:04 -07004726
4727 // Touch-up filter: number of previous positions remembered
4728 private static final int HISTORY_SIZE = 5;
4729 private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
4730 private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
4731 private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
4732 private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
4733 private int mPreviousOffsetIndex = 0;
4734 private int mNumberPreviousOffsets = 0;
4735
4736 private void startTouchUpFilter(int offset) {
4737 mNumberPreviousOffsets = 0;
4738 addPositionToTouchUpFilter(offset);
4739 }
4740
4741 private void addPositionToTouchUpFilter(int offset) {
4742 mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
4743 mPreviousOffsets[mPreviousOffsetIndex] = offset;
4744 mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
4745 mNumberPreviousOffsets++;
4746 }
4747
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004748 private void filterOnTouchUp(boolean fromTouchScreen) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004749 final long now = SystemClock.uptimeMillis();
4750 int i = 0;
4751 int index = mPreviousOffsetIndex;
4752 final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
4753 while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
4754 i++;
4755 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
4756 }
4757
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004758 if (i > 0 && i < iMax
4759 && (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004760 positionAtCursorOffset(mPreviousOffsets[index], false, fromTouchScreen);
Gilles Debunned88876a2012-03-16 17:34:04 -07004761 }
4762 }
4763
4764 public boolean offsetHasBeenChanged() {
4765 return mNumberPreviousOffsets > 1;
4766 }
4767
4768 @Override
4769 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Adam Powell3fceabd2014-08-19 18:28:04 -07004770 setMeasuredDimension(getPreferredWidth(), getPreferredHeight());
4771 }
4772
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004773 @Override
4774 public void invalidate() {
4775 super.invalidate();
4776 if (isShowing()) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004777 positionAtCursorOffset(getCurrentCursorOffset(), true, false);
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004778 }
4779 };
4780
Shu Chen9d744e52020-01-22 14:28:08 +08004781 protected final int getPreferredWidth() {
Adam Powell3fceabd2014-08-19 18:28:04 -07004782 return Math.max(mDrawable.getIntrinsicWidth(), mMinSize);
4783 }
4784
Shu Chen9d744e52020-01-22 14:28:08 +08004785 protected final int getPreferredHeight() {
Adam Powell3fceabd2014-08-19 18:28:04 -07004786 return Math.max(mDrawable.getIntrinsicHeight(), mMinSize);
Gilles Debunned88876a2012-03-16 17:34:04 -07004787 }
4788
4789 public void show() {
Nikita Dubrovsky05cfcc82019-10-24 08:57:32 -07004790 if (TextView.DEBUG_CURSOR) {
4791 logCursor(getClass().getSimpleName() + ": HandleView: show()", "offset=%s",
4792 getCurrentCursorOffset());
4793 }
4794
Gilles Debunned88876a2012-03-16 17:34:04 -07004795 if (isShowing()) return;
4796
4797 getPositionListener().addSubscriber(this, true /* local position may change */);
4798
4799 // Make sure the offset is always considered new, even when focusing at same position
4800 mPreviousOffset = -1;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004801 positionAtCursorOffset(getCurrentCursorOffset(), false, false);
Gilles Debunned88876a2012-03-16 17:34:04 -07004802 }
4803
4804 protected void dismiss() {
4805 mIsDragging = false;
4806 mContainer.dismiss();
4807 onDetached();
4808 }
4809
4810 public void hide() {
Nikita Dubrovsky05cfcc82019-10-24 08:57:32 -07004811 if (TextView.DEBUG_CURSOR) {
4812 logCursor(getClass().getSimpleName() + ": HandleView: hide()", "offset=%s",
4813 getCurrentCursorOffset());
4814 }
4815
Gilles Debunned88876a2012-03-16 17:34:04 -07004816 dismiss();
4817
4818 getPositionListener().removeSubscriber(this);
4819 }
4820
Gilles Debunned88876a2012-03-16 17:34:04 -07004821 public boolean isShowing() {
4822 return mContainer.isShowing();
4823 }
4824
Mihai Popab1b423a2018-03-27 19:03:09 +01004825 private boolean shouldShow() {
4826 // A dragging handle should always be shown.
Gilles Debunned88876a2012-03-16 17:34:04 -07004827 if (mIsDragging) {
4828 return true;
4829 }
4830
4831 if (mTextView.isInBatchEditMode()) {
4832 return false;
4833 }
4834
Phil Weaverc2e28932016-12-08 12:29:25 -08004835 return mTextView.isPositionVisible(
4836 mPositionX + mHotspotX + getHorizontalOffset(), mPositionY);
Gilles Debunned88876a2012-03-16 17:34:04 -07004837 }
4838
Mihai Popab1b423a2018-03-27 19:03:09 +01004839 private void setVisible(final boolean visible) {
4840 mContainer.getContentView().setVisibility(visible ? VISIBLE : INVISIBLE);
4841 }
4842
Gilles Debunned88876a2012-03-16 17:34:04 -07004843 public abstract int getCurrentCursorOffset();
4844
4845 protected abstract void updateSelection(int offset);
4846
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004847 protected abstract void updatePosition(float x, float y, boolean fromTouchScreen);
Gilles Debunned88876a2012-03-16 17:34:04 -07004848
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004849 @MagnifierHandleTrigger
4850 protected abstract int getMagnifierHandleTrigger();
4851
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004852 protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
4853 return layout.isRtlCharAt(offset);
4854 }
4855
4856 @VisibleForTesting
4857 public float getHorizontal(@NonNull Layout layout, int offset) {
4858 return layout.getPrimaryHorizontal(offset);
4859 }
4860
4861 protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
4862 return mTextView.getOffsetAtCoordinate(line, x);
4863 }
4864
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004865 /**
4866 * @param offset Cursor offset. Must be in [-1, length].
4867 * @param forceUpdatePosition whether to force update the position. This should be true
4868 * when If the parent has been scrolled, for example.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004869 * @param fromTouchScreen {@code true} if the cursor is moved with motion events from the
4870 * touch screen.
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004871 */
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004872 protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
4873 boolean fromTouchScreen) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004874 // A HandleView relies on the layout, which may be nulled by external methods
4875 Layout layout = mTextView.getLayout();
4876 if (layout == null) {
4877 // Will update controllers' state, hiding them and stopping selection mode if needed
4878 prepareCursorControllers();
4879 return;
4880 }
Siyamed Sinir987ec652016-02-17 19:44:41 -08004881 layout = mTextView.getLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -07004882
4883 boolean offsetChanged = offset != mPreviousOffset;
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004884 if (offsetChanged || forceUpdatePosition) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004885 if (offsetChanged) {
4886 updateSelection(offset);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004887 if (fromTouchScreen && mHapticTextHandleEnabled) {
4888 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
4889 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004890 addPositionToTouchUpFilter(offset);
4891 }
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07004892 final int line = layout.getLineForOffset(offset);
Mady Mellorb9bbbb12015-03-23 11:50:46 -07004893 mPrevLine = line;
Gilles Debunned88876a2012-03-16 17:34:04 -07004894
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004895 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
4896 - getHorizontalOffset() + getCursorOffset();
Siyamed Sinira60b59d2017-07-26 09:26:41 -07004897 mPositionY = layout.getLineBottomWithoutSpacing(line);
Gilles Debunned88876a2012-03-16 17:34:04 -07004898
4899 // Take TextView's padding and scroll into account.
4900 mPositionX += mTextView.viewportToContentHorizontalOffset();
4901 mPositionY += mTextView.viewportToContentVerticalOffset();
4902
4903 mPreviousOffset = offset;
4904 mPositionHasChanged = true;
4905 }
4906 }
4907
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004908 /**
Roozbeh Pournader9c133072017-07-26 22:36:27 -07004909 * Return the clamped horizontal position for the cursor.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004910 *
4911 * @param layout Text layout.
4912 * @param offset Character offset for the cursor.
4913 * @return The clamped horizontal position for the cursor.
4914 */
4915 int getCursorHorizontalPosition(Layout layout, int offset) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004916 return (int) (getHorizontal(layout, offset) - 0.5f);
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004917 }
4918
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004919 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07004920 public void updatePosition(int parentPositionX, int parentPositionY,
4921 boolean parentPositionChanged, boolean parentScrolled) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004922 positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled, false);
Gilles Debunned88876a2012-03-16 17:34:04 -07004923 if (parentPositionChanged || mPositionHasChanged) {
4924 if (mIsDragging) {
4925 // Update touchToWindow offset in case of parent scrolling while dragging
4926 if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
4927 mTouchToWindowOffsetX += parentPositionX - mLastParentX;
4928 mTouchToWindowOffsetY += parentPositionY - mLastParentY;
4929 mLastParentX = parentPositionX;
4930 mLastParentY = parentPositionY;
4931 }
4932
4933 onHandleMoved();
4934 }
4935
Mihai Popab1b423a2018-03-27 19:03:09 +01004936 if (shouldShow()) {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004937 // Transform to the window coordinates to follow the view tranformation.
4938 final int[] pts = { mPositionX + mHotspotX + getHorizontalOffset(), mPositionY};
4939 mTextView.transformFromViewToWindowSpace(pts);
4940 pts[0] -= mHotspotX + getHorizontalOffset();
4941
Gilles Debunned88876a2012-03-16 17:34:04 -07004942 if (isShowing()) {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004943 mContainer.update(pts[0], pts[1], -1, -1);
Gilles Debunned88876a2012-03-16 17:34:04 -07004944 } else {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004945 mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY, pts[0], pts[1]);
Gilles Debunned88876a2012-03-16 17:34:04 -07004946 }
4947 } else {
4948 if (isShowing()) {
4949 dismiss();
4950 }
4951 }
4952
4953 mPositionHasChanged = false;
4954 }
4955 }
4956
4957 @Override
4958 protected void onDraw(Canvas c) {
Adam Powell3fceabd2014-08-19 18:28:04 -07004959 final int drawWidth = mDrawable.getIntrinsicWidth();
4960 final int left = getHorizontalOffset();
4961
4962 mDrawable.setBounds(left, 0, left + drawWidth, mDrawable.getIntrinsicHeight());
Gilles Debunned88876a2012-03-16 17:34:04 -07004963 mDrawable.draw(c);
4964 }
4965
Adam Powell3fceabd2014-08-19 18:28:04 -07004966 private int getHorizontalOffset() {
4967 final int width = getPreferredWidth();
4968 final int drawWidth = mDrawable.getIntrinsicWidth();
4969 final int left;
4970 switch (mHorizontalGravity) {
4971 case Gravity.LEFT:
4972 left = 0;
4973 break;
4974 default:
4975 case Gravity.CENTER:
4976 left = (width - drawWidth) / 2;
4977 break;
4978 case Gravity.RIGHT:
4979 left = width - drawWidth;
4980 break;
4981 }
4982 return left;
4983 }
4984
4985 protected int getCursorOffset() {
4986 return 0;
4987 }
4988
Mihai Popab1b423a2018-03-27 19:03:09 +01004989 private boolean tooLargeTextForMagnifier() {
Shu Chend931a472020-02-14 14:25:14 +08004990 if (mNewMagnifierEnabled) {
4991 Layout layout = mTextView.getLayout();
4992 final int line = layout.getLineForOffset(getCurrentCursorOffset());
4993 return layout.getLineBottomWithoutSpacing(line) - layout.getLineTop(line)
4994 >= mMaxLineHeightForMagnifier;
4995 }
Mihai Popab1b423a2018-03-27 19:03:09 +01004996 final float magnifierContentHeight = Math.round(
4997 mMagnifierAnimator.mMagnifier.getHeight()
4998 / mMagnifierAnimator.mMagnifier.getZoom());
4999 final Paint.FontMetrics fontMetrics = mTextView.getPaint().getFontMetrics();
5000 final float glyphHeight = fontMetrics.descent - fontMetrics.ascent;
Mihai Popa6d26d152019-01-30 15:36:47 +00005001 return glyphHeight * mTextViewScaleY > magnifierContentHeight;
Mihai Popab1b423a2018-03-27 19:03:09 +01005002 }
5003
Mihai Popa6d26d152019-01-30 15:36:47 +00005004 /**
5005 * Traverses the hierarchy above the text view, and computes the total scale applied
5006 * to it. If a rotation is encountered, the method returns {@code false}, indicating
5007 * that the magnifier should not be shown anyways. It would be nice to keep these two
5008 * pieces of logic separate (the rotation check and the total scale calculation),
5009 * but for efficiency we can do them in a single go.
5010 * @return whether the text view is rotated
5011 */
5012 private boolean checkForTransforms() {
Mihai Popaddf9fe02018-09-28 13:54:19 +01005013 if (mMagnifierAnimator.mMagnifierIsShowing) {
5014 // Do not check again when the magnifier is currently showing.
Mihai Popaddf9fe02018-09-28 13:54:19 +01005015 return true;
5016 }
Mihai Popa6d26d152019-01-30 15:36:47 +00005017
5018 if (mTextView.getRotation() != 0f || mTextView.getRotationX() != 0f
5019 || mTextView.getRotationY() != 0f) {
5020 return false;
5021 }
5022 mTextViewScaleX = mTextView.getScaleX();
5023 mTextViewScaleY = mTextView.getScaleY();
5024
Mihai Popaddf9fe02018-09-28 13:54:19 +01005025 ViewParent viewParent = mTextView.getParent();
5026 while (viewParent != null) {
Mihai Popa6d26d152019-01-30 15:36:47 +00005027 if (viewParent instanceof View) {
5028 final View view = (View) viewParent;
5029 if (view.getRotation() != 0f || view.getRotationX() != 0f
5030 || view.getRotationY() != 0f) {
5031 return false;
5032 }
5033 mTextViewScaleX *= view.getScaleX();
5034 mTextViewScaleY *= view.getScaleY();
Mihai Popaddf9fe02018-09-28 13:54:19 +01005035 }
5036 viewParent = viewParent.getParent();
5037 }
Mihai Popa6d26d152019-01-30 15:36:47 +00005038 return true;
Mihai Popaddf9fe02018-09-28 13:54:19 +01005039 }
5040
Mihai Popae3017462018-03-07 12:25:21 +00005041 /**
5042 * Computes the position where the magnifier should be shown, relative to
5043 * {@code mTextView}, and writes them to {@code showPosInView}. Also decides
5044 * whether the magnifier should be shown or dismissed after this touch event.
5045 * @return Whether the magnifier should be shown at the computed coordinates or dismissed.
5046 */
5047 private boolean obtainMagnifierShowCoordinates(@NonNull final MotionEvent event,
5048 final PointF showPosInView) {
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005049
5050 final int trigger = getMagnifierHandleTrigger();
5051 final int offset;
Mihai Popa27e4dfb2018-03-07 14:52:05 +00005052 final int otherHandleOffset;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005053 switch (trigger) {
Mihai Popa27e4dfb2018-03-07 14:52:05 +00005054 case MagnifierHandleTrigger.INSERTION:
5055 offset = mTextView.getSelectionStart();
5056 otherHandleOffset = -1;
5057 break;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005058 case MagnifierHandleTrigger.SELECTION_START:
5059 offset = mTextView.getSelectionStart();
Mihai Popa27e4dfb2018-03-07 14:52:05 +00005060 otherHandleOffset = mTextView.getSelectionEnd();
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005061 break;
5062 case MagnifierHandleTrigger.SELECTION_END:
5063 offset = mTextView.getSelectionEnd();
Mihai Popa27e4dfb2018-03-07 14:52:05 +00005064 otherHandleOffset = mTextView.getSelectionStart();
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005065 break;
5066 default:
5067 offset = -1;
Mihai Popa27e4dfb2018-03-07 14:52:05 +00005068 otherHandleOffset = -1;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005069 break;
5070 }
5071
5072 if (offset == -1) {
Mihai Popae3017462018-03-07 12:25:21 +00005073 return false;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005074 }
5075
Mihai Popa6d26d152019-01-30 15:36:47 +00005076 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
5077 mCurrentDragInitialTouchRawX = event.getRawX();
5078 } else if (event.getActionMasked() == MotionEvent.ACTION_UP) {
5079 mCurrentDragInitialTouchRawX = UNSET_X_VALUE;
5080 }
5081
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005082 final Layout layout = mTextView.getLayout();
5083 final int lineNumber = layout.getLineForOffset(offset);
Mihai Popa27e4dfb2018-03-07 14:52:05 +00005084 // Compute whether the selection handles are currently on the same line, and,
5085 // in this particular case, whether the selected text is right to left.
5086 final boolean sameLineSelection = otherHandleOffset != -1
5087 && lineNumber == layout.getLineForOffset(otherHandleOffset);
5088 final boolean rtl = sameLineSelection
5089 && (offset < otherHandleOffset)
5090 != (getHorizontal(mTextView.getLayout(), offset)
5091 < getHorizontal(mTextView.getLayout(), otherHandleOffset));
Mihai Popae3017462018-03-07 12:25:21 +00005092
Mihai Popa27e4dfb2018-03-07 14:52:05 +00005093 // Horizontally move the magnifier smoothly, clamp inside the current line / selection.
Mihai Popa1d1ed0c2018-01-12 12:38:12 +00005094 final int[] textViewLocationOnScreen = new int[2];
5095 mTextView.getLocationOnScreen(textViewLocationOnScreen);
Mihai Popae3017462018-03-07 12:25:21 +00005096 final float touchXInView = event.getRawX() - textViewLocationOnScreen[0];
Mihai Popa27e4dfb2018-03-07 14:52:05 +00005097 float leftBound = mTextView.getTotalPaddingLeft() - mTextView.getScrollX();
5098 float rightBound = mTextView.getTotalPaddingLeft() - mTextView.getScrollX();
5099 if (sameLineSelection && ((trigger == MagnifierHandleTrigger.SELECTION_END) ^ rtl)) {
5100 leftBound += getHorizontal(mTextView.getLayout(), otherHandleOffset);
5101 } else {
5102 leftBound += mTextView.getLayout().getLineLeft(lineNumber);
5103 }
5104 if (sameLineSelection && ((trigger == MagnifierHandleTrigger.SELECTION_START) ^ rtl)) {
5105 rightBound += getHorizontal(mTextView.getLayout(), otherHandleOffset);
5106 } else {
5107 rightBound += mTextView.getLayout().getLineRight(lineNumber);
5108 }
Mihai Popa6d26d152019-01-30 15:36:47 +00005109 leftBound *= mTextViewScaleX;
5110 rightBound *= mTextViewScaleX;
Mihai Popa38722382018-03-07 19:56:21 +00005111 final float contentWidth = Math.round(mMagnifierAnimator.mMagnifier.getWidth()
5112 / mMagnifierAnimator.mMagnifier.getZoom());
Mihai Popa27e4dfb2018-03-07 14:52:05 +00005113 if (touchXInView < leftBound - contentWidth / 2
5114 || touchXInView > rightBound + contentWidth / 2) {
5115 // The touch is too far from the current line / selection, so hide the magnifier.
Mihai Popae3017462018-03-07 12:25:21 +00005116 return false;
5117 }
Mihai Popa6d26d152019-01-30 15:36:47 +00005118
5119 final float scaledTouchXInView;
5120 if (mTextViewScaleX == 1f) {
5121 // In the common case, do not use mCurrentDragInitialTouchRawX to compute this
5122 // coordinate, although the formula on the else branch should be equivalent.
5123 // Since the formula relies on mCurrentDragInitialTouchRawX being set on
5124 // MotionEvent.ACTION_DOWN, this makes us more defensive against cases when
5125 // the sequence of events might not look as expected: for example, a sequence of
5126 // ACTION_MOVE not preceded by ACTION_DOWN.
5127 scaledTouchXInView = touchXInView;
5128 } else {
5129 scaledTouchXInView = (event.getRawX() - mCurrentDragInitialTouchRawX)
5130 * mTextViewScaleX + mCurrentDragInitialTouchRawX
5131 - textViewLocationOnScreen[0];
5132 }
5133 showPosInView.x = Math.max(leftBound, Math.min(rightBound, scaledTouchXInView));
Mihai Popae3017462018-03-07 12:25:21 +00005134
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005135 // Vertically snap to middle of current line.
Mihai Popa6d26d152019-01-30 15:36:47 +00005136 showPosInView.y = ((mTextView.getLayout().getLineTop(lineNumber)
Shu Chen09ce0f12020-02-04 15:27:47 +08005137 + mTextView.getLayout().getLineBottomWithoutSpacing(lineNumber)) / 2.0f
Mihai Popa6d26d152019-01-30 15:36:47 +00005138 + mTextView.getTotalPaddingTop() - mTextView.getScrollY()) * mTextViewScaleY;
Mihai Popae3017462018-03-07 12:25:21 +00005139 return true;
5140 }
Mihai Popaa4e39c42018-02-20 15:31:11 +00005141
Mihai Popa63ee7f12018-04-05 12:01:53 +01005142 private boolean handleOverlapsMagnifier(@NonNull final HandleView handle,
5143 @NonNull final Rect magnifierRect) {
5144 final PopupWindow window = handle.mContainer;
5145 if (!window.hasDecorView()) {
5146 return false;
5147 }
5148 final Rect handleRect = new Rect(
5149 window.getDecorViewLayoutParams().x,
5150 window.getDecorViewLayoutParams().y,
5151 window.getDecorViewLayoutParams().x + window.getContentView().getWidth(),
5152 window.getDecorViewLayoutParams().y + window.getContentView().getHeight());
5153 return Rect.intersects(handleRect, magnifierRect);
Mihai Popa894469c2018-03-21 19:45:06 +00005154 }
5155
Mihai Popa63ee7f12018-04-05 12:01:53 +01005156 private @Nullable HandleView getOtherSelectionHandle() {
5157 final SelectionModifierCursorController controller = getSelectionController();
5158 if (controller == null || !controller.isActive()) {
5159 return null;
5160 }
5161 return controller.mStartHandle != this
5162 ? controller.mStartHandle
5163 : controller.mEndHandle;
5164 }
5165
Mihai Popac2e0bee2018-07-19 12:18:30 +01005166 private void updateHandlesVisibility() {
5167 final Point magnifierTopLeft = mMagnifierAnimator.mMagnifier.getPosition();
5168 if (magnifierTopLeft == null) {
5169 return;
Mihai Popa63ee7f12018-04-05 12:01:53 +01005170 }
Mihai Popac2e0bee2018-07-19 12:18:30 +01005171 final Rect magnifierRect = new Rect(magnifierTopLeft.x, magnifierTopLeft.y,
5172 magnifierTopLeft.x + mMagnifierAnimator.mMagnifier.getWidth(),
5173 magnifierTopLeft.y + mMagnifierAnimator.mMagnifier.getHeight());
5174 setVisible(!handleOverlapsMagnifier(HandleView.this, magnifierRect));
5175 final HandleView otherHandle = getOtherSelectionHandle();
5176 if (otherHandle != null) {
5177 otherHandle.setVisible(!handleOverlapsMagnifier(otherHandle, magnifierRect));
5178 }
5179 }
Mihai Popa63ee7f12018-04-05 12:01:53 +01005180
Mihai Popae3017462018-03-07 12:25:21 +00005181 protected final void updateMagnifier(@NonNull final MotionEvent event) {
Shu Chen847583c2020-01-22 09:18:40 +08005182 if (getMagnifierAnimator() == null) {
Mihai Popae3017462018-03-07 12:25:21 +00005183 return;
5184 }
5185
5186 final PointF showPosInView = new PointF();
Mihai Popa6d26d152019-01-30 15:36:47 +00005187 final boolean shouldShow = checkForTransforms() /*check not rotated and compute scale*/
5188 && !tooLargeTextForMagnifier()
Mihai Popa894469c2018-03-21 19:45:06 +00005189 && obtainMagnifierShowCoordinates(event, showPosInView);
Mihai Popae3017462018-03-07 12:25:21 +00005190 if (shouldShow) {
5191 // Make the cursor visible and stop blinking.
5192 mRenderCursorRegardlessTiming = true;
5193 mTextView.invalidateCursorPath();
5194 suspendBlink();
Mihai Popab1b423a2018-03-27 19:03:09 +01005195
Shu Chen9d744e52020-01-22 14:28:08 +08005196 if (mNewMagnifierEnabled) {
Shu Chen847583c2020-01-22 09:18:40 +08005197 // Calculates the line bounds as the content source bounds to the magnifier.
5198 Layout layout = mTextView.getLayout();
5199 int line = layout.getLineForOffset(getCurrentCursorOffset());
5200 int lineLeft = (int) layout.getLineLeft(line);
5201 lineLeft += mTextView.getTotalPaddingLeft() - mTextView.getScrollX();
5202 int lineRight = (int) layout.getLineRight(line);
Shu Chen5b14fb52020-01-23 13:15:18 +08005203 lineRight += mTextView.getTotalPaddingLeft() - mTextView.getScrollX();
Shu Chen847583c2020-01-22 09:18:40 +08005204 mMagnifierAnimator.mMagnifier.setSourceHorizontalBounds(lineLeft, lineRight);
Shu Chend931a472020-02-14 14:25:14 +08005205 final int lineHeight =
5206 layout.getLineBottomWithoutSpacing(line) - layout.getLineTop(line);
5207 float zoom = mInitialZoom;
5208 if (lineHeight < mMinLineHeightForMagnifier) {
5209 zoom = zoom * mMinLineHeightForMagnifier / lineHeight;
5210 }
5211 mMagnifierAnimator.mMagnifier.updateSourceFactors(lineHeight, zoom);
Shu Chen59bccf22020-02-07 11:17:29 +08005212 mMagnifierAnimator.mMagnifier.show(showPosInView.x, showPosInView.y);
5213 } else {
Shu Chend931a472020-02-14 14:25:14 +08005214 mMagnifierAnimator.show(showPosInView.x, showPosInView.y);
Shu Chen847583c2020-01-22 09:18:40 +08005215 }
Mihai Popac2e0bee2018-07-19 12:18:30 +01005216 updateHandlesVisibility();
Mihai Popae3017462018-03-07 12:25:21 +00005217 } else {
5218 dismissMagnifier();
5219 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005220 }
5221
5222 protected final void dismissMagnifier() {
Mihai Popa38722382018-03-07 19:56:21 +00005223 if (mMagnifierAnimator != null) {
5224 mMagnifierAnimator.dismiss();
Mihai Popaa4e39c42018-02-20 15:31:11 +00005225 mRenderCursorRegardlessTiming = false;
Andrei Stingaceanu451f9472017-10-13 16:41:28 +01005226 resumeBlink();
Mihai Popab1b423a2018-03-27 19:03:09 +01005227 setVisible(true);
Mihai Popa63ee7f12018-04-05 12:01:53 +01005228 final HandleView otherHandle = getOtherSelectionHandle();
5229 if (otherHandle != null) {
5230 otherHandle.setVisible(true);
5231 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005232 }
5233 }
5234
Gilles Debunned88876a2012-03-16 17:34:04 -07005235 @Override
5236 public boolean onTouchEvent(MotionEvent ev) {
Nikita Dubrovsky05cfcc82019-10-24 08:57:32 -07005237 if (TextView.DEBUG_CURSOR) {
5238 logCursor(this.getClass().getSimpleName() + ": HandleView: onTouchEvent",
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08005239 "%d: %s (%f,%f)",
5240 ev.getSequenceNumber(),
5241 MotionEvent.actionToString(ev.getActionMasked()),
5242 ev.getX(), ev.getY());
Nikita Dubrovsky05cfcc82019-10-24 08:57:32 -07005243 }
5244
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01005245 updateFloatingToolbarVisibility(ev);
5246
Gilles Debunned88876a2012-03-16 17:34:04 -07005247 switch (ev.getActionMasked()) {
5248 case MotionEvent.ACTION_DOWN: {
5249 startTouchUpFilter(getCurrentCursorOffset());
Gilles Debunned88876a2012-03-16 17:34:04 -07005250
5251 final PositionListener positionListener = getPositionListener();
5252 mLastParentX = positionListener.getPositionX();
5253 mLastParentY = positionListener.getPositionY();
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09005254 mLastParentXOnScreen = positionListener.getPositionXOnScreen();
5255 mLastParentYOnScreen = positionListener.getPositionYOnScreen();
5256
5257 final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
5258 final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
5259 mTouchToWindowOffsetX = xInWindow - mPositionX;
5260 mTouchToWindowOffsetY = yInWindow - mPositionY;
5261
Gilles Debunned88876a2012-03-16 17:34:04 -07005262 mIsDragging = true;
Mady Mellora6a0f782015-07-10 16:43:32 -07005263 mPreviousLineTouched = UNSET_LINE;
Gilles Debunned88876a2012-03-16 17:34:04 -07005264 break;
5265 }
5266
5267 case MotionEvent.ACTION_MOVE: {
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09005268 final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
5269 final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005270
5271 // Vertical hysteresis: vertical down movement tends to snap to ideal offset
5272 final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09005273 final float currentVerticalOffset = yInWindow - mPositionY - mLastParentY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005274 float newVerticalOffset;
5275 if (previousVerticalOffset < mIdealVerticalOffset) {
5276 newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
5277 newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
5278 } else {
5279 newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
5280 newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
5281 }
5282 mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
5283
Keisuke Kuroyanagibc89a5c2015-05-18 14:49:29 +09005284 final float newPosX =
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09005285 xInWindow - mTouchToWindowOffsetX + mHotspotX + getHorizontalOffset();
5286 final float newPosY = yInWindow - mTouchToWindowOffsetY + mTouchOffsetY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005287
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005288 updatePosition(newPosX, newPosY,
5289 ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Gilles Debunned88876a2012-03-16 17:34:04 -07005290 break;
5291 }
5292
5293 case MotionEvent.ACTION_UP:
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005294 filterOnTouchUp(ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005295 // Fall through.
Gilles Debunned88876a2012-03-16 17:34:04 -07005296 case MotionEvent.ACTION_CANCEL:
5297 mIsDragging = false;
Mihai Popa6315a322018-10-17 17:39:57 +01005298 updateDrawable(false /* updateDrawableWhenDragging */);
Gilles Debunned88876a2012-03-16 17:34:04 -07005299 break;
5300 }
5301 return true;
5302 }
5303
5304 public boolean isDragging() {
5305 return mIsDragging;
5306 }
5307
Clara Bayarri6351e662015-03-16 23:17:59 +00005308 void onHandleMoved() {}
Gilles Debunned88876a2012-03-16 17:34:04 -07005309
Clara Bayarri6351e662015-03-16 23:17:59 +00005310 public void onDetached() {}
Adam Powell86241212019-06-10 08:38:49 -07005311
5312 @Override
5313 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
5314 super.onSizeChanged(w, h, oldw, oldh);
5315 setSystemGestureExclusionRects(Collections.singletonList(new Rect(0, 0, w, h)));
5316 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005317 }
5318
5319 private class InsertionHandleView extends HandleView {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01005320 // Used to detect taps on the insertion handle, which will affect the insertion action mode
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08005321 private float mLastDownRawX, mLastDownRawY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005322 private Runnable mHider;
5323
Shu Chen38451192020-01-21 14:32:32 +08005324 // Members for fake-dismiss effect in touch through mode.
5325 // It is to make InsertionHandleView can receive the MOVE/UP events after calling dismiss(),
5326 // which could happen in case of long-press (making selection will dismiss the insertion
5327 // handle).
5328
5329 // Whether the finger is down and hasn't been up yet.
5330 private boolean mIsTouchDown = false;
5331 // Whether the popup window is in the invisible state and will be dismissed when finger up.
5332 private boolean mPendingDismissOnUp = false;
5333 // The alpha value of the drawable.
Shu Chen9d744e52020-01-22 14:28:08 +08005334 private final int mDrawableOpacity;
Shu Chen38451192020-01-21 14:32:32 +08005335
5336 // Members for toggling the insertion menu in touch through mode.
5337
5338 // The coordinate for the touch down event, which is used for transforming the coordinates
5339 // of the events to the text view.
5340 private float mTouchDownX;
5341 private float mTouchDownY;
5342 // The cursor offset when touch down. This is to detect whether the cursor is moved when
5343 // finger move/up.
5344 private int mOffsetDown;
5345 // Whether the cursor offset has been changed on the move/up events.
5346 private boolean mOffsetChanged;
5347 // Whether it is in insertion action mode when finger down.
5348 private boolean mIsInActionMode;
5349 // The timestamp for the last up event, which is used for double tap detection.
5350 private long mLastUpTime;
Shu Chen38451192020-01-21 14:32:32 +08005351
Shu Chen9d744e52020-01-22 14:28:08 +08005352 // The delta height applied to the insertion handle view.
5353 private final int mDeltaHeight;
5354
5355 InsertionHandleView(Drawable drawable) {
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09005356 super(drawable, drawable, com.android.internal.R.id.insertion_handle);
Shu Chen9d744e52020-01-22 14:28:08 +08005357
5358 int deltaHeight = 0;
5359 int opacity = 255;
Nikita Dubrovskyac919b02020-02-18 09:39:20 -08005360 if (mFlagInsertionHandleGesturesEnabled) {
Shu Chen9d744e52020-01-22 14:28:08 +08005361 deltaHeight = AppGlobals.getIntCoreSetting(
Nikita Dubrovsky9e139172020-02-18 16:23:17 -08005362 WidgetFlags.KEY_INSERTION_HANDLE_DELTA_HEIGHT,
5363 WidgetFlags.INSERTION_HANDLE_DELTA_HEIGHT_DEFAULT);
Shu Chen9d744e52020-01-22 14:28:08 +08005364 opacity = AppGlobals.getIntCoreSetting(
Nikita Dubrovsky9e139172020-02-18 16:23:17 -08005365 WidgetFlags.KEY_INSERTION_HANDLE_OPACITY,
5366 WidgetFlags.INSERTION_HANDLE_OPACITY_DEFAULT);
Shu Chen9d744e52020-01-22 14:28:08 +08005367 // Avoid invalid/unsupported values.
5368 if (deltaHeight < -25 || deltaHeight > 50) {
5369 deltaHeight = 25;
5370 }
5371 if (opacity < 10 || opacity > 100) {
5372 opacity = 50;
5373 }
Shu Chen523d33f2020-02-13 10:18:29 +08005374 // Converts the opacity value from range {0..100} to {0..255}.
5375 opacity = opacity * 255 / 100;
Shu Chen9d744e52020-01-22 14:28:08 +08005376 }
5377 mDeltaHeight = deltaHeight;
5378 mDrawableOpacity = opacity;
Gilles Debunned88876a2012-03-16 17:34:04 -07005379 }
5380
Gilles Debunned88876a2012-03-16 17:34:04 -07005381 private void hideAfterDelay() {
5382 if (mHider == null) {
5383 mHider = new Runnable() {
5384 public void run() {
5385 hide();
5386 }
5387 };
5388 } else {
5389 removeHiderCallback();
5390 }
5391 mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
5392 }
5393
5394 private void removeHiderCallback() {
5395 if (mHider != null) {
5396 mTextView.removeCallbacks(mHider);
5397 }
5398 }
5399
5400 @Override
5401 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
5402 return drawable.getIntrinsicWidth() / 2;
5403 }
5404
5405 @Override
Adam Powell3fceabd2014-08-19 18:28:04 -07005406 protected int getHorizontalGravity(boolean isRtlRun) {
5407 return Gravity.CENTER_HORIZONTAL;
5408 }
5409
5410 @Override
5411 protected int getCursorOffset() {
5412 int offset = super.getCursorOffset();
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07005413 if (mDrawableForCursor != null) {
5414 mDrawableForCursor.getPadding(mTempRect);
5415 offset += (mDrawableForCursor.getIntrinsicWidth()
Roozbeh Pournader9c133072017-07-26 22:36:27 -07005416 - mTempRect.left - mTempRect.right) / 2;
Adam Powell3fceabd2014-08-19 18:28:04 -07005417 }
5418 return offset;
5419 }
5420
5421 @Override
Siyamed Sinir217c0f72016-02-01 18:30:02 -08005422 int getCursorHorizontalPosition(Layout layout, int offset) {
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07005423 if (mDrawableForCursor != null) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005424 final float horizontal = getHorizontal(layout, offset);
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07005425 return clampHorizontalPosition(mDrawableForCursor, horizontal) + mTempRect.left;
Siyamed Sinir217c0f72016-02-01 18:30:02 -08005426 }
5427 return super.getCursorHorizontalPosition(layout, offset);
5428 }
5429
5430 @Override
Shu Chen9d744e52020-01-22 14:28:08 +08005431 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Nikita Dubrovskyac919b02020-02-18 09:39:20 -08005432 if (mFlagInsertionHandleGesturesEnabled) {
Shu Chen9d744e52020-01-22 14:28:08 +08005433 final int height = Math.max(
5434 getPreferredHeight() + mDeltaHeight, mDrawable.getIntrinsicHeight());
5435 setMeasuredDimension(getPreferredWidth(), height);
5436 return;
5437 }
5438 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
5439 }
5440
5441 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07005442 public boolean onTouchEvent(MotionEvent ev) {
Shu Cheneb8b1ba2020-04-04 14:46:50 +08005443 if (!mTextView.isFromPrimePointer(ev, true)) {
5444 return true;
5445 }
Nikita Dubrovskyac919b02020-02-18 09:39:20 -08005446 if (mFlagInsertionHandleGesturesEnabled && mFlagCursorDragFromAnywhereEnabled) {
Shu Chen38451192020-01-21 14:32:32 +08005447 // Should only enable touch through when cursor drag is enabled.
5448 // Otherwise the insertion handle view cannot be moved.
5449 return touchThrough(ev);
5450 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005451 final boolean result = super.onTouchEvent(ev);
5452
5453 switch (ev.getActionMasked()) {
5454 case MotionEvent.ACTION_DOWN:
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08005455 mLastDownRawX = ev.getRawX();
5456 mLastDownRawY = ev.getRawY();
Mihai Popae3017462018-03-07 12:25:21 +00005457 updateMagnifier(ev);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005458 break;
5459
5460 case MotionEvent.ACTION_MOVE:
Mihai Popae3017462018-03-07 12:25:21 +00005461 updateMagnifier(ev);
Gilles Debunned88876a2012-03-16 17:34:04 -07005462 break;
5463
5464 case MotionEvent.ACTION_UP:
5465 if (!offsetHasBeenChanged()) {
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08005466 ViewConfiguration config = ViewConfiguration.get(mTextView.getContext());
5467 boolean isWithinTouchSlop = EditorTouchState.isDistanceWithin(
5468 mLastDownRawX, mLastDownRawY, ev.getRawX(), ev.getRawY(),
5469 config.getScaledTouchSlop());
5470 if (isWithinTouchSlop) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01005471 // Tapping on the handle toggles the insertion action mode.
sallyyuencc02ea32020-02-10 10:45:48 -08005472 toggleInsertionActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07005473 }
Abodunrinwa Tokibcdf0ab2015-04-25 00:11:25 +01005474 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005475 if (mTextActionMode != null) {
5476 mTextActionMode.invalidateContentRect();
Abodunrinwa Tokibcdf0ab2015-04-25 00:11:25 +01005477 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005478 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005479 // Fall through.
Gilles Debunned88876a2012-03-16 17:34:04 -07005480 case MotionEvent.ACTION_CANCEL:
5481 hideAfterDelay();
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005482 dismissMagnifier();
Gilles Debunned88876a2012-03-16 17:34:04 -07005483 break;
5484
5485 default:
5486 break;
5487 }
5488
5489 return result;
5490 }
5491
Shu Chen38451192020-01-21 14:32:32 +08005492 // Handles the touch events in touch through mode.
5493 private boolean touchThrough(MotionEvent ev) {
5494 final int actionType = ev.getActionMasked();
5495 switch (actionType) {
5496 case MotionEvent.ACTION_DOWN:
5497 mIsTouchDown = true;
5498 mOffsetChanged = false;
5499 mOffsetDown = mTextView.getSelectionStart();
5500 mTouchDownX = ev.getX();
5501 mTouchDownY = ev.getY();
5502 mIsInActionMode = mTextActionMode != null;
5503 if (ev.getEventTime() - mLastUpTime < ViewConfiguration.getDoubleTapTimeout()) {
5504 stopTextActionMode(); // Avoid crash when double tap and drag backwards.
5505 }
Shu Chen38451192020-01-21 14:32:32 +08005506 mTouchState.setIsOnHandle(true);
5507 break;
5508 case MotionEvent.ACTION_UP:
5509 mLastUpTime = ev.getEventTime();
5510 break;
5511 }
5512 // Performs the touch through by forward the events to the text view.
5513 boolean ret = mTextView.onTouchEvent(transformEventForTouchThrough(ev));
5514
5515 if (actionType == MotionEvent.ACTION_UP || actionType == MotionEvent.ACTION_CANCEL) {
5516 mIsTouchDown = false;
5517 if (mPendingDismissOnUp) {
5518 dismiss();
5519 }
5520 mTouchState.setIsOnHandle(false);
5521 }
5522
5523 // Checks for cursor offset change.
5524 if (!mOffsetChanged) {
5525 int start = mTextView.getSelectionStart();
5526 int end = mTextView.getSelectionEnd();
5527 if (start != end || mOffsetDown != start) {
5528 mOffsetChanged = true;
5529 }
5530 }
5531
5532 // Toggling the insertion action mode on finger up.
5533 if (!mOffsetChanged && actionType == MotionEvent.ACTION_UP) {
5534 if (mIsInActionMode) {
5535 stopTextActionMode();
5536 } else {
5537 startInsertionActionMode();
5538 }
5539 }
5540 return ret;
5541 }
5542
5543 private MotionEvent transformEventForTouchThrough(MotionEvent ev) {
Shu Chenaf86a482020-02-11 19:53:56 +08005544 final Layout layout = mTextView.getLayout();
5545 final int line = layout.getLineForOffset(getCurrentCursorOffset());
5546 final int textHeight =
5547 layout.getLineBottomWithoutSpacing(line) - layout.getLineTop(line);
Shu Chen38451192020-01-21 14:32:32 +08005548 // Transforms the touch events to screen coordinates.
5549 // And also shift up to make the hit point is on the text.
5550 // Note:
5551 // - The revised X should reflect the distance to the horizontal center of touch down.
5552 // - The revised Y should be at the top of the text.
5553 Matrix m = new Matrix();
5554 m.setTranslate(ev.getRawX() - ev.getX() + (getMeasuredWidth() >> 1) - mTouchDownX,
Shu Chenaf86a482020-02-11 19:53:56 +08005555 ev.getRawY() - ev.getY() - (textHeight >> 1) - mTouchDownY);
Shu Chen38451192020-01-21 14:32:32 +08005556 ev.transform(m);
5557 // Transforms the touch events to text view coordinates.
5558 mTextView.toLocalMotionEvent(ev);
5559 if (TextView.DEBUG_CURSOR) {
5560 logCursor("InsertionHandleView#transformEventForTouchThrough",
5561 "Touch through: %d, (%f, %f)",
5562 ev.getAction(), ev.getX(), ev.getY());
5563 }
5564 return ev;
5565 }
5566
5567 @Override
5568 public boolean isShowing() {
5569 if (mPendingDismissOnUp) {
5570 return false;
5571 }
5572 return super.isShowing();
5573 }
5574
5575 @Override
5576 public void show() {
5577 super.show();
5578 mPendingDismissOnUp = false;
5579 mDrawable.setAlpha(mDrawableOpacity);
5580 }
5581
5582 @Override
5583 public void dismiss() {
5584 if (mIsTouchDown) {
5585 if (TextView.DEBUG_CURSOR) {
5586 logCursor("InsertionHandleView#dismiss",
5587 "Suppressed the real dismiss, only become invisible");
5588 }
5589 mPendingDismissOnUp = true;
5590 mDrawable.setAlpha(0);
5591 } else {
5592 super.dismiss();
5593 mPendingDismissOnUp = false;
5594 }
5595 }
5596
5597 @Override
5598 protected void updateDrawable(final boolean updateDrawableWhenDragging) {
5599 super.updateDrawable(updateDrawableWhenDragging);
5600 mDrawable.setAlpha(mDrawableOpacity);
5601 }
5602
Gilles Debunned88876a2012-03-16 17:34:04 -07005603 @Override
5604 public int getCurrentCursorOffset() {
5605 return mTextView.getSelectionStart();
5606 }
5607
5608 @Override
5609 public void updateSelection(int offset) {
5610 Selection.setSelection((Spannable) mTextView.getText(), offset);
5611 }
5612
5613 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005614 protected void updatePosition(float x, float y, boolean fromTouchScreen) {
Mady Melloree3821e2015-06-05 11:12:01 -07005615 Layout layout = mTextView.getLayout();
5616 int offset;
5617 if (layout != null) {
Mady Mellora6a0f782015-07-10 16:43:32 -07005618 if (mPreviousLineTouched == UNSET_LINE) {
5619 mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
5620 }
5621 int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005622 offset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellora6a0f782015-07-10 16:43:32 -07005623 mPreviousLineTouched = currLine;
Mady Melloree3821e2015-06-05 11:12:01 -07005624 } else {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005625 offset = -1;
Mady Melloree3821e2015-06-05 11:12:01 -07005626 }
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08005627 if (TextView.DEBUG_CURSOR) {
5628 logCursor("InsertionHandleView: updatePosition", "x=%f, y=%f, offset=%d, line=%d",
5629 x, y, offset, mPreviousLineTouched);
5630 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005631 positionAtCursorOffset(offset, false, fromTouchScreen);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005632 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01005633 invalidateActionMode();
Clara Bayarri1baed512015-05-11 15:29:16 +01005634 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005635 }
5636
5637 @Override
5638 void onHandleMoved() {
5639 super.onHandleMoved();
5640 removeHiderCallback();
5641 }
5642
5643 @Override
5644 public void onDetached() {
5645 super.onDetached();
5646 removeHiderCallback();
5647 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005648
5649 @Override
5650 @MagnifierHandleTrigger
5651 protected int getMagnifierHandleTrigger() {
5652 return MagnifierHandleTrigger.INSERTION;
5653 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005654 }
5655
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005656 @Retention(RetentionPolicy.SOURCE)
Jeff Sharkeyce8db992017-12-13 20:05:05 -07005657 @IntDef(prefix = { "HANDLE_TYPE_" }, value = {
5658 HANDLE_TYPE_SELECTION_START,
5659 HANDLE_TYPE_SELECTION_END
5660 })
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005661 public @interface HandleType {}
5662 public static final int HANDLE_TYPE_SELECTION_START = 0;
5663 public static final int HANDLE_TYPE_SELECTION_END = 1;
5664
Abodunrinwa Toki4a056a52017-08-05 01:56:40 +01005665 /** For selection handles */
5666 @VisibleForTesting
5667 public final class SelectionHandleView extends HandleView {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005668 // Indicates the handle type, selection start (HANDLE_TYPE_SELECTION_START) or selection
5669 // end (HANDLE_TYPE_SELECTION_END).
5670 @HandleType
5671 private final int mHandleType;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005672 // Indicates whether the cursor is making adjustments within a word.
5673 private boolean mInWord = false;
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005674 // Difference between touch position and word boundary position.
5675 private float mTouchWordDelta;
Mady Mellore264ac32015-06-22 16:46:29 -07005676 // X value of the previous updatePosition call.
5677 private float mPrevX;
5678 // Indicates if the handle has moved a boundary between LTR and RTL text.
5679 private boolean mLanguageDirectionChanged = false;
Mady Mellor42390aa2015-07-24 13:08:42 -07005680 // Distance from edge of horizontally scrolling text view
5681 // to use to switch to character mode.
5682 private final float mTextViewEdgeSlop;
5683 // Used to save text view location.
5684 private final int[] mTextViewLocation = new int[2];
Gilles Debunned88876a2012-03-16 17:34:04 -07005685
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005686 public SelectionHandleView(Drawable drawableLtr, Drawable drawableRtl, int id,
5687 @HandleType int handleType) {
5688 super(drawableLtr, drawableRtl, id);
5689 mHandleType = handleType;
5690 ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext());
Mady Mellor42390aa2015-07-24 13:08:42 -07005691 mTextViewEdgeSlop = viewConfiguration.getScaledTouchSlop() * 4;
Gilles Debunned88876a2012-03-16 17:34:04 -07005692 }
5693
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005694 private boolean isStartHandle() {
5695 return mHandleType == HANDLE_TYPE_SELECTION_START;
5696 }
5697
Gilles Debunned88876a2012-03-16 17:34:04 -07005698 @Override
5699 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005700 if (isRtlRun == isStartHandle()) {
Mady Mellor709386f2015-05-14 12:41:18 -07005701 return drawable.getIntrinsicWidth() / 4;
5702 } else {
5703 return (drawable.getIntrinsicWidth() * 3) / 4;
5704 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005705 }
5706
5707 @Override
Adam Powell3fceabd2014-08-19 18:28:04 -07005708 protected int getHorizontalGravity(boolean isRtlRun) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005709 return (isRtlRun == isStartHandle()) ? Gravity.LEFT : Gravity.RIGHT;
Adam Powell3fceabd2014-08-19 18:28:04 -07005710 }
5711
5712 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07005713 public int getCurrentCursorOffset() {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005714 return isStartHandle() ? mTextView.getSelectionStart() : mTextView.getSelectionEnd();
Gilles Debunned88876a2012-03-16 17:34:04 -07005715 }
5716
5717 @Override
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005718 protected void updateSelection(int offset) {
5719 if (isStartHandle()) {
5720 Selection.setSelection((Spannable) mTextView.getText(), offset,
5721 mTextView.getSelectionEnd());
5722 } else {
5723 Selection.setSelection((Spannable) mTextView.getText(),
5724 mTextView.getSelectionStart(), offset);
5725 }
Mihai Popa6315a322018-10-17 17:39:57 +01005726 updateDrawable(false /* updateDrawableWhenDragging */);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005727 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01005728 invalidateActionMode();
Clara Bayarri13152d12015-04-09 12:02:04 +01005729 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005730 }
5731
5732 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005733 protected void updatePosition(float x, float y, boolean fromTouchScreen) {
Mady Mellor81fa3e82015-05-14 09:17:41 -07005734 final Layout layout = mTextView.getLayout();
Mady Mellorcc65c372015-06-17 09:25:19 -07005735 if (layout == null) {
5736 // HandleView will deal appropriately in positionAtCursorOffset when
5737 // layout is null.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005738 positionAndAdjustForCrossingHandles(mTextView.getOffsetForPosition(x, y),
5739 fromTouchScreen);
Mady Mellorcc65c372015-06-17 09:25:19 -07005740 return;
5741 }
5742
Mady Mellora6a0f782015-07-10 16:43:32 -07005743 if (mPreviousLineTouched == UNSET_LINE) {
5744 mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
5745 }
5746
Mady Mellorb9bbbb12015-03-23 11:50:46 -07005747 boolean positionCursor = false;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005748 final int anotherHandleOffset =
5749 isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
Mady Mellora6a0f782015-07-10 16:43:32 -07005750 int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005751 int initialOffset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellor81fa3e82015-05-14 09:17:41 -07005752
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005753 if (isStartHandle() && initialOffset >= anotherHandleOffset
5754 || !isStartHandle() && initialOffset <= anotherHandleOffset) {
5755 // Handles have crossed, bound it to the first selected line and
Mady Mellor81fa3e82015-05-14 09:17:41 -07005756 // adjust by word / char as normal.
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005757 currLine = layout.getLineForOffset(anotherHandleOffset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005758 initialOffset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellor81fa3e82015-05-14 09:17:41 -07005759 }
5760
5761 int offset = initialOffset;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005762 final int wordEnd = getWordEnd(offset);
5763 final int wordStart = getWordStart(offset);
Gilles Debunned88876a2012-03-16 17:34:04 -07005764
Mady Mellore264ac32015-06-22 16:46:29 -07005765 if (mPrevX == UNSET_X_VALUE) {
5766 mPrevX = x;
5767 }
5768
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005769 final int currentOffset = getCurrentCursorOffset();
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005770 final boolean rtlAtCurrentOffset = isAtRtlRun(layout, currentOffset);
5771 final boolean atRtl = isAtRtlRun(layout, offset);
Mady Mellore264ac32015-06-22 16:46:29 -07005772 final boolean isLvlBoundary = layout.isLevelBoundary(offset);
Mady Mellore264ac32015-06-22 16:46:29 -07005773
5774 // We can't determine if the user is expanding or shrinking the selection if they're
5775 // on a bi-di boundary, so until they've moved past the boundary we'll just place
5776 // the cursor at the current position.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005777 if (isLvlBoundary || (rtlAtCurrentOffset && !atRtl) || (!rtlAtCurrentOffset && atRtl)) {
Mady Mellore264ac32015-06-22 16:46:29 -07005778 // We're on a boundary or this is the first direction change -- just update
5779 // to the current position.
5780 mLanguageDirectionChanged = true;
5781 mTouchWordDelta = 0.0f;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005782 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellore264ac32015-06-22 16:46:29 -07005783 return;
5784 } else if (mLanguageDirectionChanged && !isLvlBoundary) {
5785 // We've just moved past the boundary so update the position. After this we can
5786 // figure out if the user is expanding or shrinking to go by word or character.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005787 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellore264ac32015-06-22 16:46:29 -07005788 mTouchWordDelta = 0.0f;
5789 mLanguageDirectionChanged = false;
5790 return;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005791 }
5792
5793 boolean isExpanding;
5794 final float xDiff = x - mPrevX;
Keisuke Kuroyanagi26454142015-12-02 15:04:57 -08005795 if (isStartHandle()) {
5796 isExpanding = currLine < mPreviousLineTouched;
Mady Mellore264ac32015-06-22 16:46:29 -07005797 } else {
Keisuke Kuroyanagi26454142015-12-02 15:04:57 -08005798 isExpanding = currLine > mPreviousLineTouched;
5799 }
5800 if (atRtl == isStartHandle()) {
5801 isExpanding |= xDiff > 0;
5802 } else {
5803 isExpanding |= xDiff < 0;
Mady Mellore264ac32015-06-22 16:46:29 -07005804 }
5805
Mady Mellor42390aa2015-07-24 13:08:42 -07005806 if (mTextView.getHorizontallyScrolling()) {
5807 if (positionNearEdgeOfScrollingView(x, atRtl)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005808 && ((isStartHandle() && mTextView.getScrollX() != 0)
5809 || (!isStartHandle()
5810 && mTextView.canScrollHorizontally(atRtl ? -1 : 1)))
5811 && ((isExpanding && ((isStartHandle() && offset < currentOffset)
5812 || (!isStartHandle() && offset > currentOffset)))
5813 || !isExpanding)) {
5814 // If we're expanding ensure that the offset is actually expanding compared to
5815 // the current offset, if the handle snapped to the word, the finger position
Mady Mellor42390aa2015-07-24 13:08:42 -07005816 // may be out of sync and we don't want the selection to jump back.
5817 mTouchWordDelta = 0.0f;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005818 final int nextOffset = (atRtl == isStartHandle())
5819 ? layout.getOffsetToRightOf(mPreviousOffset)
Mady Mellor42390aa2015-07-24 13:08:42 -07005820 : layout.getOffsetToLeftOf(mPreviousOffset);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005821 positionAndAdjustForCrossingHandles(nextOffset, fromTouchScreen);
Mady Mellor42390aa2015-07-24 13:08:42 -07005822 return;
5823 }
5824 }
5825
Mady Mellore264ac32015-06-22 16:46:29 -07005826 if (isExpanding) {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005827 // User is increasing the selection.
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005828 int wordBoundary = isStartHandle() ? wordStart : wordEnd;
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005829 final boolean snapToWord = (!mInWord
5830 || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine))
5831 && atRtl == isAtRtlRun(layout, wordBoundary);
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005832 if (snapToWord) {
Mady Mellora5266832015-06-26 14:28:12 -07005833 // Sometimes words can be broken across lines (Chinese, hyphenation).
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005834 // We still snap to the word boundary but we only use the letters on the
Mady Mellora5266832015-06-26 14:28:12 -07005835 // current line to determine if the user is far enough into the word to snap.
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005836 if (layout.getLineForOffset(wordBoundary) != currLine) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005837 wordBoundary = isStartHandle()
5838 ? layout.getLineStart(currLine) : layout.getLineEnd(currLine);
Mady Mellora5266832015-06-26 14:28:12 -07005839 }
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005840 final int offsetThresholdToSnap = isStartHandle()
5841 ? wordEnd - ((wordEnd - wordBoundary) / 2)
5842 : wordStart + ((wordBoundary - wordStart) / 2);
5843 if (isStartHandle()
5844 && (offset <= offsetThresholdToSnap || currLine < mPrevLine)) {
5845 // User is far enough into the word or on a different line so we expand by
5846 // word.
5847 offset = wordStart;
5848 } else if (!isStartHandle()
5849 && (offset >= offsetThresholdToSnap || currLine > mPrevLine)) {
5850 // User is far enough into the word or on a different line so we expand by
5851 // word.
5852 offset = wordEnd;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005853 } else {
Mady Mellorc2225b92015-04-01 15:59:20 -07005854 offset = mPreviousOffset;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005855 }
5856 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005857 if ((isStartHandle() && offset < initialOffset)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005858 || (!isStartHandle() && offset > initialOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005859 final float adjustedX = getHorizontal(layout, offset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005860 mTouchWordDelta =
5861 mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
Keisuke Kuroyanagi50a927c2015-05-07 17:34:21 +09005862 } else {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005863 mTouchWordDelta = 0.0f;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005864 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005865 positionCursor = true;
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005866 } else {
5867 final int adjustedOffset =
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005868 getOffsetAtCoordinate(layout, currLine, x - mTouchWordDelta);
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005869 final boolean shrinking = isStartHandle()
5870 ? adjustedOffset > mPreviousOffset || currLine > mPrevLine
5871 : adjustedOffset < mPreviousOffset || currLine < mPrevLine;
5872 if (shrinking) {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005873 // User is shrinking the selection.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005874 if (currLine != mPrevLine) {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005875 // We're on a different line, so we'll snap to word boundaries.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005876 offset = isStartHandle() ? wordStart : wordEnd;
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005877 if ((isStartHandle() && offset < initialOffset)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005878 || (!isStartHandle() && offset > initialOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005879 final float adjustedX = getHorizontal(layout, offset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005880 mTouchWordDelta =
5881 mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
5882 } else {
5883 mTouchWordDelta = 0.0f;
5884 }
5885 } else {
5886 offset = adjustedOffset;
5887 }
5888 positionCursor = true;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005889 } else if ((isStartHandle() && adjustedOffset < mPreviousOffset)
5890 || (!isStartHandle() && adjustedOffset > mPreviousOffset)) {
5891 // Handle has jumped to the word boundary, and the user is moving
Mady Mellor43fd2f42015-06-08 14:03:34 -07005892 // their finger towards the handle, the delta should be updated.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005893 mTouchWordDelta = mTextView.convertToLocalHorizontalCoordinate(x)
5894 - getHorizontal(layout, mPreviousOffset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005895 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005896 }
5897
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005898 if (positionCursor) {
Mady Mellora6a0f782015-07-10 16:43:32 -07005899 mPreviousLineTouched = currLine;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005900 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005901 }
Mady Mellore264ac32015-06-22 16:46:29 -07005902 mPrevX = x;
Gilles Debunned88876a2012-03-16 17:34:04 -07005903 }
5904
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005905 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005906 protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
5907 boolean fromTouchScreen) {
5908 super.positionAtCursorOffset(offset, forceUpdatePosition, fromTouchScreen);
Yoshiki Iguchi9582e152015-10-15 13:34:41 +09005909 mInWord = (offset != -1) && !getWordIteratorWithText().isBoundary(offset);
Mady Mellor36d5a7b2015-05-22 10:31:12 -07005910 }
5911
5912 @Override
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005913 public boolean onTouchEvent(MotionEvent event) {
Shu Cheneb8b1ba2020-04-04 14:46:50 +08005914 if (!mTextView.isFromPrimePointer(event, true)) {
5915 return true;
5916 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005917 boolean superResult = super.onTouchEvent(event);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005918
5919 switch (event.getActionMasked()) {
5920 case MotionEvent.ACTION_DOWN:
5921 // Reset the touch word offset and x value when the user
5922 // re-engages the handle.
5923 mTouchWordDelta = 0.0f;
5924 mPrevX = UNSET_X_VALUE;
Mihai Popae3017462018-03-07 12:25:21 +00005925 updateMagnifier(event);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005926 break;
5927
5928 case MotionEvent.ACTION_MOVE:
Mihai Popae3017462018-03-07 12:25:21 +00005929 updateMagnifier(event);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005930 break;
5931
5932 case MotionEvent.ACTION_UP:
5933 case MotionEvent.ACTION_CANCEL:
5934 dismissMagnifier();
5935 break;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005936 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005937
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005938 return superResult;
5939 }
Mady Mellor42390aa2015-07-24 13:08:42 -07005940
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005941 private void positionAndAdjustForCrossingHandles(int offset, boolean fromTouchScreen) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005942 final int anotherHandleOffset =
5943 isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
5944 if ((isStartHandle() && offset >= anotherHandleOffset)
5945 || (!isStartHandle() && offset <= anotherHandleOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005946 mTouchWordDelta = 0.0f;
5947 final Layout layout = mTextView.getLayout();
5948 if (layout != null && offset != anotherHandleOffset) {
5949 final float horiz = getHorizontal(layout, offset);
5950 final float anotherHandleHoriz = getHorizontal(layout, anotherHandleOffset,
5951 !isStartHandle());
5952 final float currentHoriz = getHorizontal(layout, mPreviousOffset);
5953 if (currentHoriz < anotherHandleHoriz && horiz < anotherHandleHoriz
5954 || currentHoriz > anotherHandleHoriz && horiz > anotherHandleHoriz) {
5955 // This handle passes another one as it crossed a direction boundary.
5956 // Don't minimize the selection, but keep the handle at the run boundary.
5957 final int currentOffset = getCurrentCursorOffset();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005958 final int offsetToGetRunRange = isStartHandle()
5959 ? currentOffset : Math.max(currentOffset - 1, 0);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005960 final long range = layout.getRunRange(offsetToGetRunRange);
5961 if (isStartHandle()) {
5962 offset = TextUtils.unpackRangeStartFromLong(range);
5963 } else {
5964 offset = TextUtils.unpackRangeEndFromLong(range);
5965 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005966 positionAtCursorOffset(offset, false, fromTouchScreen);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005967 return;
5968 }
5969 }
Mady Mellor42390aa2015-07-24 13:08:42 -07005970 // Handles can not cross and selection is at least one character.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005971 offset = getNextCursorOffset(anotherHandleOffset, !isStartHandle());
Mady Mellor42390aa2015-07-24 13:08:42 -07005972 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005973 positionAtCursorOffset(offset, false, fromTouchScreen);
Mady Mellor42390aa2015-07-24 13:08:42 -07005974 }
5975
Mady Mellor42390aa2015-07-24 13:08:42 -07005976 private boolean positionNearEdgeOfScrollingView(float x, boolean atRtl) {
5977 mTextView.getLocationOnScreen(mTextViewLocation);
5978 boolean nearEdge;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005979 if (atRtl == isStartHandle()) {
Mady Mellor42390aa2015-07-24 13:08:42 -07005980 int rightEdge = mTextViewLocation[0] + mTextView.getWidth()
5981 - mTextView.getPaddingRight();
5982 nearEdge = x > rightEdge - mTextViewEdgeSlop;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005983 } else {
5984 int leftEdge = mTextViewLocation[0] + mTextView.getPaddingLeft();
5985 nearEdge = x < leftEdge + mTextViewEdgeSlop;
Mady Mellor42390aa2015-07-24 13:08:42 -07005986 }
5987 return nearEdge;
5988 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005989
5990 @Override
5991 protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
5992 final int offsetToCheck = isStartHandle() ? offset : Math.max(offset - 1, 0);
5993 return layout.isRtlCharAt(offsetToCheck);
5994 }
5995
5996 @Override
5997 public float getHorizontal(@NonNull Layout layout, int offset) {
5998 return getHorizontal(layout, offset, isStartHandle());
5999 }
6000
6001 private float getHorizontal(@NonNull Layout layout, int offset, boolean startHandle) {
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07006002 final int line = layout.getLineForOffset(offset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09006003 final int offsetToCheck = startHandle ? offset : Math.max(offset - 1, 0);
6004 final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
6005 final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006006 return (isRtlChar == isRtlParagraph)
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07006007 ? layout.getPrimaryHorizontal(offset) : layout.getSecondaryHorizontal(offset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09006008 }
6009
6010 @Override
6011 protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
Keisuke Kuroyanagib1b88652016-04-05 16:26:16 +09006012 final float localX = mTextView.convertToLocalHorizontalCoordinate(x);
6013 final int primaryOffset = layout.getOffsetForHorizontal(line, localX, true);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09006014 if (!layout.isLevelBoundary(primaryOffset)) {
6015 return primaryOffset;
6016 }
Keisuke Kuroyanagib1b88652016-04-05 16:26:16 +09006017 final int secondaryOffset = layout.getOffsetForHorizontal(line, localX, false);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09006018 final int currentOffset = getCurrentCursorOffset();
6019 final int primaryDiff = Math.abs(primaryOffset - currentOffset);
6020 final int secondaryDiff = Math.abs(secondaryOffset - currentOffset);
6021 if (primaryDiff < secondaryDiff) {
6022 return primaryOffset;
6023 } else if (primaryDiff > secondaryDiff) {
6024 return secondaryOffset;
6025 } else {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006026 final int offsetToCheck = isStartHandle()
6027 ? currentOffset : Math.max(currentOffset - 1, 0);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09006028 final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
6029 final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
6030 return isRtlChar == isRtlParagraph ? primaryOffset : secondaryOffset;
6031 }
6032 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01006033
6034 @MagnifierHandleTrigger
6035 protected int getMagnifierHandleTrigger() {
6036 return isStartHandle()
6037 ? MagnifierHandleTrigger.SELECTION_START
6038 : MagnifierHandleTrigger.SELECTION_END;
6039 }
Gilles Debunned88876a2012-03-16 17:34:04 -07006040 }
6041
Shu Chenafbcf852020-03-10 08:19:07 +08006042 @VisibleForTesting
6043 public void setLineChangeSlopMinMaxForTesting(final int min, final int max) {
6044 mLineChangeSlopMin = min;
6045 mLineChangeSlopMax = max;
6046 }
6047
6048 @VisibleForTesting
6049 public int getCurrentLineAdjustedForSlop(Layout layout, int prevLine, float y) {
Mady Mellor80679072015-07-09 16:05:36 -07006050 final int trueLine = mTextView.getLineAtCoordinate(y);
Mady Mellorcc65c372015-06-17 09:25:19 -07006051 if (layout == null || prevLine > layout.getLineCount()
6052 || layout.getLineCount() <= 0 || prevLine < 0) {
6053 // Invalid parameters, just return whatever line is at y.
Mady Mellor80679072015-07-09 16:05:36 -07006054 return trueLine;
6055 }
6056
6057 if (Math.abs(trueLine - prevLine) >= 2) {
6058 // Only stick to lines if we're within a line of the previous selection.
6059 return trueLine;
Mady Mellorcc65c372015-06-17 09:25:19 -07006060 }
6061
Shu Chenafbcf852020-03-10 08:19:07 +08006062 final int lineHeight = layout.getLineBottom(prevLine) - layout.getLineTop(prevLine);
6063 int slop = (int)(LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS
6064 * (layout.getLineBottom(trueLine) - layout.getLineTop(trueLine)));
6065 slop = Math.max(mLineChangeSlopMin,
6066 Math.min(mLineChangeSlopMax, lineHeight + slop)) - lineHeight;
6067 slop = Math.max(0, slop);
6068
Mady Mellorcc65c372015-06-17 09:25:19 -07006069 final float verticalOffset = mTextView.viewportToContentVerticalOffset();
Shu Chenafbcf852020-03-10 08:19:07 +08006070 if (trueLine > prevLine && y >= layout.getLineBottom(prevLine) + slop + verticalOffset) {
6071 return trueLine;
Mady Mellorcc65c372015-06-17 09:25:19 -07006072 }
Shu Chenafbcf852020-03-10 08:19:07 +08006073 if (trueLine < prevLine && y <= layout.getLineTop(prevLine) - slop + verticalOffset) {
6074 return trueLine;
6075 }
6076 return prevLine;
Mady Mellorcc65c372015-06-17 09:25:19 -07006077 }
6078
Gilles Debunned88876a2012-03-16 17:34:04 -07006079 /**
6080 * A CursorController instance can be used to control a cursor in the text.
6081 */
6082 private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
6083 /**
6084 * Makes the cursor controller visible on screen.
6085 * See also {@link #hide()}.
6086 */
6087 public void show();
6088
6089 /**
6090 * Hide the cursor controller from screen.
6091 * See also {@link #show()}.
6092 */
6093 public void hide();
6094
6095 /**
6096 * Called when the view is detached from window. Perform house keeping task, such as
6097 * stopping Runnable thread that would otherwise keep a reference on the context, thus
6098 * preventing the activity from being recycled.
6099 */
6100 public void onDetached();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006101
6102 public boolean isCursorBeingModified();
6103
6104 public boolean isActive();
Gilles Debunned88876a2012-03-16 17:34:04 -07006105 }
6106
Mihai Popa6c7ad1d2018-12-04 15:45:00 +00006107 void loadCursorDrawable() {
6108 if (mDrawableForCursor == null) {
6109 mDrawableForCursor = mTextView.getTextCursorDrawable();
6110 }
6111 }
6112
Nikita Dubrovsky21c6a962019-12-27 08:48:02 -08006113 /** Controller for the insertion cursor. */
6114 @VisibleForTesting
6115 public class InsertionPointCursorController implements CursorController {
Gilles Debunned88876a2012-03-16 17:34:04 -07006116 private InsertionHandleView mHandle;
Nikita Dubrovsky7c583592020-02-16 15:54:23 -08006117 // Tracks whether the cursor is currently being dragged.
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08006118 private boolean mIsDraggingCursor;
Nikita Dubrovsky7c583592020-02-16 15:54:23 -08006119 // During a drag, tracks whether the user's finger has adjusted to be over the handle rather
6120 // than the cursor bar.
6121 private boolean mIsTouchSnappedToHandleDuringDrag;
6122 // During a drag, tracks the line of text where the cursor was last positioned.
6123 private int mPrevLineDuringDrag;
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08006124
6125 public void onTouchEvent(MotionEvent event) {
Nikita Dubrovsky99b55fa2020-01-12 20:57:51 -08006126 if (hasSelectionController() && getSelectionController().isCursorBeingModified()) {
Nikita Dubrovsky21c6a962019-12-27 08:48:02 -08006127 return;
6128 }
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08006129 switch (event.getActionMasked()) {
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08006130 case MotionEvent.ACTION_MOVE:
Nikita Dubrovskybd50f3b2020-01-11 20:14:05 -08006131 if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
6132 break;
6133 }
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08006134 if (mIsDraggingCursor) {
6135 performCursorDrag(event);
Nikita Dubrovskyac919b02020-02-18 09:39:20 -08006136 } else if (mFlagCursorDragFromAnywhereEnabled
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08006137 && mTextView.getLayout() != null
6138 && mTextView.isFocused()
Nikita Dubrovskycd36c5e2019-12-19 16:15:17 -08006139 && mTouchState.isMovedEnoughForDrag()
6140 && !mTouchState.isDragCloseToVertical()) {
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08006141 startCursorDrag(event);
6142 }
6143 break;
6144 case MotionEvent.ACTION_UP:
6145 case MotionEvent.ACTION_CANCEL:
6146 if (mIsDraggingCursor) {
6147 endCursorDrag(event);
6148 }
6149 break;
6150 }
6151 }
6152
6153 private void positionCursorDuringDrag(MotionEvent event) {
Nikita Dubrovsky7c583592020-02-16 15:54:23 -08006154 mPrevLineDuringDrag = getLineDuringDrag(event);
6155 int offset = mTextView.getOffsetAtCoordinate(mPrevLineDuringDrag, event.getX());
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08006156 int oldSelectionStart = mTextView.getSelectionStart();
6157 int oldSelectionEnd = mTextView.getSelectionEnd();
6158 if (offset == oldSelectionStart && offset == oldSelectionEnd) {
6159 return;
6160 }
6161 Selection.setSelection((Spannable) mTextView.getText(), offset);
6162 updateCursorPosition();
6163 if (mHapticTextHandleEnabled) {
6164 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
6165 }
6166 }
6167
Nikita Dubrovsky7c583592020-02-16 15:54:23 -08006168 /**
6169 * Returns the line where the cursor should be positioned during a cursor drag. Rather than
6170 * simply returning the line directly at the touch position, this function has the following
6171 * additional logic:
6172 * 1) Apply some slop to avoid switching lines if the touch moves just slightly off the
6173 * current line.
6174 * 2) Allow the user's finger to slide down and "snap" to the handle to provide better
6175 * visibility of the cursor and text.
6176 */
6177 private int getLineDuringDrag(MotionEvent event) {
6178 final Layout layout = mTextView.getLayout();
Shu Chen77003422020-03-05 13:38:05 +08006179 if (mPrevLineDuringDrag == UNSET_LINE) {
Nikita Dubrovsky7c583592020-02-16 15:54:23 -08006180 return getCurrentLineAdjustedForSlop(layout, mPrevLineDuringDrag, event.getY());
6181 }
Shu Chen77003422020-03-05 13:38:05 +08006182 // In case of touch through on handle (when isOnHandle() returns true), event.getY()
6183 // returns the midpoint of the cursor vertical bar, while event.getRawY() returns the
6184 // finger location on the screen. See {@link InsertionHandleView#touchThrough}.
6185 final float fingerY = mTouchState.isOnHandle()
6186 ? event.getRawY() - mTextView.getLocationOnScreen()[1]
6187 : event.getY();
6188 final float cursorY = fingerY - getHandle().getIdealFingerToCursorOffset();
6189 int line = getCurrentLineAdjustedForSlop(layout, mPrevLineDuringDrag, cursorY);
Nikita Dubrovsky7c583592020-02-16 15:54:23 -08006190 if (mIsTouchSnappedToHandleDuringDrag) {
Shu Chen77003422020-03-05 13:38:05 +08006191 // Just returns the line hit by cursor Y when already snapped.
Nikita Dubrovsky7c583592020-02-16 15:54:23 -08006192 return line;
6193 }
Nikita Dubrovsky7c583592020-02-16 15:54:23 -08006194 if (line < mPrevLineDuringDrag) {
Shu Chen77003422020-03-05 13:38:05 +08006195 // The cursor Y aims too high & not yet snapped, check the finger Y.
6196 // If finger Y is moving downwards, don't jump to lower line (until snap).
6197 // If finger Y is moving upwards, can jump to upper line.
6198 return Math.min(mPrevLineDuringDrag,
6199 getCurrentLineAdjustedForSlop(layout, mPrevLineDuringDrag, fingerY));
Nikita Dubrovsky7c583592020-02-16 15:54:23 -08006200 }
Shu Chen77003422020-03-05 13:38:05 +08006201 // The cursor Y aims not too high, so snap!
Nikita Dubrovsky7c583592020-02-16 15:54:23 -08006202 mIsTouchSnappedToHandleDuringDrag = true;
6203 if (TextView.DEBUG_CURSOR) {
6204 logCursor("InsertionPointCursorController",
Shu Chen77003422020-03-05 13:38:05 +08006205 "snapped touch to handle: fingerY=%d, cursorY=%d, mLastLine=%d, line=%d",
6206 (int) fingerY, (int) cursorY, mPrevLineDuringDrag, line);
Nikita Dubrovsky7c583592020-02-16 15:54:23 -08006207 }
6208 return line;
6209 }
6210
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08006211 private void startCursorDrag(MotionEvent event) {
6212 if (TextView.DEBUG_CURSOR) {
6213 logCursor("InsertionPointCursorController", "start cursor drag");
6214 }
6215 mIsDraggingCursor = true;
Nikita Dubrovsky7c583592020-02-16 15:54:23 -08006216 mIsTouchSnappedToHandleDuringDrag = false;
6217 mPrevLineDuringDrag = UNSET_LINE;
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08006218 // We don't want the parent scroll/long-press handlers to take over while dragging.
6219 mTextView.getParent().requestDisallowInterceptTouchEvent(true);
6220 mTextView.cancelLongPress();
6221 // Update the cursor position.
6222 positionCursorDuringDrag(event);
6223 // Show the cursor handle and magnifier.
6224 show();
6225 getHandle().removeHiderCallback();
6226 getHandle().updateMagnifier(event);
6227 // TODO(b/146555651): Figure out if suspendBlink() should be called here.
6228 }
6229
6230 private void performCursorDrag(MotionEvent event) {
6231 positionCursorDuringDrag(event);
6232 getHandle().updateMagnifier(event);
6233 }
6234
6235 private void endCursorDrag(MotionEvent event) {
6236 if (TextView.DEBUG_CURSOR) {
6237 logCursor("InsertionPointCursorController", "end cursor drag");
6238 }
6239 mIsDraggingCursor = false;
Nikita Dubrovsky7c583592020-02-16 15:54:23 -08006240 mIsTouchSnappedToHandleDuringDrag = false;
6241 mPrevLineDuringDrag = UNSET_LINE;
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08006242 // Hide the magnifier and set the handle to be hidden after a delay.
6243 getHandle().dismissMagnifier();
6244 getHandle().hideAfterDelay();
6245 // We're no longer dragging, so let the parent receive events.
6246 mTextView.getParent().requestDisallowInterceptTouchEvent(false);
6247 }
Gilles Debunned88876a2012-03-16 17:34:04 -07006248
6249 public void show() {
6250 getHandle().show();
Andrei Stingaceanu35c550c2015-05-07 16:49:49 +01006251
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08006252 final long durationSinceCutOrCopy =
6253 SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime;
6254
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08006255 if (mInsertionActionModeRunnable != null) {
6256 if (mIsDraggingCursor
6257 || mTouchState.isMultiTap()
6258 || isCursorInsideEasyCorrectionSpan()) {
6259 // Cancel the runnable for showing the floating toolbar.
6260 mTextView.removeCallbacks(mInsertionActionModeRunnable);
6261 }
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08006262 }
6263
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08006264 // If the user recently performed a Cut or Copy action, we want to show the floating
6265 // toolbar even for a single tap.
6266 if (!mIsDraggingCursor
6267 && !mTouchState.isMultiTap()
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08006268 && !isCursorInsideEasyCorrectionSpan()
6269 && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION_MS)) {
6270 if (mTextActionMode == null) {
6271 if (mInsertionActionModeRunnable == null) {
6272 mInsertionActionModeRunnable = new Runnable() {
6273 @Override
6274 public void run() {
6275 startInsertionActionMode();
6276 }
6277 };
6278 }
6279 mTextView.postDelayed(
6280 mInsertionActionModeRunnable,
6281 ViewConfiguration.getDoubleTapTimeout() + 1);
6282 }
6283 }
6284
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08006285 if (!mIsDraggingCursor) {
6286 getHandle().hideAfterDelay();
6287 }
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08006288
Andrei Stingaceanu35c550c2015-05-07 16:49:49 +01006289 if (mSelectionModifierCursorController != null) {
6290 mSelectionModifierCursorController.hide();
6291 }
Gilles Debunned88876a2012-03-16 17:34:04 -07006292 }
6293
Gilles Debunned88876a2012-03-16 17:34:04 -07006294 public void hide() {
6295 if (mHandle != null) {
6296 mHandle.hide();
6297 }
6298 }
6299
6300 public void onTouchModeChanged(boolean isInTouchMode) {
6301 if (!isInTouchMode) {
6302 hide();
6303 }
6304 }
6305
Shu Chen77003422020-03-05 13:38:05 +08006306 public InsertionHandleView getHandle() {
Gilles Debunned88876a2012-03-16 17:34:04 -07006307 if (mHandle == null) {
Mihai Popa6315a322018-10-17 17:39:57 +01006308 loadHandleDrawables(false /* overwrite */);
Gilles Debunned88876a2012-03-16 17:34:04 -07006309 mHandle = new InsertionHandleView(mSelectHandleCenter);
6310 }
6311 return mHandle;
6312 }
6313
Mihai Popa6315a322018-10-17 17:39:57 +01006314 private void reloadHandleDrawable() {
6315 if (mHandle == null) {
6316 // No need to reload, the potentially new drawable will
6317 // be used when the handle is created.
6318 return;
6319 }
6320 mHandle.setDrawables(mSelectHandleCenter, mSelectHandleCenter);
6321 }
6322
Gilles Debunned88876a2012-03-16 17:34:04 -07006323 @Override
6324 public void onDetached() {
6325 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
6326 observer.removeOnTouchModeChangeListener(this);
6327
6328 if (mHandle != null) mHandle.onDetached();
6329 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006330
6331 @Override
6332 public boolean isCursorBeingModified() {
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08006333 return mIsDraggingCursor || (mHandle != null && mHandle.isDragging());
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006334 }
6335
6336 @Override
6337 public boolean isActive() {
6338 return mHandle != null && mHandle.isShowing();
6339 }
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09006340
6341 public void invalidateHandle() {
6342 if (mHandle != null) {
6343 mHandle.invalidate();
6344 }
6345 }
Gilles Debunned88876a2012-03-16 17:34:04 -07006346 }
6347
Nikita Dubrovsky21c6a962019-12-27 08:48:02 -08006348 /** Controller for selection. */
6349 @VisibleForTesting
6350 public class SelectionModifierCursorController implements CursorController {
Gilles Debunned88876a2012-03-16 17:34:04 -07006351 // The cursor controller handles, lazily created when shown.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09006352 private SelectionHandleView mStartHandle;
6353 private SelectionHandleView mEndHandle;
Gilles Debunned88876a2012-03-16 17:34:04 -07006354 // The offsets of that last touch down event. Remembered to start selection there.
6355 private int mMinTouchOffset, mMaxTouchOffset;
6356
Nikita Dubrovskye97b0ec2020-06-22 15:26:25 -07006357 private boolean mGestureStayedInTapRegion;
6358
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006359 // Where the user first starts the drag motion.
6360 private int mStartOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006361
Mady Mellor7a936442015-05-20 10:05:52 -07006362 private boolean mHaventMovedEnoughToStartDrag;
Mady Mellorf9f8aeb2015-06-17 09:46:01 -07006363 // The line that a selection happened most recently with the drag accelerator.
6364 private int mLineSelectionIsOn = -1;
6365 // Whether the drag accelerator has selected past the initial line.
6366 private boolean mSwitchedLines = false;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006367
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006368 // Indicates the drag accelerator mode that the user is currently using.
6369 private int mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
6370 // Drag accelerator is inactive.
6371 private static final int DRAG_ACCELERATOR_MODE_INACTIVE = 0;
6372 // Character based selection by dragging. Only for mouse.
6373 private static final int DRAG_ACCELERATOR_MODE_CHARACTER = 1;
6374 // Word based selection by dragging. Enabled after long pressing or double tapping.
6375 private static final int DRAG_ACCELERATOR_MODE_WORD = 2;
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09006376 // Paragraph based selection by dragging. Enabled after mouse triple click.
6377 private static final int DRAG_ACCELERATOR_MODE_PARAGRAPH = 3;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006378
Gilles Debunned88876a2012-03-16 17:34:04 -07006379 SelectionModifierCursorController() {
6380 resetTouchOffsets();
6381 }
6382
6383 public void show() {
6384 if (mTextView.isInBatchEditMode()) {
6385 return;
6386 }
Mihai Popa6315a322018-10-17 17:39:57 +01006387 loadHandleDrawables(false /* overwrite */);
Gilles Debunned88876a2012-03-16 17:34:04 -07006388 initHandles();
Gilles Debunned88876a2012-03-16 17:34:04 -07006389 }
6390
Gilles Debunned88876a2012-03-16 17:34:04 -07006391 private void initHandles() {
6392 // Lazy object creation has to be done before updatePosition() is called.
6393 if (mStartHandle == null) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09006394 mStartHandle = new SelectionHandleView(mSelectHandleLeft, mSelectHandleRight,
6395 com.android.internal.R.id.selection_start_handle,
6396 HANDLE_TYPE_SELECTION_START);
Gilles Debunned88876a2012-03-16 17:34:04 -07006397 }
6398 if (mEndHandle == null) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09006399 mEndHandle = new SelectionHandleView(mSelectHandleRight, mSelectHandleLeft,
6400 com.android.internal.R.id.selection_end_handle,
6401 HANDLE_TYPE_SELECTION_END);
Gilles Debunned88876a2012-03-16 17:34:04 -07006402 }
6403
6404 mStartHandle.show();
6405 mEndHandle.show();
6406
Gilles Debunned88876a2012-03-16 17:34:04 -07006407 hideInsertionPointCursorController();
6408 }
6409
Mihai Popa6315a322018-10-17 17:39:57 +01006410 private void reloadHandleDrawables() {
6411 if (mStartHandle == null) {
6412 // No need to reload, the potentially new drawables will
6413 // be used when the handles are created.
6414 return;
6415 }
6416 mStartHandle.setDrawables(mSelectHandleLeft, mSelectHandleRight);
6417 mEndHandle.setDrawables(mSelectHandleRight, mSelectHandleLeft);
6418 }
6419
Gilles Debunned88876a2012-03-16 17:34:04 -07006420 public void hide() {
6421 if (mStartHandle != null) mStartHandle.hide();
6422 if (mEndHandle != null) mEndHandle.hide();
6423 }
6424
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006425 public void enterDrag(int dragAcceleratorMode) {
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08006426 if (TextView.DEBUG_CURSOR) {
6427 logCursor("SelectionModifierCursorController: enterDrag",
6428 "starting selection drag: mode=%s", dragAcceleratorMode);
6429 }
6430
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006431 // Just need to init the handles / hide insertion cursor.
6432 show();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006433 mDragAcceleratorMode = dragAcceleratorMode;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006434 // Start location of selection.
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08006435 mStartOffset = mTextView.getOffsetForPosition(mTouchState.getLastDownX(),
6436 mTouchState.getLastDownY());
6437 mLineSelectionIsOn = mTextView.getLineAtCoordinate(mTouchState.getLastDownY());
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006438 // Don't show the handles until user has lifted finger.
6439 hide();
6440
6441 // This stops scrolling parents from intercepting the touch event, allowing
6442 // the user to continue dragging across the screen to select text; TextView will
6443 // scroll as necessary.
6444 mTextView.getParent().requestDisallowInterceptTouchEvent(true);
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006445 mTextView.cancelLongPress();
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006446 }
6447
Gilles Debunned88876a2012-03-16 17:34:04 -07006448 public void onTouchEvent(MotionEvent event) {
6449 // This is done even when the View does not have focus, so that long presses can start
6450 // selection and tap can move cursor from this tap position.
Mady Mellor7a936442015-05-20 10:05:52 -07006451 final float eventX = event.getX();
6452 final float eventY = event.getY();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006453 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
Gilles Debunned88876a2012-03-16 17:34:04 -07006454 switch (event.getActionMasked()) {
6455 case MotionEvent.ACTION_DOWN:
Andrei Stingaceanu838307272015-06-19 17:58:47 +01006456 if (extractedTextModeWillBeStarted()) {
6457 // Prevent duplicating the selection handles until the mode starts.
6458 hide();
6459 } else {
6460 // Remember finger down position, to be able to start selection from there.
6461 mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(
6462 eventX, eventY);
Gilles Debunned88876a2012-03-16 17:34:04 -07006463
Andrei Stingaceanu838307272015-06-19 17:58:47 +01006464 // Double tap detection
Nikita Dubrovskye97b0ec2020-06-22 15:26:25 -07006465 if (mGestureStayedInTapRegion
6466 && mTouchState.isMultiTapInSameArea()
6467 && (isMouse || isPositionOnText(eventX, eventY)
6468 || mTouchState.isOnHandle())) {
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08006469 if (TextView.DEBUG_CURSOR) {
6470 logCursor("SelectionModifierCursorController: onTouchEvent",
6471 "ACTION_DOWN: select and start drag");
Gilles Debunned88876a2012-03-16 17:34:04 -07006472 }
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08006473 if (mTouchState.isDoubleTap()) {
6474 selectCurrentWordAndStartDrag();
6475 } else if (mTouchState.isTripleClick()) {
6476 selectCurrentParagraphAndStartDrag();
6477 }
6478 mDiscardNextActionUp = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07006479 }
Nikita Dubrovskye97b0ec2020-06-22 15:26:25 -07006480 mGestureStayedInTapRegion = true;
Andrei Stingaceanu838307272015-06-19 17:58:47 +01006481 mHaventMovedEnoughToStartDrag = true;
6482 }
Gilles Debunned88876a2012-03-16 17:34:04 -07006483 break;
6484
6485 case MotionEvent.ACTION_POINTER_DOWN:
6486 case MotionEvent.ACTION_POINTER_UP:
6487 // Handle multi-point gestures. Keep min and max offset positions.
6488 // Only activated for devices that correctly handle multi-touch.
6489 if (mTextView.getContext().getPackageManager().hasSystemFeature(
6490 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
6491 updateMinAndMaxOffsets(event);
6492 }
6493 break;
6494
6495 case MotionEvent.ACTION_MOVE:
Nikita Dubrovskye97b0ec2020-06-22 15:26:25 -07006496 if (mGestureStayedInTapRegion) {
6497 final ViewConfiguration viewConfig = ViewConfiguration.get(
6498 mTextView.getContext());
6499 mGestureStayedInTapRegion = EditorTouchState.isDistanceWithin(
6500 mTouchState.getLastDownX(), mTouchState.getLastDownY(),
6501 eventX, eventY, viewConfig.getScaledDoubleTapTouchSlop());
6502 }
6503
Nikita Dubrovsky1f78b112019-12-30 08:26:12 -08006504 if (mHaventMovedEnoughToStartDrag) {
6505 mHaventMovedEnoughToStartDrag = !mTouchState.isMovedEnoughForDrag();
Gilles Debunned88876a2012-03-16 17:34:04 -07006506 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006507
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006508 if (isMouse && !isDragAcceleratorActive()) {
6509 final int offset = mTextView.getOffsetForPosition(eventX, eventY);
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09006510 if (mTextView.hasSelection()
6511 && (!mHaventMovedEnoughToStartDrag || mStartOffset != offset)
6512 && offset >= mTextView.getSelectionStart()
6513 && offset <= mTextView.getSelectionEnd()) {
6514 startDragAndDrop();
6515 break;
6516 }
6517
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006518 if (mStartOffset != offset) {
6519 // Start character based drag accelerator.
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006520 stopTextActionMode();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006521 enterDrag(DRAG_ACCELERATOR_MODE_CHARACTER);
6522 mDiscardNextActionUp = true;
6523 mHaventMovedEnoughToStartDrag = false;
6524 }
6525 }
6526
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006527 if (mStartHandle != null && mStartHandle.isShowing()) {
6528 // Don't do the drag if the handles are showing already.
6529 break;
6530 }
6531
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006532 updateSelection(event);
Gilles Debunned88876a2012-03-16 17:34:04 -07006533 break;
6534
6535 case MotionEvent.ACTION_UP:
Nikita Dubrovsky05cfcc82019-10-24 08:57:32 -07006536 if (TextView.DEBUG_CURSOR) {
6537 logCursor("SelectionModifierCursorController: onTouchEvent", "ACTION_UP");
6538 }
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006539 if (!isDragAcceleratorActive()) {
6540 break;
6541 }
6542 updateSelection(event);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006543
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006544 // No longer dragging to select text, let the parent intercept events.
6545 mTextView.getParent().requestDisallowInterceptTouchEvent(false);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006546
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006547 // No longer the first dragging motion, reset.
6548 resetDragAcceleratorState();
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09006549
6550 if (mTextView.hasSelection()) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01006551 // Drag selection should not be adjusted by the text classifier.
6552 startSelectionActionModeAsync(mHaventMovedEnoughToStartDrag);
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09006553 }
Gilles Debunned88876a2012-03-16 17:34:04 -07006554 break;
6555 }
6556 }
6557
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006558 private void updateSelection(MotionEvent event) {
6559 if (mTextView.getLayout() != null) {
6560 switch (mDragAcceleratorMode) {
6561 case DRAG_ACCELERATOR_MODE_CHARACTER:
6562 updateCharacterBasedSelection(event);
6563 break;
6564 case DRAG_ACCELERATOR_MODE_WORD:
6565 updateWordBasedSelection(event);
6566 break;
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09006567 case DRAG_ACCELERATOR_MODE_PARAGRAPH:
6568 updateParagraphBasedSelection(event);
6569 break;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006570 }
6571 }
6572 }
6573
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09006574 /**
6575 * If the TextView allows text selection, selects the current paragraph and starts a drag.
6576 *
6577 * @return true if the drag was started.
6578 */
6579 private boolean selectCurrentParagraphAndStartDrag() {
6580 if (mInsertionActionModeRunnable != null) {
6581 mTextView.removeCallbacks(mInsertionActionModeRunnable);
6582 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006583 stopTextActionMode();
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09006584 if (!selectCurrentParagraph()) {
6585 return false;
6586 }
6587 enterDrag(SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_PARAGRAPH);
6588 return true;
6589 }
6590
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006591 private void updateCharacterBasedSelection(MotionEvent event) {
6592 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07006593 updateSelectionInternal(mStartOffset, offset,
6594 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006595 }
6596
6597 private void updateWordBasedSelection(MotionEvent event) {
6598 if (mHaventMovedEnoughToStartDrag) {
6599 return;
6600 }
6601 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
6602 final ViewConfiguration viewConfig = ViewConfiguration.get(
6603 mTextView.getContext());
6604 final float eventX = event.getX();
6605 final float eventY = event.getY();
6606 final int currLine;
6607 if (isMouse) {
6608 // No need to offset the y coordinate for mouse input.
6609 currLine = mTextView.getLineAtCoordinate(eventY);
6610 } else {
6611 float y = eventY;
6612 if (mSwitchedLines) {
6613 // Offset the finger by the same vertical offset as the handles.
6614 // This improves visibility of the content being selected by
6615 // shifting the finger below the content, this is applied once
6616 // the user has switched lines.
6617 final int touchSlop = viewConfig.getScaledTouchSlop();
6618 final float fingerOffset = (mStartHandle != null)
6619 ? mStartHandle.getIdealVerticalOffset()
6620 : touchSlop;
6621 y = eventY - fingerOffset;
6622 }
6623
6624 currLine = getCurrentLineAdjustedForSlop(mTextView.getLayout(), mLineSelectionIsOn,
6625 y);
6626 if (!mSwitchedLines && currLine != mLineSelectionIsOn) {
6627 // Break early here, we want to offset the finger position from
6628 // the selection highlight, once the user moved their finger
6629 // to a different line we should apply the offset and *not* switch
6630 // lines until recomputing the position with the finger offset.
6631 mSwitchedLines = true;
6632 return;
6633 }
6634 }
6635
6636 int startOffset;
6637 int offset = mTextView.getOffsetAtCoordinate(currLine, eventX);
6638 // Snap to word boundaries.
6639 if (mStartOffset < offset) {
6640 // Expanding with end handle.
6641 offset = getWordEnd(offset);
6642 startOffset = getWordStart(mStartOffset);
6643 } else {
6644 // Expanding with start handle.
6645 offset = getWordStart(offset);
6646 startOffset = getWordEnd(mStartOffset);
Keisuke Kuroyanagi133dfc02016-07-21 18:07:23 +09006647 if (startOffset == offset) {
6648 offset = getNextCursorOffset(offset, false);
6649 }
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006650 }
6651 mLineSelectionIsOn = currLine;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07006652 updateSelectionInternal(startOffset, offset,
6653 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006654 }
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09006655
6656 private void updateParagraphBasedSelection(MotionEvent event) {
6657 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
6658
6659 final int start = Math.min(offset, mStartOffset);
6660 final int end = Math.max(offset, mStartOffset);
6661 final long paragraphsRange = getParagraphsRange(start, end);
6662 final int selectionStart = TextUtils.unpackRangeStartFromLong(paragraphsRange);
6663 final int selectionEnd = TextUtils.unpackRangeEndFromLong(paragraphsRange);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07006664 updateSelectionInternal(selectionStart, selectionEnd,
6665 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
6666 }
6667
6668 private void updateSelectionInternal(int selectionStart, int selectionEnd,
6669 boolean fromTouchScreen) {
6670 final boolean performHapticFeedback = fromTouchScreen && mHapticTextHandleEnabled
6671 && ((mTextView.getSelectionStart() != selectionStart)
6672 || (mTextView.getSelectionEnd() != selectionEnd));
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09006673 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07006674 if (performHapticFeedback) {
6675 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
6676 }
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09006677 }
6678
Gilles Debunned88876a2012-03-16 17:34:04 -07006679 /**
6680 * @param event
6681 */
6682 private void updateMinAndMaxOffsets(MotionEvent event) {
6683 int pointerCount = event.getPointerCount();
6684 for (int index = 0; index < pointerCount; index++) {
6685 int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
6686 if (offset < mMinTouchOffset) mMinTouchOffset = offset;
6687 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
6688 }
6689 }
6690
6691 public int getMinTouchOffset() {
6692 return mMinTouchOffset;
6693 }
6694
6695 public int getMaxTouchOffset() {
6696 return mMaxTouchOffset;
6697 }
6698
6699 public void resetTouchOffsets() {
6700 mMinTouchOffset = mMaxTouchOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006701 resetDragAcceleratorState();
6702 }
6703
6704 private void resetDragAcceleratorState() {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006705 mStartOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006706 mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
Mady Mellorf9f8aeb2015-06-17 09:46:01 -07006707 mSwitchedLines = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006708 final int selectionStart = mTextView.getSelectionStart();
6709 final int selectionEnd = mTextView.getSelectionEnd();
Clara Bayarri4e518772018-03-27 14:25:33 +01006710 if (selectionStart < 0 || selectionEnd < 0) {
6711 Selection.removeSelection((Spannable) mTextView.getText());
6712 } else if (selectionStart > selectionEnd) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006713 Selection.setSelection((Spannable) mTextView.getText(),
6714 selectionEnd, selectionStart);
6715 }
Gilles Debunned88876a2012-03-16 17:34:04 -07006716 }
6717
6718 /**
6719 * @return true iff this controller is currently used to move the selection start.
6720 */
6721 public boolean isSelectionStartDragged() {
6722 return mStartHandle != null && mStartHandle.isDragging();
6723 }
6724
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006725 @Override
6726 public boolean isCursorBeingModified() {
6727 return isDragAcceleratorActive() || isSelectionStartDragged()
6728 || (mEndHandle != null && mEndHandle.isDragging());
6729 }
6730
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006731 /**
6732 * @return true if the user is selecting text using the drag accelerator.
6733 */
6734 public boolean isDragAcceleratorActive() {
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006735 return mDragAcceleratorMode != DRAG_ACCELERATOR_MODE_INACTIVE;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006736 }
6737
Gilles Debunned88876a2012-03-16 17:34:04 -07006738 public void onTouchModeChanged(boolean isInTouchMode) {
6739 if (!isInTouchMode) {
6740 hide();
6741 }
6742 }
6743
6744 @Override
6745 public void onDetached() {
6746 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
6747 observer.removeOnTouchModeChangeListener(this);
6748
6749 if (mStartHandle != null) mStartHandle.onDetached();
6750 if (mEndHandle != null) mEndHandle.onDetached();
6751 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006752
6753 @Override
6754 public boolean isActive() {
6755 return mStartHandle != null && mStartHandle.isShowing();
6756 }
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09006757
6758 public void invalidateHandles() {
6759 if (mStartHandle != null) {
6760 mStartHandle.invalidate();
6761 }
6762 if (mEndHandle != null) {
6763 mEndHandle.invalidate();
6764 }
6765 }
Gilles Debunned88876a2012-03-16 17:34:04 -07006766 }
6767
Mihai Popa6315a322018-10-17 17:39:57 +01006768 /**
6769 * Loads the insertion and selection handle Drawables from TextView. If the handle
6770 * drawables are already loaded, do not overwrite them unless the method parameter
6771 * is set to true. This logic is required to avoid overwriting Drawables assigned
6772 * to mSelectHandle[Center/Left/Right] by developers using reflection, unless they
6773 * explicitly call the setters in TextView.
6774 *
6775 * @param overwrite whether to overwrite already existing nonnull Drawables
6776 */
6777 void loadHandleDrawables(final boolean overwrite) {
6778 if (mSelectHandleCenter == null || overwrite) {
6779 mSelectHandleCenter = mTextView.getTextSelectHandle();
6780 if (hasInsertionController()) {
6781 getInsertionController().reloadHandleDrawable();
6782 }
6783 }
6784
6785 if (mSelectHandleLeft == null || mSelectHandleRight == null || overwrite) {
6786 mSelectHandleLeft = mTextView.getTextSelectHandleLeft();
6787 mSelectHandleRight = mTextView.getTextSelectHandleRight();
6788 if (hasSelectionController()) {
6789 getSelectionController().reloadHandleDrawables();
6790 }
6791 }
6792 }
6793
Gilles Debunned88876a2012-03-16 17:34:04 -07006794 private class CorrectionHighlighter {
6795 private final Path mPath = new Path();
Chris Craik6a49dde2015-05-12 10:28:14 -07006796 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
Gilles Debunned88876a2012-03-16 17:34:04 -07006797 private int mStart, mEnd;
6798 private long mFadingStartTime;
6799 private RectF mTempRectF;
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006800 private static final int FADE_OUT_DURATION = 400;
Gilles Debunned88876a2012-03-16 17:34:04 -07006801
6802 public CorrectionHighlighter() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006803 mPaint.setCompatibilityScaling(
6804 mTextView.getResources().getCompatibilityInfo().applicationScale);
Gilles Debunned88876a2012-03-16 17:34:04 -07006805 mPaint.setStyle(Paint.Style.FILL);
6806 }
6807
6808 public void highlight(CorrectionInfo info) {
6809 mStart = info.getOffset();
6810 mEnd = mStart + info.getNewText().length();
6811 mFadingStartTime = SystemClock.uptimeMillis();
6812
6813 if (mStart < 0 || mEnd < 0) {
6814 stopAnimation();
6815 }
6816 }
6817
6818 public void draw(Canvas canvas, int cursorOffsetVertical) {
6819 if (updatePath() && updatePaint()) {
6820 if (cursorOffsetVertical != 0) {
6821 canvas.translate(0, cursorOffsetVertical);
6822 }
6823
6824 canvas.drawPath(mPath, mPaint);
6825
6826 if (cursorOffsetVertical != 0) {
6827 canvas.translate(0, -cursorOffsetVertical);
6828 }
6829 invalidate(true); // TODO invalidate cursor region only
6830 } else {
6831 stopAnimation();
6832 invalidate(false); // TODO invalidate cursor region only
6833 }
6834 }
6835
6836 private boolean updatePaint() {
6837 final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
6838 if (duration > FADE_OUT_DURATION) return false;
6839
6840 final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
6841 final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006842 final int color = (mTextView.mHighlightColor & 0x00FFFFFF)
6843 + ((int) (highlightColorAlpha * coef) << 24);
Gilles Debunned88876a2012-03-16 17:34:04 -07006844 mPaint.setColor(color);
6845 return true;
6846 }
6847
6848 private boolean updatePath() {
6849 final Layout layout = mTextView.getLayout();
6850 if (layout == null) return false;
6851
6852 // Update in case text is edited while the animation is run
6853 final int length = mTextView.getText().length();
6854 int start = Math.min(length, mStart);
6855 int end = Math.min(length, mEnd);
6856
6857 mPath.reset();
6858 layout.getSelectionPath(start, end, mPath);
6859 return true;
6860 }
6861
6862 private void invalidate(boolean delayed) {
6863 if (mTextView.getLayout() == null) return;
6864
6865 if (mTempRectF == null) mTempRectF = new RectF();
6866 mPath.computeBounds(mTempRectF, false);
6867
6868 int left = mTextView.getCompoundPaddingLeft();
6869 int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
6870
6871 if (delayed) {
6872 mTextView.postInvalidateOnAnimation(
6873 left + (int) mTempRectF.left, top + (int) mTempRectF.top,
6874 left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
6875 } else {
6876 mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
6877 (int) mTempRectF.right, (int) mTempRectF.bottom);
6878 }
6879 }
6880
6881 private void stopAnimation() {
6882 Editor.this.mCorrectionHighlighter = null;
6883 }
6884 }
6885
6886 private static class ErrorPopup extends PopupWindow {
6887 private boolean mAbove = false;
6888 private final TextView mView;
6889 private int mPopupInlineErrorBackgroundId = 0;
6890 private int mPopupInlineErrorAboveBackgroundId = 0;
6891
6892 ErrorPopup(TextView v, int width, int height) {
6893 super(v, width, height);
6894 mView = v;
6895 // 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 -08006896 // shown and positioned. Initialized with below background, which should have
Gilles Debunned88876a2012-03-16 17:34:04 -07006897 // dimensions identical to the above version for this to work (and is more likely).
6898 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
6899 com.android.internal.R.styleable.Theme_errorMessageBackground);
6900 mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
6901 }
6902
6903 void fixDirection(boolean above) {
6904 mAbove = above;
6905
6906 if (above) {
6907 mPopupInlineErrorAboveBackgroundId =
6908 getResourceId(mPopupInlineErrorAboveBackgroundId,
6909 com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
6910 } else {
6911 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
6912 com.android.internal.R.styleable.Theme_errorMessageBackground);
6913 }
6914
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006915 mView.setBackgroundResource(
6916 above ? mPopupInlineErrorAboveBackgroundId : mPopupInlineErrorBackgroundId);
Gilles Debunned88876a2012-03-16 17:34:04 -07006917 }
6918
6919 private int getResourceId(int currentId, int index) {
6920 if (currentId == 0) {
6921 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
6922 R.styleable.Theme);
6923 currentId = styledAttributes.getResourceId(index, 0);
6924 styledAttributes.recycle();
6925 }
6926 return currentId;
6927 }
6928
6929 @Override
6930 public void update(int x, int y, int w, int h, boolean force) {
6931 super.update(x, y, w, h, force);
6932
6933 boolean above = isAboveAnchor();
6934 if (above != mAbove) {
6935 fixDirection(above);
6936 }
6937 }
6938 }
6939
6940 static class InputContentType {
6941 int imeOptions = EditorInfo.IME_NULL;
Mathew Inwood978c6e22018-08-21 15:58:55 +01006942 @UnsupportedAppUsage
Gilles Debunned88876a2012-03-16 17:34:04 -07006943 String privateImeOptions;
6944 CharSequence imeActionLabel;
6945 int imeActionId;
6946 Bundle extras;
6947 OnEditorActionListener onEditorActionListener;
6948 boolean enterDown;
Yohei Yukawad469f212016-01-21 12:38:09 -08006949 LocaleList imeHintLocales;
Gilles Debunned88876a2012-03-16 17:34:04 -07006950 }
6951
6952 static class InputMethodState {
Gilles Debunnec62589c2012-04-12 14:50:23 -07006953 ExtractedTextRequest mExtractedTextRequest;
6954 final ExtractedText mExtractedText = new ExtractedText();
Gilles Debunned88876a2012-03-16 17:34:04 -07006955 int mBatchEditNesting;
6956 boolean mCursorChanged;
6957 boolean mSelectionModeChanged;
6958 boolean mContentChanged;
6959 int mChangedStart, mChangedEnd, mChangedDelta;
6960 }
Satoshi Kataoka0e3849a2012-12-13 14:37:19 +09006961
James Cookf59152c2015-02-26 18:03:58 -08006962 /**
James Cook471559f2015-02-27 10:31:20 -08006963 * @return True iff (start, end) is a valid range within the text.
6964 */
6965 private static boolean isValidRange(CharSequence text, int start, int end) {
6966 return 0 <= start && start <= end && end <= text.length();
6967 }
6968
6969 /**
James Cookf59152c2015-02-26 18:03:58 -08006970 * An InputFilter that monitors text input to maintain undo history. It does not modify the
6971 * text being typed (and hence always returns null from the filter() method).
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006972 *
6973 * TODO: Make this span aware.
James Cookf59152c2015-02-26 18:03:58 -08006974 */
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006975 public static class UndoInputFilter implements InputFilter {
James Cookf59152c2015-02-26 18:03:58 -08006976 private final Editor mEditor;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006977
James Cook48e0fac2015-02-25 15:44:51 -08006978 // Whether the current filter pass is directly caused by an end-user text edit.
6979 private boolean mIsUserEdit;
6980
James Cookd2026682015-03-03 14:40:14 -08006981 // Whether the text field is handling an IME composition. Must be parceled in case the user
6982 // rotates the screen during composition.
6983 private boolean mHasComposition;
James Cook48e0fac2015-02-25 15:44:51 -08006984
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006985 // Whether the user is expanding or shortening the text
6986 private boolean mExpanding;
6987
6988 // Whether the previous edit operation was in the current batch edit.
6989 private boolean mPreviousOperationWasInSameBatchEdit;
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08006990
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006991 public UndoInputFilter(Editor editor) {
6992 mEditor = editor;
6993 }
6994
James Cookd2026682015-03-03 14:40:14 -08006995 public void saveInstanceState(Parcel parcel) {
6996 parcel.writeInt(mIsUserEdit ? 1 : 0);
6997 parcel.writeInt(mHasComposition ? 1 : 0);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006998 parcel.writeInt(mExpanding ? 1 : 0);
6999 parcel.writeInt(mPreviousOperationWasInSameBatchEdit ? 1 : 0);
James Cookd2026682015-03-03 14:40:14 -08007000 }
7001
7002 public void restoreInstanceState(Parcel parcel) {
7003 mIsUserEdit = parcel.readInt() != 0;
7004 mHasComposition = parcel.readInt() != 0;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007005 mExpanding = parcel.readInt() != 0;
7006 mPreviousOperationWasInSameBatchEdit = parcel.readInt() != 0;
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08007007 }
7008
James Cook48e0fac2015-02-25 15:44:51 -08007009 /**
7010 * Signals that a user-triggered edit is starting.
7011 */
7012 public void beginBatchEdit() {
7013 if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit");
7014 mIsUserEdit = true;
James Cook48e0fac2015-02-25 15:44:51 -08007015 }
7016
7017 public void endBatchEdit() {
7018 if (DEBUG_UNDO) Log.d(TAG, "endBatchEdit");
7019 mIsUserEdit = false;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007020 mPreviousOperationWasInSameBatchEdit = false;
James Cook48e0fac2015-02-25 15:44:51 -08007021 }
7022
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007023 @Override
7024 public CharSequence filter(CharSequence source, int start, int end,
7025 Spanned dest, int dstart, int dend) {
7026 if (DEBUG_UNDO) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007027 Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ") "
7028 + "dest=" + dest + " (" + dstart + "-" + dend + ")");
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007029 }
James Cookf1dad1e2015-02-27 11:00:01 -08007030
James Cook48e0fac2015-02-25 15:44:51 -08007031 // Check to see if this edit should be tracked for undo.
7032 if (!canUndoEdit(source, start, end, dest, dstart, dend)) {
James Cookf1dad1e2015-02-27 11:00:01 -08007033 return null;
7034 }
7035
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007036 final boolean hadComposition = mHasComposition;
7037 mHasComposition = isComposition(source);
7038 final boolean wasExpanding = mExpanding;
7039 boolean shouldCreateSeparateState = false;
7040 if ((end - start) != (dend - dstart)) {
7041 mExpanding = (end - start) > (dend - dstart);
7042 if (hadComposition && mExpanding != wasExpanding) {
7043 shouldCreateSeparateState = true;
7044 }
James Cookd2026682015-03-03 14:40:14 -08007045 }
7046
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007047 // Handle edit.
7048 handleEdit(source, start, end, dest, dstart, dend, shouldCreateSeparateState);
James Cookd2026682015-03-03 14:40:14 -08007049 return null;
7050 }
7051
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09007052 void freezeLastEdit() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007053 mEditor.mUndoManager.beginUpdate("Edit text");
7054 EditOperation lastEdit = getLastEdit();
7055 if (lastEdit != null) {
7056 lastEdit.mFrozen = true;
James Cookd2026682015-03-03 14:40:14 -08007057 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007058 mEditor.mUndoManager.endUpdate();
James Cookd2026682015-03-03 14:40:14 -08007059 }
7060
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007061 @Retention(RetentionPolicy.SOURCE)
Jeff Sharkeyce8db992017-12-13 20:05:05 -07007062 @IntDef(prefix = { "MERGE_EDIT_MODE_" }, value = {
7063 MERGE_EDIT_MODE_FORCE_MERGE,
7064 MERGE_EDIT_MODE_NEVER_MERGE,
7065 MERGE_EDIT_MODE_NORMAL
7066 })
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007067 private @interface MergeMode {}
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007068 private static final int MERGE_EDIT_MODE_FORCE_MERGE = 0;
7069 private static final int MERGE_EDIT_MODE_NEVER_MERGE = 1;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007070 /** Use {@link EditOperation#mergeWith} to merge */
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007071 private static final int MERGE_EDIT_MODE_NORMAL = 2;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007072
7073 private void handleEdit(CharSequence source, int start, int end,
7074 Spanned dest, int dstart, int dend, boolean shouldCreateSeparateState) {
James Cook48e0fac2015-02-25 15:44:51 -08007075 // An application may install a TextWatcher to provide additional modifications after
7076 // the initial input filters run (e.g. a credit card formatter that adds spaces to a
7077 // string). This results in multiple filter() calls for what the user considers to be
7078 // a single operation. Always undo the whole set of changes in one step.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007079 @MergeMode
7080 final int mergeMode;
7081 if (isInTextWatcher() || mPreviousOperationWasInSameBatchEdit) {
7082 mergeMode = MERGE_EDIT_MODE_FORCE_MERGE;
7083 } else if (shouldCreateSeparateState) {
7084 mergeMode = MERGE_EDIT_MODE_NEVER_MERGE;
7085 } else {
7086 mergeMode = MERGE_EDIT_MODE_NORMAL;
7087 }
James Cook471559f2015-02-27 10:31:20 -08007088 // Build a new operation with all the information from this edit.
James Cookd2026682015-03-03 14:40:14 -08007089 String newText = TextUtils.substring(source, start, end);
7090 String oldText = TextUtils.substring(dest, dstart, dend);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007091 EditOperation edit = new EditOperation(mEditor, oldText, dstart, newText,
7092 mHasComposition);
7093 if (mHasComposition && TextUtils.equals(edit.mNewText, edit.mOldText)) {
7094 return;
7095 }
7096 recordEdit(edit, mergeMode);
James Cookd2026682015-03-03 14:40:14 -08007097 }
James Cook471559f2015-02-27 10:31:20 -08007098
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007099 private EditOperation getLastEdit() {
7100 final UndoManager um = mEditor.mUndoManager;
7101 return um.getLastOperation(
7102 EditOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE);
7103 }
James Cook22054252015-03-25 14:04:01 -07007104 /**
7105 * Fetches the last undo operation and checks to see if a new edit should be merged into it.
7106 * If forceMerge is true then the new edit is always merged.
7107 */
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007108 private void recordEdit(EditOperation edit, @MergeMode int mergeMode) {
James Cook471559f2015-02-27 10:31:20 -08007109 // Fetch the last edit operation and attempt to merge in the new edit.
James Cook48e0fac2015-02-25 15:44:51 -08007110 final UndoManager um = mEditor.mUndoManager;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007111 um.beginUpdate("Edit text");
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007112 EditOperation lastEdit = getLastEdit();
James Cook471559f2015-02-27 10:31:20 -08007113 if (lastEdit == null) {
7114 // Add this as the first edit.
7115 if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit);
7116 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007117 } else if (mergeMode == MERGE_EDIT_MODE_FORCE_MERGE) {
James Cook22054252015-03-25 14:04:01 -07007118 // Forced merges take priority because they could be the result of a non-user-edit
7119 // change and this case should not create a new undo operation.
7120 if (DEBUG_UNDO) Log.d(TAG, "filter: force merge " + edit);
7121 lastEdit.forceMergeWith(edit);
James Cook48e0fac2015-02-25 15:44:51 -08007122 } else if (!mIsUserEdit) {
7123 // An application directly modified the Editable outside of a text edit. Treat this
7124 // as a new change and don't attempt to merge.
7125 if (DEBUG_UNDO) Log.d(TAG, "non-user edit, new op " + edit);
7126 um.commitState(mEditor.mUndoOwner);
7127 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007128 } else if (mergeMode == MERGE_EDIT_MODE_NORMAL && lastEdit.mergeWith(edit)) {
James Cook471559f2015-02-27 10:31:20 -08007129 // Merge succeeded, nothing else to do.
7130 if (DEBUG_UNDO) Log.d(TAG, "filter: merge succeeded, created " + lastEdit);
James Cook3ac0bcb2015-02-26 10:53:41 -08007131 } else {
James Cook471559f2015-02-27 10:31:20 -08007132 // Could not merge with the last edit, so commit the last edit and add this edit.
7133 if (DEBUG_UNDO) Log.d(TAG, "filter: merge failed, adding " + edit);
7134 um.commitState(mEditor.mUndoOwner);
7135 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
James Cook3ac0bcb2015-02-26 10:53:41 -08007136 }
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09007137 mPreviousOperationWasInSameBatchEdit = mIsUserEdit;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007138 um.endUpdate();
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007139 }
James Cook48e0fac2015-02-25 15:44:51 -08007140
7141 private boolean canUndoEdit(CharSequence source, int start, int end,
7142 Spanned dest, int dstart, int dend) {
7143 if (!mEditor.mAllowUndo) {
7144 if (DEBUG_UNDO) Log.d(TAG, "filter: undo is disabled");
7145 return false;
7146 }
7147
7148 if (mEditor.mUndoManager.isInUndo()) {
7149 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping, currently performing undo/redo");
7150 return false;
7151 }
7152
7153 // Text filters run before input operations are applied. However, some input operations
7154 // are invalid and will throw exceptions when applied. This is common in tests. Don't
7155 // attempt to undo invalid operations.
7156 if (!isValidRange(source, start, end) || !isValidRange(dest, dstart, dend)) {
7157 if (DEBUG_UNDO) Log.d(TAG, "filter: invalid op");
7158 return false;
7159 }
7160
7161 // Earlier filters can rewrite input to be a no-op, for example due to a length limit
7162 // on an input field. Skip no-op changes.
7163 if (start == end && dstart == dend) {
7164 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping no-op");
7165 return false;
7166 }
7167
7168 return true;
7169 }
James Cookd2026682015-03-03 14:40:14 -08007170
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007171 private static boolean isComposition(CharSequence source) {
James Cookd2026682015-03-03 14:40:14 -08007172 if (!(source instanceof Spannable)) {
7173 return false;
7174 }
7175 // This is a composition edit if the source has a non-zero-length composing span.
7176 Spannable text = (Spannable) source;
7177 int composeBegin = EditableInputConnection.getComposingSpanStart(text);
7178 int composeEnd = EditableInputConnection.getComposingSpanEnd(text);
7179 return composeBegin < composeEnd;
7180 }
7181
7182 private boolean isInTextWatcher() {
7183 CharSequence text = mEditor.mTextView.getText();
7184 return (text instanceof SpannableStringBuilder)
7185 && ((SpannableStringBuilder) text).getTextWatcherDepth() > 0;
7186 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007187 }
7188
James Cookf59152c2015-02-26 18:03:58 -08007189 /**
7190 * An operation to undo a single "edit" to a text view.
7191 */
James Cook471559f2015-02-27 10:31:20 -08007192 public static class EditOperation extends UndoOperation<Editor> {
7193 private static final int TYPE_INSERT = 0;
7194 private static final int TYPE_DELETE = 1;
7195 private static final int TYPE_REPLACE = 2;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007196
James Cook471559f2015-02-27 10:31:20 -08007197 private int mType;
7198 private String mOldText;
James Cook471559f2015-02-27 10:31:20 -08007199 private String mNewText;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007200 private int mStart;
James Cook471559f2015-02-27 10:31:20 -08007201
7202 private int mOldCursorPos;
7203 private int mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007204 private boolean mFrozen;
7205 private boolean mIsComposition;
James Cook471559f2015-02-27 10:31:20 -08007206
7207 /**
James Cookd2026682015-03-03 14:40:14 -08007208 * Constructs an edit operation from a text input operation on editor that replaces the
James Cook22054252015-03-25 14:04:01 -07007209 * oldText starting at dstart with newText.
James Cook471559f2015-02-27 10:31:20 -08007210 */
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007211 public EditOperation(Editor editor, String oldText, int dstart, String newText,
7212 boolean isComposition) {
James Cook471559f2015-02-27 10:31:20 -08007213 super(editor.mUndoOwner);
James Cookd2026682015-03-03 14:40:14 -08007214 mOldText = oldText;
7215 mNewText = newText;
James Cook471559f2015-02-27 10:31:20 -08007216
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007217 // Determine the type of the edit.
James Cook471559f2015-02-27 10:31:20 -08007218 if (mNewText.length() > 0 && mOldText.length() == 0) {
7219 mType = TYPE_INSERT;
James Cook471559f2015-02-27 10:31:20 -08007220 } else if (mNewText.length() == 0 && mOldText.length() > 0) {
7221 mType = TYPE_DELETE;
James Cook471559f2015-02-27 10:31:20 -08007222 } else {
7223 mType = TYPE_REPLACE;
James Cook471559f2015-02-27 10:31:20 -08007224 }
7225
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007226 mStart = dstart;
James Cook471559f2015-02-27 10:31:20 -08007227 // Store cursor data.
7228 mOldCursorPos = editor.mTextView.getSelectionStart();
James Cookd2026682015-03-03 14:40:14 -08007229 mNewCursorPos = dstart + mNewText.length();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007230 mIsComposition = isComposition;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007231 }
7232
James Cook471559f2015-02-27 10:31:20 -08007233 public EditOperation(Parcel src, ClassLoader loader) {
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007234 super(src, loader);
James Cook471559f2015-02-27 10:31:20 -08007235 mType = src.readInt();
7236 mOldText = src.readString();
James Cook471559f2015-02-27 10:31:20 -08007237 mNewText = src.readString();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007238 mStart = src.readInt();
James Cook471559f2015-02-27 10:31:20 -08007239 mOldCursorPos = src.readInt();
7240 mNewCursorPos = src.readInt();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007241 mFrozen = src.readInt() == 1;
7242 mIsComposition = src.readInt() == 1;
James Cook471559f2015-02-27 10:31:20 -08007243 }
7244
7245 @Override
7246 public void writeToParcel(Parcel dest, int flags) {
7247 dest.writeInt(mType);
7248 dest.writeString(mOldText);
James Cook471559f2015-02-27 10:31:20 -08007249 dest.writeString(mNewText);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007250 dest.writeInt(mStart);
James Cook471559f2015-02-27 10:31:20 -08007251 dest.writeInt(mOldCursorPos);
7252 dest.writeInt(mNewCursorPos);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007253 dest.writeInt(mFrozen ? 1 : 0);
7254 dest.writeInt(mIsComposition ? 1 : 0);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007255 }
7256
James Cook48e0fac2015-02-25 15:44:51 -08007257 private int getNewTextEnd() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007258 return mStart + mNewText.length();
James Cook48e0fac2015-02-25 15:44:51 -08007259 }
7260
7261 private int getOldTextEnd() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007262 return mStart + mOldText.length();
James Cook48e0fac2015-02-25 15:44:51 -08007263 }
7264
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007265 @Override
7266 public void commit() {
7267 }
7268
7269 @Override
7270 public void undo() {
James Cook471559f2015-02-27 10:31:20 -08007271 if (DEBUG_UNDO) Log.d(TAG, "undo");
7272 // Remove the new text and insert the old.
James Cook48e0fac2015-02-25 15:44:51 -08007273 Editor editor = getOwnerData();
7274 Editable text = (Editable) editor.mTextView.getText();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007275 modifyText(text, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007276 }
7277
7278 @Override
7279 public void redo() {
James Cook471559f2015-02-27 10:31:20 -08007280 if (DEBUG_UNDO) Log.d(TAG, "redo");
7281 // Remove the old text and insert the new.
James Cook48e0fac2015-02-25 15:44:51 -08007282 Editor editor = getOwnerData();
7283 Editable text = (Editable) editor.mTextView.getText();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007284 modifyText(text, mStart, getOldTextEnd(), mNewText, mStart, mNewCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007285 }
7286
James Cook471559f2015-02-27 10:31:20 -08007287 /**
7288 * Attempts to merge this existing operation with a new edit.
7289 * @param edit The new edit operation.
7290 * @return If the merge succeeded, returns true. Otherwise returns false and leaves this
7291 * object unchanged.
7292 */
7293 private boolean mergeWith(EditOperation edit) {
James Cook48e0fac2015-02-25 15:44:51 -08007294 if (DEBUG_UNDO) {
7295 Log.d(TAG, "mergeWith old " + this);
7296 Log.d(TAG, "mergeWith new " + edit);
7297 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007298
7299 if (mFrozen) {
7300 return false;
7301 }
7302
James Cook471559f2015-02-27 10:31:20 -08007303 switch (mType) {
7304 case TYPE_INSERT:
7305 return mergeInsertWith(edit);
7306 case TYPE_DELETE:
7307 return mergeDeleteWith(edit);
7308 case TYPE_REPLACE:
7309 return mergeReplaceWith(edit);
7310 default:
7311 return false;
7312 }
7313 }
7314
7315 private boolean mergeInsertWith(EditOperation edit) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007316 if (edit.mType == TYPE_INSERT) {
7317 // Merge insertions that are contiguous even when it's frozen.
7318 if (getNewTextEnd() != edit.mStart) {
7319 return false;
7320 }
7321 mNewText += edit.mNewText;
7322 mNewCursorPos = edit.mNewCursorPos;
7323 mFrozen = edit.mFrozen;
7324 mIsComposition = edit.mIsComposition;
7325 return true;
James Cook471559f2015-02-27 10:31:20 -08007326 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007327 if (mIsComposition && edit.mType == TYPE_REPLACE
7328 && mStart <= edit.mStart && getNewTextEnd() >= edit.getOldTextEnd()) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007329 // Merge insertion with replace as they can be single insertion.
7330 mNewText = mNewText.substring(0, edit.mStart - mStart) + edit.mNewText
7331 + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
7332 mNewCursorPos = edit.mNewCursorPos;
7333 mIsComposition = edit.mIsComposition;
7334 return true;
James Cook471559f2015-02-27 10:31:20 -08007335 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007336 return false;
James Cook471559f2015-02-27 10:31:20 -08007337 }
7338
7339 // TODO: Support forward delete.
7340 private boolean mergeDeleteWith(EditOperation edit) {
James Cook471559f2015-02-27 10:31:20 -08007341 // Only merge continuous deletes.
7342 if (edit.mType != TYPE_DELETE) {
7343 return false;
7344 }
7345 // Only merge deletions that are contiguous.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007346 if (mStart != edit.getOldTextEnd()) {
James Cook471559f2015-02-27 10:31:20 -08007347 return false;
7348 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007349 mStart = edit.mStart;
James Cook471559f2015-02-27 10:31:20 -08007350 mOldText = edit.mOldText + mOldText;
7351 mNewCursorPos = edit.mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007352 mIsComposition = edit.mIsComposition;
James Cook471559f2015-02-27 10:31:20 -08007353 return true;
7354 }
7355
7356 private boolean mergeReplaceWith(EditOperation edit) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007357 if (edit.mType == TYPE_INSERT && getNewTextEnd() == edit.mStart) {
7358 // Merge with adjacent insert.
7359 mNewText += edit.mNewText;
7360 mNewCursorPos = edit.mNewCursorPos;
7361 return true;
7362 }
7363 if (!mIsComposition) {
James Cook471559f2015-02-27 10:31:20 -08007364 return false;
7365 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007366 if (edit.mType == TYPE_DELETE && mStart <= edit.mStart
7367 && getNewTextEnd() >= edit.getOldTextEnd()) {
7368 // Merge with delete as they can be single operation.
7369 mNewText = mNewText.substring(0, edit.mStart - mStart)
7370 + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
7371 if (mNewText.isEmpty()) {
7372 mType = TYPE_DELETE;
7373 }
7374 mNewCursorPos = edit.mNewCursorPos;
7375 mIsComposition = edit.mIsComposition;
7376 return true;
7377 }
7378 if (edit.mType == TYPE_REPLACE && mStart == edit.mStart
7379 && TextUtils.equals(mNewText, edit.mOldText)) {
7380 // Merge with the replace that replaces the same region.
7381 mNewText = edit.mNewText;
7382 mNewCursorPos = edit.mNewCursorPos;
7383 mIsComposition = edit.mIsComposition;
7384 return true;
7385 }
7386 return false;
James Cook471559f2015-02-27 10:31:20 -08007387 }
7388
James Cook48e0fac2015-02-25 15:44:51 -08007389 /**
7390 * Forcibly creates a single merged edit operation by simulating the entire text
7391 * contents being replaced.
7392 */
James Cook22054252015-03-25 14:04:01 -07007393 public void forceMergeWith(EditOperation edit) {
James Cook48e0fac2015-02-25 15:44:51 -08007394 if (DEBUG_UNDO) Log.d(TAG, "forceMerge");
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007395 if (mergeWith(edit)) {
7396 return;
7397 }
James Cookf59152c2015-02-26 18:03:58 -08007398 Editor editor = getOwnerData();
James Cook48e0fac2015-02-25 15:44:51 -08007399
7400 // Copy the text of the current field.
7401 // NOTE: Using StringBuilder instead of SpannableStringBuilder would be somewhat faster,
7402 // but would require two parallel implementations of modifyText() because Editable and
7403 // StringBuilder do not share an interface for replace/delete/insert.
7404 Editable editable = (Editable) editor.mTextView.getText();
7405 Editable originalText = new SpannableStringBuilder(editable.toString());
7406
7407 // Roll back the last operation.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007408 modifyText(originalText, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
James Cook48e0fac2015-02-25 15:44:51 -08007409
7410 // Clone the text again and apply the new operation.
7411 Editable finalText = new SpannableStringBuilder(editable.toString());
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007412 modifyText(finalText, edit.mStart, edit.getOldTextEnd(),
7413 edit.mNewText, edit.mStart, edit.mNewCursorPos);
James Cook48e0fac2015-02-25 15:44:51 -08007414
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007415 // Convert this operation into a replace operation.
James Cook48e0fac2015-02-25 15:44:51 -08007416 mType = TYPE_REPLACE;
7417 mNewText = finalText.toString();
James Cook48e0fac2015-02-25 15:44:51 -08007418 mOldText = originalText.toString();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007419 mStart = 0;
James Cook48e0fac2015-02-25 15:44:51 -08007420 mNewCursorPos = edit.mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09007421 mIsComposition = edit.mIsComposition;
James Cook48e0fac2015-02-25 15:44:51 -08007422 // mOldCursorPos is unchanged.
7423 }
7424
7425 private static void modifyText(Editable text, int deleteFrom, int deleteTo,
7426 CharSequence newText, int newTextInsertAt, int newCursorPos) {
James Cook471559f2015-02-27 10:31:20 -08007427 // Apply the edit if it is still valid.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007428 if (isValidRange(text, deleteFrom, deleteTo)
7429 && newTextInsertAt <= text.length() - (deleteTo - deleteFrom)) {
James Cook471559f2015-02-27 10:31:20 -08007430 if (deleteFrom != deleteTo) {
7431 text.delete(deleteFrom, deleteTo);
7432 }
7433 if (newText.length() != 0) {
7434 text.insert(newTextInsertAt, newText);
7435 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007436 }
James Cook900185d2015-03-10 09:48:11 -07007437 // Restore the cursor position. If there wasn't an old cursor (newCursorPos == -1) then
7438 // don't explicitly set it and rely on SpannableStringBuilder to position it.
James Cook471559f2015-02-27 10:31:20 -08007439 // TODO: Select all the text that was undone.
James Cook900185d2015-03-10 09:48:11 -07007440 if (0 <= newCursorPos && newCursorPos <= text.length()) {
James Cook471559f2015-02-27 10:31:20 -08007441 Selection.setSelection(text, newCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007442 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007443 }
7444
James Cook48e0fac2015-02-25 15:44:51 -08007445 private String getTypeString() {
7446 switch (mType) {
7447 case TYPE_INSERT:
7448 return "insert";
7449 case TYPE_DELETE:
7450 return "delete";
7451 case TYPE_REPLACE:
7452 return "replace";
7453 default:
7454 return "";
7455 }
7456 }
7457
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007458 @Override
James Cook471559f2015-02-27 10:31:20 -08007459 public String toString() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007460 return "[mType=" + getTypeString() + ", "
7461 + "mOldText=" + mOldText + ", "
7462 + "mNewText=" + mNewText + ", "
7463 + "mStart=" + mStart + ", "
7464 + "mOldCursorPos=" + mOldCursorPos + ", "
7465 + "mNewCursorPos=" + mNewCursorPos + ", "
7466 + "mFrozen=" + mFrozen + ", "
7467 + "mIsComposition=" + mIsComposition + "]";
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007468 }
7469
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007470 public static final Parcelable.ClassLoaderCreator<EditOperation> CREATOR =
7471 new Parcelable.ClassLoaderCreator<EditOperation>() {
James Cookf59152c2015-02-26 18:03:58 -08007472 @Override
James Cook471559f2015-02-27 10:31:20 -08007473 public EditOperation createFromParcel(Parcel in) {
7474 return new EditOperation(in, null);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007475 }
7476
James Cookf59152c2015-02-26 18:03:58 -08007477 @Override
James Cook471559f2015-02-27 10:31:20 -08007478 public EditOperation createFromParcel(Parcel in, ClassLoader loader) {
7479 return new EditOperation(in, loader);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007480 }
7481
James Cookf59152c2015-02-26 18:03:58 -08007482 @Override
James Cook471559f2015-02-27 10:31:20 -08007483 public EditOperation[] newArray(int size) {
7484 return new EditOperation[size];
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007485 }
7486 };
7487 }
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007488
7489 /**
7490 * A helper for enabling and handling "PROCESS_TEXT" menu actions.
7491 * These allow external applications to plug into currently selected text.
7492 */
7493 static final class ProcessTextIntentActionsHandler {
7494
7495 private final Editor mEditor;
7496 private final TextView mTextView;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01007497 private final Context mContext;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007498 private final PackageManager mPackageManager;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01007499 private final String mPackageName;
7500 private final SparseArray<Intent> mAccessibilityIntents = new SparseArray<>();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007501 private final SparseArray<AccessibilityNodeInfo.AccessibilityAction> mAccessibilityActions =
7502 new SparseArray<>();
7503 private final List<ResolveInfo> mSupportedActivities = new ArrayList<>();
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007504
7505 private ProcessTextIntentActionsHandler(Editor editor) {
Daulet Zhanguzincb0d19b2019-12-18 15:08:09 +00007506 mEditor = Objects.requireNonNull(editor);
7507 mTextView = Objects.requireNonNull(mEditor.mTextView);
7508 mContext = Objects.requireNonNull(mTextView.getContext());
7509 mPackageManager = Objects.requireNonNull(mContext.getPackageManager());
7510 mPackageName = Objects.requireNonNull(mContext.getPackageName());
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007511 }
7512
7513 /**
7514 * Adds "PROCESS_TEXT" menu items to the specified menu.
7515 */
7516 public void onInitializeMenu(Menu menu) {
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01007517 loadSupportedActivities();
Abodunrinwa Tokic28be382017-11-07 18:46:50 +00007518 final int size = mSupportedActivities.size();
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +01007519 for (int i = 0; i < size; i++) {
7520 final ResolveInfo resolveInfo = mSupportedActivities.get(i);
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007521 menu.add(Menu.NONE, Menu.NONE,
Abodunrinwa Tokic28be382017-11-07 18:46:50 +00007522 Editor.MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i,
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007523 getLabel(resolveInfo))
7524 .setIntent(createProcessTextIntentForResolveInfo(resolveInfo))
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00007525 .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007526 }
7527 }
7528
7529 /**
7530 * Performs a "PROCESS_TEXT" action if there is one associated with the specified
7531 * menu item.
7532 *
7533 * @return True if the action was performed, false otherwise.
7534 */
7535 public boolean performMenuItemAction(MenuItem item) {
7536 return fireIntent(item.getIntent());
7537 }
7538
7539 /**
7540 * Initializes and caches "PROCESS_TEXT" accessibility actions.
7541 */
7542 public void initializeAccessibilityActions() {
7543 mAccessibilityIntents.clear();
7544 mAccessibilityActions.clear();
7545 int i = 0;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01007546 loadSupportedActivities();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007547 for (ResolveInfo resolveInfo : mSupportedActivities) {
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007548 int actionId = TextView.ACCESSIBILITY_ACTION_PROCESS_TEXT_START_ID + i++;
7549 mAccessibilityActions.put(
7550 actionId,
7551 new AccessibilityNodeInfo.AccessibilityAction(
7552 actionId, getLabel(resolveInfo)));
7553 mAccessibilityIntents.put(
7554 actionId, createProcessTextIntentForResolveInfo(resolveInfo));
7555 }
7556 }
7557
7558 /**
7559 * Adds "PROCESS_TEXT" accessibility actions to the specified accessibility node info.
7560 * NOTE: This needs a prior call to {@link #initializeAccessibilityActions()} to make the
7561 * latest accessibility actions available for this call.
7562 */
7563 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) {
7564 for (int i = 0; i < mAccessibilityActions.size(); i++) {
7565 nodeInfo.addAction(mAccessibilityActions.valueAt(i));
7566 }
7567 }
7568
7569 /**
7570 * Performs a "PROCESS_TEXT" action if there is one associated with the specified
7571 * accessibility action id.
7572 *
7573 * @return True if the action was performed, false otherwise.
7574 */
7575 public boolean performAccessibilityAction(int actionId) {
7576 return fireIntent(mAccessibilityIntents.get(actionId));
7577 }
7578
7579 private boolean fireIntent(Intent intent) {
7580 if (intent != null && Intent.ACTION_PROCESS_TEXT.equals(intent.getAction())) {
Siyamed Sinirce3b05a2017-07-18 18:54:31 -07007581 String selectedText = mTextView.getSelectedText();
7582 selectedText = TextUtils.trimToParcelableSize(selectedText);
7583 intent.putExtra(Intent.EXTRA_PROCESS_TEXT, selectedText);
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08007584 mEditor.mPreserveSelection = true;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007585 mTextView.startActivityForResult(intent, TextView.PROCESS_TEXT_REQUEST_CODE);
7586 return true;
7587 }
7588 return false;
7589 }
7590
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01007591 private void loadSupportedActivities() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007592 mSupportedActivities.clear();
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01007593 if (!mContext.canStartActivityForResult()) {
7594 return;
7595 }
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007596 PackageManager packageManager = mTextView.getContext().getPackageManager();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007597 List<ResolveInfo> unfiltered =
7598 packageManager.queryIntentActivities(createProcessTextIntent(), 0);
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01007599 for (ResolveInfo info : unfiltered) {
7600 if (isSupportedActivity(info)) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007601 mSupportedActivities.add(info);
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01007602 }
7603 }
7604 }
7605
7606 private boolean isSupportedActivity(ResolveInfo info) {
7607 return mPackageName.equals(info.activityInfo.packageName)
7608 || info.activityInfo.exported
7609 && (info.activityInfo.permission == null
7610 || mContext.checkSelfPermission(info.activityInfo.permission)
7611 == PackageManager.PERMISSION_GRANTED);
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007612 }
7613
7614 private Intent createProcessTextIntentForResolveInfo(ResolveInfo info) {
7615 return createProcessTextIntent()
7616 .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !mTextView.isTextEditable())
7617 .setClassName(info.activityInfo.packageName, info.activityInfo.name);
7618 }
7619
7620 private Intent createProcessTextIntent() {
7621 return new Intent()
7622 .setAction(Intent.ACTION_PROCESS_TEXT)
7623 .setType("text/plain");
7624 }
7625
7626 private CharSequence getLabel(ResolveInfo resolveInfo) {
7627 return resolveInfo.loadLabel(mPackageManager);
7628 }
7629 }
Nikita Dubrovsky05cfcc82019-10-24 08:57:32 -07007630
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08007631 static void logCursor(String location, @Nullable String msgFormat, Object ... msgArgs) {
Nikita Dubrovsky05cfcc82019-10-24 08:57:32 -07007632 if (msgFormat == null) {
7633 Log.d(TAG, location);
7634 } else {
7635 Log.d(TAG, location + ": " + String.format(msgFormat, msgArgs));
7636 }
7637 }
Gilles Debunned88876a2012-03-16 17:34:04 -07007638}