blob: 0340cb317046fe3de59552ae5f110f8084bea175 [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;
Luca Zanolin1b15ba52013-02-20 14:31:37 +000024import android.app.PendingIntent;
25import android.app.PendingIntent.CanceledException;
Jan Althaus20d346e2018-03-23 14:03:52 +010026import android.app.RemoteAction;
Artur Satayeved5a6ae2019-12-10 17:47:54 +000027import android.compat.annotation.UnsupportedAppUsage;
Gilles Debunned88876a2012-03-16 17:34:04 -070028import android.content.ClipData;
29import android.content.ClipData.Item;
30import android.content.Context;
31import android.content.Intent;
Raph Levien26d443a2015-03-30 14:18:32 -070032import android.content.UndoManager;
33import android.content.UndoOperation;
34import android.content.UndoOwner;
Gilles Debunned88876a2012-03-16 17:34:04 -070035import android.content.pm.PackageManager;
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +000036import android.content.pm.ResolveInfo;
Gilles Debunned88876a2012-03-16 17:34:04 -070037import android.content.res.TypedArray;
38import android.graphics.Canvas;
39import android.graphics.Color;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +090040import android.graphics.Matrix;
Gilles Debunned88876a2012-03-16 17:34:04 -070041import android.graphics.Paint;
42import android.graphics.Path;
Mihai Popa63ee7f12018-04-05 12:01:53 +010043import android.graphics.Point;
Mihai Popae3017462018-03-07 12:25:21 +000044import android.graphics.PointF;
John Reck32f140aa62018-10-04 15:08:24 -070045import android.graphics.RecordingCanvas;
Gilles Debunned88876a2012-03-16 17:34:04 -070046import android.graphics.Rect;
47import android.graphics.RectF;
John Reck32f140aa62018-10-04 15:08:24 -070048import android.graphics.RenderNode;
Seigo Nonaka3ed1b392016-01-19 13:54:59 +090049import android.graphics.drawable.ColorDrawable;
Gilles Debunned88876a2012-03-16 17:34:04 -070050import android.graphics.drawable.Drawable;
Mihai Popa6315a322018-10-17 17:39:57 +010051import android.os.Build;
Gilles Debunned88876a2012-03-16 17:34:04 -070052import android.os.Bundle;
Yohei Yukawa23cbe852016-05-17 16:42:58 -070053import android.os.LocaleList;
Raph Levien26d443a2015-03-30 14:18:32 -070054import android.os.Parcel;
55import android.os.Parcelable;
James Cookf59152c2015-02-26 18:03:58 -080056import android.os.ParcelableParcel;
Gilles Debunned88876a2012-03-16 17:34:04 -070057import android.os.SystemClock;
58import android.provider.Settings;
59import android.text.DynamicLayout;
60import android.text.Editable;
Raph Levien26d443a2015-03-30 14:18:32 -070061import android.text.InputFilter;
Gilles Debunned88876a2012-03-16 17:34:04 -070062import android.text.InputType;
63import android.text.Layout;
64import android.text.ParcelableSpan;
65import android.text.Selection;
66import android.text.SpanWatcher;
67import android.text.Spannable;
68import android.text.SpannableStringBuilder;
69import android.text.Spanned;
70import android.text.StaticLayout;
71import android.text.TextUtils;
Gilles Debunned88876a2012-03-16 17:34:04 -070072import android.text.method.KeyListener;
73import android.text.method.MetaKeyKeyListener;
74import android.text.method.MovementMethod;
Gilles Debunned88876a2012-03-16 17:34:04 -070075import android.text.method.WordIterator;
76import android.text.style.EasyEditSpan;
77import android.text.style.SuggestionRangeSpan;
78import android.text.style.SuggestionSpan;
79import android.text.style.TextAppearanceSpan;
80import android.text.style.URLSpan;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +090081import android.util.ArraySet;
Gilles Debunned88876a2012-03-16 17:34:04 -070082import android.util.DisplayMetrics;
83import android.util.Log;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -070084import android.util.SparseArray;
Gilles Debunned88876a2012-03-16 17:34:04 -070085import android.view.ActionMode;
86import android.view.ActionMode.Callback;
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +090087import android.view.ContextMenu;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +090088import android.view.ContextThemeWrapper;
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -070089import android.view.DragAndDropPermissions;
Gilles Debunned88876a2012-03-16 17:34:04 -070090import android.view.DragEvent;
91import android.view.Gravity;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -070092import android.view.HapticFeedbackConstants;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -080093import android.view.InputDevice;
Gilles Debunned88876a2012-03-16 17:34:04 -070094import android.view.LayoutInflater;
95import android.view.Menu;
96import android.view.MenuItem;
97import android.view.MotionEvent;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +090098import android.view.SubMenu;
Gilles Debunned88876a2012-03-16 17:34:04 -070099import android.view.View;
Gilles Debunned88876a2012-03-16 17:34:04 -0700100import android.view.View.DragShadowBuilder;
101import android.view.View.OnClickListener;
Adam Powell057a5852012-05-11 10:28:38 -0700102import android.view.ViewConfiguration;
103import android.view.ViewGroup;
Gilles Debunned88876a2012-03-16 17:34:04 -0700104import android.view.ViewGroup.LayoutParams;
Mihai Popaddf9fe02018-09-28 13:54:19 +0100105import android.view.ViewParent;
Gilles Debunned88876a2012-03-16 17:34:04 -0700106import android.view.ViewTreeObserver;
107import android.view.WindowManager;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700108import android.view.accessibility.AccessibilityNodeInfo;
Mihai Popa38722382018-03-07 19:56:21 +0000109import android.view.animation.LinearInterpolator;
Gilles Debunned88876a2012-03-16 17:34:04 -0700110import android.view.inputmethod.CorrectionInfo;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900111import android.view.inputmethod.CursorAnchorInfo;
Gilles Debunned88876a2012-03-16 17:34:04 -0700112import android.view.inputmethod.EditorInfo;
113import android.view.inputmethod.ExtractedText;
114import android.view.inputmethod.ExtractedTextRequest;
115import android.view.inputmethod.InputConnection;
116import android.view.inputmethod.InputMethodManager;
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100117import android.view.textclassifier.TextClassification;
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000118import android.view.textclassifier.TextClassificationManager;
Gilles Debunned88876a2012-03-16 17:34:04 -0700119import android.widget.AdapterView.OnItemClickListener;
120import android.widget.TextView.Drawables;
121import android.widget.TextView.OnEditorActionListener;
122
Seigo Nonakaa60160b2015-08-19 12:38:35 -0700123import com.android.internal.annotations.VisibleForTesting;
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +0000124import com.android.internal.logging.MetricsLogger;
125import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
Raph Levien26d443a2015-03-30 14:18:32 -0700126import com.android.internal.util.ArrayUtils;
127import com.android.internal.util.GrowingArrayUtils;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700128import com.android.internal.util.Preconditions;
Abodunrinwa Toki29cb7682018-04-11 21:24:20 +0100129import com.android.internal.view.FloatingActionMode;
Raph Levien26d443a2015-03-30 14:18:32 -0700130import com.android.internal.widget.EditableInputConnection;
131
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +0900132import java.lang.annotation.Retention;
133import java.lang.annotation.RetentionPolicy;
Andrei Stingaceanu2aaeefe2015-10-20 19:11:23 +0100134import java.text.BreakIterator;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +0100135import java.util.ArrayList;
Andrei Stingaceanu2aaeefe2015-10-20 19:11:23 +0100136import java.util.Arrays;
Adam Powell86241212019-06-10 08:38:49 -0700137import java.util.Collections;
Andrei Stingaceanu2aaeefe2015-10-20 19:11:23 +0100138import java.util.Comparator;
139import java.util.HashMap;
140import java.util.List;
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +0100141import java.util.Map;
Daulet Zhanguzincb0d19b2019-12-18 15:08:09 +0000142import java.util.Objects;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700143
Gilles Debunned88876a2012-03-16 17:34:04 -0700144/**
145 * Helper class used by TextView to handle editable text views.
146 *
147 * @hide
148 */
149public class Editor {
Adam Powell057a5852012-05-11 10:28:38 -0700150 private static final String TAG = "Editor";
James Cookf59152c2015-02-26 18:03:58 -0800151 private static final boolean DEBUG_UNDO = false;
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800152
153 // Specifies whether to allow starting a cursor drag by dragging anywhere over the text.
154 @VisibleForTesting
155 public static boolean FLAG_ENABLE_CURSOR_DRAG = false;
156 // Specifies whether to use the magnifier when pressing the insertion or selection handles.
Andrei Stingaceanu060b3d72017-10-04 11:27:08 +0100157 private static final boolean FLAG_USE_MAGNIFIER = true;
Adam Powell057a5852012-05-11 10:28:38 -0700158
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -0800159 private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
160 private static final int RECENT_CUT_COPY_DURATION_MS = 15 * 1000; // 15 seconds in millis
161
Gilles Debunned88876a2012-03-16 17:34:04 -0700162 static final int BLINK = 500;
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700163 private static final int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
Mady Mellorcc65c372015-06-17 09:25:19 -0700164 private static final float LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS = 0.5f;
Mady Mellore264ac32015-06-22 16:46:29 -0700165 private static final int UNSET_X_VALUE = -1;
Mady Mellora6a0f782015-07-10 16:43:32 -0700166 private static final int UNSET_LINE = -1;
James Cookf59152c2015-02-26 18:03:58 -0800167 // Tag used when the Editor maintains its own separate UndoManager.
168 private static final String UNDO_OWNER_TAG = "Editor";
Gilles Debunned88876a2012-03-16 17:34:04 -0700169
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900170 // Ordering constants used to place the Action Mode or context menu items in their menu.
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +0100171 private static final int MENU_ITEM_ORDER_ASSIST = 0;
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +0000172 private static final int MENU_ITEM_ORDER_UNDO = 2;
173 private static final int MENU_ITEM_ORDER_REDO = 3;
Abodunrinwa Toki5fedfb82017-02-06 19:34:00 +0000174 private static final int MENU_ITEM_ORDER_CUT = 4;
175 private static final int MENU_ITEM_ORDER_COPY = 5;
176 private static final int MENU_ITEM_ORDER_PASTE = 6;
177 private static final int MENU_ITEM_ORDER_SHARE = 7;
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +0100178 private static final int MENU_ITEM_ORDER_SELECT_ALL = 8;
179 private static final int MENU_ITEM_ORDER_REPLACE = 9;
180 private static final int MENU_ITEM_ORDER_AUTOFILL = 10;
181 private static final int MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT = 11;
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +0100182 private static final int MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START = 50;
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +0100183 private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 100;
Clara Bayarri3b69fd82015-06-03 21:52:02 +0100184
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100185 @IntDef({MagnifierHandleTrigger.SELECTION_START,
186 MagnifierHandleTrigger.SELECTION_END,
187 MagnifierHandleTrigger.INSERTION})
188 @Retention(RetentionPolicy.SOURCE)
189 private @interface MagnifierHandleTrigger {
190 int INSERTION = 0;
191 int SELECTION_START = 1;
192 int SELECTION_END = 2;
193 }
194
Richard Ledley26b87222017-11-30 10:54:08 +0000195 @IntDef({TextActionMode.SELECTION, TextActionMode.INSERTION, TextActionMode.TEXT_LINK})
196 @interface TextActionMode {
197 int SELECTION = 0;
198 int INSERTION = 1;
199 int TEXT_LINK = 2;
200 }
201
James Cookf59152c2015-02-26 18:03:58 -0800202 // Each Editor manages its own undo stack.
203 private final UndoManager mUndoManager = new UndoManager();
204 private UndoOwner mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
James Cook48e0fac2015-02-25 15:44:51 -0800205 final UndoInputFilter mUndoInputFilter = new UndoInputFilter(this);
James Cookf1dad1e2015-02-27 11:00:01 -0800206 boolean mAllowUndo = true;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700207
Abodunrinwa Toki54486c12017-04-19 21:02:36 +0100208 private final MetricsLogger mMetricsLogger = new MetricsLogger();
209
Gilles Debunned88876a2012-03-16 17:34:04 -0700210 // Cursor Controllers.
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800211 InsertionPointCursorController mInsertionPointCursorController;
Gilles Debunned88876a2012-03-16 17:34:04 -0700212 SelectionModifierCursorController mSelectionModifierCursorController;
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100213 // Action mode used when text is selected or when actions on an insertion cursor are triggered.
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800214 private ActionMode mTextActionMode;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100215 @UnsupportedAppUsage
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900216 private boolean mInsertionControllerEnabled;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100217 @UnsupportedAppUsage
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900218 private boolean mSelectionControllerEnabled;
Gilles Debunned88876a2012-03-16 17:34:04 -0700219
Yohei Yukawac9cd9db2017-06-19 18:27:34 -0700220 private final boolean mHapticTextHandleEnabled;
221
Mihai Popa38722382018-03-07 19:56:21 +0000222 private final MagnifierMotionAnimator mMagnifierAnimator;
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000223 private final Runnable mUpdateMagnifierRunnable = new Runnable() {
224 @Override
225 public void run() {
Mihai Popa38722382018-03-07 19:56:21 +0000226 mMagnifierAnimator.update();
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000227 }
228 };
229 // Update the magnifier contents whenever anything in the view hierarchy is updated.
230 // Note: this only captures UI thread-visible changes, so it's a known issue that an animating
231 // VectorDrawable or Ripple animation will not trigger capture, since they're owned by
232 // RenderThread.
233 private final ViewTreeObserver.OnDrawListener mMagnifierOnDrawListener =
234 new ViewTreeObserver.OnDrawListener() {
235 @Override
236 public void onDraw() {
Mihai Popa38722382018-03-07 19:56:21 +0000237 if (mMagnifierAnimator != null) {
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000238 // Posting the method will ensure that updating the magnifier contents will
239 // happen right after the rendering of the current frame.
240 mTextView.post(mUpdateMagnifierRunnable);
241 }
242 }
243 };
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100244
Gilles Debunned88876a2012-03-16 17:34:04 -0700245 // Used to highlight a word when it is corrected by the IME
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900246 private CorrectionHighlighter mCorrectionHighlighter;
Gilles Debunned88876a2012-03-16 17:34:04 -0700247
248 InputContentType mInputContentType;
249 InputMethodState mInputMethodState;
250
Chris Craik956f3402015-04-27 16:41:00 -0700251 private static class TextRenderNode {
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +0900252 // Render node has 3 recording states:
253 // 1. Recorded operations are valid.
254 // #needsRecord() returns false, but needsToBeShifted is false.
255 // 2. Recorded operations are not valid, but just the position needed to be updated.
256 // #needsRecord() returns false, but needsToBeShifted is true.
257 // 3. Recorded operations are not valid. Need to record operations. #needsRecord() returns
258 // true.
Chris Craik956f3402015-04-27 16:41:00 -0700259 RenderNode renderNode;
John Reck7558aa72014-03-05 14:59:59 -0800260 boolean isDirty;
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +0900261 // Becomes true when recorded operations can be reused, but the position has to be updated.
262 boolean needsToBeShifted;
Chris Craik956f3402015-04-27 16:41:00 -0700263 public TextRenderNode(String name) {
Chris Craik956f3402015-04-27 16:41:00 -0700264 renderNode = RenderNode.create(name, null);
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +0900265 isDirty = true;
266 needsToBeShifted = true;
John Reck7558aa72014-03-05 14:59:59 -0800267 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700268 boolean needsRecord() {
John Reckc7ddcf32018-10-25 13:56:17 -0700269 return isDirty || !renderNode.hasDisplayList();
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700270 }
John Reck7558aa72014-03-05 14:59:59 -0800271 }
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900272 private TextRenderNode[] mTextRenderNodes;
Gilles Debunned88876a2012-03-16 17:34:04 -0700273
274 boolean mFrozenWithFocus;
275 boolean mSelectionMoved;
276 boolean mTouchFocusSelected;
277
278 KeyListener mKeyListener;
279 int mInputType = EditorInfo.TYPE_NULL;
280
281 boolean mDiscardNextActionUp;
282 boolean mIgnoreActionUpEvent;
283
Louis Pullen-Freilich1c400a32019-02-05 14:35:20 +0000284 /**
285 * To set a custom cursor, you should use {@link TextView#setTextCursorDrawable(Drawable)}
286 * or {@link TextView#setTextCursorDrawable(int)}.
287 */
288 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
Mihai Popaa4e39c42018-02-20 15:31:11 +0000289 private long mShowCursor;
290 private boolean mRenderCursorRegardlessTiming;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900291 private Blink mBlink;
Gilles Debunned88876a2012-03-16 17:34:04 -0700292
293 boolean mCursorVisible = true;
294 boolean mSelectAllOnFocus;
295 boolean mTextIsSelectable;
296
297 CharSequence mError;
298 boolean mErrorWasChanged;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900299 private ErrorPopup mErrorPopup;
Fabrice Di Meglio1957d282012-10-25 17:42:39 -0700300
Gilles Debunned88876a2012-03-16 17:34:04 -0700301 /**
302 * This flag is set if the TextView tries to display an error before it
303 * is attached to the window (so its position is still unknown).
304 * It causes the error to be shown later, when onAttachedToWindow()
305 * is called.
306 */
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900307 private boolean mShowErrorAfterAttach;
Gilles Debunned88876a2012-03-16 17:34:04 -0700308
309 boolean mInBatchEditControllers;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100310 @UnsupportedAppUsage
Gilles Debunne3473b2b2012-04-20 16:21:10 -0700311 boolean mShowSoftInputOnFocus = true;
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -0800312 private boolean mPreserveSelection;
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +0900313 private boolean mRestartActionModeOnNextRefresh;
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000314 private boolean mRequestingLinkActionMode;
Gilles Debunned88876a2012-03-16 17:34:04 -0700315
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800316 private SelectionActionModeHelper mSelectionActionModeHelper;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +0000317
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900318 boolean mIsBeingLongClicked;
319
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900320 private SuggestionsPopupWindow mSuggestionsPopupWindow;
Gilles Debunned88876a2012-03-16 17:34:04 -0700321 SuggestionRangeSpan mSuggestionRangeSpan;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900322 private Runnable mShowSuggestionRunnable;
Gilles Debunned88876a2012-03-16 17:34:04 -0700323
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -0700324 Drawable mDrawableForCursor = null;
Gilles Debunned88876a2012-03-16 17:34:04 -0700325
Mihai Popa6315a322018-10-17 17:39:57 +0100326 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
327 Drawable mSelectHandleLeft;
328 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
329 Drawable mSelectHandleRight;
330 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
331 Drawable mSelectHandleCenter;
Gilles Debunned88876a2012-03-16 17:34:04 -0700332
333 // Global listener that detects changes in the global position of the TextView
334 private PositionListener mPositionListener;
335
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900336 private float mContextMenuAnchorX, mContextMenuAnchorY;
Gilles Debunned88876a2012-03-16 17:34:04 -0700337 Callback mCustomSelectionActionModeCallback;
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100338 Callback mCustomInsertionActionModeCallback;
Gilles Debunned88876a2012-03-16 17:34:04 -0700339
340 // Set when this TextView gained focus with some text selected. Will start selection mode.
Mathew Inwood978c6e22018-08-21 15:58:55 +0100341 @UnsupportedAppUsage
Gilles Debunned88876a2012-03-16 17:34:04 -0700342 boolean mCreatedWithASelection;
343
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900344 // The button state as of the last time #onTouchEvent is called.
345 private int mLastButtonState;
346
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -0800347 private final EditorTouchState mTouchState = new EditorTouchState();
348
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100349 private Runnable mInsertionActionModeRunnable;
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100350
Jean Chalardbaf30942013-02-28 16:01:51 -0800351 // The span controller helps monitoring the changes to which the Editor needs to react:
352 // - EasyEditSpans, for which we have some UI to display on attach and on hide
353 // - SelectionSpans, for which we need to call updateSelection if an IME is attached
354 private SpanController mSpanController;
Gilles Debunned88876a2012-03-16 17:34:04 -0700355
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900356 private WordIterator mWordIterator;
Gilles Debunned88876a2012-03-16 17:34:04 -0700357 SpellChecker mSpellChecker;
358
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800359 // This word iterator is set with text and used to determine word boundaries
360 // when a user is selecting text.
361 private WordIterator mWordIteratorWithText;
362 // Indicate that the text in the word iterator needs to be updated.
363 private boolean mUpdateWordIteratorText;
364
Gilles Debunned88876a2012-03-16 17:34:04 -0700365 private Rect mTempRect;
366
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800367 private final TextView mTextView;
Gilles Debunned88876a2012-03-16 17:34:04 -0700368
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700369 final ProcessTextIntentActionsHandler mProcessTextIntentActionsHandler;
370
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700371 private final CursorAnchorInfoNotifier mCursorAnchorInfoNotifier =
372 new CursorAnchorInfoNotifier();
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900373
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100374 private final Runnable mShowFloatingToolbar = new Runnable() {
375 @Override
376 public void run() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100377 if (mTextActionMode != null) {
Abodunrinwa Toki9e211282015-06-05 02:46:57 +0100378 mTextActionMode.hide(0); // hide off.
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100379 }
380 }
381 };
382
Clara Bayarrib71dddd2015-06-04 23:17:30 +0100383 boolean mIsInsertionActionModeStartPending = false;
384
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +0900385 private final SuggestionHelper mSuggestionHelper = new SuggestionHelper();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +0900386
Gilles Debunned88876a2012-03-16 17:34:04 -0700387 Editor(TextView textView) {
388 mTextView = textView;
James Cookf59152c2015-02-26 18:03:58 -0800389 // Synchronize the filter list, which places the undo input filter at the end.
390 mTextView.setFilters(mTextView.getFilters());
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700391 mProcessTextIntentActionsHandler = new ProcessTextIntentActionsHandler(this);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -0700392 mHapticTextHandleEnabled = mTextView.getContext().getResources().getBoolean(
393 com.android.internal.R.bool.config_enableHapticTextHandle);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100394
Mihai Popa38722382018-03-07 19:56:21 +0000395 if (FLAG_USE_MAGNIFIER) {
Mihai Popac6950292018-11-15 21:32:42 +0000396 final Magnifier magnifier =
397 Magnifier.createBuilderWithOldMagnifierDefaults(mTextView).build();
Mihai Popab6ca9092018-09-24 21:14:50 +0100398 mMagnifierAnimator = new MagnifierMotionAnimator(magnifier);
Mihai Popa38722382018-03-07 19:56:21 +0000399 }
James Cookf59152c2015-02-26 18:03:58 -0800400 }
401
402 ParcelableParcel saveInstanceState() {
James Cookd2026682015-03-03 14:40:14 -0800403 ParcelableParcel state = new ParcelableParcel(getClass().getClassLoader());
404 Parcel parcel = state.getParcel();
405 mUndoManager.saveInstanceState(parcel);
406 mUndoInputFilter.saveInstanceState(parcel);
407 return state;
James Cookf59152c2015-02-26 18:03:58 -0800408 }
409
410 void restoreInstanceState(ParcelableParcel state) {
James Cookd2026682015-03-03 14:40:14 -0800411 Parcel parcel = state.getParcel();
412 mUndoManager.restoreInstanceState(parcel, state.getClassLoader());
413 mUndoInputFilter.restoreInstanceState(parcel);
James Cookf59152c2015-02-26 18:03:58 -0800414 // Re-associate this object as the owner of undo state.
415 mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
416 }
417
James Cook48e0fac2015-02-25 15:44:51 -0800418 /**
419 * Forgets all undo and redo operations for this Editor.
420 */
421 void forgetUndoRedo() {
422 UndoOwner[] owners = { mUndoOwner };
423 mUndoManager.forgetUndos(owners, -1 /* all */);
424 mUndoManager.forgetRedos(owners, -1 /* all */);
425 }
426
James Cookf59152c2015-02-26 18:03:58 -0800427 boolean canUndo() {
428 UndoOwner[] owners = { mUndoOwner };
James Cookf1dad1e2015-02-27 11:00:01 -0800429 return mAllowUndo && mUndoManager.countUndos(owners) > 0;
James Cookf59152c2015-02-26 18:03:58 -0800430 }
431
432 boolean canRedo() {
433 UndoOwner[] owners = { mUndoOwner };
James Cookf1dad1e2015-02-27 11:00:01 -0800434 return mAllowUndo && mUndoManager.countRedos(owners) > 0;
James Cookf59152c2015-02-26 18:03:58 -0800435 }
436
437 void undo() {
James Cookf1dad1e2015-02-27 11:00:01 -0800438 if (!mAllowUndo) {
439 return;
440 }
James Cookf59152c2015-02-26 18:03:58 -0800441 UndoOwner[] owners = { mUndoOwner };
442 mUndoManager.undo(owners, 1); // Undo 1 action.
443 }
444
445 void redo() {
James Cookf1dad1e2015-02-27 11:00:01 -0800446 if (!mAllowUndo) {
447 return;
448 }
James Cookf59152c2015-02-26 18:03:58 -0800449 UndoOwner[] owners = { mUndoOwner };
450 mUndoManager.redo(owners, 1); // Redo 1 action.
Gilles Debunned88876a2012-03-16 17:34:04 -0700451 }
452
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100453 void replace() {
Keisuke Kuroyanagi713be062016-02-29 16:07:54 -0800454 if (mSuggestionsPopupWindow == null) {
455 mSuggestionsPopupWindow = new SuggestionsPopupWindow();
456 }
457 hideCursorAndSpanControllers();
458 mSuggestionsPopupWindow.show();
459
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100460 int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100461 Selection.setSelection((Spannable) mTextView.getText(), middle);
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100462 }
463
Gilles Debunned88876a2012-03-16 17:34:04 -0700464 void onAttachedToWindow() {
465 if (mShowErrorAfterAttach) {
466 showError();
467 mShowErrorAfterAttach = false;
468 }
469
470 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000471 if (observer.isAlive()) {
472 // No need to create the controller.
473 // The get method will add the listener on controller creation.
474 if (mInsertionPointCursorController != null) {
475 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
476 }
477 if (mSelectionModifierCursorController != null) {
478 mSelectionModifierCursorController.resetTouchOffsets();
479 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
480 }
481 if (FLAG_USE_MAGNIFIER) {
482 observer.addOnDrawListener(mMagnifierOnDrawListener);
483 }
Gilles Debunned88876a2012-03-16 17:34:04 -0700484 }
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000485
Gilles Debunned88876a2012-03-16 17:34:04 -0700486 updateSpellCheckSpans(0, mTextView.getText().length(),
487 true /* create the spell checker if needed */);
Adam Powell057a5852012-05-11 10:28:38 -0700488
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +0900489 if (mTextView.hasSelection()) {
490 refreshTextActionMode();
Adam Powell057a5852012-05-11 10:28:38 -0700491 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900492
493 getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true);
Mikael Gullstrand5b734f22013-07-09 14:41:28 +0200494 resumeBlink();
Gilles Debunned88876a2012-03-16 17:34:04 -0700495 }
496
497 void onDetachedFromWindow() {
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900498 getPositionListener().removeSubscriber(mCursorAnchorInfoNotifier);
499
Gilles Debunned88876a2012-03-16 17:34:04 -0700500 if (mError != null) {
501 hideError();
502 }
503
Mikael Gullstrand5b734f22013-07-09 14:41:28 +0200504 suspendBlink();
Gilles Debunned88876a2012-03-16 17:34:04 -0700505
506 if (mInsertionPointCursorController != null) {
507 mInsertionPointCursorController.onDetached();
508 }
509
510 if (mSelectionModifierCursorController != null) {
511 mSelectionModifierCursorController.onDetached();
512 }
513
514 if (mShowSuggestionRunnable != null) {
515 mTextView.removeCallbacks(mShowSuggestionRunnable);
516 }
517
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100518 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100519 if (mInsertionActionModeRunnable != null) {
520 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100521 }
522
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100523 mTextView.removeCallbacks(mShowFloatingToolbar);
524
Chris Craik003cc3d2015-10-16 10:24:55 -0700525 discardTextDisplayLists();
Gilles Debunned88876a2012-03-16 17:34:04 -0700526
527 if (mSpellChecker != null) {
528 mSpellChecker.closeSession();
529 // Forces the creation of a new SpellChecker next time this window is created.
530 // Will handle the cases where the settings has been changed in the meantime.
531 mSpellChecker = null;
532 }
533
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000534 if (FLAG_USE_MAGNIFIER) {
535 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
536 if (observer.isAlive()) {
537 observer.removeOnDrawListener(mMagnifierOnDrawListener);
538 }
539 }
540
Mady Mellora2861452015-06-25 08:40:27 -0700541 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -0800542 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -0700543 }
544
Chris Craik003cc3d2015-10-16 10:24:55 -0700545 private void discardTextDisplayLists() {
Chris Craik956f3402015-04-27 16:41:00 -0700546 if (mTextRenderNodes != null) {
547 for (int i = 0; i < mTextRenderNodes.length; i++) {
548 RenderNode displayList = mTextRenderNodes[i] != null
549 ? mTextRenderNodes[i].renderNode : null;
John Reckc7ddcf32018-10-25 13:56:17 -0700550 if (displayList != null && displayList.hasDisplayList()) {
Chris Craik003cc3d2015-10-16 10:24:55 -0700551 displayList.discardDisplayList();
John Reck7558aa72014-03-05 14:59:59 -0800552 }
553 }
554 }
555 }
556
Gilles Debunned88876a2012-03-16 17:34:04 -0700557 private void showError() {
558 if (mTextView.getWindowToken() == null) {
559 mShowErrorAfterAttach = true;
560 return;
561 }
562
563 if (mErrorPopup == null) {
564 LayoutInflater inflater = LayoutInflater.from(mTextView.getContext());
565 final TextView err = (TextView) inflater.inflate(
566 com.android.internal.R.layout.textview_hint, null);
567
568 final float scale = mTextView.getResources().getDisplayMetrics().density;
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700569 mErrorPopup =
570 new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f));
Gilles Debunned88876a2012-03-16 17:34:04 -0700571 mErrorPopup.setFocusable(false);
572 // The user is entering text, so the input method is needed. We
573 // don't want the popup to be displayed on top of it.
574 mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
575 }
576
577 TextView tv = (TextView) mErrorPopup.getContentView();
578 chooseSize(mErrorPopup, mError, tv);
579 tv.setText(mError);
580
Hidehiko Tsuchiyaa0c8c1c2017-11-13 10:52:23 +0900581 mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY(),
582 Gravity.TOP | Gravity.LEFT);
Gilles Debunned88876a2012-03-16 17:34:04 -0700583 mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor());
584 }
585
586 public void setError(CharSequence error, Drawable icon) {
587 mError = TextUtils.stringOrSpannedString(error);
588 mErrorWasChanged = true;
Romain Guyd1cc1872012-11-05 17:43:25 -0800589
Gilles Debunned88876a2012-03-16 17:34:04 -0700590 if (mError == null) {
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800591 setErrorIcon(null);
Gilles Debunned88876a2012-03-16 17:34:04 -0700592 if (mErrorPopup != null) {
593 if (mErrorPopup.isShowing()) {
594 mErrorPopup.dismiss();
595 }
596
597 mErrorPopup = null;
598 }
Daniel 2 Olofssonf4ecc552013-08-13 10:30:26 +0200599 mShowErrorAfterAttach = false;
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800600 } else {
Romain Guyd1cc1872012-11-05 17:43:25 -0800601 setErrorIcon(icon);
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800602 if (mTextView.isFocused()) {
603 showError();
604 }
Romain Guyd1cc1872012-11-05 17:43:25 -0800605 }
606 }
607
608 private void setErrorIcon(Drawable icon) {
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800609 Drawables dr = mTextView.mDrawables;
610 if (dr == null) {
Fabrice Di Megliof7a5cdf2013-03-15 15:36:51 -0700611 mTextView.mDrawables = dr = new Drawables(mTextView.getContext());
Gilles Debunned88876a2012-03-16 17:34:04 -0700612 }
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800613 dr.setErrorDrawable(icon, mTextView);
614
615 mTextView.resetResolvedDrawables();
616 mTextView.invalidate();
617 mTextView.requestLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -0700618 }
619
620 private void hideError() {
621 if (mErrorPopup != null) {
622 if (mErrorPopup.isShowing()) {
623 mErrorPopup.dismiss();
624 }
625 }
626
627 mShowErrorAfterAttach = false;
628 }
629
630 /**
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800631 * Returns the X offset to make the pointy top of the error point
Gilles Debunned88876a2012-03-16 17:34:04 -0700632 * at the middle of the error icon.
633 */
634 private int getErrorX() {
635 /*
636 * The "25" is the distance between the point and the right edge
637 * of the background
638 */
639 final float scale = mTextView.getResources().getDisplayMetrics().density;
640
641 final Drawables dr = mTextView.mDrawables;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800642
643 final int layoutDirection = mTextView.getLayoutDirection();
644 int errorX;
645 int offset;
646 switch (layoutDirection) {
647 default:
648 case View.LAYOUT_DIRECTION_LTR:
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700649 offset = -(dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
650 errorX = mTextView.getWidth() - mErrorPopup.getWidth()
651 - mTextView.getPaddingRight() + offset;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800652 break;
653 case View.LAYOUT_DIRECTION_RTL:
654 offset = (dr != null ? dr.mDrawableSizeLeft : 0) / 2 - (int) (25 * scale + 0.5f);
655 errorX = mTextView.getPaddingLeft() + offset;
656 break;
657 }
658 return errorX;
Gilles Debunned88876a2012-03-16 17:34:04 -0700659 }
660
661 /**
662 * Returns the Y offset to make the pointy top of the error point
663 * at the bottom of the error icon.
664 */
665 private int getErrorY() {
666 /*
667 * Compound, not extended, because the icon is not clipped
668 * if the text height is smaller.
669 */
670 final int compoundPaddingTop = mTextView.getCompoundPaddingTop();
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700671 int vspace = mTextView.getBottom() - mTextView.getTop()
672 - mTextView.getCompoundPaddingBottom() - compoundPaddingTop;
Gilles Debunned88876a2012-03-16 17:34:04 -0700673
674 final Drawables dr = mTextView.mDrawables;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800675
676 final int layoutDirection = mTextView.getLayoutDirection();
677 int height;
678 switch (layoutDirection) {
679 default:
680 case View.LAYOUT_DIRECTION_LTR:
681 height = (dr != null ? dr.mDrawableHeightRight : 0);
682 break;
683 case View.LAYOUT_DIRECTION_RTL:
684 height = (dr != null ? dr.mDrawableHeightLeft : 0);
685 break;
686 }
687
688 int icontop = compoundPaddingTop + (vspace - height) / 2;
Gilles Debunned88876a2012-03-16 17:34:04 -0700689
690 /*
691 * The "2" is the distance between the point and the top edge
692 * of the background.
693 */
694 final float scale = mTextView.getResources().getDisplayMetrics().density;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800695 return icontop + height - mTextView.getHeight() - (int) (2 * scale + 0.5f);
Gilles Debunned88876a2012-03-16 17:34:04 -0700696 }
697
698 void createInputContentTypeIfNeeded() {
699 if (mInputContentType == null) {
700 mInputContentType = new InputContentType();
701 }
702 }
703
704 void createInputMethodStateIfNeeded() {
705 if (mInputMethodState == null) {
706 mInputMethodState = new InputMethodState();
707 }
708 }
709
Mihai Popaa4e39c42018-02-20 15:31:11 +0000710 private boolean isCursorVisible() {
Gilles Debunned88876a2012-03-16 17:34:04 -0700711 // The default value is true, even when there is no associated Editor
712 return mCursorVisible && mTextView.isTextEditable();
713 }
714
Mihai Popaa4e39c42018-02-20 15:31:11 +0000715 boolean shouldRenderCursor() {
716 if (!isCursorVisible()) {
717 return false;
718 }
719 if (mRenderCursorRegardlessTiming) {
720 return true;
721 }
722 final long showCursorDelta = SystemClock.uptimeMillis() - mShowCursor;
723 return showCursorDelta % (2 * BLINK) < BLINK;
724 }
725
Gilles Debunned88876a2012-03-16 17:34:04 -0700726 void prepareCursorControllers() {
727 boolean windowSupportsHandles = false;
728
729 ViewGroup.LayoutParams params = mTextView.getRootView().getLayoutParams();
730 if (params instanceof WindowManager.LayoutParams) {
731 WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
732 windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
733 || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
734 }
735
736 boolean enabled = windowSupportsHandles && mTextView.getLayout() != null;
737 mInsertionControllerEnabled = enabled && isCursorVisible();
738 mSelectionControllerEnabled = enabled && mTextView.textCanBeSelected();
739
740 if (!mInsertionControllerEnabled) {
741 hideInsertionPointCursorController();
742 if (mInsertionPointCursorController != null) {
743 mInsertionPointCursorController.onDetached();
744 mInsertionPointCursorController = null;
745 }
746 }
747
748 if (!mSelectionControllerEnabled) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100749 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -0700750 if (mSelectionModifierCursorController != null) {
751 mSelectionModifierCursorController.onDetached();
752 mSelectionModifierCursorController = null;
753 }
754 }
755 }
756
Seigo Nonakabb6a62c2015-03-31 21:59:30 +0900757 void hideInsertionPointCursorController() {
Gilles Debunned88876a2012-03-16 17:34:04 -0700758 if (mInsertionPointCursorController != null) {
759 mInsertionPointCursorController.hide();
760 }
761 }
762
763 /**
Mady Mellora2861452015-06-25 08:40:27 -0700764 * Hides the insertion and span controllers.
Gilles Debunned88876a2012-03-16 17:34:04 -0700765 */
Mady Mellora2861452015-06-25 08:40:27 -0700766 void hideCursorAndSpanControllers() {
Gilles Debunned88876a2012-03-16 17:34:04 -0700767 hideCursorControllers();
768 hideSpanControllers();
769 }
770
771 private void hideSpanControllers() {
Jean Chalardbaf30942013-02-28 16:01:51 -0800772 if (mSpanController != null) {
773 mSpanController.hide();
Gilles Debunned88876a2012-03-16 17:34:04 -0700774 }
775 }
776
777 private void hideCursorControllers() {
Yohei Yukawa85d08f12015-04-29 20:12:37 -0700778 // When mTextView is not ExtractEditText, we need to distinguish two kinds of focus-lost.
779 // One is the true focus lost where suggestions pop-up (if any) should be dismissed, and the
780 // other is an side effect of showing the suggestions pop-up itself. We use isShowingUp()
781 // to distinguish one from the other.
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700782 if (mSuggestionsPopupWindow != null && ((mTextView.isInExtractedMode())
783 || !mSuggestionsPopupWindow.isShowingUp())) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700784 // Should be done before hide insertion point controller since it triggers a show of it
785 mSuggestionsPopupWindow.hide();
786 }
787 hideInsertionPointCursorController();
Gilles Debunned88876a2012-03-16 17:34:04 -0700788 }
789
790 /**
791 * Create new SpellCheckSpans on the modified region.
792 */
793 private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
Satoshi Kataokad7429c12013-06-05 16:30:23 +0900794 // Remove spans whose adjacent characters are text not punctuation
795 mTextView.removeAdjacentSuggestionSpans(start);
796 mTextView.removeAdjacentSuggestionSpans(end);
797
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700798 if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled()
799 && !(mTextView.isInExtractedMode())) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700800 if (mSpellChecker == null && createSpellChecker) {
801 mSpellChecker = new SpellChecker(mTextView);
802 }
803 if (mSpellChecker != null) {
804 mSpellChecker.spellCheck(start, end);
805 }
806 }
807 }
808
809 void onScreenStateChanged(int screenState) {
810 switch (screenState) {
811 case View.SCREEN_STATE_ON:
812 resumeBlink();
813 break;
814 case View.SCREEN_STATE_OFF:
815 suspendBlink();
816 break;
817 }
818 }
819
820 private void suspendBlink() {
821 if (mBlink != null) {
822 mBlink.cancel();
823 }
824 }
825
826 private void resumeBlink() {
827 if (mBlink != null) {
828 mBlink.uncancel();
829 makeBlink();
830 }
831 }
832
833 void adjustInputType(boolean password, boolean passwordInputType,
834 boolean webPasswordInputType, boolean numberPasswordInputType) {
835 // mInputType has been set from inputType, possibly modified by mInputMethod.
836 // Specialize mInputType to [web]password if we have a text class and the original input
837 // type was a password.
838 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
839 if (password || passwordInputType) {
840 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
841 | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
842 }
843 if (webPasswordInputType) {
844 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
845 | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
846 }
847 } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
848 if (numberPasswordInputType) {
849 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
850 | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
851 }
852 }
853 }
854
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700855 private void chooseSize(@NonNull PopupWindow pop, @NonNull CharSequence text,
856 @NonNull TextView tv) {
857 final int wid = tv.getPaddingLeft() + tv.getPaddingRight();
858 final int ht = tv.getPaddingTop() + tv.getPaddingBottom();
Gilles Debunned88876a2012-03-16 17:34:04 -0700859
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700860 final int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize(
Gilles Debunned88876a2012-03-16 17:34:04 -0700861 com.android.internal.R.dimen.textview_error_popup_default_width);
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700862 final StaticLayout l = StaticLayout.Builder.obtain(text, 0, text.length(), tv.getPaint(),
863 defaultWidthInPixels)
864 .setUseLineSpacingFromFallbacks(tv.mUseFallbackLineSpacing)
865 .build();
866
Gilles Debunned88876a2012-03-16 17:34:04 -0700867 float max = 0;
868 for (int i = 0; i < l.getLineCount(); i++) {
869 max = Math.max(max, l.getLineWidth(i));
870 }
871
872 /*
873 * Now set the popup size to be big enough for the text plus the border capped
874 * to DEFAULT_MAX_POPUP_WIDTH
875 */
876 pop.setWidth(wid + (int) Math.ceil(max));
877 pop.setHeight(ht + l.getHeight());
878 }
879
880 void setFrame() {
881 if (mErrorPopup != null) {
882 TextView tv = (TextView) mErrorPopup.getContentView();
883 chooseSize(mErrorPopup, mError, tv);
884 mErrorPopup.update(mTextView, getErrorX(), getErrorY(),
885 mErrorPopup.getWidth(), mErrorPopup.getHeight());
886 }
887 }
888
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800889 private int getWordStart(int offset) {
890 // FIXME - For this and similar methods we're not doing anything to check if there's
891 // a LocaleSpan in the text, this may be something we should try handling or checking for.
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700892 int retOffset = getWordIteratorWithText().prevBoundary(offset);
Mady Mellor58c90872015-05-12 11:09:37 -0700893 if (getWordIteratorWithText().isOnPunctuation(retOffset)) {
894 // On punctuation boundary or within group of punctuation, find punctuation start.
895 retOffset = getWordIteratorWithText().getPunctuationBeginning(offset);
896 } else {
897 // Not on a punctuation boundary, find the word start.
Mady Mellore264ac32015-06-22 16:46:29 -0700898 retOffset = getWordIteratorWithText().getPrevWordBeginningOnTwoWordsBoundary(offset);
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800899 }
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700900 if (retOffset == BreakIterator.DONE) {
901 return offset;
902 }
903 return retOffset;
904 }
905
906 private int getWordEnd(int offset) {
907 int retOffset = getWordIteratorWithText().nextBoundary(offset);
Mady Mellor58c90872015-05-12 11:09:37 -0700908 if (getWordIteratorWithText().isAfterPunctuation(retOffset)) {
909 // On punctuation boundary or within group of punctuation, find punctuation end.
910 retOffset = getWordIteratorWithText().getPunctuationEnd(offset);
911 } else {
912 // Not on a punctuation boundary, find the word end.
Mady Mellore264ac32015-06-22 16:46:29 -0700913 retOffset = getWordIteratorWithText().getNextWordEndOnTwoWordBoundary(offset);
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700914 }
915 if (retOffset == BreakIterator.DONE) {
916 return offset;
917 }
918 return retOffset;
919 }
920
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900921 private boolean needsToSelectAllToSelectWordOrParagraph() {
Andrei Stingaceanu47f82ae2015-04-28 17:43:54 +0100922 if (mTextView.hasPasswordTransformationMethod()) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700923 // Always select all on a password field.
924 // Cut/copy menu entries are not available for passwords, but being able to select all
925 // is however useful to delete or paste to replace the entire content.
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900926 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -0700927 }
928
929 int inputType = mTextView.getInputType();
930 int klass = inputType & InputType.TYPE_MASK_CLASS;
931 int variation = inputType & InputType.TYPE_MASK_VARIATION;
932
933 // Specific text field types: select the entire text for these
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700934 if (klass == InputType.TYPE_CLASS_NUMBER
935 || klass == InputType.TYPE_CLASS_PHONE
936 || klass == InputType.TYPE_CLASS_DATETIME
937 || variation == InputType.TYPE_TEXT_VARIATION_URI
938 || variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
939 || variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS
940 || variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900941 return true;
942 }
943 return false;
944 }
945
946 /**
947 * Adjusts selection to the word under last touch offset. Return true if the operation was
948 * successfully performed.
949 */
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100950 boolean selectCurrentWord() {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900951 if (!mTextView.canSelectText()) {
952 return false;
953 }
954
955 if (needsToSelectAllToSelectWordOrParagraph()) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700956 return mTextView.selectAllText();
957 }
958
959 long lastTouchOffsets = getLastTouchOffsets();
960 final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
961 final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
962
963 // Safety check in case standard touch event handling has been bypassed
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -0800964 if (minOffset < 0 || minOffset > mTextView.getText().length()) return false;
965 if (maxOffset < 0 || maxOffset > mTextView.getText().length()) return false;
Gilles Debunned88876a2012-03-16 17:34:04 -0700966
967 int selectionStart, selectionEnd;
968
969 // If a URLSpan (web address, email, phone...) is found at that position, select it.
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700970 URLSpan[] urlSpans =
971 ((Spanned) mTextView.getText()).getSpans(minOffset, maxOffset, URLSpan.class);
Gilles Debunned88876a2012-03-16 17:34:04 -0700972 if (urlSpans.length >= 1) {
973 URLSpan urlSpan = urlSpans[0];
974 selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan);
975 selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan);
976 } else {
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800977 // FIXME - We should check if there's a LocaleSpan in the text, this may be
978 // something we should try handling or checking for.
Gilles Debunned88876a2012-03-16 17:34:04 -0700979 final WordIterator wordIterator = getWordIterator();
980 wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset);
981
982 selectionStart = wordIterator.getBeginning(minOffset);
983 selectionEnd = wordIterator.getEnd(maxOffset);
984
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700985 if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE
986 || selectionStart == selectionEnd) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700987 // Possible when the word iterator does not properly handle the text's language
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +0900988 long range = getCharClusterRange(minOffset);
Gilles Debunned88876a2012-03-16 17:34:04 -0700989 selectionStart = TextUtils.unpackRangeStartFromLong(range);
990 selectionEnd = TextUtils.unpackRangeEndFromLong(range);
991 }
992 }
993
994 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
995 return selectionEnd > selectionStart;
996 }
997
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900998 /**
999 * Adjusts selection to the paragraph under last touch offset. Return true if the operation was
1000 * successfully performed.
1001 */
1002 private boolean selectCurrentParagraph() {
1003 if (!mTextView.canSelectText()) {
1004 return false;
1005 }
1006
1007 if (needsToSelectAllToSelectWordOrParagraph()) {
1008 return mTextView.selectAllText();
1009 }
1010
1011 long lastTouchOffsets = getLastTouchOffsets();
1012 final int minLastTouchOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
1013 final int maxLastTouchOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
1014
1015 final long paragraphsRange = getParagraphsRange(minLastTouchOffset, maxLastTouchOffset);
1016 final int start = TextUtils.unpackRangeStartFromLong(paragraphsRange);
1017 final int end = TextUtils.unpackRangeEndFromLong(paragraphsRange);
1018 if (start < end) {
1019 Selection.setSelection((Spannable) mTextView.getText(), start, end);
1020 return true;
1021 }
1022 return false;
1023 }
1024
1025 /**
1026 * Get the minimum range of paragraphs that contains startOffset and endOffset.
1027 */
1028 private long getParagraphsRange(int startOffset, int endOffset) {
1029 final Layout layout = mTextView.getLayout();
1030 if (layout == null) {
1031 return TextUtils.packRangeInLong(-1, -1);
1032 }
1033 final CharSequence text = mTextView.getText();
1034 int minLine = layout.getLineForOffset(startOffset);
1035 // Search paragraph start.
1036 while (minLine > 0) {
1037 final int prevLineEndOffset = layout.getLineEnd(minLine - 1);
1038 if (text.charAt(prevLineEndOffset - 1) == '\n') {
1039 break;
1040 }
1041 minLine--;
1042 }
1043 int maxLine = layout.getLineForOffset(endOffset);
1044 // Search paragraph end.
1045 while (maxLine < layout.getLineCount() - 1) {
1046 final int lineEndOffset = layout.getLineEnd(maxLine);
1047 if (text.charAt(lineEndOffset - 1) == '\n') {
1048 break;
1049 }
1050 maxLine++;
1051 }
1052 return TextUtils.packRangeInLong(layout.getLineStart(minLine), layout.getLineEnd(maxLine));
1053 }
1054
Gilles Debunned88876a2012-03-16 17:34:04 -07001055 void onLocaleChanged() {
Keisuke Kuroyanagie0ac5ac2016-03-09 15:33:30 +09001056 // Will be re-created on demand in getWordIterator and getWordIteratorWithText with the
1057 // proper new locale
Gilles Debunned88876a2012-03-16 17:34:04 -07001058 mWordIterator = null;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08001059 mWordIteratorWithText = null;
Gilles Debunned88876a2012-03-16 17:34:04 -07001060 }
1061
Gilles Debunned88876a2012-03-16 17:34:04 -07001062 public WordIterator getWordIterator() {
1063 if (mWordIterator == null) {
1064 mWordIterator = new WordIterator(mTextView.getTextServicesLocale());
1065 }
1066 return mWordIterator;
1067 }
1068
Mady Mellor2ff2cd82015-03-02 10:37:01 -08001069 private WordIterator getWordIteratorWithText() {
1070 if (mWordIteratorWithText == null) {
1071 mWordIteratorWithText = new WordIterator(mTextView.getTextServicesLocale());
1072 mUpdateWordIteratorText = true;
1073 }
1074 if (mUpdateWordIteratorText) {
1075 // FIXME - Shouldn't copy all of the text as only the area of the text relevant
1076 // to the user's selection is needed. A possible solution would be to
1077 // copy some number N of characters near the selection and then when the
1078 // user approaches N then we'd do another copy of the next N characters.
1079 CharSequence text = mTextView.getText();
1080 mWordIteratorWithText.setCharSequence(text, 0, text.length());
1081 mUpdateWordIteratorText = false;
1082 }
1083 return mWordIteratorWithText;
1084 }
1085
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +09001086 private int getNextCursorOffset(int offset, boolean findAfterGivenOffset) {
1087 final Layout layout = mTextView.getLayout();
1088 if (layout == null) return offset;
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001089 return findAfterGivenOffset == layout.isRtlCharAt(offset)
1090 ? layout.getOffsetToLeftOf(offset) : layout.getOffsetToRightOf(offset);
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +09001091 }
1092
1093 private long getCharClusterRange(int offset) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001094 final int textLength = mTextView.getText().length();
Gilles Debunned88876a2012-03-16 17:34:04 -07001095 if (offset < textLength) {
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08001096 final int clusterEndOffset = getNextCursorOffset(offset, true);
1097 return TextUtils.packRangeInLong(
1098 getNextCursorOffset(clusterEndOffset, false), clusterEndOffset);
Gilles Debunned88876a2012-03-16 17:34:04 -07001099 }
1100 if (offset - 1 >= 0) {
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08001101 final int clusterStartOffset = getNextCursorOffset(offset, false);
1102 return TextUtils.packRangeInLong(clusterStartOffset,
1103 getNextCursorOffset(clusterStartOffset, true));
Gilles Debunned88876a2012-03-16 17:34:04 -07001104 }
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +09001105 return TextUtils.packRangeInLong(offset, offset);
Gilles Debunned88876a2012-03-16 17:34:04 -07001106 }
1107
1108 private boolean touchPositionIsInSelection() {
1109 int selectionStart = mTextView.getSelectionStart();
1110 int selectionEnd = mTextView.getSelectionEnd();
1111
1112 if (selectionStart == selectionEnd) {
1113 return false;
1114 }
1115
1116 if (selectionStart > selectionEnd) {
1117 int tmp = selectionStart;
1118 selectionStart = selectionEnd;
1119 selectionEnd = tmp;
1120 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
1121 }
1122
1123 SelectionModifierCursorController selectionController = getSelectionController();
1124 int minOffset = selectionController.getMinTouchOffset();
1125 int maxOffset = selectionController.getMaxTouchOffset();
1126
1127 return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
1128 }
1129
1130 private PositionListener getPositionListener() {
1131 if (mPositionListener == null) {
1132 mPositionListener = new PositionListener();
1133 }
1134 return mPositionListener;
1135 }
1136
1137 private interface TextViewPositionListener {
1138 public void updatePosition(int parentPositionX, int parentPositionY,
1139 boolean parentPositionChanged, boolean parentScrolled);
1140 }
1141
Gilles Debunned88876a2012-03-16 17:34:04 -07001142 private boolean isOffsetVisible(int offset) {
1143 Layout layout = mTextView.getLayout();
Victoria Leaseb9b77ae2013-10-13 15:12:52 -07001144 if (layout == null) return false;
1145
Gilles Debunned88876a2012-03-16 17:34:04 -07001146 final int line = layout.getLineForOffset(offset);
1147 final int lineBottom = layout.getLineBottom(line);
1148 final int primaryHorizontal = (int) layout.getPrimaryHorizontal(offset);
Phil Weaverc2e28932016-12-08 12:29:25 -08001149 return mTextView.isPositionVisible(
1150 primaryHorizontal + mTextView.viewportToContentHorizontalOffset(),
Gilles Debunned88876a2012-03-16 17:34:04 -07001151 lineBottom + mTextView.viewportToContentVerticalOffset());
1152 }
1153
1154 /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
1155 * in the view. Returns false when the position is in the empty space of left/right of text.
1156 */
1157 private boolean isPositionOnText(float x, float y) {
1158 Layout layout = mTextView.getLayout();
1159 if (layout == null) return false;
1160
1161 final int line = mTextView.getLineAtCoordinate(y);
1162 x = mTextView.convertToLocalHorizontalCoordinate(x);
1163
1164 if (x < layout.getLineLeft(line)) return false;
1165 if (x > layout.getLineRight(line)) return false;
1166 return true;
1167 }
1168
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001169 private void startDragAndDrop() {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001170 getSelectionActionModeHelper().onSelectionDrag();
1171
Keisuke Kuroyanagifdfc93d2016-03-15 14:47:08 +09001172 // TODO: Fix drag and drop in full screen extracted mode.
1173 if (mTextView.isInExtractedMode()) {
1174 return;
1175 }
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001176 final int start = mTextView.getSelectionStart();
1177 final int end = mTextView.getSelectionEnd();
1178 CharSequence selectedText = mTextView.getTransformedText(start, end);
1179 ClipData data = ClipData.newPlainText(null, selectedText);
1180 DragLocalState localState = new DragLocalState(mTextView, start, end);
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08001181 mTextView.startDragAndDrop(data, getTextThumbnailBuilder(start, end), localState,
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001182 View.DRAG_FLAG_GLOBAL);
1183 stopTextActionMode();
1184 if (hasSelectionController()) {
1185 getSelectionController().resetTouchOffsets();
1186 }
1187 }
1188
Gilles Debunned88876a2012-03-16 17:34:04 -07001189 public boolean performLongClick(boolean handled) {
Nikita Dubrovsky05cfcc82019-10-24 08:57:32 -07001190 if (TextView.DEBUG_CURSOR) {
1191 logCursor("performLongClick", "handled=%s", handled);
1192 }
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001193 // Long press in empty space moves cursor and starts the insertion action mode.
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08001194 if (!handled && !isPositionOnText(mTouchState.getLastDownX(), mTouchState.getLastDownY())
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001195 && mInsertionControllerEnabled) {
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08001196 final int offset = mTextView.getOffsetForPosition(mTouchState.getLastDownX(),
1197 mTouchState.getLastDownY());
Gilles Debunned88876a2012-03-16 17:34:04 -07001198 Selection.setSelection((Spannable) mTextView.getText(), offset);
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00001199 getInsertionController().show();
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001200 mIsInsertionActionModeStartPending = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001201 handled = true;
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001202 MetricsLogger.action(
1203 mTextView.getContext(),
1204 MetricsEvent.TEXT_LONGPRESS,
1205 TextViewMetrics.SUBTYPE_LONG_PRESS_OTHER);
Gilles Debunned88876a2012-03-16 17:34:04 -07001206 }
1207
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001208 if (!handled && mTextActionMode != null) {
Andrei Stingaceanu2aaeefe2015-10-20 19:11:23 +01001209 if (touchPositionIsInSelection()) {
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001210 startDragAndDrop();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001211 MetricsLogger.action(
1212 mTextView.getContext(),
1213 MetricsEvent.TEXT_LONGPRESS,
1214 TextViewMetrics.SUBTYPE_LONG_PRESS_DRAG_AND_DROP);
Gilles Debunned88876a2012-03-16 17:34:04 -07001215 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001216 stopTextActionMode();
Clara Bayarridfac4432015-05-15 12:18:24 +01001217 selectCurrentWordAndStartDrag();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001218 MetricsLogger.action(
1219 mTextView.getContext(),
1220 MetricsEvent.TEXT_LONGPRESS,
1221 TextViewMetrics.SUBTYPE_LONG_PRESS_SELECTION);
Gilles Debunned88876a2012-03-16 17:34:04 -07001222 }
1223 handled = true;
1224 }
1225
1226 // Start a new selection
1227 if (!handled) {
Clara Bayarridfac4432015-05-15 12:18:24 +01001228 handled = selectCurrentWordAndStartDrag();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001229 if (handled) {
1230 MetricsLogger.action(
1231 mTextView.getContext(),
1232 MetricsEvent.TEXT_LONGPRESS,
1233 TextViewMetrics.SUBTYPE_LONG_PRESS_SELECTION);
1234 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001235 }
1236
1237 return handled;
1238 }
1239
Petar Å egina91df3f92017-08-15 16:20:43 +01001240 float getLastUpPositionX() {
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08001241 return mTouchState.getLastUpX();
Petar Å egina91df3f92017-08-15 16:20:43 +01001242 }
1243
1244 float getLastUpPositionY() {
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08001245 return mTouchState.getLastUpY();
Petar Å egina91df3f92017-08-15 16:20:43 +01001246 }
1247
Gilles Debunned88876a2012-03-16 17:34:04 -07001248 private long getLastTouchOffsets() {
1249 SelectionModifierCursorController selectionController = getSelectionController();
1250 final int minOffset = selectionController.getMinTouchOffset();
1251 final int maxOffset = selectionController.getMaxTouchOffset();
1252 return TextUtils.packRangeInLong(minOffset, maxOffset);
1253 }
1254
1255 void onFocusChanged(boolean focused, int direction) {
Nikita Dubrovsky05cfcc82019-10-24 08:57:32 -07001256 if (TextView.DEBUG_CURSOR) {
1257 logCursor("onFocusChanged", "focused=%s", focused);
1258 }
1259
Gilles Debunned88876a2012-03-16 17:34:04 -07001260 mShowCursor = SystemClock.uptimeMillis();
1261 ensureEndedBatchEdit();
1262
1263 if (focused) {
1264 int selStart = mTextView.getSelectionStart();
1265 int selEnd = mTextView.getSelectionEnd();
1266
1267 // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
1268 // mode for these, unless there was a specific selection already started.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001269 final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0
1270 && selEnd == mTextView.getText().length();
Gilles Debunned88876a2012-03-16 17:34:04 -07001271
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001272 mCreatedWithASelection = mFrozenWithFocus && mTextView.hasSelection()
1273 && !isFocusHighlighted;
Gilles Debunned88876a2012-03-16 17:34:04 -07001274
1275 if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
1276 // If a tap was used to give focus to that view, move cursor at tap position.
1277 // Has to be done before onTakeFocus, which can be overloaded.
1278 final int lastTapPosition = getLastTapPosition();
1279 if (lastTapPosition >= 0) {
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08001280 if (TextView.DEBUG_CURSOR) {
1281 logCursor("onFocusChanged", "setting cursor position: %d", lastTapPosition);
1282 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001283 Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition);
1284 }
1285
1286 // Note this may have to be moved out of the Editor class
1287 MovementMethod mMovement = mTextView.getMovementMethod();
1288 if (mMovement != null) {
1289 mMovement.onTakeFocus(mTextView, (Spannable) mTextView.getText(), direction);
1290 }
1291
1292 // The DecorView does not have focus when the 'Done' ExtractEditText button is
1293 // pressed. Since it is the ViewAncestor's mView, it requests focus before
1294 // ExtractEditText clears focus, which gives focus to the ExtractEditText.
1295 // This special case ensure that we keep current selection in that case.
1296 // It would be better to know why the DecorView does not have focus at that time.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001297 if (((mTextView.isInExtractedMode()) || mSelectionMoved)
1298 && selStart >= 0 && selEnd >= 0) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001299 /*
1300 * Someone intentionally set the selection, so let them
1301 * do whatever it is that they wanted to do instead of
1302 * the default on-focus behavior. We reset the selection
1303 * here instead of just skipping the onTakeFocus() call
1304 * because some movement methods do something other than
1305 * just setting the selection in theirs and we still
1306 * need to go through that path.
1307 */
1308 Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
1309 }
1310
1311 if (mSelectAllOnFocus) {
1312 mTextView.selectAllText();
1313 }
1314
1315 mTouchFocusSelected = true;
1316 }
1317
1318 mFrozenWithFocus = false;
1319 mSelectionMoved = false;
1320
1321 if (mError != null) {
1322 showError();
1323 }
1324
1325 makeBlink();
1326 } else {
1327 if (mError != null) {
1328 hideError();
1329 }
1330 // Don't leave us in the middle of a batch edit.
1331 mTextView.onEndBatchEdit();
1332
Andrei Stingaceanub1891b32015-06-19 16:44:37 +01001333 if (mTextView.isInExtractedMode()) {
Mady Mellora2861452015-06-25 08:40:27 -07001334 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001335 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -07001336 } else {
Mady Mellora2861452015-06-25 08:40:27 -07001337 hideCursorAndSpanControllers();
Yohei Yukawa24df9312016-03-31 17:15:23 -07001338 if (mTextView.isTemporarilyDetached()) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001339 stopTextActionModeWithPreservingSelection();
1340 } else {
1341 stopTextActionMode();
1342 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001343 downgradeEasyCorrectionSpans();
1344 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001345 // No need to create the controller
1346 if (mSelectionModifierCursorController != null) {
1347 mSelectionModifierCursorController.resetTouchOffsets();
1348 }
Richard Ledley5f2f8202018-02-05 14:55:47 +00001349
1350 ensureNoSelectionIfNonSelectable();
1351 }
1352 }
1353
1354 private void ensureNoSelectionIfNonSelectable() {
1355 // This could be the case if a TextLink has been tapped.
1356 if (!mTextView.textCanBeSelected() && mTextView.hasSelection()) {
1357 Selection.setSelection((Spannable) mTextView.getText(),
1358 mTextView.length(), mTextView.length());
Gilles Debunned88876a2012-03-16 17:34:04 -07001359 }
1360 }
1361
1362 /**
1363 * Downgrades to simple suggestions all the easy correction spans that are not a spell check
1364 * span.
1365 */
1366 private void downgradeEasyCorrectionSpans() {
1367 CharSequence text = mTextView.getText();
1368 if (text instanceof Spannable) {
1369 Spannable spannable = (Spannable) text;
1370 SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
1371 spannable.length(), SuggestionSpan.class);
1372 for (int i = 0; i < suggestionSpans.length; i++) {
1373 int flags = suggestionSpans[i].getFlags();
1374 if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
1375 && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
1376 flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
1377 suggestionSpans[i].setFlags(flags);
1378 }
1379 }
1380 }
1381 }
1382
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +01001383 void sendOnTextChanged(int start, int before, int after) {
1384 getSelectionActionModeHelper().onTextChanged(start, start + before);
Gilles Debunned88876a2012-03-16 17:34:04 -07001385 updateSpellCheckSpans(start, start + after, false);
1386
Mady Mellor2ff2cd82015-03-02 10:37:01 -08001387 // Flip flag to indicate the word iterator needs to have the text reset.
1388 mUpdateWordIteratorText = true;
1389
Gilles Debunned88876a2012-03-16 17:34:04 -07001390 // Hide the controllers as soon as text is modified (typing, procedural...)
1391 // We do not hide the span controllers, since they can be added when a new text is
1392 // inserted into the text view (voice IME).
1393 hideCursorControllers();
Keisuke Kuroyanagif4e347d2015-06-11 17:41:00 +09001394 // Reset drag accelerator.
1395 if (mSelectionModifierCursorController != null) {
1396 mSelectionModifierCursorController.resetTouchOffsets();
1397 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001398 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07001399 }
1400
1401 private int getLastTapPosition() {
1402 // No need to create the controller at that point, no last tap position saved
1403 if (mSelectionModifierCursorController != null) {
1404 int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
1405 if (lastTapPosition >= 0) {
1406 // Safety check, should not be possible.
1407 if (lastTapPosition > mTextView.getText().length()) {
1408 lastTapPosition = mTextView.getText().length();
1409 }
1410 return lastTapPosition;
1411 }
1412 }
1413
1414 return -1;
1415 }
1416
1417 void onWindowFocusChanged(boolean hasWindowFocus) {
1418 if (hasWindowFocus) {
1419 if (mBlink != null) {
1420 mBlink.uncancel();
1421 makeBlink();
1422 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001423 if (mTextView.hasSelection() && !extractedTextModeWillBeStarted()) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001424 refreshTextActionMode();
Mady Mellora2861452015-06-25 08:40:27 -07001425 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001426 } else {
1427 if (mBlink != null) {
1428 mBlink.cancel();
1429 }
1430 if (mInputContentType != null) {
1431 mInputContentType.enterDown = false;
1432 }
1433 // Order matters! Must be done before onParentLostFocus to rely on isShowingUp
Mady Mellora2861452015-06-25 08:40:27 -07001434 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001435 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -07001436 if (mSuggestionsPopupWindow != null) {
1437 mSuggestionsPopupWindow.onParentLostFocus();
1438 }
1439
Gilles Debunnec72fba82012-06-26 14:47:07 -07001440 // Don't leave us in the middle of a batch edit. Same as in onFocusChanged
1441 ensureEndedBatchEdit();
Richard Ledley5f2f8202018-02-05 14:55:47 +00001442
1443 ensureNoSelectionIfNonSelectable();
Gilles Debunned88876a2012-03-16 17:34:04 -07001444 }
1445 }
1446
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09001447 private boolean shouldFilterOutTouchEvent(MotionEvent event) {
1448 if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) {
1449 return false;
1450 }
1451 final boolean primaryButtonStateChanged =
1452 ((mLastButtonState ^ event.getButtonState()) & MotionEvent.BUTTON_PRIMARY) != 0;
1453 final int action = event.getActionMasked();
1454 if ((action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_UP)
1455 && !primaryButtonStateChanged) {
1456 return true;
1457 }
1458 if (action == MotionEvent.ACTION_MOVE
1459 && !event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
1460 return true;
1461 }
1462 return false;
1463 }
1464
Gilles Debunned88876a2012-03-16 17:34:04 -07001465 void onTouchEvent(MotionEvent event) {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09001466 final boolean filterOutEvent = shouldFilterOutTouchEvent(event);
1467 mLastButtonState = event.getButtonState();
1468 if (filterOutEvent) {
1469 if (event.getActionMasked() == MotionEvent.ACTION_UP) {
1470 mDiscardNextActionUp = true;
1471 }
1472 return;
1473 }
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08001474 ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext());
1475 mTouchState.update(event, viewConfiguration);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001476 updateFloatingToolbarVisibility(event);
1477
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08001478 if (hasInsertionController()) {
1479 getInsertionController().onTouchEvent(event);
1480 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001481 if (hasSelectionController()) {
1482 getSelectionController().onTouchEvent(event);
1483 }
1484
1485 if (mShowSuggestionRunnable != null) {
1486 mTextView.removeCallbacks(mShowSuggestionRunnable);
1487 mShowSuggestionRunnable = null;
1488 }
1489
1490 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001491 // Reset this state; it will be re-set if super.onTouchEvent
1492 // causes focus to move to the view.
1493 mTouchFocusSelected = false;
1494 mIgnoreActionUpEvent = false;
1495 }
1496 }
1497
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001498 private void updateFloatingToolbarVisibility(MotionEvent event) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001499 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001500 switch (event.getActionMasked()) {
1501 case MotionEvent.ACTION_MOVE:
Abodunrinwa Toki4a7aeb32017-07-13 02:06:56 +01001502 hideFloatingToolbar(ActionMode.DEFAULT_HIDE_DURATION);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001503 break;
1504 case MotionEvent.ACTION_UP: // fall through
1505 case MotionEvent.ACTION_CANCEL:
1506 showFloatingToolbar();
1507 }
1508 }
1509 }
1510
Abodunrinwa Toki4a7aeb32017-07-13 02:06:56 +01001511 void hideFloatingToolbar(int duration) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001512 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001513 mTextView.removeCallbacks(mShowFloatingToolbar);
Abodunrinwa Toki4a7aeb32017-07-13 02:06:56 +01001514 mTextActionMode.hide(duration);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001515 }
1516 }
1517
1518 private void showFloatingToolbar() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001519 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001520 // Delay "show" so it doesn't interfere with click confirmations
1521 // or double-clicks that could "dismiss" the floating toolbar.
1522 int delay = ViewConfiguration.getDoubleTapTimeout();
1523 mTextView.postDelayed(mShowFloatingToolbar, delay);
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01001524
1525 // This classifies the text and most likely returns before the toolbar is actually
1526 // shown. If not, it will update the toolbar with the result when classification
1527 // returns. We would rather not wait for a long running classification process.
1528 invalidateActionModeAsync();
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001529 }
1530 }
1531
Yohei Yukawa484d4af2018-09-17 16:47:08 -07001532 private InputMethodManager getInputMethodManager() {
1533 return mTextView.getContext().getSystemService(InputMethodManager.class);
1534 }
1535
Gilles Debunned88876a2012-03-16 17:34:04 -07001536 public void beginBatchEdit() {
1537 mInBatchEditControllers = true;
1538 final InputMethodState ims = mInputMethodState;
1539 if (ims != null) {
1540 int nesting = ++ims.mBatchEditNesting;
1541 if (nesting == 1) {
1542 ims.mCursorChanged = false;
1543 ims.mChangedDelta = 0;
1544 if (ims.mContentChanged) {
1545 // We already have a pending change from somewhere else,
1546 // so turn this into a full update.
1547 ims.mChangedStart = 0;
1548 ims.mChangedEnd = mTextView.getText().length();
1549 } else {
1550 ims.mChangedStart = EXTRACT_UNKNOWN;
1551 ims.mChangedEnd = EXTRACT_UNKNOWN;
1552 ims.mContentChanged = false;
1553 }
James Cook48e0fac2015-02-25 15:44:51 -08001554 mUndoInputFilter.beginBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001555 mTextView.onBeginBatchEdit();
1556 }
1557 }
1558 }
1559
1560 public void endBatchEdit() {
1561 mInBatchEditControllers = false;
1562 final InputMethodState ims = mInputMethodState;
1563 if (ims != null) {
1564 int nesting = --ims.mBatchEditNesting;
1565 if (nesting == 0) {
1566 finishBatchEdit(ims);
1567 }
1568 }
1569 }
1570
1571 void ensureEndedBatchEdit() {
1572 final InputMethodState ims = mInputMethodState;
1573 if (ims != null && ims.mBatchEditNesting != 0) {
1574 ims.mBatchEditNesting = 0;
1575 finishBatchEdit(ims);
1576 }
1577 }
1578
1579 void finishBatchEdit(final InputMethodState ims) {
1580 mTextView.onEndBatchEdit();
James Cook48e0fac2015-02-25 15:44:51 -08001581 mUndoInputFilter.endBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001582
1583 if (ims.mContentChanged || ims.mSelectionModeChanged) {
1584 mTextView.updateAfterEdit();
1585 reportExtractedText();
1586 } else if (ims.mCursorChanged) {
Jean Chalardc99d33f2013-02-28 16:39:47 -08001587 // Cheesy way to get us to report the current cursor location.
Gilles Debunned88876a2012-03-16 17:34:04 -07001588 mTextView.invalidateCursor();
1589 }
Jean Chalardc99d33f2013-02-28 16:39:47 -08001590 // sendUpdateSelection knows to avoid sending if the selection did
1591 // not actually change.
1592 sendUpdateSelection();
Keisuke Kuroyanagic6fad962016-05-02 15:11:41 +09001593
1594 // Show drag handles if they were blocked by batch edit mode.
1595 if (mTextActionMode != null) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001596 final CursorController cursorController = mTextView.hasSelection()
1597 ? getSelectionController() : getInsertionController();
Keisuke Kuroyanagic6fad962016-05-02 15:11:41 +09001598 if (cursorController != null && !cursorController.isActive()
1599 && !cursorController.isCursorBeingModified()) {
1600 cursorController.show();
1601 }
1602 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001603 }
1604
1605 static final int EXTRACT_NOTHING = -2;
1606 static final int EXTRACT_UNKNOWN = -1;
1607
1608 boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
1609 return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
1610 EXTRACT_UNKNOWN, outText);
1611 }
1612
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001613 private boolean extractTextInternal(@Nullable ExtractedTextRequest request,
Gilles Debunned88876a2012-03-16 17:34:04 -07001614 int partialStartOffset, int partialEndOffset, int delta,
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001615 @Nullable ExtractedText outText) {
1616 if (request == null || outText == null) {
1617 return false;
Gilles Debunned88876a2012-03-16 17:34:04 -07001618 }
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001619
1620 final CharSequence content = mTextView.getText();
1621 if (content == null) {
1622 return false;
1623 }
1624
1625 if (partialStartOffset != EXTRACT_NOTHING) {
1626 final int N = content.length();
1627 if (partialStartOffset < 0) {
1628 outText.partialStartOffset = outText.partialEndOffset = -1;
1629 partialStartOffset = 0;
1630 partialEndOffset = N;
1631 } else {
1632 // Now use the delta to determine the actual amount of text
1633 // we need.
1634 partialEndOffset += delta;
1635 // Adjust offsets to ensure we contain full spans.
1636 if (content instanceof Spanned) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001637 Spanned spanned = (Spanned) content;
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001638 Object[] spans = spanned.getSpans(partialStartOffset,
1639 partialEndOffset, ParcelableSpan.class);
1640 int i = spans.length;
1641 while (i > 0) {
1642 i--;
1643 int j = spanned.getSpanStart(spans[i]);
1644 if (j < partialStartOffset) partialStartOffset = j;
1645 j = spanned.getSpanEnd(spans[i]);
1646 if (j > partialEndOffset) partialEndOffset = j;
1647 }
1648 }
1649 outText.partialStartOffset = partialStartOffset;
1650 outText.partialEndOffset = partialEndOffset - delta;
1651
1652 if (partialStartOffset > N) {
1653 partialStartOffset = N;
1654 } else if (partialStartOffset < 0) {
1655 partialStartOffset = 0;
1656 }
1657 if (partialEndOffset > N) {
1658 partialEndOffset = N;
1659 } else if (partialEndOffset < 0) {
1660 partialEndOffset = 0;
1661 }
1662 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001663 if ((request.flags & InputConnection.GET_TEXT_WITH_STYLES) != 0) {
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001664 outText.text = content.subSequence(partialStartOffset,
1665 partialEndOffset);
1666 } else {
1667 outText.text = TextUtils.substring(content, partialStartOffset,
1668 partialEndOffset);
1669 }
1670 } else {
1671 outText.partialStartOffset = 0;
1672 outText.partialEndOffset = 0;
1673 outText.text = "";
1674 }
1675 outText.flags = 0;
1676 if (MetaKeyKeyListener.getMetaState(content, MetaKeyKeyListener.META_SELECTING) != 0) {
1677 outText.flags |= ExtractedText.FLAG_SELECTING;
1678 }
1679 if (mTextView.isSingleLine()) {
1680 outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
1681 }
1682 outText.startOffset = 0;
1683 outText.selectionStart = mTextView.getSelectionStart();
1684 outText.selectionEnd = mTextView.getSelectionEnd();
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001685 outText.hint = mTextView.getHint();
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001686 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001687 }
1688
1689 boolean reportExtractedText() {
1690 final Editor.InputMethodState ims = mInputMethodState;
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001691 if (ims == null) {
1692 return false;
1693 }
Clara Bayarri038f7a82018-06-04 15:00:07 +01001694 final boolean wasContentChanged = ims.mContentChanged;
1695 if (!wasContentChanged && !ims.mSelectionModeChanged) {
1696 return false;
1697 }
1698 ims.mContentChanged = false;
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001699 ims.mSelectionModeChanged = false;
1700 final ExtractedTextRequest req = ims.mExtractedTextRequest;
1701 if (req == null) {
1702 return false;
1703 }
Yohei Yukawa484d4af2018-09-17 16:47:08 -07001704 final InputMethodManager imm = getInputMethodManager();
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001705 if (imm == null) {
1706 return false;
1707 }
1708 if (TextView.DEBUG_EXTRACT) {
1709 Log.v(TextView.LOG_TAG, "Retrieving extracted start="
1710 + ims.mChangedStart
1711 + " end=" + ims.mChangedEnd
1712 + " delta=" + ims.mChangedDelta);
1713 }
Clara Bayarri038f7a82018-06-04 15:00:07 +01001714 if (ims.mChangedStart < 0 && !wasContentChanged) {
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001715 ims.mChangedStart = EXTRACT_NOTHING;
1716 }
1717 if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
1718 ims.mChangedDelta, ims.mExtractedText)) {
1719 if (TextView.DEBUG_EXTRACT) {
1720 Log.v(TextView.LOG_TAG,
1721 "Reporting extracted start="
1722 + ims.mExtractedText.partialStartOffset
1723 + " end=" + ims.mExtractedText.partialEndOffset
1724 + ": " + ims.mExtractedText.text);
Gilles Debunned88876a2012-03-16 17:34:04 -07001725 }
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001726
1727 imm.updateExtractedText(mTextView, req.token, ims.mExtractedText);
1728 ims.mChangedStart = EXTRACT_UNKNOWN;
1729 ims.mChangedEnd = EXTRACT_UNKNOWN;
1730 ims.mChangedDelta = 0;
1731 ims.mContentChanged = false;
1732 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001733 }
1734 return false;
1735 }
1736
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001737 private void sendUpdateSelection() {
1738 if (null != mInputMethodState && mInputMethodState.mBatchEditNesting <= 0) {
Yohei Yukawa484d4af2018-09-17 16:47:08 -07001739 final InputMethodManager imm = getInputMethodManager();
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001740 if (null != imm) {
1741 final int selectionStart = mTextView.getSelectionStart();
1742 final int selectionEnd = mTextView.getSelectionEnd();
1743 int candStart = -1;
1744 int candEnd = -1;
1745 if (mTextView.getText() instanceof Spannable) {
1746 final Spannable sp = (Spannable) mTextView.getText();
1747 candStart = EditableInputConnection.getComposingSpanStart(sp);
1748 candEnd = EditableInputConnection.getComposingSpanEnd(sp);
1749 }
Jean Chalardc99d33f2013-02-28 16:39:47 -08001750 // InputMethodManager#updateSelection skips sending the message if
1751 // none of the parameters have changed since the last time we called it.
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001752 imm.updateSelection(mTextView,
1753 selectionStart, selectionEnd, candStart, candEnd);
1754 }
1755 }
1756 }
1757
Gilles Debunned88876a2012-03-16 17:34:04 -07001758 void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint,
1759 int cursorOffsetVertical) {
1760 final int selectionStart = mTextView.getSelectionStart();
1761 final int selectionEnd = mTextView.getSelectionEnd();
1762
1763 final InputMethodState ims = mInputMethodState;
1764 if (ims != null && ims.mBatchEditNesting == 0) {
Yohei Yukawa484d4af2018-09-17 16:47:08 -07001765 InputMethodManager imm = getInputMethodManager();
Gilles Debunned88876a2012-03-16 17:34:04 -07001766 if (imm != null) {
1767 if (imm.isActive(mTextView)) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001768 if (ims.mContentChanged || ims.mSelectionModeChanged) {
1769 // We are in extract mode and the content has changed
1770 // in some way... just report complete new text to the
1771 // input method.
Yohei Yukawab6bec1a2015-05-01 16:18:25 -07001772 reportExtractedText();
Gilles Debunned88876a2012-03-16 17:34:04 -07001773 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001774 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001775 }
1776 }
1777
1778 if (mCorrectionHighlighter != null) {
1779 mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
1780 }
1781
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07001782 if (highlight != null && selectionStart == selectionEnd && mDrawableForCursor != null) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001783 drawCursor(canvas, cursorOffsetVertical);
1784 // Rely on the drawable entirely, do not draw the cursor line.
1785 // Has to be done after the IMM related code above which relies on the highlight.
1786 highlight = null;
1787 }
1788
Jan Althaus80620c52018-02-02 17:39:22 +01001789 if (mSelectionActionModeHelper != null) {
1790 mSelectionActionModeHelper.onDraw(canvas);
1791 if (mSelectionActionModeHelper.isDrawingHighlight()) {
1792 highlight = null;
1793 }
1794 }
1795
Gilles Debunned88876a2012-03-16 17:34:04 -07001796 if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
1797 drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
1798 cursorOffsetVertical);
1799 } else {
1800 layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
1801 }
1802 }
1803
1804 private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
1805 Paint highlightPaint, int cursorOffsetVertical) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001806 final long lineRange = layout.getLineRangeForDraw(canvas);
1807 int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
1808 int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
1809 if (lastLine < 0) return;
1810
1811 layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
1812 firstLine, lastLine);
1813
1814 if (layout instanceof DynamicLayout) {
Chris Craik956f3402015-04-27 16:41:00 -07001815 if (mTextRenderNodes == null) {
1816 mTextRenderNodes = ArrayUtils.emptyArray(TextRenderNode.class);
Gilles Debunned88876a2012-03-16 17:34:04 -07001817 }
1818
1819 DynamicLayout dynamicLayout = (DynamicLayout) layout;
Gilles Debunne157aafc2012-04-19 17:21:57 -07001820 int[] blockEndLines = dynamicLayout.getBlockEndLines();
Gilles Debunned88876a2012-03-16 17:34:04 -07001821 int[] blockIndices = dynamicLayout.getBlockIndices();
1822 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
Sangkyu Lee955beb22012-12-10 15:47:00 +09001823 final int indexFirstChangedBlock = dynamicLayout.getIndexFirstChangedBlock();
Gilles Debunned88876a2012-03-16 17:34:04 -07001824
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001825 final ArraySet<Integer> blockSet = dynamicLayout.getBlocksAlwaysNeedToBeRedrawn();
1826 if (blockSet != null) {
1827 for (int i = 0; i < blockSet.size(); i++) {
1828 final int blockIndex = dynamicLayout.getBlockIndex(blockSet.valueAt(i));
1829 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
1830 && mTextRenderNodes[blockIndex] != null) {
1831 mTextRenderNodes[blockIndex].needsToBeShifted = true;
1832 }
1833 }
1834 }
1835
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001836 int startBlock = Arrays.binarySearch(blockEndLines, 0, numberOfBlocks, firstLine);
1837 if (startBlock < 0) {
1838 startBlock = -(startBlock + 1);
1839 }
1840 startBlock = Math.min(indexFirstChangedBlock, startBlock);
Gilles Debunned88876a2012-03-16 17:34:04 -07001841
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001842 int startIndexToFindAvailableRenderNode = 0;
1843 int lastIndex = numberOfBlocks;
1844
1845 for (int i = startBlock; i < numberOfBlocks; i++) {
1846 final int blockIndex = blockIndices[i];
1847 if (i >= indexFirstChangedBlock
1848 && blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
1849 && mTextRenderNodes[blockIndex] != null) {
1850 mTextRenderNodes[blockIndex].needsToBeShifted = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001851 }
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001852 if (blockEndLines[i] < firstLine) {
1853 // Blocks in [indexFirstChangedBlock, firstLine) are not redrawn here. They will
1854 // be redrawn after they get scrolled into drawing range.
1855 continue;
Gilles Debunned88876a2012-03-16 17:34:04 -07001856 }
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001857 startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas, layout,
1858 highlight, highlightPaint, cursorOffsetVertical, blockEndLines,
1859 blockIndices, i, numberOfBlocks, startIndexToFindAvailableRenderNode);
1860 if (blockEndLines[i] >= lastLine) {
1861 lastIndex = Math.max(indexFirstChangedBlock, i + 1);
1862 break;
Gilles Debunned88876a2012-03-16 17:34:04 -07001863 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001864 }
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001865 if (blockSet != null) {
1866 for (int i = 0; i < blockSet.size(); i++) {
1867 final int block = blockSet.valueAt(i);
1868 final int blockIndex = dynamicLayout.getBlockIndex(block);
1869 if (blockIndex == DynamicLayout.INVALID_BLOCK_INDEX
1870 || mTextRenderNodes[blockIndex] == null
1871 || mTextRenderNodes[blockIndex].needsToBeShifted) {
1872 startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas,
1873 layout, highlight, highlightPaint, cursorOffsetVertical,
1874 blockEndLines, blockIndices, block, numberOfBlocks,
1875 startIndexToFindAvailableRenderNode);
1876 }
1877 }
1878 }
Sangkyu Lee955beb22012-12-10 15:47:00 +09001879
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001880 dynamicLayout.setIndexFirstChangedBlock(lastIndex);
Gilles Debunned88876a2012-03-16 17:34:04 -07001881 } else {
1882 // Boring layout is used for empty and hint text
1883 layout.drawText(canvas, firstLine, lastLine);
1884 }
1885 }
1886
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001887 private int drawHardwareAcceleratedInner(Canvas canvas, Layout layout, Path highlight,
1888 Paint highlightPaint, int cursorOffsetVertical, int[] blockEndLines,
1889 int[] blockIndices, int blockInfoIndex, int numberOfBlocks,
1890 int startIndexToFindAvailableRenderNode) {
1891 final int blockEndLine = blockEndLines[blockInfoIndex];
1892 int blockIndex = blockIndices[blockInfoIndex];
1893
1894 final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
1895 if (blockIsInvalid) {
1896 blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
1897 startIndexToFindAvailableRenderNode);
1898 // Note how dynamic layout's internal block indices get updated from Editor
1899 blockIndices[blockInfoIndex] = blockIndex;
1900 if (mTextRenderNodes[blockIndex] != null) {
1901 mTextRenderNodes[blockIndex].isDirty = true;
1902 }
1903 startIndexToFindAvailableRenderNode = blockIndex + 1;
1904 }
1905
1906 if (mTextRenderNodes[blockIndex] == null) {
1907 mTextRenderNodes[blockIndex] = new TextRenderNode("Text " + blockIndex);
1908 }
1909
1910 final boolean blockDisplayListIsInvalid = mTextRenderNodes[blockIndex].needsRecord();
1911 RenderNode blockDisplayList = mTextRenderNodes[blockIndex].renderNode;
1912 if (mTextRenderNodes[blockIndex].needsToBeShifted || blockDisplayListIsInvalid) {
1913 final int blockBeginLine = blockInfoIndex == 0 ?
1914 0 : blockEndLines[blockInfoIndex - 1] + 1;
1915 final int top = layout.getLineTop(blockBeginLine);
1916 final int bottom = layout.getLineBottom(blockEndLine);
1917 int left = 0;
1918 int right = mTextView.getWidth();
1919 if (mTextView.getHorizontallyScrolling()) {
1920 float min = Float.MAX_VALUE;
1921 float max = Float.MIN_VALUE;
1922 for (int line = blockBeginLine; line <= blockEndLine; line++) {
1923 min = Math.min(min, layout.getLineLeft(line));
1924 max = Math.max(max, layout.getLineRight(line));
1925 }
1926 left = (int) min;
1927 right = (int) (max + 0.5f);
1928 }
1929
1930 // Rebuild display list if it is invalid
1931 if (blockDisplayListIsInvalid) {
John Recke57475e2019-02-20 17:39:52 -08001932 final RecordingCanvas recordingCanvas = blockDisplayList.beginRecording(
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001933 right - left, bottom - top);
1934 try {
1935 // drawText is always relative to TextView's origin, this translation
1936 // brings this range of text back to the top left corner of the viewport
John Reck32f140aa62018-10-04 15:08:24 -07001937 recordingCanvas.translate(-left, -top);
1938 layout.drawText(recordingCanvas, blockBeginLine, blockEndLine);
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001939 mTextRenderNodes[blockIndex].isDirty = false;
1940 // No need to untranslate, previous context is popped after
1941 // drawDisplayList
1942 } finally {
John Recke57475e2019-02-20 17:39:52 -08001943 blockDisplayList.endRecording();
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001944 // Same as drawDisplayList below, handled by our TextView's parent
1945 blockDisplayList.setClipToBounds(false);
1946 }
1947 }
1948
1949 // Valid display list only needs to update its drawing location.
1950 blockDisplayList.setLeftTopRightBottom(left, top, right, bottom);
1951 mTextRenderNodes[blockIndex].needsToBeShifted = false;
1952 }
John Reck32f140aa62018-10-04 15:08:24 -07001953 ((RecordingCanvas) canvas).drawRenderNode(blockDisplayList);
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001954 return startIndexToFindAvailableRenderNode;
1955 }
1956
Gilles Debunned88876a2012-03-16 17:34:04 -07001957 private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
1958 int searchStartIndex) {
Chris Craik956f3402015-04-27 16:41:00 -07001959 int length = mTextRenderNodes.length;
Gilles Debunned88876a2012-03-16 17:34:04 -07001960 for (int i = searchStartIndex; i < length; i++) {
1961 boolean blockIndexFound = false;
1962 for (int j = 0; j < numberOfBlocks; j++) {
1963 if (blockIndices[j] == i) {
1964 blockIndexFound = true;
1965 break;
1966 }
1967 }
1968 if (blockIndexFound) continue;
1969 return i;
1970 }
1971
1972 // No available index found, the pool has to grow
Chris Craik956f3402015-04-27 16:41:00 -07001973 mTextRenderNodes = GrowingArrayUtils.append(mTextRenderNodes, length, null);
Gilles Debunned88876a2012-03-16 17:34:04 -07001974 return length;
1975 }
1976
1977 private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
1978 final boolean translate = cursorOffsetVertical != 0;
1979 if (translate) canvas.translate(0, cursorOffsetVertical);
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07001980 if (mDrawableForCursor != null) {
1981 mDrawableForCursor.draw(canvas);
Gilles Debunned88876a2012-03-16 17:34:04 -07001982 }
1983 if (translate) canvas.translate(0, -cursorOffsetVertical);
1984 }
1985
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09001986 void invalidateHandlesAndActionMode() {
1987 if (mSelectionModifierCursorController != null) {
1988 mSelectionModifierCursorController.invalidateHandles();
1989 }
1990 if (mInsertionPointCursorController != null) {
1991 mInsertionPointCursorController.invalidateHandle();
1992 }
1993 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01001994 invalidateActionMode();
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09001995 }
1996 }
1997
Gilles Debunneebc86af2012-04-20 15:10:47 -07001998 /**
1999 * Invalidates all the sub-display lists that overlap the specified character range
2000 */
2001 void invalidateTextDisplayList(Layout layout, int start, int end) {
Chris Craik956f3402015-04-27 16:41:00 -07002002 if (mTextRenderNodes != null && layout instanceof DynamicLayout) {
Gilles Debunneebc86af2012-04-20 15:10:47 -07002003 final int firstLine = layout.getLineForOffset(start);
2004 final int lastLine = layout.getLineForOffset(end);
2005
2006 DynamicLayout dynamicLayout = (DynamicLayout) layout;
2007 int[] blockEndLines = dynamicLayout.getBlockEndLines();
2008 int[] blockIndices = dynamicLayout.getBlockIndices();
2009 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
2010
2011 int i = 0;
2012 // Skip the blocks before firstLine
2013 while (i < numberOfBlocks) {
2014 if (blockEndLines[i] >= firstLine) break;
2015 i++;
2016 }
2017
2018 // Invalidate all subsequent blocks until lastLine is passed
2019 while (i < numberOfBlocks) {
2020 final int blockIndex = blockIndices[i];
2021 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX) {
Chris Craik956f3402015-04-27 16:41:00 -07002022 mTextRenderNodes[blockIndex].isDirty = true;
Gilles Debunneebc86af2012-04-20 15:10:47 -07002023 }
2024 if (blockEndLines[i] >= lastLine) break;
2025 i++;
2026 }
2027 }
2028 }
2029
Mathew Inwood978c6e22018-08-21 15:58:55 +01002030 @UnsupportedAppUsage
Gilles Debunned88876a2012-03-16 17:34:04 -07002031 void invalidateTextDisplayList() {
Chris Craik956f3402015-04-27 16:41:00 -07002032 if (mTextRenderNodes != null) {
2033 for (int i = 0; i < mTextRenderNodes.length; i++) {
2034 if (mTextRenderNodes[i] != null) mTextRenderNodes[i].isDirty = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07002035 }
2036 }
2037 }
2038
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002039 void updateCursorPosition() {
Mihai Popa6c7ad1d2018-12-04 15:45:00 +00002040 loadCursorDrawable();
2041 if (mDrawableForCursor == null) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002042 return;
2043 }
2044
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002045 final Layout layout = mTextView.getLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -07002046 final int offset = mTextView.getSelectionStart();
2047 final int line = layout.getLineForOffset(offset);
2048 final int top = layout.getLineTop(line);
Siyamed Sinira60b59d2017-07-26 09:26:41 -07002049 final int bottom = layout.getLineBottomWithoutSpacing(line);
Gilles Debunned88876a2012-03-16 17:34:04 -07002050
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002051 final boolean clamped = layout.shouldClampCursor(line);
2052 updateCursorPosition(top, bottom, layout.getPrimaryHorizontal(offset, clamped));
Gilles Debunned88876a2012-03-16 17:34:04 -07002053 }
2054
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002055 void refreshTextActionMode() {
2056 if (extractedTextModeWillBeStarted()) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002057 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002058 return;
2059 }
2060 final boolean hasSelection = mTextView.hasSelection();
2061 final SelectionModifierCursorController selectionController = getSelectionController();
2062 final InsertionPointCursorController insertionController = getInsertionController();
2063 if ((selectionController != null && selectionController.isCursorBeingModified())
2064 || (insertionController != null && insertionController.isCursorBeingModified())) {
2065 // ActionMode should be managed by the currently active cursor controller.
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002066 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002067 return;
2068 }
2069 if (hasSelection) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002070 hideInsertionPointCursorController();
2071 if (mTextActionMode == null) {
Keisuke Kuroyanagi0fd28c92016-04-04 17:43:06 +09002072 if (mRestartActionModeOnNextRefresh) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002073 // To avoid distraction, newly start action mode only when selection action
Keisuke Kuroyanagi0fd28c92016-04-04 17:43:06 +09002074 // mode is being restarted.
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002075 startSelectionActionModeAsync(false);
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002076 }
2077 } else if (selectionController == null || !selectionController.isActive()) {
2078 // Insertion action mode is active. Avoid dismissing the selection.
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002079 stopTextActionModeWithPreservingSelection();
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002080 startSelectionActionModeAsync(false);
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002081 } else {
2082 mTextActionMode.invalidateContentRect();
2083 }
2084 } else {
2085 // Insertion action mode is started only when insertion controller is explicitly
2086 // activated.
2087 if (insertionController == null || !insertionController.isActive()) {
2088 stopTextActionMode();
2089 } else if (mTextActionMode != null) {
2090 mTextActionMode.invalidateContentRect();
2091 }
2092 }
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002093 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002094 }
2095
Gilles Debunned88876a2012-03-16 17:34:04 -07002096 /**
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002097 * Start an Insertion action mode.
Gilles Debunned88876a2012-03-16 17:34:04 -07002098 */
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002099 void startInsertionActionMode() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002100 if (mInsertionActionModeRunnable != null) {
2101 mTextView.removeCallbacks(mInsertionActionModeRunnable);
2102 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002103 if (extractedTextModeWillBeStarted()) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002104 return;
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002105 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002106 stopTextActionMode();
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002107
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002108 ActionMode.Callback actionModeCallback =
Richard Ledley26b87222017-11-30 10:54:08 +00002109 new TextActionModeCallback(TextActionMode.INSERTION);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002110 mTextActionMode = mTextView.startActionMode(
Clara Bayarrib8ed5b72015-04-09 15:26:41 +01002111 actionModeCallback, ActionMode.TYPE_FLOATING);
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002112 if (mTextActionMode != null && getInsertionController() != null) {
2113 getInsertionController().show();
2114 }
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00002115 }
2116
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002117 @NonNull
2118 TextView getTextView() {
2119 return mTextView;
2120 }
2121
2122 @Nullable
2123 ActionMode getTextActionMode() {
2124 return mTextActionMode;
2125 }
2126
2127 void setRestartActionModeOnNextRefresh(boolean value) {
2128 mRestartActionModeOnNextRefresh = value;
2129 }
2130
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00002131 /**
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002132 * Asynchronously starts a selection action mode using the TextClassifier.
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00002133 */
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002134 void startSelectionActionModeAsync(boolean adjustSelection) {
Richard Ledley26b87222017-11-30 10:54:08 +00002135 getSelectionActionModeHelper().startSelectionActionModeAsync(adjustSelection);
2136 }
2137
Richard Ledley27db81b2018-03-01 12:34:55 +00002138 void startLinkActionModeAsync(int start, int end) {
Richard Ledley26b87222017-11-30 10:54:08 +00002139 if (!(mTextView.getText() instanceof Spannable)) {
2140 return;
2141 }
Richard Ledley26b87222017-11-30 10:54:08 +00002142 stopTextActionMode();
Abodunrinwa Toki52096912018-03-21 23:14:42 +00002143 mRequestingLinkActionMode = true;
Richard Ledley27db81b2018-03-01 12:34:55 +00002144 getSelectionActionModeHelper().startLinkActionModeAsync(start, end);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002145 }
2146
2147 /**
2148 * Asynchronously invalidates an action mode using the TextClassifier.
2149 */
Abodunrinwa Toki4ce651e2017-05-12 15:37:29 +01002150 void invalidateActionModeAsync() {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002151 getSelectionActionModeHelper().invalidateActionModeAsync();
2152 }
2153
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002154 /**
2155 * Synchronously invalidates an action mode without the TextClassifier.
2156 */
2157 private void invalidateActionMode() {
2158 if (mTextActionMode != null) {
2159 mTextActionMode.invalidate();
2160 }
2161 }
2162
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002163 private SelectionActionModeHelper getSelectionActionModeHelper() {
2164 if (mSelectionActionModeHelper == null) {
2165 mSelectionActionModeHelper = new SelectionActionModeHelper(this);
Clara Bayarri578286f2015-04-10 15:35:31 +01002166 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002167 return mSelectionActionModeHelper;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00002168 }
2169
Clara Bayarridfac4432015-05-15 12:18:24 +01002170 /**
2171 * If the TextView allows text selection, selects the current word when no existing selection
2172 * was available and starts a drag.
2173 *
2174 * @return true if the drag was started.
2175 */
2176 private boolean selectCurrentWordAndStartDrag() {
Clara Bayarri7184c8a2015-06-05 17:34:09 +01002177 if (mInsertionActionModeRunnable != null) {
2178 mTextView.removeCallbacks(mInsertionActionModeRunnable);
2179 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002180 if (extractedTextModeWillBeStarted()) {
Clara Bayarridfac4432015-05-15 12:18:24 +01002181 return false;
2182 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002183 if (!checkField()) {
Clara Bayarridfac4432015-05-15 12:18:24 +01002184 return false;
2185 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002186 if (!mTextView.hasSelection() && !selectCurrentWord()) {
2187 // No selection and cannot select a word.
2188 return false;
2189 }
2190 stopTextActionModeWithPreservingSelection();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08002191 getSelectionController().enterDrag(
2192 SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_WORD);
Clara Bayarridfac4432015-05-15 12:18:24 +01002193 return true;
2194 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002195
Clara Bayarridfac4432015-05-15 12:18:24 +01002196 /**
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002197 * Checks whether a selection can be performed on the current TextView.
Clara Bayarridfac4432015-05-15 12:18:24 +01002198 *
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002199 * @return true if a selection can be performed
Clara Bayarridfac4432015-05-15 12:18:24 +01002200 */
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002201 boolean checkField() {
Clara Bayarridfac4432015-05-15 12:18:24 +01002202 if (!mTextView.canSelectText() || !mTextView.requestFocus()) {
2203 Log.w(TextView.LOG_TAG,
2204 "TextView does not support text selection. Selection cancelled.");
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002205 return false;
2206 }
Clara Bayarridfac4432015-05-15 12:18:24 +01002207 return true;
2208 }
2209
Richard Ledley26b87222017-11-30 10:54:08 +00002210 boolean startActionModeInternal(@TextActionMode int actionMode) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002211 if (extractedTextModeWillBeStarted()) {
2212 return false;
2213 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002214 if (mTextActionMode != null) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002215 // Text action mode is already started
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002216 invalidateActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07002217 return false;
2218 }
2219
Richard Ledley724eff92017-12-21 10:11:34 +00002220 if (actionMode != TextActionMode.TEXT_LINK
2221 && (!checkField() || !mTextView.hasSelection())) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002222 return false;
2223 }
2224
Richard Ledley26b87222017-11-30 10:54:08 +00002225 ActionMode.Callback actionModeCallback = new TextActionModeCallback(actionMode);
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002226 mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING);
Gilles Debunned88876a2012-03-16 17:34:04 -07002227
Abodunrinwa Toki29cb7682018-04-11 21:24:20 +01002228 final boolean selectableText = mTextView.isTextEditable() || mTextView.isTextSelectable();
2229 if (actionMode == TextActionMode.TEXT_LINK && !selectableText
2230 && mTextActionMode instanceof FloatingActionMode) {
2231 // Make the toolbar outside-touchable so that it can be dismissed when the user clicks
2232 // outside of it.
2233 ((FloatingActionMode) mTextActionMode).setOutsideTouchable(true,
2234 () -> stopTextActionMode());
2235 }
2236
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002237 final boolean selectionStarted = mTextActionMode != null;
Abodunrinwa Toki52096912018-03-21 23:14:42 +00002238 if (selectionStarted
2239 && mTextView.isTextEditable() && !mTextView.isTextSelectable()
2240 && mShowSoftInputOnFocus) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002241 // Show the IME to be able to replace text, except when selecting non editable text.
Yohei Yukawa484d4af2018-09-17 16:47:08 -07002242 final InputMethodManager imm = getInputMethodManager();
Gilles Debunned88876a2012-03-16 17:34:04 -07002243 if (imm != null) {
2244 imm.showSoftInput(mTextView, 0, null);
2245 }
2246 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002247 return selectionStarted;
2248 }
2249
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09002250 private boolean extractedTextModeWillBeStarted() {
Andrei Stingaceanub1891b32015-06-19 16:44:37 +01002251 if (!(mTextView.isInExtractedMode())) {
Yohei Yukawa484d4af2018-09-17 16:47:08 -07002252 final InputMethodManager imm = getInputMethodManager();
Gilles Debunned88876a2012-03-16 17:34:04 -07002253 return imm != null && imm.isFullscreenMode();
2254 }
2255 return false;
2256 }
2257
2258 /**
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002259 * @return <code>true</code> if it's reasonable to offer to show suggestions depending on
2260 * the current cursor position or selection range. This method is consistent with the
2261 * method to show suggestions {@link SuggestionsPopupWindow#updateSuggestions}.
Gilles Debunned88876a2012-03-16 17:34:04 -07002262 */
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002263 private boolean shouldOfferToShowSuggestions() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002264 CharSequence text = mTextView.getText();
2265 if (!(text instanceof Spannable)) return false;
2266
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002267 final Spannable spannable = (Spannable) text;
2268 final int selectionStart = mTextView.getSelectionStart();
2269 final int selectionEnd = mTextView.getSelectionEnd();
2270 final SuggestionSpan[] suggestionSpans = spannable.getSpans(selectionStart, selectionEnd,
2271 SuggestionSpan.class);
2272 if (suggestionSpans.length == 0) {
2273 return false;
2274 }
2275 if (selectionStart == selectionEnd) {
2276 // Spans overlap the cursor.
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002277 for (int i = 0; i < suggestionSpans.length; i++) {
2278 if (suggestionSpans[i].getSuggestions().length > 0) {
2279 return true;
2280 }
2281 }
2282 return false;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002283 }
2284 int minSpanStart = mTextView.getText().length();
2285 int maxSpanEnd = 0;
2286 int unionOfSpansCoveringSelectionStartStart = mTextView.getText().length();
2287 int unionOfSpansCoveringSelectionStartEnd = 0;
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002288 boolean hasValidSuggestions = false;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002289 for (int i = 0; i < suggestionSpans.length; i++) {
2290 final int spanStart = spannable.getSpanStart(suggestionSpans[i]);
2291 final int spanEnd = spannable.getSpanEnd(suggestionSpans[i]);
2292 minSpanStart = Math.min(minSpanStart, spanStart);
2293 maxSpanEnd = Math.max(maxSpanEnd, spanEnd);
2294 if (selectionStart < spanStart || selectionStart > spanEnd) {
2295 // The span doesn't cover the current selection start point.
2296 continue;
2297 }
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002298 hasValidSuggestions =
2299 hasValidSuggestions || suggestionSpans[i].getSuggestions().length > 0;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002300 unionOfSpansCoveringSelectionStartStart =
2301 Math.min(unionOfSpansCoveringSelectionStartStart, spanStart);
2302 unionOfSpansCoveringSelectionStartEnd =
2303 Math.max(unionOfSpansCoveringSelectionStartEnd, spanEnd);
2304 }
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002305 if (!hasValidSuggestions) {
2306 return false;
2307 }
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002308 if (unionOfSpansCoveringSelectionStartStart >= unionOfSpansCoveringSelectionStartEnd) {
2309 // No spans cover the selection start point.
2310 return false;
2311 }
2312 if (minSpanStart < unionOfSpansCoveringSelectionStartStart
2313 || maxSpanEnd > unionOfSpansCoveringSelectionStartEnd) {
2314 // There is a span that is not covered by the union. In this case, we soouldn't offer
2315 // to show suggestions as it's confusing.
2316 return false;
2317 }
2318 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07002319 }
2320
2321 /**
2322 * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
2323 * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
2324 */
2325 private boolean isCursorInsideEasyCorrectionSpan() {
2326 Spannable spannable = (Spannable) mTextView.getText();
2327 SuggestionSpan[] suggestionSpans = spannable.getSpans(mTextView.getSelectionStart(),
2328 mTextView.getSelectionEnd(), SuggestionSpan.class);
2329 for (int i = 0; i < suggestionSpans.length; i++) {
2330 if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
2331 return true;
2332 }
2333 }
2334 return false;
2335 }
2336
2337 void onTouchUpEvent(MotionEvent event) {
Nikita Dubrovsky05cfcc82019-10-24 08:57:32 -07002338 if (TextView.DEBUG_CURSOR) {
2339 logCursor("onTouchUpEvent", null);
2340 }
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +01002341 if (getSelectionActionModeHelper().resetSelection(
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +00002342 getTextView().getOffsetForPosition(event.getX(), event.getY()))) {
2343 return;
2344 }
2345
Gilles Debunned88876a2012-03-16 17:34:04 -07002346 boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect();
Mady Mellora2861452015-06-25 08:40:27 -07002347 hideCursorAndSpanControllers();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002348 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07002349 CharSequence text = mTextView.getText();
2350 if (!selectAllGotFocus && text.length() > 0) {
2351 // Move cursor
2352 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
Abodunrinwa Toki52096912018-03-21 23:14:42 +00002353
2354 final boolean shouldInsertCursor = !mRequestingLinkActionMode;
2355 if (shouldInsertCursor) {
2356 Selection.setSelection((Spannable) text, offset);
2357 if (mSpellChecker != null) {
2358 // When the cursor moves, the word that was typed may need spell check
2359 mSpellChecker.onSelectionChanged();
2360 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002361 }
Andrei Stingaceanu35c550c2015-05-07 16:49:49 +01002362
Gilles Debunned88876a2012-03-16 17:34:04 -07002363 if (!extractedTextModeWillBeStarted()) {
2364 if (isCursorInsideEasyCorrectionSpan()) {
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01002365 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002366 if (mInsertionActionModeRunnable != null) {
2367 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01002368 }
2369
Abodunrinwa Toki52096912018-03-21 23:14:42 +00002370 mShowSuggestionRunnable = this::replace;
2371
Gilles Debunned88876a2012-03-16 17:34:04 -07002372 // removeCallbacks is performed on every touch
2373 mTextView.postDelayed(mShowSuggestionRunnable,
2374 ViewConfiguration.getDoubleTapTimeout());
2375 } else if (hasInsertionController()) {
Abodunrinwa Toki52096912018-03-21 23:14:42 +00002376 if (shouldInsertCursor) {
2377 getInsertionController().show();
2378 } else {
2379 getInsertionController().hide();
2380 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002381 }
2382 }
2383 }
2384 }
2385
Yohei Yukawa401e3d42019-01-19 11:49:37 -08002386 /**
2387 * Called when {@link TextView#mTextOperationUser} has changed.
2388 *
2389 * <p>Any user-specific resources need to be refreshed here.</p>
2390 */
2391 final void onTextOperationUserChanged() {
2392 if (mSpellChecker != null) {
2393 mSpellChecker.resetSession();
2394 }
2395 }
2396
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002397 protected void stopTextActionMode() {
2398 if (mTextActionMode != null) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002399 // This will hide the mSelectionModifierCursorController
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002400 mTextActionMode.finish();
Gilles Debunned88876a2012-03-16 17:34:04 -07002401 }
2402 }
2403
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002404 private void stopTextActionModeWithPreservingSelection() {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002405 if (mTextActionMode != null) {
2406 mRestartActionModeOnNextRefresh = true;
2407 }
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002408 mPreserveSelection = true;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002409 stopTextActionMode();
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002410 mPreserveSelection = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002411 }
2412
Gilles Debunned88876a2012-03-16 17:34:04 -07002413 /**
2414 * @return True if this view supports insertion handles.
2415 */
2416 boolean hasInsertionController() {
2417 return mInsertionControllerEnabled;
2418 }
2419
2420 /**
2421 * @return True if this view supports selection handles.
2422 */
2423 boolean hasSelectionController() {
2424 return mSelectionControllerEnabled;
2425 }
2426
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09002427 private InsertionPointCursorController getInsertionController() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002428 if (!mInsertionControllerEnabled) {
2429 return null;
2430 }
2431
2432 if (mInsertionPointCursorController == null) {
2433 mInsertionPointCursorController = new InsertionPointCursorController();
2434
2435 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
2436 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
2437 }
2438
2439 return mInsertionPointCursorController;
2440 }
2441
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002442 @Nullable
2443 SelectionModifierCursorController getSelectionController() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002444 if (!mSelectionControllerEnabled) {
2445 return null;
2446 }
2447
2448 if (mSelectionModifierCursorController == null) {
2449 mSelectionModifierCursorController = new SelectionModifierCursorController();
2450
2451 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
2452 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
2453 }
2454
2455 return mSelectionModifierCursorController;
2456 }
2457
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002458 @VisibleForTesting
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002459 @Nullable
2460 public Drawable getCursorDrawable() {
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07002461 return mDrawableForCursor;
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002462 }
2463
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002464 private void updateCursorPosition(int top, int bottom, float horizontal) {
Mihai Popa6c7ad1d2018-12-04 15:45:00 +00002465 loadCursorDrawable();
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07002466 final int left = clampHorizontalPosition(mDrawableForCursor, horizontal);
2467 final int width = mDrawableForCursor.getIntrinsicWidth();
Nikita Dubrovsky05cfcc82019-10-24 08:57:32 -07002468 if (TextView.DEBUG_CURSOR) {
2469 logCursor("updateCursorPosition", "left=%s, top=%s", left, (top - mTempRect.top));
2470 }
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07002471 mDrawableForCursor.setBounds(left, top - mTempRect.top, left + width,
Gilles Debunned88876a2012-03-16 17:34:04 -07002472 bottom + mTempRect.bottom);
2473 }
2474
2475 /**
Siyamed Sinir987ec652016-02-17 19:44:41 -08002476 * Return clamped position for the drawable. If the drawable is within the boundaries of the
2477 * 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 -08002478 * 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 -08002479 * the view boundary. If the drawable is null, horizontal parameter is aligned to left or right
2480 * of the view.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002481 *
Siyamed Sinir987ec652016-02-17 19:44:41 -08002482 * @param drawable Drawable. Can be null.
2483 * @param horizontal Horizontal position for the drawable.
2484 * @return The clamped horizontal position for the drawable.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002485 */
Siyamed Sinir987ec652016-02-17 19:44:41 -08002486 private int clampHorizontalPosition(@Nullable final Drawable drawable, float horizontal) {
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002487 horizontal = Math.max(0.5f, horizontal - 0.5f);
2488 if (mTempRect == null) mTempRect = new Rect();
Siyamed Sinir987ec652016-02-17 19:44:41 -08002489
2490 int drawableWidth = 0;
2491 if (drawable != null) {
2492 drawable.getPadding(mTempRect);
2493 drawableWidth = drawable.getIntrinsicWidth();
2494 } else {
2495 mTempRect.setEmpty();
2496 }
2497
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002498 int scrollX = mTextView.getScrollX();
2499 float horizontalDiff = horizontal - scrollX;
2500 int viewClippedWidth = mTextView.getWidth() - mTextView.getCompoundPaddingLeft()
2501 - mTextView.getCompoundPaddingRight();
2502
2503 final int left;
2504 if (horizontalDiff >= (viewClippedWidth - 1f)) {
2505 // at the rightmost position
Siyamed Sinir987ec652016-02-17 19:44:41 -08002506 left = viewClippedWidth + scrollX - (drawableWidth - mTempRect.right);
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002507 } else if (Math.abs(horizontalDiff) <= 1f
2508 || (TextUtils.isEmpty(mTextView.getText())
Siyamed Sinir987ec652016-02-17 19:44:41 -08002509 && (TextView.VERY_WIDE - scrollX) <= (viewClippedWidth + 1f)
2510 && horizontal <= 1f)) {
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002511 // at the leftmost position
2512 left = scrollX - mTempRect.left;
2513 } else {
2514 left = (int) horizontal - mTempRect.left;
2515 }
2516 return left;
2517 }
2518
2519 /**
Gilles Debunned88876a2012-03-16 17:34:04 -07002520 * 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 -08002521 * a dictionary) from the current input method, provided by it calling
Gilles Debunned88876a2012-03-16 17:34:04 -07002522 * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
2523 * implementation flashes the background of the corrected word to provide feedback to the user.
2524 *
2525 * @param info The auto correct info about the text that was corrected.
2526 */
2527 public void onCommitCorrection(CorrectionInfo info) {
2528 if (mCorrectionHighlighter == null) {
2529 mCorrectionHighlighter = new CorrectionHighlighter();
2530 } else {
2531 mCorrectionHighlighter.invalidate(false);
2532 }
2533
2534 mCorrectionHighlighter.highlight(info);
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002535 mUndoInputFilter.freezeLastEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07002536 }
2537
Gilles Debunned88876a2012-03-16 17:34:04 -07002538 void onScrollChanged() {
Gilles Debunne157aafc2012-04-19 17:21:57 -07002539 if (mPositionListener != null) {
2540 mPositionListener.onScrollChanged();
2541 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002542 if (mTextActionMode != null) {
2543 mTextActionMode.invalidateContentRect();
Abodunrinwa Toki56195db2015-04-22 06:46:54 +01002544 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002545 }
2546
2547 /**
2548 * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
2549 */
2550 private boolean shouldBlink() {
Long Ling0c27fbb2019-11-19 18:41:21 +00002551 if (!isCursorVisible() || !mTextView.isFocused()) return false;
Gilles Debunned88876a2012-03-16 17:34:04 -07002552
2553 final int start = mTextView.getSelectionStart();
2554 if (start < 0) return false;
2555
2556 final int end = mTextView.getSelectionEnd();
2557 if (end < 0) return false;
2558
2559 return start == end;
2560 }
2561
2562 void makeBlink() {
2563 if (shouldBlink()) {
2564 mShowCursor = SystemClock.uptimeMillis();
2565 if (mBlink == null) mBlink = new Blink();
John Reckd0374c62015-10-20 13:25:01 -07002566 mTextView.removeCallbacks(mBlink);
2567 mTextView.postDelayed(mBlink, BLINK);
Gilles Debunned88876a2012-03-16 17:34:04 -07002568 } else {
John Reckd0374c62015-10-20 13:25:01 -07002569 if (mBlink != null) mTextView.removeCallbacks(mBlink);
Gilles Debunned88876a2012-03-16 17:34:04 -07002570 }
2571 }
2572
John Reckd0374c62015-10-20 13:25:01 -07002573 private class Blink implements Runnable {
Gilles Debunned88876a2012-03-16 17:34:04 -07002574 private boolean mCancelled;
2575
2576 public void run() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002577 if (mCancelled) {
2578 return;
2579 }
2580
John Reckd0374c62015-10-20 13:25:01 -07002581 mTextView.removeCallbacks(this);
Gilles Debunned88876a2012-03-16 17:34:04 -07002582
2583 if (shouldBlink()) {
2584 if (mTextView.getLayout() != null) {
2585 mTextView.invalidateCursorPath();
2586 }
2587
John Reckd0374c62015-10-20 13:25:01 -07002588 mTextView.postDelayed(this, BLINK);
Gilles Debunned88876a2012-03-16 17:34:04 -07002589 }
2590 }
2591
2592 void cancel() {
2593 if (!mCancelled) {
John Reckd0374c62015-10-20 13:25:01 -07002594 mTextView.removeCallbacks(this);
Gilles Debunned88876a2012-03-16 17:34:04 -07002595 mCancelled = true;
2596 }
2597 }
2598
2599 void uncancel() {
2600 mCancelled = false;
2601 }
2602 }
2603
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002604 private DragShadowBuilder getTextThumbnailBuilder(int start, int end) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002605 TextView shadowView = (TextView) View.inflate(mTextView.getContext(),
2606 com.android.internal.R.layout.text_drag_thumbnail, null);
2607
2608 if (shadowView == null) {
2609 throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
2610 }
2611
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002612 if (end - start > DRAG_SHADOW_MAX_TEXT_LENGTH) {
2613 final long range = getCharClusterRange(start + DRAG_SHADOW_MAX_TEXT_LENGTH);
2614 end = TextUtils.unpackRangeEndFromLong(range);
Gilles Debunned88876a2012-03-16 17:34:04 -07002615 }
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002616 final CharSequence text = mTextView.getTransformedText(start, end);
Gilles Debunned88876a2012-03-16 17:34:04 -07002617 shadowView.setText(text);
2618 shadowView.setTextColor(mTextView.getTextColors());
2619
Alan Viverettebb98ebd2015-05-08 17:17:44 -07002620 shadowView.setTextAppearance(R.styleable.Theme_textAppearanceLarge);
Gilles Debunned88876a2012-03-16 17:34:04 -07002621 shadowView.setGravity(Gravity.CENTER);
2622
2623 shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
2624 ViewGroup.LayoutParams.WRAP_CONTENT));
2625
2626 final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
2627 shadowView.measure(size, size);
2628
2629 shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
2630 shadowView.invalidate();
2631 return new DragShadowBuilder(shadowView);
2632 }
2633
2634 private static class DragLocalState {
2635 public TextView sourceTextView;
2636 public int start, end;
2637
2638 public DragLocalState(TextView sourceTextView, int start, int end) {
2639 this.sourceTextView = sourceTextView;
2640 this.start = start;
2641 this.end = end;
2642 }
2643 }
2644
2645 void onDrop(DragEvent event) {
Ben Murdoch3dac4602017-01-17 11:27:37 +00002646 SpannableStringBuilder content = new SpannableStringBuilder();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002647
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -07002648 final DragAndDropPermissions permissions = DragAndDropPermissions.obtain(event);
2649 if (permissions != null) {
2650 permissions.takeTransient();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002651 }
2652
2653 try {
2654 ClipData clipData = event.getClipData();
2655 final int itemCount = clipData.getItemCount();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002656 for (int i = 0; i < itemCount; i++) {
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002657 Item item = clipData.getItemAt(i);
2658 content.append(item.coerceToStyledText(mTextView.getContext()));
2659 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002660 } finally {
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -07002661 if (permissions != null) {
2662 permissions.release();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002663 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002664 }
2665
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002666 mTextView.beginBatchEdit();
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002667 mUndoInputFilter.freezeLastEdit();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002668 try {
2669 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
2670 Object localState = event.getLocalState();
2671 DragLocalState dragLocalState = null;
2672 if (localState instanceof DragLocalState) {
2673 dragLocalState = (DragLocalState) localState;
Gilles Debunned88876a2012-03-16 17:34:04 -07002674 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002675 boolean dragDropIntoItself = dragLocalState != null
2676 && dragLocalState.sourceTextView == mTextView;
Gilles Debunned88876a2012-03-16 17:34:04 -07002677
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002678 if (dragDropIntoItself) {
2679 if (offset >= dragLocalState.start && offset < dragLocalState.end) {
2680 // A drop inside the original selection discards the drop.
2681 return;
2682 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002683 }
2684
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002685 final int originalLength = mTextView.getText().length();
2686 int min = offset;
2687 int max = offset;
2688
2689 Selection.setSelection((Spannable) mTextView.getText(), max);
2690 mTextView.replaceText_internal(min, max, content);
2691
2692 if (dragDropIntoItself) {
2693 int dragSourceStart = dragLocalState.start;
2694 int dragSourceEnd = dragLocalState.end;
2695 if (max <= dragSourceStart) {
2696 // Inserting text before selection has shifted positions
2697 final int shift = mTextView.getText().length() - originalLength;
2698 dragSourceStart += shift;
2699 dragSourceEnd += shift;
2700 }
2701
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08002702 // Delete original selection
2703 mTextView.deleteText_internal(dragSourceStart, dragSourceEnd);
Gilles Debunned88876a2012-03-16 17:34:04 -07002704
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08002705 // Make sure we do not leave two adjacent spaces.
2706 final int prevCharIdx = Math.max(0, dragSourceStart - 1);
2707 final int nextCharIdx = Math.min(mTextView.getText().length(), dragSourceStart + 1);
2708 if (nextCharIdx > prevCharIdx + 1) {
2709 CharSequence t = mTextView.getTransformedText(prevCharIdx, nextCharIdx);
2710 if (Character.isSpaceChar(t.charAt(0)) && Character.isSpaceChar(t.charAt(1))) {
2711 mTextView.deleteText_internal(prevCharIdx, prevCharIdx + 1);
2712 }
Victoria Lease91373202012-09-07 16:41:59 -07002713 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002714 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002715 } finally {
2716 mTextView.endBatchEdit();
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002717 mUndoInputFilter.freezeLastEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07002718 }
2719 }
2720
Gilles Debunnec62589c2012-04-12 14:50:23 -07002721 public void addSpanWatchers(Spannable text) {
2722 final int textLength = text.length();
2723
2724 if (mKeyListener != null) {
2725 text.setSpan(mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
2726 }
2727
Jean Chalardbaf30942013-02-28 16:01:51 -08002728 if (mSpanController == null) {
2729 mSpanController = new SpanController();
Gilles Debunnec62589c2012-04-12 14:50:23 -07002730 }
Jean Chalardbaf30942013-02-28 16:01:51 -08002731 text.setSpan(mSpanController, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002732 }
2733
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002734 void setContextMenuAnchor(float x, float y) {
2735 mContextMenuAnchorX = x;
2736 mContextMenuAnchorY = y;
2737 }
2738
2739 void onCreateContextMenu(ContextMenu menu) {
2740 if (mIsBeingLongClicked || Float.isNaN(mContextMenuAnchorX)
2741 || Float.isNaN(mContextMenuAnchorY)) {
2742 return;
2743 }
2744 final int offset = mTextView.getOffsetForPosition(mContextMenuAnchorX, mContextMenuAnchorY);
2745 if (offset == -1) {
2746 return;
2747 }
Siyamed Sinir532f3c92017-06-15 18:22:31 -07002748
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002749 stopTextActionModeWithPreservingSelection();
Siyamed Sinir532f3c92017-06-15 18:22:31 -07002750 if (mTextView.canSelectText()) {
2751 final boolean isOnSelection = mTextView.hasSelection()
2752 && offset >= mTextView.getSelectionStart()
2753 && offset <= mTextView.getSelectionEnd();
2754 if (!isOnSelection) {
2755 // Right clicked position is not on the selection. Remove the selection and move the
2756 // cursor to the right clicked position.
2757 Selection.setSelection((Spannable) mTextView.getText(), offset);
2758 stopTextActionMode();
2759 }
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002760 }
2761
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002762 if (shouldOfferToShowSuggestions()) {
Keisuke Kuroyanagi182f5fe2016-03-11 16:31:29 +09002763 final SuggestionInfo[] suggestionInfoArray =
2764 new SuggestionInfo[SuggestionSpan.SUGGESTIONS_MAX_SIZE];
2765 for (int i = 0; i < suggestionInfoArray.length; i++) {
2766 suggestionInfoArray[i] = new SuggestionInfo();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002767 }
2768 final SubMenu subMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, MENU_ITEM_ORDER_REPLACE,
2769 com.android.internal.R.string.replace);
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002770 final int numItems = mSuggestionHelper.getSuggestionInfo(suggestionInfoArray, null);
Keisuke Kuroyanagi182f5fe2016-03-11 16:31:29 +09002771 for (int i = 0; i < numItems; i++) {
2772 final SuggestionInfo info = suggestionInfoArray[i];
2773 subMenu.add(Menu.NONE, Menu.NONE, i, info.mText)
2774 .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
2775 @Override
2776 public boolean onMenuItemClick(MenuItem item) {
2777 replaceWithSuggestion(info);
2778 return true;
2779 }
2780 });
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002781 }
2782 }
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002783
2784 menu.add(Menu.NONE, TextView.ID_UNDO, MENU_ITEM_ORDER_UNDO,
2785 com.android.internal.R.string.undo)
2786 .setAlphabeticShortcut('z')
2787 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2788 .setEnabled(mTextView.canUndo());
2789 menu.add(Menu.NONE, TextView.ID_REDO, MENU_ITEM_ORDER_REDO,
2790 com.android.internal.R.string.redo)
2791 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2792 .setEnabled(mTextView.canRedo());
2793
2794 menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
2795 com.android.internal.R.string.cut)
2796 .setAlphabeticShortcut('x')
2797 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2798 .setEnabled(mTextView.canCut());
2799 menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
2800 com.android.internal.R.string.copy)
2801 .setAlphabeticShortcut('c')
2802 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2803 .setEnabled(mTextView.canCopy());
2804 menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
2805 com.android.internal.R.string.paste)
2806 .setAlphabeticShortcut('v')
2807 .setEnabled(mTextView.canPaste())
2808 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01002809 menu.add(Menu.NONE, TextView.ID_PASTE_AS_PLAIN_TEXT, MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002810 com.android.internal.R.string.paste_as_plain_text)
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01002811 .setEnabled(mTextView.canPasteAsPlainText())
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002812 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2813 menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
2814 com.android.internal.R.string.share)
2815 .setEnabled(mTextView.canShare())
2816 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2817 menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
2818 com.android.internal.R.string.selectAll)
2819 .setAlphabeticShortcut('a')
2820 .setEnabled(mTextView.canSelectAllText())
2821 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Felipe Leme2ac463e2017-03-13 14:06:25 -07002822 menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
Felipe Leme555bcac2017-06-26 12:53:56 -07002823 android.R.string.autofill)
Felipe Leme2ac463e2017-03-13 14:06:25 -07002824 .setEnabled(mTextView.canRequestAutofill())
2825 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002826
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002827 mPreserveSelection = true;
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002828 }
2829
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002830 @Nullable
2831 private SuggestionSpan findEquivalentSuggestionSpan(
2832 @NonNull SuggestionSpanInfo suggestionSpanInfo) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002833 final Editable editable = (Editable) mTextView.getText();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002834 if (editable.getSpanStart(suggestionSpanInfo.mSuggestionSpan) >= 0) {
2835 // Exactly same span is found.
2836 return suggestionSpanInfo.mSuggestionSpan;
2837 }
2838 // Suggestion span couldn't be found. Try to find a suggestion span that has the same
2839 // contents.
2840 final SuggestionSpan[] suggestionSpans = editable.getSpans(suggestionSpanInfo.mSpanStart,
2841 suggestionSpanInfo.mSpanEnd, SuggestionSpan.class);
2842 for (final SuggestionSpan suggestionSpan : suggestionSpans) {
2843 final int start = editable.getSpanStart(suggestionSpan);
2844 if (start != suggestionSpanInfo.mSpanStart) {
2845 continue;
2846 }
2847 final int end = editable.getSpanEnd(suggestionSpan);
2848 if (end != suggestionSpanInfo.mSpanEnd) {
2849 continue;
2850 }
2851 if (suggestionSpan.equals(suggestionSpanInfo.mSuggestionSpan)) {
2852 return suggestionSpan;
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08002853 }
2854 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002855 return null;
2856 }
2857
2858 private void replaceWithSuggestion(@NonNull final SuggestionInfo suggestionInfo) {
2859 final SuggestionSpan targetSuggestionSpan = findEquivalentSuggestionSpan(
2860 suggestionInfo.mSuggestionSpanInfo);
2861 if (targetSuggestionSpan == null) {
2862 // Span has been removed
2863 return;
2864 }
2865 final Editable editable = (Editable) mTextView.getText();
2866 final int spanStart = editable.getSpanStart(targetSuggestionSpan);
2867 final int spanEnd = editable.getSpanEnd(targetSuggestionSpan);
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08002868 if (spanStart < 0 || spanEnd <= spanStart) {
2869 // Span has been removed
2870 return;
2871 }
2872
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002873 final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
2874 // SuggestionSpans are removed by replace: save them before
2875 SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
2876 SuggestionSpan.class);
2877 final int length = suggestionSpans.length;
2878 int[] suggestionSpansStarts = new int[length];
2879 int[] suggestionSpansEnds = new int[length];
2880 int[] suggestionSpansFlags = new int[length];
2881 for (int i = 0; i < length; i++) {
2882 final SuggestionSpan suggestionSpan = suggestionSpans[i];
2883 suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
2884 suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
2885 suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
2886
2887 // Remove potential misspelled flags
2888 int suggestionSpanFlags = suggestionSpan.getFlags();
2889 if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
2890 suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
2891 suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
2892 suggestionSpan.setFlags(suggestionSpanFlags);
2893 }
2894 }
2895
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002896 // Swap text content between actual text and Suggestion span
2897 final int suggestionStart = suggestionInfo.mSuggestionStart;
2898 final int suggestionEnd = suggestionInfo.mSuggestionEnd;
2899 final String suggestion = suggestionInfo.mText.subSequence(
2900 suggestionStart, suggestionEnd).toString();
2901 mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
2902
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002903 String[] suggestions = targetSuggestionSpan.getSuggestions();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002904 suggestions[suggestionInfo.mSuggestionIndex] = originalText;
2905
2906 // Restore previous SuggestionSpans
2907 final int lengthDelta = suggestion.length() - (spanEnd - spanStart);
2908 for (int i = 0; i < length; i++) {
2909 // Only spans that include the modified region make sense after replacement
2910 // Spans partially included in the replaced region are removed, there is no
2911 // way to assign them a valid range after replacement
2912 if (suggestionSpansStarts[i] <= spanStart && suggestionSpansEnds[i] >= spanEnd) {
2913 mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
2914 suggestionSpansEnds[i] + lengthDelta, suggestionSpansFlags[i]);
2915 }
2916 }
2917 // Move cursor at the end of the replaced word
2918 final int newCursorPosition = spanEnd + lengthDelta;
2919 mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
2920 }
2921
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002922 private final MenuItem.OnMenuItemClickListener mOnContextMenuItemClickListener =
2923 new MenuItem.OnMenuItemClickListener() {
2924 @Override
2925 public boolean onMenuItemClick(MenuItem item) {
2926 if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
2927 return true;
2928 }
2929 return mTextView.onTextContextMenuItem(item.getItemId());
2930 }
2931 };
2932
Gilles Debunned88876a2012-03-16 17:34:04 -07002933 /**
2934 * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
2935 * pop-up should be displayed.
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07002936 * Also monitors {@link Selection} to call back to the attached input method.
Gilles Debunned88876a2012-03-16 17:34:04 -07002937 */
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09002938 private class SpanController implements SpanWatcher {
Gilles Debunned88876a2012-03-16 17:34:04 -07002939
2940 private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
2941
2942 private EasyEditPopupWindow mPopupWindow;
2943
Gilles Debunned88876a2012-03-16 17:34:04 -07002944 private Runnable mHidePopup;
2945
Jean Chalardbaf30942013-02-28 16:01:51 -08002946 // This function is pure but inner classes can't have static functions
2947 private boolean isNonIntermediateSelectionSpan(final Spannable text,
2948 final Object span) {
2949 return (Selection.SELECTION_START == span || Selection.SELECTION_END == span)
2950 && (text.getSpanFlags(span) & Spanned.SPAN_INTERMEDIATE) == 0;
2951 }
2952
Gilles Debunnec62589c2012-04-12 14:50:23 -07002953 @Override
2954 public void onSpanAdded(Spannable text, Object span, int start, int end) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002955 if (isNonIntermediateSelectionSpan(text, span)) {
2956 sendUpdateSelection();
2957 } else if (span instanceof EasyEditSpan) {
Gilles Debunnec62589c2012-04-12 14:50:23 -07002958 if (mPopupWindow == null) {
2959 mPopupWindow = new EasyEditPopupWindow();
2960 mHidePopup = new Runnable() {
2961 @Override
2962 public void run() {
2963 hide();
2964 }
2965 };
2966 }
2967
2968 // Make sure there is only at most one EasyEditSpan in the text
2969 if (mPopupWindow.mEasyEditSpan != null) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002970 mPopupWindow.mEasyEditSpan.setDeleteEnabled(false);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002971 }
2972
2973 mPopupWindow.setEasyEditSpan((EasyEditSpan) span);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002974 mPopupWindow.setOnDeleteListener(new EasyEditDeleteListener() {
2975 @Override
2976 public void onDeleteClick(EasyEditSpan span) {
2977 Editable editable = (Editable) mTextView.getText();
2978 int start = editable.getSpanStart(span);
2979 int end = editable.getSpanEnd(span);
2980 if (start >= 0 && end >= 0) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002981 sendEasySpanNotification(EasyEditSpan.TEXT_DELETED, span);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002982 mTextView.deleteText_internal(start, end);
2983 }
2984 editable.removeSpan(span);
2985 }
2986 });
Gilles Debunnec62589c2012-04-12 14:50:23 -07002987
2988 if (mTextView.getWindowVisibility() != View.VISIBLE) {
2989 // The window is not visible yet, ignore the text change.
2990 return;
2991 }
2992
2993 if (mTextView.getLayout() == null) {
2994 // The view has not been laid out yet, ignore the text change
2995 return;
2996 }
2997
2998 if (extractedTextModeWillBeStarted()) {
2999 // The input is in extract mode. Do not handle the easy edit in
3000 // the original TextView, as the ExtractEditText will do
3001 return;
3002 }
3003
3004 mPopupWindow.show();
3005 mTextView.removeCallbacks(mHidePopup);
3006 mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
3007 }
3008 }
3009
3010 @Override
3011 public void onSpanRemoved(Spannable text, Object span, int start, int end) {
Jean Chalardbaf30942013-02-28 16:01:51 -08003012 if (isNonIntermediateSelectionSpan(text, span)) {
3013 sendUpdateSelection();
3014 } else if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
Gilles Debunnec62589c2012-04-12 14:50:23 -07003015 hide();
3016 }
3017 }
3018
3019 @Override
3020 public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd,
3021 int newStart, int newEnd) {
Jean Chalardbaf30942013-02-28 16:01:51 -08003022 if (isNonIntermediateSelectionSpan(text, span)) {
3023 sendUpdateSelection();
3024 } else if (mPopupWindow != null && span instanceof EasyEditSpan) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003025 EasyEditSpan easyEditSpan = (EasyEditSpan) span;
Jean Chalardbaf30942013-02-28 16:01:51 -08003026 sendEasySpanNotification(EasyEditSpan.TEXT_MODIFIED, easyEditSpan);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003027 text.removeSpan(easyEditSpan);
Gilles Debunnec62589c2012-04-12 14:50:23 -07003028 }
3029 }
3030
Gilles Debunned88876a2012-03-16 17:34:04 -07003031 public void hide() {
3032 if (mPopupWindow != null) {
3033 mPopupWindow.hide();
3034 mTextView.removeCallbacks(mHidePopup);
3035 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003036 }
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003037
Jean Chalardbaf30942013-02-28 16:01:51 -08003038 private void sendEasySpanNotification(int textChangedType, EasyEditSpan span) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003039 try {
3040 PendingIntent pendingIntent = span.getPendingIntent();
3041 if (pendingIntent != null) {
3042 Intent intent = new Intent();
3043 intent.putExtra(EasyEditSpan.EXTRA_TEXT_CHANGED_TYPE, textChangedType);
3044 pendingIntent.send(mTextView.getContext(), 0, intent);
3045 }
3046 } catch (CanceledException e) {
3047 // This should not happen, as we should try to send the intent only once.
3048 Log.w(TAG, "PendingIntent for notification cannot be sent", e);
3049 }
3050 }
3051 }
3052
3053 /**
3054 * Listens for the delete event triggered by {@link EasyEditPopupWindow}.
3055 */
3056 private interface EasyEditDeleteListener {
3057
3058 /**
3059 * Clicks the delete pop-up.
3060 */
3061 void onDeleteClick(EasyEditSpan span);
Gilles Debunned88876a2012-03-16 17:34:04 -07003062 }
3063
3064 /**
3065 * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07003066 * by {@link SpanController}.
Gilles Debunned88876a2012-03-16 17:34:04 -07003067 */
3068 private class EasyEditPopupWindow extends PinnedPopupWindow
3069 implements OnClickListener {
3070 private static final int POPUP_TEXT_LAYOUT =
3071 com.android.internal.R.layout.text_edit_action_popup_text;
3072 private TextView mDeleteTextView;
3073 private EasyEditSpan mEasyEditSpan;
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003074 private EasyEditDeleteListener mOnDeleteListener;
Gilles Debunned88876a2012-03-16 17:34:04 -07003075
3076 @Override
3077 protected void createPopupWindow() {
3078 mPopupWindow = new PopupWindow(mTextView.getContext(), null,
3079 com.android.internal.R.attr.textSelectHandleWindowStyle);
3080 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
3081 mPopupWindow.setClippingEnabled(true);
3082 }
3083
3084 @Override
3085 protected void initContentView() {
3086 LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
3087 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
3088 mContentView = linearLayout;
3089 mContentView.setBackgroundResource(
3090 com.android.internal.R.drawable.text_edit_side_paste_window);
3091
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003092 LayoutInflater inflater = (LayoutInflater) mTextView.getContext()
3093 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003094
3095 LayoutParams wrapContent = new LayoutParams(
3096 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
3097
3098 mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
3099 mDeleteTextView.setLayoutParams(wrapContent);
3100 mDeleteTextView.setText(com.android.internal.R.string.delete);
3101 mDeleteTextView.setOnClickListener(this);
3102 mContentView.addView(mDeleteTextView);
3103 }
3104
Gilles Debunnec62589c2012-04-12 14:50:23 -07003105 public void setEasyEditSpan(EasyEditSpan easyEditSpan) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003106 mEasyEditSpan = easyEditSpan;
Gilles Debunned88876a2012-03-16 17:34:04 -07003107 }
3108
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003109 private void setOnDeleteListener(EasyEditDeleteListener listener) {
3110 mOnDeleteListener = listener;
3111 }
3112
Gilles Debunned88876a2012-03-16 17:34:04 -07003113 @Override
3114 public void onClick(View view) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003115 if (view == mDeleteTextView
3116 && mEasyEditSpan != null && mEasyEditSpan.isDeleteEnabled()
3117 && mOnDeleteListener != null) {
3118 mOnDeleteListener.onDeleteClick(mEasyEditSpan);
Gilles Debunned88876a2012-03-16 17:34:04 -07003119 }
3120 }
3121
3122 @Override
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003123 public void hide() {
3124 if (mEasyEditSpan != null) {
3125 mEasyEditSpan.setDeleteEnabled(false);
3126 }
3127 mOnDeleteListener = null;
3128 super.hide();
3129 }
3130
3131 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07003132 protected int getTextOffset() {
3133 // Place the pop-up at the end of the span
3134 Editable editable = (Editable) mTextView.getText();
3135 return editable.getSpanEnd(mEasyEditSpan);
3136 }
3137
3138 @Override
3139 protected int getVerticalLocalPosition(int line) {
Siyamed Sinira60b59d2017-07-26 09:26:41 -07003140 final Layout layout = mTextView.getLayout();
3141 return layout.getLineBottomWithoutSpacing(line);
Gilles Debunned88876a2012-03-16 17:34:04 -07003142 }
3143
3144 @Override
3145 protected int clipVertically(int positionY) {
3146 // As we display the pop-up below the span, no vertical clipping is required.
3147 return positionY;
3148 }
3149 }
3150
3151 private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
3152 // 3 handles
3153 // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003154 // 1 CursorAnchorInfoNotifier
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003155 private static final int MAXIMUM_NUMBER_OF_LISTENERS = 7;
Gilles Debunned88876a2012-03-16 17:34:04 -07003156 private TextViewPositionListener[] mPositionListeners =
3157 new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003158 private boolean[] mCanMove = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
Gilles Debunned88876a2012-03-16 17:34:04 -07003159 private boolean mPositionHasChanged = true;
3160 // Absolute position of the TextView with respect to its parent window
3161 private int mPositionX, mPositionY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003162 private int mPositionXOnScreen, mPositionYOnScreen;
Gilles Debunned88876a2012-03-16 17:34:04 -07003163 private int mNumberOfListeners;
3164 private boolean mScrollHasChanged;
3165 final int[] mTempCoords = new int[2];
3166
3167 public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
3168 if (mNumberOfListeners == 0) {
3169 updatePosition();
3170 ViewTreeObserver vto = mTextView.getViewTreeObserver();
3171 vto.addOnPreDrawListener(this);
3172 }
3173
3174 int emptySlotIndex = -1;
3175 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3176 TextViewPositionListener listener = mPositionListeners[i];
3177 if (listener == positionListener) {
3178 return;
3179 } else if (emptySlotIndex < 0 && listener == null) {
3180 emptySlotIndex = i;
3181 }
3182 }
3183
3184 mPositionListeners[emptySlotIndex] = positionListener;
3185 mCanMove[emptySlotIndex] = canMove;
3186 mNumberOfListeners++;
3187 }
3188
3189 public void removeSubscriber(TextViewPositionListener positionListener) {
3190 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3191 if (mPositionListeners[i] == positionListener) {
3192 mPositionListeners[i] = null;
3193 mNumberOfListeners--;
3194 break;
3195 }
3196 }
3197
3198 if (mNumberOfListeners == 0) {
3199 ViewTreeObserver vto = mTextView.getViewTreeObserver();
3200 vto.removeOnPreDrawListener(this);
3201 }
3202 }
3203
3204 public int getPositionX() {
3205 return mPositionX;
3206 }
3207
3208 public int getPositionY() {
3209 return mPositionY;
3210 }
3211
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003212 public int getPositionXOnScreen() {
3213 return mPositionXOnScreen;
3214 }
3215
3216 public int getPositionYOnScreen() {
3217 return mPositionYOnScreen;
3218 }
3219
Gilles Debunned88876a2012-03-16 17:34:04 -07003220 @Override
3221 public boolean onPreDraw() {
3222 updatePosition();
3223
3224 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3225 if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
3226 TextViewPositionListener positionListener = mPositionListeners[i];
3227 if (positionListener != null) {
3228 positionListener.updatePosition(mPositionX, mPositionY,
3229 mPositionHasChanged, mScrollHasChanged);
3230 }
3231 }
3232 }
3233
3234 mScrollHasChanged = false;
3235 return true;
3236 }
3237
3238 private void updatePosition() {
3239 mTextView.getLocationInWindow(mTempCoords);
3240
3241 mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
3242
3243 mPositionX = mTempCoords[0];
3244 mPositionY = mTempCoords[1];
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003245
3246 mTextView.getLocationOnScreen(mTempCoords);
3247
3248 mPositionXOnScreen = mTempCoords[0];
3249 mPositionYOnScreen = mTempCoords[1];
Gilles Debunned88876a2012-03-16 17:34:04 -07003250 }
3251
3252 public void onScrollChanged() {
3253 mScrollHasChanged = true;
3254 }
3255 }
3256
3257 private abstract class PinnedPopupWindow implements TextViewPositionListener {
3258 protected PopupWindow mPopupWindow;
3259 protected ViewGroup mContentView;
3260 int mPositionX, mPositionY;
Seigo Nonaka60490d12016-01-28 17:25:18 +09003261 int mClippingLimitLeft, mClippingLimitRight;
Gilles Debunned88876a2012-03-16 17:34:04 -07003262
3263 protected abstract void createPopupWindow();
3264 protected abstract void initContentView();
3265 protected abstract int getTextOffset();
3266 protected abstract int getVerticalLocalPosition(int line);
3267 protected abstract int clipVertically(int positionY);
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003268 protected void setUp() {
3269 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003270
3271 public PinnedPopupWindow() {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003272 // Due to calling subclass methods in base constructor, subclass constructor is not
3273 // called before subclass methods, e.g. createPopupWindow or initContentView. To give
3274 // a chance to initialize subclasses, call setUp() method here.
3275 // TODO: It is good to extract non trivial initialization code from constructor.
3276 setUp();
3277
Gilles Debunned88876a2012-03-16 17:34:04 -07003278 createPopupWindow();
3279
Alan Viverette80ebe0d2015-04-30 15:53:11 -07003280 mPopupWindow.setWindowLayoutType(
3281 WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
Gilles Debunned88876a2012-03-16 17:34:04 -07003282 mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
3283 mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
3284
3285 initContentView();
3286
3287 LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
3288 ViewGroup.LayoutParams.WRAP_CONTENT);
3289 mContentView.setLayoutParams(wrapContent);
3290
3291 mPopupWindow.setContentView(mContentView);
3292 }
3293
3294 public void show() {
3295 getPositionListener().addSubscriber(this, false /* offset is fixed */);
3296
3297 computeLocalPosition();
3298
3299 final PositionListener positionListener = getPositionListener();
3300 updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
3301 }
3302
3303 protected void measureContent() {
3304 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3305 mContentView.measure(
3306 View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
3307 View.MeasureSpec.AT_MOST),
3308 View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
3309 View.MeasureSpec.AT_MOST));
3310 }
3311
3312 /* The popup window will be horizontally centered on the getTextOffset() and vertically
3313 * positioned according to viewportToContentHorizontalOffset.
3314 *
3315 * This method assumes that mContentView has properly been measured from its content. */
3316 private void computeLocalPosition() {
3317 measureContent();
3318 final int width = mContentView.getMeasuredWidth();
3319 final int offset = getTextOffset();
3320 mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f);
3321 mPositionX += mTextView.viewportToContentHorizontalOffset();
3322
3323 final int line = mTextView.getLayout().getLineForOffset(offset);
3324 mPositionY = getVerticalLocalPosition(line);
3325 mPositionY += mTextView.viewportToContentVerticalOffset();
3326 }
3327
3328 private void updatePosition(int parentPositionX, int parentPositionY) {
3329 int positionX = parentPositionX + mPositionX;
3330 int positionY = parentPositionY + mPositionY;
3331
3332 positionY = clipVertically(positionY);
3333
3334 // Horizontal clipping
3335 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3336 final int width = mContentView.getMeasuredWidth();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003337 positionX = Math.min(
3338 displayMetrics.widthPixels - width + mClippingLimitRight, positionX);
3339 positionX = Math.max(-mClippingLimitLeft, positionX);
Gilles Debunned88876a2012-03-16 17:34:04 -07003340
3341 if (isShowing()) {
3342 mPopupWindow.update(positionX, positionY, -1, -1);
3343 } else {
3344 mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
3345 positionX, positionY);
3346 }
3347 }
3348
3349 public void hide() {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09003350 if (!isShowing()) {
3351 return;
3352 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003353 mPopupWindow.dismiss();
3354 getPositionListener().removeSubscriber(this);
3355 }
3356
3357 @Override
3358 public void updatePosition(int parentPositionX, int parentPositionY,
3359 boolean parentPositionChanged, boolean parentScrolled) {
3360 // Either parentPositionChanged or parentScrolled is true, check if still visible
3361 if (isShowing() && isOffsetVisible(getTextOffset())) {
3362 if (parentScrolled) computeLocalPosition();
3363 updatePosition(parentPositionX, parentPositionY);
3364 } else {
3365 hide();
3366 }
3367 }
3368
3369 public boolean isShowing() {
3370 return mPopupWindow.isShowing();
3371 }
3372 }
3373
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003374 private static final class SuggestionInfo {
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003375 // Range of actual suggestion within mText
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003376 int mSuggestionStart, mSuggestionEnd;
3377
3378 // The SuggestionSpan that this TextView represents
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003379 final SuggestionSpanInfo mSuggestionSpanInfo = new SuggestionSpanInfo();
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003380
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003381 // The index of this suggestion inside suggestionSpan
3382 int mSuggestionIndex;
3383
3384 final SpannableStringBuilder mText = new SpannableStringBuilder();
3385
3386 void clear() {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003387 mSuggestionSpanInfo.clear();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003388 mText.clear();
3389 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003390
3391 // Utility method to set attributes about a SuggestionSpan.
3392 void setSpanInfo(SuggestionSpan span, int spanStart, int spanEnd) {
3393 mSuggestionSpanInfo.mSuggestionSpan = span;
3394 mSuggestionSpanInfo.mSpanStart = spanStart;
3395 mSuggestionSpanInfo.mSpanEnd = spanEnd;
3396 }
3397 }
3398
3399 private static final class SuggestionSpanInfo {
3400 // The SuggestionSpan;
3401 @Nullable
3402 SuggestionSpan mSuggestionSpan;
3403
3404 // The SuggestionSpan start position
3405 int mSpanStart;
3406
3407 // The SuggestionSpan end position
3408 int mSpanEnd;
3409
3410 void clear() {
3411 mSuggestionSpan = null;
3412 }
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003413 }
3414
3415 private class SuggestionHelper {
3416 private final Comparator<SuggestionSpan> mSuggestionSpanComparator =
3417 new SuggestionSpanComparator();
3418 private final HashMap<SuggestionSpan, Integer> mSpansLengths =
3419 new HashMap<SuggestionSpan, Integer>();
3420
3421 private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
3422 public int compare(SuggestionSpan span1, SuggestionSpan span2) {
3423 final int flag1 = span1.getFlags();
3424 final int flag2 = span2.getFlags();
3425 if (flag1 != flag2) {
3426 // The order here should match what is used in updateDrawState
3427 final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
3428 final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
3429 final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
3430 final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
3431 if (easy1 && !misspelled1) return -1;
3432 if (easy2 && !misspelled2) return 1;
3433 if (misspelled1) return -1;
3434 if (misspelled2) return 1;
3435 }
3436
3437 return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
3438 }
3439 }
3440
3441 /**
3442 * Returns the suggestion spans that cover the current cursor position. The suggestion
3443 * spans are sorted according to the length of text that they are attached to.
3444 */
3445 private SuggestionSpan[] getSortedSuggestionSpans() {
3446 int pos = mTextView.getSelectionStart();
3447 Spannable spannable = (Spannable) mTextView.getText();
3448 SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
3449
3450 mSpansLengths.clear();
3451 for (SuggestionSpan suggestionSpan : suggestionSpans) {
3452 int start = spannable.getSpanStart(suggestionSpan);
3453 int end = spannable.getSpanEnd(suggestionSpan);
3454 mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
3455 }
3456
3457 // The suggestions are sorted according to their types (easy correction first, then
3458 // misspelled) and to the length of the text that they cover (shorter first).
3459 Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
3460 mSpansLengths.clear();
3461
3462 return suggestionSpans;
3463 }
3464
3465 /**
3466 * Gets the SuggestionInfo list that contains suggestion information at the current cursor
3467 * position.
3468 *
3469 * @param suggestionInfos SuggestionInfo array the results will be set.
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003470 * @param misspelledSpanInfo a struct the misspelled SuggestionSpan info will be set.
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003471 * @return the number of suggestions actually fetched.
3472 */
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003473 public int getSuggestionInfo(SuggestionInfo[] suggestionInfos,
3474 @Nullable SuggestionSpanInfo misspelledSpanInfo) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003475 final Spannable spannable = (Spannable) mTextView.getText();
3476 final SuggestionSpan[] suggestionSpans = getSortedSuggestionSpans();
3477 final int nbSpans = suggestionSpans.length;
3478 if (nbSpans == 0) return 0;
3479
3480 int numberOfSuggestions = 0;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003481 for (final SuggestionSpan suggestionSpan : suggestionSpans) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003482 final int spanStart = spannable.getSpanStart(suggestionSpan);
3483 final int spanEnd = spannable.getSpanEnd(suggestionSpan);
3484
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003485 if (misspelledSpanInfo != null
3486 && (suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
3487 misspelledSpanInfo.mSuggestionSpan = suggestionSpan;
3488 misspelledSpanInfo.mSpanStart = spanStart;
3489 misspelledSpanInfo.mSpanEnd = spanEnd;
3490 }
3491
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003492 final String[] suggestions = suggestionSpan.getSuggestions();
3493 final int nbSuggestions = suggestions.length;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003494 suggestionLoop:
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003495 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
3496 final String suggestion = suggestions[suggestionIndex];
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003497 for (int i = 0; i < numberOfSuggestions; i++) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003498 final SuggestionInfo otherSuggestionInfo = suggestionInfos[i];
3499 if (otherSuggestionInfo.mText.toString().equals(suggestion)) {
3500 final int otherSpanStart =
3501 otherSuggestionInfo.mSuggestionSpanInfo.mSpanStart;
3502 final int otherSpanEnd =
3503 otherSuggestionInfo.mSuggestionSpanInfo.mSpanEnd;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003504 if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003505 continue suggestionLoop;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003506 }
3507 }
3508 }
3509
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003510 SuggestionInfo suggestionInfo = suggestionInfos[numberOfSuggestions];
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003511 suggestionInfo.setSpanInfo(suggestionSpan, spanStart, spanEnd);
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003512 suggestionInfo.mSuggestionIndex = suggestionIndex;
3513 suggestionInfo.mSuggestionStart = 0;
3514 suggestionInfo.mSuggestionEnd = suggestion.length();
3515 suggestionInfo.mText.replace(0, suggestionInfo.mText.length(), suggestion);
3516 numberOfSuggestions++;
3517 if (numberOfSuggestions >= suggestionInfos.length) {
3518 return numberOfSuggestions;
3519 }
3520 }
3521 }
3522 return numberOfSuggestions;
3523 }
3524 }
3525
Yohei Yukawaca9376c2019-02-01 23:38:30 -08003526 private final class SuggestionsPopupWindow extends PinnedPopupWindow
3527 implements OnItemClickListener {
Gilles Debunned88876a2012-03-16 17:34:04 -07003528 private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003529
3530 // Key of intent extras for inserting new word into user dictionary.
3531 private static final String USER_DICTIONARY_EXTRA_WORD = "word";
3532 private static final String USER_DICTIONARY_EXTRA_LOCALE = "locale";
3533
Gilles Debunned88876a2012-03-16 17:34:04 -07003534 private SuggestionInfo[] mSuggestionInfos;
3535 private int mNumberOfSuggestions;
3536 private boolean mCursorWasVisibleBeforeSuggestions;
3537 private boolean mIsShowingUp = false;
3538 private SuggestionAdapter mSuggestionsAdapter;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003539 private TextAppearanceSpan mHighlightSpan; // TODO: Make mHighlightSpan final.
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003540 private TextView mAddToDictionaryButton;
3541 private TextView mDeleteButton;
Seigo Nonakaf47976e2016-03-01 09:17:37 -08003542 private ListView mSuggestionListView;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003543 private final SuggestionSpanInfo mMisspelledSpanInfo = new SuggestionSpanInfo();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003544 private int mContainerMarginWidth;
3545 private int mContainerMarginTop;
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003546 private LinearLayout mContainerView;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003547 private Context mContext; // TODO: Make mContext final.
Gilles Debunned88876a2012-03-16 17:34:04 -07003548
3549 private class CustomPopupWindow extends PopupWindow {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003550
Gilles Debunned88876a2012-03-16 17:34:04 -07003551 @Override
3552 public void dismiss() {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09003553 if (!isShowing()) {
3554 return;
3555 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003556 super.dismiss();
Gilles Debunned88876a2012-03-16 17:34:04 -07003557 getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
3558
3559 // Safe cast since show() checks that mTextView.getText() is an Editable
3560 ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan);
3561
3562 mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions);
Keisuke Kuroyanagi4a696ac2016-02-23 11:02:07 -08003563 if (hasInsertionController() && !extractedTextModeWillBeStarted()) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003564 getInsertionController().show();
3565 }
3566 }
3567 }
3568
3569 public SuggestionsPopupWindow() {
3570 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
Gilles Debunned88876a2012-03-16 17:34:04 -07003571 }
3572
3573 @Override
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003574 protected void setUp() {
3575 mContext = applyDefaultTheme(mTextView.getContext());
3576 mHighlightSpan = new TextAppearanceSpan(mContext,
3577 mTextView.mTextEditSuggestionHighlightStyle);
3578 }
3579
3580 private Context applyDefaultTheme(Context originalContext) {
3581 TypedArray a = originalContext.obtainStyledAttributes(
3582 new int[]{com.android.internal.R.attr.isLightTheme});
3583 boolean isLightTheme = a.getBoolean(0, true);
3584 int themeId = isLightTheme ? R.style.ThemeOverlay_Material_Light
3585 : R.style.ThemeOverlay_Material_Dark;
3586 a.recycle();
3587 return new ContextThemeWrapper(originalContext, themeId);
3588 }
3589
3590 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07003591 protected void createPopupWindow() {
Seigo Nonaka3ed1b392016-01-19 13:54:59 +09003592 mPopupWindow = new CustomPopupWindow();
Gilles Debunned88876a2012-03-16 17:34:04 -07003593 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
Seigo Nonaka3ed1b392016-01-19 13:54:59 +09003594 mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
Gilles Debunned88876a2012-03-16 17:34:04 -07003595 mPopupWindow.setFocusable(true);
3596 mPopupWindow.setClippingEnabled(false);
3597 }
3598
3599 @Override
3600 protected void initContentView() {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003601 final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
3602 Context.LAYOUT_INFLATER_SERVICE);
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003603 mContentView = (ViewGroup) inflater.inflate(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003604 mTextView.mTextEditSuggestionContainerLayout, null);
Gilles Debunned88876a2012-03-16 17:34:04 -07003605
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003606 mContainerView = (LinearLayout) mContentView.findViewById(
3607 com.android.internal.R.id.suggestionWindowContainer);
Seigo Nonaka60490d12016-01-28 17:25:18 +09003608 ViewGroup.MarginLayoutParams lp =
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003609 (ViewGroup.MarginLayoutParams) mContainerView.getLayoutParams();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003610 mContainerMarginWidth = lp.leftMargin + lp.rightMargin;
3611 mContainerMarginTop = lp.topMargin;
3612 mClippingLimitLeft = lp.leftMargin;
3613 mClippingLimitRight = lp.rightMargin;
3614
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003615 mSuggestionListView = (ListView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003616 com.android.internal.R.id.suggestionContainer);
3617
3618 mSuggestionsAdapter = new SuggestionAdapter();
Seigo Nonakaf47976e2016-03-01 09:17:37 -08003619 mSuggestionListView.setAdapter(mSuggestionsAdapter);
3620 mSuggestionListView.setOnItemClickListener(this);
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003621
3622 // Inflate the suggestion items once and for all.
3623 mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS];
Gilles Debunned88876a2012-03-16 17:34:04 -07003624 for (int i = 0; i < mSuggestionInfos.length; i++) {
3625 mSuggestionInfos[i] = new SuggestionInfo();
3626 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003627
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003628 mAddToDictionaryButton = (TextView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003629 com.android.internal.R.id.addToDictionaryButton);
3630 mAddToDictionaryButton.setOnClickListener(new View.OnClickListener() {
3631 public void onClick(View v) {
Keisuke Kuroyanagi6e0860d2016-03-15 15:40:43 +09003632 final SuggestionSpan misspelledSpan =
3633 findEquivalentSuggestionSpan(mMisspelledSpanInfo);
3634 if (misspelledSpan == null) {
3635 // Span has been removed.
3636 return;
3637 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003638 final Editable editable = (Editable) mTextView.getText();
Keisuke Kuroyanagi6e0860d2016-03-15 15:40:43 +09003639 final int spanStart = editable.getSpanStart(misspelledSpan);
3640 final int spanEnd = editable.getSpanEnd(misspelledSpan);
3641 if (spanStart < 0 || spanEnd <= spanStart) {
3642 return;
3643 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003644 final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
3645
3646 final Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
3647 intent.putExtra(USER_DICTIONARY_EXTRA_WORD, originalText);
3648 intent.putExtra(USER_DICTIONARY_EXTRA_LOCALE,
3649 mTextView.getTextServicesLocale().toString());
3650 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
Yohei Yukawa0115ac12019-02-05 22:27:20 -08003651 mTextView.startActivityAsTextOperationUserIfNecessary(intent);
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003652 // There is no way to know if the word was indeed added. Re-check.
3653 // TODO The ExtractEditText should remove the span in the original text instead
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003654 editable.removeSpan(mMisspelledSpanInfo.mSuggestionSpan);
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003655 Selection.setSelection(editable, spanEnd);
3656 updateSpellCheckSpans(spanStart, spanEnd, false);
3657 hideWithCleanUp();
3658 }
3659 });
3660
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003661 mDeleteButton = (TextView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003662 com.android.internal.R.id.deleteButton);
3663 mDeleteButton.setOnClickListener(new View.OnClickListener() {
3664 public void onClick(View v) {
3665 final Editable editable = (Editable) mTextView.getText();
3666
3667 final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
3668 int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
3669 if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
3670 // Do not leave two adjacent spaces after deletion, or one at beginning of
3671 // text
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003672 if (spanUnionEnd < editable.length()
3673 && Character.isSpaceChar(editable.charAt(spanUnionEnd))
3674 && (spanUnionStart == 0
3675 || Character.isSpaceChar(
3676 editable.charAt(spanUnionStart - 1)))) {
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003677 spanUnionEnd = spanUnionEnd + 1;
3678 }
3679 mTextView.deleteText_internal(spanUnionStart, spanUnionEnd);
3680 }
3681 hideWithCleanUp();
3682 }
3683 });
3684
Gilles Debunned88876a2012-03-16 17:34:04 -07003685 }
3686
3687 public boolean isShowingUp() {
3688 return mIsShowingUp;
3689 }
3690
3691 public void onParentLostFocus() {
3692 mIsShowingUp = false;
3693 }
3694
Gilles Debunned88876a2012-03-16 17:34:04 -07003695 private class SuggestionAdapter extends BaseAdapter {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003696 private LayoutInflater mInflater = (LayoutInflater) mContext.getSystemService(
3697 Context.LAYOUT_INFLATER_SERVICE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003698
3699 @Override
3700 public int getCount() {
3701 return mNumberOfSuggestions;
3702 }
3703
3704 @Override
3705 public Object getItem(int position) {
3706 return mSuggestionInfos[position];
3707 }
3708
3709 @Override
3710 public long getItemId(int position) {
3711 return position;
3712 }
3713
3714 @Override
3715 public View getView(int position, View convertView, ViewGroup parent) {
3716 TextView textView = (TextView) convertView;
3717
3718 if (textView == null) {
3719 textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout,
3720 parent, false);
3721 }
3722
3723 final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003724 textView.setText(suggestionInfo.mText);
Gilles Debunned88876a2012-03-16 17:34:04 -07003725 return textView;
3726 }
3727 }
3728
Gilles Debunned88876a2012-03-16 17:34:04 -07003729 @Override
3730 public void show() {
3731 if (!(mTextView.getText() instanceof Editable)) return;
Keisuke Kuroyanagi4a696ac2016-02-23 11:02:07 -08003732 if (extractedTextModeWillBeStarted()) {
3733 return;
3734 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003735
3736 if (updateSuggestions()) {
3737 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
3738 mTextView.setCursorVisible(false);
3739 mIsShowingUp = true;
3740 super.show();
3741 }
Clara Bayarri428e5232017-07-18 16:42:16 +01003742
3743 mSuggestionListView.setVisibility(mNumberOfSuggestions == 0 ? View.GONE : View.VISIBLE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003744 }
3745
3746 @Override
3747 protected void measureContent() {
3748 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3749 final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
3750 displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
3751 final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
3752 displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
3753
3754 int width = 0;
3755 View view = null;
3756 for (int i = 0; i < mNumberOfSuggestions; i++) {
3757 view = mSuggestionsAdapter.getView(i, view, mContentView);
3758 view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
3759 view.measure(horizontalMeasure, verticalMeasure);
3760 width = Math.max(width, view.getMeasuredWidth());
3761 }
3762
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003763 if (mAddToDictionaryButton.getVisibility() != View.GONE) {
3764 mAddToDictionaryButton.measure(horizontalMeasure, verticalMeasure);
3765 width = Math.max(width, mAddToDictionaryButton.getMeasuredWidth());
3766 }
3767
3768 mDeleteButton.measure(horizontalMeasure, verticalMeasure);
3769 width = Math.max(width, mDeleteButton.getMeasuredWidth());
3770
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003771 width += mContainerView.getPaddingLeft() + mContainerView.getPaddingRight()
3772 + mContainerMarginWidth;
Seigo Nonaka60490d12016-01-28 17:25:18 +09003773
Gilles Debunned88876a2012-03-16 17:34:04 -07003774 // Enforce the width based on actual text widths
3775 mContentView.measure(
3776 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
3777 verticalMeasure);
3778
3779 Drawable popupBackground = mPopupWindow.getBackground();
3780 if (popupBackground != null) {
3781 if (mTempRect == null) mTempRect = new Rect();
3782 popupBackground.getPadding(mTempRect);
3783 width += mTempRect.left + mTempRect.right;
3784 }
3785 mPopupWindow.setWidth(width);
3786 }
3787
3788 @Override
3789 protected int getTextOffset() {
Keisuke Kuroyanagi713be062016-02-29 16:07:54 -08003790 return (mTextView.getSelectionStart() + mTextView.getSelectionStart()) / 2;
Gilles Debunned88876a2012-03-16 17:34:04 -07003791 }
3792
3793 @Override
3794 protected int getVerticalLocalPosition(int line) {
Siyamed Sinira60b59d2017-07-26 09:26:41 -07003795 final Layout layout = mTextView.getLayout();
3796 return layout.getLineBottomWithoutSpacing(line) - mContainerMarginTop;
Gilles Debunned88876a2012-03-16 17:34:04 -07003797 }
3798
3799 @Override
3800 protected int clipVertically(int positionY) {
3801 final int height = mContentView.getMeasuredHeight();
3802 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3803 return Math.min(positionY, displayMetrics.heightPixels - height);
3804 }
3805
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003806 private void hideWithCleanUp() {
3807 for (final SuggestionInfo info : mSuggestionInfos) {
3808 info.clear();
3809 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003810 mMisspelledSpanInfo.clear();
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003811 hide();
Gilles Debunned88876a2012-03-16 17:34:04 -07003812 }
3813
3814 private boolean updateSuggestions() {
3815 Spannable spannable = (Spannable) mTextView.getText();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003816 mNumberOfSuggestions =
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003817 mSuggestionHelper.getSuggestionInfo(mSuggestionInfos, mMisspelledSpanInfo);
3818 if (mNumberOfSuggestions == 0 && mMisspelledSpanInfo.mSuggestionSpan == null) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003819 return false;
3820 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003821
Gilles Debunned88876a2012-03-16 17:34:04 -07003822 int spanUnionStart = mTextView.getText().length();
3823 int spanUnionEnd = 0;
3824
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003825 for (int i = 0; i < mNumberOfSuggestions; i++) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003826 final SuggestionSpanInfo spanInfo = mSuggestionInfos[i].mSuggestionSpanInfo;
3827 spanUnionStart = Math.min(spanUnionStart, spanInfo.mSpanStart);
3828 spanUnionEnd = Math.max(spanUnionEnd, spanInfo.mSpanEnd);
3829 }
3830 if (mMisspelledSpanInfo.mSuggestionSpan != null) {
3831 spanUnionStart = Math.min(spanUnionStart, mMisspelledSpanInfo.mSpanStart);
3832 spanUnionEnd = Math.max(spanUnionEnd, mMisspelledSpanInfo.mSpanEnd);
Gilles Debunned88876a2012-03-16 17:34:04 -07003833 }
3834
3835 for (int i = 0; i < mNumberOfSuggestions; i++) {
3836 highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
3837 }
3838
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003839 // Make "Add to dictionary" item visible if there is a span with the misspelled flag
3840 int addToDictionaryButtonVisibility = View.GONE;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003841 if (mMisspelledSpanInfo.mSuggestionSpan != null) {
3842 if (mMisspelledSpanInfo.mSpanStart >= 0
3843 && mMisspelledSpanInfo.mSpanEnd > mMisspelledSpanInfo.mSpanStart) {
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003844 addToDictionaryButtonVisibility = View.VISIBLE;
Gilles Debunned88876a2012-03-16 17:34:04 -07003845 }
3846 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003847 mAddToDictionaryButton.setVisibility(addToDictionaryButtonVisibility);
Gilles Debunned88876a2012-03-16 17:34:04 -07003848
3849 if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003850 final int underlineColor;
3851 if (mNumberOfSuggestions != 0) {
3852 underlineColor =
3853 mSuggestionInfos[0].mSuggestionSpanInfo.mSuggestionSpan.getUnderlineColor();
3854 } else {
3855 underlineColor = mMisspelledSpanInfo.mSuggestionSpan.getUnderlineColor();
3856 }
3857
Gilles Debunned88876a2012-03-16 17:34:04 -07003858 if (underlineColor == 0) {
3859 // Fallback on the default highlight color when the first span does not provide one
3860 mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor);
3861 } else {
3862 final float BACKGROUND_TRANSPARENCY = 0.4f;
3863 final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
3864 mSuggestionRangeSpan.setBackgroundColor(
3865 (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
3866 }
3867 spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
3868 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
3869
3870 mSuggestionsAdapter.notifyDataSetChanged();
3871 return true;
3872 }
3873
3874 private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
3875 int unionEnd) {
3876 final Spannable text = (Spannable) mTextView.getText();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003877 final int spanStart = suggestionInfo.mSuggestionSpanInfo.mSpanStart;
3878 final int spanEnd = suggestionInfo.mSuggestionSpanInfo.mSpanEnd;
Gilles Debunned88876a2012-03-16 17:34:04 -07003879
3880 // Adjust the start/end of the suggestion span
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003881 suggestionInfo.mSuggestionStart = spanStart - unionStart;
3882 suggestionInfo.mSuggestionEnd = suggestionInfo.mSuggestionStart
3883 + suggestionInfo.mText.length();
Gilles Debunned88876a2012-03-16 17:34:04 -07003884
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003885 suggestionInfo.mText.setSpan(mHighlightSpan, 0, suggestionInfo.mText.length(),
Seigo Nonakabffbd302015-08-18 18:27:56 -07003886 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003887
3888 // Add the text before and after the span.
3889 final String textAsString = text.toString();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003890 suggestionInfo.mText.insert(0, textAsString.substring(unionStart, spanStart));
3891 suggestionInfo.mText.append(textAsString.substring(spanEnd, unionEnd));
Gilles Debunned88876a2012-03-16 17:34:04 -07003892 }
3893
3894 @Override
3895 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003896 SuggestionInfo suggestionInfo = mSuggestionInfos[position];
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003897 replaceWithSuggestion(suggestionInfo);
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003898 hideWithCleanUp();
Gilles Debunned88876a2012-03-16 17:34:04 -07003899 }
3900 }
3901
3902 /**
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003903 * An ActionMode Callback class that is used to provide actions while in text insertion or
3904 * selection mode.
Gilles Debunned88876a2012-03-16 17:34:04 -07003905 *
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003906 * The default callback provides a subset of Select All, Cut, Copy, Paste, Share and Replace
3907 * actions, depending on which of these this TextView supports and the current selection.
Gilles Debunned88876a2012-03-16 17:34:04 -07003908 */
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003909 private class TextActionModeCallback extends ActionMode.Callback2 {
Clara Bayarriea4f1502015-03-18 00:25:01 +00003910 private final Path mSelectionPath = new Path();
3911 private final RectF mSelectionBounds = new RectF();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003912 private final boolean mHasSelection;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003913 private final int mHandleHeight;
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01003914 private final Map<MenuItem, OnClickListener> mAssistClickHandlers = new HashMap<>();
Clara Bayarriea4f1502015-03-18 00:25:01 +00003915
Richard Ledley26b87222017-11-30 10:54:08 +00003916 TextActionModeCallback(@TextActionMode int mode) {
3917 mHasSelection = mode == TextActionMode.SELECTION
3918 || (mTextIsSelectable && mode == TextActionMode.TEXT_LINK);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003919 if (mHasSelection) {
3920 SelectionModifierCursorController selectionController = getSelectionController();
3921 if (selectionController.mStartHandle == null) {
3922 // As these are for initializing selectionController, hide() must be called.
Mihai Popadb68c542018-11-08 15:23:01 +00003923 loadHandleDrawables(false /* overwrite */);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003924 selectionController.initHandles();
3925 selectionController.hide();
3926 }
3927 mHandleHeight = Math.max(
3928 mSelectHandleLeft.getMinimumHeight(),
3929 mSelectHandleRight.getMinimumHeight());
3930 } else {
3931 InsertionPointCursorController insertionController = getInsertionController();
3932 if (insertionController != null) {
3933 insertionController.getHandle();
3934 mHandleHeight = mSelectHandleCenter.getMinimumHeight();
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003935 } else {
3936 mHandleHeight = 0;
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003937 }
Clara Bayarri7fc946e2015-03-31 14:48:33 +01003938 }
Clara Bayarriea4f1502015-03-18 00:25:01 +00003939 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003940
3941 @Override
3942 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01003943 mAssistClickHandlers.clear();
3944
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003945 mode.setTitle(null);
Clara Bayarri13152d12015-04-09 12:02:04 +01003946 mode.setSubtitle(null);
3947 mode.setTitleOptionalHint(true);
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003948 populateMenuWithItems(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01003949
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003950 Callback customCallback = getCustomCallback();
3951 if (customCallback != null) {
3952 if (!customCallback.onCreateActionMode(mode, menu)) {
Clara Bayarri01243ac2015-06-03 00:46:29 +01003953 // The custom mode can choose to cancel the action mode, dismiss selection.
3954 Selection.setSelection((Spannable) mTextView.getText(),
3955 mTextView.getSelectionEnd());
Clara Bayarri13152d12015-04-09 12:02:04 +01003956 return false;
3957 }
3958 }
3959
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07003960 if (mTextView.canProcessText()) {
3961 mProcessTextIntentActionsHandler.onInitializeMenu(menu);
3962 }
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +00003963
Abodunrinwa Tokideb2f492017-11-06 18:55:17 +00003964 if (mHasSelection && !mTextView.hasTransientState()) {
3965 mTextView.setHasTransientState(true);
Clara Bayarri13152d12015-04-09 12:02:04 +01003966 }
Abodunrinwa Tokideb2f492017-11-06 18:55:17 +00003967 return true;
Clara Bayarri13152d12015-04-09 12:02:04 +01003968 }
3969
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003970 private Callback getCustomCallback() {
3971 return mHasSelection
3972 ? mCustomSelectionActionModeCallback
3973 : mCustomInsertionActionModeCallback;
3974 }
3975
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003976 private void populateMenuWithItems(Menu menu) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003977 if (mTextView.canCut()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003978 menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003979 com.android.internal.R.string.cut)
3980 .setAlphabeticShortcut('x')
3981 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07003982 }
3983
3984 if (mTextView.canCopy()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003985 menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003986 com.android.internal.R.string.copy)
3987 .setAlphabeticShortcut('c')
3988 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07003989 }
3990
3991 if (mTextView.canPaste()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003992 menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003993 com.android.internal.R.string.paste)
3994 .setAlphabeticShortcut('v')
3995 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07003996 }
3997
Andrei Stingaceanu7f0c5bd2015-04-14 17:12:08 +01003998 if (mTextView.canShare()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003999 menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004000 com.android.internal.R.string.share)
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +00004001 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
Andrei Stingaceanu7f0c5bd2015-04-14 17:12:08 +01004002 }
4003
Felipe Leme2ac463e2017-03-13 14:06:25 -07004004 if (mTextView.canRequestAutofill()) {
Felipe Leme1c1626e2017-06-02 10:53:13 -07004005 final String selected = mTextView.getSelectedText();
4006 if (selected == null || selected.isEmpty()) {
4007 menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
4008 com.android.internal.R.string.autofill)
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01004009 .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
Felipe Leme1c1626e2017-06-02 10:53:13 -07004010 }
Felipe Leme2ac463e2017-03-13 14:06:25 -07004011 }
4012
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01004013 if (mTextView.canPasteAsPlainText()) {
4014 menu.add(
4015 Menu.NONE,
4016 TextView.ID_PASTE_AS_PLAIN_TEXT,
4017 MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
4018 com.android.internal.R.string.paste_as_plain_text)
4019 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
4020 }
4021
Clara Bayarri3b69fd82015-06-03 21:52:02 +01004022 updateSelectAllItem(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01004023 updateReplaceItem(menu);
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004024 updateAssistMenuItems(menu);
Gilles Debunned88876a2012-03-16 17:34:04 -07004025 }
4026
4027 @Override
4028 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01004029 updateSelectAllItem(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01004030 updateReplaceItem(menu);
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004031 updateAssistMenuItems(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01004032
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004033 Callback customCallback = getCustomCallback();
4034 if (customCallback != null) {
4035 return customCallback.onPrepareActionMode(mode, menu);
Gilles Debunned88876a2012-03-16 17:34:04 -07004036 }
4037 return true;
4038 }
4039
Clara Bayarri3b69fd82015-06-03 21:52:02 +01004040 private void updateSelectAllItem(Menu menu) {
4041 boolean canSelectAll = mTextView.canSelectAllText();
4042 boolean selectAllItemExists = menu.findItem(TextView.ID_SELECT_ALL) != null;
4043 if (canSelectAll && !selectAllItemExists) {
4044 menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
4045 com.android.internal.R.string.selectAll)
4046 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
4047 } else if (!canSelectAll && selectAllItemExists) {
4048 menu.removeItem(TextView.ID_SELECT_ALL);
4049 }
4050 }
4051
Clara Bayarri13152d12015-04-09 12:02:04 +01004052 private void updateReplaceItem(Menu menu) {
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08004053 boolean canReplace = mTextView.isSuggestionsEnabled() && shouldOfferToShowSuggestions();
Clara Bayarri13152d12015-04-09 12:02:04 +01004054 boolean replaceItemExists = menu.findItem(TextView.ID_REPLACE) != null;
4055 if (canReplace && !replaceItemExists) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01004056 menu.add(Menu.NONE, TextView.ID_REPLACE, MENU_ITEM_ORDER_REPLACE,
4057 com.android.internal.R.string.replace)
4058 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
Clara Bayarri13152d12015-04-09 12:02:04 +01004059 } else if (!canReplace && replaceItemExists) {
4060 menu.removeItem(TextView.ID_REPLACE);
4061 }
4062 }
4063
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004064 private void updateAssistMenuItems(Menu menu) {
4065 clearAssistMenuItems(menu);
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +00004066 if (!shouldEnableAssistMenuItems()) {
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004067 return;
4068 }
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +01004069 final TextClassification textClassification =
4070 getSelectionActionModeHelper().getTextClassification();
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00004071 if (textClassification == null) {
4072 return;
4073 }
Jan Althaus20d346e2018-03-23 14:03:52 +01004074 if (!textClassification.getActions().isEmpty()) {
4075 // Primary assist action (Always shown).
4076 final MenuItem item = addAssistMenuItem(menu,
4077 textClassification.getActions().get(0), TextView.ID_ASSIST,
4078 MENU_ITEM_ORDER_ASSIST, MenuItem.SHOW_AS_ACTION_ALWAYS);
4079 item.setIntent(textClassification.getIntent());
4080 } else if (hasLegacyAssistItem(textClassification)) {
4081 // Legacy primary assist action (Always shown).
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00004082 final MenuItem item = menu.add(
4083 TextView.ID_ASSIST, TextView.ID_ASSIST, MENU_ITEM_ORDER_ASSIST,
4084 textClassification.getLabel())
4085 .setIcon(textClassification.getIcon())
4086 .setIntent(textClassification.getIntent());
4087 item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Jan Althaus20d346e2018-03-23 14:03:52 +01004088 mAssistClickHandlers.put(item, TextClassification.createIntentOnClickListener(
4089 TextClassification.createPendingIntent(mTextView.getContext(),
Abodunrinwa Toki904a9312018-04-18 21:21:27 +01004090 textClassification.getIntent(),
4091 createAssistMenuItemPendingIntentRequestCode())));
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00004092 }
Jan Althaus20d346e2018-03-23 14:03:52 +01004093 final int count = textClassification.getActions().size();
4094 for (int i = 1; i < count; i++) {
4095 // Secondary assist action (Never shown).
4096 addAssistMenuItem(menu, textClassification.getActions().get(i), Menu.NONE,
4097 MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START + i - 1,
4098 MenuItem.SHOW_AS_ACTION_NEVER);
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00004099 }
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +00004100 }
4101
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +00004102 private MenuItem addAssistMenuItem(Menu menu, RemoteAction action, int itemId, int order,
Jan Althaus20d346e2018-03-23 14:03:52 +01004103 int showAsAction) {
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +00004104 final MenuItem item = menu.add(TextView.ID_ASSIST, itemId, order, action.getTitle())
Jan Althaus20d346e2018-03-23 14:03:52 +01004105 .setContentDescription(action.getContentDescription());
4106 if (action.shouldShowIcon()) {
4107 item.setIcon(action.getIcon().loadDrawable(mTextView.getContext()));
4108 }
4109 item.setShowAsAction(showAsAction);
4110 mAssistClickHandlers.put(item,
4111 TextClassification.createIntentOnClickListener(action.getActionIntent()));
4112 return item;
4113 }
4114
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004115 private void clearAssistMenuItems(Menu menu) {
4116 int i = 0;
4117 while (i < menu.size()) {
4118 final MenuItem menuItem = menu.getItem(i);
4119 if (menuItem.getGroupId() == TextView.ID_ASSIST) {
4120 menu.removeItem(menuItem.getItemId());
4121 continue;
4122 }
4123 i++;
4124 }
4125 }
4126
Jan Althaus20d346e2018-03-23 14:03:52 +01004127 private boolean hasLegacyAssistItem(TextClassification classification) {
4128 // Check whether we have the UI data and and action.
4129 return (classification.getIcon() != null || !TextUtils.isEmpty(
4130 classification.getLabel())) && (classification.getIntent() != null
4131 || classification.getOnClickListener() != null);
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004132 }
4133
4134 private boolean onAssistMenuItemClicked(MenuItem assistMenuItem) {
4135 Preconditions.checkArgument(assistMenuItem.getGroupId() == TextView.ID_ASSIST);
4136
4137 final TextClassification textClassification =
4138 getSelectionActionModeHelper().getTextClassification();
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +00004139 if (!shouldEnableAssistMenuItems() || textClassification == null) {
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004140 // No textClassification result to handle the click. Eat the click.
4141 return true;
4142 }
4143
4144 OnClickListener onClickListener = mAssistClickHandlers.get(assistMenuItem);
4145 if (onClickListener == null) {
4146 final Intent intent = assistMenuItem.getIntent();
4147 if (intent != null) {
Abodunrinwa Toki2f19b922018-02-12 19:59:28 +00004148 onClickListener = TextClassification.createIntentOnClickListener(
Abodunrinwa Toki904a9312018-04-18 21:21:27 +01004149 TextClassification.createPendingIntent(
4150 mTextView.getContext(), intent,
4151 createAssistMenuItemPendingIntentRequestCode()));
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004152 }
4153 }
4154 if (onClickListener != null) {
4155 onClickListener.onClick(mTextView);
4156 stopTextActionMode();
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004157 }
4158 // We tried our best.
4159 return true;
Abodunrinwa Toki9796a1b2017-06-28 02:49:07 +01004160 }
4161
Abodunrinwa Toki904a9312018-04-18 21:21:27 +01004162 private int createAssistMenuItemPendingIntentRequestCode() {
4163 return mTextView.hasSelection()
4164 ? mTextView.getText().subSequence(
4165 mTextView.getSelectionStart(), mTextView.getSelectionEnd())
4166 .hashCode()
4167 : 0;
4168 }
4169
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +00004170 private boolean shouldEnableAssistMenuItems() {
4171 return mTextView.isDeviceProvisioned()
4172 && TextClassificationManager.getSettings(mTextView.getContext())
4173 .isSmartTextShareEnabled();
4174 }
4175
Gilles Debunned88876a2012-03-16 17:34:04 -07004176 @Override
4177 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Abodunrinwa Toki520b2f82019-01-27 07:48:02 +00004178 getSelectionActionModeHelper()
4179 .onSelectionAction(item.getItemId(), item.getTitle().toString());
Abodunrinwa Toki1d775572017-05-08 16:03:01 +01004180
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07004181 if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +00004182 return true;
4183 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004184 Callback customCallback = getCustomCallback();
4185 if (customCallback != null && customCallback.onActionItemClicked(mode, item)) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004186 return true;
4187 }
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004188 if (item.getGroupId() == TextView.ID_ASSIST && onAssistMenuItemClicked(item)) {
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +00004189 return true;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00004190 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004191 return mTextView.onTextContextMenuItem(item.getItemId());
4192 }
4193
4194 @Override
4195 public void onDestroyActionMode(ActionMode mode) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09004196 // Clear mTextActionMode not to recursively destroy action mode by clearing selection.
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +00004197 getSelectionActionModeHelper().onDestroyActionMode();
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09004198 mTextActionMode = null;
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004199 Callback customCallback = getCustomCallback();
4200 if (customCallback != null) {
4201 customCallback.onDestroyActionMode(mode);
Gilles Debunned88876a2012-03-16 17:34:04 -07004202 }
Adam Powell057a5852012-05-11 10:28:38 -07004203
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08004204 if (!mPreserveSelection) {
4205 /*
4206 * Leave current selection when we tentatively destroy action mode for the
4207 * selection. If we're detaching from a window, we'll bring back the selection
4208 * mode when (if) we get reattached.
4209 */
Adam Powell057a5852012-05-11 10:28:38 -07004210 Selection.setSelection((Spannable) mTextView.getText(),
4211 mTextView.getSelectionEnd());
Adam Powell057a5852012-05-11 10:28:38 -07004212 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004213
4214 if (mSelectionModifierCursorController != null) {
4215 mSelectionModifierCursorController.hide();
4216 }
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004217
4218 mAssistClickHandlers.clear();
Abodunrinwa Toki52096912018-03-21 23:14:42 +00004219 mRequestingLinkActionMode = false;
Gilles Debunned88876a2012-03-16 17:34:04 -07004220 }
Clara Bayarriea4f1502015-03-18 00:25:01 +00004221
4222 @Override
4223 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
4224 if (!view.equals(mTextView) || mTextView.getLayout() == null) {
4225 super.onGetContentRect(mode, view, outRect);
4226 return;
4227 }
4228 if (mTextView.getSelectionStart() != mTextView.getSelectionEnd()) {
4229 // We have a selection.
4230 mSelectionPath.reset();
4231 mTextView.getLayout().getSelectionPath(
4232 mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mSelectionPath);
4233 mSelectionPath.computeBounds(mSelectionBounds, true);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004234 mSelectionBounds.bottom += mHandleHeight;
Clara Bayarriea4f1502015-03-18 00:25:01 +00004235 } else {
Roozbeh Pournader9c133072017-07-26 22:36:27 -07004236 // We have a cursor.
Siyamed Sinir987ec652016-02-17 19:44:41 -08004237 Layout layout = mTextView.getLayout();
Mady Mellorff66ca52015-07-08 12:31:45 -07004238 int line = layout.getLineForOffset(mTextView.getSelectionStart());
Siyamed Sinir987ec652016-02-17 19:44:41 -08004239 float primaryHorizontal = clampHorizontalPosition(null,
4240 layout.getPrimaryHorizontal(mTextView.getSelectionStart()));
Clara Bayarriea4f1502015-03-18 00:25:01 +00004241 mSelectionBounds.set(
4242 primaryHorizontal,
Mady Mellorff66ca52015-07-08 12:31:45 -07004243 layout.getLineTop(line),
Clara Bayarrif95ed102015-08-12 19:46:47 +01004244 primaryHorizontal,
Siyamed Sinirfdbc5ee2018-02-09 11:24:16 -08004245 layout.getLineBottom(line) + mHandleHeight);
Clara Bayarriea4f1502015-03-18 00:25:01 +00004246 }
4247 // Take TextView's padding and scroll into account.
4248 int textHorizontalOffset = mTextView.viewportToContentHorizontalOffset();
4249 int textVerticalOffset = mTextView.viewportToContentVerticalOffset();
4250 outRect.set(
4251 (int) Math.floor(mSelectionBounds.left + textHorizontalOffset),
4252 (int) Math.floor(mSelectionBounds.top + textVerticalOffset),
4253 (int) Math.ceil(mSelectionBounds.right + textHorizontalOffset),
4254 (int) Math.ceil(mSelectionBounds.bottom + textVerticalOffset));
4255 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004256 }
4257
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004258 /**
4259 * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}
4260 * while the input method is requesting the cursor/anchor position. Does nothing as long as
4261 * {@link InputMethodManager#isWatchingCursor(View)} returns false.
4262 */
4263 private final class CursorAnchorInfoNotifier implements TextViewPositionListener {
Yohei Yukawac46b5f02014-06-10 12:26:34 +09004264 final CursorAnchorInfo.Builder mSelectionInfoBuilder = new CursorAnchorInfo.Builder();
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004265 final int[] mTmpIntOffset = new int[2];
4266 final Matrix mViewToScreenMatrix = new Matrix();
4267
4268 @Override
4269 public void updatePosition(int parentPositionX, int parentPositionY,
4270 boolean parentPositionChanged, boolean parentScrolled) {
4271 final InputMethodState ims = mInputMethodState;
4272 if (ims == null || ims.mBatchEditNesting > 0) {
4273 return;
4274 }
Yohei Yukawa484d4af2018-09-17 16:47:08 -07004275 final InputMethodManager imm = getInputMethodManager();
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004276 if (null == imm) {
4277 return;
4278 }
Yohei Yukawa0023d0e2014-07-11 04:13:03 +09004279 if (!imm.isActive(mTextView)) {
4280 return;
4281 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004282 // Skip if the IME has not requested the cursor/anchor position.
Yohei Yukawa0023d0e2014-07-11 04:13:03 +09004283 if (!imm.isCursorAnchorInfoEnabled()) {
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004284 return;
4285 }
4286 Layout layout = mTextView.getLayout();
4287 if (layout == null) {
4288 return;
4289 }
4290
Yohei Yukawac46b5f02014-06-10 12:26:34 +09004291 final CursorAnchorInfo.Builder builder = mSelectionInfoBuilder;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004292 builder.reset();
4293
4294 final int selectionStart = mTextView.getSelectionStart();
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004295 builder.setSelectionRange(selectionStart, mTextView.getSelectionEnd());
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004296
4297 // Construct transformation matrix from view local coordinates to screen coordinates.
4298 mViewToScreenMatrix.set(mTextView.getMatrix());
4299 mTextView.getLocationOnScreen(mTmpIntOffset);
4300 mViewToScreenMatrix.postTranslate(mTmpIntOffset[0], mTmpIntOffset[1]);
4301 builder.setMatrix(mViewToScreenMatrix);
4302
4303 final float viewportToContentHorizontalOffset =
4304 mTextView.viewportToContentHorizontalOffset();
4305 final float viewportToContentVerticalOffset =
4306 mTextView.viewportToContentVerticalOffset();
4307
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004308 final CharSequence text = mTextView.getText();
4309 if (text instanceof Spannable) {
4310 final Spannable sp = (Spannable) text;
4311 int composingTextStart = EditableInputConnection.getComposingSpanStart(sp);
4312 int composingTextEnd = EditableInputConnection.getComposingSpanEnd(sp);
4313 if (composingTextEnd < composingTextStart) {
4314 final int temp = composingTextEnd;
4315 composingTextEnd = composingTextStart;
4316 composingTextStart = temp;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004317 }
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004318 final boolean hasComposingText =
4319 (0 <= composingTextStart) && (composingTextStart < composingTextEnd);
4320 if (hasComposingText) {
4321 final CharSequence composingText = text.subSequence(composingTextStart,
4322 composingTextEnd);
4323 builder.setComposingText(composingTextStart, composingText);
Phil Weaverc2e28932016-12-08 12:29:25 -08004324 mTextView.populateCharacterBounds(builder, composingTextStart,
4325 composingTextEnd, viewportToContentHorizontalOffset,
4326 viewportToContentVerticalOffset);
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004327 }
4328 }
4329
4330 // Treat selectionStart as the insertion point.
4331 if (0 <= selectionStart) {
4332 final int offset = selectionStart;
4333 final int line = layout.getLineForOffset(offset);
4334 final float insertionMarkerX = layout.getPrimaryHorizontal(offset)
4335 + viewportToContentHorizontalOffset;
4336 final float insertionMarkerTop = layout.getLineTop(line)
4337 + viewportToContentVerticalOffset;
4338 final float insertionMarkerBaseline = layout.getLineBaseline(line)
4339 + viewportToContentVerticalOffset;
Siyamed Sinira60b59d2017-07-26 09:26:41 -07004340 final float insertionMarkerBottom = layout.getLineBottomWithoutSpacing(line)
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004341 + viewportToContentVerticalOffset;
Phil Weaverc2e28932016-12-08 12:29:25 -08004342 final boolean isTopVisible = mTextView
4343 .isPositionVisible(insertionMarkerX, insertionMarkerTop);
4344 final boolean isBottomVisible = mTextView
4345 .isPositionVisible(insertionMarkerX, insertionMarkerBottom);
Yohei Yukawacc24e2b2014-08-29 20:21:10 -07004346 int insertionMarkerFlags = 0;
4347 if (isTopVisible || isBottomVisible) {
4348 insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
4349 }
4350 if (!isTopVisible || !isBottomVisible) {
4351 insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
4352 }
Yohei Yukawa5f183f02014-09-02 14:18:40 -07004353 if (layout.isRtlCharAt(offset)) {
4354 insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL;
4355 }
Yohei Yukawa0b01e7f2014-07-08 15:29:51 +09004356 builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop,
Yohei Yukawacc24e2b2014-08-29 20:21:10 -07004357 insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags);
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004358 }
4359
4360 imm.updateCursorAnchorInfo(mTextView, builder.build());
4361 }
4362 }
4363
Mihai Popa38722382018-03-07 19:56:21 +00004364 private static class MagnifierMotionAnimator {
4365 private static final long DURATION = 100 /* miliseconds */;
4366
4367 // The magnifier being animated.
4368 private final Magnifier mMagnifier;
4369 // A value animator used to animate the magnifier.
4370 private final ValueAnimator mAnimator;
4371
4372 // Whether the magnifier is currently visible.
4373 private boolean mMagnifierIsShowing;
4374 // The coordinates of the magnifier when the currently running animation started.
4375 private float mAnimationStartX;
4376 private float mAnimationStartY;
4377 // The coordinates of the magnifier in the latest animation frame.
4378 private float mAnimationCurrentX;
4379 private float mAnimationCurrentY;
4380 // The latest coordinates the motion animator was asked to #show() the magnifier at.
4381 private float mLastX;
4382 private float mLastY;
4383
4384 private MagnifierMotionAnimator(final Magnifier magnifier) {
4385 mMagnifier = magnifier;
4386 // Prepare the animator used to run the motion animation.
4387 mAnimator = ValueAnimator.ofFloat(0, 1);
4388 mAnimator.setDuration(DURATION);
4389 mAnimator.setInterpolator(new LinearInterpolator());
4390 mAnimator.addUpdateListener((animation) -> {
4391 // Interpolate to find the current position of the magnifier.
4392 mAnimationCurrentX = mAnimationStartX
4393 + (mLastX - mAnimationStartX) * animation.getAnimatedFraction();
4394 mAnimationCurrentY = mAnimationStartY
4395 + (mLastY - mAnimationStartY) * animation.getAnimatedFraction();
4396 mMagnifier.show(mAnimationCurrentX, mAnimationCurrentY);
4397 });
4398 }
4399
4400 /**
4401 * Shows the magnifier at a new position.
4402 * If the y coordinate is different from the previous y coordinate
4403 * (probably corresponding to a line jump in the text), a short
4404 * animation is added to the jump.
4405 */
4406 private void show(final float x, final float y) {
4407 final boolean startNewAnimation = mMagnifierIsShowing && y != mLastY;
4408
4409 if (startNewAnimation) {
4410 if (mAnimator.isRunning()) {
4411 mAnimator.cancel();
4412 mAnimationStartX = mAnimationCurrentX;
4413 mAnimationStartY = mAnimationCurrentY;
4414 } else {
4415 mAnimationStartX = mLastX;
4416 mAnimationStartY = mLastY;
4417 }
4418 mAnimator.start();
4419 } else {
4420 if (!mAnimator.isRunning()) {
4421 mMagnifier.show(x, y);
4422 }
4423 }
4424 mLastX = x;
4425 mLastY = y;
4426 mMagnifierIsShowing = true;
4427 }
4428
4429 /**
4430 * Updates the content of the magnifier.
4431 */
4432 private void update() {
4433 mMagnifier.update();
4434 }
4435
4436 /**
4437 * Dismisses the magnifier, or does nothing if it is already dismissed.
4438 */
4439 private void dismiss() {
4440 mMagnifier.dismiss();
4441 mAnimator.cancel();
4442 mMagnifierIsShowing = false;
4443 }
4444 }
4445
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004446 @VisibleForTesting
4447 public abstract class HandleView extends View implements TextViewPositionListener {
Gilles Debunned88876a2012-03-16 17:34:04 -07004448 protected Drawable mDrawable;
4449 protected Drawable mDrawableLtr;
4450 protected Drawable mDrawableRtl;
4451 private final PopupWindow mContainer;
4452 // Position with respect to the parent TextView
4453 private int mPositionX, mPositionY;
4454 private boolean mIsDragging;
4455 // Offset from touch position to mPosition
4456 private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
4457 protected int mHotspotX;
Adam Powell3fceabd2014-08-19 18:28:04 -07004458 protected int mHorizontalGravity;
Gilles Debunned88876a2012-03-16 17:34:04 -07004459 // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
4460 private float mTouchOffsetY;
4461 // Where the touch position should be on the handle to ensure a maximum cursor visibility
4462 private float mIdealVerticalOffset;
4463 // Parent's (TextView) previous position in window
4464 private int mLastParentX, mLastParentY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004465 // Parent's (TextView) previous position on screen
4466 private int mLastParentXOnScreen, mLastParentYOnScreen;
Gilles Debunned88876a2012-03-16 17:34:04 -07004467 // Previous text character offset
Mady Mellorc2225b92015-04-01 15:59:20 -07004468 protected int mPreviousOffset = -1;
Gilles Debunned88876a2012-03-16 17:34:04 -07004469 // Previous text character offset
4470 private boolean mPositionHasChanged = true;
Adam Powell3fceabd2014-08-19 18:28:04 -07004471 // Minimum touch target size for handles
4472 private int mMinSize;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004473 // Indicates the line of text that the handle is on.
Mady Mellora6a0f782015-07-10 16:43:32 -07004474 protected int mPrevLine = UNSET_LINE;
4475 // Indicates the line of text that the user was touching. This can differ from mPrevLine
4476 // when selecting text when the handles jump to the end / start of words which may be on
4477 // a different line.
4478 protected int mPreviousLineTouched = UNSET_LINE;
Mihai Popa6d26d152019-01-30 15:36:47 +00004479 // The raw x coordinate of the motion down event which started the current dragging session.
4480 // Only used and stored when magnifier is used.
4481 private float mCurrentDragInitialTouchRawX = UNSET_X_VALUE;
4482 // The scale transform applied by containers to the TextView. Only used and computed
4483 // when magnifier is used.
4484 private float mTextViewScaleX;
4485 private float mTextViewScaleY;
Gilles Debunned88876a2012-03-16 17:34:04 -07004486
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004487 private HandleView(Drawable drawableLtr, Drawable drawableRtl, final int id) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004488 super(mTextView.getContext());
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004489 setId(id);
Gilles Debunned88876a2012-03-16 17:34:04 -07004490 mContainer = new PopupWindow(mTextView.getContext(), null,
4491 com.android.internal.R.attr.textSelectHandleWindowStyle);
4492 mContainer.setSplitTouchEnabled(true);
4493 mContainer.setClippingEnabled(false);
4494 mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
Keisuke Kuroyanagi7340be72015-02-27 17:57:49 +09004495 mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
4496 mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
Gilles Debunned88876a2012-03-16 17:34:04 -07004497 mContainer.setContentView(this);
4498
Mihai Popa6315a322018-10-17 17:39:57 +01004499 setDrawables(drawableLtr, drawableRtl);
4500
Adam Powell3fceabd2014-08-19 18:28:04 -07004501 mMinSize = mTextView.getContext().getResources().getDimensionPixelSize(
4502 com.android.internal.R.dimen.text_handle_min_size);
Gilles Debunned88876a2012-03-16 17:34:04 -07004503
Adam Powell3fceabd2014-08-19 18:28:04 -07004504 final int handleHeight = getPreferredHeight();
Gilles Debunned88876a2012-03-16 17:34:04 -07004505 mTouchOffsetY = -0.3f * handleHeight;
4506 mIdealVerticalOffset = 0.7f * handleHeight;
4507 }
4508
Mady Mellor7a936442015-05-20 10:05:52 -07004509 public float getIdealVerticalOffset() {
4510 return mIdealVerticalOffset;
4511 }
4512
Mihai Popa6315a322018-10-17 17:39:57 +01004513 void setDrawables(final Drawable drawableLtr, final Drawable drawableRtl) {
4514 mDrawableLtr = drawableLtr;
4515 mDrawableRtl = drawableRtl;
4516 updateDrawable(true /* updateDrawableWhenDragging */);
4517 }
4518
4519 protected void updateDrawable(final boolean updateDrawableWhenDragging) {
4520 if (!updateDrawableWhenDragging && mIsDragging) {
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004521 return;
4522 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004523 final Layout layout = mTextView.getLayout();
4524 if (layout == null) {
4525 return;
4526 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004527 final int offset = getCurrentCursorOffset();
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004528 final boolean isRtlCharAtOffset = isAtRtlRun(layout, offset);
Keisuke Kuroyanagi33f81ac2015-05-14 20:10:57 +09004529 final Drawable oldDrawable = mDrawable;
Gilles Debunned88876a2012-03-16 17:34:04 -07004530 mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
4531 mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
Adam Powell3fceabd2014-08-19 18:28:04 -07004532 mHorizontalGravity = getHorizontalGravity(isRtlCharAtOffset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004533 if (oldDrawable != mDrawable && isShowing()) {
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004534 // Update popup window position.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004535 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
4536 - getHorizontalOffset() + getCursorOffset();
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004537 mPositionX += mTextView.viewportToContentHorizontalOffset();
4538 mPositionHasChanged = true;
4539 updatePosition(mLastParentX, mLastParentY, false, false);
Keisuke Kuroyanagi33f81ac2015-05-14 20:10:57 +09004540 postInvalidate();
4541 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004542 }
4543
4544 protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
Adam Powell3fceabd2014-08-19 18:28:04 -07004545 protected abstract int getHorizontalGravity(boolean isRtlRun);
Gilles Debunned88876a2012-03-16 17:34:04 -07004546
4547 // Touch-up filter: number of previous positions remembered
4548 private static final int HISTORY_SIZE = 5;
4549 private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
4550 private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
4551 private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
4552 private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
4553 private int mPreviousOffsetIndex = 0;
4554 private int mNumberPreviousOffsets = 0;
4555
4556 private void startTouchUpFilter(int offset) {
4557 mNumberPreviousOffsets = 0;
4558 addPositionToTouchUpFilter(offset);
4559 }
4560
4561 private void addPositionToTouchUpFilter(int offset) {
4562 mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
4563 mPreviousOffsets[mPreviousOffsetIndex] = offset;
4564 mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
4565 mNumberPreviousOffsets++;
4566 }
4567
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004568 private void filterOnTouchUp(boolean fromTouchScreen) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004569 final long now = SystemClock.uptimeMillis();
4570 int i = 0;
4571 int index = mPreviousOffsetIndex;
4572 final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
4573 while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
4574 i++;
4575 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
4576 }
4577
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004578 if (i > 0 && i < iMax
4579 && (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004580 positionAtCursorOffset(mPreviousOffsets[index], false, fromTouchScreen);
Gilles Debunned88876a2012-03-16 17:34:04 -07004581 }
4582 }
4583
4584 public boolean offsetHasBeenChanged() {
4585 return mNumberPreviousOffsets > 1;
4586 }
4587
4588 @Override
4589 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Adam Powell3fceabd2014-08-19 18:28:04 -07004590 setMeasuredDimension(getPreferredWidth(), getPreferredHeight());
4591 }
4592
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004593 @Override
4594 public void invalidate() {
4595 super.invalidate();
4596 if (isShowing()) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004597 positionAtCursorOffset(getCurrentCursorOffset(), true, false);
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004598 }
4599 };
4600
Adam Powell3fceabd2014-08-19 18:28:04 -07004601 private int getPreferredWidth() {
4602 return Math.max(mDrawable.getIntrinsicWidth(), mMinSize);
4603 }
4604
4605 private int getPreferredHeight() {
4606 return Math.max(mDrawable.getIntrinsicHeight(), mMinSize);
Gilles Debunned88876a2012-03-16 17:34:04 -07004607 }
4608
4609 public void show() {
Nikita Dubrovsky05cfcc82019-10-24 08:57:32 -07004610 if (TextView.DEBUG_CURSOR) {
4611 logCursor(getClass().getSimpleName() + ": HandleView: show()", "offset=%s",
4612 getCurrentCursorOffset());
4613 }
4614
Gilles Debunned88876a2012-03-16 17:34:04 -07004615 if (isShowing()) return;
4616
4617 getPositionListener().addSubscriber(this, true /* local position may change */);
4618
4619 // Make sure the offset is always considered new, even when focusing at same position
4620 mPreviousOffset = -1;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004621 positionAtCursorOffset(getCurrentCursorOffset(), false, false);
Gilles Debunned88876a2012-03-16 17:34:04 -07004622 }
4623
4624 protected void dismiss() {
4625 mIsDragging = false;
4626 mContainer.dismiss();
4627 onDetached();
4628 }
4629
4630 public void hide() {
Nikita Dubrovsky05cfcc82019-10-24 08:57:32 -07004631 if (TextView.DEBUG_CURSOR) {
4632 logCursor(getClass().getSimpleName() + ": HandleView: hide()", "offset=%s",
4633 getCurrentCursorOffset());
4634 }
4635
Gilles Debunned88876a2012-03-16 17:34:04 -07004636 dismiss();
4637
4638 getPositionListener().removeSubscriber(this);
4639 }
4640
Gilles Debunned88876a2012-03-16 17:34:04 -07004641 public boolean isShowing() {
4642 return mContainer.isShowing();
4643 }
4644
Mihai Popab1b423a2018-03-27 19:03:09 +01004645 private boolean shouldShow() {
4646 // A dragging handle should always be shown.
Gilles Debunned88876a2012-03-16 17:34:04 -07004647 if (mIsDragging) {
4648 return true;
4649 }
4650
4651 if (mTextView.isInBatchEditMode()) {
4652 return false;
4653 }
4654
Phil Weaverc2e28932016-12-08 12:29:25 -08004655 return mTextView.isPositionVisible(
4656 mPositionX + mHotspotX + getHorizontalOffset(), mPositionY);
Gilles Debunned88876a2012-03-16 17:34:04 -07004657 }
4658
Mihai Popab1b423a2018-03-27 19:03:09 +01004659 private void setVisible(final boolean visible) {
4660 mContainer.getContentView().setVisibility(visible ? VISIBLE : INVISIBLE);
4661 }
4662
Gilles Debunned88876a2012-03-16 17:34:04 -07004663 public abstract int getCurrentCursorOffset();
4664
4665 protected abstract void updateSelection(int offset);
4666
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004667 protected abstract void updatePosition(float x, float y, boolean fromTouchScreen);
Gilles Debunned88876a2012-03-16 17:34:04 -07004668
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004669 @MagnifierHandleTrigger
4670 protected abstract int getMagnifierHandleTrigger();
4671
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004672 protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
4673 return layout.isRtlCharAt(offset);
4674 }
4675
4676 @VisibleForTesting
4677 public float getHorizontal(@NonNull Layout layout, int offset) {
4678 return layout.getPrimaryHorizontal(offset);
4679 }
4680
4681 protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
4682 return mTextView.getOffsetAtCoordinate(line, x);
4683 }
4684
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004685 /**
4686 * @param offset Cursor offset. Must be in [-1, length].
4687 * @param forceUpdatePosition whether to force update the position. This should be true
4688 * when If the parent has been scrolled, for example.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004689 * @param fromTouchScreen {@code true} if the cursor is moved with motion events from the
4690 * touch screen.
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004691 */
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004692 protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
4693 boolean fromTouchScreen) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004694 // A HandleView relies on the layout, which may be nulled by external methods
4695 Layout layout = mTextView.getLayout();
4696 if (layout == null) {
4697 // Will update controllers' state, hiding them and stopping selection mode if needed
4698 prepareCursorControllers();
4699 return;
4700 }
Siyamed Sinir987ec652016-02-17 19:44:41 -08004701 layout = mTextView.getLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -07004702
4703 boolean offsetChanged = offset != mPreviousOffset;
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004704 if (offsetChanged || forceUpdatePosition) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004705 if (offsetChanged) {
4706 updateSelection(offset);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004707 if (fromTouchScreen && mHapticTextHandleEnabled) {
4708 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
4709 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004710 addPositionToTouchUpFilter(offset);
4711 }
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07004712 final int line = layout.getLineForOffset(offset);
Mady Mellorb9bbbb12015-03-23 11:50:46 -07004713 mPrevLine = line;
Gilles Debunned88876a2012-03-16 17:34:04 -07004714
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004715 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
4716 - getHorizontalOffset() + getCursorOffset();
Siyamed Sinira60b59d2017-07-26 09:26:41 -07004717 mPositionY = layout.getLineBottomWithoutSpacing(line);
Gilles Debunned88876a2012-03-16 17:34:04 -07004718
4719 // Take TextView's padding and scroll into account.
4720 mPositionX += mTextView.viewportToContentHorizontalOffset();
4721 mPositionY += mTextView.viewportToContentVerticalOffset();
4722
4723 mPreviousOffset = offset;
4724 mPositionHasChanged = true;
4725 }
4726 }
4727
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004728 /**
Roozbeh Pournader9c133072017-07-26 22:36:27 -07004729 * Return the clamped horizontal position for the cursor.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004730 *
4731 * @param layout Text layout.
4732 * @param offset Character offset for the cursor.
4733 * @return The clamped horizontal position for the cursor.
4734 */
4735 int getCursorHorizontalPosition(Layout layout, int offset) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004736 return (int) (getHorizontal(layout, offset) - 0.5f);
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004737 }
4738
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004739 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07004740 public void updatePosition(int parentPositionX, int parentPositionY,
4741 boolean parentPositionChanged, boolean parentScrolled) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004742 positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled, false);
Gilles Debunned88876a2012-03-16 17:34:04 -07004743 if (parentPositionChanged || mPositionHasChanged) {
4744 if (mIsDragging) {
4745 // Update touchToWindow offset in case of parent scrolling while dragging
4746 if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
4747 mTouchToWindowOffsetX += parentPositionX - mLastParentX;
4748 mTouchToWindowOffsetY += parentPositionY - mLastParentY;
4749 mLastParentX = parentPositionX;
4750 mLastParentY = parentPositionY;
4751 }
4752
4753 onHandleMoved();
4754 }
4755
Mihai Popab1b423a2018-03-27 19:03:09 +01004756 if (shouldShow()) {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004757 // Transform to the window coordinates to follow the view tranformation.
4758 final int[] pts = { mPositionX + mHotspotX + getHorizontalOffset(), mPositionY};
4759 mTextView.transformFromViewToWindowSpace(pts);
4760 pts[0] -= mHotspotX + getHorizontalOffset();
4761
Gilles Debunned88876a2012-03-16 17:34:04 -07004762 if (isShowing()) {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004763 mContainer.update(pts[0], pts[1], -1, -1);
Gilles Debunned88876a2012-03-16 17:34:04 -07004764 } else {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004765 mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY, pts[0], pts[1]);
Gilles Debunned88876a2012-03-16 17:34:04 -07004766 }
4767 } else {
4768 if (isShowing()) {
4769 dismiss();
4770 }
4771 }
4772
4773 mPositionHasChanged = false;
4774 }
4775 }
4776
4777 @Override
4778 protected void onDraw(Canvas c) {
Adam Powell3fceabd2014-08-19 18:28:04 -07004779 final int drawWidth = mDrawable.getIntrinsicWidth();
4780 final int left = getHorizontalOffset();
4781
4782 mDrawable.setBounds(left, 0, left + drawWidth, mDrawable.getIntrinsicHeight());
Gilles Debunned88876a2012-03-16 17:34:04 -07004783 mDrawable.draw(c);
4784 }
4785
Adam Powell3fceabd2014-08-19 18:28:04 -07004786 private int getHorizontalOffset() {
4787 final int width = getPreferredWidth();
4788 final int drawWidth = mDrawable.getIntrinsicWidth();
4789 final int left;
4790 switch (mHorizontalGravity) {
4791 case Gravity.LEFT:
4792 left = 0;
4793 break;
4794 default:
4795 case Gravity.CENTER:
4796 left = (width - drawWidth) / 2;
4797 break;
4798 case Gravity.RIGHT:
4799 left = width - drawWidth;
4800 break;
4801 }
4802 return left;
4803 }
4804
4805 protected int getCursorOffset() {
4806 return 0;
4807 }
4808
Mihai Popab1b423a2018-03-27 19:03:09 +01004809 private boolean tooLargeTextForMagnifier() {
4810 final float magnifierContentHeight = Math.round(
4811 mMagnifierAnimator.mMagnifier.getHeight()
4812 / mMagnifierAnimator.mMagnifier.getZoom());
4813 final Paint.FontMetrics fontMetrics = mTextView.getPaint().getFontMetrics();
4814 final float glyphHeight = fontMetrics.descent - fontMetrics.ascent;
Mihai Popa6d26d152019-01-30 15:36:47 +00004815 return glyphHeight * mTextViewScaleY > magnifierContentHeight;
Mihai Popab1b423a2018-03-27 19:03:09 +01004816 }
4817
Mihai Popa6d26d152019-01-30 15:36:47 +00004818 /**
4819 * Traverses the hierarchy above the text view, and computes the total scale applied
4820 * to it. If a rotation is encountered, the method returns {@code false}, indicating
4821 * that the magnifier should not be shown anyways. It would be nice to keep these two
4822 * pieces of logic separate (the rotation check and the total scale calculation),
4823 * but for efficiency we can do them in a single go.
4824 * @return whether the text view is rotated
4825 */
4826 private boolean checkForTransforms() {
Mihai Popaddf9fe02018-09-28 13:54:19 +01004827 if (mMagnifierAnimator.mMagnifierIsShowing) {
4828 // Do not check again when the magnifier is currently showing.
Mihai Popaddf9fe02018-09-28 13:54:19 +01004829 return true;
4830 }
Mihai Popa6d26d152019-01-30 15:36:47 +00004831
4832 if (mTextView.getRotation() != 0f || mTextView.getRotationX() != 0f
4833 || mTextView.getRotationY() != 0f) {
4834 return false;
4835 }
4836 mTextViewScaleX = mTextView.getScaleX();
4837 mTextViewScaleY = mTextView.getScaleY();
4838
Mihai Popaddf9fe02018-09-28 13:54:19 +01004839 ViewParent viewParent = mTextView.getParent();
4840 while (viewParent != null) {
Mihai Popa6d26d152019-01-30 15:36:47 +00004841 if (viewParent instanceof View) {
4842 final View view = (View) viewParent;
4843 if (view.getRotation() != 0f || view.getRotationX() != 0f
4844 || view.getRotationY() != 0f) {
4845 return false;
4846 }
4847 mTextViewScaleX *= view.getScaleX();
4848 mTextViewScaleY *= view.getScaleY();
Mihai Popaddf9fe02018-09-28 13:54:19 +01004849 }
4850 viewParent = viewParent.getParent();
4851 }
Mihai Popa6d26d152019-01-30 15:36:47 +00004852 return true;
Mihai Popaddf9fe02018-09-28 13:54:19 +01004853 }
4854
Mihai Popae3017462018-03-07 12:25:21 +00004855 /**
4856 * Computes the position where the magnifier should be shown, relative to
4857 * {@code mTextView}, and writes them to {@code showPosInView}. Also decides
4858 * whether the magnifier should be shown or dismissed after this touch event.
4859 * @return Whether the magnifier should be shown at the computed coordinates or dismissed.
4860 */
4861 private boolean obtainMagnifierShowCoordinates(@NonNull final MotionEvent event,
4862 final PointF showPosInView) {
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004863
4864 final int trigger = getMagnifierHandleTrigger();
4865 final int offset;
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004866 final int otherHandleOffset;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004867 switch (trigger) {
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004868 case MagnifierHandleTrigger.INSERTION:
4869 offset = mTextView.getSelectionStart();
4870 otherHandleOffset = -1;
4871 break;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004872 case MagnifierHandleTrigger.SELECTION_START:
4873 offset = mTextView.getSelectionStart();
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004874 otherHandleOffset = mTextView.getSelectionEnd();
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004875 break;
4876 case MagnifierHandleTrigger.SELECTION_END:
4877 offset = mTextView.getSelectionEnd();
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004878 otherHandleOffset = mTextView.getSelectionStart();
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004879 break;
4880 default:
4881 offset = -1;
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004882 otherHandleOffset = -1;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004883 break;
4884 }
4885
4886 if (offset == -1) {
Mihai Popae3017462018-03-07 12:25:21 +00004887 return false;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004888 }
4889
Mihai Popa6d26d152019-01-30 15:36:47 +00004890 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
4891 mCurrentDragInitialTouchRawX = event.getRawX();
4892 } else if (event.getActionMasked() == MotionEvent.ACTION_UP) {
4893 mCurrentDragInitialTouchRawX = UNSET_X_VALUE;
4894 }
4895
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004896 final Layout layout = mTextView.getLayout();
4897 final int lineNumber = layout.getLineForOffset(offset);
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004898 // Compute whether the selection handles are currently on the same line, and,
4899 // in this particular case, whether the selected text is right to left.
4900 final boolean sameLineSelection = otherHandleOffset != -1
4901 && lineNumber == layout.getLineForOffset(otherHandleOffset);
4902 final boolean rtl = sameLineSelection
4903 && (offset < otherHandleOffset)
4904 != (getHorizontal(mTextView.getLayout(), offset)
4905 < getHorizontal(mTextView.getLayout(), otherHandleOffset));
Mihai Popae3017462018-03-07 12:25:21 +00004906
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004907 // Horizontally move the magnifier smoothly, clamp inside the current line / selection.
Mihai Popa1d1ed0c2018-01-12 12:38:12 +00004908 final int[] textViewLocationOnScreen = new int[2];
4909 mTextView.getLocationOnScreen(textViewLocationOnScreen);
Mihai Popae3017462018-03-07 12:25:21 +00004910 final float touchXInView = event.getRawX() - textViewLocationOnScreen[0];
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004911 float leftBound = mTextView.getTotalPaddingLeft() - mTextView.getScrollX();
4912 float rightBound = mTextView.getTotalPaddingLeft() - mTextView.getScrollX();
4913 if (sameLineSelection && ((trigger == MagnifierHandleTrigger.SELECTION_END) ^ rtl)) {
4914 leftBound += getHorizontal(mTextView.getLayout(), otherHandleOffset);
4915 } else {
4916 leftBound += mTextView.getLayout().getLineLeft(lineNumber);
4917 }
4918 if (sameLineSelection && ((trigger == MagnifierHandleTrigger.SELECTION_START) ^ rtl)) {
4919 rightBound += getHorizontal(mTextView.getLayout(), otherHandleOffset);
4920 } else {
4921 rightBound += mTextView.getLayout().getLineRight(lineNumber);
4922 }
Mihai Popa6d26d152019-01-30 15:36:47 +00004923 leftBound *= mTextViewScaleX;
4924 rightBound *= mTextViewScaleX;
Mihai Popa38722382018-03-07 19:56:21 +00004925 final float contentWidth = Math.round(mMagnifierAnimator.mMagnifier.getWidth()
4926 / mMagnifierAnimator.mMagnifier.getZoom());
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004927 if (touchXInView < leftBound - contentWidth / 2
4928 || touchXInView > rightBound + contentWidth / 2) {
4929 // The touch is too far from the current line / selection, so hide the magnifier.
Mihai Popae3017462018-03-07 12:25:21 +00004930 return false;
4931 }
Mihai Popa6d26d152019-01-30 15:36:47 +00004932
4933 final float scaledTouchXInView;
4934 if (mTextViewScaleX == 1f) {
4935 // In the common case, do not use mCurrentDragInitialTouchRawX to compute this
4936 // coordinate, although the formula on the else branch should be equivalent.
4937 // Since the formula relies on mCurrentDragInitialTouchRawX being set on
4938 // MotionEvent.ACTION_DOWN, this makes us more defensive against cases when
4939 // the sequence of events might not look as expected: for example, a sequence of
4940 // ACTION_MOVE not preceded by ACTION_DOWN.
4941 scaledTouchXInView = touchXInView;
4942 } else {
4943 scaledTouchXInView = (event.getRawX() - mCurrentDragInitialTouchRawX)
4944 * mTextViewScaleX + mCurrentDragInitialTouchRawX
4945 - textViewLocationOnScreen[0];
4946 }
4947 showPosInView.x = Math.max(leftBound, Math.min(rightBound, scaledTouchXInView));
Mihai Popae3017462018-03-07 12:25:21 +00004948
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004949 // Vertically snap to middle of current line.
Mihai Popa6d26d152019-01-30 15:36:47 +00004950 showPosInView.y = ((mTextView.getLayout().getLineTop(lineNumber)
Andrei Stingaceanuca189fe2017-10-19 17:02:22 +01004951 + mTextView.getLayout().getLineBottom(lineNumber)) / 2.0f
Mihai Popa6d26d152019-01-30 15:36:47 +00004952 + mTextView.getTotalPaddingTop() - mTextView.getScrollY()) * mTextViewScaleY;
Mihai Popae3017462018-03-07 12:25:21 +00004953 return true;
4954 }
Mihai Popaa4e39c42018-02-20 15:31:11 +00004955
Mihai Popa63ee7f12018-04-05 12:01:53 +01004956 private boolean handleOverlapsMagnifier(@NonNull final HandleView handle,
4957 @NonNull final Rect magnifierRect) {
4958 final PopupWindow window = handle.mContainer;
4959 if (!window.hasDecorView()) {
4960 return false;
4961 }
4962 final Rect handleRect = new Rect(
4963 window.getDecorViewLayoutParams().x,
4964 window.getDecorViewLayoutParams().y,
4965 window.getDecorViewLayoutParams().x + window.getContentView().getWidth(),
4966 window.getDecorViewLayoutParams().y + window.getContentView().getHeight());
4967 return Rect.intersects(handleRect, magnifierRect);
Mihai Popa894469c2018-03-21 19:45:06 +00004968 }
4969
Mihai Popa63ee7f12018-04-05 12:01:53 +01004970 private @Nullable HandleView getOtherSelectionHandle() {
4971 final SelectionModifierCursorController controller = getSelectionController();
4972 if (controller == null || !controller.isActive()) {
4973 return null;
4974 }
4975 return controller.mStartHandle != this
4976 ? controller.mStartHandle
4977 : controller.mEndHandle;
4978 }
4979
Mihai Popac2e0bee2018-07-19 12:18:30 +01004980 private void updateHandlesVisibility() {
4981 final Point magnifierTopLeft = mMagnifierAnimator.mMagnifier.getPosition();
4982 if (magnifierTopLeft == null) {
4983 return;
Mihai Popa63ee7f12018-04-05 12:01:53 +01004984 }
Mihai Popac2e0bee2018-07-19 12:18:30 +01004985 final Rect magnifierRect = new Rect(magnifierTopLeft.x, magnifierTopLeft.y,
4986 magnifierTopLeft.x + mMagnifierAnimator.mMagnifier.getWidth(),
4987 magnifierTopLeft.y + mMagnifierAnimator.mMagnifier.getHeight());
4988 setVisible(!handleOverlapsMagnifier(HandleView.this, magnifierRect));
4989 final HandleView otherHandle = getOtherSelectionHandle();
4990 if (otherHandle != null) {
4991 otherHandle.setVisible(!handleOverlapsMagnifier(otherHandle, magnifierRect));
4992 }
4993 }
Mihai Popa63ee7f12018-04-05 12:01:53 +01004994
Mihai Popae3017462018-03-07 12:25:21 +00004995 protected final void updateMagnifier(@NonNull final MotionEvent event) {
Mihai Popa38722382018-03-07 19:56:21 +00004996 if (mMagnifierAnimator == null) {
Mihai Popae3017462018-03-07 12:25:21 +00004997 return;
4998 }
4999
5000 final PointF showPosInView = new PointF();
Mihai Popa6d26d152019-01-30 15:36:47 +00005001 final boolean shouldShow = checkForTransforms() /*check not rotated and compute scale*/
5002 && !tooLargeTextForMagnifier()
Mihai Popa894469c2018-03-21 19:45:06 +00005003 && obtainMagnifierShowCoordinates(event, showPosInView);
Mihai Popae3017462018-03-07 12:25:21 +00005004 if (shouldShow) {
5005 // Make the cursor visible and stop blinking.
5006 mRenderCursorRegardlessTiming = true;
5007 mTextView.invalidateCursorPath();
5008 suspendBlink();
Mihai Popab1b423a2018-03-27 19:03:09 +01005009
Mihai Popa38722382018-03-07 19:56:21 +00005010 mMagnifierAnimator.show(showPosInView.x, showPosInView.y);
Mihai Popac2e0bee2018-07-19 12:18:30 +01005011 updateHandlesVisibility();
Mihai Popae3017462018-03-07 12:25:21 +00005012 } else {
5013 dismissMagnifier();
5014 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005015 }
5016
5017 protected final void dismissMagnifier() {
Mihai Popa38722382018-03-07 19:56:21 +00005018 if (mMagnifierAnimator != null) {
5019 mMagnifierAnimator.dismiss();
Mihai Popaa4e39c42018-02-20 15:31:11 +00005020 mRenderCursorRegardlessTiming = false;
Andrei Stingaceanu451f9472017-10-13 16:41:28 +01005021 resumeBlink();
Mihai Popab1b423a2018-03-27 19:03:09 +01005022 setVisible(true);
Mihai Popa63ee7f12018-04-05 12:01:53 +01005023 final HandleView otherHandle = getOtherSelectionHandle();
5024 if (otherHandle != null) {
5025 otherHandle.setVisible(true);
5026 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005027 }
5028 }
5029
Gilles Debunned88876a2012-03-16 17:34:04 -07005030 @Override
5031 public boolean onTouchEvent(MotionEvent ev) {
Nikita Dubrovsky05cfcc82019-10-24 08:57:32 -07005032 if (TextView.DEBUG_CURSOR) {
5033 logCursor(this.getClass().getSimpleName() + ": HandleView: onTouchEvent",
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08005034 "%d: %s (%f,%f)",
5035 ev.getSequenceNumber(),
5036 MotionEvent.actionToString(ev.getActionMasked()),
5037 ev.getX(), ev.getY());
Nikita Dubrovsky05cfcc82019-10-24 08:57:32 -07005038 }
5039
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01005040 updateFloatingToolbarVisibility(ev);
5041
Gilles Debunned88876a2012-03-16 17:34:04 -07005042 switch (ev.getActionMasked()) {
5043 case MotionEvent.ACTION_DOWN: {
5044 startTouchUpFilter(getCurrentCursorOffset());
Gilles Debunned88876a2012-03-16 17:34:04 -07005045
5046 final PositionListener positionListener = getPositionListener();
5047 mLastParentX = positionListener.getPositionX();
5048 mLastParentY = positionListener.getPositionY();
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09005049 mLastParentXOnScreen = positionListener.getPositionXOnScreen();
5050 mLastParentYOnScreen = positionListener.getPositionYOnScreen();
5051
5052 final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
5053 final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
5054 mTouchToWindowOffsetX = xInWindow - mPositionX;
5055 mTouchToWindowOffsetY = yInWindow - mPositionY;
5056
Gilles Debunned88876a2012-03-16 17:34:04 -07005057 mIsDragging = true;
Mady Mellora6a0f782015-07-10 16:43:32 -07005058 mPreviousLineTouched = UNSET_LINE;
Gilles Debunned88876a2012-03-16 17:34:04 -07005059 break;
5060 }
5061
5062 case MotionEvent.ACTION_MOVE: {
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09005063 final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
5064 final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005065
5066 // Vertical hysteresis: vertical down movement tends to snap to ideal offset
5067 final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09005068 final float currentVerticalOffset = yInWindow - mPositionY - mLastParentY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005069 float newVerticalOffset;
5070 if (previousVerticalOffset < mIdealVerticalOffset) {
5071 newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
5072 newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
5073 } else {
5074 newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
5075 newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
5076 }
5077 mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
5078
Keisuke Kuroyanagibc89a5c2015-05-18 14:49:29 +09005079 final float newPosX =
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09005080 xInWindow - mTouchToWindowOffsetX + mHotspotX + getHorizontalOffset();
5081 final float newPosY = yInWindow - mTouchToWindowOffsetY + mTouchOffsetY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005082
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005083 updatePosition(newPosX, newPosY,
5084 ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Gilles Debunned88876a2012-03-16 17:34:04 -07005085 break;
5086 }
5087
5088 case MotionEvent.ACTION_UP:
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005089 filterOnTouchUp(ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005090 // Fall through.
Gilles Debunned88876a2012-03-16 17:34:04 -07005091 case MotionEvent.ACTION_CANCEL:
5092 mIsDragging = false;
Mihai Popa6315a322018-10-17 17:39:57 +01005093 updateDrawable(false /* updateDrawableWhenDragging */);
Gilles Debunned88876a2012-03-16 17:34:04 -07005094 break;
5095 }
5096 return true;
5097 }
5098
5099 public boolean isDragging() {
5100 return mIsDragging;
5101 }
5102
Clara Bayarri6351e662015-03-16 23:17:59 +00005103 void onHandleMoved() {}
Gilles Debunned88876a2012-03-16 17:34:04 -07005104
Clara Bayarri6351e662015-03-16 23:17:59 +00005105 public void onDetached() {}
Adam Powell86241212019-06-10 08:38:49 -07005106
5107 @Override
5108 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
5109 super.onSizeChanged(w, h, oldw, oldh);
5110 setSystemGestureExclusionRects(Collections.singletonList(new Rect(0, 0, w, h)));
5111 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005112 }
5113
5114 private class InsertionHandleView extends HandleView {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01005115 // Used to detect taps on the insertion handle, which will affect the insertion action mode
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08005116 private float mLastDownRawX, mLastDownRawY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005117 private Runnable mHider;
5118
5119 public InsertionHandleView(Drawable drawable) {
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09005120 super(drawable, drawable, com.android.internal.R.id.insertion_handle);
Gilles Debunned88876a2012-03-16 17:34:04 -07005121 }
5122
Gilles Debunned88876a2012-03-16 17:34:04 -07005123 private void hideAfterDelay() {
5124 if (mHider == null) {
5125 mHider = new Runnable() {
5126 public void run() {
5127 hide();
5128 }
5129 };
5130 } else {
5131 removeHiderCallback();
5132 }
5133 mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
5134 }
5135
5136 private void removeHiderCallback() {
5137 if (mHider != null) {
5138 mTextView.removeCallbacks(mHider);
5139 }
5140 }
5141
5142 @Override
5143 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
5144 return drawable.getIntrinsicWidth() / 2;
5145 }
5146
5147 @Override
Adam Powell3fceabd2014-08-19 18:28:04 -07005148 protected int getHorizontalGravity(boolean isRtlRun) {
5149 return Gravity.CENTER_HORIZONTAL;
5150 }
5151
5152 @Override
5153 protected int getCursorOffset() {
5154 int offset = super.getCursorOffset();
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07005155 if (mDrawableForCursor != null) {
5156 mDrawableForCursor.getPadding(mTempRect);
5157 offset += (mDrawableForCursor.getIntrinsicWidth()
Roozbeh Pournader9c133072017-07-26 22:36:27 -07005158 - mTempRect.left - mTempRect.right) / 2;
Adam Powell3fceabd2014-08-19 18:28:04 -07005159 }
5160 return offset;
5161 }
5162
5163 @Override
Siyamed Sinir217c0f72016-02-01 18:30:02 -08005164 int getCursorHorizontalPosition(Layout layout, int offset) {
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07005165 if (mDrawableForCursor != null) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005166 final float horizontal = getHorizontal(layout, offset);
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07005167 return clampHorizontalPosition(mDrawableForCursor, horizontal) + mTempRect.left;
Siyamed Sinir217c0f72016-02-01 18:30:02 -08005168 }
5169 return super.getCursorHorizontalPosition(layout, offset);
5170 }
5171
5172 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07005173 public boolean onTouchEvent(MotionEvent ev) {
5174 final boolean result = super.onTouchEvent(ev);
5175
5176 switch (ev.getActionMasked()) {
5177 case MotionEvent.ACTION_DOWN:
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08005178 mLastDownRawX = ev.getRawX();
5179 mLastDownRawY = ev.getRawY();
Mihai Popae3017462018-03-07 12:25:21 +00005180 updateMagnifier(ev);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005181 break;
5182
5183 case MotionEvent.ACTION_MOVE:
Mihai Popae3017462018-03-07 12:25:21 +00005184 updateMagnifier(ev);
Gilles Debunned88876a2012-03-16 17:34:04 -07005185 break;
5186
5187 case MotionEvent.ACTION_UP:
5188 if (!offsetHasBeenChanged()) {
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08005189 ViewConfiguration config = ViewConfiguration.get(mTextView.getContext());
5190 boolean isWithinTouchSlop = EditorTouchState.isDistanceWithin(
5191 mLastDownRawX, mLastDownRawY, ev.getRawX(), ev.getRawY(),
5192 config.getScaledTouchSlop());
5193 if (isWithinTouchSlop) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01005194 // Tapping on the handle toggles the insertion action mode.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005195 if (mTextActionMode != null) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005196 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07005197 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005198 startInsertionActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07005199 }
5200 }
Abodunrinwa Tokibcdf0ab2015-04-25 00:11:25 +01005201 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005202 if (mTextActionMode != null) {
5203 mTextActionMode.invalidateContentRect();
Abodunrinwa Tokibcdf0ab2015-04-25 00:11:25 +01005204 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005205 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005206 // Fall through.
Gilles Debunned88876a2012-03-16 17:34:04 -07005207 case MotionEvent.ACTION_CANCEL:
5208 hideAfterDelay();
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005209 dismissMagnifier();
Gilles Debunned88876a2012-03-16 17:34:04 -07005210 break;
5211
5212 default:
5213 break;
5214 }
5215
5216 return result;
5217 }
5218
5219 @Override
5220 public int getCurrentCursorOffset() {
5221 return mTextView.getSelectionStart();
5222 }
5223
5224 @Override
5225 public void updateSelection(int offset) {
5226 Selection.setSelection((Spannable) mTextView.getText(), offset);
5227 }
5228
5229 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005230 protected void updatePosition(float x, float y, boolean fromTouchScreen) {
Mady Melloree3821e2015-06-05 11:12:01 -07005231 Layout layout = mTextView.getLayout();
5232 int offset;
5233 if (layout != null) {
Mady Mellora6a0f782015-07-10 16:43:32 -07005234 if (mPreviousLineTouched == UNSET_LINE) {
5235 mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
5236 }
5237 int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005238 offset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellora6a0f782015-07-10 16:43:32 -07005239 mPreviousLineTouched = currLine;
Mady Melloree3821e2015-06-05 11:12:01 -07005240 } else {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005241 offset = -1;
Mady Melloree3821e2015-06-05 11:12:01 -07005242 }
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08005243 if (TextView.DEBUG_CURSOR) {
5244 logCursor("InsertionHandleView: updatePosition", "x=%f, y=%f, offset=%d, line=%d",
5245 x, y, offset, mPreviousLineTouched);
5246 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005247 positionAtCursorOffset(offset, false, fromTouchScreen);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005248 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01005249 invalidateActionMode();
Clara Bayarri1baed512015-05-11 15:29:16 +01005250 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005251 }
5252
5253 @Override
5254 void onHandleMoved() {
5255 super.onHandleMoved();
5256 removeHiderCallback();
5257 }
5258
5259 @Override
5260 public void onDetached() {
5261 super.onDetached();
5262 removeHiderCallback();
5263 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005264
5265 @Override
5266 @MagnifierHandleTrigger
5267 protected int getMagnifierHandleTrigger() {
5268 return MagnifierHandleTrigger.INSERTION;
5269 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005270 }
5271
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005272 @Retention(RetentionPolicy.SOURCE)
Jeff Sharkeyce8db992017-12-13 20:05:05 -07005273 @IntDef(prefix = { "HANDLE_TYPE_" }, value = {
5274 HANDLE_TYPE_SELECTION_START,
5275 HANDLE_TYPE_SELECTION_END
5276 })
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005277 public @interface HandleType {}
5278 public static final int HANDLE_TYPE_SELECTION_START = 0;
5279 public static final int HANDLE_TYPE_SELECTION_END = 1;
5280
Abodunrinwa Toki4a056a52017-08-05 01:56:40 +01005281 /** For selection handles */
5282 @VisibleForTesting
5283 public final class SelectionHandleView extends HandleView {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005284 // Indicates the handle type, selection start (HANDLE_TYPE_SELECTION_START) or selection
5285 // end (HANDLE_TYPE_SELECTION_END).
5286 @HandleType
5287 private final int mHandleType;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005288 // Indicates whether the cursor is making adjustments within a word.
5289 private boolean mInWord = false;
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005290 // Difference between touch position and word boundary position.
5291 private float mTouchWordDelta;
Mady Mellore264ac32015-06-22 16:46:29 -07005292 // X value of the previous updatePosition call.
5293 private float mPrevX;
5294 // Indicates if the handle has moved a boundary between LTR and RTL text.
5295 private boolean mLanguageDirectionChanged = false;
Mady Mellor42390aa2015-07-24 13:08:42 -07005296 // Distance from edge of horizontally scrolling text view
5297 // to use to switch to character mode.
5298 private final float mTextViewEdgeSlop;
5299 // Used to save text view location.
5300 private final int[] mTextViewLocation = new int[2];
Gilles Debunned88876a2012-03-16 17:34:04 -07005301
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005302 public SelectionHandleView(Drawable drawableLtr, Drawable drawableRtl, int id,
5303 @HandleType int handleType) {
5304 super(drawableLtr, drawableRtl, id);
5305 mHandleType = handleType;
5306 ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext());
Mady Mellor42390aa2015-07-24 13:08:42 -07005307 mTextViewEdgeSlop = viewConfiguration.getScaledTouchSlop() * 4;
Gilles Debunned88876a2012-03-16 17:34:04 -07005308 }
5309
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005310 private boolean isStartHandle() {
5311 return mHandleType == HANDLE_TYPE_SELECTION_START;
5312 }
5313
Gilles Debunned88876a2012-03-16 17:34:04 -07005314 @Override
5315 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005316 if (isRtlRun == isStartHandle()) {
Mady Mellor709386f2015-05-14 12:41:18 -07005317 return drawable.getIntrinsicWidth() / 4;
5318 } else {
5319 return (drawable.getIntrinsicWidth() * 3) / 4;
5320 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005321 }
5322
5323 @Override
Adam Powell3fceabd2014-08-19 18:28:04 -07005324 protected int getHorizontalGravity(boolean isRtlRun) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005325 return (isRtlRun == isStartHandle()) ? Gravity.LEFT : Gravity.RIGHT;
Adam Powell3fceabd2014-08-19 18:28:04 -07005326 }
5327
5328 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07005329 public int getCurrentCursorOffset() {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005330 return isStartHandle() ? mTextView.getSelectionStart() : mTextView.getSelectionEnd();
Gilles Debunned88876a2012-03-16 17:34:04 -07005331 }
5332
5333 @Override
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005334 protected void updateSelection(int offset) {
5335 if (isStartHandle()) {
5336 Selection.setSelection((Spannable) mTextView.getText(), offset,
5337 mTextView.getSelectionEnd());
5338 } else {
5339 Selection.setSelection((Spannable) mTextView.getText(),
5340 mTextView.getSelectionStart(), offset);
5341 }
Mihai Popa6315a322018-10-17 17:39:57 +01005342 updateDrawable(false /* updateDrawableWhenDragging */);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005343 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01005344 invalidateActionMode();
Clara Bayarri13152d12015-04-09 12:02:04 +01005345 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005346 }
5347
5348 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005349 protected void updatePosition(float x, float y, boolean fromTouchScreen) {
Mady Mellor81fa3e82015-05-14 09:17:41 -07005350 final Layout layout = mTextView.getLayout();
Mady Mellorcc65c372015-06-17 09:25:19 -07005351 if (layout == null) {
5352 // HandleView will deal appropriately in positionAtCursorOffset when
5353 // layout is null.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005354 positionAndAdjustForCrossingHandles(mTextView.getOffsetForPosition(x, y),
5355 fromTouchScreen);
Mady Mellorcc65c372015-06-17 09:25:19 -07005356 return;
5357 }
5358
Mady Mellora6a0f782015-07-10 16:43:32 -07005359 if (mPreviousLineTouched == UNSET_LINE) {
5360 mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
5361 }
5362
Mady Mellorb9bbbb12015-03-23 11:50:46 -07005363 boolean positionCursor = false;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005364 final int anotherHandleOffset =
5365 isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
Mady Mellora6a0f782015-07-10 16:43:32 -07005366 int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005367 int initialOffset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellor81fa3e82015-05-14 09:17:41 -07005368
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005369 if (isStartHandle() && initialOffset >= anotherHandleOffset
5370 || !isStartHandle() && initialOffset <= anotherHandleOffset) {
5371 // Handles have crossed, bound it to the first selected line and
Mady Mellor81fa3e82015-05-14 09:17:41 -07005372 // adjust by word / char as normal.
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005373 currLine = layout.getLineForOffset(anotherHandleOffset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005374 initialOffset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellor81fa3e82015-05-14 09:17:41 -07005375 }
5376
5377 int offset = initialOffset;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005378 final int wordEnd = getWordEnd(offset);
5379 final int wordStart = getWordStart(offset);
Gilles Debunned88876a2012-03-16 17:34:04 -07005380
Mady Mellore264ac32015-06-22 16:46:29 -07005381 if (mPrevX == UNSET_X_VALUE) {
5382 mPrevX = x;
5383 }
5384
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005385 final int currentOffset = getCurrentCursorOffset();
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005386 final boolean rtlAtCurrentOffset = isAtRtlRun(layout, currentOffset);
5387 final boolean atRtl = isAtRtlRun(layout, offset);
Mady Mellore264ac32015-06-22 16:46:29 -07005388 final boolean isLvlBoundary = layout.isLevelBoundary(offset);
Mady Mellore264ac32015-06-22 16:46:29 -07005389
5390 // We can't determine if the user is expanding or shrinking the selection if they're
5391 // on a bi-di boundary, so until they've moved past the boundary we'll just place
5392 // the cursor at the current position.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005393 if (isLvlBoundary || (rtlAtCurrentOffset && !atRtl) || (!rtlAtCurrentOffset && atRtl)) {
Mady Mellore264ac32015-06-22 16:46:29 -07005394 // We're on a boundary or this is the first direction change -- just update
5395 // to the current position.
5396 mLanguageDirectionChanged = true;
5397 mTouchWordDelta = 0.0f;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005398 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellore264ac32015-06-22 16:46:29 -07005399 return;
5400 } else if (mLanguageDirectionChanged && !isLvlBoundary) {
5401 // We've just moved past the boundary so update the position. After this we can
5402 // figure out if the user is expanding or shrinking to go by word or character.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005403 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellore264ac32015-06-22 16:46:29 -07005404 mTouchWordDelta = 0.0f;
5405 mLanguageDirectionChanged = false;
5406 return;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005407 }
5408
5409 boolean isExpanding;
5410 final float xDiff = x - mPrevX;
Keisuke Kuroyanagi26454142015-12-02 15:04:57 -08005411 if (isStartHandle()) {
5412 isExpanding = currLine < mPreviousLineTouched;
Mady Mellore264ac32015-06-22 16:46:29 -07005413 } else {
Keisuke Kuroyanagi26454142015-12-02 15:04:57 -08005414 isExpanding = currLine > mPreviousLineTouched;
5415 }
5416 if (atRtl == isStartHandle()) {
5417 isExpanding |= xDiff > 0;
5418 } else {
5419 isExpanding |= xDiff < 0;
Mady Mellore264ac32015-06-22 16:46:29 -07005420 }
5421
Mady Mellor42390aa2015-07-24 13:08:42 -07005422 if (mTextView.getHorizontallyScrolling()) {
5423 if (positionNearEdgeOfScrollingView(x, atRtl)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005424 && ((isStartHandle() && mTextView.getScrollX() != 0)
5425 || (!isStartHandle()
5426 && mTextView.canScrollHorizontally(atRtl ? -1 : 1)))
5427 && ((isExpanding && ((isStartHandle() && offset < currentOffset)
5428 || (!isStartHandle() && offset > currentOffset)))
5429 || !isExpanding)) {
5430 // If we're expanding ensure that the offset is actually expanding compared to
5431 // the current offset, if the handle snapped to the word, the finger position
Mady Mellor42390aa2015-07-24 13:08:42 -07005432 // may be out of sync and we don't want the selection to jump back.
5433 mTouchWordDelta = 0.0f;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005434 final int nextOffset = (atRtl == isStartHandle())
5435 ? layout.getOffsetToRightOf(mPreviousOffset)
Mady Mellor42390aa2015-07-24 13:08:42 -07005436 : layout.getOffsetToLeftOf(mPreviousOffset);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005437 positionAndAdjustForCrossingHandles(nextOffset, fromTouchScreen);
Mady Mellor42390aa2015-07-24 13:08:42 -07005438 return;
5439 }
5440 }
5441
Mady Mellore264ac32015-06-22 16:46:29 -07005442 if (isExpanding) {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005443 // User is increasing the selection.
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005444 int wordBoundary = isStartHandle() ? wordStart : wordEnd;
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005445 final boolean snapToWord = (!mInWord
5446 || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine))
5447 && atRtl == isAtRtlRun(layout, wordBoundary);
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005448 if (snapToWord) {
Mady Mellora5266832015-06-26 14:28:12 -07005449 // Sometimes words can be broken across lines (Chinese, hyphenation).
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005450 // We still snap to the word boundary but we only use the letters on the
Mady Mellora5266832015-06-26 14:28:12 -07005451 // current line to determine if the user is far enough into the word to snap.
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005452 if (layout.getLineForOffset(wordBoundary) != currLine) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005453 wordBoundary = isStartHandle()
5454 ? layout.getLineStart(currLine) : layout.getLineEnd(currLine);
Mady Mellora5266832015-06-26 14:28:12 -07005455 }
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005456 final int offsetThresholdToSnap = isStartHandle()
5457 ? wordEnd - ((wordEnd - wordBoundary) / 2)
5458 : wordStart + ((wordBoundary - wordStart) / 2);
5459 if (isStartHandle()
5460 && (offset <= offsetThresholdToSnap || currLine < mPrevLine)) {
5461 // User is far enough into the word or on a different line so we expand by
5462 // word.
5463 offset = wordStart;
5464 } else if (!isStartHandle()
5465 && (offset >= offsetThresholdToSnap || currLine > mPrevLine)) {
5466 // User is far enough into the word or on a different line so we expand by
5467 // word.
5468 offset = wordEnd;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005469 } else {
Mady Mellorc2225b92015-04-01 15:59:20 -07005470 offset = mPreviousOffset;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005471 }
5472 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005473 if ((isStartHandle() && offset < initialOffset)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005474 || (!isStartHandle() && offset > initialOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005475 final float adjustedX = getHorizontal(layout, offset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005476 mTouchWordDelta =
5477 mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
Keisuke Kuroyanagi50a927c2015-05-07 17:34:21 +09005478 } else {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005479 mTouchWordDelta = 0.0f;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005480 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005481 positionCursor = true;
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005482 } else {
5483 final int adjustedOffset =
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005484 getOffsetAtCoordinate(layout, currLine, x - mTouchWordDelta);
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005485 final boolean shrinking = isStartHandle()
5486 ? adjustedOffset > mPreviousOffset || currLine > mPrevLine
5487 : adjustedOffset < mPreviousOffset || currLine < mPrevLine;
5488 if (shrinking) {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005489 // User is shrinking the selection.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005490 if (currLine != mPrevLine) {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005491 // We're on a different line, so we'll snap to word boundaries.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005492 offset = isStartHandle() ? wordStart : wordEnd;
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005493 if ((isStartHandle() && offset < initialOffset)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005494 || (!isStartHandle() && offset > initialOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005495 final float adjustedX = getHorizontal(layout, offset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005496 mTouchWordDelta =
5497 mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
5498 } else {
5499 mTouchWordDelta = 0.0f;
5500 }
5501 } else {
5502 offset = adjustedOffset;
5503 }
5504 positionCursor = true;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005505 } else if ((isStartHandle() && adjustedOffset < mPreviousOffset)
5506 || (!isStartHandle() && adjustedOffset > mPreviousOffset)) {
5507 // Handle has jumped to the word boundary, and the user is moving
Mady Mellor43fd2f42015-06-08 14:03:34 -07005508 // their finger towards the handle, the delta should be updated.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005509 mTouchWordDelta = mTextView.convertToLocalHorizontalCoordinate(x)
5510 - getHorizontal(layout, mPreviousOffset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005511 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005512 }
5513
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005514 if (positionCursor) {
Mady Mellora6a0f782015-07-10 16:43:32 -07005515 mPreviousLineTouched = currLine;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005516 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005517 }
Mady Mellore264ac32015-06-22 16:46:29 -07005518 mPrevX = x;
Gilles Debunned88876a2012-03-16 17:34:04 -07005519 }
5520
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005521 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005522 protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
5523 boolean fromTouchScreen) {
5524 super.positionAtCursorOffset(offset, forceUpdatePosition, fromTouchScreen);
Yoshiki Iguchi9582e152015-10-15 13:34:41 +09005525 mInWord = (offset != -1) && !getWordIteratorWithText().isBoundary(offset);
Mady Mellor36d5a7b2015-05-22 10:31:12 -07005526 }
5527
5528 @Override
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005529 public boolean onTouchEvent(MotionEvent event) {
5530 boolean superResult = super.onTouchEvent(event);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005531
5532 switch (event.getActionMasked()) {
5533 case MotionEvent.ACTION_DOWN:
5534 // Reset the touch word offset and x value when the user
5535 // re-engages the handle.
5536 mTouchWordDelta = 0.0f;
5537 mPrevX = UNSET_X_VALUE;
Mihai Popae3017462018-03-07 12:25:21 +00005538 updateMagnifier(event);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005539 break;
5540
5541 case MotionEvent.ACTION_MOVE:
Mihai Popae3017462018-03-07 12:25:21 +00005542 updateMagnifier(event);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005543 break;
5544
5545 case MotionEvent.ACTION_UP:
5546 case MotionEvent.ACTION_CANCEL:
5547 dismissMagnifier();
5548 break;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005549 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005550
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005551 return superResult;
5552 }
Mady Mellor42390aa2015-07-24 13:08:42 -07005553
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005554 private void positionAndAdjustForCrossingHandles(int offset, boolean fromTouchScreen) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005555 final int anotherHandleOffset =
5556 isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
5557 if ((isStartHandle() && offset >= anotherHandleOffset)
5558 || (!isStartHandle() && offset <= anotherHandleOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005559 mTouchWordDelta = 0.0f;
5560 final Layout layout = mTextView.getLayout();
5561 if (layout != null && offset != anotherHandleOffset) {
5562 final float horiz = getHorizontal(layout, offset);
5563 final float anotherHandleHoriz = getHorizontal(layout, anotherHandleOffset,
5564 !isStartHandle());
5565 final float currentHoriz = getHorizontal(layout, mPreviousOffset);
5566 if (currentHoriz < anotherHandleHoriz && horiz < anotherHandleHoriz
5567 || currentHoriz > anotherHandleHoriz && horiz > anotherHandleHoriz) {
5568 // This handle passes another one as it crossed a direction boundary.
5569 // Don't minimize the selection, but keep the handle at the run boundary.
5570 final int currentOffset = getCurrentCursorOffset();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005571 final int offsetToGetRunRange = isStartHandle()
5572 ? currentOffset : Math.max(currentOffset - 1, 0);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005573 final long range = layout.getRunRange(offsetToGetRunRange);
5574 if (isStartHandle()) {
5575 offset = TextUtils.unpackRangeStartFromLong(range);
5576 } else {
5577 offset = TextUtils.unpackRangeEndFromLong(range);
5578 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005579 positionAtCursorOffset(offset, false, fromTouchScreen);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005580 return;
5581 }
5582 }
Mady Mellor42390aa2015-07-24 13:08:42 -07005583 // Handles can not cross and selection is at least one character.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005584 offset = getNextCursorOffset(anotherHandleOffset, !isStartHandle());
Mady Mellor42390aa2015-07-24 13:08:42 -07005585 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005586 positionAtCursorOffset(offset, false, fromTouchScreen);
Mady Mellor42390aa2015-07-24 13:08:42 -07005587 }
5588
Mady Mellor42390aa2015-07-24 13:08:42 -07005589 private boolean positionNearEdgeOfScrollingView(float x, boolean atRtl) {
5590 mTextView.getLocationOnScreen(mTextViewLocation);
5591 boolean nearEdge;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005592 if (atRtl == isStartHandle()) {
Mady Mellor42390aa2015-07-24 13:08:42 -07005593 int rightEdge = mTextViewLocation[0] + mTextView.getWidth()
5594 - mTextView.getPaddingRight();
5595 nearEdge = x > rightEdge - mTextViewEdgeSlop;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005596 } else {
5597 int leftEdge = mTextViewLocation[0] + mTextView.getPaddingLeft();
5598 nearEdge = x < leftEdge + mTextViewEdgeSlop;
Mady Mellor42390aa2015-07-24 13:08:42 -07005599 }
5600 return nearEdge;
5601 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005602
5603 @Override
5604 protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
5605 final int offsetToCheck = isStartHandle() ? offset : Math.max(offset - 1, 0);
5606 return layout.isRtlCharAt(offsetToCheck);
5607 }
5608
5609 @Override
5610 public float getHorizontal(@NonNull Layout layout, int offset) {
5611 return getHorizontal(layout, offset, isStartHandle());
5612 }
5613
5614 private float getHorizontal(@NonNull Layout layout, int offset, boolean startHandle) {
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005615 final int line = layout.getLineForOffset(offset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005616 final int offsetToCheck = startHandle ? offset : Math.max(offset - 1, 0);
5617 final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
5618 final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005619 return (isRtlChar == isRtlParagraph)
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005620 ? layout.getPrimaryHorizontal(offset) : layout.getSecondaryHorizontal(offset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005621 }
5622
5623 @Override
5624 protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
Keisuke Kuroyanagib1b88652016-04-05 16:26:16 +09005625 final float localX = mTextView.convertToLocalHorizontalCoordinate(x);
5626 final int primaryOffset = layout.getOffsetForHorizontal(line, localX, true);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005627 if (!layout.isLevelBoundary(primaryOffset)) {
5628 return primaryOffset;
5629 }
Keisuke Kuroyanagib1b88652016-04-05 16:26:16 +09005630 final int secondaryOffset = layout.getOffsetForHorizontal(line, localX, false);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005631 final int currentOffset = getCurrentCursorOffset();
5632 final int primaryDiff = Math.abs(primaryOffset - currentOffset);
5633 final int secondaryDiff = Math.abs(secondaryOffset - currentOffset);
5634 if (primaryDiff < secondaryDiff) {
5635 return primaryOffset;
5636 } else if (primaryDiff > secondaryDiff) {
5637 return secondaryOffset;
5638 } else {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005639 final int offsetToCheck = isStartHandle()
5640 ? currentOffset : Math.max(currentOffset - 1, 0);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005641 final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
5642 final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
5643 return isRtlChar == isRtlParagraph ? primaryOffset : secondaryOffset;
5644 }
5645 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005646
5647 @MagnifierHandleTrigger
5648 protected int getMagnifierHandleTrigger() {
5649 return isStartHandle()
5650 ? MagnifierHandleTrigger.SELECTION_START
5651 : MagnifierHandleTrigger.SELECTION_END;
5652 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005653 }
5654
Mady Mellorcc65c372015-06-17 09:25:19 -07005655 private int getCurrentLineAdjustedForSlop(Layout layout, int prevLine, float y) {
Mady Mellor80679072015-07-09 16:05:36 -07005656 final int trueLine = mTextView.getLineAtCoordinate(y);
Mady Mellorcc65c372015-06-17 09:25:19 -07005657 if (layout == null || prevLine > layout.getLineCount()
5658 || layout.getLineCount() <= 0 || prevLine < 0) {
5659 // Invalid parameters, just return whatever line is at y.
Mady Mellor80679072015-07-09 16:05:36 -07005660 return trueLine;
5661 }
5662
5663 if (Math.abs(trueLine - prevLine) >= 2) {
5664 // Only stick to lines if we're within a line of the previous selection.
5665 return trueLine;
Mady Mellorcc65c372015-06-17 09:25:19 -07005666 }
5667
5668 final float verticalOffset = mTextView.viewportToContentVerticalOffset();
5669 final int lineCount = layout.getLineCount();
5670 final float slop = mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS;
5671
5672 final float firstLineTop = layout.getLineTop(0) + verticalOffset;
5673 final float prevLineTop = layout.getLineTop(prevLine) + verticalOffset;
5674 final float yTopBound = Math.max(prevLineTop - slop, firstLineTop + slop);
5675
5676 final float lastLineBottom = layout.getLineBottom(lineCount - 1) + verticalOffset;
5677 final float prevLineBottom = layout.getLineBottom(prevLine) + verticalOffset;
5678 final float yBottomBound = Math.min(prevLineBottom + slop, lastLineBottom - slop);
5679
5680 // Determine if we've moved lines based on y position and previous line.
5681 int currLine;
5682 if (y <= yTopBound) {
5683 currLine = Math.max(prevLine - 1, 0);
5684 } else if (y >= yBottomBound) {
5685 currLine = Math.min(prevLine + 1, lineCount - 1);
5686 } else {
5687 currLine = prevLine;
5688 }
5689 return currLine;
5690 }
5691
Gilles Debunned88876a2012-03-16 17:34:04 -07005692 /**
5693 * A CursorController instance can be used to control a cursor in the text.
5694 */
5695 private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
5696 /**
5697 * Makes the cursor controller visible on screen.
5698 * See also {@link #hide()}.
5699 */
5700 public void show();
5701
5702 /**
5703 * Hide the cursor controller from screen.
5704 * See also {@link #show()}.
5705 */
5706 public void hide();
5707
5708 /**
5709 * Called when the view is detached from window. Perform house keeping task, such as
5710 * stopping Runnable thread that would otherwise keep a reference on the context, thus
5711 * preventing the activity from being recycled.
5712 */
5713 public void onDetached();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005714
5715 public boolean isCursorBeingModified();
5716
5717 public boolean isActive();
Gilles Debunned88876a2012-03-16 17:34:04 -07005718 }
5719
Mihai Popa6c7ad1d2018-12-04 15:45:00 +00005720 void loadCursorDrawable() {
5721 if (mDrawableForCursor == null) {
5722 mDrawableForCursor = mTextView.getTextCursorDrawable();
5723 }
5724 }
5725
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08005726 class InsertionPointCursorController implements CursorController {
Gilles Debunned88876a2012-03-16 17:34:04 -07005727 private InsertionHandleView mHandle;
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08005728 private boolean mIsDraggingCursor;
5729
5730 public void onTouchEvent(MotionEvent event) {
5731 switch (event.getActionMasked()) {
5732 case MotionEvent.ACTION_DOWN:
5733 mIsDraggingCursor = false;
5734 break;
5735 case MotionEvent.ACTION_MOVE:
5736 if (mIsDraggingCursor) {
5737 performCursorDrag(event);
5738 } else if (FLAG_ENABLE_CURSOR_DRAG
5739 && mTextView.getLayout() != null
5740 && mTextView.isFocused()
5741 && mTouchState.isMovedEnoughForDrag()) {
5742 startCursorDrag(event);
5743 }
5744 break;
5745 case MotionEvent.ACTION_UP:
5746 case MotionEvent.ACTION_CANCEL:
5747 if (mIsDraggingCursor) {
5748 endCursorDrag(event);
5749 }
5750 break;
5751 }
5752 }
5753
5754 private void positionCursorDuringDrag(MotionEvent event) {
5755 int line = mTextView.getLineAtCoordinate(event.getY());
5756 int offset = mTextView.getOffsetAtCoordinate(line, event.getX());
5757 int oldSelectionStart = mTextView.getSelectionStart();
5758 int oldSelectionEnd = mTextView.getSelectionEnd();
5759 if (offset == oldSelectionStart && offset == oldSelectionEnd) {
5760 return;
5761 }
5762 Selection.setSelection((Spannable) mTextView.getText(), offset);
5763 updateCursorPosition();
5764 if (mHapticTextHandleEnabled) {
5765 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
5766 }
5767 }
5768
5769 private void startCursorDrag(MotionEvent event) {
5770 if (TextView.DEBUG_CURSOR) {
5771 logCursor("InsertionPointCursorController", "start cursor drag");
5772 }
5773 mIsDraggingCursor = true;
5774 // We don't want the parent scroll/long-press handlers to take over while dragging.
5775 mTextView.getParent().requestDisallowInterceptTouchEvent(true);
5776 mTextView.cancelLongPress();
5777 // Update the cursor position.
5778 positionCursorDuringDrag(event);
5779 // Show the cursor handle and magnifier.
5780 show();
5781 getHandle().removeHiderCallback();
5782 getHandle().updateMagnifier(event);
5783 // TODO(b/146555651): Figure out if suspendBlink() should be called here.
5784 }
5785
5786 private void performCursorDrag(MotionEvent event) {
5787 positionCursorDuringDrag(event);
5788 getHandle().updateMagnifier(event);
5789 }
5790
5791 private void endCursorDrag(MotionEvent event) {
5792 if (TextView.DEBUG_CURSOR) {
5793 logCursor("InsertionPointCursorController", "end cursor drag");
5794 }
5795 mIsDraggingCursor = false;
5796 // Hide the magnifier and set the handle to be hidden after a delay.
5797 getHandle().dismissMagnifier();
5798 getHandle().hideAfterDelay();
5799 // We're no longer dragging, so let the parent receive events.
5800 mTextView.getParent().requestDisallowInterceptTouchEvent(false);
5801 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005802
5803 public void show() {
5804 getHandle().show();
Andrei Stingaceanu35c550c2015-05-07 16:49:49 +01005805
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08005806 final long durationSinceCutOrCopy =
5807 SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime;
5808
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08005809 if (mInsertionActionModeRunnable != null) {
5810 if (mIsDraggingCursor
5811 || mTouchState.isMultiTap()
5812 || isCursorInsideEasyCorrectionSpan()) {
5813 // Cancel the runnable for showing the floating toolbar.
5814 mTextView.removeCallbacks(mInsertionActionModeRunnable);
5815 }
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08005816 }
5817
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08005818 // If the user recently performed a Cut or Copy action, we want to show the floating
5819 // toolbar even for a single tap.
5820 if (!mIsDraggingCursor
5821 && !mTouchState.isMultiTap()
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08005822 && !isCursorInsideEasyCorrectionSpan()
5823 && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION_MS)) {
5824 if (mTextActionMode == null) {
5825 if (mInsertionActionModeRunnable == null) {
5826 mInsertionActionModeRunnable = new Runnable() {
5827 @Override
5828 public void run() {
5829 startInsertionActionMode();
5830 }
5831 };
5832 }
5833 mTextView.postDelayed(
5834 mInsertionActionModeRunnable,
5835 ViewConfiguration.getDoubleTapTimeout() + 1);
5836 }
5837 }
5838
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08005839 if (!mIsDraggingCursor) {
5840 getHandle().hideAfterDelay();
5841 }
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08005842
Andrei Stingaceanu35c550c2015-05-07 16:49:49 +01005843 if (mSelectionModifierCursorController != null) {
5844 mSelectionModifierCursorController.hide();
5845 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005846 }
5847
Gilles Debunned88876a2012-03-16 17:34:04 -07005848 public void hide() {
5849 if (mHandle != null) {
5850 mHandle.hide();
5851 }
5852 }
5853
5854 public void onTouchModeChanged(boolean isInTouchMode) {
5855 if (!isInTouchMode) {
5856 hide();
5857 }
5858 }
5859
5860 private InsertionHandleView getHandle() {
Gilles Debunned88876a2012-03-16 17:34:04 -07005861 if (mHandle == null) {
Mihai Popa6315a322018-10-17 17:39:57 +01005862 loadHandleDrawables(false /* overwrite */);
Gilles Debunned88876a2012-03-16 17:34:04 -07005863 mHandle = new InsertionHandleView(mSelectHandleCenter);
5864 }
5865 return mHandle;
5866 }
5867
Mihai Popa6315a322018-10-17 17:39:57 +01005868 private void reloadHandleDrawable() {
5869 if (mHandle == null) {
5870 // No need to reload, the potentially new drawable will
5871 // be used when the handle is created.
5872 return;
5873 }
5874 mHandle.setDrawables(mSelectHandleCenter, mSelectHandleCenter);
5875 }
5876
Gilles Debunned88876a2012-03-16 17:34:04 -07005877 @Override
5878 public void onDetached() {
5879 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
5880 observer.removeOnTouchModeChangeListener(this);
5881
5882 if (mHandle != null) mHandle.onDetached();
5883 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005884
5885 @Override
5886 public boolean isCursorBeingModified() {
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08005887 return mIsDraggingCursor || (mHandle != null && mHandle.isDragging());
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005888 }
5889
5890 @Override
5891 public boolean isActive() {
5892 return mHandle != null && mHandle.isShowing();
5893 }
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09005894
5895 public void invalidateHandle() {
5896 if (mHandle != null) {
5897 mHandle.invalidate();
5898 }
5899 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005900 }
5901
5902 class SelectionModifierCursorController implements CursorController {
Gilles Debunned88876a2012-03-16 17:34:04 -07005903 // The cursor controller handles, lazily created when shown.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005904 private SelectionHandleView mStartHandle;
5905 private SelectionHandleView mEndHandle;
Gilles Debunned88876a2012-03-16 17:34:04 -07005906 // The offsets of that last touch down event. Remembered to start selection there.
5907 private int mMinTouchOffset, mMaxTouchOffset;
5908
Gilles Debunned88876a2012-03-16 17:34:04 -07005909 private boolean mGestureStayedInTapRegion;
5910
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005911 // Where the user first starts the drag motion.
5912 private int mStartOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005913
Mady Mellor7a936442015-05-20 10:05:52 -07005914 private boolean mHaventMovedEnoughToStartDrag;
Mady Mellorf9f8aeb2015-06-17 09:46:01 -07005915 // The line that a selection happened most recently with the drag accelerator.
5916 private int mLineSelectionIsOn = -1;
5917 // Whether the drag accelerator has selected past the initial line.
5918 private boolean mSwitchedLines = false;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005919
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005920 // Indicates the drag accelerator mode that the user is currently using.
5921 private int mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
5922 // Drag accelerator is inactive.
5923 private static final int DRAG_ACCELERATOR_MODE_INACTIVE = 0;
5924 // Character based selection by dragging. Only for mouse.
5925 private static final int DRAG_ACCELERATOR_MODE_CHARACTER = 1;
5926 // Word based selection by dragging. Enabled after long pressing or double tapping.
5927 private static final int DRAG_ACCELERATOR_MODE_WORD = 2;
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005928 // Paragraph based selection by dragging. Enabled after mouse triple click.
5929 private static final int DRAG_ACCELERATOR_MODE_PARAGRAPH = 3;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005930
Gilles Debunned88876a2012-03-16 17:34:04 -07005931 SelectionModifierCursorController() {
5932 resetTouchOffsets();
5933 }
5934
5935 public void show() {
5936 if (mTextView.isInBatchEditMode()) {
5937 return;
5938 }
Mihai Popa6315a322018-10-17 17:39:57 +01005939 loadHandleDrawables(false /* overwrite */);
Gilles Debunned88876a2012-03-16 17:34:04 -07005940 initHandles();
Gilles Debunned88876a2012-03-16 17:34:04 -07005941 }
5942
Gilles Debunned88876a2012-03-16 17:34:04 -07005943 private void initHandles() {
5944 // Lazy object creation has to be done before updatePosition() is called.
5945 if (mStartHandle == null) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005946 mStartHandle = new SelectionHandleView(mSelectHandleLeft, mSelectHandleRight,
5947 com.android.internal.R.id.selection_start_handle,
5948 HANDLE_TYPE_SELECTION_START);
Gilles Debunned88876a2012-03-16 17:34:04 -07005949 }
5950 if (mEndHandle == null) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005951 mEndHandle = new SelectionHandleView(mSelectHandleRight, mSelectHandleLeft,
5952 com.android.internal.R.id.selection_end_handle,
5953 HANDLE_TYPE_SELECTION_END);
Gilles Debunned88876a2012-03-16 17:34:04 -07005954 }
5955
5956 mStartHandle.show();
5957 mEndHandle.show();
5958
Gilles Debunned88876a2012-03-16 17:34:04 -07005959 hideInsertionPointCursorController();
5960 }
5961
Mihai Popa6315a322018-10-17 17:39:57 +01005962 private void reloadHandleDrawables() {
5963 if (mStartHandle == null) {
5964 // No need to reload, the potentially new drawables will
5965 // be used when the handles are created.
5966 return;
5967 }
5968 mStartHandle.setDrawables(mSelectHandleLeft, mSelectHandleRight);
5969 mEndHandle.setDrawables(mSelectHandleRight, mSelectHandleLeft);
5970 }
5971
Gilles Debunned88876a2012-03-16 17:34:04 -07005972 public void hide() {
5973 if (mStartHandle != null) mStartHandle.hide();
5974 if (mEndHandle != null) mEndHandle.hide();
5975 }
5976
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005977 public void enterDrag(int dragAcceleratorMode) {
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08005978 if (TextView.DEBUG_CURSOR) {
5979 logCursor("SelectionModifierCursorController: enterDrag",
5980 "starting selection drag: mode=%s", dragAcceleratorMode);
5981 }
5982
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005983 // Just need to init the handles / hide insertion cursor.
5984 show();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005985 mDragAcceleratorMode = dragAcceleratorMode;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005986 // Start location of selection.
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08005987 mStartOffset = mTextView.getOffsetForPosition(mTouchState.getLastDownX(),
5988 mTouchState.getLastDownY());
5989 mLineSelectionIsOn = mTextView.getLineAtCoordinate(mTouchState.getLastDownY());
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005990 // Don't show the handles until user has lifted finger.
5991 hide();
5992
5993 // This stops scrolling parents from intercepting the touch event, allowing
5994 // the user to continue dragging across the screen to select text; TextView will
5995 // scroll as necessary.
5996 mTextView.getParent().requestDisallowInterceptTouchEvent(true);
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005997 mTextView.cancelLongPress();
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005998 }
5999
Gilles Debunned88876a2012-03-16 17:34:04 -07006000 public void onTouchEvent(MotionEvent event) {
6001 // This is done even when the View does not have focus, so that long presses can start
6002 // selection and tap can move cursor from this tap position.
Mady Mellor7a936442015-05-20 10:05:52 -07006003 final float eventX = event.getX();
6004 final float eventY = event.getY();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006005 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
Gilles Debunned88876a2012-03-16 17:34:04 -07006006 switch (event.getActionMasked()) {
6007 case MotionEvent.ACTION_DOWN:
Andrei Stingaceanu838307272015-06-19 17:58:47 +01006008 if (extractedTextModeWillBeStarted()) {
6009 // Prevent duplicating the selection handles until the mode starts.
6010 hide();
6011 } else {
6012 // Remember finger down position, to be able to start selection from there.
6013 mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(
6014 eventX, eventY);
Gilles Debunned88876a2012-03-16 17:34:04 -07006015
Andrei Stingaceanu838307272015-06-19 17:58:47 +01006016 // Double tap detection
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08006017 if (mGestureStayedInTapRegion
6018 && mTouchState.isMultiTapInSameArea()
6019 && (isMouse || isPositionOnText(eventX, eventY))) {
6020 if (TextView.DEBUG_CURSOR) {
6021 logCursor("SelectionModifierCursorController: onTouchEvent",
6022 "ACTION_DOWN: select and start drag");
Gilles Debunned88876a2012-03-16 17:34:04 -07006023 }
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08006024 if (mTouchState.isDoubleTap()) {
6025 selectCurrentWordAndStartDrag();
6026 } else if (mTouchState.isTripleClick()) {
6027 selectCurrentParagraphAndStartDrag();
6028 }
6029 mDiscardNextActionUp = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07006030 }
Andrei Stingaceanu838307272015-06-19 17:58:47 +01006031 mGestureStayedInTapRegion = true;
6032 mHaventMovedEnoughToStartDrag = true;
6033 }
Gilles Debunned88876a2012-03-16 17:34:04 -07006034 break;
6035
6036 case MotionEvent.ACTION_POINTER_DOWN:
6037 case MotionEvent.ACTION_POINTER_UP:
6038 // Handle multi-point gestures. Keep min and max offset positions.
6039 // Only activated for devices that correctly handle multi-touch.
6040 if (mTextView.getContext().getPackageManager().hasSystemFeature(
6041 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
6042 updateMinAndMaxOffsets(event);
6043 }
6044 break;
6045
6046 case MotionEvent.ACTION_MOVE:
Mady Mellor7a936442015-05-20 10:05:52 -07006047 final ViewConfiguration viewConfig = ViewConfiguration.get(
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006048 mTextView.getContext());
6049
Mady Mellor7a936442015-05-20 10:05:52 -07006050 if (mGestureStayedInTapRegion || mHaventMovedEnoughToStartDrag) {
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08006051 final float deltaX = eventX - mTouchState.getLastDownX();
6052 final float deltaY = eventY - mTouchState.getLastDownY();
Gilles Debunned88876a2012-03-16 17:34:04 -07006053 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
6054
Mady Mellor7a936442015-05-20 10:05:52 -07006055 if (mGestureStayedInTapRegion) {
6056 int doubleTapTouchSlop = viewConfig.getScaledDoubleTapTouchSlop();
6057 mGestureStayedInTapRegion =
6058 distanceSquared <= doubleTapTouchSlop * doubleTapTouchSlop;
6059 }
6060 if (mHaventMovedEnoughToStartDrag) {
6061 // We don't start dragging until the user has moved enough.
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -08006062 int touchSlop = viewConfig.getScaledTouchSlop();
Mady Mellor7a936442015-05-20 10:05:52 -07006063 mHaventMovedEnoughToStartDrag =
6064 distanceSquared <= touchSlop * touchSlop;
Gilles Debunned88876a2012-03-16 17:34:04 -07006065 }
6066 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006067
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006068 if (isMouse && !isDragAcceleratorActive()) {
6069 final int offset = mTextView.getOffsetForPosition(eventX, eventY);
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09006070 if (mTextView.hasSelection()
6071 && (!mHaventMovedEnoughToStartDrag || mStartOffset != offset)
6072 && offset >= mTextView.getSelectionStart()
6073 && offset <= mTextView.getSelectionEnd()) {
6074 startDragAndDrop();
6075 break;
6076 }
6077
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006078 if (mStartOffset != offset) {
6079 // Start character based drag accelerator.
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006080 stopTextActionMode();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006081 enterDrag(DRAG_ACCELERATOR_MODE_CHARACTER);
6082 mDiscardNextActionUp = true;
6083 mHaventMovedEnoughToStartDrag = false;
6084 }
6085 }
6086
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006087 if (mStartHandle != null && mStartHandle.isShowing()) {
6088 // Don't do the drag if the handles are showing already.
6089 break;
6090 }
6091
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006092 updateSelection(event);
Gilles Debunned88876a2012-03-16 17:34:04 -07006093 break;
6094
6095 case MotionEvent.ACTION_UP:
Nikita Dubrovsky05cfcc82019-10-24 08:57:32 -07006096 if (TextView.DEBUG_CURSOR) {
6097 logCursor("SelectionModifierCursorController: onTouchEvent", "ACTION_UP");
6098 }
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006099 if (!isDragAcceleratorActive()) {
6100 break;
6101 }
6102 updateSelection(event);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006103
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006104 // No longer dragging to select text, let the parent intercept events.
6105 mTextView.getParent().requestDisallowInterceptTouchEvent(false);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006106
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006107 // No longer the first dragging motion, reset.
6108 resetDragAcceleratorState();
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09006109
6110 if (mTextView.hasSelection()) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01006111 // Drag selection should not be adjusted by the text classifier.
6112 startSelectionActionModeAsync(mHaventMovedEnoughToStartDrag);
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09006113 }
Gilles Debunned88876a2012-03-16 17:34:04 -07006114 break;
6115 }
6116 }
6117
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006118 private void updateSelection(MotionEvent event) {
6119 if (mTextView.getLayout() != null) {
6120 switch (mDragAcceleratorMode) {
6121 case DRAG_ACCELERATOR_MODE_CHARACTER:
6122 updateCharacterBasedSelection(event);
6123 break;
6124 case DRAG_ACCELERATOR_MODE_WORD:
6125 updateWordBasedSelection(event);
6126 break;
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09006127 case DRAG_ACCELERATOR_MODE_PARAGRAPH:
6128 updateParagraphBasedSelection(event);
6129 break;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006130 }
6131 }
6132 }
6133
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09006134 /**
6135 * If the TextView allows text selection, selects the current paragraph and starts a drag.
6136 *
6137 * @return true if the drag was started.
6138 */
6139 private boolean selectCurrentParagraphAndStartDrag() {
6140 if (mInsertionActionModeRunnable != null) {
6141 mTextView.removeCallbacks(mInsertionActionModeRunnable);
6142 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006143 stopTextActionMode();
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09006144 if (!selectCurrentParagraph()) {
6145 return false;
6146 }
6147 enterDrag(SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_PARAGRAPH);
6148 return true;
6149 }
6150
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006151 private void updateCharacterBasedSelection(MotionEvent event) {
6152 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07006153 updateSelectionInternal(mStartOffset, offset,
6154 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006155 }
6156
6157 private void updateWordBasedSelection(MotionEvent event) {
6158 if (mHaventMovedEnoughToStartDrag) {
6159 return;
6160 }
6161 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
6162 final ViewConfiguration viewConfig = ViewConfiguration.get(
6163 mTextView.getContext());
6164 final float eventX = event.getX();
6165 final float eventY = event.getY();
6166 final int currLine;
6167 if (isMouse) {
6168 // No need to offset the y coordinate for mouse input.
6169 currLine = mTextView.getLineAtCoordinate(eventY);
6170 } else {
6171 float y = eventY;
6172 if (mSwitchedLines) {
6173 // Offset the finger by the same vertical offset as the handles.
6174 // This improves visibility of the content being selected by
6175 // shifting the finger below the content, this is applied once
6176 // the user has switched lines.
6177 final int touchSlop = viewConfig.getScaledTouchSlop();
6178 final float fingerOffset = (mStartHandle != null)
6179 ? mStartHandle.getIdealVerticalOffset()
6180 : touchSlop;
6181 y = eventY - fingerOffset;
6182 }
6183
6184 currLine = getCurrentLineAdjustedForSlop(mTextView.getLayout(), mLineSelectionIsOn,
6185 y);
6186 if (!mSwitchedLines && currLine != mLineSelectionIsOn) {
6187 // Break early here, we want to offset the finger position from
6188 // the selection highlight, once the user moved their finger
6189 // to a different line we should apply the offset and *not* switch
6190 // lines until recomputing the position with the finger offset.
6191 mSwitchedLines = true;
6192 return;
6193 }
6194 }
6195
6196 int startOffset;
6197 int offset = mTextView.getOffsetAtCoordinate(currLine, eventX);
6198 // Snap to word boundaries.
6199 if (mStartOffset < offset) {
6200 // Expanding with end handle.
6201 offset = getWordEnd(offset);
6202 startOffset = getWordStart(mStartOffset);
6203 } else {
6204 // Expanding with start handle.
6205 offset = getWordStart(offset);
6206 startOffset = getWordEnd(mStartOffset);
Keisuke Kuroyanagi133dfc02016-07-21 18:07:23 +09006207 if (startOffset == offset) {
6208 offset = getNextCursorOffset(offset, false);
6209 }
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006210 }
6211 mLineSelectionIsOn = currLine;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07006212 updateSelectionInternal(startOffset, offset,
6213 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006214 }
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09006215
6216 private void updateParagraphBasedSelection(MotionEvent event) {
6217 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
6218
6219 final int start = Math.min(offset, mStartOffset);
6220 final int end = Math.max(offset, mStartOffset);
6221 final long paragraphsRange = getParagraphsRange(start, end);
6222 final int selectionStart = TextUtils.unpackRangeStartFromLong(paragraphsRange);
6223 final int selectionEnd = TextUtils.unpackRangeEndFromLong(paragraphsRange);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07006224 updateSelectionInternal(selectionStart, selectionEnd,
6225 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
6226 }
6227
6228 private void updateSelectionInternal(int selectionStart, int selectionEnd,
6229 boolean fromTouchScreen) {
6230 final boolean performHapticFeedback = fromTouchScreen && mHapticTextHandleEnabled
6231 && ((mTextView.getSelectionStart() != selectionStart)
6232 || (mTextView.getSelectionEnd() != selectionEnd));
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09006233 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07006234 if (performHapticFeedback) {
6235 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
6236 }
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09006237 }
6238
Gilles Debunned88876a2012-03-16 17:34:04 -07006239 /**
6240 * @param event
6241 */
6242 private void updateMinAndMaxOffsets(MotionEvent event) {
6243 int pointerCount = event.getPointerCount();
6244 for (int index = 0; index < pointerCount; index++) {
6245 int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
6246 if (offset < mMinTouchOffset) mMinTouchOffset = offset;
6247 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
6248 }
6249 }
6250
6251 public int getMinTouchOffset() {
6252 return mMinTouchOffset;
6253 }
6254
6255 public int getMaxTouchOffset() {
6256 return mMaxTouchOffset;
6257 }
6258
6259 public void resetTouchOffsets() {
6260 mMinTouchOffset = mMaxTouchOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006261 resetDragAcceleratorState();
6262 }
6263
6264 private void resetDragAcceleratorState() {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006265 mStartOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006266 mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
Mady Mellorf9f8aeb2015-06-17 09:46:01 -07006267 mSwitchedLines = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006268 final int selectionStart = mTextView.getSelectionStart();
6269 final int selectionEnd = mTextView.getSelectionEnd();
Clara Bayarri4e518772018-03-27 14:25:33 +01006270 if (selectionStart < 0 || selectionEnd < 0) {
6271 Selection.removeSelection((Spannable) mTextView.getText());
6272 } else if (selectionStart > selectionEnd) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006273 Selection.setSelection((Spannable) mTextView.getText(),
6274 selectionEnd, selectionStart);
6275 }
Gilles Debunned88876a2012-03-16 17:34:04 -07006276 }
6277
6278 /**
6279 * @return true iff this controller is currently used to move the selection start.
6280 */
6281 public boolean isSelectionStartDragged() {
6282 return mStartHandle != null && mStartHandle.isDragging();
6283 }
6284
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006285 @Override
6286 public boolean isCursorBeingModified() {
6287 return isDragAcceleratorActive() || isSelectionStartDragged()
6288 || (mEndHandle != null && mEndHandle.isDragging());
6289 }
6290
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006291 /**
6292 * @return true if the user is selecting text using the drag accelerator.
6293 */
6294 public boolean isDragAcceleratorActive() {
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006295 return mDragAcceleratorMode != DRAG_ACCELERATOR_MODE_INACTIVE;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006296 }
6297
Gilles Debunned88876a2012-03-16 17:34:04 -07006298 public void onTouchModeChanged(boolean isInTouchMode) {
6299 if (!isInTouchMode) {
6300 hide();
6301 }
6302 }
6303
6304 @Override
6305 public void onDetached() {
6306 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
6307 observer.removeOnTouchModeChangeListener(this);
6308
6309 if (mStartHandle != null) mStartHandle.onDetached();
6310 if (mEndHandle != null) mEndHandle.onDetached();
6311 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006312
6313 @Override
6314 public boolean isActive() {
6315 return mStartHandle != null && mStartHandle.isShowing();
6316 }
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09006317
6318 public void invalidateHandles() {
6319 if (mStartHandle != null) {
6320 mStartHandle.invalidate();
6321 }
6322 if (mEndHandle != null) {
6323 mEndHandle.invalidate();
6324 }
6325 }
Gilles Debunned88876a2012-03-16 17:34:04 -07006326 }
6327
Mihai Popa6315a322018-10-17 17:39:57 +01006328 /**
6329 * Loads the insertion and selection handle Drawables from TextView. If the handle
6330 * drawables are already loaded, do not overwrite them unless the method parameter
6331 * is set to true. This logic is required to avoid overwriting Drawables assigned
6332 * to mSelectHandle[Center/Left/Right] by developers using reflection, unless they
6333 * explicitly call the setters in TextView.
6334 *
6335 * @param overwrite whether to overwrite already existing nonnull Drawables
6336 */
6337 void loadHandleDrawables(final boolean overwrite) {
6338 if (mSelectHandleCenter == null || overwrite) {
6339 mSelectHandleCenter = mTextView.getTextSelectHandle();
6340 if (hasInsertionController()) {
6341 getInsertionController().reloadHandleDrawable();
6342 }
6343 }
6344
6345 if (mSelectHandleLeft == null || mSelectHandleRight == null || overwrite) {
6346 mSelectHandleLeft = mTextView.getTextSelectHandleLeft();
6347 mSelectHandleRight = mTextView.getTextSelectHandleRight();
6348 if (hasSelectionController()) {
6349 getSelectionController().reloadHandleDrawables();
6350 }
6351 }
6352 }
6353
Gilles Debunned88876a2012-03-16 17:34:04 -07006354 private class CorrectionHighlighter {
6355 private final Path mPath = new Path();
Chris Craik6a49dde2015-05-12 10:28:14 -07006356 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
Gilles Debunned88876a2012-03-16 17:34:04 -07006357 private int mStart, mEnd;
6358 private long mFadingStartTime;
6359 private RectF mTempRectF;
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006360 private static final int FADE_OUT_DURATION = 400;
Gilles Debunned88876a2012-03-16 17:34:04 -07006361
6362 public CorrectionHighlighter() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006363 mPaint.setCompatibilityScaling(
6364 mTextView.getResources().getCompatibilityInfo().applicationScale);
Gilles Debunned88876a2012-03-16 17:34:04 -07006365 mPaint.setStyle(Paint.Style.FILL);
6366 }
6367
6368 public void highlight(CorrectionInfo info) {
6369 mStart = info.getOffset();
6370 mEnd = mStart + info.getNewText().length();
6371 mFadingStartTime = SystemClock.uptimeMillis();
6372
6373 if (mStart < 0 || mEnd < 0) {
6374 stopAnimation();
6375 }
6376 }
6377
6378 public void draw(Canvas canvas, int cursorOffsetVertical) {
6379 if (updatePath() && updatePaint()) {
6380 if (cursorOffsetVertical != 0) {
6381 canvas.translate(0, cursorOffsetVertical);
6382 }
6383
6384 canvas.drawPath(mPath, mPaint);
6385
6386 if (cursorOffsetVertical != 0) {
6387 canvas.translate(0, -cursorOffsetVertical);
6388 }
6389 invalidate(true); // TODO invalidate cursor region only
6390 } else {
6391 stopAnimation();
6392 invalidate(false); // TODO invalidate cursor region only
6393 }
6394 }
6395
6396 private boolean updatePaint() {
6397 final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
6398 if (duration > FADE_OUT_DURATION) return false;
6399
6400 final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
6401 final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006402 final int color = (mTextView.mHighlightColor & 0x00FFFFFF)
6403 + ((int) (highlightColorAlpha * coef) << 24);
Gilles Debunned88876a2012-03-16 17:34:04 -07006404 mPaint.setColor(color);
6405 return true;
6406 }
6407
6408 private boolean updatePath() {
6409 final Layout layout = mTextView.getLayout();
6410 if (layout == null) return false;
6411
6412 // Update in case text is edited while the animation is run
6413 final int length = mTextView.getText().length();
6414 int start = Math.min(length, mStart);
6415 int end = Math.min(length, mEnd);
6416
6417 mPath.reset();
6418 layout.getSelectionPath(start, end, mPath);
6419 return true;
6420 }
6421
6422 private void invalidate(boolean delayed) {
6423 if (mTextView.getLayout() == null) return;
6424
6425 if (mTempRectF == null) mTempRectF = new RectF();
6426 mPath.computeBounds(mTempRectF, false);
6427
6428 int left = mTextView.getCompoundPaddingLeft();
6429 int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
6430
6431 if (delayed) {
6432 mTextView.postInvalidateOnAnimation(
6433 left + (int) mTempRectF.left, top + (int) mTempRectF.top,
6434 left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
6435 } else {
6436 mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
6437 (int) mTempRectF.right, (int) mTempRectF.bottom);
6438 }
6439 }
6440
6441 private void stopAnimation() {
6442 Editor.this.mCorrectionHighlighter = null;
6443 }
6444 }
6445
6446 private static class ErrorPopup extends PopupWindow {
6447 private boolean mAbove = false;
6448 private final TextView mView;
6449 private int mPopupInlineErrorBackgroundId = 0;
6450 private int mPopupInlineErrorAboveBackgroundId = 0;
6451
6452 ErrorPopup(TextView v, int width, int height) {
6453 super(v, width, height);
6454 mView = v;
6455 // 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 -08006456 // shown and positioned. Initialized with below background, which should have
Gilles Debunned88876a2012-03-16 17:34:04 -07006457 // dimensions identical to the above version for this to work (and is more likely).
6458 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
6459 com.android.internal.R.styleable.Theme_errorMessageBackground);
6460 mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
6461 }
6462
6463 void fixDirection(boolean above) {
6464 mAbove = above;
6465
6466 if (above) {
6467 mPopupInlineErrorAboveBackgroundId =
6468 getResourceId(mPopupInlineErrorAboveBackgroundId,
6469 com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
6470 } else {
6471 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
6472 com.android.internal.R.styleable.Theme_errorMessageBackground);
6473 }
6474
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006475 mView.setBackgroundResource(
6476 above ? mPopupInlineErrorAboveBackgroundId : mPopupInlineErrorBackgroundId);
Gilles Debunned88876a2012-03-16 17:34:04 -07006477 }
6478
6479 private int getResourceId(int currentId, int index) {
6480 if (currentId == 0) {
6481 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
6482 R.styleable.Theme);
6483 currentId = styledAttributes.getResourceId(index, 0);
6484 styledAttributes.recycle();
6485 }
6486 return currentId;
6487 }
6488
6489 @Override
6490 public void update(int x, int y, int w, int h, boolean force) {
6491 super.update(x, y, w, h, force);
6492
6493 boolean above = isAboveAnchor();
6494 if (above != mAbove) {
6495 fixDirection(above);
6496 }
6497 }
6498 }
6499
6500 static class InputContentType {
6501 int imeOptions = EditorInfo.IME_NULL;
Mathew Inwood978c6e22018-08-21 15:58:55 +01006502 @UnsupportedAppUsage
Gilles Debunned88876a2012-03-16 17:34:04 -07006503 String privateImeOptions;
6504 CharSequence imeActionLabel;
6505 int imeActionId;
6506 Bundle extras;
6507 OnEditorActionListener onEditorActionListener;
6508 boolean enterDown;
Yohei Yukawad469f212016-01-21 12:38:09 -08006509 LocaleList imeHintLocales;
Gilles Debunned88876a2012-03-16 17:34:04 -07006510 }
6511
6512 static class InputMethodState {
Gilles Debunnec62589c2012-04-12 14:50:23 -07006513 ExtractedTextRequest mExtractedTextRequest;
6514 final ExtractedText mExtractedText = new ExtractedText();
Gilles Debunned88876a2012-03-16 17:34:04 -07006515 int mBatchEditNesting;
6516 boolean mCursorChanged;
6517 boolean mSelectionModeChanged;
6518 boolean mContentChanged;
6519 int mChangedStart, mChangedEnd, mChangedDelta;
6520 }
Satoshi Kataoka0e3849a2012-12-13 14:37:19 +09006521
James Cookf59152c2015-02-26 18:03:58 -08006522 /**
James Cook471559f2015-02-27 10:31:20 -08006523 * @return True iff (start, end) is a valid range within the text.
6524 */
6525 private static boolean isValidRange(CharSequence text, int start, int end) {
6526 return 0 <= start && start <= end && end <= text.length();
6527 }
6528
6529 /**
James Cookf59152c2015-02-26 18:03:58 -08006530 * An InputFilter that monitors text input to maintain undo history. It does not modify the
6531 * text being typed (and hence always returns null from the filter() method).
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006532 *
6533 * TODO: Make this span aware.
James Cookf59152c2015-02-26 18:03:58 -08006534 */
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006535 public static class UndoInputFilter implements InputFilter {
James Cookf59152c2015-02-26 18:03:58 -08006536 private final Editor mEditor;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006537
James Cook48e0fac2015-02-25 15:44:51 -08006538 // Whether the current filter pass is directly caused by an end-user text edit.
6539 private boolean mIsUserEdit;
6540
James Cookd2026682015-03-03 14:40:14 -08006541 // Whether the text field is handling an IME composition. Must be parceled in case the user
6542 // rotates the screen during composition.
6543 private boolean mHasComposition;
James Cook48e0fac2015-02-25 15:44:51 -08006544
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006545 // Whether the user is expanding or shortening the text
6546 private boolean mExpanding;
6547
6548 // Whether the previous edit operation was in the current batch edit.
6549 private boolean mPreviousOperationWasInSameBatchEdit;
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08006550
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006551 public UndoInputFilter(Editor editor) {
6552 mEditor = editor;
6553 }
6554
James Cookd2026682015-03-03 14:40:14 -08006555 public void saveInstanceState(Parcel parcel) {
6556 parcel.writeInt(mIsUserEdit ? 1 : 0);
6557 parcel.writeInt(mHasComposition ? 1 : 0);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006558 parcel.writeInt(mExpanding ? 1 : 0);
6559 parcel.writeInt(mPreviousOperationWasInSameBatchEdit ? 1 : 0);
James Cookd2026682015-03-03 14:40:14 -08006560 }
6561
6562 public void restoreInstanceState(Parcel parcel) {
6563 mIsUserEdit = parcel.readInt() != 0;
6564 mHasComposition = parcel.readInt() != 0;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006565 mExpanding = parcel.readInt() != 0;
6566 mPreviousOperationWasInSameBatchEdit = parcel.readInt() != 0;
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08006567 }
6568
James Cook48e0fac2015-02-25 15:44:51 -08006569 /**
6570 * Signals that a user-triggered edit is starting.
6571 */
6572 public void beginBatchEdit() {
6573 if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit");
6574 mIsUserEdit = true;
James Cook48e0fac2015-02-25 15:44:51 -08006575 }
6576
6577 public void endBatchEdit() {
6578 if (DEBUG_UNDO) Log.d(TAG, "endBatchEdit");
6579 mIsUserEdit = false;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006580 mPreviousOperationWasInSameBatchEdit = false;
James Cook48e0fac2015-02-25 15:44:51 -08006581 }
6582
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006583 @Override
6584 public CharSequence filter(CharSequence source, int start, int end,
6585 Spanned dest, int dstart, int dend) {
6586 if (DEBUG_UNDO) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006587 Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ") "
6588 + "dest=" + dest + " (" + dstart + "-" + dend + ")");
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006589 }
James Cookf1dad1e2015-02-27 11:00:01 -08006590
James Cook48e0fac2015-02-25 15:44:51 -08006591 // Check to see if this edit should be tracked for undo.
6592 if (!canUndoEdit(source, start, end, dest, dstart, dend)) {
James Cookf1dad1e2015-02-27 11:00:01 -08006593 return null;
6594 }
6595
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006596 final boolean hadComposition = mHasComposition;
6597 mHasComposition = isComposition(source);
6598 final boolean wasExpanding = mExpanding;
6599 boolean shouldCreateSeparateState = false;
6600 if ((end - start) != (dend - dstart)) {
6601 mExpanding = (end - start) > (dend - dstart);
6602 if (hadComposition && mExpanding != wasExpanding) {
6603 shouldCreateSeparateState = true;
6604 }
James Cookd2026682015-03-03 14:40:14 -08006605 }
6606
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006607 // Handle edit.
6608 handleEdit(source, start, end, dest, dstart, dend, shouldCreateSeparateState);
James Cookd2026682015-03-03 14:40:14 -08006609 return null;
6610 }
6611
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09006612 void freezeLastEdit() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006613 mEditor.mUndoManager.beginUpdate("Edit text");
6614 EditOperation lastEdit = getLastEdit();
6615 if (lastEdit != null) {
6616 lastEdit.mFrozen = true;
James Cookd2026682015-03-03 14:40:14 -08006617 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006618 mEditor.mUndoManager.endUpdate();
James Cookd2026682015-03-03 14:40:14 -08006619 }
6620
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006621 @Retention(RetentionPolicy.SOURCE)
Jeff Sharkeyce8db992017-12-13 20:05:05 -07006622 @IntDef(prefix = { "MERGE_EDIT_MODE_" }, value = {
6623 MERGE_EDIT_MODE_FORCE_MERGE,
6624 MERGE_EDIT_MODE_NEVER_MERGE,
6625 MERGE_EDIT_MODE_NORMAL
6626 })
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006627 private @interface MergeMode {}
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006628 private static final int MERGE_EDIT_MODE_FORCE_MERGE = 0;
6629 private static final int MERGE_EDIT_MODE_NEVER_MERGE = 1;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006630 /** Use {@link EditOperation#mergeWith} to merge */
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006631 private static final int MERGE_EDIT_MODE_NORMAL = 2;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006632
6633 private void handleEdit(CharSequence source, int start, int end,
6634 Spanned dest, int dstart, int dend, boolean shouldCreateSeparateState) {
James Cook48e0fac2015-02-25 15:44:51 -08006635 // An application may install a TextWatcher to provide additional modifications after
6636 // the initial input filters run (e.g. a credit card formatter that adds spaces to a
6637 // string). This results in multiple filter() calls for what the user considers to be
6638 // a single operation. Always undo the whole set of changes in one step.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006639 @MergeMode
6640 final int mergeMode;
6641 if (isInTextWatcher() || mPreviousOperationWasInSameBatchEdit) {
6642 mergeMode = MERGE_EDIT_MODE_FORCE_MERGE;
6643 } else if (shouldCreateSeparateState) {
6644 mergeMode = MERGE_EDIT_MODE_NEVER_MERGE;
6645 } else {
6646 mergeMode = MERGE_EDIT_MODE_NORMAL;
6647 }
James Cook471559f2015-02-27 10:31:20 -08006648 // Build a new operation with all the information from this edit.
James Cookd2026682015-03-03 14:40:14 -08006649 String newText = TextUtils.substring(source, start, end);
6650 String oldText = TextUtils.substring(dest, dstart, dend);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006651 EditOperation edit = new EditOperation(mEditor, oldText, dstart, newText,
6652 mHasComposition);
6653 if (mHasComposition && TextUtils.equals(edit.mNewText, edit.mOldText)) {
6654 return;
6655 }
6656 recordEdit(edit, mergeMode);
James Cookd2026682015-03-03 14:40:14 -08006657 }
James Cook471559f2015-02-27 10:31:20 -08006658
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006659 private EditOperation getLastEdit() {
6660 final UndoManager um = mEditor.mUndoManager;
6661 return um.getLastOperation(
6662 EditOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE);
6663 }
James Cook22054252015-03-25 14:04:01 -07006664 /**
6665 * Fetches the last undo operation and checks to see if a new edit should be merged into it.
6666 * If forceMerge is true then the new edit is always merged.
6667 */
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006668 private void recordEdit(EditOperation edit, @MergeMode int mergeMode) {
James Cook471559f2015-02-27 10:31:20 -08006669 // Fetch the last edit operation and attempt to merge in the new edit.
James Cook48e0fac2015-02-25 15:44:51 -08006670 final UndoManager um = mEditor.mUndoManager;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006671 um.beginUpdate("Edit text");
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006672 EditOperation lastEdit = getLastEdit();
James Cook471559f2015-02-27 10:31:20 -08006673 if (lastEdit == null) {
6674 // Add this as the first edit.
6675 if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit);
6676 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006677 } else if (mergeMode == MERGE_EDIT_MODE_FORCE_MERGE) {
James Cook22054252015-03-25 14:04:01 -07006678 // Forced merges take priority because they could be the result of a non-user-edit
6679 // change and this case should not create a new undo operation.
6680 if (DEBUG_UNDO) Log.d(TAG, "filter: force merge " + edit);
6681 lastEdit.forceMergeWith(edit);
James Cook48e0fac2015-02-25 15:44:51 -08006682 } else if (!mIsUserEdit) {
6683 // An application directly modified the Editable outside of a text edit. Treat this
6684 // as a new change and don't attempt to merge.
6685 if (DEBUG_UNDO) Log.d(TAG, "non-user edit, new op " + edit);
6686 um.commitState(mEditor.mUndoOwner);
6687 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006688 } else if (mergeMode == MERGE_EDIT_MODE_NORMAL && lastEdit.mergeWith(edit)) {
James Cook471559f2015-02-27 10:31:20 -08006689 // Merge succeeded, nothing else to do.
6690 if (DEBUG_UNDO) Log.d(TAG, "filter: merge succeeded, created " + lastEdit);
James Cook3ac0bcb2015-02-26 10:53:41 -08006691 } else {
James Cook471559f2015-02-27 10:31:20 -08006692 // Could not merge with the last edit, so commit the last edit and add this edit.
6693 if (DEBUG_UNDO) Log.d(TAG, "filter: merge failed, adding " + edit);
6694 um.commitState(mEditor.mUndoOwner);
6695 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
James Cook3ac0bcb2015-02-26 10:53:41 -08006696 }
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09006697 mPreviousOperationWasInSameBatchEdit = mIsUserEdit;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006698 um.endUpdate();
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006699 }
James Cook48e0fac2015-02-25 15:44:51 -08006700
6701 private boolean canUndoEdit(CharSequence source, int start, int end,
6702 Spanned dest, int dstart, int dend) {
6703 if (!mEditor.mAllowUndo) {
6704 if (DEBUG_UNDO) Log.d(TAG, "filter: undo is disabled");
6705 return false;
6706 }
6707
6708 if (mEditor.mUndoManager.isInUndo()) {
6709 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping, currently performing undo/redo");
6710 return false;
6711 }
6712
6713 // Text filters run before input operations are applied. However, some input operations
6714 // are invalid and will throw exceptions when applied. This is common in tests. Don't
6715 // attempt to undo invalid operations.
6716 if (!isValidRange(source, start, end) || !isValidRange(dest, dstart, dend)) {
6717 if (DEBUG_UNDO) Log.d(TAG, "filter: invalid op");
6718 return false;
6719 }
6720
6721 // Earlier filters can rewrite input to be a no-op, for example due to a length limit
6722 // on an input field. Skip no-op changes.
6723 if (start == end && dstart == dend) {
6724 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping no-op");
6725 return false;
6726 }
6727
6728 return true;
6729 }
James Cookd2026682015-03-03 14:40:14 -08006730
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006731 private static boolean isComposition(CharSequence source) {
James Cookd2026682015-03-03 14:40:14 -08006732 if (!(source instanceof Spannable)) {
6733 return false;
6734 }
6735 // This is a composition edit if the source has a non-zero-length composing span.
6736 Spannable text = (Spannable) source;
6737 int composeBegin = EditableInputConnection.getComposingSpanStart(text);
6738 int composeEnd = EditableInputConnection.getComposingSpanEnd(text);
6739 return composeBegin < composeEnd;
6740 }
6741
6742 private boolean isInTextWatcher() {
6743 CharSequence text = mEditor.mTextView.getText();
6744 return (text instanceof SpannableStringBuilder)
6745 && ((SpannableStringBuilder) text).getTextWatcherDepth() > 0;
6746 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006747 }
6748
James Cookf59152c2015-02-26 18:03:58 -08006749 /**
6750 * An operation to undo a single "edit" to a text view.
6751 */
James Cook471559f2015-02-27 10:31:20 -08006752 public static class EditOperation extends UndoOperation<Editor> {
6753 private static final int TYPE_INSERT = 0;
6754 private static final int TYPE_DELETE = 1;
6755 private static final int TYPE_REPLACE = 2;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006756
James Cook471559f2015-02-27 10:31:20 -08006757 private int mType;
6758 private String mOldText;
James Cook471559f2015-02-27 10:31:20 -08006759 private String mNewText;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006760 private int mStart;
James Cook471559f2015-02-27 10:31:20 -08006761
6762 private int mOldCursorPos;
6763 private int mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006764 private boolean mFrozen;
6765 private boolean mIsComposition;
James Cook471559f2015-02-27 10:31:20 -08006766
6767 /**
James Cookd2026682015-03-03 14:40:14 -08006768 * Constructs an edit operation from a text input operation on editor that replaces the
James Cook22054252015-03-25 14:04:01 -07006769 * oldText starting at dstart with newText.
James Cook471559f2015-02-27 10:31:20 -08006770 */
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006771 public EditOperation(Editor editor, String oldText, int dstart, String newText,
6772 boolean isComposition) {
James Cook471559f2015-02-27 10:31:20 -08006773 super(editor.mUndoOwner);
James Cookd2026682015-03-03 14:40:14 -08006774 mOldText = oldText;
6775 mNewText = newText;
James Cook471559f2015-02-27 10:31:20 -08006776
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006777 // Determine the type of the edit.
James Cook471559f2015-02-27 10:31:20 -08006778 if (mNewText.length() > 0 && mOldText.length() == 0) {
6779 mType = TYPE_INSERT;
James Cook471559f2015-02-27 10:31:20 -08006780 } else if (mNewText.length() == 0 && mOldText.length() > 0) {
6781 mType = TYPE_DELETE;
James Cook471559f2015-02-27 10:31:20 -08006782 } else {
6783 mType = TYPE_REPLACE;
James Cook471559f2015-02-27 10:31:20 -08006784 }
6785
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006786 mStart = dstart;
James Cook471559f2015-02-27 10:31:20 -08006787 // Store cursor data.
6788 mOldCursorPos = editor.mTextView.getSelectionStart();
James Cookd2026682015-03-03 14:40:14 -08006789 mNewCursorPos = dstart + mNewText.length();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006790 mIsComposition = isComposition;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006791 }
6792
James Cook471559f2015-02-27 10:31:20 -08006793 public EditOperation(Parcel src, ClassLoader loader) {
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006794 super(src, loader);
James Cook471559f2015-02-27 10:31:20 -08006795 mType = src.readInt();
6796 mOldText = src.readString();
James Cook471559f2015-02-27 10:31:20 -08006797 mNewText = src.readString();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006798 mStart = src.readInt();
James Cook471559f2015-02-27 10:31:20 -08006799 mOldCursorPos = src.readInt();
6800 mNewCursorPos = src.readInt();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006801 mFrozen = src.readInt() == 1;
6802 mIsComposition = src.readInt() == 1;
James Cook471559f2015-02-27 10:31:20 -08006803 }
6804
6805 @Override
6806 public void writeToParcel(Parcel dest, int flags) {
6807 dest.writeInt(mType);
6808 dest.writeString(mOldText);
James Cook471559f2015-02-27 10:31:20 -08006809 dest.writeString(mNewText);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006810 dest.writeInt(mStart);
James Cook471559f2015-02-27 10:31:20 -08006811 dest.writeInt(mOldCursorPos);
6812 dest.writeInt(mNewCursorPos);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006813 dest.writeInt(mFrozen ? 1 : 0);
6814 dest.writeInt(mIsComposition ? 1 : 0);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006815 }
6816
James Cook48e0fac2015-02-25 15:44:51 -08006817 private int getNewTextEnd() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006818 return mStart + mNewText.length();
James Cook48e0fac2015-02-25 15:44:51 -08006819 }
6820
6821 private int getOldTextEnd() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006822 return mStart + mOldText.length();
James Cook48e0fac2015-02-25 15:44:51 -08006823 }
6824
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006825 @Override
6826 public void commit() {
6827 }
6828
6829 @Override
6830 public void undo() {
James Cook471559f2015-02-27 10:31:20 -08006831 if (DEBUG_UNDO) Log.d(TAG, "undo");
6832 // Remove the new text and insert the old.
James Cook48e0fac2015-02-25 15:44:51 -08006833 Editor editor = getOwnerData();
6834 Editable text = (Editable) editor.mTextView.getText();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006835 modifyText(text, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006836 }
6837
6838 @Override
6839 public void redo() {
James Cook471559f2015-02-27 10:31:20 -08006840 if (DEBUG_UNDO) Log.d(TAG, "redo");
6841 // Remove the old text and insert the new.
James Cook48e0fac2015-02-25 15:44:51 -08006842 Editor editor = getOwnerData();
6843 Editable text = (Editable) editor.mTextView.getText();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006844 modifyText(text, mStart, getOldTextEnd(), mNewText, mStart, mNewCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006845 }
6846
James Cook471559f2015-02-27 10:31:20 -08006847 /**
6848 * Attempts to merge this existing operation with a new edit.
6849 * @param edit The new edit operation.
6850 * @return If the merge succeeded, returns true. Otherwise returns false and leaves this
6851 * object unchanged.
6852 */
6853 private boolean mergeWith(EditOperation edit) {
James Cook48e0fac2015-02-25 15:44:51 -08006854 if (DEBUG_UNDO) {
6855 Log.d(TAG, "mergeWith old " + this);
6856 Log.d(TAG, "mergeWith new " + edit);
6857 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006858
6859 if (mFrozen) {
6860 return false;
6861 }
6862
James Cook471559f2015-02-27 10:31:20 -08006863 switch (mType) {
6864 case TYPE_INSERT:
6865 return mergeInsertWith(edit);
6866 case TYPE_DELETE:
6867 return mergeDeleteWith(edit);
6868 case TYPE_REPLACE:
6869 return mergeReplaceWith(edit);
6870 default:
6871 return false;
6872 }
6873 }
6874
6875 private boolean mergeInsertWith(EditOperation edit) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006876 if (edit.mType == TYPE_INSERT) {
6877 // Merge insertions that are contiguous even when it's frozen.
6878 if (getNewTextEnd() != edit.mStart) {
6879 return false;
6880 }
6881 mNewText += edit.mNewText;
6882 mNewCursorPos = edit.mNewCursorPos;
6883 mFrozen = edit.mFrozen;
6884 mIsComposition = edit.mIsComposition;
6885 return true;
James Cook471559f2015-02-27 10:31:20 -08006886 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006887 if (mIsComposition && edit.mType == TYPE_REPLACE
6888 && mStart <= edit.mStart && getNewTextEnd() >= edit.getOldTextEnd()) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006889 // Merge insertion with replace as they can be single insertion.
6890 mNewText = mNewText.substring(0, edit.mStart - mStart) + edit.mNewText
6891 + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
6892 mNewCursorPos = edit.mNewCursorPos;
6893 mIsComposition = edit.mIsComposition;
6894 return true;
James Cook471559f2015-02-27 10:31:20 -08006895 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006896 return false;
James Cook471559f2015-02-27 10:31:20 -08006897 }
6898
6899 // TODO: Support forward delete.
6900 private boolean mergeDeleteWith(EditOperation edit) {
James Cook471559f2015-02-27 10:31:20 -08006901 // Only merge continuous deletes.
6902 if (edit.mType != TYPE_DELETE) {
6903 return false;
6904 }
6905 // Only merge deletions that are contiguous.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006906 if (mStart != edit.getOldTextEnd()) {
James Cook471559f2015-02-27 10:31:20 -08006907 return false;
6908 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006909 mStart = edit.mStart;
James Cook471559f2015-02-27 10:31:20 -08006910 mOldText = edit.mOldText + mOldText;
6911 mNewCursorPos = edit.mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006912 mIsComposition = edit.mIsComposition;
James Cook471559f2015-02-27 10:31:20 -08006913 return true;
6914 }
6915
6916 private boolean mergeReplaceWith(EditOperation edit) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006917 if (edit.mType == TYPE_INSERT && getNewTextEnd() == edit.mStart) {
6918 // Merge with adjacent insert.
6919 mNewText += edit.mNewText;
6920 mNewCursorPos = edit.mNewCursorPos;
6921 return true;
6922 }
6923 if (!mIsComposition) {
James Cook471559f2015-02-27 10:31:20 -08006924 return false;
6925 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006926 if (edit.mType == TYPE_DELETE && mStart <= edit.mStart
6927 && getNewTextEnd() >= edit.getOldTextEnd()) {
6928 // Merge with delete as they can be single operation.
6929 mNewText = mNewText.substring(0, edit.mStart - mStart)
6930 + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
6931 if (mNewText.isEmpty()) {
6932 mType = TYPE_DELETE;
6933 }
6934 mNewCursorPos = edit.mNewCursorPos;
6935 mIsComposition = edit.mIsComposition;
6936 return true;
6937 }
6938 if (edit.mType == TYPE_REPLACE && mStart == edit.mStart
6939 && TextUtils.equals(mNewText, edit.mOldText)) {
6940 // Merge with the replace that replaces the same region.
6941 mNewText = edit.mNewText;
6942 mNewCursorPos = edit.mNewCursorPos;
6943 mIsComposition = edit.mIsComposition;
6944 return true;
6945 }
6946 return false;
James Cook471559f2015-02-27 10:31:20 -08006947 }
6948
James Cook48e0fac2015-02-25 15:44:51 -08006949 /**
6950 * Forcibly creates a single merged edit operation by simulating the entire text
6951 * contents being replaced.
6952 */
James Cook22054252015-03-25 14:04:01 -07006953 public void forceMergeWith(EditOperation edit) {
James Cook48e0fac2015-02-25 15:44:51 -08006954 if (DEBUG_UNDO) Log.d(TAG, "forceMerge");
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006955 if (mergeWith(edit)) {
6956 return;
6957 }
James Cookf59152c2015-02-26 18:03:58 -08006958 Editor editor = getOwnerData();
James Cook48e0fac2015-02-25 15:44:51 -08006959
6960 // Copy the text of the current field.
6961 // NOTE: Using StringBuilder instead of SpannableStringBuilder would be somewhat faster,
6962 // but would require two parallel implementations of modifyText() because Editable and
6963 // StringBuilder do not share an interface for replace/delete/insert.
6964 Editable editable = (Editable) editor.mTextView.getText();
6965 Editable originalText = new SpannableStringBuilder(editable.toString());
6966
6967 // Roll back the last operation.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006968 modifyText(originalText, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
James Cook48e0fac2015-02-25 15:44:51 -08006969
6970 // Clone the text again and apply the new operation.
6971 Editable finalText = new SpannableStringBuilder(editable.toString());
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006972 modifyText(finalText, edit.mStart, edit.getOldTextEnd(),
6973 edit.mNewText, edit.mStart, edit.mNewCursorPos);
James Cook48e0fac2015-02-25 15:44:51 -08006974
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006975 // Convert this operation into a replace operation.
James Cook48e0fac2015-02-25 15:44:51 -08006976 mType = TYPE_REPLACE;
6977 mNewText = finalText.toString();
James Cook48e0fac2015-02-25 15:44:51 -08006978 mOldText = originalText.toString();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006979 mStart = 0;
James Cook48e0fac2015-02-25 15:44:51 -08006980 mNewCursorPos = edit.mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006981 mIsComposition = edit.mIsComposition;
James Cook48e0fac2015-02-25 15:44:51 -08006982 // mOldCursorPos is unchanged.
6983 }
6984
6985 private static void modifyText(Editable text, int deleteFrom, int deleteTo,
6986 CharSequence newText, int newTextInsertAt, int newCursorPos) {
James Cook471559f2015-02-27 10:31:20 -08006987 // Apply the edit if it is still valid.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006988 if (isValidRange(text, deleteFrom, deleteTo)
6989 && newTextInsertAt <= text.length() - (deleteTo - deleteFrom)) {
James Cook471559f2015-02-27 10:31:20 -08006990 if (deleteFrom != deleteTo) {
6991 text.delete(deleteFrom, deleteTo);
6992 }
6993 if (newText.length() != 0) {
6994 text.insert(newTextInsertAt, newText);
6995 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006996 }
James Cook900185d2015-03-10 09:48:11 -07006997 // Restore the cursor position. If there wasn't an old cursor (newCursorPos == -1) then
6998 // don't explicitly set it and rely on SpannableStringBuilder to position it.
James Cook471559f2015-02-27 10:31:20 -08006999 // TODO: Select all the text that was undone.
James Cook900185d2015-03-10 09:48:11 -07007000 if (0 <= newCursorPos && newCursorPos <= text.length()) {
James Cook471559f2015-02-27 10:31:20 -08007001 Selection.setSelection(text, newCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007002 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007003 }
7004
James Cook48e0fac2015-02-25 15:44:51 -08007005 private String getTypeString() {
7006 switch (mType) {
7007 case TYPE_INSERT:
7008 return "insert";
7009 case TYPE_DELETE:
7010 return "delete";
7011 case TYPE_REPLACE:
7012 return "replace";
7013 default:
7014 return "";
7015 }
7016 }
7017
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007018 @Override
James Cook471559f2015-02-27 10:31:20 -08007019 public String toString() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007020 return "[mType=" + getTypeString() + ", "
7021 + "mOldText=" + mOldText + ", "
7022 + "mNewText=" + mNewText + ", "
7023 + "mStart=" + mStart + ", "
7024 + "mOldCursorPos=" + mOldCursorPos + ", "
7025 + "mNewCursorPos=" + mNewCursorPos + ", "
7026 + "mFrozen=" + mFrozen + ", "
7027 + "mIsComposition=" + mIsComposition + "]";
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007028 }
7029
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007030 public static final Parcelable.ClassLoaderCreator<EditOperation> CREATOR =
7031 new Parcelable.ClassLoaderCreator<EditOperation>() {
James Cookf59152c2015-02-26 18:03:58 -08007032 @Override
James Cook471559f2015-02-27 10:31:20 -08007033 public EditOperation createFromParcel(Parcel in) {
7034 return new EditOperation(in, null);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007035 }
7036
James Cookf59152c2015-02-26 18:03:58 -08007037 @Override
James Cook471559f2015-02-27 10:31:20 -08007038 public EditOperation createFromParcel(Parcel in, ClassLoader loader) {
7039 return new EditOperation(in, loader);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007040 }
7041
James Cookf59152c2015-02-26 18:03:58 -08007042 @Override
James Cook471559f2015-02-27 10:31:20 -08007043 public EditOperation[] newArray(int size) {
7044 return new EditOperation[size];
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07007045 }
7046 };
7047 }
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007048
7049 /**
7050 * A helper for enabling and handling "PROCESS_TEXT" menu actions.
7051 * These allow external applications to plug into currently selected text.
7052 */
7053 static final class ProcessTextIntentActionsHandler {
7054
7055 private final Editor mEditor;
7056 private final TextView mTextView;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01007057 private final Context mContext;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007058 private final PackageManager mPackageManager;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01007059 private final String mPackageName;
7060 private final SparseArray<Intent> mAccessibilityIntents = new SparseArray<>();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007061 private final SparseArray<AccessibilityNodeInfo.AccessibilityAction> mAccessibilityActions =
7062 new SparseArray<>();
7063 private final List<ResolveInfo> mSupportedActivities = new ArrayList<>();
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007064
7065 private ProcessTextIntentActionsHandler(Editor editor) {
Daulet Zhanguzincb0d19b2019-12-18 15:08:09 +00007066 mEditor = Objects.requireNonNull(editor);
7067 mTextView = Objects.requireNonNull(mEditor.mTextView);
7068 mContext = Objects.requireNonNull(mTextView.getContext());
7069 mPackageManager = Objects.requireNonNull(mContext.getPackageManager());
7070 mPackageName = Objects.requireNonNull(mContext.getPackageName());
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007071 }
7072
7073 /**
7074 * Adds "PROCESS_TEXT" menu items to the specified menu.
7075 */
7076 public void onInitializeMenu(Menu menu) {
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01007077 loadSupportedActivities();
Abodunrinwa Tokic28be382017-11-07 18:46:50 +00007078 final int size = mSupportedActivities.size();
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +01007079 for (int i = 0; i < size; i++) {
7080 final ResolveInfo resolveInfo = mSupportedActivities.get(i);
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007081 menu.add(Menu.NONE, Menu.NONE,
Abodunrinwa Tokic28be382017-11-07 18:46:50 +00007082 Editor.MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i,
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007083 getLabel(resolveInfo))
7084 .setIntent(createProcessTextIntentForResolveInfo(resolveInfo))
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00007085 .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007086 }
7087 }
7088
7089 /**
7090 * Performs a "PROCESS_TEXT" action if there is one associated with the specified
7091 * menu item.
7092 *
7093 * @return True if the action was performed, false otherwise.
7094 */
7095 public boolean performMenuItemAction(MenuItem item) {
7096 return fireIntent(item.getIntent());
7097 }
7098
7099 /**
7100 * Initializes and caches "PROCESS_TEXT" accessibility actions.
7101 */
7102 public void initializeAccessibilityActions() {
7103 mAccessibilityIntents.clear();
7104 mAccessibilityActions.clear();
7105 int i = 0;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01007106 loadSupportedActivities();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007107 for (ResolveInfo resolveInfo : mSupportedActivities) {
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007108 int actionId = TextView.ACCESSIBILITY_ACTION_PROCESS_TEXT_START_ID + i++;
7109 mAccessibilityActions.put(
7110 actionId,
7111 new AccessibilityNodeInfo.AccessibilityAction(
7112 actionId, getLabel(resolveInfo)));
7113 mAccessibilityIntents.put(
7114 actionId, createProcessTextIntentForResolveInfo(resolveInfo));
7115 }
7116 }
7117
7118 /**
7119 * Adds "PROCESS_TEXT" accessibility actions to the specified accessibility node info.
7120 * NOTE: This needs a prior call to {@link #initializeAccessibilityActions()} to make the
7121 * latest accessibility actions available for this call.
7122 */
7123 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) {
7124 for (int i = 0; i < mAccessibilityActions.size(); i++) {
7125 nodeInfo.addAction(mAccessibilityActions.valueAt(i));
7126 }
7127 }
7128
7129 /**
7130 * Performs a "PROCESS_TEXT" action if there is one associated with the specified
7131 * accessibility action id.
7132 *
7133 * @return True if the action was performed, false otherwise.
7134 */
7135 public boolean performAccessibilityAction(int actionId) {
7136 return fireIntent(mAccessibilityIntents.get(actionId));
7137 }
7138
7139 private boolean fireIntent(Intent intent) {
7140 if (intent != null && Intent.ACTION_PROCESS_TEXT.equals(intent.getAction())) {
Siyamed Sinirce3b05a2017-07-18 18:54:31 -07007141 String selectedText = mTextView.getSelectedText();
7142 selectedText = TextUtils.trimToParcelableSize(selectedText);
7143 intent.putExtra(Intent.EXTRA_PROCESS_TEXT, selectedText);
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08007144 mEditor.mPreserveSelection = true;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007145 mTextView.startActivityForResult(intent, TextView.PROCESS_TEXT_REQUEST_CODE);
7146 return true;
7147 }
7148 return false;
7149 }
7150
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01007151 private void loadSupportedActivities() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007152 mSupportedActivities.clear();
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01007153 if (!mContext.canStartActivityForResult()) {
7154 return;
7155 }
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007156 PackageManager packageManager = mTextView.getContext().getPackageManager();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007157 List<ResolveInfo> unfiltered =
7158 packageManager.queryIntentActivities(createProcessTextIntent(), 0);
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01007159 for (ResolveInfo info : unfiltered) {
7160 if (isSupportedActivity(info)) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07007161 mSupportedActivities.add(info);
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01007162 }
7163 }
7164 }
7165
7166 private boolean isSupportedActivity(ResolveInfo info) {
7167 return mPackageName.equals(info.activityInfo.packageName)
7168 || info.activityInfo.exported
7169 && (info.activityInfo.permission == null
7170 || mContext.checkSelfPermission(info.activityInfo.permission)
7171 == PackageManager.PERMISSION_GRANTED);
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07007172 }
7173
7174 private Intent createProcessTextIntentForResolveInfo(ResolveInfo info) {
7175 return createProcessTextIntent()
7176 .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !mTextView.isTextEditable())
7177 .setClassName(info.activityInfo.packageName, info.activityInfo.name);
7178 }
7179
7180 private Intent createProcessTextIntent() {
7181 return new Intent()
7182 .setAction(Intent.ACTION_PROCESS_TEXT)
7183 .setType("text/plain");
7184 }
7185
7186 private CharSequence getLabel(ResolveInfo resolveInfo) {
7187 return resolveInfo.loadLabel(mPackageManager);
7188 }
7189 }
Nikita Dubrovsky05cfcc82019-10-24 08:57:32 -07007190
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08007191 static void logCursor(String location, @Nullable String msgFormat, Object ... msgArgs) {
Nikita Dubrovsky05cfcc82019-10-24 08:57:32 -07007192 if (msgFormat == null) {
7193 Log.d(TAG, location);
7194 } else {
7195 Log.d(TAG, location + ": " + String.format(msgFormat, msgArgs));
7196 }
7197 }
Gilles Debunned88876a2012-03-16 17:34:04 -07007198}