blob: f6ac1cc84f430aa90dc93b131e4689623997f5ad [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;
Gilles Debunned88876a2012-03-16 17:34:04 -070027import android.content.ClipData;
28import android.content.ClipData.Item;
29import android.content.Context;
30import android.content.Intent;
Raph Levien26d443a2015-03-30 14:18:32 -070031import android.content.UndoManager;
32import android.content.UndoOperation;
33import android.content.UndoOwner;
Gilles Debunned88876a2012-03-16 17:34:04 -070034import android.content.pm.PackageManager;
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +000035import android.content.pm.ResolveInfo;
Gilles Debunned88876a2012-03-16 17:34:04 -070036import android.content.res.TypedArray;
37import android.graphics.Canvas;
38import android.graphics.Color;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +090039import android.graphics.Matrix;
Gilles Debunned88876a2012-03-16 17:34:04 -070040import android.graphics.Paint;
41import android.graphics.Path;
Mihai Popa63ee7f12018-04-05 12:01:53 +010042import android.graphics.Point;
Mihai Popae3017462018-03-07 12:25:21 +000043import android.graphics.PointF;
Gilles Debunned88876a2012-03-16 17:34:04 -070044import android.graphics.Rect;
45import android.graphics.RectF;
Seigo Nonaka3ed1b392016-01-19 13:54:59 +090046import android.graphics.drawable.ColorDrawable;
Gilles Debunned88876a2012-03-16 17:34:04 -070047import android.graphics.drawable.Drawable;
Gilles Debunned88876a2012-03-16 17:34:04 -070048import android.os.Bundle;
Yohei Yukawa23cbe852016-05-17 16:42:58 -070049import android.os.LocaleList;
Raph Levien26d443a2015-03-30 14:18:32 -070050import android.os.Parcel;
51import android.os.Parcelable;
James Cookf59152c2015-02-26 18:03:58 -080052import android.os.ParcelableParcel;
Gilles Debunned88876a2012-03-16 17:34:04 -070053import android.os.SystemClock;
54import android.provider.Settings;
55import android.text.DynamicLayout;
56import android.text.Editable;
Raph Levien26d443a2015-03-30 14:18:32 -070057import android.text.InputFilter;
Gilles Debunned88876a2012-03-16 17:34:04 -070058import android.text.InputType;
59import android.text.Layout;
60import android.text.ParcelableSpan;
61import android.text.Selection;
62import android.text.SpanWatcher;
63import android.text.Spannable;
64import android.text.SpannableStringBuilder;
65import android.text.Spanned;
66import android.text.StaticLayout;
67import android.text.TextUtils;
Gilles Debunned88876a2012-03-16 17:34:04 -070068import android.text.method.KeyListener;
69import android.text.method.MetaKeyKeyListener;
70import android.text.method.MovementMethod;
Gilles Debunned88876a2012-03-16 17:34:04 -070071import android.text.method.WordIterator;
72import android.text.style.EasyEditSpan;
73import android.text.style.SuggestionRangeSpan;
74import android.text.style.SuggestionSpan;
75import android.text.style.TextAppearanceSpan;
76import android.text.style.URLSpan;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +090077import android.util.ArraySet;
Gilles Debunned88876a2012-03-16 17:34:04 -070078import android.util.DisplayMetrics;
79import android.util.Log;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -070080import android.util.SparseArray;
Gilles Debunned88876a2012-03-16 17:34:04 -070081import android.view.ActionMode;
82import android.view.ActionMode.Callback;
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +090083import android.view.ContextMenu;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +090084import android.view.ContextThemeWrapper;
Chris Craikf6829a02015-03-10 10:28:59 -070085import android.view.DisplayListCanvas;
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -070086import android.view.DragAndDropPermissions;
Gilles Debunned88876a2012-03-16 17:34:04 -070087import android.view.DragEvent;
88import android.view.Gravity;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -070089import android.view.HapticFeedbackConstants;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -080090import android.view.InputDevice;
Gilles Debunned88876a2012-03-16 17:34:04 -070091import android.view.LayoutInflater;
92import android.view.Menu;
93import android.view.MenuItem;
94import android.view.MotionEvent;
Chris Craikf6829a02015-03-10 10:28:59 -070095import android.view.RenderNode;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +090096import android.view.SubMenu;
Gilles Debunned88876a2012-03-16 17:34:04 -070097import android.view.View;
Gilles Debunned88876a2012-03-16 17:34:04 -070098import android.view.View.DragShadowBuilder;
99import android.view.View.OnClickListener;
Adam Powell057a5852012-05-11 10:28:38 -0700100import android.view.ViewConfiguration;
101import android.view.ViewGroup;
Gilles Debunned88876a2012-03-16 17:34:04 -0700102import android.view.ViewGroup.LayoutParams;
Gilles Debunned88876a2012-03-16 17:34:04 -0700103import android.view.ViewTreeObserver;
104import android.view.WindowManager;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700105import android.view.accessibility.AccessibilityNodeInfo;
Mihai Popa38722382018-03-07 19:56:21 +0000106import android.view.animation.LinearInterpolator;
Gilles Debunned88876a2012-03-16 17:34:04 -0700107import android.view.inputmethod.CorrectionInfo;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900108import android.view.inputmethod.CursorAnchorInfo;
Gilles Debunned88876a2012-03-16 17:34:04 -0700109import android.view.inputmethod.EditorInfo;
110import android.view.inputmethod.ExtractedText;
111import android.view.inputmethod.ExtractedTextRequest;
112import android.view.inputmethod.InputConnection;
113import android.view.inputmethod.InputMethodManager;
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100114import android.view.textclassifier.TextClassification;
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +0000115import android.view.textclassifier.TextClassificationManager;
Gilles Debunned88876a2012-03-16 17:34:04 -0700116import android.widget.AdapterView.OnItemClickListener;
117import android.widget.TextView.Drawables;
118import android.widget.TextView.OnEditorActionListener;
119
Seigo Nonakaa60160b2015-08-19 12:38:35 -0700120import com.android.internal.annotations.VisibleForTesting;
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +0000121import com.android.internal.logging.MetricsLogger;
122import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
Raph Levien26d443a2015-03-30 14:18:32 -0700123import com.android.internal.util.ArrayUtils;
124import com.android.internal.util.GrowingArrayUtils;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700125import com.android.internal.util.Preconditions;
Raph Levien26d443a2015-03-30 14:18:32 -0700126import com.android.internal.widget.EditableInputConnection;
127
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +0900128import java.lang.annotation.Retention;
129import java.lang.annotation.RetentionPolicy;
Andrei Stingaceanu2aaeefe2015-10-20 19:11:23 +0100130import java.text.BreakIterator;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +0100131import java.util.ArrayList;
Andrei Stingaceanu2aaeefe2015-10-20 19:11:23 +0100132import java.util.Arrays;
133import java.util.Comparator;
134import java.util.HashMap;
135import java.util.List;
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +0100136import java.util.Map;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700137
Gilles Debunned88876a2012-03-16 17:34:04 -0700138/**
139 * Helper class used by TextView to handle editable text views.
140 *
141 * @hide
142 */
143public class Editor {
Adam Powell057a5852012-05-11 10:28:38 -0700144 private static final String TAG = "Editor";
James Cookf59152c2015-02-26 18:03:58 -0800145 private static final boolean DEBUG_UNDO = false;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100146 // Specifies whether to use or not the magnifier when pressing the insertion or selection
147 // handles.
Andrei Stingaceanu060b3d72017-10-04 11:27:08 +0100148 private static final boolean FLAG_USE_MAGNIFIER = true;
Adam Powell057a5852012-05-11 10:28:38 -0700149
Gilles Debunned88876a2012-03-16 17:34:04 -0700150 static final int BLINK = 500;
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700151 private static final int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
Mady Mellorcc65c372015-06-17 09:25:19 -0700152 private static final float LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS = 0.5f;
Mady Mellore264ac32015-06-22 16:46:29 -0700153 private static final int UNSET_X_VALUE = -1;
Mady Mellora6a0f782015-07-10 16:43:32 -0700154 private static final int UNSET_LINE = -1;
James Cookf59152c2015-02-26 18:03:58 -0800155 // Tag used when the Editor maintains its own separate UndoManager.
156 private static final String UNDO_OWNER_TAG = "Editor";
Gilles Debunned88876a2012-03-16 17:34:04 -0700157
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900158 // Ordering constants used to place the Action Mode or context menu items in their menu.
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +0100159 private static final int MENU_ITEM_ORDER_ASSIST = 0;
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +0000160 private static final int MENU_ITEM_ORDER_UNDO = 2;
161 private static final int MENU_ITEM_ORDER_REDO = 3;
Abodunrinwa Toki5fedfb82017-02-06 19:34:00 +0000162 private static final int MENU_ITEM_ORDER_CUT = 4;
163 private static final int MENU_ITEM_ORDER_COPY = 5;
164 private static final int MENU_ITEM_ORDER_PASTE = 6;
165 private static final int MENU_ITEM_ORDER_SHARE = 7;
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +0100166 private static final int MENU_ITEM_ORDER_SELECT_ALL = 8;
167 private static final int MENU_ITEM_ORDER_REPLACE = 9;
168 private static final int MENU_ITEM_ORDER_AUTOFILL = 10;
169 private static final int MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT = 11;
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +0100170 private static final int MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START = 50;
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +0100171 private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 100;
Clara Bayarri3b69fd82015-06-03 21:52:02 +0100172
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100173 @IntDef({MagnifierHandleTrigger.SELECTION_START,
174 MagnifierHandleTrigger.SELECTION_END,
175 MagnifierHandleTrigger.INSERTION})
176 @Retention(RetentionPolicy.SOURCE)
177 private @interface MagnifierHandleTrigger {
178 int INSERTION = 0;
179 int SELECTION_START = 1;
180 int SELECTION_END = 2;
181 }
182
Richard Ledley26b87222017-11-30 10:54:08 +0000183 @IntDef({TextActionMode.SELECTION, TextActionMode.INSERTION, TextActionMode.TEXT_LINK})
184 @interface TextActionMode {
185 int SELECTION = 0;
186 int INSERTION = 1;
187 int TEXT_LINK = 2;
188 }
189
James Cookf59152c2015-02-26 18:03:58 -0800190 // Each Editor manages its own undo stack.
191 private final UndoManager mUndoManager = new UndoManager();
192 private UndoOwner mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
James Cook48e0fac2015-02-25 15:44:51 -0800193 final UndoInputFilter mUndoInputFilter = new UndoInputFilter(this);
James Cookf1dad1e2015-02-27 11:00:01 -0800194 boolean mAllowUndo = true;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700195
Abodunrinwa Toki54486c12017-04-19 21:02:36 +0100196 private final MetricsLogger mMetricsLogger = new MetricsLogger();
197
Gilles Debunned88876a2012-03-16 17:34:04 -0700198 // Cursor Controllers.
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900199 private InsertionPointCursorController mInsertionPointCursorController;
Gilles Debunned88876a2012-03-16 17:34:04 -0700200 SelectionModifierCursorController mSelectionModifierCursorController;
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100201 // Action mode used when text is selected or when actions on an insertion cursor are triggered.
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800202 private ActionMode mTextActionMode;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900203 private boolean mInsertionControllerEnabled;
204 private boolean mSelectionControllerEnabled;
Gilles Debunned88876a2012-03-16 17:34:04 -0700205
Yohei Yukawac9cd9db2017-06-19 18:27:34 -0700206 private final boolean mHapticTextHandleEnabled;
207
Mihai Popa38722382018-03-07 19:56:21 +0000208 private final MagnifierMotionAnimator mMagnifierAnimator;
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000209 private final Runnable mUpdateMagnifierRunnable = new Runnable() {
210 @Override
211 public void run() {
Mihai Popa38722382018-03-07 19:56:21 +0000212 mMagnifierAnimator.update();
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000213 }
214 };
215 // Update the magnifier contents whenever anything in the view hierarchy is updated.
216 // Note: this only captures UI thread-visible changes, so it's a known issue that an animating
217 // VectorDrawable or Ripple animation will not trigger capture, since they're owned by
218 // RenderThread.
219 private final ViewTreeObserver.OnDrawListener mMagnifierOnDrawListener =
220 new ViewTreeObserver.OnDrawListener() {
221 @Override
222 public void onDraw() {
Mihai Popa38722382018-03-07 19:56:21 +0000223 if (mMagnifierAnimator != null) {
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000224 // Posting the method will ensure that updating the magnifier contents will
225 // happen right after the rendering of the current frame.
226 mTextView.post(mUpdateMagnifierRunnable);
227 }
228 }
229 };
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100230
Gilles Debunned88876a2012-03-16 17:34:04 -0700231 // Used to highlight a word when it is corrected by the IME
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900232 private CorrectionHighlighter mCorrectionHighlighter;
Gilles Debunned88876a2012-03-16 17:34:04 -0700233
234 InputContentType mInputContentType;
235 InputMethodState mInputMethodState;
236
Chris Craik956f3402015-04-27 16:41:00 -0700237 private static class TextRenderNode {
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +0900238 // Render node has 3 recording states:
239 // 1. Recorded operations are valid.
240 // #needsRecord() returns false, but needsToBeShifted is false.
241 // 2. Recorded operations are not valid, but just the position needed to be updated.
242 // #needsRecord() returns false, but needsToBeShifted is true.
243 // 3. Recorded operations are not valid. Need to record operations. #needsRecord() returns
244 // true.
Chris Craik956f3402015-04-27 16:41:00 -0700245 RenderNode renderNode;
John Reck7558aa72014-03-05 14:59:59 -0800246 boolean isDirty;
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +0900247 // Becomes true when recorded operations can be reused, but the position has to be updated.
248 boolean needsToBeShifted;
Chris Craik956f3402015-04-27 16:41:00 -0700249 public TextRenderNode(String name) {
Chris Craik956f3402015-04-27 16:41:00 -0700250 renderNode = RenderNode.create(name, null);
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +0900251 isDirty = true;
252 needsToBeShifted = true;
John Reck7558aa72014-03-05 14:59:59 -0800253 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700254 boolean needsRecord() {
255 return isDirty || !renderNode.isValid();
256 }
John Reck7558aa72014-03-05 14:59:59 -0800257 }
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900258 private TextRenderNode[] mTextRenderNodes;
Gilles Debunned88876a2012-03-16 17:34:04 -0700259
260 boolean mFrozenWithFocus;
261 boolean mSelectionMoved;
262 boolean mTouchFocusSelected;
263
264 KeyListener mKeyListener;
265 int mInputType = EditorInfo.TYPE_NULL;
266
267 boolean mDiscardNextActionUp;
268 boolean mIgnoreActionUpEvent;
269
Mihai Popaa4e39c42018-02-20 15:31:11 +0000270 private long mShowCursor;
271 private boolean mRenderCursorRegardlessTiming;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900272 private Blink mBlink;
Gilles Debunned88876a2012-03-16 17:34:04 -0700273
274 boolean mCursorVisible = true;
275 boolean mSelectAllOnFocus;
276 boolean mTextIsSelectable;
277
278 CharSequence mError;
279 boolean mErrorWasChanged;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900280 private ErrorPopup mErrorPopup;
Fabrice Di Meglio1957d282012-10-25 17:42:39 -0700281
Gilles Debunned88876a2012-03-16 17:34:04 -0700282 /**
283 * This flag is set if the TextView tries to display an error before it
284 * is attached to the window (so its position is still unknown).
285 * It causes the error to be shown later, when onAttachedToWindow()
286 * is called.
287 */
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900288 private boolean mShowErrorAfterAttach;
Gilles Debunned88876a2012-03-16 17:34:04 -0700289
290 boolean mInBatchEditControllers;
Gilles Debunne3473b2b2012-04-20 16:21:10 -0700291 boolean mShowSoftInputOnFocus = true;
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -0800292 private boolean mPreserveSelection;
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +0900293 private boolean mRestartActionModeOnNextRefresh;
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000294 private boolean mRequestingLinkActionMode;
Gilles Debunned88876a2012-03-16 17:34:04 -0700295
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800296 private SelectionActionModeHelper mSelectionActionModeHelper;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +0000297
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900298 boolean mIsBeingLongClicked;
299
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900300 private SuggestionsPopupWindow mSuggestionsPopupWindow;
Gilles Debunned88876a2012-03-16 17:34:04 -0700301 SuggestionRangeSpan mSuggestionRangeSpan;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900302 private Runnable mShowSuggestionRunnable;
Gilles Debunned88876a2012-03-16 17:34:04 -0700303
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -0700304 Drawable mDrawableForCursor = null;
Gilles Debunned88876a2012-03-16 17:34:04 -0700305
306 private Drawable mSelectHandleLeft;
307 private Drawable mSelectHandleRight;
308 private Drawable mSelectHandleCenter;
309
310 // Global listener that detects changes in the global position of the TextView
311 private PositionListener mPositionListener;
312
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900313 private float mLastDownPositionX, mLastDownPositionY;
Petar Å egina91df3f92017-08-15 16:20:43 +0100314 private float mLastUpPositionX, mLastUpPositionY;
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900315 private float mContextMenuAnchorX, mContextMenuAnchorY;
Gilles Debunned88876a2012-03-16 17:34:04 -0700316 Callback mCustomSelectionActionModeCallback;
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100317 Callback mCustomInsertionActionModeCallback;
Gilles Debunned88876a2012-03-16 17:34:04 -0700318
319 // Set when this TextView gained focus with some text selected. Will start selection mode.
320 boolean mCreatedWithASelection;
321
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +0900322 // Indicates the current tap state (first tap, double tap, or triple click).
323 private int mTapState = TAP_STATE_INITIAL;
324 private long mLastTouchUpTime = 0;
325 private static final int TAP_STATE_INITIAL = 0;
326 private static final int TAP_STATE_FIRST_TAP = 1;
327 private static final int TAP_STATE_DOUBLE_TAP = 2;
328 // Only for mouse input.
329 private static final int TAP_STATE_TRIPLE_CLICK = 3;
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100330
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900331 // The button state as of the last time #onTouchEvent is called.
332 private int mLastButtonState;
333
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100334 private Runnable mInsertionActionModeRunnable;
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100335
Jean Chalardbaf30942013-02-28 16:01:51 -0800336 // The span controller helps monitoring the changes to which the Editor needs to react:
337 // - EasyEditSpans, for which we have some UI to display on attach and on hide
338 // - SelectionSpans, for which we need to call updateSelection if an IME is attached
339 private SpanController mSpanController;
Gilles Debunned88876a2012-03-16 17:34:04 -0700340
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900341 private WordIterator mWordIterator;
Gilles Debunned88876a2012-03-16 17:34:04 -0700342 SpellChecker mSpellChecker;
343
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800344 // This word iterator is set with text and used to determine word boundaries
345 // when a user is selecting text.
346 private WordIterator mWordIteratorWithText;
347 // Indicate that the text in the word iterator needs to be updated.
348 private boolean mUpdateWordIteratorText;
349
Gilles Debunned88876a2012-03-16 17:34:04 -0700350 private Rect mTempRect;
351
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800352 private final TextView mTextView;
Gilles Debunned88876a2012-03-16 17:34:04 -0700353
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700354 final ProcessTextIntentActionsHandler mProcessTextIntentActionsHandler;
355
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700356 private final CursorAnchorInfoNotifier mCursorAnchorInfoNotifier =
357 new CursorAnchorInfoNotifier();
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900358
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100359 private final Runnable mShowFloatingToolbar = new Runnable() {
360 @Override
361 public void run() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100362 if (mTextActionMode != null) {
Abodunrinwa Toki9e211282015-06-05 02:46:57 +0100363 mTextActionMode.hide(0); // hide off.
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100364 }
365 }
366 };
367
Clara Bayarrib71dddd2015-06-04 23:17:30 +0100368 boolean mIsInsertionActionModeStartPending = false;
369
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +0900370 private final SuggestionHelper mSuggestionHelper = new SuggestionHelper();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +0900371
Gilles Debunned88876a2012-03-16 17:34:04 -0700372 Editor(TextView textView) {
373 mTextView = textView;
James Cookf59152c2015-02-26 18:03:58 -0800374 // Synchronize the filter list, which places the undo input filter at the end.
375 mTextView.setFilters(mTextView.getFilters());
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700376 mProcessTextIntentActionsHandler = new ProcessTextIntentActionsHandler(this);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -0700377 mHapticTextHandleEnabled = mTextView.getContext().getResources().getBoolean(
378 com.android.internal.R.bool.config_enableHapticTextHandle);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100379
Mihai Popa38722382018-03-07 19:56:21 +0000380 if (FLAG_USE_MAGNIFIER) {
381 mMagnifierAnimator = new MagnifierMotionAnimator(new Magnifier(mTextView));
382 }
James Cookf59152c2015-02-26 18:03:58 -0800383 }
384
385 ParcelableParcel saveInstanceState() {
James Cookd2026682015-03-03 14:40:14 -0800386 ParcelableParcel state = new ParcelableParcel(getClass().getClassLoader());
387 Parcel parcel = state.getParcel();
388 mUndoManager.saveInstanceState(parcel);
389 mUndoInputFilter.saveInstanceState(parcel);
390 return state;
James Cookf59152c2015-02-26 18:03:58 -0800391 }
392
393 void restoreInstanceState(ParcelableParcel state) {
James Cookd2026682015-03-03 14:40:14 -0800394 Parcel parcel = state.getParcel();
395 mUndoManager.restoreInstanceState(parcel, state.getClassLoader());
396 mUndoInputFilter.restoreInstanceState(parcel);
James Cookf59152c2015-02-26 18:03:58 -0800397 // Re-associate this object as the owner of undo state.
398 mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
399 }
400
James Cook48e0fac2015-02-25 15:44:51 -0800401 /**
402 * Forgets all undo and redo operations for this Editor.
403 */
404 void forgetUndoRedo() {
405 UndoOwner[] owners = { mUndoOwner };
406 mUndoManager.forgetUndos(owners, -1 /* all */);
407 mUndoManager.forgetRedos(owners, -1 /* all */);
408 }
409
James Cookf59152c2015-02-26 18:03:58 -0800410 boolean canUndo() {
411 UndoOwner[] owners = { mUndoOwner };
James Cookf1dad1e2015-02-27 11:00:01 -0800412 return mAllowUndo && mUndoManager.countUndos(owners) > 0;
James Cookf59152c2015-02-26 18:03:58 -0800413 }
414
415 boolean canRedo() {
416 UndoOwner[] owners = { mUndoOwner };
James Cookf1dad1e2015-02-27 11:00:01 -0800417 return mAllowUndo && mUndoManager.countRedos(owners) > 0;
James Cookf59152c2015-02-26 18:03:58 -0800418 }
419
420 void undo() {
James Cookf1dad1e2015-02-27 11:00:01 -0800421 if (!mAllowUndo) {
422 return;
423 }
James Cookf59152c2015-02-26 18:03:58 -0800424 UndoOwner[] owners = { mUndoOwner };
425 mUndoManager.undo(owners, 1); // Undo 1 action.
426 }
427
428 void redo() {
James Cookf1dad1e2015-02-27 11:00:01 -0800429 if (!mAllowUndo) {
430 return;
431 }
James Cookf59152c2015-02-26 18:03:58 -0800432 UndoOwner[] owners = { mUndoOwner };
433 mUndoManager.redo(owners, 1); // Redo 1 action.
Gilles Debunned88876a2012-03-16 17:34:04 -0700434 }
435
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100436 void replace() {
Keisuke Kuroyanagi713be062016-02-29 16:07:54 -0800437 if (mSuggestionsPopupWindow == null) {
438 mSuggestionsPopupWindow = new SuggestionsPopupWindow();
439 }
440 hideCursorAndSpanControllers();
441 mSuggestionsPopupWindow.show();
442
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100443 int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100444 Selection.setSelection((Spannable) mTextView.getText(), middle);
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100445 }
446
Gilles Debunned88876a2012-03-16 17:34:04 -0700447 void onAttachedToWindow() {
448 if (mShowErrorAfterAttach) {
449 showError();
450 mShowErrorAfterAttach = false;
451 }
452
453 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000454 if (observer.isAlive()) {
455 // No need to create the controller.
456 // The get method will add the listener on controller creation.
457 if (mInsertionPointCursorController != null) {
458 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
459 }
460 if (mSelectionModifierCursorController != null) {
461 mSelectionModifierCursorController.resetTouchOffsets();
462 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
463 }
464 if (FLAG_USE_MAGNIFIER) {
465 observer.addOnDrawListener(mMagnifierOnDrawListener);
466 }
Gilles Debunned88876a2012-03-16 17:34:04 -0700467 }
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000468
Gilles Debunned88876a2012-03-16 17:34:04 -0700469 updateSpellCheckSpans(0, mTextView.getText().length(),
470 true /* create the spell checker if needed */);
Adam Powell057a5852012-05-11 10:28:38 -0700471
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +0900472 if (mTextView.hasSelection()) {
473 refreshTextActionMode();
Adam Powell057a5852012-05-11 10:28:38 -0700474 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900475
476 getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true);
Mikael Gullstrand5b734f22013-07-09 14:41:28 +0200477 resumeBlink();
Gilles Debunned88876a2012-03-16 17:34:04 -0700478 }
479
480 void onDetachedFromWindow() {
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900481 getPositionListener().removeSubscriber(mCursorAnchorInfoNotifier);
482
Gilles Debunned88876a2012-03-16 17:34:04 -0700483 if (mError != null) {
484 hideError();
485 }
486
Mikael Gullstrand5b734f22013-07-09 14:41:28 +0200487 suspendBlink();
Gilles Debunned88876a2012-03-16 17:34:04 -0700488
489 if (mInsertionPointCursorController != null) {
490 mInsertionPointCursorController.onDetached();
491 }
492
493 if (mSelectionModifierCursorController != null) {
494 mSelectionModifierCursorController.onDetached();
495 }
496
497 if (mShowSuggestionRunnable != null) {
498 mTextView.removeCallbacks(mShowSuggestionRunnable);
499 }
500
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100501 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100502 if (mInsertionActionModeRunnable != null) {
503 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100504 }
505
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100506 mTextView.removeCallbacks(mShowFloatingToolbar);
507
Chris Craik003cc3d2015-10-16 10:24:55 -0700508 discardTextDisplayLists();
Gilles Debunned88876a2012-03-16 17:34:04 -0700509
510 if (mSpellChecker != null) {
511 mSpellChecker.closeSession();
512 // Forces the creation of a new SpellChecker next time this window is created.
513 // Will handle the cases where the settings has been changed in the meantime.
514 mSpellChecker = null;
515 }
516
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000517 if (FLAG_USE_MAGNIFIER) {
518 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
519 if (observer.isAlive()) {
520 observer.removeOnDrawListener(mMagnifierOnDrawListener);
521 }
522 }
523
Mady Mellora2861452015-06-25 08:40:27 -0700524 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -0800525 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -0700526 }
527
Chris Craik003cc3d2015-10-16 10:24:55 -0700528 private void discardTextDisplayLists() {
Chris Craik956f3402015-04-27 16:41:00 -0700529 if (mTextRenderNodes != null) {
530 for (int i = 0; i < mTextRenderNodes.length; i++) {
531 RenderNode displayList = mTextRenderNodes[i] != null
532 ? mTextRenderNodes[i].renderNode : null;
John Reck7558aa72014-03-05 14:59:59 -0800533 if (displayList != null && displayList.isValid()) {
Chris Craik003cc3d2015-10-16 10:24:55 -0700534 displayList.discardDisplayList();
John Reck7558aa72014-03-05 14:59:59 -0800535 }
536 }
537 }
538 }
539
Gilles Debunned88876a2012-03-16 17:34:04 -0700540 private void showError() {
541 if (mTextView.getWindowToken() == null) {
542 mShowErrorAfterAttach = true;
543 return;
544 }
545
546 if (mErrorPopup == null) {
547 LayoutInflater inflater = LayoutInflater.from(mTextView.getContext());
548 final TextView err = (TextView) inflater.inflate(
549 com.android.internal.R.layout.textview_hint, null);
550
551 final float scale = mTextView.getResources().getDisplayMetrics().density;
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700552 mErrorPopup =
553 new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f));
Gilles Debunned88876a2012-03-16 17:34:04 -0700554 mErrorPopup.setFocusable(false);
555 // The user is entering text, so the input method is needed. We
556 // don't want the popup to be displayed on top of it.
557 mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
558 }
559
560 TextView tv = (TextView) mErrorPopup.getContentView();
561 chooseSize(mErrorPopup, mError, tv);
562 tv.setText(mError);
563
Hidehiko Tsuchiyaa0c8c1c2017-11-13 10:52:23 +0900564 mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY(),
565 Gravity.TOP | Gravity.LEFT);
Gilles Debunned88876a2012-03-16 17:34:04 -0700566 mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor());
567 }
568
569 public void setError(CharSequence error, Drawable icon) {
570 mError = TextUtils.stringOrSpannedString(error);
571 mErrorWasChanged = true;
Romain Guyd1cc1872012-11-05 17:43:25 -0800572
Gilles Debunned88876a2012-03-16 17:34:04 -0700573 if (mError == null) {
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800574 setErrorIcon(null);
Gilles Debunned88876a2012-03-16 17:34:04 -0700575 if (mErrorPopup != null) {
576 if (mErrorPopup.isShowing()) {
577 mErrorPopup.dismiss();
578 }
579
580 mErrorPopup = null;
581 }
Daniel 2 Olofssonf4ecc552013-08-13 10:30:26 +0200582 mShowErrorAfterAttach = false;
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800583 } else {
Romain Guyd1cc1872012-11-05 17:43:25 -0800584 setErrorIcon(icon);
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800585 if (mTextView.isFocused()) {
586 showError();
587 }
Romain Guyd1cc1872012-11-05 17:43:25 -0800588 }
589 }
590
591 private void setErrorIcon(Drawable icon) {
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800592 Drawables dr = mTextView.mDrawables;
593 if (dr == null) {
Fabrice Di Megliof7a5cdf2013-03-15 15:36:51 -0700594 mTextView.mDrawables = dr = new Drawables(mTextView.getContext());
Gilles Debunned88876a2012-03-16 17:34:04 -0700595 }
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800596 dr.setErrorDrawable(icon, mTextView);
597
598 mTextView.resetResolvedDrawables();
599 mTextView.invalidate();
600 mTextView.requestLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -0700601 }
602
603 private void hideError() {
604 if (mErrorPopup != null) {
605 if (mErrorPopup.isShowing()) {
606 mErrorPopup.dismiss();
607 }
608 }
609
610 mShowErrorAfterAttach = false;
611 }
612
613 /**
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800614 * Returns the X offset to make the pointy top of the error point
Gilles Debunned88876a2012-03-16 17:34:04 -0700615 * at the middle of the error icon.
616 */
617 private int getErrorX() {
618 /*
619 * The "25" is the distance between the point and the right edge
620 * of the background
621 */
622 final float scale = mTextView.getResources().getDisplayMetrics().density;
623
624 final Drawables dr = mTextView.mDrawables;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800625
626 final int layoutDirection = mTextView.getLayoutDirection();
627 int errorX;
628 int offset;
629 switch (layoutDirection) {
630 default:
631 case View.LAYOUT_DIRECTION_LTR:
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700632 offset = -(dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
633 errorX = mTextView.getWidth() - mErrorPopup.getWidth()
634 - mTextView.getPaddingRight() + offset;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800635 break;
636 case View.LAYOUT_DIRECTION_RTL:
637 offset = (dr != null ? dr.mDrawableSizeLeft : 0) / 2 - (int) (25 * scale + 0.5f);
638 errorX = mTextView.getPaddingLeft() + offset;
639 break;
640 }
641 return errorX;
Gilles Debunned88876a2012-03-16 17:34:04 -0700642 }
643
644 /**
645 * Returns the Y offset to make the pointy top of the error point
646 * at the bottom of the error icon.
647 */
648 private int getErrorY() {
649 /*
650 * Compound, not extended, because the icon is not clipped
651 * if the text height is smaller.
652 */
653 final int compoundPaddingTop = mTextView.getCompoundPaddingTop();
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700654 int vspace = mTextView.getBottom() - mTextView.getTop()
655 - mTextView.getCompoundPaddingBottom() - compoundPaddingTop;
Gilles Debunned88876a2012-03-16 17:34:04 -0700656
657 final Drawables dr = mTextView.mDrawables;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800658
659 final int layoutDirection = mTextView.getLayoutDirection();
660 int height;
661 switch (layoutDirection) {
662 default:
663 case View.LAYOUT_DIRECTION_LTR:
664 height = (dr != null ? dr.mDrawableHeightRight : 0);
665 break;
666 case View.LAYOUT_DIRECTION_RTL:
667 height = (dr != null ? dr.mDrawableHeightLeft : 0);
668 break;
669 }
670
671 int icontop = compoundPaddingTop + (vspace - height) / 2;
Gilles Debunned88876a2012-03-16 17:34:04 -0700672
673 /*
674 * The "2" is the distance between the point and the top edge
675 * of the background.
676 */
677 final float scale = mTextView.getResources().getDisplayMetrics().density;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800678 return icontop + height - mTextView.getHeight() - (int) (2 * scale + 0.5f);
Gilles Debunned88876a2012-03-16 17:34:04 -0700679 }
680
681 void createInputContentTypeIfNeeded() {
682 if (mInputContentType == null) {
683 mInputContentType = new InputContentType();
684 }
685 }
686
687 void createInputMethodStateIfNeeded() {
688 if (mInputMethodState == null) {
689 mInputMethodState = new InputMethodState();
690 }
691 }
692
Mihai Popaa4e39c42018-02-20 15:31:11 +0000693 private boolean isCursorVisible() {
Gilles Debunned88876a2012-03-16 17:34:04 -0700694 // The default value is true, even when there is no associated Editor
695 return mCursorVisible && mTextView.isTextEditable();
696 }
697
Mihai Popaa4e39c42018-02-20 15:31:11 +0000698 boolean shouldRenderCursor() {
699 if (!isCursorVisible()) {
700 return false;
701 }
702 if (mRenderCursorRegardlessTiming) {
703 return true;
704 }
705 final long showCursorDelta = SystemClock.uptimeMillis() - mShowCursor;
706 return showCursorDelta % (2 * BLINK) < BLINK;
707 }
708
Gilles Debunned88876a2012-03-16 17:34:04 -0700709 void prepareCursorControllers() {
710 boolean windowSupportsHandles = false;
711
712 ViewGroup.LayoutParams params = mTextView.getRootView().getLayoutParams();
713 if (params instanceof WindowManager.LayoutParams) {
714 WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
715 windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
716 || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
717 }
718
719 boolean enabled = windowSupportsHandles && mTextView.getLayout() != null;
720 mInsertionControllerEnabled = enabled && isCursorVisible();
721 mSelectionControllerEnabled = enabled && mTextView.textCanBeSelected();
722
723 if (!mInsertionControllerEnabled) {
724 hideInsertionPointCursorController();
725 if (mInsertionPointCursorController != null) {
726 mInsertionPointCursorController.onDetached();
727 mInsertionPointCursorController = null;
728 }
729 }
730
731 if (!mSelectionControllerEnabled) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100732 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -0700733 if (mSelectionModifierCursorController != null) {
734 mSelectionModifierCursorController.onDetached();
735 mSelectionModifierCursorController = null;
736 }
737 }
738 }
739
Seigo Nonakabb6a62c2015-03-31 21:59:30 +0900740 void hideInsertionPointCursorController() {
Gilles Debunned88876a2012-03-16 17:34:04 -0700741 if (mInsertionPointCursorController != null) {
742 mInsertionPointCursorController.hide();
743 }
744 }
745
746 /**
Mady Mellora2861452015-06-25 08:40:27 -0700747 * Hides the insertion and span controllers.
Gilles Debunned88876a2012-03-16 17:34:04 -0700748 */
Mady Mellora2861452015-06-25 08:40:27 -0700749 void hideCursorAndSpanControllers() {
Gilles Debunned88876a2012-03-16 17:34:04 -0700750 hideCursorControllers();
751 hideSpanControllers();
752 }
753
754 private void hideSpanControllers() {
Jean Chalardbaf30942013-02-28 16:01:51 -0800755 if (mSpanController != null) {
756 mSpanController.hide();
Gilles Debunned88876a2012-03-16 17:34:04 -0700757 }
758 }
759
760 private void hideCursorControllers() {
Yohei Yukawa85d08f12015-04-29 20:12:37 -0700761 // When mTextView is not ExtractEditText, we need to distinguish two kinds of focus-lost.
762 // One is the true focus lost where suggestions pop-up (if any) should be dismissed, and the
763 // other is an side effect of showing the suggestions pop-up itself. We use isShowingUp()
764 // to distinguish one from the other.
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700765 if (mSuggestionsPopupWindow != null && ((mTextView.isInExtractedMode())
766 || !mSuggestionsPopupWindow.isShowingUp())) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700767 // Should be done before hide insertion point controller since it triggers a show of it
768 mSuggestionsPopupWindow.hide();
769 }
770 hideInsertionPointCursorController();
Gilles Debunned88876a2012-03-16 17:34:04 -0700771 }
772
773 /**
774 * Create new SpellCheckSpans on the modified region.
775 */
776 private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
Satoshi Kataokad7429c12013-06-05 16:30:23 +0900777 // Remove spans whose adjacent characters are text not punctuation
778 mTextView.removeAdjacentSuggestionSpans(start);
779 mTextView.removeAdjacentSuggestionSpans(end);
780
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700781 if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled()
782 && !(mTextView.isInExtractedMode())) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700783 if (mSpellChecker == null && createSpellChecker) {
784 mSpellChecker = new SpellChecker(mTextView);
785 }
786 if (mSpellChecker != null) {
787 mSpellChecker.spellCheck(start, end);
788 }
789 }
790 }
791
792 void onScreenStateChanged(int screenState) {
793 switch (screenState) {
794 case View.SCREEN_STATE_ON:
795 resumeBlink();
796 break;
797 case View.SCREEN_STATE_OFF:
798 suspendBlink();
799 break;
800 }
801 }
802
803 private void suspendBlink() {
804 if (mBlink != null) {
805 mBlink.cancel();
806 }
807 }
808
809 private void resumeBlink() {
810 if (mBlink != null) {
811 mBlink.uncancel();
812 makeBlink();
813 }
814 }
815
816 void adjustInputType(boolean password, boolean passwordInputType,
817 boolean webPasswordInputType, boolean numberPasswordInputType) {
818 // mInputType has been set from inputType, possibly modified by mInputMethod.
819 // Specialize mInputType to [web]password if we have a text class and the original input
820 // type was a password.
821 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
822 if (password || passwordInputType) {
823 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
824 | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
825 }
826 if (webPasswordInputType) {
827 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
828 | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
829 }
830 } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
831 if (numberPasswordInputType) {
832 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
833 | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
834 }
835 }
836 }
837
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700838 private void chooseSize(@NonNull PopupWindow pop, @NonNull CharSequence text,
839 @NonNull TextView tv) {
840 final int wid = tv.getPaddingLeft() + tv.getPaddingRight();
841 final int ht = tv.getPaddingTop() + tv.getPaddingBottom();
Gilles Debunned88876a2012-03-16 17:34:04 -0700842
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700843 final int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize(
Gilles Debunned88876a2012-03-16 17:34:04 -0700844 com.android.internal.R.dimen.textview_error_popup_default_width);
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700845 final StaticLayout l = StaticLayout.Builder.obtain(text, 0, text.length(), tv.getPaint(),
846 defaultWidthInPixels)
847 .setUseLineSpacingFromFallbacks(tv.mUseFallbackLineSpacing)
848 .build();
849
Gilles Debunned88876a2012-03-16 17:34:04 -0700850 float max = 0;
851 for (int i = 0; i < l.getLineCount(); i++) {
852 max = Math.max(max, l.getLineWidth(i));
853 }
854
855 /*
856 * Now set the popup size to be big enough for the text plus the border capped
857 * to DEFAULT_MAX_POPUP_WIDTH
858 */
859 pop.setWidth(wid + (int) Math.ceil(max));
860 pop.setHeight(ht + l.getHeight());
861 }
862
863 void setFrame() {
864 if (mErrorPopup != null) {
865 TextView tv = (TextView) mErrorPopup.getContentView();
866 chooseSize(mErrorPopup, mError, tv);
867 mErrorPopup.update(mTextView, getErrorX(), getErrorY(),
868 mErrorPopup.getWidth(), mErrorPopup.getHeight());
869 }
870 }
871
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800872 private int getWordStart(int offset) {
873 // FIXME - For this and similar methods we're not doing anything to check if there's
874 // a LocaleSpan in the text, this may be something we should try handling or checking for.
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700875 int retOffset = getWordIteratorWithText().prevBoundary(offset);
Mady Mellor58c90872015-05-12 11:09:37 -0700876 if (getWordIteratorWithText().isOnPunctuation(retOffset)) {
877 // On punctuation boundary or within group of punctuation, find punctuation start.
878 retOffset = getWordIteratorWithText().getPunctuationBeginning(offset);
879 } else {
880 // Not on a punctuation boundary, find the word start.
Mady Mellore264ac32015-06-22 16:46:29 -0700881 retOffset = getWordIteratorWithText().getPrevWordBeginningOnTwoWordsBoundary(offset);
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800882 }
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700883 if (retOffset == BreakIterator.DONE) {
884 return offset;
885 }
886 return retOffset;
887 }
888
889 private int getWordEnd(int offset) {
890 int retOffset = getWordIteratorWithText().nextBoundary(offset);
Mady Mellor58c90872015-05-12 11:09:37 -0700891 if (getWordIteratorWithText().isAfterPunctuation(retOffset)) {
892 // On punctuation boundary or within group of punctuation, find punctuation end.
893 retOffset = getWordIteratorWithText().getPunctuationEnd(offset);
894 } else {
895 // Not on a punctuation boundary, find the word end.
Mady Mellore264ac32015-06-22 16:46:29 -0700896 retOffset = getWordIteratorWithText().getNextWordEndOnTwoWordBoundary(offset);
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700897 }
898 if (retOffset == BreakIterator.DONE) {
899 return offset;
900 }
901 return retOffset;
902 }
903
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900904 private boolean needsToSelectAllToSelectWordOrParagraph() {
Andrei Stingaceanu47f82ae2015-04-28 17:43:54 +0100905 if (mTextView.hasPasswordTransformationMethod()) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700906 // Always select all on a password field.
907 // Cut/copy menu entries are not available for passwords, but being able to select all
908 // is however useful to delete or paste to replace the entire content.
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900909 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -0700910 }
911
912 int inputType = mTextView.getInputType();
913 int klass = inputType & InputType.TYPE_MASK_CLASS;
914 int variation = inputType & InputType.TYPE_MASK_VARIATION;
915
916 // Specific text field types: select the entire text for these
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700917 if (klass == InputType.TYPE_CLASS_NUMBER
918 || klass == InputType.TYPE_CLASS_PHONE
919 || klass == InputType.TYPE_CLASS_DATETIME
920 || variation == InputType.TYPE_TEXT_VARIATION_URI
921 || variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
922 || variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS
923 || variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900924 return true;
925 }
926 return false;
927 }
928
929 /**
930 * Adjusts selection to the word under last touch offset. Return true if the operation was
931 * successfully performed.
932 */
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100933 boolean selectCurrentWord() {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900934 if (!mTextView.canSelectText()) {
935 return false;
936 }
937
938 if (needsToSelectAllToSelectWordOrParagraph()) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700939 return mTextView.selectAllText();
940 }
941
942 long lastTouchOffsets = getLastTouchOffsets();
943 final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
944 final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
945
946 // Safety check in case standard touch event handling has been bypassed
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -0800947 if (minOffset < 0 || minOffset > mTextView.getText().length()) return false;
948 if (maxOffset < 0 || maxOffset > mTextView.getText().length()) return false;
Gilles Debunned88876a2012-03-16 17:34:04 -0700949
950 int selectionStart, selectionEnd;
951
952 // If a URLSpan (web address, email, phone...) is found at that position, select it.
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700953 URLSpan[] urlSpans =
954 ((Spanned) mTextView.getText()).getSpans(minOffset, maxOffset, URLSpan.class);
Gilles Debunned88876a2012-03-16 17:34:04 -0700955 if (urlSpans.length >= 1) {
956 URLSpan urlSpan = urlSpans[0];
957 selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan);
958 selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan);
959 } else {
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800960 // FIXME - We should check if there's a LocaleSpan in the text, this may be
961 // something we should try handling or checking for.
Gilles Debunned88876a2012-03-16 17:34:04 -0700962 final WordIterator wordIterator = getWordIterator();
963 wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset);
964
965 selectionStart = wordIterator.getBeginning(minOffset);
966 selectionEnd = wordIterator.getEnd(maxOffset);
967
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700968 if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE
969 || selectionStart == selectionEnd) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700970 // Possible when the word iterator does not properly handle the text's language
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +0900971 long range = getCharClusterRange(minOffset);
Gilles Debunned88876a2012-03-16 17:34:04 -0700972 selectionStart = TextUtils.unpackRangeStartFromLong(range);
973 selectionEnd = TextUtils.unpackRangeEndFromLong(range);
974 }
975 }
976
977 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
978 return selectionEnd > selectionStart;
979 }
980
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900981 /**
982 * Adjusts selection to the paragraph under last touch offset. Return true if the operation was
983 * successfully performed.
984 */
985 private boolean selectCurrentParagraph() {
986 if (!mTextView.canSelectText()) {
987 return false;
988 }
989
990 if (needsToSelectAllToSelectWordOrParagraph()) {
991 return mTextView.selectAllText();
992 }
993
994 long lastTouchOffsets = getLastTouchOffsets();
995 final int minLastTouchOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
996 final int maxLastTouchOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
997
998 final long paragraphsRange = getParagraphsRange(minLastTouchOffset, maxLastTouchOffset);
999 final int start = TextUtils.unpackRangeStartFromLong(paragraphsRange);
1000 final int end = TextUtils.unpackRangeEndFromLong(paragraphsRange);
1001 if (start < end) {
1002 Selection.setSelection((Spannable) mTextView.getText(), start, end);
1003 return true;
1004 }
1005 return false;
1006 }
1007
1008 /**
1009 * Get the minimum range of paragraphs that contains startOffset and endOffset.
1010 */
1011 private long getParagraphsRange(int startOffset, int endOffset) {
1012 final Layout layout = mTextView.getLayout();
1013 if (layout == null) {
1014 return TextUtils.packRangeInLong(-1, -1);
1015 }
1016 final CharSequence text = mTextView.getText();
1017 int minLine = layout.getLineForOffset(startOffset);
1018 // Search paragraph start.
1019 while (minLine > 0) {
1020 final int prevLineEndOffset = layout.getLineEnd(minLine - 1);
1021 if (text.charAt(prevLineEndOffset - 1) == '\n') {
1022 break;
1023 }
1024 minLine--;
1025 }
1026 int maxLine = layout.getLineForOffset(endOffset);
1027 // Search paragraph end.
1028 while (maxLine < layout.getLineCount() - 1) {
1029 final int lineEndOffset = layout.getLineEnd(maxLine);
1030 if (text.charAt(lineEndOffset - 1) == '\n') {
1031 break;
1032 }
1033 maxLine++;
1034 }
1035 return TextUtils.packRangeInLong(layout.getLineStart(minLine), layout.getLineEnd(maxLine));
1036 }
1037
Gilles Debunned88876a2012-03-16 17:34:04 -07001038 void onLocaleChanged() {
Keisuke Kuroyanagie0ac5ac2016-03-09 15:33:30 +09001039 // Will be re-created on demand in getWordIterator and getWordIteratorWithText with the
1040 // proper new locale
Gilles Debunned88876a2012-03-16 17:34:04 -07001041 mWordIterator = null;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08001042 mWordIteratorWithText = null;
Gilles Debunned88876a2012-03-16 17:34:04 -07001043 }
1044
Gilles Debunned88876a2012-03-16 17:34:04 -07001045 public WordIterator getWordIterator() {
1046 if (mWordIterator == null) {
1047 mWordIterator = new WordIterator(mTextView.getTextServicesLocale());
1048 }
1049 return mWordIterator;
1050 }
1051
Mady Mellor2ff2cd82015-03-02 10:37:01 -08001052 private WordIterator getWordIteratorWithText() {
1053 if (mWordIteratorWithText == null) {
1054 mWordIteratorWithText = new WordIterator(mTextView.getTextServicesLocale());
1055 mUpdateWordIteratorText = true;
1056 }
1057 if (mUpdateWordIteratorText) {
1058 // FIXME - Shouldn't copy all of the text as only the area of the text relevant
1059 // to the user's selection is needed. A possible solution would be to
1060 // copy some number N of characters near the selection and then when the
1061 // user approaches N then we'd do another copy of the next N characters.
1062 CharSequence text = mTextView.getText();
1063 mWordIteratorWithText.setCharSequence(text, 0, text.length());
1064 mUpdateWordIteratorText = false;
1065 }
1066 return mWordIteratorWithText;
1067 }
1068
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +09001069 private int getNextCursorOffset(int offset, boolean findAfterGivenOffset) {
1070 final Layout layout = mTextView.getLayout();
1071 if (layout == null) return offset;
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001072 return findAfterGivenOffset == layout.isRtlCharAt(offset)
1073 ? layout.getOffsetToLeftOf(offset) : layout.getOffsetToRightOf(offset);
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +09001074 }
1075
1076 private long getCharClusterRange(int offset) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001077 final int textLength = mTextView.getText().length();
Gilles Debunned88876a2012-03-16 17:34:04 -07001078 if (offset < textLength) {
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08001079 final int clusterEndOffset = getNextCursorOffset(offset, true);
1080 return TextUtils.packRangeInLong(
1081 getNextCursorOffset(clusterEndOffset, false), clusterEndOffset);
Gilles Debunned88876a2012-03-16 17:34:04 -07001082 }
1083 if (offset - 1 >= 0) {
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08001084 final int clusterStartOffset = getNextCursorOffset(offset, false);
1085 return TextUtils.packRangeInLong(clusterStartOffset,
1086 getNextCursorOffset(clusterStartOffset, true));
Gilles Debunned88876a2012-03-16 17:34:04 -07001087 }
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +09001088 return TextUtils.packRangeInLong(offset, offset);
Gilles Debunned88876a2012-03-16 17:34:04 -07001089 }
1090
1091 private boolean touchPositionIsInSelection() {
1092 int selectionStart = mTextView.getSelectionStart();
1093 int selectionEnd = mTextView.getSelectionEnd();
1094
1095 if (selectionStart == selectionEnd) {
1096 return false;
1097 }
1098
1099 if (selectionStart > selectionEnd) {
1100 int tmp = selectionStart;
1101 selectionStart = selectionEnd;
1102 selectionEnd = tmp;
1103 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
1104 }
1105
1106 SelectionModifierCursorController selectionController = getSelectionController();
1107 int minOffset = selectionController.getMinTouchOffset();
1108 int maxOffset = selectionController.getMaxTouchOffset();
1109
1110 return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
1111 }
1112
1113 private PositionListener getPositionListener() {
1114 if (mPositionListener == null) {
1115 mPositionListener = new PositionListener();
1116 }
1117 return mPositionListener;
1118 }
1119
1120 private interface TextViewPositionListener {
1121 public void updatePosition(int parentPositionX, int parentPositionY,
1122 boolean parentPositionChanged, boolean parentScrolled);
1123 }
1124
Gilles Debunned88876a2012-03-16 17:34:04 -07001125 private boolean isOffsetVisible(int offset) {
1126 Layout layout = mTextView.getLayout();
Victoria Leaseb9b77ae2013-10-13 15:12:52 -07001127 if (layout == null) return false;
1128
Gilles Debunned88876a2012-03-16 17:34:04 -07001129 final int line = layout.getLineForOffset(offset);
1130 final int lineBottom = layout.getLineBottom(line);
1131 final int primaryHorizontal = (int) layout.getPrimaryHorizontal(offset);
Phil Weaverc2e28932016-12-08 12:29:25 -08001132 return mTextView.isPositionVisible(
1133 primaryHorizontal + mTextView.viewportToContentHorizontalOffset(),
Gilles Debunned88876a2012-03-16 17:34:04 -07001134 lineBottom + mTextView.viewportToContentVerticalOffset());
1135 }
1136
1137 /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
1138 * in the view. Returns false when the position is in the empty space of left/right of text.
1139 */
1140 private boolean isPositionOnText(float x, float y) {
1141 Layout layout = mTextView.getLayout();
1142 if (layout == null) return false;
1143
1144 final int line = mTextView.getLineAtCoordinate(y);
1145 x = mTextView.convertToLocalHorizontalCoordinate(x);
1146
1147 if (x < layout.getLineLeft(line)) return false;
1148 if (x > layout.getLineRight(line)) return false;
1149 return true;
1150 }
1151
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001152 private void startDragAndDrop() {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001153 getSelectionActionModeHelper().onSelectionDrag();
1154
Keisuke Kuroyanagifdfc93d2016-03-15 14:47:08 +09001155 // TODO: Fix drag and drop in full screen extracted mode.
1156 if (mTextView.isInExtractedMode()) {
1157 return;
1158 }
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001159 final int start = mTextView.getSelectionStart();
1160 final int end = mTextView.getSelectionEnd();
1161 CharSequence selectedText = mTextView.getTransformedText(start, end);
1162 ClipData data = ClipData.newPlainText(null, selectedText);
1163 DragLocalState localState = new DragLocalState(mTextView, start, end);
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08001164 mTextView.startDragAndDrop(data, getTextThumbnailBuilder(start, end), localState,
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001165 View.DRAG_FLAG_GLOBAL);
1166 stopTextActionMode();
1167 if (hasSelectionController()) {
1168 getSelectionController().resetTouchOffsets();
1169 }
1170 }
1171
Gilles Debunned88876a2012-03-16 17:34:04 -07001172 public boolean performLongClick(boolean handled) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001173 // Long press in empty space moves cursor and starts the insertion action mode.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001174 if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY)
1175 && mInsertionControllerEnabled) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001176 final int offset = mTextView.getOffsetForPosition(mLastDownPositionX,
1177 mLastDownPositionY);
Gilles Debunned88876a2012-03-16 17:34:04 -07001178 Selection.setSelection((Spannable) mTextView.getText(), offset);
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00001179 getInsertionController().show();
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001180 mIsInsertionActionModeStartPending = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001181 handled = true;
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001182 MetricsLogger.action(
1183 mTextView.getContext(),
1184 MetricsEvent.TEXT_LONGPRESS,
1185 TextViewMetrics.SUBTYPE_LONG_PRESS_OTHER);
Gilles Debunned88876a2012-03-16 17:34:04 -07001186 }
1187
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001188 if (!handled && mTextActionMode != null) {
Andrei Stingaceanu2aaeefe2015-10-20 19:11:23 +01001189 if (touchPositionIsInSelection()) {
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001190 startDragAndDrop();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001191 MetricsLogger.action(
1192 mTextView.getContext(),
1193 MetricsEvent.TEXT_LONGPRESS,
1194 TextViewMetrics.SUBTYPE_LONG_PRESS_DRAG_AND_DROP);
Gilles Debunned88876a2012-03-16 17:34:04 -07001195 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001196 stopTextActionMode();
Clara Bayarridfac4432015-05-15 12:18:24 +01001197 selectCurrentWordAndStartDrag();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001198 MetricsLogger.action(
1199 mTextView.getContext(),
1200 MetricsEvent.TEXT_LONGPRESS,
1201 TextViewMetrics.SUBTYPE_LONG_PRESS_SELECTION);
Gilles Debunned88876a2012-03-16 17:34:04 -07001202 }
1203 handled = true;
1204 }
1205
1206 // Start a new selection
1207 if (!handled) {
Clara Bayarridfac4432015-05-15 12:18:24 +01001208 handled = selectCurrentWordAndStartDrag();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001209 if (handled) {
1210 MetricsLogger.action(
1211 mTextView.getContext(),
1212 MetricsEvent.TEXT_LONGPRESS,
1213 TextViewMetrics.SUBTYPE_LONG_PRESS_SELECTION);
1214 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001215 }
1216
1217 return handled;
1218 }
1219
Petar Å egina91df3f92017-08-15 16:20:43 +01001220 float getLastUpPositionX() {
1221 return mLastUpPositionX;
1222 }
1223
1224 float getLastUpPositionY() {
1225 return mLastUpPositionY;
1226 }
1227
Gilles Debunned88876a2012-03-16 17:34:04 -07001228 private long getLastTouchOffsets() {
1229 SelectionModifierCursorController selectionController = getSelectionController();
1230 final int minOffset = selectionController.getMinTouchOffset();
1231 final int maxOffset = selectionController.getMaxTouchOffset();
1232 return TextUtils.packRangeInLong(minOffset, maxOffset);
1233 }
1234
1235 void onFocusChanged(boolean focused, int direction) {
1236 mShowCursor = SystemClock.uptimeMillis();
1237 ensureEndedBatchEdit();
1238
1239 if (focused) {
1240 int selStart = mTextView.getSelectionStart();
1241 int selEnd = mTextView.getSelectionEnd();
1242
1243 // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
1244 // mode for these, unless there was a specific selection already started.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001245 final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0
1246 && selEnd == mTextView.getText().length();
Gilles Debunned88876a2012-03-16 17:34:04 -07001247
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001248 mCreatedWithASelection = mFrozenWithFocus && mTextView.hasSelection()
1249 && !isFocusHighlighted;
Gilles Debunned88876a2012-03-16 17:34:04 -07001250
1251 if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
1252 // If a tap was used to give focus to that view, move cursor at tap position.
1253 // Has to be done before onTakeFocus, which can be overloaded.
1254 final int lastTapPosition = getLastTapPosition();
1255 if (lastTapPosition >= 0) {
1256 Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition);
1257 }
1258
1259 // Note this may have to be moved out of the Editor class
1260 MovementMethod mMovement = mTextView.getMovementMethod();
1261 if (mMovement != null) {
1262 mMovement.onTakeFocus(mTextView, (Spannable) mTextView.getText(), direction);
1263 }
1264
1265 // The DecorView does not have focus when the 'Done' ExtractEditText button is
1266 // pressed. Since it is the ViewAncestor's mView, it requests focus before
1267 // ExtractEditText clears focus, which gives focus to the ExtractEditText.
1268 // This special case ensure that we keep current selection in that case.
1269 // It would be better to know why the DecorView does not have focus at that time.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001270 if (((mTextView.isInExtractedMode()) || mSelectionMoved)
1271 && selStart >= 0 && selEnd >= 0) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001272 /*
1273 * Someone intentionally set the selection, so let them
1274 * do whatever it is that they wanted to do instead of
1275 * the default on-focus behavior. We reset the selection
1276 * here instead of just skipping the onTakeFocus() call
1277 * because some movement methods do something other than
1278 * just setting the selection in theirs and we still
1279 * need to go through that path.
1280 */
1281 Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
1282 }
1283
1284 if (mSelectAllOnFocus) {
1285 mTextView.selectAllText();
1286 }
1287
1288 mTouchFocusSelected = true;
1289 }
1290
1291 mFrozenWithFocus = false;
1292 mSelectionMoved = false;
1293
1294 if (mError != null) {
1295 showError();
1296 }
1297
1298 makeBlink();
1299 } else {
1300 if (mError != null) {
1301 hideError();
1302 }
1303 // Don't leave us in the middle of a batch edit.
1304 mTextView.onEndBatchEdit();
1305
Andrei Stingaceanub1891b32015-06-19 16:44:37 +01001306 if (mTextView.isInExtractedMode()) {
Mady Mellora2861452015-06-25 08:40:27 -07001307 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001308 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -07001309 } else {
Mady Mellora2861452015-06-25 08:40:27 -07001310 hideCursorAndSpanControllers();
Yohei Yukawa24df9312016-03-31 17:15:23 -07001311 if (mTextView.isTemporarilyDetached()) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001312 stopTextActionModeWithPreservingSelection();
1313 } else {
1314 stopTextActionMode();
1315 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001316 downgradeEasyCorrectionSpans();
1317 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001318 // No need to create the controller
1319 if (mSelectionModifierCursorController != null) {
1320 mSelectionModifierCursorController.resetTouchOffsets();
1321 }
Richard Ledley5f2f8202018-02-05 14:55:47 +00001322
1323 ensureNoSelectionIfNonSelectable();
1324 }
1325 }
1326
1327 private void ensureNoSelectionIfNonSelectable() {
1328 // This could be the case if a TextLink has been tapped.
1329 if (!mTextView.textCanBeSelected() && mTextView.hasSelection()) {
1330 Selection.setSelection((Spannable) mTextView.getText(),
1331 mTextView.length(), mTextView.length());
Gilles Debunned88876a2012-03-16 17:34:04 -07001332 }
1333 }
1334
1335 /**
1336 * Downgrades to simple suggestions all the easy correction spans that are not a spell check
1337 * span.
1338 */
1339 private void downgradeEasyCorrectionSpans() {
1340 CharSequence text = mTextView.getText();
1341 if (text instanceof Spannable) {
1342 Spannable spannable = (Spannable) text;
1343 SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
1344 spannable.length(), SuggestionSpan.class);
1345 for (int i = 0; i < suggestionSpans.length; i++) {
1346 int flags = suggestionSpans[i].getFlags();
1347 if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
1348 && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
1349 flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
1350 suggestionSpans[i].setFlags(flags);
1351 }
1352 }
1353 }
1354 }
1355
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +01001356 void sendOnTextChanged(int start, int before, int after) {
1357 getSelectionActionModeHelper().onTextChanged(start, start + before);
Gilles Debunned88876a2012-03-16 17:34:04 -07001358 updateSpellCheckSpans(start, start + after, false);
1359
Mady Mellor2ff2cd82015-03-02 10:37:01 -08001360 // Flip flag to indicate the word iterator needs to have the text reset.
1361 mUpdateWordIteratorText = true;
1362
Gilles Debunned88876a2012-03-16 17:34:04 -07001363 // Hide the controllers as soon as text is modified (typing, procedural...)
1364 // We do not hide the span controllers, since they can be added when a new text is
1365 // inserted into the text view (voice IME).
1366 hideCursorControllers();
Keisuke Kuroyanagif4e347d2015-06-11 17:41:00 +09001367 // Reset drag accelerator.
1368 if (mSelectionModifierCursorController != null) {
1369 mSelectionModifierCursorController.resetTouchOffsets();
1370 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001371 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07001372 }
1373
1374 private int getLastTapPosition() {
1375 // No need to create the controller at that point, no last tap position saved
1376 if (mSelectionModifierCursorController != null) {
1377 int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
1378 if (lastTapPosition >= 0) {
1379 // Safety check, should not be possible.
1380 if (lastTapPosition > mTextView.getText().length()) {
1381 lastTapPosition = mTextView.getText().length();
1382 }
1383 return lastTapPosition;
1384 }
1385 }
1386
1387 return -1;
1388 }
1389
1390 void onWindowFocusChanged(boolean hasWindowFocus) {
1391 if (hasWindowFocus) {
1392 if (mBlink != null) {
1393 mBlink.uncancel();
1394 makeBlink();
1395 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001396 if (mTextView.hasSelection() && !extractedTextModeWillBeStarted()) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001397 refreshTextActionMode();
Mady Mellora2861452015-06-25 08:40:27 -07001398 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001399 } else {
1400 if (mBlink != null) {
1401 mBlink.cancel();
1402 }
1403 if (mInputContentType != null) {
1404 mInputContentType.enterDown = false;
1405 }
1406 // Order matters! Must be done before onParentLostFocus to rely on isShowingUp
Mady Mellora2861452015-06-25 08:40:27 -07001407 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001408 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -07001409 if (mSuggestionsPopupWindow != null) {
1410 mSuggestionsPopupWindow.onParentLostFocus();
1411 }
1412
Gilles Debunnec72fba82012-06-26 14:47:07 -07001413 // Don't leave us in the middle of a batch edit. Same as in onFocusChanged
1414 ensureEndedBatchEdit();
Richard Ledley5f2f8202018-02-05 14:55:47 +00001415
1416 ensureNoSelectionIfNonSelectable();
Gilles Debunned88876a2012-03-16 17:34:04 -07001417 }
1418 }
1419
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09001420 private void updateTapState(MotionEvent event) {
1421 final int action = event.getActionMasked();
1422 if (action == MotionEvent.ACTION_DOWN) {
1423 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
1424 // Detect double tap and triple click.
1425 if (((mTapState == TAP_STATE_FIRST_TAP)
1426 || ((mTapState == TAP_STATE_DOUBLE_TAP) && isMouse))
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001427 && (SystemClock.uptimeMillis() - mLastTouchUpTime)
1428 <= ViewConfiguration.getDoubleTapTimeout()) {
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09001429 if (mTapState == TAP_STATE_FIRST_TAP) {
1430 mTapState = TAP_STATE_DOUBLE_TAP;
1431 } else {
1432 mTapState = TAP_STATE_TRIPLE_CLICK;
1433 }
1434 } else {
1435 mTapState = TAP_STATE_FIRST_TAP;
1436 }
1437 }
1438 if (action == MotionEvent.ACTION_UP) {
1439 mLastTouchUpTime = SystemClock.uptimeMillis();
1440 }
1441 }
1442
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09001443 private boolean shouldFilterOutTouchEvent(MotionEvent event) {
1444 if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) {
1445 return false;
1446 }
1447 final boolean primaryButtonStateChanged =
1448 ((mLastButtonState ^ event.getButtonState()) & MotionEvent.BUTTON_PRIMARY) != 0;
1449 final int action = event.getActionMasked();
1450 if ((action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_UP)
1451 && !primaryButtonStateChanged) {
1452 return true;
1453 }
1454 if (action == MotionEvent.ACTION_MOVE
1455 && !event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
1456 return true;
1457 }
1458 return false;
1459 }
1460
Gilles Debunned88876a2012-03-16 17:34:04 -07001461 void onTouchEvent(MotionEvent event) {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09001462 final boolean filterOutEvent = shouldFilterOutTouchEvent(event);
1463 mLastButtonState = event.getButtonState();
1464 if (filterOutEvent) {
1465 if (event.getActionMasked() == MotionEvent.ACTION_UP) {
1466 mDiscardNextActionUp = true;
1467 }
1468 return;
1469 }
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09001470 updateTapState(event);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001471 updateFloatingToolbarVisibility(event);
1472
Gilles Debunned88876a2012-03-16 17:34:04 -07001473 if (hasSelectionController()) {
1474 getSelectionController().onTouchEvent(event);
1475 }
1476
1477 if (mShowSuggestionRunnable != null) {
1478 mTextView.removeCallbacks(mShowSuggestionRunnable);
1479 mShowSuggestionRunnable = null;
1480 }
1481
Petar Å egina91df3f92017-08-15 16:20:43 +01001482 if (event.getActionMasked() == MotionEvent.ACTION_UP) {
1483 mLastUpPositionX = event.getX();
1484 mLastUpPositionY = event.getY();
1485 }
1486
Gilles Debunned88876a2012-03-16 17:34:04 -07001487 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
1488 mLastDownPositionX = event.getX();
1489 mLastDownPositionY = event.getY();
1490
1491 // 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
Gilles Debunned88876a2012-03-16 17:34:04 -07001532 public void beginBatchEdit() {
1533 mInBatchEditControllers = true;
1534 final InputMethodState ims = mInputMethodState;
1535 if (ims != null) {
1536 int nesting = ++ims.mBatchEditNesting;
1537 if (nesting == 1) {
1538 ims.mCursorChanged = false;
1539 ims.mChangedDelta = 0;
1540 if (ims.mContentChanged) {
1541 // We already have a pending change from somewhere else,
1542 // so turn this into a full update.
1543 ims.mChangedStart = 0;
1544 ims.mChangedEnd = mTextView.getText().length();
1545 } else {
1546 ims.mChangedStart = EXTRACT_UNKNOWN;
1547 ims.mChangedEnd = EXTRACT_UNKNOWN;
1548 ims.mContentChanged = false;
1549 }
James Cook48e0fac2015-02-25 15:44:51 -08001550 mUndoInputFilter.beginBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001551 mTextView.onBeginBatchEdit();
1552 }
1553 }
1554 }
1555
1556 public void endBatchEdit() {
1557 mInBatchEditControllers = false;
1558 final InputMethodState ims = mInputMethodState;
1559 if (ims != null) {
1560 int nesting = --ims.mBatchEditNesting;
1561 if (nesting == 0) {
1562 finishBatchEdit(ims);
1563 }
1564 }
1565 }
1566
1567 void ensureEndedBatchEdit() {
1568 final InputMethodState ims = mInputMethodState;
1569 if (ims != null && ims.mBatchEditNesting != 0) {
1570 ims.mBatchEditNesting = 0;
1571 finishBatchEdit(ims);
1572 }
1573 }
1574
1575 void finishBatchEdit(final InputMethodState ims) {
1576 mTextView.onEndBatchEdit();
James Cook48e0fac2015-02-25 15:44:51 -08001577 mUndoInputFilter.endBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001578
1579 if (ims.mContentChanged || ims.mSelectionModeChanged) {
1580 mTextView.updateAfterEdit();
1581 reportExtractedText();
1582 } else if (ims.mCursorChanged) {
Jean Chalardc99d33f2013-02-28 16:39:47 -08001583 // Cheesy way to get us to report the current cursor location.
Gilles Debunned88876a2012-03-16 17:34:04 -07001584 mTextView.invalidateCursor();
1585 }
Jean Chalardc99d33f2013-02-28 16:39:47 -08001586 // sendUpdateSelection knows to avoid sending if the selection did
1587 // not actually change.
1588 sendUpdateSelection();
Keisuke Kuroyanagic6fad962016-05-02 15:11:41 +09001589
1590 // Show drag handles if they were blocked by batch edit mode.
1591 if (mTextActionMode != null) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001592 final CursorController cursorController = mTextView.hasSelection()
1593 ? getSelectionController() : getInsertionController();
Keisuke Kuroyanagic6fad962016-05-02 15:11:41 +09001594 if (cursorController != null && !cursorController.isActive()
1595 && !cursorController.isCursorBeingModified()) {
1596 cursorController.show();
1597 }
1598 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001599 }
1600
1601 static final int EXTRACT_NOTHING = -2;
1602 static final int EXTRACT_UNKNOWN = -1;
1603
1604 boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
1605 return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
1606 EXTRACT_UNKNOWN, outText);
1607 }
1608
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001609 private boolean extractTextInternal(@Nullable ExtractedTextRequest request,
Gilles Debunned88876a2012-03-16 17:34:04 -07001610 int partialStartOffset, int partialEndOffset, int delta,
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001611 @Nullable ExtractedText outText) {
1612 if (request == null || outText == null) {
1613 return false;
Gilles Debunned88876a2012-03-16 17:34:04 -07001614 }
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001615
1616 final CharSequence content = mTextView.getText();
1617 if (content == null) {
1618 return false;
1619 }
1620
1621 if (partialStartOffset != EXTRACT_NOTHING) {
1622 final int N = content.length();
1623 if (partialStartOffset < 0) {
1624 outText.partialStartOffset = outText.partialEndOffset = -1;
1625 partialStartOffset = 0;
1626 partialEndOffset = N;
1627 } else {
1628 // Now use the delta to determine the actual amount of text
1629 // we need.
1630 partialEndOffset += delta;
1631 // Adjust offsets to ensure we contain full spans.
1632 if (content instanceof Spanned) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001633 Spanned spanned = (Spanned) content;
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001634 Object[] spans = spanned.getSpans(partialStartOffset,
1635 partialEndOffset, ParcelableSpan.class);
1636 int i = spans.length;
1637 while (i > 0) {
1638 i--;
1639 int j = spanned.getSpanStart(spans[i]);
1640 if (j < partialStartOffset) partialStartOffset = j;
1641 j = spanned.getSpanEnd(spans[i]);
1642 if (j > partialEndOffset) partialEndOffset = j;
1643 }
1644 }
1645 outText.partialStartOffset = partialStartOffset;
1646 outText.partialEndOffset = partialEndOffset - delta;
1647
1648 if (partialStartOffset > N) {
1649 partialStartOffset = N;
1650 } else if (partialStartOffset < 0) {
1651 partialStartOffset = 0;
1652 }
1653 if (partialEndOffset > N) {
1654 partialEndOffset = N;
1655 } else if (partialEndOffset < 0) {
1656 partialEndOffset = 0;
1657 }
1658 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001659 if ((request.flags & InputConnection.GET_TEXT_WITH_STYLES) != 0) {
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001660 outText.text = content.subSequence(partialStartOffset,
1661 partialEndOffset);
1662 } else {
1663 outText.text = TextUtils.substring(content, partialStartOffset,
1664 partialEndOffset);
1665 }
1666 } else {
1667 outText.partialStartOffset = 0;
1668 outText.partialEndOffset = 0;
1669 outText.text = "";
1670 }
1671 outText.flags = 0;
1672 if (MetaKeyKeyListener.getMetaState(content, MetaKeyKeyListener.META_SELECTING) != 0) {
1673 outText.flags |= ExtractedText.FLAG_SELECTING;
1674 }
1675 if (mTextView.isSingleLine()) {
1676 outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
1677 }
1678 outText.startOffset = 0;
1679 outText.selectionStart = mTextView.getSelectionStart();
1680 outText.selectionEnd = mTextView.getSelectionEnd();
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001681 outText.hint = mTextView.getHint();
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001682 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001683 }
1684
1685 boolean reportExtractedText() {
1686 final Editor.InputMethodState ims = mInputMethodState;
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001687 if (ims == null) {
1688 return false;
1689 }
1690 ims.mSelectionModeChanged = false;
1691 final ExtractedTextRequest req = ims.mExtractedTextRequest;
1692 if (req == null) {
1693 return false;
1694 }
1695 final InputMethodManager imm = InputMethodManager.peekInstance();
1696 if (imm == null) {
1697 return false;
1698 }
1699 if (TextView.DEBUG_EXTRACT) {
1700 Log.v(TextView.LOG_TAG, "Retrieving extracted start="
1701 + ims.mChangedStart
1702 + " end=" + ims.mChangedEnd
1703 + " delta=" + ims.mChangedDelta);
1704 }
1705 if (ims.mChangedStart < 0 && !ims.mContentChanged) {
1706 ims.mChangedStart = EXTRACT_NOTHING;
1707 }
1708 if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
1709 ims.mChangedDelta, ims.mExtractedText)) {
1710 if (TextView.DEBUG_EXTRACT) {
1711 Log.v(TextView.LOG_TAG,
1712 "Reporting extracted start="
1713 + ims.mExtractedText.partialStartOffset
1714 + " end=" + ims.mExtractedText.partialEndOffset
1715 + ": " + ims.mExtractedText.text);
Gilles Debunned88876a2012-03-16 17:34:04 -07001716 }
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001717
1718 imm.updateExtractedText(mTextView, req.token, ims.mExtractedText);
1719 ims.mChangedStart = EXTRACT_UNKNOWN;
1720 ims.mChangedEnd = EXTRACT_UNKNOWN;
1721 ims.mChangedDelta = 0;
1722 ims.mContentChanged = false;
1723 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001724 }
1725 return false;
1726 }
1727
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001728 private void sendUpdateSelection() {
1729 if (null != mInputMethodState && mInputMethodState.mBatchEditNesting <= 0) {
1730 final InputMethodManager imm = InputMethodManager.peekInstance();
1731 if (null != imm) {
1732 final int selectionStart = mTextView.getSelectionStart();
1733 final int selectionEnd = mTextView.getSelectionEnd();
1734 int candStart = -1;
1735 int candEnd = -1;
1736 if (mTextView.getText() instanceof Spannable) {
1737 final Spannable sp = (Spannable) mTextView.getText();
1738 candStart = EditableInputConnection.getComposingSpanStart(sp);
1739 candEnd = EditableInputConnection.getComposingSpanEnd(sp);
1740 }
Jean Chalardc99d33f2013-02-28 16:39:47 -08001741 // InputMethodManager#updateSelection skips sending the message if
1742 // none of the parameters have changed since the last time we called it.
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001743 imm.updateSelection(mTextView,
1744 selectionStart, selectionEnd, candStart, candEnd);
1745 }
1746 }
1747 }
1748
Gilles Debunned88876a2012-03-16 17:34:04 -07001749 void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint,
1750 int cursorOffsetVertical) {
1751 final int selectionStart = mTextView.getSelectionStart();
1752 final int selectionEnd = mTextView.getSelectionEnd();
1753
1754 final InputMethodState ims = mInputMethodState;
1755 if (ims != null && ims.mBatchEditNesting == 0) {
1756 InputMethodManager imm = InputMethodManager.peekInstance();
1757 if (imm != null) {
1758 if (imm.isActive(mTextView)) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001759 if (ims.mContentChanged || ims.mSelectionModeChanged) {
1760 // We are in extract mode and the content has changed
1761 // in some way... just report complete new text to the
1762 // input method.
Yohei Yukawab6bec1a2015-05-01 16:18:25 -07001763 reportExtractedText();
Gilles Debunned88876a2012-03-16 17:34:04 -07001764 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001765 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001766 }
1767 }
1768
1769 if (mCorrectionHighlighter != null) {
1770 mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
1771 }
1772
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07001773 if (highlight != null && selectionStart == selectionEnd && mDrawableForCursor != null) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001774 drawCursor(canvas, cursorOffsetVertical);
1775 // Rely on the drawable entirely, do not draw the cursor line.
1776 // Has to be done after the IMM related code above which relies on the highlight.
1777 highlight = null;
1778 }
1779
Jan Althaus80620c52018-02-02 17:39:22 +01001780 if (mSelectionActionModeHelper != null) {
1781 mSelectionActionModeHelper.onDraw(canvas);
1782 if (mSelectionActionModeHelper.isDrawingHighlight()) {
1783 highlight = null;
1784 }
1785 }
1786
Gilles Debunned88876a2012-03-16 17:34:04 -07001787 if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
1788 drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
1789 cursorOffsetVertical);
1790 } else {
1791 layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
1792 }
1793 }
1794
1795 private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
1796 Paint highlightPaint, int cursorOffsetVertical) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001797 final long lineRange = layout.getLineRangeForDraw(canvas);
1798 int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
1799 int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
1800 if (lastLine < 0) return;
1801
1802 layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
1803 firstLine, lastLine);
1804
1805 if (layout instanceof DynamicLayout) {
Chris Craik956f3402015-04-27 16:41:00 -07001806 if (mTextRenderNodes == null) {
1807 mTextRenderNodes = ArrayUtils.emptyArray(TextRenderNode.class);
Gilles Debunned88876a2012-03-16 17:34:04 -07001808 }
1809
1810 DynamicLayout dynamicLayout = (DynamicLayout) layout;
Gilles Debunne157aafc2012-04-19 17:21:57 -07001811 int[] blockEndLines = dynamicLayout.getBlockEndLines();
Gilles Debunned88876a2012-03-16 17:34:04 -07001812 int[] blockIndices = dynamicLayout.getBlockIndices();
1813 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
Sangkyu Lee955beb22012-12-10 15:47:00 +09001814 final int indexFirstChangedBlock = dynamicLayout.getIndexFirstChangedBlock();
Gilles Debunned88876a2012-03-16 17:34:04 -07001815
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001816 final ArraySet<Integer> blockSet = dynamicLayout.getBlocksAlwaysNeedToBeRedrawn();
1817 if (blockSet != null) {
1818 for (int i = 0; i < blockSet.size(); i++) {
1819 final int blockIndex = dynamicLayout.getBlockIndex(blockSet.valueAt(i));
1820 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
1821 && mTextRenderNodes[blockIndex] != null) {
1822 mTextRenderNodes[blockIndex].needsToBeShifted = true;
1823 }
1824 }
1825 }
1826
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001827 int startBlock = Arrays.binarySearch(blockEndLines, 0, numberOfBlocks, firstLine);
1828 if (startBlock < 0) {
1829 startBlock = -(startBlock + 1);
1830 }
1831 startBlock = Math.min(indexFirstChangedBlock, startBlock);
Gilles Debunned88876a2012-03-16 17:34:04 -07001832
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001833 int startIndexToFindAvailableRenderNode = 0;
1834 int lastIndex = numberOfBlocks;
1835
1836 for (int i = startBlock; i < numberOfBlocks; i++) {
1837 final int blockIndex = blockIndices[i];
1838 if (i >= indexFirstChangedBlock
1839 && blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
1840 && mTextRenderNodes[blockIndex] != null) {
1841 mTextRenderNodes[blockIndex].needsToBeShifted = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001842 }
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001843 if (blockEndLines[i] < firstLine) {
1844 // Blocks in [indexFirstChangedBlock, firstLine) are not redrawn here. They will
1845 // be redrawn after they get scrolled into drawing range.
1846 continue;
Gilles Debunned88876a2012-03-16 17:34:04 -07001847 }
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001848 startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas, layout,
1849 highlight, highlightPaint, cursorOffsetVertical, blockEndLines,
1850 blockIndices, i, numberOfBlocks, startIndexToFindAvailableRenderNode);
1851 if (blockEndLines[i] >= lastLine) {
1852 lastIndex = Math.max(indexFirstChangedBlock, i + 1);
1853 break;
Gilles Debunned88876a2012-03-16 17:34:04 -07001854 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001855 }
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001856 if (blockSet != null) {
1857 for (int i = 0; i < blockSet.size(); i++) {
1858 final int block = blockSet.valueAt(i);
1859 final int blockIndex = dynamicLayout.getBlockIndex(block);
1860 if (blockIndex == DynamicLayout.INVALID_BLOCK_INDEX
1861 || mTextRenderNodes[blockIndex] == null
1862 || mTextRenderNodes[blockIndex].needsToBeShifted) {
1863 startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas,
1864 layout, highlight, highlightPaint, cursorOffsetVertical,
1865 blockEndLines, blockIndices, block, numberOfBlocks,
1866 startIndexToFindAvailableRenderNode);
1867 }
1868 }
1869 }
Sangkyu Lee955beb22012-12-10 15:47:00 +09001870
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001871 dynamicLayout.setIndexFirstChangedBlock(lastIndex);
Gilles Debunned88876a2012-03-16 17:34:04 -07001872 } else {
1873 // Boring layout is used for empty and hint text
1874 layout.drawText(canvas, firstLine, lastLine);
1875 }
1876 }
1877
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001878 private int drawHardwareAcceleratedInner(Canvas canvas, Layout layout, Path highlight,
1879 Paint highlightPaint, int cursorOffsetVertical, int[] blockEndLines,
1880 int[] blockIndices, int blockInfoIndex, int numberOfBlocks,
1881 int startIndexToFindAvailableRenderNode) {
1882 final int blockEndLine = blockEndLines[blockInfoIndex];
1883 int blockIndex = blockIndices[blockInfoIndex];
1884
1885 final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
1886 if (blockIsInvalid) {
1887 blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
1888 startIndexToFindAvailableRenderNode);
1889 // Note how dynamic layout's internal block indices get updated from Editor
1890 blockIndices[blockInfoIndex] = blockIndex;
1891 if (mTextRenderNodes[blockIndex] != null) {
1892 mTextRenderNodes[blockIndex].isDirty = true;
1893 }
1894 startIndexToFindAvailableRenderNode = blockIndex + 1;
1895 }
1896
1897 if (mTextRenderNodes[blockIndex] == null) {
1898 mTextRenderNodes[blockIndex] = new TextRenderNode("Text " + blockIndex);
1899 }
1900
1901 final boolean blockDisplayListIsInvalid = mTextRenderNodes[blockIndex].needsRecord();
1902 RenderNode blockDisplayList = mTextRenderNodes[blockIndex].renderNode;
1903 if (mTextRenderNodes[blockIndex].needsToBeShifted || blockDisplayListIsInvalid) {
1904 final int blockBeginLine = blockInfoIndex == 0 ?
1905 0 : blockEndLines[blockInfoIndex - 1] + 1;
1906 final int top = layout.getLineTop(blockBeginLine);
1907 final int bottom = layout.getLineBottom(blockEndLine);
1908 int left = 0;
1909 int right = mTextView.getWidth();
1910 if (mTextView.getHorizontallyScrolling()) {
1911 float min = Float.MAX_VALUE;
1912 float max = Float.MIN_VALUE;
1913 for (int line = blockBeginLine; line <= blockEndLine; line++) {
1914 min = Math.min(min, layout.getLineLeft(line));
1915 max = Math.max(max, layout.getLineRight(line));
1916 }
1917 left = (int) min;
1918 right = (int) (max + 0.5f);
1919 }
1920
1921 // Rebuild display list if it is invalid
1922 if (blockDisplayListIsInvalid) {
1923 final DisplayListCanvas displayListCanvas = blockDisplayList.start(
1924 right - left, bottom - top);
1925 try {
1926 // drawText is always relative to TextView's origin, this translation
1927 // brings this range of text back to the top left corner of the viewport
1928 displayListCanvas.translate(-left, -top);
1929 layout.drawText(displayListCanvas, blockBeginLine, blockEndLine);
1930 mTextRenderNodes[blockIndex].isDirty = false;
1931 // No need to untranslate, previous context is popped after
1932 // drawDisplayList
1933 } finally {
1934 blockDisplayList.end(displayListCanvas);
1935 // Same as drawDisplayList below, handled by our TextView's parent
1936 blockDisplayList.setClipToBounds(false);
1937 }
1938 }
1939
1940 // Valid display list only needs to update its drawing location.
1941 blockDisplayList.setLeftTopRightBottom(left, top, right, bottom);
1942 mTextRenderNodes[blockIndex].needsToBeShifted = false;
1943 }
1944 ((DisplayListCanvas) canvas).drawRenderNode(blockDisplayList);
1945 return startIndexToFindAvailableRenderNode;
1946 }
1947
Gilles Debunned88876a2012-03-16 17:34:04 -07001948 private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
1949 int searchStartIndex) {
Chris Craik956f3402015-04-27 16:41:00 -07001950 int length = mTextRenderNodes.length;
Gilles Debunned88876a2012-03-16 17:34:04 -07001951 for (int i = searchStartIndex; i < length; i++) {
1952 boolean blockIndexFound = false;
1953 for (int j = 0; j < numberOfBlocks; j++) {
1954 if (blockIndices[j] == i) {
1955 blockIndexFound = true;
1956 break;
1957 }
1958 }
1959 if (blockIndexFound) continue;
1960 return i;
1961 }
1962
1963 // No available index found, the pool has to grow
Chris Craik956f3402015-04-27 16:41:00 -07001964 mTextRenderNodes = GrowingArrayUtils.append(mTextRenderNodes, length, null);
Gilles Debunned88876a2012-03-16 17:34:04 -07001965 return length;
1966 }
1967
1968 private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
1969 final boolean translate = cursorOffsetVertical != 0;
1970 if (translate) canvas.translate(0, cursorOffsetVertical);
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07001971 if (mDrawableForCursor != null) {
1972 mDrawableForCursor.draw(canvas);
Gilles Debunned88876a2012-03-16 17:34:04 -07001973 }
1974 if (translate) canvas.translate(0, -cursorOffsetVertical);
1975 }
1976
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09001977 void invalidateHandlesAndActionMode() {
1978 if (mSelectionModifierCursorController != null) {
1979 mSelectionModifierCursorController.invalidateHandles();
1980 }
1981 if (mInsertionPointCursorController != null) {
1982 mInsertionPointCursorController.invalidateHandle();
1983 }
1984 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01001985 invalidateActionMode();
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09001986 }
1987 }
1988
Gilles Debunneebc86af2012-04-20 15:10:47 -07001989 /**
1990 * Invalidates all the sub-display lists that overlap the specified character range
1991 */
1992 void invalidateTextDisplayList(Layout layout, int start, int end) {
Chris Craik956f3402015-04-27 16:41:00 -07001993 if (mTextRenderNodes != null && layout instanceof DynamicLayout) {
Gilles Debunneebc86af2012-04-20 15:10:47 -07001994 final int firstLine = layout.getLineForOffset(start);
1995 final int lastLine = layout.getLineForOffset(end);
1996
1997 DynamicLayout dynamicLayout = (DynamicLayout) layout;
1998 int[] blockEndLines = dynamicLayout.getBlockEndLines();
1999 int[] blockIndices = dynamicLayout.getBlockIndices();
2000 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
2001
2002 int i = 0;
2003 // Skip the blocks before firstLine
2004 while (i < numberOfBlocks) {
2005 if (blockEndLines[i] >= firstLine) break;
2006 i++;
2007 }
2008
2009 // Invalidate all subsequent blocks until lastLine is passed
2010 while (i < numberOfBlocks) {
2011 final int blockIndex = blockIndices[i];
2012 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX) {
Chris Craik956f3402015-04-27 16:41:00 -07002013 mTextRenderNodes[blockIndex].isDirty = true;
Gilles Debunneebc86af2012-04-20 15:10:47 -07002014 }
2015 if (blockEndLines[i] >= lastLine) break;
2016 i++;
2017 }
2018 }
2019 }
2020
Gilles Debunned88876a2012-03-16 17:34:04 -07002021 void invalidateTextDisplayList() {
Chris Craik956f3402015-04-27 16:41:00 -07002022 if (mTextRenderNodes != null) {
2023 for (int i = 0; i < mTextRenderNodes.length; i++) {
2024 if (mTextRenderNodes[i] != null) mTextRenderNodes[i].isDirty = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07002025 }
2026 }
2027 }
2028
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002029 void updateCursorPosition() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002030 if (mTextView.mCursorDrawableRes == 0) {
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07002031 mDrawableForCursor = null;
Gilles Debunned88876a2012-03-16 17:34:04 -07002032 return;
2033 }
2034
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002035 final Layout layout = mTextView.getLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -07002036 final int offset = mTextView.getSelectionStart();
2037 final int line = layout.getLineForOffset(offset);
2038 final int top = layout.getLineTop(line);
Siyamed Sinira60b59d2017-07-26 09:26:41 -07002039 final int bottom = layout.getLineBottomWithoutSpacing(line);
Gilles Debunned88876a2012-03-16 17:34:04 -07002040
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002041 final boolean clamped = layout.shouldClampCursor(line);
2042 updateCursorPosition(top, bottom, layout.getPrimaryHorizontal(offset, clamped));
Gilles Debunned88876a2012-03-16 17:34:04 -07002043 }
2044
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002045 void refreshTextActionMode() {
2046 if (extractedTextModeWillBeStarted()) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002047 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002048 return;
2049 }
2050 final boolean hasSelection = mTextView.hasSelection();
2051 final SelectionModifierCursorController selectionController = getSelectionController();
2052 final InsertionPointCursorController insertionController = getInsertionController();
2053 if ((selectionController != null && selectionController.isCursorBeingModified())
2054 || (insertionController != null && insertionController.isCursorBeingModified())) {
2055 // ActionMode should be managed by the currently active cursor controller.
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002056 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002057 return;
2058 }
2059 if (hasSelection) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002060 hideInsertionPointCursorController();
2061 if (mTextActionMode == null) {
Keisuke Kuroyanagi0fd28c92016-04-04 17:43:06 +09002062 if (mRestartActionModeOnNextRefresh) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002063 // To avoid distraction, newly start action mode only when selection action
Keisuke Kuroyanagi0fd28c92016-04-04 17:43:06 +09002064 // mode is being restarted.
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002065 startSelectionActionModeAsync(false);
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002066 }
2067 } else if (selectionController == null || !selectionController.isActive()) {
2068 // Insertion action mode is active. Avoid dismissing the selection.
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002069 stopTextActionModeWithPreservingSelection();
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002070 startSelectionActionModeAsync(false);
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002071 } else {
2072 mTextActionMode.invalidateContentRect();
2073 }
2074 } else {
2075 // Insertion action mode is started only when insertion controller is explicitly
2076 // activated.
2077 if (insertionController == null || !insertionController.isActive()) {
2078 stopTextActionMode();
2079 } else if (mTextActionMode != null) {
2080 mTextActionMode.invalidateContentRect();
2081 }
2082 }
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002083 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002084 }
2085
Gilles Debunned88876a2012-03-16 17:34:04 -07002086 /**
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002087 * Start an Insertion action mode.
Gilles Debunned88876a2012-03-16 17:34:04 -07002088 */
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002089 void startInsertionActionMode() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002090 if (mInsertionActionModeRunnable != null) {
2091 mTextView.removeCallbacks(mInsertionActionModeRunnable);
2092 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002093 if (extractedTextModeWillBeStarted()) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002094 return;
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002095 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002096 stopTextActionMode();
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002097
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002098 ActionMode.Callback actionModeCallback =
Richard Ledley26b87222017-11-30 10:54:08 +00002099 new TextActionModeCallback(TextActionMode.INSERTION);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002100 mTextActionMode = mTextView.startActionMode(
Clara Bayarrib8ed5b72015-04-09 15:26:41 +01002101 actionModeCallback, ActionMode.TYPE_FLOATING);
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002102 if (mTextActionMode != null && getInsertionController() != null) {
2103 getInsertionController().show();
2104 }
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00002105 }
2106
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002107 @NonNull
2108 TextView getTextView() {
2109 return mTextView;
2110 }
2111
2112 @Nullable
2113 ActionMode getTextActionMode() {
2114 return mTextActionMode;
2115 }
2116
2117 void setRestartActionModeOnNextRefresh(boolean value) {
2118 mRestartActionModeOnNextRefresh = value;
2119 }
2120
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00002121 /**
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002122 * Asynchronously starts a selection action mode using the TextClassifier.
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00002123 */
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002124 void startSelectionActionModeAsync(boolean adjustSelection) {
Richard Ledley26b87222017-11-30 10:54:08 +00002125 getSelectionActionModeHelper().startSelectionActionModeAsync(adjustSelection);
2126 }
2127
Richard Ledley27db81b2018-03-01 12:34:55 +00002128 void startLinkActionModeAsync(int start, int end) {
Richard Ledley26b87222017-11-30 10:54:08 +00002129 if (!(mTextView.getText() instanceof Spannable)) {
2130 return;
2131 }
Richard Ledley26b87222017-11-30 10:54:08 +00002132 stopTextActionMode();
Abodunrinwa Toki52096912018-03-21 23:14:42 +00002133 mRequestingLinkActionMode = true;
Richard Ledley27db81b2018-03-01 12:34:55 +00002134 getSelectionActionModeHelper().startLinkActionModeAsync(start, end);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002135 }
2136
2137 /**
2138 * Asynchronously invalidates an action mode using the TextClassifier.
2139 */
Abodunrinwa Toki4ce651e2017-05-12 15:37:29 +01002140 void invalidateActionModeAsync() {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002141 getSelectionActionModeHelper().invalidateActionModeAsync();
2142 }
2143
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002144 /**
2145 * Synchronously invalidates an action mode without the TextClassifier.
2146 */
2147 private void invalidateActionMode() {
2148 if (mTextActionMode != null) {
2149 mTextActionMode.invalidate();
2150 }
2151 }
2152
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002153 private SelectionActionModeHelper getSelectionActionModeHelper() {
2154 if (mSelectionActionModeHelper == null) {
2155 mSelectionActionModeHelper = new SelectionActionModeHelper(this);
Clara Bayarri578286f2015-04-10 15:35:31 +01002156 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002157 return mSelectionActionModeHelper;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00002158 }
2159
Clara Bayarridfac4432015-05-15 12:18:24 +01002160 /**
2161 * If the TextView allows text selection, selects the current word when no existing selection
2162 * was available and starts a drag.
2163 *
2164 * @return true if the drag was started.
2165 */
2166 private boolean selectCurrentWordAndStartDrag() {
Clara Bayarri7184c8a2015-06-05 17:34:09 +01002167 if (mInsertionActionModeRunnable != null) {
2168 mTextView.removeCallbacks(mInsertionActionModeRunnable);
2169 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002170 if (extractedTextModeWillBeStarted()) {
Clara Bayarridfac4432015-05-15 12:18:24 +01002171 return false;
2172 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002173 if (!checkField()) {
Clara Bayarridfac4432015-05-15 12:18:24 +01002174 return false;
2175 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002176 if (!mTextView.hasSelection() && !selectCurrentWord()) {
2177 // No selection and cannot select a word.
2178 return false;
2179 }
2180 stopTextActionModeWithPreservingSelection();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08002181 getSelectionController().enterDrag(
2182 SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_WORD);
Clara Bayarridfac4432015-05-15 12:18:24 +01002183 return true;
2184 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002185
Clara Bayarridfac4432015-05-15 12:18:24 +01002186 /**
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002187 * Checks whether a selection can be performed on the current TextView.
Clara Bayarridfac4432015-05-15 12:18:24 +01002188 *
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002189 * @return true if a selection can be performed
Clara Bayarridfac4432015-05-15 12:18:24 +01002190 */
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002191 boolean checkField() {
Clara Bayarridfac4432015-05-15 12:18:24 +01002192 if (!mTextView.canSelectText() || !mTextView.requestFocus()) {
2193 Log.w(TextView.LOG_TAG,
2194 "TextView does not support text selection. Selection cancelled.");
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002195 return false;
2196 }
Clara Bayarridfac4432015-05-15 12:18:24 +01002197 return true;
2198 }
2199
Richard Ledley26b87222017-11-30 10:54:08 +00002200 boolean startActionModeInternal(@TextActionMode int actionMode) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002201 if (extractedTextModeWillBeStarted()) {
2202 return false;
2203 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002204 if (mTextActionMode != null) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002205 // Text action mode is already started
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002206 invalidateActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07002207 return false;
2208 }
2209
Richard Ledley724eff92017-12-21 10:11:34 +00002210 if (actionMode != TextActionMode.TEXT_LINK
2211 && (!checkField() || !mTextView.hasSelection())) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002212 return false;
2213 }
2214
Richard Ledley26b87222017-11-30 10:54:08 +00002215 ActionMode.Callback actionModeCallback = new TextActionModeCallback(actionMode);
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002216 mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING);
Gilles Debunned88876a2012-03-16 17:34:04 -07002217
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002218 final boolean selectionStarted = mTextActionMode != null;
Abodunrinwa Toki52096912018-03-21 23:14:42 +00002219 if (selectionStarted
2220 && mTextView.isTextEditable() && !mTextView.isTextSelectable()
2221 && mShowSoftInputOnFocus) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002222 // Show the IME to be able to replace text, except when selecting non editable text.
2223 final InputMethodManager imm = InputMethodManager.peekInstance();
2224 if (imm != null) {
2225 imm.showSoftInput(mTextView, 0, null);
2226 }
2227 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002228 return selectionStarted;
2229 }
2230
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09002231 private boolean extractedTextModeWillBeStarted() {
Andrei Stingaceanub1891b32015-06-19 16:44:37 +01002232 if (!(mTextView.isInExtractedMode())) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002233 final InputMethodManager imm = InputMethodManager.peekInstance();
2234 return imm != null && imm.isFullscreenMode();
2235 }
2236 return false;
2237 }
2238
2239 /**
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002240 * @return <code>true</code> if it's reasonable to offer to show suggestions depending on
2241 * the current cursor position or selection range. This method is consistent with the
2242 * method to show suggestions {@link SuggestionsPopupWindow#updateSuggestions}.
Gilles Debunned88876a2012-03-16 17:34:04 -07002243 */
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002244 private boolean shouldOfferToShowSuggestions() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002245 CharSequence text = mTextView.getText();
2246 if (!(text instanceof Spannable)) return false;
2247
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002248 final Spannable spannable = (Spannable) text;
2249 final int selectionStart = mTextView.getSelectionStart();
2250 final int selectionEnd = mTextView.getSelectionEnd();
2251 final SuggestionSpan[] suggestionSpans = spannable.getSpans(selectionStart, selectionEnd,
2252 SuggestionSpan.class);
2253 if (suggestionSpans.length == 0) {
2254 return false;
2255 }
2256 if (selectionStart == selectionEnd) {
2257 // Spans overlap the cursor.
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002258 for (int i = 0; i < suggestionSpans.length; i++) {
2259 if (suggestionSpans[i].getSuggestions().length > 0) {
2260 return true;
2261 }
2262 }
2263 return false;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002264 }
2265 int minSpanStart = mTextView.getText().length();
2266 int maxSpanEnd = 0;
2267 int unionOfSpansCoveringSelectionStartStart = mTextView.getText().length();
2268 int unionOfSpansCoveringSelectionStartEnd = 0;
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002269 boolean hasValidSuggestions = false;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002270 for (int i = 0; i < suggestionSpans.length; i++) {
2271 final int spanStart = spannable.getSpanStart(suggestionSpans[i]);
2272 final int spanEnd = spannable.getSpanEnd(suggestionSpans[i]);
2273 minSpanStart = Math.min(minSpanStart, spanStart);
2274 maxSpanEnd = Math.max(maxSpanEnd, spanEnd);
2275 if (selectionStart < spanStart || selectionStart > spanEnd) {
2276 // The span doesn't cover the current selection start point.
2277 continue;
2278 }
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002279 hasValidSuggestions =
2280 hasValidSuggestions || suggestionSpans[i].getSuggestions().length > 0;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002281 unionOfSpansCoveringSelectionStartStart =
2282 Math.min(unionOfSpansCoveringSelectionStartStart, spanStart);
2283 unionOfSpansCoveringSelectionStartEnd =
2284 Math.max(unionOfSpansCoveringSelectionStartEnd, spanEnd);
2285 }
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002286 if (!hasValidSuggestions) {
2287 return false;
2288 }
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002289 if (unionOfSpansCoveringSelectionStartStart >= unionOfSpansCoveringSelectionStartEnd) {
2290 // No spans cover the selection start point.
2291 return false;
2292 }
2293 if (minSpanStart < unionOfSpansCoveringSelectionStartStart
2294 || maxSpanEnd > unionOfSpansCoveringSelectionStartEnd) {
2295 // There is a span that is not covered by the union. In this case, we soouldn't offer
2296 // to show suggestions as it's confusing.
2297 return false;
2298 }
2299 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07002300 }
2301
2302 /**
2303 * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
2304 * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
2305 */
2306 private boolean isCursorInsideEasyCorrectionSpan() {
2307 Spannable spannable = (Spannable) mTextView.getText();
2308 SuggestionSpan[] suggestionSpans = spannable.getSpans(mTextView.getSelectionStart(),
2309 mTextView.getSelectionEnd(), SuggestionSpan.class);
2310 for (int i = 0; i < suggestionSpans.length; i++) {
2311 if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
2312 return true;
2313 }
2314 }
2315 return false;
2316 }
2317
2318 void onTouchUpEvent(MotionEvent event) {
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +01002319 if (getSelectionActionModeHelper().resetSelection(
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +00002320 getTextView().getOffsetForPosition(event.getX(), event.getY()))) {
2321 return;
2322 }
2323
Gilles Debunned88876a2012-03-16 17:34:04 -07002324 boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect();
Mady Mellora2861452015-06-25 08:40:27 -07002325 hideCursorAndSpanControllers();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002326 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07002327 CharSequence text = mTextView.getText();
2328 if (!selectAllGotFocus && text.length() > 0) {
2329 // Move cursor
2330 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
Abodunrinwa Toki52096912018-03-21 23:14:42 +00002331
2332 final boolean shouldInsertCursor = !mRequestingLinkActionMode;
2333 if (shouldInsertCursor) {
2334 Selection.setSelection((Spannable) text, offset);
2335 if (mSpellChecker != null) {
2336 // When the cursor moves, the word that was typed may need spell check
2337 mSpellChecker.onSelectionChanged();
2338 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002339 }
Andrei Stingaceanu35c550c2015-05-07 16:49:49 +01002340
Gilles Debunned88876a2012-03-16 17:34:04 -07002341 if (!extractedTextModeWillBeStarted()) {
2342 if (isCursorInsideEasyCorrectionSpan()) {
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01002343 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002344 if (mInsertionActionModeRunnable != null) {
2345 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01002346 }
2347
Abodunrinwa Toki52096912018-03-21 23:14:42 +00002348 mShowSuggestionRunnable = this::replace;
2349
Gilles Debunned88876a2012-03-16 17:34:04 -07002350 // removeCallbacks is performed on every touch
2351 mTextView.postDelayed(mShowSuggestionRunnable,
2352 ViewConfiguration.getDoubleTapTimeout());
2353 } else if (hasInsertionController()) {
Abodunrinwa Toki52096912018-03-21 23:14:42 +00002354 if (shouldInsertCursor) {
2355 getInsertionController().show();
2356 } else {
2357 getInsertionController().hide();
2358 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002359 }
2360 }
2361 }
2362 }
2363
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002364 protected void stopTextActionMode() {
2365 if (mTextActionMode != null) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002366 // This will hide the mSelectionModifierCursorController
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002367 mTextActionMode.finish();
Gilles Debunned88876a2012-03-16 17:34:04 -07002368 }
2369 }
2370
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002371 private void stopTextActionModeWithPreservingSelection() {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002372 if (mTextActionMode != null) {
2373 mRestartActionModeOnNextRefresh = true;
2374 }
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002375 mPreserveSelection = true;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002376 stopTextActionMode();
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002377 mPreserveSelection = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002378 }
2379
Gilles Debunned88876a2012-03-16 17:34:04 -07002380 /**
2381 * @return True if this view supports insertion handles.
2382 */
2383 boolean hasInsertionController() {
2384 return mInsertionControllerEnabled;
2385 }
2386
2387 /**
2388 * @return True if this view supports selection handles.
2389 */
2390 boolean hasSelectionController() {
2391 return mSelectionControllerEnabled;
2392 }
2393
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09002394 private InsertionPointCursorController getInsertionController() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002395 if (!mInsertionControllerEnabled) {
2396 return null;
2397 }
2398
2399 if (mInsertionPointCursorController == null) {
2400 mInsertionPointCursorController = new InsertionPointCursorController();
2401
2402 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
2403 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
2404 }
2405
2406 return mInsertionPointCursorController;
2407 }
2408
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002409 @Nullable
2410 SelectionModifierCursorController getSelectionController() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002411 if (!mSelectionControllerEnabled) {
2412 return null;
2413 }
2414
2415 if (mSelectionModifierCursorController == null) {
2416 mSelectionModifierCursorController = new SelectionModifierCursorController();
2417
2418 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
2419 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
2420 }
2421
2422 return mSelectionModifierCursorController;
2423 }
2424
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002425 @VisibleForTesting
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002426 @Nullable
2427 public Drawable getCursorDrawable() {
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07002428 return mDrawableForCursor;
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002429 }
2430
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002431 private void updateCursorPosition(int top, int bottom, float horizontal) {
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07002432 if (mDrawableForCursor == null) {
2433 mDrawableForCursor = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07002434 mTextView.mCursorDrawableRes);
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002435 }
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07002436 final int left = clampHorizontalPosition(mDrawableForCursor, horizontal);
2437 final int width = mDrawableForCursor.getIntrinsicWidth();
2438 mDrawableForCursor.setBounds(left, top - mTempRect.top, left + width,
Gilles Debunned88876a2012-03-16 17:34:04 -07002439 bottom + mTempRect.bottom);
2440 }
2441
2442 /**
Siyamed Sinir987ec652016-02-17 19:44:41 -08002443 * Return clamped position for the drawable. If the drawable is within the boundaries of the
2444 * 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 -08002445 * 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 -08002446 * the view boundary. If the drawable is null, horizontal parameter is aligned to left or right
2447 * of the view.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002448 *
Siyamed Sinir987ec652016-02-17 19:44:41 -08002449 * @param drawable Drawable. Can be null.
2450 * @param horizontal Horizontal position for the drawable.
2451 * @return The clamped horizontal position for the drawable.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002452 */
Siyamed Sinir987ec652016-02-17 19:44:41 -08002453 private int clampHorizontalPosition(@Nullable final Drawable drawable, float horizontal) {
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002454 horizontal = Math.max(0.5f, horizontal - 0.5f);
2455 if (mTempRect == null) mTempRect = new Rect();
Siyamed Sinir987ec652016-02-17 19:44:41 -08002456
2457 int drawableWidth = 0;
2458 if (drawable != null) {
2459 drawable.getPadding(mTempRect);
2460 drawableWidth = drawable.getIntrinsicWidth();
2461 } else {
2462 mTempRect.setEmpty();
2463 }
2464
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002465 int scrollX = mTextView.getScrollX();
2466 float horizontalDiff = horizontal - scrollX;
2467 int viewClippedWidth = mTextView.getWidth() - mTextView.getCompoundPaddingLeft()
2468 - mTextView.getCompoundPaddingRight();
2469
2470 final int left;
2471 if (horizontalDiff >= (viewClippedWidth - 1f)) {
2472 // at the rightmost position
Siyamed Sinir987ec652016-02-17 19:44:41 -08002473 left = viewClippedWidth + scrollX - (drawableWidth - mTempRect.right);
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002474 } else if (Math.abs(horizontalDiff) <= 1f
2475 || (TextUtils.isEmpty(mTextView.getText())
Siyamed Sinir987ec652016-02-17 19:44:41 -08002476 && (TextView.VERY_WIDE - scrollX) <= (viewClippedWidth + 1f)
2477 && horizontal <= 1f)) {
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002478 // at the leftmost position
2479 left = scrollX - mTempRect.left;
2480 } else {
2481 left = (int) horizontal - mTempRect.left;
2482 }
2483 return left;
2484 }
2485
2486 /**
Gilles Debunned88876a2012-03-16 17:34:04 -07002487 * 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 -08002488 * a dictionary) from the current input method, provided by it calling
Gilles Debunned88876a2012-03-16 17:34:04 -07002489 * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
2490 * implementation flashes the background of the corrected word to provide feedback to the user.
2491 *
2492 * @param info The auto correct info about the text that was corrected.
2493 */
2494 public void onCommitCorrection(CorrectionInfo info) {
2495 if (mCorrectionHighlighter == null) {
2496 mCorrectionHighlighter = new CorrectionHighlighter();
2497 } else {
2498 mCorrectionHighlighter.invalidate(false);
2499 }
2500
2501 mCorrectionHighlighter.highlight(info);
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002502 mUndoInputFilter.freezeLastEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07002503 }
2504
Gilles Debunned88876a2012-03-16 17:34:04 -07002505 void onScrollChanged() {
Gilles Debunne157aafc2012-04-19 17:21:57 -07002506 if (mPositionListener != null) {
2507 mPositionListener.onScrollChanged();
2508 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002509 if (mTextActionMode != null) {
2510 mTextActionMode.invalidateContentRect();
Abodunrinwa Toki56195db2015-04-22 06:46:54 +01002511 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002512 }
2513
2514 /**
2515 * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
2516 */
2517 private boolean shouldBlink() {
2518 if (!isCursorVisible() || !mTextView.isFocused()) return false;
2519
2520 final int start = mTextView.getSelectionStart();
2521 if (start < 0) return false;
2522
2523 final int end = mTextView.getSelectionEnd();
2524 if (end < 0) return false;
2525
2526 return start == end;
2527 }
2528
2529 void makeBlink() {
2530 if (shouldBlink()) {
2531 mShowCursor = SystemClock.uptimeMillis();
2532 if (mBlink == null) mBlink = new Blink();
John Reckd0374c62015-10-20 13:25:01 -07002533 mTextView.removeCallbacks(mBlink);
2534 mTextView.postDelayed(mBlink, BLINK);
Gilles Debunned88876a2012-03-16 17:34:04 -07002535 } else {
John Reckd0374c62015-10-20 13:25:01 -07002536 if (mBlink != null) mTextView.removeCallbacks(mBlink);
Gilles Debunned88876a2012-03-16 17:34:04 -07002537 }
2538 }
2539
John Reckd0374c62015-10-20 13:25:01 -07002540 private class Blink implements Runnable {
Gilles Debunned88876a2012-03-16 17:34:04 -07002541 private boolean mCancelled;
2542
2543 public void run() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002544 if (mCancelled) {
2545 return;
2546 }
2547
John Reckd0374c62015-10-20 13:25:01 -07002548 mTextView.removeCallbacks(this);
Gilles Debunned88876a2012-03-16 17:34:04 -07002549
2550 if (shouldBlink()) {
2551 if (mTextView.getLayout() != null) {
2552 mTextView.invalidateCursorPath();
2553 }
2554
John Reckd0374c62015-10-20 13:25:01 -07002555 mTextView.postDelayed(this, BLINK);
Gilles Debunned88876a2012-03-16 17:34:04 -07002556 }
2557 }
2558
2559 void cancel() {
2560 if (!mCancelled) {
John Reckd0374c62015-10-20 13:25:01 -07002561 mTextView.removeCallbacks(this);
Gilles Debunned88876a2012-03-16 17:34:04 -07002562 mCancelled = true;
2563 }
2564 }
2565
2566 void uncancel() {
2567 mCancelled = false;
2568 }
2569 }
2570
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002571 private DragShadowBuilder getTextThumbnailBuilder(int start, int end) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002572 TextView shadowView = (TextView) View.inflate(mTextView.getContext(),
2573 com.android.internal.R.layout.text_drag_thumbnail, null);
2574
2575 if (shadowView == null) {
2576 throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
2577 }
2578
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002579 if (end - start > DRAG_SHADOW_MAX_TEXT_LENGTH) {
2580 final long range = getCharClusterRange(start + DRAG_SHADOW_MAX_TEXT_LENGTH);
2581 end = TextUtils.unpackRangeEndFromLong(range);
Gilles Debunned88876a2012-03-16 17:34:04 -07002582 }
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002583 final CharSequence text = mTextView.getTransformedText(start, end);
Gilles Debunned88876a2012-03-16 17:34:04 -07002584 shadowView.setText(text);
2585 shadowView.setTextColor(mTextView.getTextColors());
2586
Alan Viverettebb98ebd2015-05-08 17:17:44 -07002587 shadowView.setTextAppearance(R.styleable.Theme_textAppearanceLarge);
Gilles Debunned88876a2012-03-16 17:34:04 -07002588 shadowView.setGravity(Gravity.CENTER);
2589
2590 shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
2591 ViewGroup.LayoutParams.WRAP_CONTENT));
2592
2593 final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
2594 shadowView.measure(size, size);
2595
2596 shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
2597 shadowView.invalidate();
2598 return new DragShadowBuilder(shadowView);
2599 }
2600
2601 private static class DragLocalState {
2602 public TextView sourceTextView;
2603 public int start, end;
2604
2605 public DragLocalState(TextView sourceTextView, int start, int end) {
2606 this.sourceTextView = sourceTextView;
2607 this.start = start;
2608 this.end = end;
2609 }
2610 }
2611
2612 void onDrop(DragEvent event) {
Ben Murdoch3dac4602017-01-17 11:27:37 +00002613 SpannableStringBuilder content = new SpannableStringBuilder();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002614
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -07002615 final DragAndDropPermissions permissions = DragAndDropPermissions.obtain(event);
2616 if (permissions != null) {
2617 permissions.takeTransient();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002618 }
2619
2620 try {
2621 ClipData clipData = event.getClipData();
2622 final int itemCount = clipData.getItemCount();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002623 for (int i = 0; i < itemCount; i++) {
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002624 Item item = clipData.getItemAt(i);
2625 content.append(item.coerceToStyledText(mTextView.getContext()));
2626 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002627 } finally {
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -07002628 if (permissions != null) {
2629 permissions.release();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002630 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002631 }
2632
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002633 mTextView.beginBatchEdit();
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002634 mUndoInputFilter.freezeLastEdit();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002635 try {
2636 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
2637 Object localState = event.getLocalState();
2638 DragLocalState dragLocalState = null;
2639 if (localState instanceof DragLocalState) {
2640 dragLocalState = (DragLocalState) localState;
Gilles Debunned88876a2012-03-16 17:34:04 -07002641 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002642 boolean dragDropIntoItself = dragLocalState != null
2643 && dragLocalState.sourceTextView == mTextView;
Gilles Debunned88876a2012-03-16 17:34:04 -07002644
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002645 if (dragDropIntoItself) {
2646 if (offset >= dragLocalState.start && offset < dragLocalState.end) {
2647 // A drop inside the original selection discards the drop.
2648 return;
2649 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002650 }
2651
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002652 final int originalLength = mTextView.getText().length();
2653 int min = offset;
2654 int max = offset;
2655
2656 Selection.setSelection((Spannable) mTextView.getText(), max);
2657 mTextView.replaceText_internal(min, max, content);
2658
2659 if (dragDropIntoItself) {
2660 int dragSourceStart = dragLocalState.start;
2661 int dragSourceEnd = dragLocalState.end;
2662 if (max <= dragSourceStart) {
2663 // Inserting text before selection has shifted positions
2664 final int shift = mTextView.getText().length() - originalLength;
2665 dragSourceStart += shift;
2666 dragSourceEnd += shift;
2667 }
2668
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08002669 // Delete original selection
2670 mTextView.deleteText_internal(dragSourceStart, dragSourceEnd);
Gilles Debunned88876a2012-03-16 17:34:04 -07002671
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08002672 // Make sure we do not leave two adjacent spaces.
2673 final int prevCharIdx = Math.max(0, dragSourceStart - 1);
2674 final int nextCharIdx = Math.min(mTextView.getText().length(), dragSourceStart + 1);
2675 if (nextCharIdx > prevCharIdx + 1) {
2676 CharSequence t = mTextView.getTransformedText(prevCharIdx, nextCharIdx);
2677 if (Character.isSpaceChar(t.charAt(0)) && Character.isSpaceChar(t.charAt(1))) {
2678 mTextView.deleteText_internal(prevCharIdx, prevCharIdx + 1);
2679 }
Victoria Lease91373202012-09-07 16:41:59 -07002680 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002681 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002682 } finally {
2683 mTextView.endBatchEdit();
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002684 mUndoInputFilter.freezeLastEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07002685 }
2686 }
2687
Gilles Debunnec62589c2012-04-12 14:50:23 -07002688 public void addSpanWatchers(Spannable text) {
2689 final int textLength = text.length();
2690
2691 if (mKeyListener != null) {
2692 text.setSpan(mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
2693 }
2694
Jean Chalardbaf30942013-02-28 16:01:51 -08002695 if (mSpanController == null) {
2696 mSpanController = new SpanController();
Gilles Debunnec62589c2012-04-12 14:50:23 -07002697 }
Jean Chalardbaf30942013-02-28 16:01:51 -08002698 text.setSpan(mSpanController, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002699 }
2700
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002701 void setContextMenuAnchor(float x, float y) {
2702 mContextMenuAnchorX = x;
2703 mContextMenuAnchorY = y;
2704 }
2705
2706 void onCreateContextMenu(ContextMenu menu) {
2707 if (mIsBeingLongClicked || Float.isNaN(mContextMenuAnchorX)
2708 || Float.isNaN(mContextMenuAnchorY)) {
2709 return;
2710 }
2711 final int offset = mTextView.getOffsetForPosition(mContextMenuAnchorX, mContextMenuAnchorY);
2712 if (offset == -1) {
2713 return;
2714 }
Siyamed Sinir532f3c92017-06-15 18:22:31 -07002715
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002716 stopTextActionModeWithPreservingSelection();
Siyamed Sinir532f3c92017-06-15 18:22:31 -07002717 if (mTextView.canSelectText()) {
2718 final boolean isOnSelection = mTextView.hasSelection()
2719 && offset >= mTextView.getSelectionStart()
2720 && offset <= mTextView.getSelectionEnd();
2721 if (!isOnSelection) {
2722 // Right clicked position is not on the selection. Remove the selection and move the
2723 // cursor to the right clicked position.
2724 Selection.setSelection((Spannable) mTextView.getText(), offset);
2725 stopTextActionMode();
2726 }
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002727 }
2728
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002729 if (shouldOfferToShowSuggestions()) {
Keisuke Kuroyanagi182f5fe2016-03-11 16:31:29 +09002730 final SuggestionInfo[] suggestionInfoArray =
2731 new SuggestionInfo[SuggestionSpan.SUGGESTIONS_MAX_SIZE];
2732 for (int i = 0; i < suggestionInfoArray.length; i++) {
2733 suggestionInfoArray[i] = new SuggestionInfo();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002734 }
2735 final SubMenu subMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, MENU_ITEM_ORDER_REPLACE,
2736 com.android.internal.R.string.replace);
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002737 final int numItems = mSuggestionHelper.getSuggestionInfo(suggestionInfoArray, null);
Keisuke Kuroyanagi182f5fe2016-03-11 16:31:29 +09002738 for (int i = 0; i < numItems; i++) {
2739 final SuggestionInfo info = suggestionInfoArray[i];
2740 subMenu.add(Menu.NONE, Menu.NONE, i, info.mText)
2741 .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
2742 @Override
2743 public boolean onMenuItemClick(MenuItem item) {
2744 replaceWithSuggestion(info);
2745 return true;
2746 }
2747 });
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002748 }
2749 }
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002750
2751 menu.add(Menu.NONE, TextView.ID_UNDO, MENU_ITEM_ORDER_UNDO,
2752 com.android.internal.R.string.undo)
2753 .setAlphabeticShortcut('z')
2754 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2755 .setEnabled(mTextView.canUndo());
2756 menu.add(Menu.NONE, TextView.ID_REDO, MENU_ITEM_ORDER_REDO,
2757 com.android.internal.R.string.redo)
2758 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2759 .setEnabled(mTextView.canRedo());
2760
2761 menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
2762 com.android.internal.R.string.cut)
2763 .setAlphabeticShortcut('x')
2764 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2765 .setEnabled(mTextView.canCut());
2766 menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
2767 com.android.internal.R.string.copy)
2768 .setAlphabeticShortcut('c')
2769 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2770 .setEnabled(mTextView.canCopy());
2771 menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
2772 com.android.internal.R.string.paste)
2773 .setAlphabeticShortcut('v')
2774 .setEnabled(mTextView.canPaste())
2775 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01002776 menu.add(Menu.NONE, TextView.ID_PASTE_AS_PLAIN_TEXT, MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002777 com.android.internal.R.string.paste_as_plain_text)
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01002778 .setEnabled(mTextView.canPasteAsPlainText())
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002779 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2780 menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
2781 com.android.internal.R.string.share)
2782 .setEnabled(mTextView.canShare())
2783 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2784 menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
2785 com.android.internal.R.string.selectAll)
2786 .setAlphabeticShortcut('a')
2787 .setEnabled(mTextView.canSelectAllText())
2788 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Felipe Leme2ac463e2017-03-13 14:06:25 -07002789 menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
Felipe Leme555bcac2017-06-26 12:53:56 -07002790 android.R.string.autofill)
Felipe Leme2ac463e2017-03-13 14:06:25 -07002791 .setEnabled(mTextView.canRequestAutofill())
2792 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002793
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002794 mPreserveSelection = true;
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002795 }
2796
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002797 @Nullable
2798 private SuggestionSpan findEquivalentSuggestionSpan(
2799 @NonNull SuggestionSpanInfo suggestionSpanInfo) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002800 final Editable editable = (Editable) mTextView.getText();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002801 if (editable.getSpanStart(suggestionSpanInfo.mSuggestionSpan) >= 0) {
2802 // Exactly same span is found.
2803 return suggestionSpanInfo.mSuggestionSpan;
2804 }
2805 // Suggestion span couldn't be found. Try to find a suggestion span that has the same
2806 // contents.
2807 final SuggestionSpan[] suggestionSpans = editable.getSpans(suggestionSpanInfo.mSpanStart,
2808 suggestionSpanInfo.mSpanEnd, SuggestionSpan.class);
2809 for (final SuggestionSpan suggestionSpan : suggestionSpans) {
2810 final int start = editable.getSpanStart(suggestionSpan);
2811 if (start != suggestionSpanInfo.mSpanStart) {
2812 continue;
2813 }
2814 final int end = editable.getSpanEnd(suggestionSpan);
2815 if (end != suggestionSpanInfo.mSpanEnd) {
2816 continue;
2817 }
2818 if (suggestionSpan.equals(suggestionSpanInfo.mSuggestionSpan)) {
2819 return suggestionSpan;
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08002820 }
2821 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002822 return null;
2823 }
2824
2825 private void replaceWithSuggestion(@NonNull final SuggestionInfo suggestionInfo) {
2826 final SuggestionSpan targetSuggestionSpan = findEquivalentSuggestionSpan(
2827 suggestionInfo.mSuggestionSpanInfo);
2828 if (targetSuggestionSpan == null) {
2829 // Span has been removed
2830 return;
2831 }
2832 final Editable editable = (Editable) mTextView.getText();
2833 final int spanStart = editable.getSpanStart(targetSuggestionSpan);
2834 final int spanEnd = editable.getSpanEnd(targetSuggestionSpan);
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08002835 if (spanStart < 0 || spanEnd <= spanStart) {
2836 // Span has been removed
2837 return;
2838 }
2839
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002840 final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
2841 // SuggestionSpans are removed by replace: save them before
2842 SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
2843 SuggestionSpan.class);
2844 final int length = suggestionSpans.length;
2845 int[] suggestionSpansStarts = new int[length];
2846 int[] suggestionSpansEnds = new int[length];
2847 int[] suggestionSpansFlags = new int[length];
2848 for (int i = 0; i < length; i++) {
2849 final SuggestionSpan suggestionSpan = suggestionSpans[i];
2850 suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
2851 suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
2852 suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
2853
2854 // Remove potential misspelled flags
2855 int suggestionSpanFlags = suggestionSpan.getFlags();
2856 if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
2857 suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
2858 suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
2859 suggestionSpan.setFlags(suggestionSpanFlags);
2860 }
2861 }
2862
2863 // Notify source IME of the suggestion pick. Do this before swapping texts.
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002864 targetSuggestionSpan.notifySelection(
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002865 mTextView.getContext(), originalText, suggestionInfo.mSuggestionIndex);
2866
2867 // Swap text content between actual text and Suggestion span
2868 final int suggestionStart = suggestionInfo.mSuggestionStart;
2869 final int suggestionEnd = suggestionInfo.mSuggestionEnd;
2870 final String suggestion = suggestionInfo.mText.subSequence(
2871 suggestionStart, suggestionEnd).toString();
2872 mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
2873
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002874 String[] suggestions = targetSuggestionSpan.getSuggestions();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002875 suggestions[suggestionInfo.mSuggestionIndex] = originalText;
2876
2877 // Restore previous SuggestionSpans
2878 final int lengthDelta = suggestion.length() - (spanEnd - spanStart);
2879 for (int i = 0; i < length; i++) {
2880 // Only spans that include the modified region make sense after replacement
2881 // Spans partially included in the replaced region are removed, there is no
2882 // way to assign them a valid range after replacement
2883 if (suggestionSpansStarts[i] <= spanStart && suggestionSpansEnds[i] >= spanEnd) {
2884 mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
2885 suggestionSpansEnds[i] + lengthDelta, suggestionSpansFlags[i]);
2886 }
2887 }
2888 // Move cursor at the end of the replaced word
2889 final int newCursorPosition = spanEnd + lengthDelta;
2890 mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
2891 }
2892
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002893 private final MenuItem.OnMenuItemClickListener mOnContextMenuItemClickListener =
2894 new MenuItem.OnMenuItemClickListener() {
2895 @Override
2896 public boolean onMenuItemClick(MenuItem item) {
2897 if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
2898 return true;
2899 }
2900 return mTextView.onTextContextMenuItem(item.getItemId());
2901 }
2902 };
2903
Gilles Debunned88876a2012-03-16 17:34:04 -07002904 /**
2905 * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
2906 * pop-up should be displayed.
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07002907 * Also monitors {@link Selection} to call back to the attached input method.
Gilles Debunned88876a2012-03-16 17:34:04 -07002908 */
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09002909 private class SpanController implements SpanWatcher {
Gilles Debunned88876a2012-03-16 17:34:04 -07002910
2911 private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
2912
2913 private EasyEditPopupWindow mPopupWindow;
2914
Gilles Debunned88876a2012-03-16 17:34:04 -07002915 private Runnable mHidePopup;
2916
Jean Chalardbaf30942013-02-28 16:01:51 -08002917 // This function is pure but inner classes can't have static functions
2918 private boolean isNonIntermediateSelectionSpan(final Spannable text,
2919 final Object span) {
2920 return (Selection.SELECTION_START == span || Selection.SELECTION_END == span)
2921 && (text.getSpanFlags(span) & Spanned.SPAN_INTERMEDIATE) == 0;
2922 }
2923
Gilles Debunnec62589c2012-04-12 14:50:23 -07002924 @Override
2925 public void onSpanAdded(Spannable text, Object span, int start, int end) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002926 if (isNonIntermediateSelectionSpan(text, span)) {
2927 sendUpdateSelection();
2928 } else if (span instanceof EasyEditSpan) {
Gilles Debunnec62589c2012-04-12 14:50:23 -07002929 if (mPopupWindow == null) {
2930 mPopupWindow = new EasyEditPopupWindow();
2931 mHidePopup = new Runnable() {
2932 @Override
2933 public void run() {
2934 hide();
2935 }
2936 };
2937 }
2938
2939 // Make sure there is only at most one EasyEditSpan in the text
2940 if (mPopupWindow.mEasyEditSpan != null) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002941 mPopupWindow.mEasyEditSpan.setDeleteEnabled(false);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002942 }
2943
2944 mPopupWindow.setEasyEditSpan((EasyEditSpan) span);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002945 mPopupWindow.setOnDeleteListener(new EasyEditDeleteListener() {
2946 @Override
2947 public void onDeleteClick(EasyEditSpan span) {
2948 Editable editable = (Editable) mTextView.getText();
2949 int start = editable.getSpanStart(span);
2950 int end = editable.getSpanEnd(span);
2951 if (start >= 0 && end >= 0) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002952 sendEasySpanNotification(EasyEditSpan.TEXT_DELETED, span);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002953 mTextView.deleteText_internal(start, end);
2954 }
2955 editable.removeSpan(span);
2956 }
2957 });
Gilles Debunnec62589c2012-04-12 14:50:23 -07002958
2959 if (mTextView.getWindowVisibility() != View.VISIBLE) {
2960 // The window is not visible yet, ignore the text change.
2961 return;
2962 }
2963
2964 if (mTextView.getLayout() == null) {
2965 // The view has not been laid out yet, ignore the text change
2966 return;
2967 }
2968
2969 if (extractedTextModeWillBeStarted()) {
2970 // The input is in extract mode. Do not handle the easy edit in
2971 // the original TextView, as the ExtractEditText will do
2972 return;
2973 }
2974
2975 mPopupWindow.show();
2976 mTextView.removeCallbacks(mHidePopup);
2977 mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
2978 }
2979 }
2980
2981 @Override
2982 public void onSpanRemoved(Spannable text, Object span, int start, int end) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002983 if (isNonIntermediateSelectionSpan(text, span)) {
2984 sendUpdateSelection();
2985 } else if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
Gilles Debunnec62589c2012-04-12 14:50:23 -07002986 hide();
2987 }
2988 }
2989
2990 @Override
2991 public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd,
2992 int newStart, int newEnd) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002993 if (isNonIntermediateSelectionSpan(text, span)) {
2994 sendUpdateSelection();
2995 } else if (mPopupWindow != null && span instanceof EasyEditSpan) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002996 EasyEditSpan easyEditSpan = (EasyEditSpan) span;
Jean Chalardbaf30942013-02-28 16:01:51 -08002997 sendEasySpanNotification(EasyEditSpan.TEXT_MODIFIED, easyEditSpan);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002998 text.removeSpan(easyEditSpan);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002999 }
3000 }
3001
Gilles Debunned88876a2012-03-16 17:34:04 -07003002 public void hide() {
3003 if (mPopupWindow != null) {
3004 mPopupWindow.hide();
3005 mTextView.removeCallbacks(mHidePopup);
3006 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003007 }
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003008
Jean Chalardbaf30942013-02-28 16:01:51 -08003009 private void sendEasySpanNotification(int textChangedType, EasyEditSpan span) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003010 try {
3011 PendingIntent pendingIntent = span.getPendingIntent();
3012 if (pendingIntent != null) {
3013 Intent intent = new Intent();
3014 intent.putExtra(EasyEditSpan.EXTRA_TEXT_CHANGED_TYPE, textChangedType);
3015 pendingIntent.send(mTextView.getContext(), 0, intent);
3016 }
3017 } catch (CanceledException e) {
3018 // This should not happen, as we should try to send the intent only once.
3019 Log.w(TAG, "PendingIntent for notification cannot be sent", e);
3020 }
3021 }
3022 }
3023
3024 /**
3025 * Listens for the delete event triggered by {@link EasyEditPopupWindow}.
3026 */
3027 private interface EasyEditDeleteListener {
3028
3029 /**
3030 * Clicks the delete pop-up.
3031 */
3032 void onDeleteClick(EasyEditSpan span);
Gilles Debunned88876a2012-03-16 17:34:04 -07003033 }
3034
3035 /**
3036 * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07003037 * by {@link SpanController}.
Gilles Debunned88876a2012-03-16 17:34:04 -07003038 */
3039 private class EasyEditPopupWindow extends PinnedPopupWindow
3040 implements OnClickListener {
3041 private static final int POPUP_TEXT_LAYOUT =
3042 com.android.internal.R.layout.text_edit_action_popup_text;
3043 private TextView mDeleteTextView;
3044 private EasyEditSpan mEasyEditSpan;
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003045 private EasyEditDeleteListener mOnDeleteListener;
Gilles Debunned88876a2012-03-16 17:34:04 -07003046
3047 @Override
3048 protected void createPopupWindow() {
3049 mPopupWindow = new PopupWindow(mTextView.getContext(), null,
3050 com.android.internal.R.attr.textSelectHandleWindowStyle);
3051 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
3052 mPopupWindow.setClippingEnabled(true);
3053 }
3054
3055 @Override
3056 protected void initContentView() {
3057 LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
3058 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
3059 mContentView = linearLayout;
3060 mContentView.setBackgroundResource(
3061 com.android.internal.R.drawable.text_edit_side_paste_window);
3062
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003063 LayoutInflater inflater = (LayoutInflater) mTextView.getContext()
3064 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003065
3066 LayoutParams wrapContent = new LayoutParams(
3067 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
3068
3069 mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
3070 mDeleteTextView.setLayoutParams(wrapContent);
3071 mDeleteTextView.setText(com.android.internal.R.string.delete);
3072 mDeleteTextView.setOnClickListener(this);
3073 mContentView.addView(mDeleteTextView);
3074 }
3075
Gilles Debunnec62589c2012-04-12 14:50:23 -07003076 public void setEasyEditSpan(EasyEditSpan easyEditSpan) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003077 mEasyEditSpan = easyEditSpan;
Gilles Debunned88876a2012-03-16 17:34:04 -07003078 }
3079
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003080 private void setOnDeleteListener(EasyEditDeleteListener listener) {
3081 mOnDeleteListener = listener;
3082 }
3083
Gilles Debunned88876a2012-03-16 17:34:04 -07003084 @Override
3085 public void onClick(View view) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003086 if (view == mDeleteTextView
3087 && mEasyEditSpan != null && mEasyEditSpan.isDeleteEnabled()
3088 && mOnDeleteListener != null) {
3089 mOnDeleteListener.onDeleteClick(mEasyEditSpan);
Gilles Debunned88876a2012-03-16 17:34:04 -07003090 }
3091 }
3092
3093 @Override
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003094 public void hide() {
3095 if (mEasyEditSpan != null) {
3096 mEasyEditSpan.setDeleteEnabled(false);
3097 }
3098 mOnDeleteListener = null;
3099 super.hide();
3100 }
3101
3102 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07003103 protected int getTextOffset() {
3104 // Place the pop-up at the end of the span
3105 Editable editable = (Editable) mTextView.getText();
3106 return editable.getSpanEnd(mEasyEditSpan);
3107 }
3108
3109 @Override
3110 protected int getVerticalLocalPosition(int line) {
Siyamed Sinira60b59d2017-07-26 09:26:41 -07003111 final Layout layout = mTextView.getLayout();
3112 return layout.getLineBottomWithoutSpacing(line);
Gilles Debunned88876a2012-03-16 17:34:04 -07003113 }
3114
3115 @Override
3116 protected int clipVertically(int positionY) {
3117 // As we display the pop-up below the span, no vertical clipping is required.
3118 return positionY;
3119 }
3120 }
3121
3122 private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
3123 // 3 handles
3124 // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003125 // 1 CursorAnchorInfoNotifier
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003126 private static final int MAXIMUM_NUMBER_OF_LISTENERS = 7;
Gilles Debunned88876a2012-03-16 17:34:04 -07003127 private TextViewPositionListener[] mPositionListeners =
3128 new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003129 private boolean[] mCanMove = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
Gilles Debunned88876a2012-03-16 17:34:04 -07003130 private boolean mPositionHasChanged = true;
3131 // Absolute position of the TextView with respect to its parent window
3132 private int mPositionX, mPositionY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003133 private int mPositionXOnScreen, mPositionYOnScreen;
Gilles Debunned88876a2012-03-16 17:34:04 -07003134 private int mNumberOfListeners;
3135 private boolean mScrollHasChanged;
3136 final int[] mTempCoords = new int[2];
3137
3138 public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
3139 if (mNumberOfListeners == 0) {
3140 updatePosition();
3141 ViewTreeObserver vto = mTextView.getViewTreeObserver();
3142 vto.addOnPreDrawListener(this);
3143 }
3144
3145 int emptySlotIndex = -1;
3146 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3147 TextViewPositionListener listener = mPositionListeners[i];
3148 if (listener == positionListener) {
3149 return;
3150 } else if (emptySlotIndex < 0 && listener == null) {
3151 emptySlotIndex = i;
3152 }
3153 }
3154
3155 mPositionListeners[emptySlotIndex] = positionListener;
3156 mCanMove[emptySlotIndex] = canMove;
3157 mNumberOfListeners++;
3158 }
3159
3160 public void removeSubscriber(TextViewPositionListener positionListener) {
3161 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3162 if (mPositionListeners[i] == positionListener) {
3163 mPositionListeners[i] = null;
3164 mNumberOfListeners--;
3165 break;
3166 }
3167 }
3168
3169 if (mNumberOfListeners == 0) {
3170 ViewTreeObserver vto = mTextView.getViewTreeObserver();
3171 vto.removeOnPreDrawListener(this);
3172 }
3173 }
3174
3175 public int getPositionX() {
3176 return mPositionX;
3177 }
3178
3179 public int getPositionY() {
3180 return mPositionY;
3181 }
3182
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003183 public int getPositionXOnScreen() {
3184 return mPositionXOnScreen;
3185 }
3186
3187 public int getPositionYOnScreen() {
3188 return mPositionYOnScreen;
3189 }
3190
Gilles Debunned88876a2012-03-16 17:34:04 -07003191 @Override
3192 public boolean onPreDraw() {
3193 updatePosition();
3194
3195 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3196 if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
3197 TextViewPositionListener positionListener = mPositionListeners[i];
3198 if (positionListener != null) {
3199 positionListener.updatePosition(mPositionX, mPositionY,
3200 mPositionHasChanged, mScrollHasChanged);
3201 }
3202 }
3203 }
3204
3205 mScrollHasChanged = false;
3206 return true;
3207 }
3208
3209 private void updatePosition() {
3210 mTextView.getLocationInWindow(mTempCoords);
3211
3212 mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
3213
3214 mPositionX = mTempCoords[0];
3215 mPositionY = mTempCoords[1];
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003216
3217 mTextView.getLocationOnScreen(mTempCoords);
3218
3219 mPositionXOnScreen = mTempCoords[0];
3220 mPositionYOnScreen = mTempCoords[1];
Gilles Debunned88876a2012-03-16 17:34:04 -07003221 }
3222
3223 public void onScrollChanged() {
3224 mScrollHasChanged = true;
3225 }
3226 }
3227
3228 private abstract class PinnedPopupWindow implements TextViewPositionListener {
3229 protected PopupWindow mPopupWindow;
3230 protected ViewGroup mContentView;
3231 int mPositionX, mPositionY;
Seigo Nonaka60490d12016-01-28 17:25:18 +09003232 int mClippingLimitLeft, mClippingLimitRight;
Gilles Debunned88876a2012-03-16 17:34:04 -07003233
3234 protected abstract void createPopupWindow();
3235 protected abstract void initContentView();
3236 protected abstract int getTextOffset();
3237 protected abstract int getVerticalLocalPosition(int line);
3238 protected abstract int clipVertically(int positionY);
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003239 protected void setUp() {
3240 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003241
3242 public PinnedPopupWindow() {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003243 // Due to calling subclass methods in base constructor, subclass constructor is not
3244 // called before subclass methods, e.g. createPopupWindow or initContentView. To give
3245 // a chance to initialize subclasses, call setUp() method here.
3246 // TODO: It is good to extract non trivial initialization code from constructor.
3247 setUp();
3248
Gilles Debunned88876a2012-03-16 17:34:04 -07003249 createPopupWindow();
3250
Alan Viverette80ebe0d2015-04-30 15:53:11 -07003251 mPopupWindow.setWindowLayoutType(
3252 WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
Gilles Debunned88876a2012-03-16 17:34:04 -07003253 mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
3254 mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
3255
3256 initContentView();
3257
3258 LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
3259 ViewGroup.LayoutParams.WRAP_CONTENT);
3260 mContentView.setLayoutParams(wrapContent);
3261
3262 mPopupWindow.setContentView(mContentView);
3263 }
3264
3265 public void show() {
3266 getPositionListener().addSubscriber(this, false /* offset is fixed */);
3267
3268 computeLocalPosition();
3269
3270 final PositionListener positionListener = getPositionListener();
3271 updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
3272 }
3273
3274 protected void measureContent() {
3275 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3276 mContentView.measure(
3277 View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
3278 View.MeasureSpec.AT_MOST),
3279 View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
3280 View.MeasureSpec.AT_MOST));
3281 }
3282
3283 /* The popup window will be horizontally centered on the getTextOffset() and vertically
3284 * positioned according to viewportToContentHorizontalOffset.
3285 *
3286 * This method assumes that mContentView has properly been measured from its content. */
3287 private void computeLocalPosition() {
3288 measureContent();
3289 final int width = mContentView.getMeasuredWidth();
3290 final int offset = getTextOffset();
3291 mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f);
3292 mPositionX += mTextView.viewportToContentHorizontalOffset();
3293
3294 final int line = mTextView.getLayout().getLineForOffset(offset);
3295 mPositionY = getVerticalLocalPosition(line);
3296 mPositionY += mTextView.viewportToContentVerticalOffset();
3297 }
3298
3299 private void updatePosition(int parentPositionX, int parentPositionY) {
3300 int positionX = parentPositionX + mPositionX;
3301 int positionY = parentPositionY + mPositionY;
3302
3303 positionY = clipVertically(positionY);
3304
3305 // Horizontal clipping
3306 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3307 final int width = mContentView.getMeasuredWidth();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003308 positionX = Math.min(
3309 displayMetrics.widthPixels - width + mClippingLimitRight, positionX);
3310 positionX = Math.max(-mClippingLimitLeft, positionX);
Gilles Debunned88876a2012-03-16 17:34:04 -07003311
3312 if (isShowing()) {
3313 mPopupWindow.update(positionX, positionY, -1, -1);
3314 } else {
3315 mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
3316 positionX, positionY);
3317 }
3318 }
3319
3320 public void hide() {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09003321 if (!isShowing()) {
3322 return;
3323 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003324 mPopupWindow.dismiss();
3325 getPositionListener().removeSubscriber(this);
3326 }
3327
3328 @Override
3329 public void updatePosition(int parentPositionX, int parentPositionY,
3330 boolean parentPositionChanged, boolean parentScrolled) {
3331 // Either parentPositionChanged or parentScrolled is true, check if still visible
3332 if (isShowing() && isOffsetVisible(getTextOffset())) {
3333 if (parentScrolled) computeLocalPosition();
3334 updatePosition(parentPositionX, parentPositionY);
3335 } else {
3336 hide();
3337 }
3338 }
3339
3340 public boolean isShowing() {
3341 return mPopupWindow.isShowing();
3342 }
3343 }
3344
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003345 private static final class SuggestionInfo {
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003346 // Range of actual suggestion within mText
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003347 int mSuggestionStart, mSuggestionEnd;
3348
3349 // The SuggestionSpan that this TextView represents
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003350 final SuggestionSpanInfo mSuggestionSpanInfo = new SuggestionSpanInfo();
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003351
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003352 // The index of this suggestion inside suggestionSpan
3353 int mSuggestionIndex;
3354
3355 final SpannableStringBuilder mText = new SpannableStringBuilder();
3356
3357 void clear() {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003358 mSuggestionSpanInfo.clear();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003359 mText.clear();
3360 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003361
3362 // Utility method to set attributes about a SuggestionSpan.
3363 void setSpanInfo(SuggestionSpan span, int spanStart, int spanEnd) {
3364 mSuggestionSpanInfo.mSuggestionSpan = span;
3365 mSuggestionSpanInfo.mSpanStart = spanStart;
3366 mSuggestionSpanInfo.mSpanEnd = spanEnd;
3367 }
3368 }
3369
3370 private static final class SuggestionSpanInfo {
3371 // The SuggestionSpan;
3372 @Nullable
3373 SuggestionSpan mSuggestionSpan;
3374
3375 // The SuggestionSpan start position
3376 int mSpanStart;
3377
3378 // The SuggestionSpan end position
3379 int mSpanEnd;
3380
3381 void clear() {
3382 mSuggestionSpan = null;
3383 }
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003384 }
3385
3386 private class SuggestionHelper {
3387 private final Comparator<SuggestionSpan> mSuggestionSpanComparator =
3388 new SuggestionSpanComparator();
3389 private final HashMap<SuggestionSpan, Integer> mSpansLengths =
3390 new HashMap<SuggestionSpan, Integer>();
3391
3392 private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
3393 public int compare(SuggestionSpan span1, SuggestionSpan span2) {
3394 final int flag1 = span1.getFlags();
3395 final int flag2 = span2.getFlags();
3396 if (flag1 != flag2) {
3397 // The order here should match what is used in updateDrawState
3398 final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
3399 final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
3400 final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
3401 final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
3402 if (easy1 && !misspelled1) return -1;
3403 if (easy2 && !misspelled2) return 1;
3404 if (misspelled1) return -1;
3405 if (misspelled2) return 1;
3406 }
3407
3408 return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
3409 }
3410 }
3411
3412 /**
3413 * Returns the suggestion spans that cover the current cursor position. The suggestion
3414 * spans are sorted according to the length of text that they are attached to.
3415 */
3416 private SuggestionSpan[] getSortedSuggestionSpans() {
3417 int pos = mTextView.getSelectionStart();
3418 Spannable spannable = (Spannable) mTextView.getText();
3419 SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
3420
3421 mSpansLengths.clear();
3422 for (SuggestionSpan suggestionSpan : suggestionSpans) {
3423 int start = spannable.getSpanStart(suggestionSpan);
3424 int end = spannable.getSpanEnd(suggestionSpan);
3425 mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
3426 }
3427
3428 // The suggestions are sorted according to their types (easy correction first, then
3429 // misspelled) and to the length of the text that they cover (shorter first).
3430 Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
3431 mSpansLengths.clear();
3432
3433 return suggestionSpans;
3434 }
3435
3436 /**
3437 * Gets the SuggestionInfo list that contains suggestion information at the current cursor
3438 * position.
3439 *
3440 * @param suggestionInfos SuggestionInfo array the results will be set.
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003441 * @param misspelledSpanInfo a struct the misspelled SuggestionSpan info will be set.
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003442 * @return the number of suggestions actually fetched.
3443 */
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003444 public int getSuggestionInfo(SuggestionInfo[] suggestionInfos,
3445 @Nullable SuggestionSpanInfo misspelledSpanInfo) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003446 final Spannable spannable = (Spannable) mTextView.getText();
3447 final SuggestionSpan[] suggestionSpans = getSortedSuggestionSpans();
3448 final int nbSpans = suggestionSpans.length;
3449 if (nbSpans == 0) return 0;
3450
3451 int numberOfSuggestions = 0;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003452 for (final SuggestionSpan suggestionSpan : suggestionSpans) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003453 final int spanStart = spannable.getSpanStart(suggestionSpan);
3454 final int spanEnd = spannable.getSpanEnd(suggestionSpan);
3455
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003456 if (misspelledSpanInfo != null
3457 && (suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
3458 misspelledSpanInfo.mSuggestionSpan = suggestionSpan;
3459 misspelledSpanInfo.mSpanStart = spanStart;
3460 misspelledSpanInfo.mSpanEnd = spanEnd;
3461 }
3462
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003463 final String[] suggestions = suggestionSpan.getSuggestions();
3464 final int nbSuggestions = suggestions.length;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003465 suggestionLoop:
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003466 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
3467 final String suggestion = suggestions[suggestionIndex];
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003468 for (int i = 0; i < numberOfSuggestions; i++) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003469 final SuggestionInfo otherSuggestionInfo = suggestionInfos[i];
3470 if (otherSuggestionInfo.mText.toString().equals(suggestion)) {
3471 final int otherSpanStart =
3472 otherSuggestionInfo.mSuggestionSpanInfo.mSpanStart;
3473 final int otherSpanEnd =
3474 otherSuggestionInfo.mSuggestionSpanInfo.mSpanEnd;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003475 if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003476 continue suggestionLoop;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003477 }
3478 }
3479 }
3480
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003481 SuggestionInfo suggestionInfo = suggestionInfos[numberOfSuggestions];
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003482 suggestionInfo.setSpanInfo(suggestionSpan, spanStart, spanEnd);
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003483 suggestionInfo.mSuggestionIndex = suggestionIndex;
3484 suggestionInfo.mSuggestionStart = 0;
3485 suggestionInfo.mSuggestionEnd = suggestion.length();
3486 suggestionInfo.mText.replace(0, suggestionInfo.mText.length(), suggestion);
3487 numberOfSuggestions++;
3488 if (numberOfSuggestions >= suggestionInfos.length) {
3489 return numberOfSuggestions;
3490 }
3491 }
3492 }
3493 return numberOfSuggestions;
3494 }
3495 }
3496
Seigo Nonakaa60160b2015-08-19 12:38:35 -07003497 @VisibleForTesting
3498 public class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
Gilles Debunned88876a2012-03-16 17:34:04 -07003499 private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003500
3501 // Key of intent extras for inserting new word into user dictionary.
3502 private static final String USER_DICTIONARY_EXTRA_WORD = "word";
3503 private static final String USER_DICTIONARY_EXTRA_LOCALE = "locale";
3504
Gilles Debunned88876a2012-03-16 17:34:04 -07003505 private SuggestionInfo[] mSuggestionInfos;
3506 private int mNumberOfSuggestions;
3507 private boolean mCursorWasVisibleBeforeSuggestions;
3508 private boolean mIsShowingUp = false;
3509 private SuggestionAdapter mSuggestionsAdapter;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003510 private TextAppearanceSpan mHighlightSpan; // TODO: Make mHighlightSpan final.
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003511 private TextView mAddToDictionaryButton;
3512 private TextView mDeleteButton;
Seigo Nonakaf47976e2016-03-01 09:17:37 -08003513 private ListView mSuggestionListView;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003514 private final SuggestionSpanInfo mMisspelledSpanInfo = new SuggestionSpanInfo();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003515 private int mContainerMarginWidth;
3516 private int mContainerMarginTop;
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003517 private LinearLayout mContainerView;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003518 private Context mContext; // TODO: Make mContext final.
Gilles Debunned88876a2012-03-16 17:34:04 -07003519
3520 private class CustomPopupWindow extends PopupWindow {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003521
Gilles Debunned88876a2012-03-16 17:34:04 -07003522 @Override
3523 public void dismiss() {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09003524 if (!isShowing()) {
3525 return;
3526 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003527 super.dismiss();
Gilles Debunned88876a2012-03-16 17:34:04 -07003528 getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
3529
3530 // Safe cast since show() checks that mTextView.getText() is an Editable
3531 ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan);
3532
3533 mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions);
Keisuke Kuroyanagi4a696ac2016-02-23 11:02:07 -08003534 if (hasInsertionController() && !extractedTextModeWillBeStarted()) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003535 getInsertionController().show();
3536 }
3537 }
3538 }
3539
3540 public SuggestionsPopupWindow() {
3541 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
Gilles Debunned88876a2012-03-16 17:34:04 -07003542 }
3543
3544 @Override
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003545 protected void setUp() {
3546 mContext = applyDefaultTheme(mTextView.getContext());
3547 mHighlightSpan = new TextAppearanceSpan(mContext,
3548 mTextView.mTextEditSuggestionHighlightStyle);
3549 }
3550
3551 private Context applyDefaultTheme(Context originalContext) {
3552 TypedArray a = originalContext.obtainStyledAttributes(
3553 new int[]{com.android.internal.R.attr.isLightTheme});
3554 boolean isLightTheme = a.getBoolean(0, true);
3555 int themeId = isLightTheme ? R.style.ThemeOverlay_Material_Light
3556 : R.style.ThemeOverlay_Material_Dark;
3557 a.recycle();
3558 return new ContextThemeWrapper(originalContext, themeId);
3559 }
3560
3561 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07003562 protected void createPopupWindow() {
Seigo Nonaka3ed1b392016-01-19 13:54:59 +09003563 mPopupWindow = new CustomPopupWindow();
Gilles Debunned88876a2012-03-16 17:34:04 -07003564 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
Seigo Nonaka3ed1b392016-01-19 13:54:59 +09003565 mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
Gilles Debunned88876a2012-03-16 17:34:04 -07003566 mPopupWindow.setFocusable(true);
3567 mPopupWindow.setClippingEnabled(false);
3568 }
3569
3570 @Override
3571 protected void initContentView() {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003572 final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
3573 Context.LAYOUT_INFLATER_SERVICE);
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003574 mContentView = (ViewGroup) inflater.inflate(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003575 mTextView.mTextEditSuggestionContainerLayout, null);
Gilles Debunned88876a2012-03-16 17:34:04 -07003576
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003577 mContainerView = (LinearLayout) mContentView.findViewById(
3578 com.android.internal.R.id.suggestionWindowContainer);
Seigo Nonaka60490d12016-01-28 17:25:18 +09003579 ViewGroup.MarginLayoutParams lp =
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003580 (ViewGroup.MarginLayoutParams) mContainerView.getLayoutParams();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003581 mContainerMarginWidth = lp.leftMargin + lp.rightMargin;
3582 mContainerMarginTop = lp.topMargin;
3583 mClippingLimitLeft = lp.leftMargin;
3584 mClippingLimitRight = lp.rightMargin;
3585
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003586 mSuggestionListView = (ListView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003587 com.android.internal.R.id.suggestionContainer);
3588
3589 mSuggestionsAdapter = new SuggestionAdapter();
Seigo Nonakaf47976e2016-03-01 09:17:37 -08003590 mSuggestionListView.setAdapter(mSuggestionsAdapter);
3591 mSuggestionListView.setOnItemClickListener(this);
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003592
3593 // Inflate the suggestion items once and for all.
3594 mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS];
Gilles Debunned88876a2012-03-16 17:34:04 -07003595 for (int i = 0; i < mSuggestionInfos.length; i++) {
3596 mSuggestionInfos[i] = new SuggestionInfo();
3597 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003598
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003599 mAddToDictionaryButton = (TextView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003600 com.android.internal.R.id.addToDictionaryButton);
3601 mAddToDictionaryButton.setOnClickListener(new View.OnClickListener() {
3602 public void onClick(View v) {
Keisuke Kuroyanagi6e0860d2016-03-15 15:40:43 +09003603 final SuggestionSpan misspelledSpan =
3604 findEquivalentSuggestionSpan(mMisspelledSpanInfo);
3605 if (misspelledSpan == null) {
3606 // Span has been removed.
3607 return;
3608 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003609 final Editable editable = (Editable) mTextView.getText();
Keisuke Kuroyanagi6e0860d2016-03-15 15:40:43 +09003610 final int spanStart = editable.getSpanStart(misspelledSpan);
3611 final int spanEnd = editable.getSpanEnd(misspelledSpan);
3612 if (spanStart < 0 || spanEnd <= spanStart) {
3613 return;
3614 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003615 final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
3616
3617 final Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
3618 intent.putExtra(USER_DICTIONARY_EXTRA_WORD, originalText);
3619 intent.putExtra(USER_DICTIONARY_EXTRA_LOCALE,
3620 mTextView.getTextServicesLocale().toString());
3621 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
3622 mTextView.getContext().startActivity(intent);
3623 // There is no way to know if the word was indeed added. Re-check.
3624 // TODO The ExtractEditText should remove the span in the original text instead
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003625 editable.removeSpan(mMisspelledSpanInfo.mSuggestionSpan);
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003626 Selection.setSelection(editable, spanEnd);
3627 updateSpellCheckSpans(spanStart, spanEnd, false);
3628 hideWithCleanUp();
3629 }
3630 });
3631
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003632 mDeleteButton = (TextView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003633 com.android.internal.R.id.deleteButton);
3634 mDeleteButton.setOnClickListener(new View.OnClickListener() {
3635 public void onClick(View v) {
3636 final Editable editable = (Editable) mTextView.getText();
3637
3638 final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
3639 int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
3640 if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
3641 // Do not leave two adjacent spaces after deletion, or one at beginning of
3642 // text
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003643 if (spanUnionEnd < editable.length()
3644 && Character.isSpaceChar(editable.charAt(spanUnionEnd))
3645 && (spanUnionStart == 0
3646 || Character.isSpaceChar(
3647 editable.charAt(spanUnionStart - 1)))) {
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003648 spanUnionEnd = spanUnionEnd + 1;
3649 }
3650 mTextView.deleteText_internal(spanUnionStart, spanUnionEnd);
3651 }
3652 hideWithCleanUp();
3653 }
3654 });
3655
Gilles Debunned88876a2012-03-16 17:34:04 -07003656 }
3657
3658 public boolean isShowingUp() {
3659 return mIsShowingUp;
3660 }
3661
3662 public void onParentLostFocus() {
3663 mIsShowingUp = false;
3664 }
3665
Gilles Debunned88876a2012-03-16 17:34:04 -07003666 private class SuggestionAdapter extends BaseAdapter {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003667 private LayoutInflater mInflater = (LayoutInflater) mContext.getSystemService(
3668 Context.LAYOUT_INFLATER_SERVICE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003669
3670 @Override
3671 public int getCount() {
3672 return mNumberOfSuggestions;
3673 }
3674
3675 @Override
3676 public Object getItem(int position) {
3677 return mSuggestionInfos[position];
3678 }
3679
3680 @Override
3681 public long getItemId(int position) {
3682 return position;
3683 }
3684
3685 @Override
3686 public View getView(int position, View convertView, ViewGroup parent) {
3687 TextView textView = (TextView) convertView;
3688
3689 if (textView == null) {
3690 textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout,
3691 parent, false);
3692 }
3693
3694 final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003695 textView.setText(suggestionInfo.mText);
Gilles Debunned88876a2012-03-16 17:34:04 -07003696 return textView;
3697 }
3698 }
3699
Seigo Nonakaa60160b2015-08-19 12:38:35 -07003700 @VisibleForTesting
3701 public ViewGroup getContentViewForTesting() {
3702 return mContentView;
3703 }
3704
Gilles Debunned88876a2012-03-16 17:34:04 -07003705 @Override
3706 public void show() {
3707 if (!(mTextView.getText() instanceof Editable)) return;
Keisuke Kuroyanagi4a696ac2016-02-23 11:02:07 -08003708 if (extractedTextModeWillBeStarted()) {
3709 return;
3710 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003711
3712 if (updateSuggestions()) {
3713 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
3714 mTextView.setCursorVisible(false);
3715 mIsShowingUp = true;
3716 super.show();
3717 }
Clara Bayarri428e5232017-07-18 16:42:16 +01003718
3719 mSuggestionListView.setVisibility(mNumberOfSuggestions == 0 ? View.GONE : View.VISIBLE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003720 }
3721
3722 @Override
3723 protected void measureContent() {
3724 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3725 final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
3726 displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
3727 final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
3728 displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
3729
3730 int width = 0;
3731 View view = null;
3732 for (int i = 0; i < mNumberOfSuggestions; i++) {
3733 view = mSuggestionsAdapter.getView(i, view, mContentView);
3734 view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
3735 view.measure(horizontalMeasure, verticalMeasure);
3736 width = Math.max(width, view.getMeasuredWidth());
3737 }
3738
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003739 if (mAddToDictionaryButton.getVisibility() != View.GONE) {
3740 mAddToDictionaryButton.measure(horizontalMeasure, verticalMeasure);
3741 width = Math.max(width, mAddToDictionaryButton.getMeasuredWidth());
3742 }
3743
3744 mDeleteButton.measure(horizontalMeasure, verticalMeasure);
3745 width = Math.max(width, mDeleteButton.getMeasuredWidth());
3746
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003747 width += mContainerView.getPaddingLeft() + mContainerView.getPaddingRight()
3748 + mContainerMarginWidth;
Seigo Nonaka60490d12016-01-28 17:25:18 +09003749
Gilles Debunned88876a2012-03-16 17:34:04 -07003750 // Enforce the width based on actual text widths
3751 mContentView.measure(
3752 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
3753 verticalMeasure);
3754
3755 Drawable popupBackground = mPopupWindow.getBackground();
3756 if (popupBackground != null) {
3757 if (mTempRect == null) mTempRect = new Rect();
3758 popupBackground.getPadding(mTempRect);
3759 width += mTempRect.left + mTempRect.right;
3760 }
3761 mPopupWindow.setWidth(width);
3762 }
3763
3764 @Override
3765 protected int getTextOffset() {
Keisuke Kuroyanagi713be062016-02-29 16:07:54 -08003766 return (mTextView.getSelectionStart() + mTextView.getSelectionStart()) / 2;
Gilles Debunned88876a2012-03-16 17:34:04 -07003767 }
3768
3769 @Override
3770 protected int getVerticalLocalPosition(int line) {
Siyamed Sinira60b59d2017-07-26 09:26:41 -07003771 final Layout layout = mTextView.getLayout();
3772 return layout.getLineBottomWithoutSpacing(line) - mContainerMarginTop;
Gilles Debunned88876a2012-03-16 17:34:04 -07003773 }
3774
3775 @Override
3776 protected int clipVertically(int positionY) {
3777 final int height = mContentView.getMeasuredHeight();
3778 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3779 return Math.min(positionY, displayMetrics.heightPixels - height);
3780 }
3781
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003782 private void hideWithCleanUp() {
3783 for (final SuggestionInfo info : mSuggestionInfos) {
3784 info.clear();
3785 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003786 mMisspelledSpanInfo.clear();
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003787 hide();
Gilles Debunned88876a2012-03-16 17:34:04 -07003788 }
3789
3790 private boolean updateSuggestions() {
3791 Spannable spannable = (Spannable) mTextView.getText();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003792 mNumberOfSuggestions =
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003793 mSuggestionHelper.getSuggestionInfo(mSuggestionInfos, mMisspelledSpanInfo);
3794 if (mNumberOfSuggestions == 0 && mMisspelledSpanInfo.mSuggestionSpan == null) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003795 return false;
3796 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003797
Gilles Debunned88876a2012-03-16 17:34:04 -07003798 int spanUnionStart = mTextView.getText().length();
3799 int spanUnionEnd = 0;
3800
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003801 for (int i = 0; i < mNumberOfSuggestions; i++) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003802 final SuggestionSpanInfo spanInfo = mSuggestionInfos[i].mSuggestionSpanInfo;
3803 spanUnionStart = Math.min(spanUnionStart, spanInfo.mSpanStart);
3804 spanUnionEnd = Math.max(spanUnionEnd, spanInfo.mSpanEnd);
3805 }
3806 if (mMisspelledSpanInfo.mSuggestionSpan != null) {
3807 spanUnionStart = Math.min(spanUnionStart, mMisspelledSpanInfo.mSpanStart);
3808 spanUnionEnd = Math.max(spanUnionEnd, mMisspelledSpanInfo.mSpanEnd);
Gilles Debunned88876a2012-03-16 17:34:04 -07003809 }
3810
3811 for (int i = 0; i < mNumberOfSuggestions; i++) {
3812 highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
3813 }
3814
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003815 // Make "Add to dictionary" item visible if there is a span with the misspelled flag
3816 int addToDictionaryButtonVisibility = View.GONE;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003817 if (mMisspelledSpanInfo.mSuggestionSpan != null) {
3818 if (mMisspelledSpanInfo.mSpanStart >= 0
3819 && mMisspelledSpanInfo.mSpanEnd > mMisspelledSpanInfo.mSpanStart) {
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003820 addToDictionaryButtonVisibility = View.VISIBLE;
Gilles Debunned88876a2012-03-16 17:34:04 -07003821 }
3822 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003823 mAddToDictionaryButton.setVisibility(addToDictionaryButtonVisibility);
Gilles Debunned88876a2012-03-16 17:34:04 -07003824
3825 if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003826 final int underlineColor;
3827 if (mNumberOfSuggestions != 0) {
3828 underlineColor =
3829 mSuggestionInfos[0].mSuggestionSpanInfo.mSuggestionSpan.getUnderlineColor();
3830 } else {
3831 underlineColor = mMisspelledSpanInfo.mSuggestionSpan.getUnderlineColor();
3832 }
3833
Gilles Debunned88876a2012-03-16 17:34:04 -07003834 if (underlineColor == 0) {
3835 // Fallback on the default highlight color when the first span does not provide one
3836 mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor);
3837 } else {
3838 final float BACKGROUND_TRANSPARENCY = 0.4f;
3839 final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
3840 mSuggestionRangeSpan.setBackgroundColor(
3841 (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
3842 }
3843 spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
3844 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
3845
3846 mSuggestionsAdapter.notifyDataSetChanged();
3847 return true;
3848 }
3849
3850 private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
3851 int unionEnd) {
3852 final Spannable text = (Spannable) mTextView.getText();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003853 final int spanStart = suggestionInfo.mSuggestionSpanInfo.mSpanStart;
3854 final int spanEnd = suggestionInfo.mSuggestionSpanInfo.mSpanEnd;
Gilles Debunned88876a2012-03-16 17:34:04 -07003855
3856 // Adjust the start/end of the suggestion span
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003857 suggestionInfo.mSuggestionStart = spanStart - unionStart;
3858 suggestionInfo.mSuggestionEnd = suggestionInfo.mSuggestionStart
3859 + suggestionInfo.mText.length();
Gilles Debunned88876a2012-03-16 17:34:04 -07003860
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003861 suggestionInfo.mText.setSpan(mHighlightSpan, 0, suggestionInfo.mText.length(),
Seigo Nonakabffbd302015-08-18 18:27:56 -07003862 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003863
3864 // Add the text before and after the span.
3865 final String textAsString = text.toString();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003866 suggestionInfo.mText.insert(0, textAsString.substring(unionStart, spanStart));
3867 suggestionInfo.mText.append(textAsString.substring(spanEnd, unionEnd));
Gilles Debunned88876a2012-03-16 17:34:04 -07003868 }
3869
3870 @Override
3871 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003872 SuggestionInfo suggestionInfo = mSuggestionInfos[position];
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003873 replaceWithSuggestion(suggestionInfo);
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003874 hideWithCleanUp();
Gilles Debunned88876a2012-03-16 17:34:04 -07003875 }
3876 }
3877
3878 /**
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003879 * An ActionMode Callback class that is used to provide actions while in text insertion or
3880 * selection mode.
Gilles Debunned88876a2012-03-16 17:34:04 -07003881 *
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003882 * The default callback provides a subset of Select All, Cut, Copy, Paste, Share and Replace
3883 * actions, depending on which of these this TextView supports and the current selection.
Gilles Debunned88876a2012-03-16 17:34:04 -07003884 */
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003885 private class TextActionModeCallback extends ActionMode.Callback2 {
Clara Bayarriea4f1502015-03-18 00:25:01 +00003886 private final Path mSelectionPath = new Path();
3887 private final RectF mSelectionBounds = new RectF();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003888 private final boolean mHasSelection;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003889 private final int mHandleHeight;
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01003890 private final Map<MenuItem, OnClickListener> mAssistClickHandlers = new HashMap<>();
Clara Bayarriea4f1502015-03-18 00:25:01 +00003891
Richard Ledley26b87222017-11-30 10:54:08 +00003892 TextActionModeCallback(@TextActionMode int mode) {
3893 mHasSelection = mode == TextActionMode.SELECTION
3894 || (mTextIsSelectable && mode == TextActionMode.TEXT_LINK);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003895 if (mHasSelection) {
3896 SelectionModifierCursorController selectionController = getSelectionController();
3897 if (selectionController.mStartHandle == null) {
3898 // As these are for initializing selectionController, hide() must be called.
3899 selectionController.initDrawables();
3900 selectionController.initHandles();
3901 selectionController.hide();
3902 }
3903 mHandleHeight = Math.max(
3904 mSelectHandleLeft.getMinimumHeight(),
3905 mSelectHandleRight.getMinimumHeight());
3906 } else {
3907 InsertionPointCursorController insertionController = getInsertionController();
3908 if (insertionController != null) {
3909 insertionController.getHandle();
3910 mHandleHeight = mSelectHandleCenter.getMinimumHeight();
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003911 } else {
3912 mHandleHeight = 0;
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003913 }
Clara Bayarri7fc946e2015-03-31 14:48:33 +01003914 }
Clara Bayarriea4f1502015-03-18 00:25:01 +00003915 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003916
3917 @Override
3918 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01003919 mAssistClickHandlers.clear();
3920
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003921 mode.setTitle(null);
Clara Bayarri13152d12015-04-09 12:02:04 +01003922 mode.setSubtitle(null);
3923 mode.setTitleOptionalHint(true);
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003924 populateMenuWithItems(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01003925
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003926 Callback customCallback = getCustomCallback();
3927 if (customCallback != null) {
3928 if (!customCallback.onCreateActionMode(mode, menu)) {
Clara Bayarri01243ac2015-06-03 00:46:29 +01003929 // The custom mode can choose to cancel the action mode, dismiss selection.
3930 Selection.setSelection((Spannable) mTextView.getText(),
3931 mTextView.getSelectionEnd());
Clara Bayarri13152d12015-04-09 12:02:04 +01003932 return false;
3933 }
3934 }
3935
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07003936 if (mTextView.canProcessText()) {
3937 mProcessTextIntentActionsHandler.onInitializeMenu(menu);
3938 }
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +00003939
Abodunrinwa Tokideb2f492017-11-06 18:55:17 +00003940 if (mHasSelection && !mTextView.hasTransientState()) {
3941 mTextView.setHasTransientState(true);
Clara Bayarri13152d12015-04-09 12:02:04 +01003942 }
Abodunrinwa Tokideb2f492017-11-06 18:55:17 +00003943 return true;
Clara Bayarri13152d12015-04-09 12:02:04 +01003944 }
3945
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003946 private Callback getCustomCallback() {
3947 return mHasSelection
3948 ? mCustomSelectionActionModeCallback
3949 : mCustomInsertionActionModeCallback;
3950 }
3951
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003952 private void populateMenuWithItems(Menu menu) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003953 if (mTextView.canCut()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003954 menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003955 com.android.internal.R.string.cut)
3956 .setAlphabeticShortcut('x')
3957 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07003958 }
3959
3960 if (mTextView.canCopy()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003961 menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003962 com.android.internal.R.string.copy)
3963 .setAlphabeticShortcut('c')
3964 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07003965 }
3966
3967 if (mTextView.canPaste()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003968 menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003969 com.android.internal.R.string.paste)
3970 .setAlphabeticShortcut('v')
3971 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07003972 }
3973
Andrei Stingaceanu7f0c5bd2015-04-14 17:12:08 +01003974 if (mTextView.canShare()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003975 menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003976 com.android.internal.R.string.share)
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +00003977 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
Andrei Stingaceanu7f0c5bd2015-04-14 17:12:08 +01003978 }
3979
Felipe Leme2ac463e2017-03-13 14:06:25 -07003980 if (mTextView.canRequestAutofill()) {
Felipe Leme1c1626e2017-06-02 10:53:13 -07003981 final String selected = mTextView.getSelectedText();
3982 if (selected == null || selected.isEmpty()) {
3983 menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
3984 com.android.internal.R.string.autofill)
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01003985 .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
Felipe Leme1c1626e2017-06-02 10:53:13 -07003986 }
Felipe Leme2ac463e2017-03-13 14:06:25 -07003987 }
3988
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01003989 if (mTextView.canPasteAsPlainText()) {
3990 menu.add(
3991 Menu.NONE,
3992 TextView.ID_PASTE_AS_PLAIN_TEXT,
3993 MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
3994 com.android.internal.R.string.paste_as_plain_text)
3995 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
3996 }
3997
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003998 updateSelectAllItem(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01003999 updateReplaceItem(menu);
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004000 updateAssistMenuItems(menu);
Gilles Debunned88876a2012-03-16 17:34:04 -07004001 }
4002
4003 @Override
4004 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01004005 updateSelectAllItem(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01004006 updateReplaceItem(menu);
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004007 updateAssistMenuItems(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01004008
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004009 Callback customCallback = getCustomCallback();
4010 if (customCallback != null) {
4011 return customCallback.onPrepareActionMode(mode, menu);
Gilles Debunned88876a2012-03-16 17:34:04 -07004012 }
4013 return true;
4014 }
4015
Clara Bayarri3b69fd82015-06-03 21:52:02 +01004016 private void updateSelectAllItem(Menu menu) {
4017 boolean canSelectAll = mTextView.canSelectAllText();
4018 boolean selectAllItemExists = menu.findItem(TextView.ID_SELECT_ALL) != null;
4019 if (canSelectAll && !selectAllItemExists) {
4020 menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
4021 com.android.internal.R.string.selectAll)
4022 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
4023 } else if (!canSelectAll && selectAllItemExists) {
4024 menu.removeItem(TextView.ID_SELECT_ALL);
4025 }
4026 }
4027
Clara Bayarri13152d12015-04-09 12:02:04 +01004028 private void updateReplaceItem(Menu menu) {
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08004029 boolean canReplace = mTextView.isSuggestionsEnabled() && shouldOfferToShowSuggestions();
Clara Bayarri13152d12015-04-09 12:02:04 +01004030 boolean replaceItemExists = menu.findItem(TextView.ID_REPLACE) != null;
4031 if (canReplace && !replaceItemExists) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01004032 menu.add(Menu.NONE, TextView.ID_REPLACE, MENU_ITEM_ORDER_REPLACE,
4033 com.android.internal.R.string.replace)
4034 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
Clara Bayarri13152d12015-04-09 12:02:04 +01004035 } else if (!canReplace && replaceItemExists) {
4036 menu.removeItem(TextView.ID_REPLACE);
4037 }
4038 }
4039
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004040 private void updateAssistMenuItems(Menu menu) {
4041 clearAssistMenuItems(menu);
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +00004042 if (!shouldEnableAssistMenuItems()) {
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004043 return;
4044 }
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +01004045 final TextClassification textClassification =
4046 getSelectionActionModeHelper().getTextClassification();
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00004047 if (textClassification == null) {
4048 return;
4049 }
Jan Althaus20d346e2018-03-23 14:03:52 +01004050 if (!textClassification.getActions().isEmpty()) {
4051 // Primary assist action (Always shown).
4052 final MenuItem item = addAssistMenuItem(menu,
4053 textClassification.getActions().get(0), TextView.ID_ASSIST,
4054 MENU_ITEM_ORDER_ASSIST, MenuItem.SHOW_AS_ACTION_ALWAYS);
4055 item.setIntent(textClassification.getIntent());
4056 } else if (hasLegacyAssistItem(textClassification)) {
4057 // Legacy primary assist action (Always shown).
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00004058 final MenuItem item = menu.add(
4059 TextView.ID_ASSIST, TextView.ID_ASSIST, MENU_ITEM_ORDER_ASSIST,
4060 textClassification.getLabel())
4061 .setIcon(textClassification.getIcon())
4062 .setIntent(textClassification.getIntent());
4063 item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Jan Althaus20d346e2018-03-23 14:03:52 +01004064 mAssistClickHandlers.put(item, TextClassification.createIntentOnClickListener(
4065 TextClassification.createPendingIntent(mTextView.getContext(),
Abodunrinwa Toki904a9312018-04-18 21:21:27 +01004066 textClassification.getIntent(),
4067 createAssistMenuItemPendingIntentRequestCode())));
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00004068 }
Jan Althaus20d346e2018-03-23 14:03:52 +01004069 final int count = textClassification.getActions().size();
4070 for (int i = 1; i < count; i++) {
4071 // Secondary assist action (Never shown).
4072 addAssistMenuItem(menu, textClassification.getActions().get(i), Menu.NONE,
4073 MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START + i - 1,
4074 MenuItem.SHOW_AS_ACTION_NEVER);
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00004075 }
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +00004076 }
4077
Jan Althaus20d346e2018-03-23 14:03:52 +01004078 private MenuItem addAssistMenuItem(Menu menu, RemoteAction action, int intemId, int order,
4079 int showAsAction) {
4080 final MenuItem item = menu.add(TextView.ID_ASSIST, intemId, order, action.getTitle())
4081 .setContentDescription(action.getContentDescription());
4082 if (action.shouldShowIcon()) {
4083 item.setIcon(action.getIcon().loadDrawable(mTextView.getContext()));
4084 }
4085 item.setShowAsAction(showAsAction);
4086 mAssistClickHandlers.put(item,
4087 TextClassification.createIntentOnClickListener(action.getActionIntent()));
4088 return item;
4089 }
4090
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004091 private void clearAssistMenuItems(Menu menu) {
4092 int i = 0;
4093 while (i < menu.size()) {
4094 final MenuItem menuItem = menu.getItem(i);
4095 if (menuItem.getGroupId() == TextView.ID_ASSIST) {
4096 menu.removeItem(menuItem.getItemId());
4097 continue;
4098 }
4099 i++;
4100 }
4101 }
4102
Jan Althaus20d346e2018-03-23 14:03:52 +01004103 private boolean hasLegacyAssistItem(TextClassification classification) {
4104 // Check whether we have the UI data and and action.
4105 return (classification.getIcon() != null || !TextUtils.isEmpty(
4106 classification.getLabel())) && (classification.getIntent() != null
4107 || classification.getOnClickListener() != null);
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004108 }
4109
4110 private boolean onAssistMenuItemClicked(MenuItem assistMenuItem) {
4111 Preconditions.checkArgument(assistMenuItem.getGroupId() == TextView.ID_ASSIST);
4112
4113 final TextClassification textClassification =
4114 getSelectionActionModeHelper().getTextClassification();
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +00004115 if (!shouldEnableAssistMenuItems() || textClassification == null) {
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004116 // No textClassification result to handle the click. Eat the click.
4117 return true;
4118 }
4119
4120 OnClickListener onClickListener = mAssistClickHandlers.get(assistMenuItem);
4121 if (onClickListener == null) {
4122 final Intent intent = assistMenuItem.getIntent();
4123 if (intent != null) {
Abodunrinwa Toki2f19b922018-02-12 19:59:28 +00004124 onClickListener = TextClassification.createIntentOnClickListener(
Abodunrinwa Toki904a9312018-04-18 21:21:27 +01004125 TextClassification.createPendingIntent(
4126 mTextView.getContext(), intent,
4127 createAssistMenuItemPendingIntentRequestCode()));
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004128 }
4129 }
4130 if (onClickListener != null) {
4131 onClickListener.onClick(mTextView);
4132 stopTextActionMode();
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004133 }
4134 // We tried our best.
4135 return true;
Abodunrinwa Toki9796a1b2017-06-28 02:49:07 +01004136 }
4137
Abodunrinwa Toki904a9312018-04-18 21:21:27 +01004138 private int createAssistMenuItemPendingIntentRequestCode() {
4139 return mTextView.hasSelection()
4140 ? mTextView.getText().subSequence(
4141 mTextView.getSelectionStart(), mTextView.getSelectionEnd())
4142 .hashCode()
4143 : 0;
4144 }
4145
Abodunrinwa Tokidb8fc312018-02-26 21:37:51 +00004146 private boolean shouldEnableAssistMenuItems() {
4147 return mTextView.isDeviceProvisioned()
4148 && TextClassificationManager.getSettings(mTextView.getContext())
4149 .isSmartTextShareEnabled();
4150 }
4151
Gilles Debunned88876a2012-03-16 17:34:04 -07004152 @Override
4153 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01004154 getSelectionActionModeHelper().onSelectionAction(item.getItemId());
Abodunrinwa Toki1d775572017-05-08 16:03:01 +01004155
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07004156 if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +00004157 return true;
4158 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004159 Callback customCallback = getCustomCallback();
4160 if (customCallback != null && customCallback.onActionItemClicked(mode, item)) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004161 return true;
4162 }
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004163 if (item.getGroupId() == TextView.ID_ASSIST && onAssistMenuItemClicked(item)) {
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +00004164 return true;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00004165 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004166 return mTextView.onTextContextMenuItem(item.getItemId());
4167 }
4168
4169 @Override
4170 public void onDestroyActionMode(ActionMode mode) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09004171 // Clear mTextActionMode not to recursively destroy action mode by clearing selection.
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +00004172 getSelectionActionModeHelper().onDestroyActionMode();
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09004173 mTextActionMode = null;
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004174 Callback customCallback = getCustomCallback();
4175 if (customCallback != null) {
4176 customCallback.onDestroyActionMode(mode);
Gilles Debunned88876a2012-03-16 17:34:04 -07004177 }
Adam Powell057a5852012-05-11 10:28:38 -07004178
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08004179 if (!mPreserveSelection) {
4180 /*
4181 * Leave current selection when we tentatively destroy action mode for the
4182 * selection. If we're detaching from a window, we'll bring back the selection
4183 * mode when (if) we get reattached.
4184 */
Adam Powell057a5852012-05-11 10:28:38 -07004185 Selection.setSelection((Spannable) mTextView.getText(),
4186 mTextView.getSelectionEnd());
Adam Powell057a5852012-05-11 10:28:38 -07004187 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004188
4189 if (mSelectionModifierCursorController != null) {
4190 mSelectionModifierCursorController.hide();
4191 }
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004192
4193 mAssistClickHandlers.clear();
Abodunrinwa Toki52096912018-03-21 23:14:42 +00004194 mRequestingLinkActionMode = false;
Gilles Debunned88876a2012-03-16 17:34:04 -07004195 }
Clara Bayarriea4f1502015-03-18 00:25:01 +00004196
4197 @Override
4198 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
4199 if (!view.equals(mTextView) || mTextView.getLayout() == null) {
4200 super.onGetContentRect(mode, view, outRect);
4201 return;
4202 }
4203 if (mTextView.getSelectionStart() != mTextView.getSelectionEnd()) {
4204 // We have a selection.
4205 mSelectionPath.reset();
4206 mTextView.getLayout().getSelectionPath(
4207 mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mSelectionPath);
4208 mSelectionPath.computeBounds(mSelectionBounds, true);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004209 mSelectionBounds.bottom += mHandleHeight;
Clara Bayarriea4f1502015-03-18 00:25:01 +00004210 } else {
Roozbeh Pournader9c133072017-07-26 22:36:27 -07004211 // We have a cursor.
Siyamed Sinir987ec652016-02-17 19:44:41 -08004212 Layout layout = mTextView.getLayout();
Mady Mellorff66ca52015-07-08 12:31:45 -07004213 int line = layout.getLineForOffset(mTextView.getSelectionStart());
Siyamed Sinir987ec652016-02-17 19:44:41 -08004214 float primaryHorizontal = clampHorizontalPosition(null,
4215 layout.getPrimaryHorizontal(mTextView.getSelectionStart()));
Clara Bayarriea4f1502015-03-18 00:25:01 +00004216 mSelectionBounds.set(
4217 primaryHorizontal,
Mady Mellorff66ca52015-07-08 12:31:45 -07004218 layout.getLineTop(line),
Clara Bayarrif95ed102015-08-12 19:46:47 +01004219 primaryHorizontal,
Siyamed Sinirfdbc5ee2018-02-09 11:24:16 -08004220 layout.getLineBottom(line) + mHandleHeight);
Clara Bayarriea4f1502015-03-18 00:25:01 +00004221 }
4222 // Take TextView's padding and scroll into account.
4223 int textHorizontalOffset = mTextView.viewportToContentHorizontalOffset();
4224 int textVerticalOffset = mTextView.viewportToContentVerticalOffset();
4225 outRect.set(
4226 (int) Math.floor(mSelectionBounds.left + textHorizontalOffset),
4227 (int) Math.floor(mSelectionBounds.top + textVerticalOffset),
4228 (int) Math.ceil(mSelectionBounds.right + textHorizontalOffset),
4229 (int) Math.ceil(mSelectionBounds.bottom + textVerticalOffset));
4230 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004231 }
4232
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004233 /**
4234 * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}
4235 * while the input method is requesting the cursor/anchor position. Does nothing as long as
4236 * {@link InputMethodManager#isWatchingCursor(View)} returns false.
4237 */
4238 private final class CursorAnchorInfoNotifier implements TextViewPositionListener {
Yohei Yukawac46b5f02014-06-10 12:26:34 +09004239 final CursorAnchorInfo.Builder mSelectionInfoBuilder = new CursorAnchorInfo.Builder();
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004240 final int[] mTmpIntOffset = new int[2];
4241 final Matrix mViewToScreenMatrix = new Matrix();
4242
4243 @Override
4244 public void updatePosition(int parentPositionX, int parentPositionY,
4245 boolean parentPositionChanged, boolean parentScrolled) {
4246 final InputMethodState ims = mInputMethodState;
4247 if (ims == null || ims.mBatchEditNesting > 0) {
4248 return;
4249 }
4250 final InputMethodManager imm = InputMethodManager.peekInstance();
4251 if (null == imm) {
4252 return;
4253 }
Yohei Yukawa0023d0e2014-07-11 04:13:03 +09004254 if (!imm.isActive(mTextView)) {
4255 return;
4256 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004257 // Skip if the IME has not requested the cursor/anchor position.
Yohei Yukawa0023d0e2014-07-11 04:13:03 +09004258 if (!imm.isCursorAnchorInfoEnabled()) {
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004259 return;
4260 }
4261 Layout layout = mTextView.getLayout();
4262 if (layout == null) {
4263 return;
4264 }
4265
Yohei Yukawac46b5f02014-06-10 12:26:34 +09004266 final CursorAnchorInfo.Builder builder = mSelectionInfoBuilder;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004267 builder.reset();
4268
4269 final int selectionStart = mTextView.getSelectionStart();
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004270 builder.setSelectionRange(selectionStart, mTextView.getSelectionEnd());
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004271
4272 // Construct transformation matrix from view local coordinates to screen coordinates.
4273 mViewToScreenMatrix.set(mTextView.getMatrix());
4274 mTextView.getLocationOnScreen(mTmpIntOffset);
4275 mViewToScreenMatrix.postTranslate(mTmpIntOffset[0], mTmpIntOffset[1]);
4276 builder.setMatrix(mViewToScreenMatrix);
4277
4278 final float viewportToContentHorizontalOffset =
4279 mTextView.viewportToContentHorizontalOffset();
4280 final float viewportToContentVerticalOffset =
4281 mTextView.viewportToContentVerticalOffset();
4282
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004283 final CharSequence text = mTextView.getText();
4284 if (text instanceof Spannable) {
4285 final Spannable sp = (Spannable) text;
4286 int composingTextStart = EditableInputConnection.getComposingSpanStart(sp);
4287 int composingTextEnd = EditableInputConnection.getComposingSpanEnd(sp);
4288 if (composingTextEnd < composingTextStart) {
4289 final int temp = composingTextEnd;
4290 composingTextEnd = composingTextStart;
4291 composingTextStart = temp;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004292 }
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004293 final boolean hasComposingText =
4294 (0 <= composingTextStart) && (composingTextStart < composingTextEnd);
4295 if (hasComposingText) {
4296 final CharSequence composingText = text.subSequence(composingTextStart,
4297 composingTextEnd);
4298 builder.setComposingText(composingTextStart, composingText);
Phil Weaverc2e28932016-12-08 12:29:25 -08004299 mTextView.populateCharacterBounds(builder, composingTextStart,
4300 composingTextEnd, viewportToContentHorizontalOffset,
4301 viewportToContentVerticalOffset);
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004302 }
4303 }
4304
4305 // Treat selectionStart as the insertion point.
4306 if (0 <= selectionStart) {
4307 final int offset = selectionStart;
4308 final int line = layout.getLineForOffset(offset);
4309 final float insertionMarkerX = layout.getPrimaryHorizontal(offset)
4310 + viewportToContentHorizontalOffset;
4311 final float insertionMarkerTop = layout.getLineTop(line)
4312 + viewportToContentVerticalOffset;
4313 final float insertionMarkerBaseline = layout.getLineBaseline(line)
4314 + viewportToContentVerticalOffset;
Siyamed Sinira60b59d2017-07-26 09:26:41 -07004315 final float insertionMarkerBottom = layout.getLineBottomWithoutSpacing(line)
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004316 + viewportToContentVerticalOffset;
Phil Weaverc2e28932016-12-08 12:29:25 -08004317 final boolean isTopVisible = mTextView
4318 .isPositionVisible(insertionMarkerX, insertionMarkerTop);
4319 final boolean isBottomVisible = mTextView
4320 .isPositionVisible(insertionMarkerX, insertionMarkerBottom);
Yohei Yukawacc24e2b2014-08-29 20:21:10 -07004321 int insertionMarkerFlags = 0;
4322 if (isTopVisible || isBottomVisible) {
4323 insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
4324 }
4325 if (!isTopVisible || !isBottomVisible) {
4326 insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
4327 }
Yohei Yukawa5f183f02014-09-02 14:18:40 -07004328 if (layout.isRtlCharAt(offset)) {
4329 insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL;
4330 }
Yohei Yukawa0b01e7f2014-07-08 15:29:51 +09004331 builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop,
Yohei Yukawacc24e2b2014-08-29 20:21:10 -07004332 insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags);
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004333 }
4334
4335 imm.updateCursorAnchorInfo(mTextView, builder.build());
4336 }
4337 }
4338
Mihai Popa38722382018-03-07 19:56:21 +00004339 private static class MagnifierMotionAnimator {
4340 private static final long DURATION = 100 /* miliseconds */;
4341
4342 // The magnifier being animated.
4343 private final Magnifier mMagnifier;
4344 // A value animator used to animate the magnifier.
4345 private final ValueAnimator mAnimator;
4346
4347 // Whether the magnifier is currently visible.
4348 private boolean mMagnifierIsShowing;
4349 // The coordinates of the magnifier when the currently running animation started.
4350 private float mAnimationStartX;
4351 private float mAnimationStartY;
4352 // The coordinates of the magnifier in the latest animation frame.
4353 private float mAnimationCurrentX;
4354 private float mAnimationCurrentY;
4355 // The latest coordinates the motion animator was asked to #show() the magnifier at.
4356 private float mLastX;
4357 private float mLastY;
4358
4359 private MagnifierMotionAnimator(final Magnifier magnifier) {
4360 mMagnifier = magnifier;
4361 // Prepare the animator used to run the motion animation.
4362 mAnimator = ValueAnimator.ofFloat(0, 1);
4363 mAnimator.setDuration(DURATION);
4364 mAnimator.setInterpolator(new LinearInterpolator());
4365 mAnimator.addUpdateListener((animation) -> {
4366 // Interpolate to find the current position of the magnifier.
4367 mAnimationCurrentX = mAnimationStartX
4368 + (mLastX - mAnimationStartX) * animation.getAnimatedFraction();
4369 mAnimationCurrentY = mAnimationStartY
4370 + (mLastY - mAnimationStartY) * animation.getAnimatedFraction();
4371 mMagnifier.show(mAnimationCurrentX, mAnimationCurrentY);
4372 });
4373 }
4374
4375 /**
4376 * Shows the magnifier at a new position.
4377 * If the y coordinate is different from the previous y coordinate
4378 * (probably corresponding to a line jump in the text), a short
4379 * animation is added to the jump.
4380 */
4381 private void show(final float x, final float y) {
4382 final boolean startNewAnimation = mMagnifierIsShowing && y != mLastY;
4383
4384 if (startNewAnimation) {
4385 if (mAnimator.isRunning()) {
4386 mAnimator.cancel();
4387 mAnimationStartX = mAnimationCurrentX;
4388 mAnimationStartY = mAnimationCurrentY;
4389 } else {
4390 mAnimationStartX = mLastX;
4391 mAnimationStartY = mLastY;
4392 }
4393 mAnimator.start();
4394 } else {
4395 if (!mAnimator.isRunning()) {
4396 mMagnifier.show(x, y);
4397 }
4398 }
4399 mLastX = x;
4400 mLastY = y;
4401 mMagnifierIsShowing = true;
4402 }
4403
4404 /**
4405 * Updates the content of the magnifier.
4406 */
4407 private void update() {
4408 mMagnifier.update();
4409 }
4410
4411 /**
4412 * Dismisses the magnifier, or does nothing if it is already dismissed.
4413 */
4414 private void dismiss() {
4415 mMagnifier.dismiss();
4416 mAnimator.cancel();
4417 mMagnifierIsShowing = false;
4418 }
4419 }
4420
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004421 @VisibleForTesting
4422 public abstract class HandleView extends View implements TextViewPositionListener {
Gilles Debunned88876a2012-03-16 17:34:04 -07004423 protected Drawable mDrawable;
4424 protected Drawable mDrawableLtr;
4425 protected Drawable mDrawableRtl;
4426 private final PopupWindow mContainer;
4427 // Position with respect to the parent TextView
4428 private int mPositionX, mPositionY;
4429 private boolean mIsDragging;
4430 // Offset from touch position to mPosition
4431 private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
4432 protected int mHotspotX;
Adam Powell3fceabd2014-08-19 18:28:04 -07004433 protected int mHorizontalGravity;
Gilles Debunned88876a2012-03-16 17:34:04 -07004434 // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
4435 private float mTouchOffsetY;
4436 // Where the touch position should be on the handle to ensure a maximum cursor visibility
4437 private float mIdealVerticalOffset;
4438 // Parent's (TextView) previous position in window
4439 private int mLastParentX, mLastParentY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004440 // Parent's (TextView) previous position on screen
4441 private int mLastParentXOnScreen, mLastParentYOnScreen;
Gilles Debunned88876a2012-03-16 17:34:04 -07004442 // Previous text character offset
Mady Mellorc2225b92015-04-01 15:59:20 -07004443 protected int mPreviousOffset = -1;
Gilles Debunned88876a2012-03-16 17:34:04 -07004444 // Previous text character offset
4445 private boolean mPositionHasChanged = true;
Adam Powell3fceabd2014-08-19 18:28:04 -07004446 // Minimum touch target size for handles
4447 private int mMinSize;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004448 // Indicates the line of text that the handle is on.
Mady Mellora6a0f782015-07-10 16:43:32 -07004449 protected int mPrevLine = UNSET_LINE;
4450 // Indicates the line of text that the user was touching. This can differ from mPrevLine
4451 // when selecting text when the handles jump to the end / start of words which may be on
4452 // a different line.
4453 protected int mPreviousLineTouched = UNSET_LINE;
Gilles Debunned88876a2012-03-16 17:34:04 -07004454
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004455 private HandleView(Drawable drawableLtr, Drawable drawableRtl, final int id) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004456 super(mTextView.getContext());
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004457 setId(id);
Gilles Debunned88876a2012-03-16 17:34:04 -07004458 mContainer = new PopupWindow(mTextView.getContext(), null,
4459 com.android.internal.R.attr.textSelectHandleWindowStyle);
4460 mContainer.setSplitTouchEnabled(true);
4461 mContainer.setClippingEnabled(false);
4462 mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
Keisuke Kuroyanagi7340be72015-02-27 17:57:49 +09004463 mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
4464 mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
Gilles Debunned88876a2012-03-16 17:34:04 -07004465 mContainer.setContentView(this);
4466
4467 mDrawableLtr = drawableLtr;
4468 mDrawableRtl = drawableRtl;
Adam Powell3fceabd2014-08-19 18:28:04 -07004469 mMinSize = mTextView.getContext().getResources().getDimensionPixelSize(
4470 com.android.internal.R.dimen.text_handle_min_size);
Gilles Debunned88876a2012-03-16 17:34:04 -07004471
4472 updateDrawable();
4473
Adam Powell3fceabd2014-08-19 18:28:04 -07004474 final int handleHeight = getPreferredHeight();
Gilles Debunned88876a2012-03-16 17:34:04 -07004475 mTouchOffsetY = -0.3f * handleHeight;
4476 mIdealVerticalOffset = 0.7f * handleHeight;
4477 }
4478
Mady Mellor7a936442015-05-20 10:05:52 -07004479 public float getIdealVerticalOffset() {
4480 return mIdealVerticalOffset;
4481 }
4482
Gilles Debunned88876a2012-03-16 17:34:04 -07004483 protected void updateDrawable() {
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004484 if (mIsDragging) {
4485 // Don't update drawable during dragging.
4486 return;
4487 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004488 final Layout layout = mTextView.getLayout();
4489 if (layout == null) {
4490 return;
4491 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004492 final int offset = getCurrentCursorOffset();
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004493 final boolean isRtlCharAtOffset = isAtRtlRun(layout, offset);
Keisuke Kuroyanagi33f81ac2015-05-14 20:10:57 +09004494 final Drawable oldDrawable = mDrawable;
Gilles Debunned88876a2012-03-16 17:34:04 -07004495 mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
4496 mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
Adam Powell3fceabd2014-08-19 18:28:04 -07004497 mHorizontalGravity = getHorizontalGravity(isRtlCharAtOffset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004498 if (oldDrawable != mDrawable && isShowing()) {
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004499 // Update popup window position.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004500 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
4501 - getHorizontalOffset() + getCursorOffset();
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004502 mPositionX += mTextView.viewportToContentHorizontalOffset();
4503 mPositionHasChanged = true;
4504 updatePosition(mLastParentX, mLastParentY, false, false);
Keisuke Kuroyanagi33f81ac2015-05-14 20:10:57 +09004505 postInvalidate();
4506 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004507 }
4508
4509 protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
Adam Powell3fceabd2014-08-19 18:28:04 -07004510 protected abstract int getHorizontalGravity(boolean isRtlRun);
Gilles Debunned88876a2012-03-16 17:34:04 -07004511
4512 // Touch-up filter: number of previous positions remembered
4513 private static final int HISTORY_SIZE = 5;
4514 private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
4515 private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
4516 private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
4517 private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
4518 private int mPreviousOffsetIndex = 0;
4519 private int mNumberPreviousOffsets = 0;
4520
4521 private void startTouchUpFilter(int offset) {
4522 mNumberPreviousOffsets = 0;
4523 addPositionToTouchUpFilter(offset);
4524 }
4525
4526 private void addPositionToTouchUpFilter(int offset) {
4527 mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
4528 mPreviousOffsets[mPreviousOffsetIndex] = offset;
4529 mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
4530 mNumberPreviousOffsets++;
4531 }
4532
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004533 private void filterOnTouchUp(boolean fromTouchScreen) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004534 final long now = SystemClock.uptimeMillis();
4535 int i = 0;
4536 int index = mPreviousOffsetIndex;
4537 final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
4538 while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
4539 i++;
4540 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
4541 }
4542
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004543 if (i > 0 && i < iMax
4544 && (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004545 positionAtCursorOffset(mPreviousOffsets[index], false, fromTouchScreen);
Gilles Debunned88876a2012-03-16 17:34:04 -07004546 }
4547 }
4548
4549 public boolean offsetHasBeenChanged() {
4550 return mNumberPreviousOffsets > 1;
4551 }
4552
4553 @Override
4554 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Adam Powell3fceabd2014-08-19 18:28:04 -07004555 setMeasuredDimension(getPreferredWidth(), getPreferredHeight());
4556 }
4557
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004558 @Override
4559 public void invalidate() {
4560 super.invalidate();
4561 if (isShowing()) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004562 positionAtCursorOffset(getCurrentCursorOffset(), true, false);
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004563 }
4564 };
4565
Adam Powell3fceabd2014-08-19 18:28:04 -07004566 private int getPreferredWidth() {
4567 return Math.max(mDrawable.getIntrinsicWidth(), mMinSize);
4568 }
4569
4570 private int getPreferredHeight() {
4571 return Math.max(mDrawable.getIntrinsicHeight(), mMinSize);
Gilles Debunned88876a2012-03-16 17:34:04 -07004572 }
4573
4574 public void show() {
4575 if (isShowing()) return;
4576
4577 getPositionListener().addSubscriber(this, true /* local position may change */);
4578
4579 // Make sure the offset is always considered new, even when focusing at same position
4580 mPreviousOffset = -1;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004581 positionAtCursorOffset(getCurrentCursorOffset(), false, false);
Gilles Debunned88876a2012-03-16 17:34:04 -07004582 }
4583
4584 protected void dismiss() {
4585 mIsDragging = false;
4586 mContainer.dismiss();
4587 onDetached();
4588 }
4589
4590 public void hide() {
4591 dismiss();
4592
4593 getPositionListener().removeSubscriber(this);
4594 }
4595
Gilles Debunned88876a2012-03-16 17:34:04 -07004596 public boolean isShowing() {
4597 return mContainer.isShowing();
4598 }
4599
Mihai Popab1b423a2018-03-27 19:03:09 +01004600 private boolean shouldShow() {
4601 // A dragging handle should always be shown.
Gilles Debunned88876a2012-03-16 17:34:04 -07004602 if (mIsDragging) {
4603 return true;
4604 }
4605
4606 if (mTextView.isInBatchEditMode()) {
4607 return false;
4608 }
4609
Phil Weaverc2e28932016-12-08 12:29:25 -08004610 return mTextView.isPositionVisible(
4611 mPositionX + mHotspotX + getHorizontalOffset(), mPositionY);
Gilles Debunned88876a2012-03-16 17:34:04 -07004612 }
4613
Mihai Popab1b423a2018-03-27 19:03:09 +01004614 private void setVisible(final boolean visible) {
4615 mContainer.getContentView().setVisibility(visible ? VISIBLE : INVISIBLE);
4616 }
4617
Gilles Debunned88876a2012-03-16 17:34:04 -07004618 public abstract int getCurrentCursorOffset();
4619
4620 protected abstract void updateSelection(int offset);
4621
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004622 protected abstract void updatePosition(float x, float y, boolean fromTouchScreen);
Gilles Debunned88876a2012-03-16 17:34:04 -07004623
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004624 @MagnifierHandleTrigger
4625 protected abstract int getMagnifierHandleTrigger();
4626
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004627 protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
4628 return layout.isRtlCharAt(offset);
4629 }
4630
4631 @VisibleForTesting
4632 public float getHorizontal(@NonNull Layout layout, int offset) {
4633 return layout.getPrimaryHorizontal(offset);
4634 }
4635
4636 protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
4637 return mTextView.getOffsetAtCoordinate(line, x);
4638 }
4639
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004640 /**
4641 * @param offset Cursor offset. Must be in [-1, length].
4642 * @param forceUpdatePosition whether to force update the position. This should be true
4643 * when If the parent has been scrolled, for example.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004644 * @param fromTouchScreen {@code true} if the cursor is moved with motion events from the
4645 * touch screen.
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004646 */
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004647 protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
4648 boolean fromTouchScreen) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004649 // A HandleView relies on the layout, which may be nulled by external methods
4650 Layout layout = mTextView.getLayout();
4651 if (layout == null) {
4652 // Will update controllers' state, hiding them and stopping selection mode if needed
4653 prepareCursorControllers();
4654 return;
4655 }
Siyamed Sinir987ec652016-02-17 19:44:41 -08004656 layout = mTextView.getLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -07004657
4658 boolean offsetChanged = offset != mPreviousOffset;
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004659 if (offsetChanged || forceUpdatePosition) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004660 if (offsetChanged) {
4661 updateSelection(offset);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004662 if (fromTouchScreen && mHapticTextHandleEnabled) {
4663 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
4664 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004665 addPositionToTouchUpFilter(offset);
4666 }
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07004667 final int line = layout.getLineForOffset(offset);
Mady Mellorb9bbbb12015-03-23 11:50:46 -07004668 mPrevLine = line;
Gilles Debunned88876a2012-03-16 17:34:04 -07004669
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004670 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
4671 - getHorizontalOffset() + getCursorOffset();
Siyamed Sinira60b59d2017-07-26 09:26:41 -07004672 mPositionY = layout.getLineBottomWithoutSpacing(line);
Gilles Debunned88876a2012-03-16 17:34:04 -07004673
4674 // Take TextView's padding and scroll into account.
4675 mPositionX += mTextView.viewportToContentHorizontalOffset();
4676 mPositionY += mTextView.viewportToContentVerticalOffset();
4677
4678 mPreviousOffset = offset;
4679 mPositionHasChanged = true;
4680 }
4681 }
4682
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004683 /**
Roozbeh Pournader9c133072017-07-26 22:36:27 -07004684 * Return the clamped horizontal position for the cursor.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004685 *
4686 * @param layout Text layout.
4687 * @param offset Character offset for the cursor.
4688 * @return The clamped horizontal position for the cursor.
4689 */
4690 int getCursorHorizontalPosition(Layout layout, int offset) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004691 return (int) (getHorizontal(layout, offset) - 0.5f);
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004692 }
4693
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004694 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07004695 public void updatePosition(int parentPositionX, int parentPositionY,
4696 boolean parentPositionChanged, boolean parentScrolled) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004697 positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled, false);
Gilles Debunned88876a2012-03-16 17:34:04 -07004698 if (parentPositionChanged || mPositionHasChanged) {
4699 if (mIsDragging) {
4700 // Update touchToWindow offset in case of parent scrolling while dragging
4701 if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
4702 mTouchToWindowOffsetX += parentPositionX - mLastParentX;
4703 mTouchToWindowOffsetY += parentPositionY - mLastParentY;
4704 mLastParentX = parentPositionX;
4705 mLastParentY = parentPositionY;
4706 }
4707
4708 onHandleMoved();
4709 }
4710
Mihai Popab1b423a2018-03-27 19:03:09 +01004711 if (shouldShow()) {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004712 // Transform to the window coordinates to follow the view tranformation.
4713 final int[] pts = { mPositionX + mHotspotX + getHorizontalOffset(), mPositionY};
4714 mTextView.transformFromViewToWindowSpace(pts);
4715 pts[0] -= mHotspotX + getHorizontalOffset();
4716
Gilles Debunned88876a2012-03-16 17:34:04 -07004717 if (isShowing()) {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004718 mContainer.update(pts[0], pts[1], -1, -1);
Gilles Debunned88876a2012-03-16 17:34:04 -07004719 } else {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004720 mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY, pts[0], pts[1]);
Gilles Debunned88876a2012-03-16 17:34:04 -07004721 }
4722 } else {
4723 if (isShowing()) {
4724 dismiss();
4725 }
4726 }
4727
4728 mPositionHasChanged = false;
4729 }
4730 }
4731
4732 @Override
4733 protected void onDraw(Canvas c) {
Adam Powell3fceabd2014-08-19 18:28:04 -07004734 final int drawWidth = mDrawable.getIntrinsicWidth();
4735 final int left = getHorizontalOffset();
4736
4737 mDrawable.setBounds(left, 0, left + drawWidth, mDrawable.getIntrinsicHeight());
Gilles Debunned88876a2012-03-16 17:34:04 -07004738 mDrawable.draw(c);
4739 }
4740
Adam Powell3fceabd2014-08-19 18:28:04 -07004741 private int getHorizontalOffset() {
4742 final int width = getPreferredWidth();
4743 final int drawWidth = mDrawable.getIntrinsicWidth();
4744 final int left;
4745 switch (mHorizontalGravity) {
4746 case Gravity.LEFT:
4747 left = 0;
4748 break;
4749 default:
4750 case Gravity.CENTER:
4751 left = (width - drawWidth) / 2;
4752 break;
4753 case Gravity.RIGHT:
4754 left = width - drawWidth;
4755 break;
4756 }
4757 return left;
4758 }
4759
4760 protected int getCursorOffset() {
4761 return 0;
4762 }
4763
Mihai Popab1b423a2018-03-27 19:03:09 +01004764 private boolean tooLargeTextForMagnifier() {
4765 final float magnifierContentHeight = Math.round(
4766 mMagnifierAnimator.mMagnifier.getHeight()
4767 / mMagnifierAnimator.mMagnifier.getZoom());
4768 final Paint.FontMetrics fontMetrics = mTextView.getPaint().getFontMetrics();
4769 final float glyphHeight = fontMetrics.descent - fontMetrics.ascent;
4770 return glyphHeight > magnifierContentHeight;
4771 }
4772
Mihai Popae3017462018-03-07 12:25:21 +00004773 /**
4774 * Computes the position where the magnifier should be shown, relative to
4775 * {@code mTextView}, and writes them to {@code showPosInView}. Also decides
4776 * whether the magnifier should be shown or dismissed after this touch event.
4777 * @return Whether the magnifier should be shown at the computed coordinates or dismissed.
4778 */
4779 private boolean obtainMagnifierShowCoordinates(@NonNull final MotionEvent event,
4780 final PointF showPosInView) {
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004781
4782 final int trigger = getMagnifierHandleTrigger();
4783 final int offset;
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004784 final int otherHandleOffset;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004785 switch (trigger) {
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004786 case MagnifierHandleTrigger.INSERTION:
4787 offset = mTextView.getSelectionStart();
4788 otherHandleOffset = -1;
4789 break;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004790 case MagnifierHandleTrigger.SELECTION_START:
4791 offset = mTextView.getSelectionStart();
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004792 otherHandleOffset = mTextView.getSelectionEnd();
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004793 break;
4794 case MagnifierHandleTrigger.SELECTION_END:
4795 offset = mTextView.getSelectionEnd();
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004796 otherHandleOffset = mTextView.getSelectionStart();
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004797 break;
4798 default:
4799 offset = -1;
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004800 otherHandleOffset = -1;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004801 break;
4802 }
4803
4804 if (offset == -1) {
Mihai Popae3017462018-03-07 12:25:21 +00004805 return false;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004806 }
4807
4808 final Layout layout = mTextView.getLayout();
4809 final int lineNumber = layout.getLineForOffset(offset);
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004810 // Compute whether the selection handles are currently on the same line, and,
4811 // in this particular case, whether the selected text is right to left.
4812 final boolean sameLineSelection = otherHandleOffset != -1
4813 && lineNumber == layout.getLineForOffset(otherHandleOffset);
4814 final boolean rtl = sameLineSelection
4815 && (offset < otherHandleOffset)
4816 != (getHorizontal(mTextView.getLayout(), offset)
4817 < getHorizontal(mTextView.getLayout(), otherHandleOffset));
Mihai Popae3017462018-03-07 12:25:21 +00004818
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004819 // Horizontally move the magnifier smoothly, clamp inside the current line / selection.
Mihai Popa1d1ed0c2018-01-12 12:38:12 +00004820 final int[] textViewLocationOnScreen = new int[2];
4821 mTextView.getLocationOnScreen(textViewLocationOnScreen);
Mihai Popae3017462018-03-07 12:25:21 +00004822 final float touchXInView = event.getRawX() - textViewLocationOnScreen[0];
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004823 float leftBound = mTextView.getTotalPaddingLeft() - mTextView.getScrollX();
4824 float rightBound = mTextView.getTotalPaddingLeft() - mTextView.getScrollX();
4825 if (sameLineSelection && ((trigger == MagnifierHandleTrigger.SELECTION_END) ^ rtl)) {
4826 leftBound += getHorizontal(mTextView.getLayout(), otherHandleOffset);
4827 } else {
4828 leftBound += mTextView.getLayout().getLineLeft(lineNumber);
4829 }
4830 if (sameLineSelection && ((trigger == MagnifierHandleTrigger.SELECTION_START) ^ rtl)) {
4831 rightBound += getHorizontal(mTextView.getLayout(), otherHandleOffset);
4832 } else {
4833 rightBound += mTextView.getLayout().getLineRight(lineNumber);
4834 }
Mihai Popa38722382018-03-07 19:56:21 +00004835 final float contentWidth = Math.round(mMagnifierAnimator.mMagnifier.getWidth()
4836 / mMagnifierAnimator.mMagnifier.getZoom());
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004837 if (touchXInView < leftBound - contentWidth / 2
4838 || touchXInView > rightBound + contentWidth / 2) {
4839 // The touch is too far from the current line / selection, so hide the magnifier.
Mihai Popae3017462018-03-07 12:25:21 +00004840 return false;
4841 }
Mihai Popa27e4dfb2018-03-07 14:52:05 +00004842 showPosInView.x = Math.max(leftBound, Math.min(rightBound, touchXInView));
Mihai Popae3017462018-03-07 12:25:21 +00004843
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004844 // Vertically snap to middle of current line.
Mihai Popae3017462018-03-07 12:25:21 +00004845 showPosInView.y = (mTextView.getLayout().getLineTop(lineNumber)
Andrei Stingaceanuca189fe2017-10-19 17:02:22 +01004846 + mTextView.getLayout().getLineBottom(lineNumber)) / 2.0f
4847 + mTextView.getTotalPaddingTop() - mTextView.getScrollY();
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004848
Mihai Popae3017462018-03-07 12:25:21 +00004849 return true;
4850 }
Mihai Popaa4e39c42018-02-20 15:31:11 +00004851
Mihai Popa63ee7f12018-04-05 12:01:53 +01004852 private boolean handleOverlapsMagnifier(@NonNull final HandleView handle,
4853 @NonNull final Rect magnifierRect) {
4854 final PopupWindow window = handle.mContainer;
4855 if (!window.hasDecorView()) {
4856 return false;
4857 }
4858 final Rect handleRect = new Rect(
4859 window.getDecorViewLayoutParams().x,
4860 window.getDecorViewLayoutParams().y,
4861 window.getDecorViewLayoutParams().x + window.getContentView().getWidth(),
4862 window.getDecorViewLayoutParams().y + window.getContentView().getHeight());
4863 return Rect.intersects(handleRect, magnifierRect);
Mihai Popa894469c2018-03-21 19:45:06 +00004864 }
4865
Mihai Popa63ee7f12018-04-05 12:01:53 +01004866 private @Nullable HandleView getOtherSelectionHandle() {
4867 final SelectionModifierCursorController controller = getSelectionController();
4868 if (controller == null || !controller.isActive()) {
4869 return null;
4870 }
4871 return controller.mStartHandle != this
4872 ? controller.mStartHandle
4873 : controller.mEndHandle;
4874 }
4875
4876 private final Magnifier.Callback mHandlesVisibilityCallback = new Magnifier.Callback() {
4877 @Override
4878 public void onOperationComplete() {
4879 final Point magnifierTopLeft = mMagnifierAnimator.mMagnifier.getWindowCoords();
4880 if (magnifierTopLeft == null) {
4881 return;
4882 }
4883 final Rect magnifierRect = new Rect(magnifierTopLeft.x, magnifierTopLeft.y,
4884 magnifierTopLeft.x + mMagnifierAnimator.mMagnifier.getWidth(),
4885 magnifierTopLeft.y + mMagnifierAnimator.mMagnifier.getHeight());
4886 setVisible(!handleOverlapsMagnifier(HandleView.this, magnifierRect));
4887 final HandleView otherHandle = getOtherSelectionHandle();
4888 if (otherHandle != null) {
4889 otherHandle.setVisible(!handleOverlapsMagnifier(otherHandle, magnifierRect));
4890 }
4891 }
4892 };
4893
Mihai Popae3017462018-03-07 12:25:21 +00004894 protected final void updateMagnifier(@NonNull final MotionEvent event) {
Mihai Popa38722382018-03-07 19:56:21 +00004895 if (mMagnifierAnimator == null) {
Mihai Popae3017462018-03-07 12:25:21 +00004896 return;
4897 }
4898
4899 final PointF showPosInView = new PointF();
Mihai Popa894469c2018-03-21 19:45:06 +00004900 final boolean shouldShow = !tooLargeTextForMagnifier()
4901 && obtainMagnifierShowCoordinates(event, showPosInView);
Mihai Popae3017462018-03-07 12:25:21 +00004902 if (shouldShow) {
4903 // Make the cursor visible and stop blinking.
4904 mRenderCursorRegardlessTiming = true;
4905 mTextView.invalidateCursorPath();
4906 suspendBlink();
Mihai Popa63ee7f12018-04-05 12:01:53 +01004907 mMagnifierAnimator.mMagnifier
4908 .setOnOperationCompleteCallback(mHandlesVisibilityCallback);
Mihai Popab1b423a2018-03-27 19:03:09 +01004909
Mihai Popa38722382018-03-07 19:56:21 +00004910 mMagnifierAnimator.show(showPosInView.x, showPosInView.y);
Mihai Popae3017462018-03-07 12:25:21 +00004911 } else {
4912 dismissMagnifier();
4913 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004914 }
4915
4916 protected final void dismissMagnifier() {
Mihai Popa38722382018-03-07 19:56:21 +00004917 if (mMagnifierAnimator != null) {
4918 mMagnifierAnimator.dismiss();
Mihai Popaa4e39c42018-02-20 15:31:11 +00004919 mRenderCursorRegardlessTiming = false;
Andrei Stingaceanu451f9472017-10-13 16:41:28 +01004920 resumeBlink();
Mihai Popab1b423a2018-03-27 19:03:09 +01004921 setVisible(true);
Mihai Popa63ee7f12018-04-05 12:01:53 +01004922 final HandleView otherHandle = getOtherSelectionHandle();
4923 if (otherHandle != null) {
4924 otherHandle.setVisible(true);
4925 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004926 }
4927 }
4928
Gilles Debunned88876a2012-03-16 17:34:04 -07004929 @Override
4930 public boolean onTouchEvent(MotionEvent ev) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01004931 updateFloatingToolbarVisibility(ev);
4932
Gilles Debunned88876a2012-03-16 17:34:04 -07004933 switch (ev.getActionMasked()) {
4934 case MotionEvent.ACTION_DOWN: {
4935 startTouchUpFilter(getCurrentCursorOffset());
Gilles Debunned88876a2012-03-16 17:34:04 -07004936
4937 final PositionListener positionListener = getPositionListener();
4938 mLastParentX = positionListener.getPositionX();
4939 mLastParentY = positionListener.getPositionY();
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004940 mLastParentXOnScreen = positionListener.getPositionXOnScreen();
4941 mLastParentYOnScreen = positionListener.getPositionYOnScreen();
4942
4943 final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
4944 final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
4945 mTouchToWindowOffsetX = xInWindow - mPositionX;
4946 mTouchToWindowOffsetY = yInWindow - mPositionY;
4947
Gilles Debunned88876a2012-03-16 17:34:04 -07004948 mIsDragging = true;
Mady Mellora6a0f782015-07-10 16:43:32 -07004949 mPreviousLineTouched = UNSET_LINE;
Gilles Debunned88876a2012-03-16 17:34:04 -07004950 break;
4951 }
4952
4953 case MotionEvent.ACTION_MOVE: {
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004954 final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
4955 final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
Gilles Debunned88876a2012-03-16 17:34:04 -07004956
4957 // Vertical hysteresis: vertical down movement tends to snap to ideal offset
4958 final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004959 final float currentVerticalOffset = yInWindow - mPositionY - mLastParentY;
Gilles Debunned88876a2012-03-16 17:34:04 -07004960 float newVerticalOffset;
4961 if (previousVerticalOffset < mIdealVerticalOffset) {
4962 newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
4963 newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
4964 } else {
4965 newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
4966 newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
4967 }
4968 mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
4969
Keisuke Kuroyanagibc89a5c2015-05-18 14:49:29 +09004970 final float newPosX =
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004971 xInWindow - mTouchToWindowOffsetX + mHotspotX + getHorizontalOffset();
4972 final float newPosY = yInWindow - mTouchToWindowOffsetY + mTouchOffsetY;
Gilles Debunned88876a2012-03-16 17:34:04 -07004973
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004974 updatePosition(newPosX, newPosY,
4975 ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Gilles Debunned88876a2012-03-16 17:34:04 -07004976 break;
4977 }
4978
4979 case MotionEvent.ACTION_UP:
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004980 filterOnTouchUp(ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004981 // Fall through.
Gilles Debunned88876a2012-03-16 17:34:04 -07004982 case MotionEvent.ACTION_CANCEL:
4983 mIsDragging = false;
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004984 updateDrawable();
Gilles Debunned88876a2012-03-16 17:34:04 -07004985 break;
4986 }
4987 return true;
4988 }
4989
4990 public boolean isDragging() {
4991 return mIsDragging;
4992 }
4993
Clara Bayarri6351e662015-03-16 23:17:59 +00004994 void onHandleMoved() {}
Gilles Debunned88876a2012-03-16 17:34:04 -07004995
Clara Bayarri6351e662015-03-16 23:17:59 +00004996 public void onDetached() {}
Gilles Debunned88876a2012-03-16 17:34:04 -07004997 }
4998
4999 private class InsertionHandleView extends HandleView {
5000 private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
5001 private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
5002
Clara Bayarrib71dddd2015-06-04 23:17:30 +01005003 // Used to detect taps on the insertion handle, which will affect the insertion action mode
Gilles Debunned88876a2012-03-16 17:34:04 -07005004 private float mDownPositionX, mDownPositionY;
5005 private Runnable mHider;
5006
5007 public InsertionHandleView(Drawable drawable) {
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09005008 super(drawable, drawable, com.android.internal.R.id.insertion_handle);
Gilles Debunned88876a2012-03-16 17:34:04 -07005009 }
5010
5011 @Override
5012 public void show() {
5013 super.show();
5014
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01005015 final long durationSinceCutOrCopy =
Andrei Stingaceanu77b9c382015-05-06 13:25:19 +01005016 SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime;
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01005017
5018 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005019 if (mInsertionActionModeRunnable != null
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09005020 && ((mTapState == TAP_STATE_DOUBLE_TAP)
5021 || (mTapState == TAP_STATE_TRIPLE_CLICK)
5022 || isCursorInsideEasyCorrectionSpan())) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005023 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01005024 }
5025
5026 // Prepare and schedule the single tap runnable to run exactly after the double tap
5027 // timeout has passed.
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09005028 if ((mTapState != TAP_STATE_DOUBLE_TAP) && (mTapState != TAP_STATE_TRIPLE_CLICK)
5029 && !isCursorInsideEasyCorrectionSpan()
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01005030 && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION)) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01005031 if (mTextActionMode == null) {
5032 if (mInsertionActionModeRunnable == null) {
5033 mInsertionActionModeRunnable = new Runnable() {
5034 @Override
5035 public void run() {
5036 startInsertionActionMode();
5037 }
5038 };
5039 }
5040 mTextView.postDelayed(
5041 mInsertionActionModeRunnable,
5042 ViewConfiguration.getDoubleTapTimeout() + 1);
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01005043 }
5044
Gilles Debunned88876a2012-03-16 17:34:04 -07005045 }
5046
5047 hideAfterDelay();
5048 }
5049
Gilles Debunned88876a2012-03-16 17:34:04 -07005050 private void hideAfterDelay() {
5051 if (mHider == null) {
5052 mHider = new Runnable() {
5053 public void run() {
5054 hide();
5055 }
5056 };
5057 } else {
5058 removeHiderCallback();
5059 }
5060 mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
5061 }
5062
5063 private void removeHiderCallback() {
5064 if (mHider != null) {
5065 mTextView.removeCallbacks(mHider);
5066 }
5067 }
5068
5069 @Override
5070 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
5071 return drawable.getIntrinsicWidth() / 2;
5072 }
5073
5074 @Override
Adam Powell3fceabd2014-08-19 18:28:04 -07005075 protected int getHorizontalGravity(boolean isRtlRun) {
5076 return Gravity.CENTER_HORIZONTAL;
5077 }
5078
5079 @Override
5080 protected int getCursorOffset() {
5081 int offset = super.getCursorOffset();
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07005082 if (mDrawableForCursor != null) {
5083 mDrawableForCursor.getPadding(mTempRect);
5084 offset += (mDrawableForCursor.getIntrinsicWidth()
Roozbeh Pournader9c133072017-07-26 22:36:27 -07005085 - mTempRect.left - mTempRect.right) / 2;
Adam Powell3fceabd2014-08-19 18:28:04 -07005086 }
5087 return offset;
5088 }
5089
5090 @Override
Siyamed Sinir217c0f72016-02-01 18:30:02 -08005091 int getCursorHorizontalPosition(Layout layout, int offset) {
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07005092 if (mDrawableForCursor != null) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005093 final float horizontal = getHorizontal(layout, offset);
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07005094 return clampHorizontalPosition(mDrawableForCursor, horizontal) + mTempRect.left;
Siyamed Sinir217c0f72016-02-01 18:30:02 -08005095 }
5096 return super.getCursorHorizontalPosition(layout, offset);
5097 }
5098
5099 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07005100 public boolean onTouchEvent(MotionEvent ev) {
5101 final boolean result = super.onTouchEvent(ev);
5102
5103 switch (ev.getActionMasked()) {
5104 case MotionEvent.ACTION_DOWN:
5105 mDownPositionX = ev.getRawX();
5106 mDownPositionY = ev.getRawY();
Mihai Popae3017462018-03-07 12:25:21 +00005107 updateMagnifier(ev);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005108 break;
5109
5110 case MotionEvent.ACTION_MOVE:
Mihai Popae3017462018-03-07 12:25:21 +00005111 updateMagnifier(ev);
Gilles Debunned88876a2012-03-16 17:34:04 -07005112 break;
5113
5114 case MotionEvent.ACTION_UP:
5115 if (!offsetHasBeenChanged()) {
5116 final float deltaX = mDownPositionX - ev.getRawX();
5117 final float deltaY = mDownPositionY - ev.getRawY();
5118 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
5119
5120 final ViewConfiguration viewConfiguration = ViewConfiguration.get(
5121 mTextView.getContext());
5122 final int touchSlop = viewConfiguration.getScaledTouchSlop();
5123
5124 if (distanceSquared < touchSlop * touchSlop) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01005125 // Tapping on the handle toggles the insertion action mode.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005126 if (mTextActionMode != null) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005127 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07005128 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005129 startInsertionActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07005130 }
5131 }
Abodunrinwa Tokibcdf0ab2015-04-25 00:11:25 +01005132 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005133 if (mTextActionMode != null) {
5134 mTextActionMode.invalidateContentRect();
Abodunrinwa Tokibcdf0ab2015-04-25 00:11:25 +01005135 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005136 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005137 // Fall through.
Gilles Debunned88876a2012-03-16 17:34:04 -07005138 case MotionEvent.ACTION_CANCEL:
5139 hideAfterDelay();
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005140 dismissMagnifier();
Gilles Debunned88876a2012-03-16 17:34:04 -07005141 break;
5142
5143 default:
5144 break;
5145 }
5146
5147 return result;
5148 }
5149
5150 @Override
5151 public int getCurrentCursorOffset() {
5152 return mTextView.getSelectionStart();
5153 }
5154
5155 @Override
5156 public void updateSelection(int offset) {
5157 Selection.setSelection((Spannable) mTextView.getText(), offset);
5158 }
5159
5160 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005161 protected void updatePosition(float x, float y, boolean fromTouchScreen) {
Mady Melloree3821e2015-06-05 11:12:01 -07005162 Layout layout = mTextView.getLayout();
5163 int offset;
5164 if (layout != null) {
Mady Mellora6a0f782015-07-10 16:43:32 -07005165 if (mPreviousLineTouched == UNSET_LINE) {
5166 mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
5167 }
5168 int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005169 offset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellora6a0f782015-07-10 16:43:32 -07005170 mPreviousLineTouched = currLine;
Mady Melloree3821e2015-06-05 11:12:01 -07005171 } else {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005172 offset = -1;
Mady Melloree3821e2015-06-05 11:12:01 -07005173 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005174 positionAtCursorOffset(offset, false, fromTouchScreen);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005175 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01005176 invalidateActionMode();
Clara Bayarri1baed512015-05-11 15:29:16 +01005177 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005178 }
5179
5180 @Override
5181 void onHandleMoved() {
5182 super.onHandleMoved();
5183 removeHiderCallback();
5184 }
5185
5186 @Override
5187 public void onDetached() {
5188 super.onDetached();
5189 removeHiderCallback();
5190 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005191
5192 @Override
5193 @MagnifierHandleTrigger
5194 protected int getMagnifierHandleTrigger() {
5195 return MagnifierHandleTrigger.INSERTION;
5196 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005197 }
5198
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005199 @Retention(RetentionPolicy.SOURCE)
Jeff Sharkeyce8db992017-12-13 20:05:05 -07005200 @IntDef(prefix = { "HANDLE_TYPE_" }, value = {
5201 HANDLE_TYPE_SELECTION_START,
5202 HANDLE_TYPE_SELECTION_END
5203 })
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005204 public @interface HandleType {}
5205 public static final int HANDLE_TYPE_SELECTION_START = 0;
5206 public static final int HANDLE_TYPE_SELECTION_END = 1;
5207
Abodunrinwa Toki4a056a52017-08-05 01:56:40 +01005208 /** For selection handles */
5209 @VisibleForTesting
5210 public final class SelectionHandleView extends HandleView {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005211 // Indicates the handle type, selection start (HANDLE_TYPE_SELECTION_START) or selection
5212 // end (HANDLE_TYPE_SELECTION_END).
5213 @HandleType
5214 private final int mHandleType;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005215 // Indicates whether the cursor is making adjustments within a word.
5216 private boolean mInWord = false;
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005217 // Difference between touch position and word boundary position.
5218 private float mTouchWordDelta;
Mady Mellore264ac32015-06-22 16:46:29 -07005219 // X value of the previous updatePosition call.
5220 private float mPrevX;
5221 // Indicates if the handle has moved a boundary between LTR and RTL text.
5222 private boolean mLanguageDirectionChanged = false;
Mady Mellor42390aa2015-07-24 13:08:42 -07005223 // Distance from edge of horizontally scrolling text view
5224 // to use to switch to character mode.
5225 private final float mTextViewEdgeSlop;
5226 // Used to save text view location.
5227 private final int[] mTextViewLocation = new int[2];
Gilles Debunned88876a2012-03-16 17:34:04 -07005228
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005229 public SelectionHandleView(Drawable drawableLtr, Drawable drawableRtl, int id,
5230 @HandleType int handleType) {
5231 super(drawableLtr, drawableRtl, id);
5232 mHandleType = handleType;
5233 ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext());
Mady Mellor42390aa2015-07-24 13:08:42 -07005234 mTextViewEdgeSlop = viewConfiguration.getScaledTouchSlop() * 4;
Gilles Debunned88876a2012-03-16 17:34:04 -07005235 }
5236
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005237 private boolean isStartHandle() {
5238 return mHandleType == HANDLE_TYPE_SELECTION_START;
5239 }
5240
Gilles Debunned88876a2012-03-16 17:34:04 -07005241 @Override
5242 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005243 if (isRtlRun == isStartHandle()) {
Mady Mellor709386f2015-05-14 12:41:18 -07005244 return drawable.getIntrinsicWidth() / 4;
5245 } else {
5246 return (drawable.getIntrinsicWidth() * 3) / 4;
5247 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005248 }
5249
5250 @Override
Adam Powell3fceabd2014-08-19 18:28:04 -07005251 protected int getHorizontalGravity(boolean isRtlRun) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005252 return (isRtlRun == isStartHandle()) ? Gravity.LEFT : Gravity.RIGHT;
Adam Powell3fceabd2014-08-19 18:28:04 -07005253 }
5254
5255 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07005256 public int getCurrentCursorOffset() {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005257 return isStartHandle() ? mTextView.getSelectionStart() : mTextView.getSelectionEnd();
Gilles Debunned88876a2012-03-16 17:34:04 -07005258 }
5259
5260 @Override
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005261 protected void updateSelection(int offset) {
5262 if (isStartHandle()) {
5263 Selection.setSelection((Spannable) mTextView.getText(), offset,
5264 mTextView.getSelectionEnd());
5265 } else {
5266 Selection.setSelection((Spannable) mTextView.getText(),
5267 mTextView.getSelectionStart(), offset);
5268 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005269 updateDrawable();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005270 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01005271 invalidateActionMode();
Clara Bayarri13152d12015-04-09 12:02:04 +01005272 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005273 }
5274
5275 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005276 protected void updatePosition(float x, float y, boolean fromTouchScreen) {
Mady Mellor81fa3e82015-05-14 09:17:41 -07005277 final Layout layout = mTextView.getLayout();
Mady Mellorcc65c372015-06-17 09:25:19 -07005278 if (layout == null) {
5279 // HandleView will deal appropriately in positionAtCursorOffset when
5280 // layout is null.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005281 positionAndAdjustForCrossingHandles(mTextView.getOffsetForPosition(x, y),
5282 fromTouchScreen);
Mady Mellorcc65c372015-06-17 09:25:19 -07005283 return;
5284 }
5285
Mady Mellora6a0f782015-07-10 16:43:32 -07005286 if (mPreviousLineTouched == UNSET_LINE) {
5287 mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
5288 }
5289
Mady Mellorb9bbbb12015-03-23 11:50:46 -07005290 boolean positionCursor = false;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005291 final int anotherHandleOffset =
5292 isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
Mady Mellora6a0f782015-07-10 16:43:32 -07005293 int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005294 int initialOffset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellor81fa3e82015-05-14 09:17:41 -07005295
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005296 if (isStartHandle() && initialOffset >= anotherHandleOffset
5297 || !isStartHandle() && initialOffset <= anotherHandleOffset) {
5298 // Handles have crossed, bound it to the first selected line and
Mady Mellor81fa3e82015-05-14 09:17:41 -07005299 // adjust by word / char as normal.
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005300 currLine = layout.getLineForOffset(anotherHandleOffset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005301 initialOffset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellor81fa3e82015-05-14 09:17:41 -07005302 }
5303
5304 int offset = initialOffset;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005305 final int wordEnd = getWordEnd(offset);
5306 final int wordStart = getWordStart(offset);
Gilles Debunned88876a2012-03-16 17:34:04 -07005307
Mady Mellore264ac32015-06-22 16:46:29 -07005308 if (mPrevX == UNSET_X_VALUE) {
5309 mPrevX = x;
5310 }
5311
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005312 final int currentOffset = getCurrentCursorOffset();
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005313 final boolean rtlAtCurrentOffset = isAtRtlRun(layout, currentOffset);
5314 final boolean atRtl = isAtRtlRun(layout, offset);
Mady Mellore264ac32015-06-22 16:46:29 -07005315 final boolean isLvlBoundary = layout.isLevelBoundary(offset);
Mady Mellore264ac32015-06-22 16:46:29 -07005316
5317 // We can't determine if the user is expanding or shrinking the selection if they're
5318 // on a bi-di boundary, so until they've moved past the boundary we'll just place
5319 // the cursor at the current position.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005320 if (isLvlBoundary || (rtlAtCurrentOffset && !atRtl) || (!rtlAtCurrentOffset && atRtl)) {
Mady Mellore264ac32015-06-22 16:46:29 -07005321 // We're on a boundary or this is the first direction change -- just update
5322 // to the current position.
5323 mLanguageDirectionChanged = true;
5324 mTouchWordDelta = 0.0f;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005325 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellore264ac32015-06-22 16:46:29 -07005326 return;
5327 } else if (mLanguageDirectionChanged && !isLvlBoundary) {
5328 // We've just moved past the boundary so update the position. After this we can
5329 // figure out if the user is expanding or shrinking to go by word or character.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005330 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellore264ac32015-06-22 16:46:29 -07005331 mTouchWordDelta = 0.0f;
5332 mLanguageDirectionChanged = false;
5333 return;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005334 }
5335
5336 boolean isExpanding;
5337 final float xDiff = x - mPrevX;
Keisuke Kuroyanagi26454142015-12-02 15:04:57 -08005338 if (isStartHandle()) {
5339 isExpanding = currLine < mPreviousLineTouched;
Mady Mellore264ac32015-06-22 16:46:29 -07005340 } else {
Keisuke Kuroyanagi26454142015-12-02 15:04:57 -08005341 isExpanding = currLine > mPreviousLineTouched;
5342 }
5343 if (atRtl == isStartHandle()) {
5344 isExpanding |= xDiff > 0;
5345 } else {
5346 isExpanding |= xDiff < 0;
Mady Mellore264ac32015-06-22 16:46:29 -07005347 }
5348
Mady Mellor42390aa2015-07-24 13:08:42 -07005349 if (mTextView.getHorizontallyScrolling()) {
5350 if (positionNearEdgeOfScrollingView(x, atRtl)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005351 && ((isStartHandle() && mTextView.getScrollX() != 0)
5352 || (!isStartHandle()
5353 && mTextView.canScrollHorizontally(atRtl ? -1 : 1)))
5354 && ((isExpanding && ((isStartHandle() && offset < currentOffset)
5355 || (!isStartHandle() && offset > currentOffset)))
5356 || !isExpanding)) {
5357 // If we're expanding ensure that the offset is actually expanding compared to
5358 // the current offset, if the handle snapped to the word, the finger position
Mady Mellor42390aa2015-07-24 13:08:42 -07005359 // may be out of sync and we don't want the selection to jump back.
5360 mTouchWordDelta = 0.0f;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005361 final int nextOffset = (atRtl == isStartHandle())
5362 ? layout.getOffsetToRightOf(mPreviousOffset)
Mady Mellor42390aa2015-07-24 13:08:42 -07005363 : layout.getOffsetToLeftOf(mPreviousOffset);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005364 positionAndAdjustForCrossingHandles(nextOffset, fromTouchScreen);
Mady Mellor42390aa2015-07-24 13:08:42 -07005365 return;
5366 }
5367 }
5368
Mady Mellore264ac32015-06-22 16:46:29 -07005369 if (isExpanding) {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005370 // User is increasing the selection.
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005371 int wordBoundary = isStartHandle() ? wordStart : wordEnd;
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005372 final boolean snapToWord = (!mInWord
5373 || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine))
5374 && atRtl == isAtRtlRun(layout, wordBoundary);
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005375 if (snapToWord) {
Mady Mellora5266832015-06-26 14:28:12 -07005376 // Sometimes words can be broken across lines (Chinese, hyphenation).
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005377 // We still snap to the word boundary but we only use the letters on the
Mady Mellora5266832015-06-26 14:28:12 -07005378 // current line to determine if the user is far enough into the word to snap.
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005379 if (layout.getLineForOffset(wordBoundary) != currLine) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005380 wordBoundary = isStartHandle()
5381 ? layout.getLineStart(currLine) : layout.getLineEnd(currLine);
Mady Mellora5266832015-06-26 14:28:12 -07005382 }
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005383 final int offsetThresholdToSnap = isStartHandle()
5384 ? wordEnd - ((wordEnd - wordBoundary) / 2)
5385 : wordStart + ((wordBoundary - wordStart) / 2);
5386 if (isStartHandle()
5387 && (offset <= offsetThresholdToSnap || currLine < mPrevLine)) {
5388 // User is far enough into the word or on a different line so we expand by
5389 // word.
5390 offset = wordStart;
5391 } else if (!isStartHandle()
5392 && (offset >= offsetThresholdToSnap || currLine > mPrevLine)) {
5393 // User is far enough into the word or on a different line so we expand by
5394 // word.
5395 offset = wordEnd;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005396 } else {
Mady Mellorc2225b92015-04-01 15:59:20 -07005397 offset = mPreviousOffset;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005398 }
5399 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005400 if ((isStartHandle() && offset < initialOffset)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005401 || (!isStartHandle() && offset > initialOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005402 final float adjustedX = getHorizontal(layout, offset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005403 mTouchWordDelta =
5404 mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
Keisuke Kuroyanagi50a927c2015-05-07 17:34:21 +09005405 } else {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005406 mTouchWordDelta = 0.0f;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005407 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005408 positionCursor = true;
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005409 } else {
5410 final int adjustedOffset =
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005411 getOffsetAtCoordinate(layout, currLine, x - mTouchWordDelta);
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005412 final boolean shrinking = isStartHandle()
5413 ? adjustedOffset > mPreviousOffset || currLine > mPrevLine
5414 : adjustedOffset < mPreviousOffset || currLine < mPrevLine;
5415 if (shrinking) {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005416 // User is shrinking the selection.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005417 if (currLine != mPrevLine) {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005418 // We're on a different line, so we'll snap to word boundaries.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005419 offset = isStartHandle() ? wordStart : wordEnd;
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005420 if ((isStartHandle() && offset < initialOffset)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005421 || (!isStartHandle() && offset > initialOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005422 final float adjustedX = getHorizontal(layout, offset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005423 mTouchWordDelta =
5424 mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
5425 } else {
5426 mTouchWordDelta = 0.0f;
5427 }
5428 } else {
5429 offset = adjustedOffset;
5430 }
5431 positionCursor = true;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005432 } else if ((isStartHandle() && adjustedOffset < mPreviousOffset)
5433 || (!isStartHandle() && adjustedOffset > mPreviousOffset)) {
5434 // Handle has jumped to the word boundary, and the user is moving
Mady Mellor43fd2f42015-06-08 14:03:34 -07005435 // their finger towards the handle, the delta should be updated.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005436 mTouchWordDelta = mTextView.convertToLocalHorizontalCoordinate(x)
5437 - getHorizontal(layout, mPreviousOffset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005438 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005439 }
5440
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005441 if (positionCursor) {
Mady Mellora6a0f782015-07-10 16:43:32 -07005442 mPreviousLineTouched = currLine;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005443 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005444 }
Mady Mellore264ac32015-06-22 16:46:29 -07005445 mPrevX = x;
Gilles Debunned88876a2012-03-16 17:34:04 -07005446 }
5447
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005448 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005449 protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
5450 boolean fromTouchScreen) {
5451 super.positionAtCursorOffset(offset, forceUpdatePosition, fromTouchScreen);
Yoshiki Iguchi9582e152015-10-15 13:34:41 +09005452 mInWord = (offset != -1) && !getWordIteratorWithText().isBoundary(offset);
Mady Mellor36d5a7b2015-05-22 10:31:12 -07005453 }
5454
5455 @Override
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005456 public boolean onTouchEvent(MotionEvent event) {
5457 boolean superResult = super.onTouchEvent(event);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005458
5459 switch (event.getActionMasked()) {
5460 case MotionEvent.ACTION_DOWN:
5461 // Reset the touch word offset and x value when the user
5462 // re-engages the handle.
5463 mTouchWordDelta = 0.0f;
5464 mPrevX = UNSET_X_VALUE;
Mihai Popae3017462018-03-07 12:25:21 +00005465 updateMagnifier(event);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005466 break;
5467
5468 case MotionEvent.ACTION_MOVE:
Mihai Popae3017462018-03-07 12:25:21 +00005469 updateMagnifier(event);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005470 break;
5471
5472 case MotionEvent.ACTION_UP:
5473 case MotionEvent.ACTION_CANCEL:
5474 dismissMagnifier();
5475 break;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005476 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005477
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005478 return superResult;
5479 }
Mady Mellor42390aa2015-07-24 13:08:42 -07005480
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005481 private void positionAndAdjustForCrossingHandles(int offset, boolean fromTouchScreen) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005482 final int anotherHandleOffset =
5483 isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
5484 if ((isStartHandle() && offset >= anotherHandleOffset)
5485 || (!isStartHandle() && offset <= anotherHandleOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005486 mTouchWordDelta = 0.0f;
5487 final Layout layout = mTextView.getLayout();
5488 if (layout != null && offset != anotherHandleOffset) {
5489 final float horiz = getHorizontal(layout, offset);
5490 final float anotherHandleHoriz = getHorizontal(layout, anotherHandleOffset,
5491 !isStartHandle());
5492 final float currentHoriz = getHorizontal(layout, mPreviousOffset);
5493 if (currentHoriz < anotherHandleHoriz && horiz < anotherHandleHoriz
5494 || currentHoriz > anotherHandleHoriz && horiz > anotherHandleHoriz) {
5495 // This handle passes another one as it crossed a direction boundary.
5496 // Don't minimize the selection, but keep the handle at the run boundary.
5497 final int currentOffset = getCurrentCursorOffset();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005498 final int offsetToGetRunRange = isStartHandle()
5499 ? currentOffset : Math.max(currentOffset - 1, 0);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005500 final long range = layout.getRunRange(offsetToGetRunRange);
5501 if (isStartHandle()) {
5502 offset = TextUtils.unpackRangeStartFromLong(range);
5503 } else {
5504 offset = TextUtils.unpackRangeEndFromLong(range);
5505 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005506 positionAtCursorOffset(offset, false, fromTouchScreen);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005507 return;
5508 }
5509 }
Mady Mellor42390aa2015-07-24 13:08:42 -07005510 // Handles can not cross and selection is at least one character.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005511 offset = getNextCursorOffset(anotherHandleOffset, !isStartHandle());
Mady Mellor42390aa2015-07-24 13:08:42 -07005512 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005513 positionAtCursorOffset(offset, false, fromTouchScreen);
Mady Mellor42390aa2015-07-24 13:08:42 -07005514 }
5515
Mady Mellor42390aa2015-07-24 13:08:42 -07005516 private boolean positionNearEdgeOfScrollingView(float x, boolean atRtl) {
5517 mTextView.getLocationOnScreen(mTextViewLocation);
5518 boolean nearEdge;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005519 if (atRtl == isStartHandle()) {
Mady Mellor42390aa2015-07-24 13:08:42 -07005520 int rightEdge = mTextViewLocation[0] + mTextView.getWidth()
5521 - mTextView.getPaddingRight();
5522 nearEdge = x > rightEdge - mTextViewEdgeSlop;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005523 } else {
5524 int leftEdge = mTextViewLocation[0] + mTextView.getPaddingLeft();
5525 nearEdge = x < leftEdge + mTextViewEdgeSlop;
Mady Mellor42390aa2015-07-24 13:08:42 -07005526 }
5527 return nearEdge;
5528 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005529
5530 @Override
5531 protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
5532 final int offsetToCheck = isStartHandle() ? offset : Math.max(offset - 1, 0);
5533 return layout.isRtlCharAt(offsetToCheck);
5534 }
5535
5536 @Override
5537 public float getHorizontal(@NonNull Layout layout, int offset) {
5538 return getHorizontal(layout, offset, isStartHandle());
5539 }
5540
5541 private float getHorizontal(@NonNull Layout layout, int offset, boolean startHandle) {
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005542 final int line = layout.getLineForOffset(offset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005543 final int offsetToCheck = startHandle ? offset : Math.max(offset - 1, 0);
5544 final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
5545 final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005546 return (isRtlChar == isRtlParagraph)
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005547 ? layout.getPrimaryHorizontal(offset) : layout.getSecondaryHorizontal(offset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005548 }
5549
5550 @Override
5551 protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
Keisuke Kuroyanagib1b88652016-04-05 16:26:16 +09005552 final float localX = mTextView.convertToLocalHorizontalCoordinate(x);
5553 final int primaryOffset = layout.getOffsetForHorizontal(line, localX, true);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005554 if (!layout.isLevelBoundary(primaryOffset)) {
5555 return primaryOffset;
5556 }
Keisuke Kuroyanagib1b88652016-04-05 16:26:16 +09005557 final int secondaryOffset = layout.getOffsetForHorizontal(line, localX, false);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005558 final int currentOffset = getCurrentCursorOffset();
5559 final int primaryDiff = Math.abs(primaryOffset - currentOffset);
5560 final int secondaryDiff = Math.abs(secondaryOffset - currentOffset);
5561 if (primaryDiff < secondaryDiff) {
5562 return primaryOffset;
5563 } else if (primaryDiff > secondaryDiff) {
5564 return secondaryOffset;
5565 } else {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005566 final int offsetToCheck = isStartHandle()
5567 ? currentOffset : Math.max(currentOffset - 1, 0);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005568 final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
5569 final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
5570 return isRtlChar == isRtlParagraph ? primaryOffset : secondaryOffset;
5571 }
5572 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005573
5574 @MagnifierHandleTrigger
5575 protected int getMagnifierHandleTrigger() {
5576 return isStartHandle()
5577 ? MagnifierHandleTrigger.SELECTION_START
5578 : MagnifierHandleTrigger.SELECTION_END;
5579 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005580 }
5581
Mady Mellorcc65c372015-06-17 09:25:19 -07005582 private int getCurrentLineAdjustedForSlop(Layout layout, int prevLine, float y) {
Mady Mellor80679072015-07-09 16:05:36 -07005583 final int trueLine = mTextView.getLineAtCoordinate(y);
Mady Mellorcc65c372015-06-17 09:25:19 -07005584 if (layout == null || prevLine > layout.getLineCount()
5585 || layout.getLineCount() <= 0 || prevLine < 0) {
5586 // Invalid parameters, just return whatever line is at y.
Mady Mellor80679072015-07-09 16:05:36 -07005587 return trueLine;
5588 }
5589
5590 if (Math.abs(trueLine - prevLine) >= 2) {
5591 // Only stick to lines if we're within a line of the previous selection.
5592 return trueLine;
Mady Mellorcc65c372015-06-17 09:25:19 -07005593 }
5594
5595 final float verticalOffset = mTextView.viewportToContentVerticalOffset();
5596 final int lineCount = layout.getLineCount();
5597 final float slop = mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS;
5598
5599 final float firstLineTop = layout.getLineTop(0) + verticalOffset;
5600 final float prevLineTop = layout.getLineTop(prevLine) + verticalOffset;
5601 final float yTopBound = Math.max(prevLineTop - slop, firstLineTop + slop);
5602
5603 final float lastLineBottom = layout.getLineBottom(lineCount - 1) + verticalOffset;
5604 final float prevLineBottom = layout.getLineBottom(prevLine) + verticalOffset;
5605 final float yBottomBound = Math.min(prevLineBottom + slop, lastLineBottom - slop);
5606
5607 // Determine if we've moved lines based on y position and previous line.
5608 int currLine;
5609 if (y <= yTopBound) {
5610 currLine = Math.max(prevLine - 1, 0);
5611 } else if (y >= yBottomBound) {
5612 currLine = Math.min(prevLine + 1, lineCount - 1);
5613 } else {
5614 currLine = prevLine;
5615 }
5616 return currLine;
5617 }
5618
Gilles Debunned88876a2012-03-16 17:34:04 -07005619 /**
5620 * A CursorController instance can be used to control a cursor in the text.
5621 */
5622 private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
5623 /**
5624 * Makes the cursor controller visible on screen.
5625 * See also {@link #hide()}.
5626 */
5627 public void show();
5628
5629 /**
5630 * Hide the cursor controller from screen.
5631 * See also {@link #show()}.
5632 */
5633 public void hide();
5634
5635 /**
5636 * Called when the view is detached from window. Perform house keeping task, such as
5637 * stopping Runnable thread that would otherwise keep a reference on the context, thus
5638 * preventing the activity from being recycled.
5639 */
5640 public void onDetached();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005641
5642 public boolean isCursorBeingModified();
5643
5644 public boolean isActive();
Gilles Debunned88876a2012-03-16 17:34:04 -07005645 }
5646
5647 private class InsertionPointCursorController implements CursorController {
5648 private InsertionHandleView mHandle;
5649
5650 public void show() {
5651 getHandle().show();
Andrei Stingaceanu35c550c2015-05-07 16:49:49 +01005652
5653 if (mSelectionModifierCursorController != null) {
5654 mSelectionModifierCursorController.hide();
5655 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005656 }
5657
Gilles Debunned88876a2012-03-16 17:34:04 -07005658 public void hide() {
5659 if (mHandle != null) {
5660 mHandle.hide();
5661 }
5662 }
5663
5664 public void onTouchModeChanged(boolean isInTouchMode) {
5665 if (!isInTouchMode) {
5666 hide();
5667 }
5668 }
5669
5670 private InsertionHandleView getHandle() {
5671 if (mSelectHandleCenter == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08005672 mSelectHandleCenter = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07005673 mTextView.mTextSelectHandleRes);
5674 }
5675 if (mHandle == null) {
5676 mHandle = new InsertionHandleView(mSelectHandleCenter);
5677 }
5678 return mHandle;
5679 }
5680
5681 @Override
5682 public void onDetached() {
5683 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
5684 observer.removeOnTouchModeChangeListener(this);
5685
5686 if (mHandle != null) mHandle.onDetached();
5687 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005688
5689 @Override
5690 public boolean isCursorBeingModified() {
5691 return mHandle != null && mHandle.isDragging();
5692 }
5693
5694 @Override
5695 public boolean isActive() {
5696 return mHandle != null && mHandle.isShowing();
5697 }
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09005698
5699 public void invalidateHandle() {
5700 if (mHandle != null) {
5701 mHandle.invalidate();
5702 }
5703 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005704 }
5705
5706 class SelectionModifierCursorController implements CursorController {
Gilles Debunned88876a2012-03-16 17:34:04 -07005707 // The cursor controller handles, lazily created when shown.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005708 private SelectionHandleView mStartHandle;
5709 private SelectionHandleView mEndHandle;
Gilles Debunned88876a2012-03-16 17:34:04 -07005710 // The offsets of that last touch down event. Remembered to start selection there.
5711 private int mMinTouchOffset, mMaxTouchOffset;
5712
Gilles Debunned88876a2012-03-16 17:34:04 -07005713 private float mDownPositionX, mDownPositionY;
5714 private boolean mGestureStayedInTapRegion;
5715
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005716 // Where the user first starts the drag motion.
5717 private int mStartOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005718
Mady Mellor7a936442015-05-20 10:05:52 -07005719 private boolean mHaventMovedEnoughToStartDrag;
Mady Mellorf9f8aeb2015-06-17 09:46:01 -07005720 // The line that a selection happened most recently with the drag accelerator.
5721 private int mLineSelectionIsOn = -1;
5722 // Whether the drag accelerator has selected past the initial line.
5723 private boolean mSwitchedLines = false;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005724
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005725 // Indicates the drag accelerator mode that the user is currently using.
5726 private int mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
5727 // Drag accelerator is inactive.
5728 private static final int DRAG_ACCELERATOR_MODE_INACTIVE = 0;
5729 // Character based selection by dragging. Only for mouse.
5730 private static final int DRAG_ACCELERATOR_MODE_CHARACTER = 1;
5731 // Word based selection by dragging. Enabled after long pressing or double tapping.
5732 private static final int DRAG_ACCELERATOR_MODE_WORD = 2;
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005733 // Paragraph based selection by dragging. Enabled after mouse triple click.
5734 private static final int DRAG_ACCELERATOR_MODE_PARAGRAPH = 3;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005735
Gilles Debunned88876a2012-03-16 17:34:04 -07005736 SelectionModifierCursorController() {
5737 resetTouchOffsets();
5738 }
5739
5740 public void show() {
5741 if (mTextView.isInBatchEditMode()) {
5742 return;
5743 }
5744 initDrawables();
5745 initHandles();
Gilles Debunned88876a2012-03-16 17:34:04 -07005746 }
5747
5748 private void initDrawables() {
5749 if (mSelectHandleLeft == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08005750 mSelectHandleLeft = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07005751 mTextView.mTextSelectHandleLeftRes);
5752 }
5753 if (mSelectHandleRight == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08005754 mSelectHandleRight = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07005755 mTextView.mTextSelectHandleRightRes);
5756 }
5757 }
5758
5759 private void initHandles() {
5760 // Lazy object creation has to be done before updatePosition() is called.
5761 if (mStartHandle == null) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005762 mStartHandle = new SelectionHandleView(mSelectHandleLeft, mSelectHandleRight,
5763 com.android.internal.R.id.selection_start_handle,
5764 HANDLE_TYPE_SELECTION_START);
Gilles Debunned88876a2012-03-16 17:34:04 -07005765 }
5766 if (mEndHandle == null) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005767 mEndHandle = new SelectionHandleView(mSelectHandleRight, mSelectHandleLeft,
5768 com.android.internal.R.id.selection_end_handle,
5769 HANDLE_TYPE_SELECTION_END);
Gilles Debunned88876a2012-03-16 17:34:04 -07005770 }
5771
5772 mStartHandle.show();
5773 mEndHandle.show();
5774
Gilles Debunned88876a2012-03-16 17:34:04 -07005775 hideInsertionPointCursorController();
5776 }
5777
5778 public void hide() {
5779 if (mStartHandle != null) mStartHandle.hide();
5780 if (mEndHandle != null) mEndHandle.hide();
5781 }
5782
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005783 public void enterDrag(int dragAcceleratorMode) {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005784 // Just need to init the handles / hide insertion cursor.
5785 show();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005786 mDragAcceleratorMode = dragAcceleratorMode;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005787 // Start location of selection.
5788 mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX,
5789 mLastDownPositionY);
Mady Mellorf9f8aeb2015-06-17 09:46:01 -07005790 mLineSelectionIsOn = mTextView.getLineAtCoordinate(mLastDownPositionY);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005791 // Don't show the handles until user has lifted finger.
5792 hide();
5793
5794 // This stops scrolling parents from intercepting the touch event, allowing
5795 // the user to continue dragging across the screen to select text; TextView will
5796 // scroll as necessary.
5797 mTextView.getParent().requestDisallowInterceptTouchEvent(true);
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005798 mTextView.cancelLongPress();
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005799 }
5800
Gilles Debunned88876a2012-03-16 17:34:04 -07005801 public void onTouchEvent(MotionEvent event) {
5802 // This is done even when the View does not have focus, so that long presses can start
5803 // selection and tap can move cursor from this tap position.
Mady Mellor7a936442015-05-20 10:05:52 -07005804 final float eventX = event.getX();
5805 final float eventY = event.getY();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005806 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
Gilles Debunned88876a2012-03-16 17:34:04 -07005807 switch (event.getActionMasked()) {
5808 case MotionEvent.ACTION_DOWN:
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005809 if (extractedTextModeWillBeStarted()) {
5810 // Prevent duplicating the selection handles until the mode starts.
5811 hide();
5812 } else {
5813 // Remember finger down position, to be able to start selection from there.
5814 mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(
5815 eventX, eventY);
Gilles Debunned88876a2012-03-16 17:34:04 -07005816
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005817 // Double tap detection
5818 if (mGestureStayedInTapRegion) {
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09005819 if (mTapState == TAP_STATE_DOUBLE_TAP
5820 || mTapState == TAP_STATE_TRIPLE_CLICK) {
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005821 final float deltaX = eventX - mDownPositionX;
5822 final float deltaY = eventY - mDownPositionY;
5823 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005824
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005825 ViewConfiguration viewConfiguration = ViewConfiguration.get(
5826 mTextView.getContext());
5827 int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
5828 boolean stayedInArea =
5829 distanceSquared < doubleTapSlop * doubleTapSlop;
Gilles Debunned88876a2012-03-16 17:34:04 -07005830
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005831 if (stayedInArea && (isMouse || isPositionOnText(eventX, eventY))) {
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09005832 if (mTapState == TAP_STATE_DOUBLE_TAP) {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005833 selectCurrentWordAndStartDrag();
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09005834 } else if (mTapState == TAP_STATE_TRIPLE_CLICK) {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005835 selectCurrentParagraphAndStartDrag();
5836 }
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005837 mDiscardNextActionUp = true;
5838 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005839 }
5840 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005841
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005842 mDownPositionX = eventX;
5843 mDownPositionY = eventY;
5844 mGestureStayedInTapRegion = true;
5845 mHaventMovedEnoughToStartDrag = true;
5846 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005847 break;
5848
5849 case MotionEvent.ACTION_POINTER_DOWN:
5850 case MotionEvent.ACTION_POINTER_UP:
5851 // Handle multi-point gestures. Keep min and max offset positions.
5852 // Only activated for devices that correctly handle multi-touch.
5853 if (mTextView.getContext().getPackageManager().hasSystemFeature(
5854 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
5855 updateMinAndMaxOffsets(event);
5856 }
5857 break;
5858
5859 case MotionEvent.ACTION_MOVE:
Mady Mellor7a936442015-05-20 10:05:52 -07005860 final ViewConfiguration viewConfig = ViewConfiguration.get(
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005861 mTextView.getContext());
Mady Mellor7a936442015-05-20 10:05:52 -07005862 final int touchSlop = viewConfig.getScaledTouchSlop();
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005863
Mady Mellor7a936442015-05-20 10:05:52 -07005864 if (mGestureStayedInTapRegion || mHaventMovedEnoughToStartDrag) {
5865 final float deltaX = eventX - mDownPositionX;
5866 final float deltaY = eventY - mDownPositionY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005867 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
5868
Mady Mellor7a936442015-05-20 10:05:52 -07005869 if (mGestureStayedInTapRegion) {
5870 int doubleTapTouchSlop = viewConfig.getScaledDoubleTapTouchSlop();
5871 mGestureStayedInTapRegion =
5872 distanceSquared <= doubleTapTouchSlop * doubleTapTouchSlop;
5873 }
5874 if (mHaventMovedEnoughToStartDrag) {
5875 // We don't start dragging until the user has moved enough.
5876 mHaventMovedEnoughToStartDrag =
5877 distanceSquared <= touchSlop * touchSlop;
Gilles Debunned88876a2012-03-16 17:34:04 -07005878 }
5879 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005880
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005881 if (isMouse && !isDragAcceleratorActive()) {
5882 final int offset = mTextView.getOffsetForPosition(eventX, eventY);
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09005883 if (mTextView.hasSelection()
5884 && (!mHaventMovedEnoughToStartDrag || mStartOffset != offset)
5885 && offset >= mTextView.getSelectionStart()
5886 && offset <= mTextView.getSelectionEnd()) {
5887 startDragAndDrop();
5888 break;
5889 }
5890
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005891 if (mStartOffset != offset) {
5892 // Start character based drag accelerator.
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005893 stopTextActionMode();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005894 enterDrag(DRAG_ACCELERATOR_MODE_CHARACTER);
5895 mDiscardNextActionUp = true;
5896 mHaventMovedEnoughToStartDrag = false;
5897 }
5898 }
5899
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005900 if (mStartHandle != null && mStartHandle.isShowing()) {
5901 // Don't do the drag if the handles are showing already.
5902 break;
5903 }
5904
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005905 updateSelection(event);
Gilles Debunned88876a2012-03-16 17:34:04 -07005906 break;
5907
5908 case MotionEvent.ACTION_UP:
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005909 if (!isDragAcceleratorActive()) {
5910 break;
5911 }
5912 updateSelection(event);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005913
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005914 // No longer dragging to select text, let the parent intercept events.
5915 mTextView.getParent().requestDisallowInterceptTouchEvent(false);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005916
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005917 // No longer the first dragging motion, reset.
5918 resetDragAcceleratorState();
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09005919
5920 if (mTextView.hasSelection()) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01005921 // Drag selection should not be adjusted by the text classifier.
5922 startSelectionActionModeAsync(mHaventMovedEnoughToStartDrag);
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09005923 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005924 break;
5925 }
5926 }
5927
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005928 private void updateSelection(MotionEvent event) {
5929 if (mTextView.getLayout() != null) {
5930 switch (mDragAcceleratorMode) {
5931 case DRAG_ACCELERATOR_MODE_CHARACTER:
5932 updateCharacterBasedSelection(event);
5933 break;
5934 case DRAG_ACCELERATOR_MODE_WORD:
5935 updateWordBasedSelection(event);
5936 break;
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005937 case DRAG_ACCELERATOR_MODE_PARAGRAPH:
5938 updateParagraphBasedSelection(event);
5939 break;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005940 }
5941 }
5942 }
5943
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005944 /**
5945 * If the TextView allows text selection, selects the current paragraph and starts a drag.
5946 *
5947 * @return true if the drag was started.
5948 */
5949 private boolean selectCurrentParagraphAndStartDrag() {
5950 if (mInsertionActionModeRunnable != null) {
5951 mTextView.removeCallbacks(mInsertionActionModeRunnable);
5952 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005953 stopTextActionMode();
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005954 if (!selectCurrentParagraph()) {
5955 return false;
5956 }
5957 enterDrag(SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_PARAGRAPH);
5958 return true;
5959 }
5960
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005961 private void updateCharacterBasedSelection(MotionEvent event) {
5962 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005963 updateSelectionInternal(mStartOffset, offset,
5964 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005965 }
5966
5967 private void updateWordBasedSelection(MotionEvent event) {
5968 if (mHaventMovedEnoughToStartDrag) {
5969 return;
5970 }
5971 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
5972 final ViewConfiguration viewConfig = ViewConfiguration.get(
5973 mTextView.getContext());
5974 final float eventX = event.getX();
5975 final float eventY = event.getY();
5976 final int currLine;
5977 if (isMouse) {
5978 // No need to offset the y coordinate for mouse input.
5979 currLine = mTextView.getLineAtCoordinate(eventY);
5980 } else {
5981 float y = eventY;
5982 if (mSwitchedLines) {
5983 // Offset the finger by the same vertical offset as the handles.
5984 // This improves visibility of the content being selected by
5985 // shifting the finger below the content, this is applied once
5986 // the user has switched lines.
5987 final int touchSlop = viewConfig.getScaledTouchSlop();
5988 final float fingerOffset = (mStartHandle != null)
5989 ? mStartHandle.getIdealVerticalOffset()
5990 : touchSlop;
5991 y = eventY - fingerOffset;
5992 }
5993
5994 currLine = getCurrentLineAdjustedForSlop(mTextView.getLayout(), mLineSelectionIsOn,
5995 y);
5996 if (!mSwitchedLines && currLine != mLineSelectionIsOn) {
5997 // Break early here, we want to offset the finger position from
5998 // the selection highlight, once the user moved their finger
5999 // to a different line we should apply the offset and *not* switch
6000 // lines until recomputing the position with the finger offset.
6001 mSwitchedLines = true;
6002 return;
6003 }
6004 }
6005
6006 int startOffset;
6007 int offset = mTextView.getOffsetAtCoordinate(currLine, eventX);
6008 // Snap to word boundaries.
6009 if (mStartOffset < offset) {
6010 // Expanding with end handle.
6011 offset = getWordEnd(offset);
6012 startOffset = getWordStart(mStartOffset);
6013 } else {
6014 // Expanding with start handle.
6015 offset = getWordStart(offset);
6016 startOffset = getWordEnd(mStartOffset);
Keisuke Kuroyanagi133dfc02016-07-21 18:07:23 +09006017 if (startOffset == offset) {
6018 offset = getNextCursorOffset(offset, false);
6019 }
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006020 }
6021 mLineSelectionIsOn = currLine;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07006022 updateSelectionInternal(startOffset, offset,
6023 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006024 }
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09006025
6026 private void updateParagraphBasedSelection(MotionEvent event) {
6027 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
6028
6029 final int start = Math.min(offset, mStartOffset);
6030 final int end = Math.max(offset, mStartOffset);
6031 final long paragraphsRange = getParagraphsRange(start, end);
6032 final int selectionStart = TextUtils.unpackRangeStartFromLong(paragraphsRange);
6033 final int selectionEnd = TextUtils.unpackRangeEndFromLong(paragraphsRange);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07006034 updateSelectionInternal(selectionStart, selectionEnd,
6035 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
6036 }
6037
6038 private void updateSelectionInternal(int selectionStart, int selectionEnd,
6039 boolean fromTouchScreen) {
6040 final boolean performHapticFeedback = fromTouchScreen && mHapticTextHandleEnabled
6041 && ((mTextView.getSelectionStart() != selectionStart)
6042 || (mTextView.getSelectionEnd() != selectionEnd));
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09006043 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07006044 if (performHapticFeedback) {
6045 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
6046 }
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09006047 }
6048
Gilles Debunned88876a2012-03-16 17:34:04 -07006049 /**
6050 * @param event
6051 */
6052 private void updateMinAndMaxOffsets(MotionEvent event) {
6053 int pointerCount = event.getPointerCount();
6054 for (int index = 0; index < pointerCount; index++) {
6055 int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
6056 if (offset < mMinTouchOffset) mMinTouchOffset = offset;
6057 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
6058 }
6059 }
6060
6061 public int getMinTouchOffset() {
6062 return mMinTouchOffset;
6063 }
6064
6065 public int getMaxTouchOffset() {
6066 return mMaxTouchOffset;
6067 }
6068
6069 public void resetTouchOffsets() {
6070 mMinTouchOffset = mMaxTouchOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006071 resetDragAcceleratorState();
6072 }
6073
6074 private void resetDragAcceleratorState() {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006075 mStartOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006076 mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
Mady Mellorf9f8aeb2015-06-17 09:46:01 -07006077 mSwitchedLines = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006078 final int selectionStart = mTextView.getSelectionStart();
6079 final int selectionEnd = mTextView.getSelectionEnd();
Clara Bayarri4e518772018-03-27 14:25:33 +01006080 if (selectionStart < 0 || selectionEnd < 0) {
6081 Selection.removeSelection((Spannable) mTextView.getText());
6082 } else if (selectionStart > selectionEnd) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006083 Selection.setSelection((Spannable) mTextView.getText(),
6084 selectionEnd, selectionStart);
6085 }
Gilles Debunned88876a2012-03-16 17:34:04 -07006086 }
6087
6088 /**
6089 * @return true iff this controller is currently used to move the selection start.
6090 */
6091 public boolean isSelectionStartDragged() {
6092 return mStartHandle != null && mStartHandle.isDragging();
6093 }
6094
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006095 @Override
6096 public boolean isCursorBeingModified() {
6097 return isDragAcceleratorActive() || isSelectionStartDragged()
6098 || (mEndHandle != null && mEndHandle.isDragging());
6099 }
6100
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006101 /**
6102 * @return true if the user is selecting text using the drag accelerator.
6103 */
6104 public boolean isDragAcceleratorActive() {
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08006105 return mDragAcceleratorMode != DRAG_ACCELERATOR_MODE_INACTIVE;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08006106 }
6107
Gilles Debunned88876a2012-03-16 17:34:04 -07006108 public void onTouchModeChanged(boolean isInTouchMode) {
6109 if (!isInTouchMode) {
6110 hide();
6111 }
6112 }
6113
6114 @Override
6115 public void onDetached() {
6116 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
6117 observer.removeOnTouchModeChangeListener(this);
6118
6119 if (mStartHandle != null) mStartHandle.onDetached();
6120 if (mEndHandle != null) mEndHandle.onDetached();
6121 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08006122
6123 @Override
6124 public boolean isActive() {
6125 return mStartHandle != null && mStartHandle.isShowing();
6126 }
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09006127
6128 public void invalidateHandles() {
6129 if (mStartHandle != null) {
6130 mStartHandle.invalidate();
6131 }
6132 if (mEndHandle != null) {
6133 mEndHandle.invalidate();
6134 }
6135 }
Gilles Debunned88876a2012-03-16 17:34:04 -07006136 }
6137
6138 private class CorrectionHighlighter {
6139 private final Path mPath = new Path();
Chris Craik6a49dde2015-05-12 10:28:14 -07006140 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
Gilles Debunned88876a2012-03-16 17:34:04 -07006141 private int mStart, mEnd;
6142 private long mFadingStartTime;
6143 private RectF mTempRectF;
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006144 private static final int FADE_OUT_DURATION = 400;
Gilles Debunned88876a2012-03-16 17:34:04 -07006145
6146 public CorrectionHighlighter() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006147 mPaint.setCompatibilityScaling(
6148 mTextView.getResources().getCompatibilityInfo().applicationScale);
Gilles Debunned88876a2012-03-16 17:34:04 -07006149 mPaint.setStyle(Paint.Style.FILL);
6150 }
6151
6152 public void highlight(CorrectionInfo info) {
6153 mStart = info.getOffset();
6154 mEnd = mStart + info.getNewText().length();
6155 mFadingStartTime = SystemClock.uptimeMillis();
6156
6157 if (mStart < 0 || mEnd < 0) {
6158 stopAnimation();
6159 }
6160 }
6161
6162 public void draw(Canvas canvas, int cursorOffsetVertical) {
6163 if (updatePath() && updatePaint()) {
6164 if (cursorOffsetVertical != 0) {
6165 canvas.translate(0, cursorOffsetVertical);
6166 }
6167
6168 canvas.drawPath(mPath, mPaint);
6169
6170 if (cursorOffsetVertical != 0) {
6171 canvas.translate(0, -cursorOffsetVertical);
6172 }
6173 invalidate(true); // TODO invalidate cursor region only
6174 } else {
6175 stopAnimation();
6176 invalidate(false); // TODO invalidate cursor region only
6177 }
6178 }
6179
6180 private boolean updatePaint() {
6181 final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
6182 if (duration > FADE_OUT_DURATION) return false;
6183
6184 final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
6185 final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006186 final int color = (mTextView.mHighlightColor & 0x00FFFFFF)
6187 + ((int) (highlightColorAlpha * coef) << 24);
Gilles Debunned88876a2012-03-16 17:34:04 -07006188 mPaint.setColor(color);
6189 return true;
6190 }
6191
6192 private boolean updatePath() {
6193 final Layout layout = mTextView.getLayout();
6194 if (layout == null) return false;
6195
6196 // Update in case text is edited while the animation is run
6197 final int length = mTextView.getText().length();
6198 int start = Math.min(length, mStart);
6199 int end = Math.min(length, mEnd);
6200
6201 mPath.reset();
6202 layout.getSelectionPath(start, end, mPath);
6203 return true;
6204 }
6205
6206 private void invalidate(boolean delayed) {
6207 if (mTextView.getLayout() == null) return;
6208
6209 if (mTempRectF == null) mTempRectF = new RectF();
6210 mPath.computeBounds(mTempRectF, false);
6211
6212 int left = mTextView.getCompoundPaddingLeft();
6213 int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
6214
6215 if (delayed) {
6216 mTextView.postInvalidateOnAnimation(
6217 left + (int) mTempRectF.left, top + (int) mTempRectF.top,
6218 left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
6219 } else {
6220 mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
6221 (int) mTempRectF.right, (int) mTempRectF.bottom);
6222 }
6223 }
6224
6225 private void stopAnimation() {
6226 Editor.this.mCorrectionHighlighter = null;
6227 }
6228 }
6229
6230 private static class ErrorPopup extends PopupWindow {
6231 private boolean mAbove = false;
6232 private final TextView mView;
6233 private int mPopupInlineErrorBackgroundId = 0;
6234 private int mPopupInlineErrorAboveBackgroundId = 0;
6235
6236 ErrorPopup(TextView v, int width, int height) {
6237 super(v, width, height);
6238 mView = v;
6239 // 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 -08006240 // shown and positioned. Initialized with below background, which should have
Gilles Debunned88876a2012-03-16 17:34:04 -07006241 // dimensions identical to the above version for this to work (and is more likely).
6242 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
6243 com.android.internal.R.styleable.Theme_errorMessageBackground);
6244 mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
6245 }
6246
6247 void fixDirection(boolean above) {
6248 mAbove = above;
6249
6250 if (above) {
6251 mPopupInlineErrorAboveBackgroundId =
6252 getResourceId(mPopupInlineErrorAboveBackgroundId,
6253 com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
6254 } else {
6255 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
6256 com.android.internal.R.styleable.Theme_errorMessageBackground);
6257 }
6258
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006259 mView.setBackgroundResource(
6260 above ? mPopupInlineErrorAboveBackgroundId : mPopupInlineErrorBackgroundId);
Gilles Debunned88876a2012-03-16 17:34:04 -07006261 }
6262
6263 private int getResourceId(int currentId, int index) {
6264 if (currentId == 0) {
6265 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
6266 R.styleable.Theme);
6267 currentId = styledAttributes.getResourceId(index, 0);
6268 styledAttributes.recycle();
6269 }
6270 return currentId;
6271 }
6272
6273 @Override
6274 public void update(int x, int y, int w, int h, boolean force) {
6275 super.update(x, y, w, h, force);
6276
6277 boolean above = isAboveAnchor();
6278 if (above != mAbove) {
6279 fixDirection(above);
6280 }
6281 }
6282 }
6283
6284 static class InputContentType {
6285 int imeOptions = EditorInfo.IME_NULL;
6286 String privateImeOptions;
6287 CharSequence imeActionLabel;
6288 int imeActionId;
6289 Bundle extras;
6290 OnEditorActionListener onEditorActionListener;
6291 boolean enterDown;
Yohei Yukawad469f212016-01-21 12:38:09 -08006292 LocaleList imeHintLocales;
Gilles Debunned88876a2012-03-16 17:34:04 -07006293 }
6294
6295 static class InputMethodState {
Gilles Debunnec62589c2012-04-12 14:50:23 -07006296 ExtractedTextRequest mExtractedTextRequest;
6297 final ExtractedText mExtractedText = new ExtractedText();
Gilles Debunned88876a2012-03-16 17:34:04 -07006298 int mBatchEditNesting;
6299 boolean mCursorChanged;
6300 boolean mSelectionModeChanged;
6301 boolean mContentChanged;
6302 int mChangedStart, mChangedEnd, mChangedDelta;
6303 }
Satoshi Kataoka0e3849a2012-12-13 14:37:19 +09006304
James Cookf59152c2015-02-26 18:03:58 -08006305 /**
James Cook471559f2015-02-27 10:31:20 -08006306 * @return True iff (start, end) is a valid range within the text.
6307 */
6308 private static boolean isValidRange(CharSequence text, int start, int end) {
6309 return 0 <= start && start <= end && end <= text.length();
6310 }
6311
Seigo Nonakaa60160b2015-08-19 12:38:35 -07006312 @VisibleForTesting
6313 public SuggestionsPopupWindow getSuggestionsPopupWindowForTesting() {
6314 return mSuggestionsPopupWindow;
6315 }
6316
James Cook471559f2015-02-27 10:31:20 -08006317 /**
James Cookf59152c2015-02-26 18:03:58 -08006318 * An InputFilter that monitors text input to maintain undo history. It does not modify the
6319 * text being typed (and hence always returns null from the filter() method).
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006320 *
6321 * TODO: Make this span aware.
James Cookf59152c2015-02-26 18:03:58 -08006322 */
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006323 public static class UndoInputFilter implements InputFilter {
James Cookf59152c2015-02-26 18:03:58 -08006324 private final Editor mEditor;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006325
James Cook48e0fac2015-02-25 15:44:51 -08006326 // Whether the current filter pass is directly caused by an end-user text edit.
6327 private boolean mIsUserEdit;
6328
James Cookd2026682015-03-03 14:40:14 -08006329 // Whether the text field is handling an IME composition. Must be parceled in case the user
6330 // rotates the screen during composition.
6331 private boolean mHasComposition;
James Cook48e0fac2015-02-25 15:44:51 -08006332
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006333 // Whether the user is expanding or shortening the text
6334 private boolean mExpanding;
6335
6336 // Whether the previous edit operation was in the current batch edit.
6337 private boolean mPreviousOperationWasInSameBatchEdit;
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08006338
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006339 public UndoInputFilter(Editor editor) {
6340 mEditor = editor;
6341 }
6342
James Cookd2026682015-03-03 14:40:14 -08006343 public void saveInstanceState(Parcel parcel) {
6344 parcel.writeInt(mIsUserEdit ? 1 : 0);
6345 parcel.writeInt(mHasComposition ? 1 : 0);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006346 parcel.writeInt(mExpanding ? 1 : 0);
6347 parcel.writeInt(mPreviousOperationWasInSameBatchEdit ? 1 : 0);
James Cookd2026682015-03-03 14:40:14 -08006348 }
6349
6350 public void restoreInstanceState(Parcel parcel) {
6351 mIsUserEdit = parcel.readInt() != 0;
6352 mHasComposition = parcel.readInt() != 0;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006353 mExpanding = parcel.readInt() != 0;
6354 mPreviousOperationWasInSameBatchEdit = parcel.readInt() != 0;
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08006355 }
6356
James Cook48e0fac2015-02-25 15:44:51 -08006357 /**
6358 * Signals that a user-triggered edit is starting.
6359 */
6360 public void beginBatchEdit() {
6361 if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit");
6362 mIsUserEdit = true;
James Cook48e0fac2015-02-25 15:44:51 -08006363 }
6364
6365 public void endBatchEdit() {
6366 if (DEBUG_UNDO) Log.d(TAG, "endBatchEdit");
6367 mIsUserEdit = false;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006368 mPreviousOperationWasInSameBatchEdit = false;
James Cook48e0fac2015-02-25 15:44:51 -08006369 }
6370
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006371 @Override
6372 public CharSequence filter(CharSequence source, int start, int end,
6373 Spanned dest, int dstart, int dend) {
6374 if (DEBUG_UNDO) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006375 Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ") "
6376 + "dest=" + dest + " (" + dstart + "-" + dend + ")");
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006377 }
James Cookf1dad1e2015-02-27 11:00:01 -08006378
James Cook48e0fac2015-02-25 15:44:51 -08006379 // Check to see if this edit should be tracked for undo.
6380 if (!canUndoEdit(source, start, end, dest, dstart, dend)) {
James Cookf1dad1e2015-02-27 11:00:01 -08006381 return null;
6382 }
6383
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006384 final boolean hadComposition = mHasComposition;
6385 mHasComposition = isComposition(source);
6386 final boolean wasExpanding = mExpanding;
6387 boolean shouldCreateSeparateState = false;
6388 if ((end - start) != (dend - dstart)) {
6389 mExpanding = (end - start) > (dend - dstart);
6390 if (hadComposition && mExpanding != wasExpanding) {
6391 shouldCreateSeparateState = true;
6392 }
James Cookd2026682015-03-03 14:40:14 -08006393 }
6394
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006395 // Handle edit.
6396 handleEdit(source, start, end, dest, dstart, dend, shouldCreateSeparateState);
James Cookd2026682015-03-03 14:40:14 -08006397 return null;
6398 }
6399
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09006400 void freezeLastEdit() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006401 mEditor.mUndoManager.beginUpdate("Edit text");
6402 EditOperation lastEdit = getLastEdit();
6403 if (lastEdit != null) {
6404 lastEdit.mFrozen = true;
James Cookd2026682015-03-03 14:40:14 -08006405 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006406 mEditor.mUndoManager.endUpdate();
James Cookd2026682015-03-03 14:40:14 -08006407 }
6408
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006409 @Retention(RetentionPolicy.SOURCE)
Jeff Sharkeyce8db992017-12-13 20:05:05 -07006410 @IntDef(prefix = { "MERGE_EDIT_MODE_" }, value = {
6411 MERGE_EDIT_MODE_FORCE_MERGE,
6412 MERGE_EDIT_MODE_NEVER_MERGE,
6413 MERGE_EDIT_MODE_NORMAL
6414 })
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006415 private @interface MergeMode {}
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006416 private static final int MERGE_EDIT_MODE_FORCE_MERGE = 0;
6417 private static final int MERGE_EDIT_MODE_NEVER_MERGE = 1;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006418 /** Use {@link EditOperation#mergeWith} to merge */
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006419 private static final int MERGE_EDIT_MODE_NORMAL = 2;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006420
6421 private void handleEdit(CharSequence source, int start, int end,
6422 Spanned dest, int dstart, int dend, boolean shouldCreateSeparateState) {
James Cook48e0fac2015-02-25 15:44:51 -08006423 // An application may install a TextWatcher to provide additional modifications after
6424 // the initial input filters run (e.g. a credit card formatter that adds spaces to a
6425 // string). This results in multiple filter() calls for what the user considers to be
6426 // a single operation. Always undo the whole set of changes in one step.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006427 @MergeMode
6428 final int mergeMode;
6429 if (isInTextWatcher() || mPreviousOperationWasInSameBatchEdit) {
6430 mergeMode = MERGE_EDIT_MODE_FORCE_MERGE;
6431 } else if (shouldCreateSeparateState) {
6432 mergeMode = MERGE_EDIT_MODE_NEVER_MERGE;
6433 } else {
6434 mergeMode = MERGE_EDIT_MODE_NORMAL;
6435 }
James Cook471559f2015-02-27 10:31:20 -08006436 // Build a new operation with all the information from this edit.
James Cookd2026682015-03-03 14:40:14 -08006437 String newText = TextUtils.substring(source, start, end);
6438 String oldText = TextUtils.substring(dest, dstart, dend);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006439 EditOperation edit = new EditOperation(mEditor, oldText, dstart, newText,
6440 mHasComposition);
6441 if (mHasComposition && TextUtils.equals(edit.mNewText, edit.mOldText)) {
6442 return;
6443 }
6444 recordEdit(edit, mergeMode);
James Cookd2026682015-03-03 14:40:14 -08006445 }
James Cook471559f2015-02-27 10:31:20 -08006446
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006447 private EditOperation getLastEdit() {
6448 final UndoManager um = mEditor.mUndoManager;
6449 return um.getLastOperation(
6450 EditOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE);
6451 }
James Cook22054252015-03-25 14:04:01 -07006452 /**
6453 * Fetches the last undo operation and checks to see if a new edit should be merged into it.
6454 * If forceMerge is true then the new edit is always merged.
6455 */
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006456 private void recordEdit(EditOperation edit, @MergeMode int mergeMode) {
James Cook471559f2015-02-27 10:31:20 -08006457 // Fetch the last edit operation and attempt to merge in the new edit.
James Cook48e0fac2015-02-25 15:44:51 -08006458 final UndoManager um = mEditor.mUndoManager;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006459 um.beginUpdate("Edit text");
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006460 EditOperation lastEdit = getLastEdit();
James Cook471559f2015-02-27 10:31:20 -08006461 if (lastEdit == null) {
6462 // Add this as the first edit.
6463 if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit);
6464 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006465 } else if (mergeMode == MERGE_EDIT_MODE_FORCE_MERGE) {
James Cook22054252015-03-25 14:04:01 -07006466 // Forced merges take priority because they could be the result of a non-user-edit
6467 // change and this case should not create a new undo operation.
6468 if (DEBUG_UNDO) Log.d(TAG, "filter: force merge " + edit);
6469 lastEdit.forceMergeWith(edit);
James Cook48e0fac2015-02-25 15:44:51 -08006470 } else if (!mIsUserEdit) {
6471 // An application directly modified the Editable outside of a text edit. Treat this
6472 // as a new change and don't attempt to merge.
6473 if (DEBUG_UNDO) Log.d(TAG, "non-user edit, new op " + edit);
6474 um.commitState(mEditor.mUndoOwner);
6475 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006476 } else if (mergeMode == MERGE_EDIT_MODE_NORMAL && lastEdit.mergeWith(edit)) {
James Cook471559f2015-02-27 10:31:20 -08006477 // Merge succeeded, nothing else to do.
6478 if (DEBUG_UNDO) Log.d(TAG, "filter: merge succeeded, created " + lastEdit);
James Cook3ac0bcb2015-02-26 10:53:41 -08006479 } else {
James Cook471559f2015-02-27 10:31:20 -08006480 // Could not merge with the last edit, so commit the last edit and add this edit.
6481 if (DEBUG_UNDO) Log.d(TAG, "filter: merge failed, adding " + edit);
6482 um.commitState(mEditor.mUndoOwner);
6483 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
James Cook3ac0bcb2015-02-26 10:53:41 -08006484 }
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09006485 mPreviousOperationWasInSameBatchEdit = mIsUserEdit;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006486 um.endUpdate();
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006487 }
James Cook48e0fac2015-02-25 15:44:51 -08006488
6489 private boolean canUndoEdit(CharSequence source, int start, int end,
6490 Spanned dest, int dstart, int dend) {
6491 if (!mEditor.mAllowUndo) {
6492 if (DEBUG_UNDO) Log.d(TAG, "filter: undo is disabled");
6493 return false;
6494 }
6495
6496 if (mEditor.mUndoManager.isInUndo()) {
6497 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping, currently performing undo/redo");
6498 return false;
6499 }
6500
6501 // Text filters run before input operations are applied. However, some input operations
6502 // are invalid and will throw exceptions when applied. This is common in tests. Don't
6503 // attempt to undo invalid operations.
6504 if (!isValidRange(source, start, end) || !isValidRange(dest, dstart, dend)) {
6505 if (DEBUG_UNDO) Log.d(TAG, "filter: invalid op");
6506 return false;
6507 }
6508
6509 // Earlier filters can rewrite input to be a no-op, for example due to a length limit
6510 // on an input field. Skip no-op changes.
6511 if (start == end && dstart == dend) {
6512 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping no-op");
6513 return false;
6514 }
6515
6516 return true;
6517 }
James Cookd2026682015-03-03 14:40:14 -08006518
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006519 private static boolean isComposition(CharSequence source) {
James Cookd2026682015-03-03 14:40:14 -08006520 if (!(source instanceof Spannable)) {
6521 return false;
6522 }
6523 // This is a composition edit if the source has a non-zero-length composing span.
6524 Spannable text = (Spannable) source;
6525 int composeBegin = EditableInputConnection.getComposingSpanStart(text);
6526 int composeEnd = EditableInputConnection.getComposingSpanEnd(text);
6527 return composeBegin < composeEnd;
6528 }
6529
6530 private boolean isInTextWatcher() {
6531 CharSequence text = mEditor.mTextView.getText();
6532 return (text instanceof SpannableStringBuilder)
6533 && ((SpannableStringBuilder) text).getTextWatcherDepth() > 0;
6534 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006535 }
6536
James Cookf59152c2015-02-26 18:03:58 -08006537 /**
6538 * An operation to undo a single "edit" to a text view.
6539 */
James Cook471559f2015-02-27 10:31:20 -08006540 public static class EditOperation extends UndoOperation<Editor> {
6541 private static final int TYPE_INSERT = 0;
6542 private static final int TYPE_DELETE = 1;
6543 private static final int TYPE_REPLACE = 2;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006544
James Cook471559f2015-02-27 10:31:20 -08006545 private int mType;
6546 private String mOldText;
James Cook471559f2015-02-27 10:31:20 -08006547 private String mNewText;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006548 private int mStart;
James Cook471559f2015-02-27 10:31:20 -08006549
6550 private int mOldCursorPos;
6551 private int mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006552 private boolean mFrozen;
6553 private boolean mIsComposition;
James Cook471559f2015-02-27 10:31:20 -08006554
6555 /**
James Cookd2026682015-03-03 14:40:14 -08006556 * Constructs an edit operation from a text input operation on editor that replaces the
James Cook22054252015-03-25 14:04:01 -07006557 * oldText starting at dstart with newText.
James Cook471559f2015-02-27 10:31:20 -08006558 */
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006559 public EditOperation(Editor editor, String oldText, int dstart, String newText,
6560 boolean isComposition) {
James Cook471559f2015-02-27 10:31:20 -08006561 super(editor.mUndoOwner);
James Cookd2026682015-03-03 14:40:14 -08006562 mOldText = oldText;
6563 mNewText = newText;
James Cook471559f2015-02-27 10:31:20 -08006564
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006565 // Determine the type of the edit.
James Cook471559f2015-02-27 10:31:20 -08006566 if (mNewText.length() > 0 && mOldText.length() == 0) {
6567 mType = TYPE_INSERT;
James Cook471559f2015-02-27 10:31:20 -08006568 } else if (mNewText.length() == 0 && mOldText.length() > 0) {
6569 mType = TYPE_DELETE;
James Cook471559f2015-02-27 10:31:20 -08006570 } else {
6571 mType = TYPE_REPLACE;
James Cook471559f2015-02-27 10:31:20 -08006572 }
6573
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006574 mStart = dstart;
James Cook471559f2015-02-27 10:31:20 -08006575 // Store cursor data.
6576 mOldCursorPos = editor.mTextView.getSelectionStart();
James Cookd2026682015-03-03 14:40:14 -08006577 mNewCursorPos = dstart + mNewText.length();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006578 mIsComposition = isComposition;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006579 }
6580
James Cook471559f2015-02-27 10:31:20 -08006581 public EditOperation(Parcel src, ClassLoader loader) {
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006582 super(src, loader);
James Cook471559f2015-02-27 10:31:20 -08006583 mType = src.readInt();
6584 mOldText = src.readString();
James Cook471559f2015-02-27 10:31:20 -08006585 mNewText = src.readString();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006586 mStart = src.readInt();
James Cook471559f2015-02-27 10:31:20 -08006587 mOldCursorPos = src.readInt();
6588 mNewCursorPos = src.readInt();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006589 mFrozen = src.readInt() == 1;
6590 mIsComposition = src.readInt() == 1;
James Cook471559f2015-02-27 10:31:20 -08006591 }
6592
6593 @Override
6594 public void writeToParcel(Parcel dest, int flags) {
6595 dest.writeInt(mType);
6596 dest.writeString(mOldText);
James Cook471559f2015-02-27 10:31:20 -08006597 dest.writeString(mNewText);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006598 dest.writeInt(mStart);
James Cook471559f2015-02-27 10:31:20 -08006599 dest.writeInt(mOldCursorPos);
6600 dest.writeInt(mNewCursorPos);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006601 dest.writeInt(mFrozen ? 1 : 0);
6602 dest.writeInt(mIsComposition ? 1 : 0);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006603 }
6604
James Cook48e0fac2015-02-25 15:44:51 -08006605 private int getNewTextEnd() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006606 return mStart + mNewText.length();
James Cook48e0fac2015-02-25 15:44:51 -08006607 }
6608
6609 private int getOldTextEnd() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006610 return mStart + mOldText.length();
James Cook48e0fac2015-02-25 15:44:51 -08006611 }
6612
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006613 @Override
6614 public void commit() {
6615 }
6616
6617 @Override
6618 public void undo() {
James Cook471559f2015-02-27 10:31:20 -08006619 if (DEBUG_UNDO) Log.d(TAG, "undo");
6620 // Remove the new text and insert the old.
James Cook48e0fac2015-02-25 15:44:51 -08006621 Editor editor = getOwnerData();
6622 Editable text = (Editable) editor.mTextView.getText();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006623 modifyText(text, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006624 }
6625
6626 @Override
6627 public void redo() {
James Cook471559f2015-02-27 10:31:20 -08006628 if (DEBUG_UNDO) Log.d(TAG, "redo");
6629 // Remove the old text and insert the new.
James Cook48e0fac2015-02-25 15:44:51 -08006630 Editor editor = getOwnerData();
6631 Editable text = (Editable) editor.mTextView.getText();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006632 modifyText(text, mStart, getOldTextEnd(), mNewText, mStart, mNewCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006633 }
6634
James Cook471559f2015-02-27 10:31:20 -08006635 /**
6636 * Attempts to merge this existing operation with a new edit.
6637 * @param edit The new edit operation.
6638 * @return If the merge succeeded, returns true. Otherwise returns false and leaves this
6639 * object unchanged.
6640 */
6641 private boolean mergeWith(EditOperation edit) {
James Cook48e0fac2015-02-25 15:44:51 -08006642 if (DEBUG_UNDO) {
6643 Log.d(TAG, "mergeWith old " + this);
6644 Log.d(TAG, "mergeWith new " + edit);
6645 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006646
6647 if (mFrozen) {
6648 return false;
6649 }
6650
James Cook471559f2015-02-27 10:31:20 -08006651 switch (mType) {
6652 case TYPE_INSERT:
6653 return mergeInsertWith(edit);
6654 case TYPE_DELETE:
6655 return mergeDeleteWith(edit);
6656 case TYPE_REPLACE:
6657 return mergeReplaceWith(edit);
6658 default:
6659 return false;
6660 }
6661 }
6662
6663 private boolean mergeInsertWith(EditOperation edit) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006664 if (edit.mType == TYPE_INSERT) {
6665 // Merge insertions that are contiguous even when it's frozen.
6666 if (getNewTextEnd() != edit.mStart) {
6667 return false;
6668 }
6669 mNewText += edit.mNewText;
6670 mNewCursorPos = edit.mNewCursorPos;
6671 mFrozen = edit.mFrozen;
6672 mIsComposition = edit.mIsComposition;
6673 return true;
James Cook471559f2015-02-27 10:31:20 -08006674 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006675 if (mIsComposition && edit.mType == TYPE_REPLACE
6676 && mStart <= edit.mStart && getNewTextEnd() >= edit.getOldTextEnd()) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006677 // Merge insertion with replace as they can be single insertion.
6678 mNewText = mNewText.substring(0, edit.mStart - mStart) + edit.mNewText
6679 + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
6680 mNewCursorPos = edit.mNewCursorPos;
6681 mIsComposition = edit.mIsComposition;
6682 return true;
James Cook471559f2015-02-27 10:31:20 -08006683 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006684 return false;
James Cook471559f2015-02-27 10:31:20 -08006685 }
6686
6687 // TODO: Support forward delete.
6688 private boolean mergeDeleteWith(EditOperation edit) {
James Cook471559f2015-02-27 10:31:20 -08006689 // Only merge continuous deletes.
6690 if (edit.mType != TYPE_DELETE) {
6691 return false;
6692 }
6693 // Only merge deletions that are contiguous.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006694 if (mStart != edit.getOldTextEnd()) {
James Cook471559f2015-02-27 10:31:20 -08006695 return false;
6696 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006697 mStart = edit.mStart;
James Cook471559f2015-02-27 10:31:20 -08006698 mOldText = edit.mOldText + mOldText;
6699 mNewCursorPos = edit.mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006700 mIsComposition = edit.mIsComposition;
James Cook471559f2015-02-27 10:31:20 -08006701 return true;
6702 }
6703
6704 private boolean mergeReplaceWith(EditOperation edit) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006705 if (edit.mType == TYPE_INSERT && getNewTextEnd() == edit.mStart) {
6706 // Merge with adjacent insert.
6707 mNewText += edit.mNewText;
6708 mNewCursorPos = edit.mNewCursorPos;
6709 return true;
6710 }
6711 if (!mIsComposition) {
James Cook471559f2015-02-27 10:31:20 -08006712 return false;
6713 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006714 if (edit.mType == TYPE_DELETE && mStart <= edit.mStart
6715 && getNewTextEnd() >= edit.getOldTextEnd()) {
6716 // Merge with delete as they can be single operation.
6717 mNewText = mNewText.substring(0, edit.mStart - mStart)
6718 + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
6719 if (mNewText.isEmpty()) {
6720 mType = TYPE_DELETE;
6721 }
6722 mNewCursorPos = edit.mNewCursorPos;
6723 mIsComposition = edit.mIsComposition;
6724 return true;
6725 }
6726 if (edit.mType == TYPE_REPLACE && mStart == edit.mStart
6727 && TextUtils.equals(mNewText, edit.mOldText)) {
6728 // Merge with the replace that replaces the same region.
6729 mNewText = edit.mNewText;
6730 mNewCursorPos = edit.mNewCursorPos;
6731 mIsComposition = edit.mIsComposition;
6732 return true;
6733 }
6734 return false;
James Cook471559f2015-02-27 10:31:20 -08006735 }
6736
James Cook48e0fac2015-02-25 15:44:51 -08006737 /**
6738 * Forcibly creates a single merged edit operation by simulating the entire text
6739 * contents being replaced.
6740 */
James Cook22054252015-03-25 14:04:01 -07006741 public void forceMergeWith(EditOperation edit) {
James Cook48e0fac2015-02-25 15:44:51 -08006742 if (DEBUG_UNDO) Log.d(TAG, "forceMerge");
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006743 if (mergeWith(edit)) {
6744 return;
6745 }
James Cookf59152c2015-02-26 18:03:58 -08006746 Editor editor = getOwnerData();
James Cook48e0fac2015-02-25 15:44:51 -08006747
6748 // Copy the text of the current field.
6749 // NOTE: Using StringBuilder instead of SpannableStringBuilder would be somewhat faster,
6750 // but would require two parallel implementations of modifyText() because Editable and
6751 // StringBuilder do not share an interface for replace/delete/insert.
6752 Editable editable = (Editable) editor.mTextView.getText();
6753 Editable originalText = new SpannableStringBuilder(editable.toString());
6754
6755 // Roll back the last operation.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006756 modifyText(originalText, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
James Cook48e0fac2015-02-25 15:44:51 -08006757
6758 // Clone the text again and apply the new operation.
6759 Editable finalText = new SpannableStringBuilder(editable.toString());
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006760 modifyText(finalText, edit.mStart, edit.getOldTextEnd(),
6761 edit.mNewText, edit.mStart, edit.mNewCursorPos);
James Cook48e0fac2015-02-25 15:44:51 -08006762
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006763 // Convert this operation into a replace operation.
James Cook48e0fac2015-02-25 15:44:51 -08006764 mType = TYPE_REPLACE;
6765 mNewText = finalText.toString();
James Cook48e0fac2015-02-25 15:44:51 -08006766 mOldText = originalText.toString();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006767 mStart = 0;
James Cook48e0fac2015-02-25 15:44:51 -08006768 mNewCursorPos = edit.mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006769 mIsComposition = edit.mIsComposition;
James Cook48e0fac2015-02-25 15:44:51 -08006770 // mOldCursorPos is unchanged.
6771 }
6772
6773 private static void modifyText(Editable text, int deleteFrom, int deleteTo,
6774 CharSequence newText, int newTextInsertAt, int newCursorPos) {
James Cook471559f2015-02-27 10:31:20 -08006775 // Apply the edit if it is still valid.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006776 if (isValidRange(text, deleteFrom, deleteTo)
6777 && newTextInsertAt <= text.length() - (deleteTo - deleteFrom)) {
James Cook471559f2015-02-27 10:31:20 -08006778 if (deleteFrom != deleteTo) {
6779 text.delete(deleteFrom, deleteTo);
6780 }
6781 if (newText.length() != 0) {
6782 text.insert(newTextInsertAt, newText);
6783 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006784 }
James Cook900185d2015-03-10 09:48:11 -07006785 // Restore the cursor position. If there wasn't an old cursor (newCursorPos == -1) then
6786 // don't explicitly set it and rely on SpannableStringBuilder to position it.
James Cook471559f2015-02-27 10:31:20 -08006787 // TODO: Select all the text that was undone.
James Cook900185d2015-03-10 09:48:11 -07006788 if (0 <= newCursorPos && newCursorPos <= text.length()) {
James Cook471559f2015-02-27 10:31:20 -08006789 Selection.setSelection(text, newCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006790 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006791 }
6792
James Cook48e0fac2015-02-25 15:44:51 -08006793 private String getTypeString() {
6794 switch (mType) {
6795 case TYPE_INSERT:
6796 return "insert";
6797 case TYPE_DELETE:
6798 return "delete";
6799 case TYPE_REPLACE:
6800 return "replace";
6801 default:
6802 return "";
6803 }
6804 }
6805
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006806 @Override
James Cook471559f2015-02-27 10:31:20 -08006807 public String toString() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006808 return "[mType=" + getTypeString() + ", "
6809 + "mOldText=" + mOldText + ", "
6810 + "mNewText=" + mNewText + ", "
6811 + "mStart=" + mStart + ", "
6812 + "mOldCursorPos=" + mOldCursorPos + ", "
6813 + "mNewCursorPos=" + mNewCursorPos + ", "
6814 + "mFrozen=" + mFrozen + ", "
6815 + "mIsComposition=" + mIsComposition + "]";
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006816 }
6817
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006818 public static final Parcelable.ClassLoaderCreator<EditOperation> CREATOR =
6819 new Parcelable.ClassLoaderCreator<EditOperation>() {
James Cookf59152c2015-02-26 18:03:58 -08006820 @Override
James Cook471559f2015-02-27 10:31:20 -08006821 public EditOperation createFromParcel(Parcel in) {
6822 return new EditOperation(in, null);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006823 }
6824
James Cookf59152c2015-02-26 18:03:58 -08006825 @Override
James Cook471559f2015-02-27 10:31:20 -08006826 public EditOperation createFromParcel(Parcel in, ClassLoader loader) {
6827 return new EditOperation(in, loader);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006828 }
6829
James Cookf59152c2015-02-26 18:03:58 -08006830 @Override
James Cook471559f2015-02-27 10:31:20 -08006831 public EditOperation[] newArray(int size) {
6832 return new EditOperation[size];
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006833 }
6834 };
6835 }
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006836
6837 /**
6838 * A helper for enabling and handling "PROCESS_TEXT" menu actions.
6839 * These allow external applications to plug into currently selected text.
6840 */
6841 static final class ProcessTextIntentActionsHandler {
6842
6843 private final Editor mEditor;
6844 private final TextView mTextView;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006845 private final Context mContext;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006846 private final PackageManager mPackageManager;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006847 private final String mPackageName;
6848 private final SparseArray<Intent> mAccessibilityIntents = new SparseArray<>();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006849 private final SparseArray<AccessibilityNodeInfo.AccessibilityAction> mAccessibilityActions =
6850 new SparseArray<>();
6851 private final List<ResolveInfo> mSupportedActivities = new ArrayList<>();
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006852
6853 private ProcessTextIntentActionsHandler(Editor editor) {
6854 mEditor = Preconditions.checkNotNull(editor);
6855 mTextView = Preconditions.checkNotNull(mEditor.mTextView);
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006856 mContext = Preconditions.checkNotNull(mTextView.getContext());
6857 mPackageManager = Preconditions.checkNotNull(mContext.getPackageManager());
6858 mPackageName = Preconditions.checkNotNull(mContext.getPackageName());
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006859 }
6860
6861 /**
6862 * Adds "PROCESS_TEXT" menu items to the specified menu.
6863 */
6864 public void onInitializeMenu(Menu menu) {
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006865 loadSupportedActivities();
Abodunrinwa Tokic28be382017-11-07 18:46:50 +00006866 final int size = mSupportedActivities.size();
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +01006867 for (int i = 0; i < size; i++) {
6868 final ResolveInfo resolveInfo = mSupportedActivities.get(i);
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006869 menu.add(Menu.NONE, Menu.NONE,
Abodunrinwa Tokic28be382017-11-07 18:46:50 +00006870 Editor.MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i,
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006871 getLabel(resolveInfo))
6872 .setIntent(createProcessTextIntentForResolveInfo(resolveInfo))
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00006873 .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006874 }
6875 }
6876
6877 /**
6878 * Performs a "PROCESS_TEXT" action if there is one associated with the specified
6879 * menu item.
6880 *
6881 * @return True if the action was performed, false otherwise.
6882 */
6883 public boolean performMenuItemAction(MenuItem item) {
6884 return fireIntent(item.getIntent());
6885 }
6886
6887 /**
6888 * Initializes and caches "PROCESS_TEXT" accessibility actions.
6889 */
6890 public void initializeAccessibilityActions() {
6891 mAccessibilityIntents.clear();
6892 mAccessibilityActions.clear();
6893 int i = 0;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006894 loadSupportedActivities();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006895 for (ResolveInfo resolveInfo : mSupportedActivities) {
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006896 int actionId = TextView.ACCESSIBILITY_ACTION_PROCESS_TEXT_START_ID + i++;
6897 mAccessibilityActions.put(
6898 actionId,
6899 new AccessibilityNodeInfo.AccessibilityAction(
6900 actionId, getLabel(resolveInfo)));
6901 mAccessibilityIntents.put(
6902 actionId, createProcessTextIntentForResolveInfo(resolveInfo));
6903 }
6904 }
6905
6906 /**
6907 * Adds "PROCESS_TEXT" accessibility actions to the specified accessibility node info.
6908 * NOTE: This needs a prior call to {@link #initializeAccessibilityActions()} to make the
6909 * latest accessibility actions available for this call.
6910 */
6911 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) {
6912 for (int i = 0; i < mAccessibilityActions.size(); i++) {
6913 nodeInfo.addAction(mAccessibilityActions.valueAt(i));
6914 }
6915 }
6916
6917 /**
6918 * Performs a "PROCESS_TEXT" action if there is one associated with the specified
6919 * accessibility action id.
6920 *
6921 * @return True if the action was performed, false otherwise.
6922 */
6923 public boolean performAccessibilityAction(int actionId) {
6924 return fireIntent(mAccessibilityIntents.get(actionId));
6925 }
6926
6927 private boolean fireIntent(Intent intent) {
6928 if (intent != null && Intent.ACTION_PROCESS_TEXT.equals(intent.getAction())) {
Siyamed Sinirce3b05a2017-07-18 18:54:31 -07006929 String selectedText = mTextView.getSelectedText();
6930 selectedText = TextUtils.trimToParcelableSize(selectedText);
6931 intent.putExtra(Intent.EXTRA_PROCESS_TEXT, selectedText);
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08006932 mEditor.mPreserveSelection = true;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006933 mTextView.startActivityForResult(intent, TextView.PROCESS_TEXT_REQUEST_CODE);
6934 return true;
6935 }
6936 return false;
6937 }
6938
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006939 private void loadSupportedActivities() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006940 mSupportedActivities.clear();
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01006941 if (!mContext.canStartActivityForResult()) {
6942 return;
6943 }
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006944 PackageManager packageManager = mTextView.getContext().getPackageManager();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006945 List<ResolveInfo> unfiltered =
6946 packageManager.queryIntentActivities(createProcessTextIntent(), 0);
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006947 for (ResolveInfo info : unfiltered) {
6948 if (isSupportedActivity(info)) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006949 mSupportedActivities.add(info);
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006950 }
6951 }
6952 }
6953
6954 private boolean isSupportedActivity(ResolveInfo info) {
6955 return mPackageName.equals(info.activityInfo.packageName)
6956 || info.activityInfo.exported
6957 && (info.activityInfo.permission == null
6958 || mContext.checkSelfPermission(info.activityInfo.permission)
6959 == PackageManager.PERMISSION_GRANTED);
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006960 }
6961
6962 private Intent createProcessTextIntentForResolveInfo(ResolveInfo info) {
6963 return createProcessTextIntent()
6964 .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !mTextView.isTextEditable())
6965 .setClassName(info.activityInfo.packageName, info.activityInfo.name);
6966 }
6967
6968 private Intent createProcessTextIntent() {
6969 return new Intent()
6970 .setAction(Intent.ACTION_PROCESS_TEXT)
6971 .setType("text/plain");
6972 }
6973
6974 private CharSequence getLabel(ResolveInfo resolveInfo) {
6975 return resolveInfo.loadLabel(mPackageManager);
6976 }
6977 }
Gilles Debunned88876a2012-03-16 17:34:04 -07006978}