blob: 7fa80666d053c21dea97641d46018bd550897fd2 [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;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +090020import android.annotation.IntDef;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +090021import android.annotation.NonNull;
Yoshiki Iguchiee147722015-04-14 00:12:44 +090022import android.annotation.Nullable;
Luca Zanolin1b15ba52013-02-20 14:31:37 +000023import android.app.PendingIntent;
24import android.app.PendingIntent.CanceledException;
Gilles Debunned88876a2012-03-16 17:34:04 -070025import android.content.ClipData;
26import android.content.ClipData.Item;
27import android.content.Context;
28import android.content.Intent;
Raph Levien26d443a2015-03-30 14:18:32 -070029import android.content.UndoManager;
30import android.content.UndoOperation;
31import android.content.UndoOwner;
Gilles Debunned88876a2012-03-16 17:34:04 -070032import android.content.pm.PackageManager;
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +000033import android.content.pm.ResolveInfo;
Gilles Debunned88876a2012-03-16 17:34:04 -070034import android.content.res.TypedArray;
35import android.graphics.Canvas;
36import android.graphics.Color;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +090037import android.graphics.Matrix;
Gilles Debunned88876a2012-03-16 17:34:04 -070038import android.graphics.Paint;
39import android.graphics.Path;
40import android.graphics.Rect;
41import android.graphics.RectF;
Seigo Nonaka3ed1b392016-01-19 13:54:59 +090042import android.graphics.drawable.ColorDrawable;
Gilles Debunned88876a2012-03-16 17:34:04 -070043import android.graphics.drawable.Drawable;
Abodunrinwa Toki54486c12017-04-19 21:02:36 +010044import android.metrics.LogMaker;
Gilles Debunned88876a2012-03-16 17:34:04 -070045import android.os.Bundle;
Yohei Yukawa23cbe852016-05-17 16:42:58 -070046import android.os.LocaleList;
Raph Levien26d443a2015-03-30 14:18:32 -070047import android.os.Parcel;
48import android.os.Parcelable;
James Cookf59152c2015-02-26 18:03:58 -080049import android.os.ParcelableParcel;
Gilles Debunned88876a2012-03-16 17:34:04 -070050import android.os.SystemClock;
51import android.provider.Settings;
52import android.text.DynamicLayout;
53import android.text.Editable;
Raph Levien26d443a2015-03-30 14:18:32 -070054import android.text.InputFilter;
Gilles Debunned88876a2012-03-16 17:34:04 -070055import android.text.InputType;
56import android.text.Layout;
57import android.text.ParcelableSpan;
58import android.text.Selection;
59import android.text.SpanWatcher;
60import android.text.Spannable;
61import android.text.SpannableStringBuilder;
62import android.text.Spanned;
63import android.text.StaticLayout;
64import android.text.TextUtils;
Gilles Debunned88876a2012-03-16 17:34:04 -070065import android.text.method.KeyListener;
66import android.text.method.MetaKeyKeyListener;
67import android.text.method.MovementMethod;
Gilles Debunned88876a2012-03-16 17:34:04 -070068import android.text.method.WordIterator;
69import android.text.style.EasyEditSpan;
70import android.text.style.SuggestionRangeSpan;
71import android.text.style.SuggestionSpan;
72import android.text.style.TextAppearanceSpan;
73import android.text.style.URLSpan;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +090074import android.util.ArraySet;
Gilles Debunned88876a2012-03-16 17:34:04 -070075import android.util.DisplayMetrics;
76import android.util.Log;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -070077import android.util.SparseArray;
Gilles Debunned88876a2012-03-16 17:34:04 -070078import android.view.ActionMode;
79import android.view.ActionMode.Callback;
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +090080import android.view.ContextMenu;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +090081import android.view.ContextThemeWrapper;
Chris Craikf6829a02015-03-10 10:28:59 -070082import android.view.DisplayListCanvas;
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -070083import android.view.DragAndDropPermissions;
Gilles Debunned88876a2012-03-16 17:34:04 -070084import android.view.DragEvent;
85import android.view.Gravity;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -070086import android.view.HapticFeedbackConstants;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -080087import android.view.InputDevice;
Gilles Debunned88876a2012-03-16 17:34:04 -070088import android.view.LayoutInflater;
89import android.view.Menu;
90import android.view.MenuItem;
91import android.view.MotionEvent;
Chris Craikf6829a02015-03-10 10:28:59 -070092import android.view.RenderNode;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +090093import android.view.SubMenu;
Gilles Debunned88876a2012-03-16 17:34:04 -070094import android.view.View;
Gilles Debunned88876a2012-03-16 17:34:04 -070095import android.view.View.DragShadowBuilder;
96import android.view.View.OnClickListener;
Adam Powell057a5852012-05-11 10:28:38 -070097import android.view.ViewConfiguration;
98import android.view.ViewGroup;
Gilles Debunned88876a2012-03-16 17:34:04 -070099import android.view.ViewGroup.LayoutParams;
Gilles Debunned88876a2012-03-16 17:34:04 -0700100import android.view.ViewTreeObserver;
101import android.view.WindowManager;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700102import android.view.accessibility.AccessibilityNodeInfo;
Gilles Debunned88876a2012-03-16 17:34:04 -0700103import android.view.inputmethod.CorrectionInfo;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900104import android.view.inputmethod.CursorAnchorInfo;
Gilles Debunned88876a2012-03-16 17:34:04 -0700105import android.view.inputmethod.EditorInfo;
106import android.view.inputmethod.ExtractedText;
107import android.view.inputmethod.ExtractedTextRequest;
108import android.view.inputmethod.InputConnection;
109import android.view.inputmethod.InputMethodManager;
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100110import android.view.textclassifier.TextClassification;
Gilles Debunned88876a2012-03-16 17:34:04 -0700111import android.widget.AdapterView.OnItemClickListener;
112import android.widget.TextView.Drawables;
113import android.widget.TextView.OnEditorActionListener;
114
Seigo Nonakaa60160b2015-08-19 12:38:35 -0700115import com.android.internal.annotations.VisibleForTesting;
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +0000116import com.android.internal.logging.MetricsLogger;
117import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
Raph Levien26d443a2015-03-30 14:18:32 -0700118import com.android.internal.util.ArrayUtils;
119import com.android.internal.util.GrowingArrayUtils;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700120import com.android.internal.util.Preconditions;
Raph Levien26d443a2015-03-30 14:18:32 -0700121import com.android.internal.widget.EditableInputConnection;
122
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +0900123import java.lang.annotation.Retention;
124import java.lang.annotation.RetentionPolicy;
Andrei Stingaceanu2aaeefe2015-10-20 19:11:23 +0100125import java.text.BreakIterator;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +0100126import java.util.ArrayList;
Andrei Stingaceanu2aaeefe2015-10-20 19:11:23 +0100127import java.util.Arrays;
128import java.util.Comparator;
129import java.util.HashMap;
130import java.util.List;
131
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700132
Gilles Debunned88876a2012-03-16 17:34:04 -0700133/**
134 * Helper class used by TextView to handle editable text views.
135 *
136 * @hide
137 */
138public class Editor {
Adam Powell057a5852012-05-11 10:28:38 -0700139 private static final String TAG = "Editor";
James Cookf59152c2015-02-26 18:03:58 -0800140 private static final boolean DEBUG_UNDO = false;
Adam Powell057a5852012-05-11 10:28:38 -0700141
Gilles Debunned88876a2012-03-16 17:34:04 -0700142 static final int BLINK = 500;
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700143 private static final int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
Mady Mellorcc65c372015-06-17 09:25:19 -0700144 private static final float LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS = 0.5f;
Mady Mellore264ac32015-06-22 16:46:29 -0700145 private static final int UNSET_X_VALUE = -1;
Mady Mellora6a0f782015-07-10 16:43:32 -0700146 private static final int UNSET_LINE = -1;
James Cookf59152c2015-02-26 18:03:58 -0800147 // Tag used when the Editor maintains its own separate UndoManager.
148 private static final String UNDO_OWNER_TAG = "Editor";
Gilles Debunned88876a2012-03-16 17:34:04 -0700149
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900150 // Ordering constants used to place the Action Mode or context menu items in their menu.
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +0100151 private static final int MENU_ITEM_ORDER_ASSIST = 0;
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +0000152 private static final int MENU_ITEM_ORDER_UNDO = 2;
153 private static final int MENU_ITEM_ORDER_REDO = 3;
Abodunrinwa Toki5fedfb82017-02-06 19:34:00 +0000154 private static final int MENU_ITEM_ORDER_CUT = 4;
155 private static final int MENU_ITEM_ORDER_COPY = 5;
156 private static final int MENU_ITEM_ORDER_PASTE = 6;
157 private static final int MENU_ITEM_ORDER_SHARE = 7;
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +0100158 private static final int MENU_ITEM_ORDER_SELECT_ALL = 8;
159 private static final int MENU_ITEM_ORDER_REPLACE = 9;
160 private static final int MENU_ITEM_ORDER_AUTOFILL = 10;
161 private static final int MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT = 11;
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +0100162 private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 100;
Clara Bayarri3b69fd82015-06-03 21:52:02 +0100163
James Cookf59152c2015-02-26 18:03:58 -0800164 // Each Editor manages its own undo stack.
165 private final UndoManager mUndoManager = new UndoManager();
166 private UndoOwner mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
James Cook48e0fac2015-02-25 15:44:51 -0800167 final UndoInputFilter mUndoInputFilter = new UndoInputFilter(this);
James Cookf1dad1e2015-02-27 11:00:01 -0800168 boolean mAllowUndo = true;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700169
Abodunrinwa Toki54486c12017-04-19 21:02:36 +0100170 private final MetricsLogger mMetricsLogger = new MetricsLogger();
171
Gilles Debunned88876a2012-03-16 17:34:04 -0700172 // Cursor Controllers.
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900173 private InsertionPointCursorController mInsertionPointCursorController;
Gilles Debunned88876a2012-03-16 17:34:04 -0700174 SelectionModifierCursorController mSelectionModifierCursorController;
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100175 // Action mode used when text is selected or when actions on an insertion cursor are triggered.
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800176 private ActionMode mTextActionMode;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900177 private boolean mInsertionControllerEnabled;
178 private boolean mSelectionControllerEnabled;
Gilles Debunned88876a2012-03-16 17:34:04 -0700179
Yohei Yukawac9cd9db2017-06-19 18:27:34 -0700180 private final boolean mHapticTextHandleEnabled;
181
Gilles Debunned88876a2012-03-16 17:34:04 -0700182 // Used to highlight a word when it is corrected by the IME
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900183 private CorrectionHighlighter mCorrectionHighlighter;
Gilles Debunned88876a2012-03-16 17:34:04 -0700184
185 InputContentType mInputContentType;
186 InputMethodState mInputMethodState;
187
Chris Craik956f3402015-04-27 16:41:00 -0700188 private static class TextRenderNode {
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +0900189 // Render node has 3 recording states:
190 // 1. Recorded operations are valid.
191 // #needsRecord() returns false, but needsToBeShifted is false.
192 // 2. Recorded operations are not valid, but just the position needed to be updated.
193 // #needsRecord() returns false, but needsToBeShifted is true.
194 // 3. Recorded operations are not valid. Need to record operations. #needsRecord() returns
195 // true.
Chris Craik956f3402015-04-27 16:41:00 -0700196 RenderNode renderNode;
John Reck7558aa72014-03-05 14:59:59 -0800197 boolean isDirty;
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +0900198 // Becomes true when recorded operations can be reused, but the position has to be updated.
199 boolean needsToBeShifted;
Chris Craik956f3402015-04-27 16:41:00 -0700200 public TextRenderNode(String name) {
Chris Craik956f3402015-04-27 16:41:00 -0700201 renderNode = RenderNode.create(name, null);
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +0900202 isDirty = true;
203 needsToBeShifted = true;
John Reck7558aa72014-03-05 14:59:59 -0800204 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700205 boolean needsRecord() {
206 return isDirty || !renderNode.isValid();
207 }
John Reck7558aa72014-03-05 14:59:59 -0800208 }
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900209 private TextRenderNode[] mTextRenderNodes;
Gilles Debunned88876a2012-03-16 17:34:04 -0700210
211 boolean mFrozenWithFocus;
212 boolean mSelectionMoved;
213 boolean mTouchFocusSelected;
214
215 KeyListener mKeyListener;
216 int mInputType = EditorInfo.TYPE_NULL;
217
218 boolean mDiscardNextActionUp;
219 boolean mIgnoreActionUpEvent;
220
221 long mShowCursor;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900222 private Blink mBlink;
Gilles Debunned88876a2012-03-16 17:34:04 -0700223
224 boolean mCursorVisible = true;
225 boolean mSelectAllOnFocus;
226 boolean mTextIsSelectable;
227
228 CharSequence mError;
229 boolean mErrorWasChanged;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900230 private ErrorPopup mErrorPopup;
Fabrice Di Meglio1957d282012-10-25 17:42:39 -0700231
Gilles Debunned88876a2012-03-16 17:34:04 -0700232 /**
233 * This flag is set if the TextView tries to display an error before it
234 * is attached to the window (so its position is still unknown).
235 * It causes the error to be shown later, when onAttachedToWindow()
236 * is called.
237 */
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900238 private boolean mShowErrorAfterAttach;
Gilles Debunned88876a2012-03-16 17:34:04 -0700239
240 boolean mInBatchEditControllers;
Gilles Debunne3473b2b2012-04-20 16:21:10 -0700241 boolean mShowSoftInputOnFocus = true;
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -0800242 private boolean mPreserveSelection;
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +0900243 private boolean mRestartActionModeOnNextRefresh;
Gilles Debunned88876a2012-03-16 17:34:04 -0700244
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800245 private SelectionActionModeHelper mSelectionActionModeHelper;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +0000246
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900247 boolean mIsBeingLongClicked;
248
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900249 private SuggestionsPopupWindow mSuggestionsPopupWindow;
Gilles Debunned88876a2012-03-16 17:34:04 -0700250 SuggestionRangeSpan mSuggestionRangeSpan;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900251 private Runnable mShowSuggestionRunnable;
Gilles Debunned88876a2012-03-16 17:34:04 -0700252
Roozbeh Pournader9c133072017-07-26 22:36:27 -0700253 Drawable mCursorDrawable = null;
Gilles Debunned88876a2012-03-16 17:34:04 -0700254
255 private Drawable mSelectHandleLeft;
256 private Drawable mSelectHandleRight;
257 private Drawable mSelectHandleCenter;
258
259 // Global listener that detects changes in the global position of the TextView
260 private PositionListener mPositionListener;
261
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900262 private float mLastDownPositionX, mLastDownPositionY;
Petar Å egina91df3f92017-08-15 16:20:43 +0100263 private float mLastUpPositionX, mLastUpPositionY;
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900264 private float mContextMenuAnchorX, mContextMenuAnchorY;
Gilles Debunned88876a2012-03-16 17:34:04 -0700265 Callback mCustomSelectionActionModeCallback;
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100266 Callback mCustomInsertionActionModeCallback;
Gilles Debunned88876a2012-03-16 17:34:04 -0700267
268 // Set when this TextView gained focus with some text selected. Will start selection mode.
269 boolean mCreatedWithASelection;
270
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +0900271 // Indicates the current tap state (first tap, double tap, or triple click).
272 private int mTapState = TAP_STATE_INITIAL;
273 private long mLastTouchUpTime = 0;
274 private static final int TAP_STATE_INITIAL = 0;
275 private static final int TAP_STATE_FIRST_TAP = 1;
276 private static final int TAP_STATE_DOUBLE_TAP = 2;
277 // Only for mouse input.
278 private static final int TAP_STATE_TRIPLE_CLICK = 3;
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100279
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900280 // The button state as of the last time #onTouchEvent is called.
281 private int mLastButtonState;
282
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100283 private Runnable mInsertionActionModeRunnable;
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100284
Jean Chalardbaf30942013-02-28 16:01:51 -0800285 // The span controller helps monitoring the changes to which the Editor needs to react:
286 // - EasyEditSpans, for which we have some UI to display on attach and on hide
287 // - SelectionSpans, for which we need to call updateSelection if an IME is attached
288 private SpanController mSpanController;
Gilles Debunned88876a2012-03-16 17:34:04 -0700289
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900290 private WordIterator mWordIterator;
Gilles Debunned88876a2012-03-16 17:34:04 -0700291 SpellChecker mSpellChecker;
292
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800293 // This word iterator is set with text and used to determine word boundaries
294 // when a user is selecting text.
295 private WordIterator mWordIteratorWithText;
296 // Indicate that the text in the word iterator needs to be updated.
297 private boolean mUpdateWordIteratorText;
298
Gilles Debunned88876a2012-03-16 17:34:04 -0700299 private Rect mTempRect;
300
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800301 private final TextView mTextView;
Gilles Debunned88876a2012-03-16 17:34:04 -0700302
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700303 final ProcessTextIntentActionsHandler mProcessTextIntentActionsHandler;
304
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700305 private final CursorAnchorInfoNotifier mCursorAnchorInfoNotifier =
306 new CursorAnchorInfoNotifier();
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900307
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100308 private final Runnable mShowFloatingToolbar = new Runnable() {
309 @Override
310 public void run() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100311 if (mTextActionMode != null) {
Abodunrinwa Toki9e211282015-06-05 02:46:57 +0100312 mTextActionMode.hide(0); // hide off.
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100313 }
314 }
315 };
316
Clara Bayarrib71dddd2015-06-04 23:17:30 +0100317 boolean mIsInsertionActionModeStartPending = false;
318
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +0900319 private final SuggestionHelper mSuggestionHelper = new SuggestionHelper();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +0900320
Gilles Debunned88876a2012-03-16 17:34:04 -0700321 Editor(TextView textView) {
322 mTextView = textView;
James Cookf59152c2015-02-26 18:03:58 -0800323 // Synchronize the filter list, which places the undo input filter at the end.
324 mTextView.setFilters(mTextView.getFilters());
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700325 mProcessTextIntentActionsHandler = new ProcessTextIntentActionsHandler(this);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -0700326 mHapticTextHandleEnabled = mTextView.getContext().getResources().getBoolean(
327 com.android.internal.R.bool.config_enableHapticTextHandle);
James Cookf59152c2015-02-26 18:03:58 -0800328 }
329
330 ParcelableParcel saveInstanceState() {
James Cookd2026682015-03-03 14:40:14 -0800331 ParcelableParcel state = new ParcelableParcel(getClass().getClassLoader());
332 Parcel parcel = state.getParcel();
333 mUndoManager.saveInstanceState(parcel);
334 mUndoInputFilter.saveInstanceState(parcel);
335 return state;
James Cookf59152c2015-02-26 18:03:58 -0800336 }
337
338 void restoreInstanceState(ParcelableParcel state) {
James Cookd2026682015-03-03 14:40:14 -0800339 Parcel parcel = state.getParcel();
340 mUndoManager.restoreInstanceState(parcel, state.getClassLoader());
341 mUndoInputFilter.restoreInstanceState(parcel);
James Cookf59152c2015-02-26 18:03:58 -0800342 // Re-associate this object as the owner of undo state.
343 mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
344 }
345
James Cook48e0fac2015-02-25 15:44:51 -0800346 /**
347 * Forgets all undo and redo operations for this Editor.
348 */
349 void forgetUndoRedo() {
350 UndoOwner[] owners = { mUndoOwner };
351 mUndoManager.forgetUndos(owners, -1 /* all */);
352 mUndoManager.forgetRedos(owners, -1 /* all */);
353 }
354
James Cookf59152c2015-02-26 18:03:58 -0800355 boolean canUndo() {
356 UndoOwner[] owners = { mUndoOwner };
James Cookf1dad1e2015-02-27 11:00:01 -0800357 return mAllowUndo && mUndoManager.countUndos(owners) > 0;
James Cookf59152c2015-02-26 18:03:58 -0800358 }
359
360 boolean canRedo() {
361 UndoOwner[] owners = { mUndoOwner };
James Cookf1dad1e2015-02-27 11:00:01 -0800362 return mAllowUndo && mUndoManager.countRedos(owners) > 0;
James Cookf59152c2015-02-26 18:03:58 -0800363 }
364
365 void undo() {
James Cookf1dad1e2015-02-27 11:00:01 -0800366 if (!mAllowUndo) {
367 return;
368 }
James Cookf59152c2015-02-26 18:03:58 -0800369 UndoOwner[] owners = { mUndoOwner };
370 mUndoManager.undo(owners, 1); // Undo 1 action.
371 }
372
373 void redo() {
James Cookf1dad1e2015-02-27 11:00:01 -0800374 if (!mAllowUndo) {
375 return;
376 }
James Cookf59152c2015-02-26 18:03:58 -0800377 UndoOwner[] owners = { mUndoOwner };
378 mUndoManager.redo(owners, 1); // Redo 1 action.
Gilles Debunned88876a2012-03-16 17:34:04 -0700379 }
380
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100381 void replace() {
Keisuke Kuroyanagi713be062016-02-29 16:07:54 -0800382 if (mSuggestionsPopupWindow == null) {
383 mSuggestionsPopupWindow = new SuggestionsPopupWindow();
384 }
385 hideCursorAndSpanControllers();
386 mSuggestionsPopupWindow.show();
387
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100388 int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100389 Selection.setSelection((Spannable) mTextView.getText(), middle);
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100390 }
391
Gilles Debunned88876a2012-03-16 17:34:04 -0700392 void onAttachedToWindow() {
393 if (mShowErrorAfterAttach) {
394 showError();
395 mShowErrorAfterAttach = false;
396 }
397
398 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
399 // No need to create the controller.
400 // The get method will add the listener on controller creation.
401 if (mInsertionPointCursorController != null) {
402 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
403 }
404 if (mSelectionModifierCursorController != null) {
Adam Powell057a5852012-05-11 10:28:38 -0700405 mSelectionModifierCursorController.resetTouchOffsets();
Gilles Debunned88876a2012-03-16 17:34:04 -0700406 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
407 }
408 updateSpellCheckSpans(0, mTextView.getText().length(),
409 true /* create the spell checker if needed */);
Adam Powell057a5852012-05-11 10:28:38 -0700410
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +0900411 if (mTextView.hasSelection()) {
412 refreshTextActionMode();
Adam Powell057a5852012-05-11 10:28:38 -0700413 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900414
415 getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true);
Mikael Gullstrand5b734f22013-07-09 14:41:28 +0200416 resumeBlink();
Gilles Debunned88876a2012-03-16 17:34:04 -0700417 }
418
419 void onDetachedFromWindow() {
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900420 getPositionListener().removeSubscriber(mCursorAnchorInfoNotifier);
421
Gilles Debunned88876a2012-03-16 17:34:04 -0700422 if (mError != null) {
423 hideError();
424 }
425
Mikael Gullstrand5b734f22013-07-09 14:41:28 +0200426 suspendBlink();
Gilles Debunned88876a2012-03-16 17:34:04 -0700427
428 if (mInsertionPointCursorController != null) {
429 mInsertionPointCursorController.onDetached();
430 }
431
432 if (mSelectionModifierCursorController != null) {
433 mSelectionModifierCursorController.onDetached();
434 }
435
436 if (mShowSuggestionRunnable != null) {
437 mTextView.removeCallbacks(mShowSuggestionRunnable);
438 }
439
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100440 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100441 if (mInsertionActionModeRunnable != null) {
442 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100443 }
444
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100445 mTextView.removeCallbacks(mShowFloatingToolbar);
446
Chris Craik003cc3d2015-10-16 10:24:55 -0700447 discardTextDisplayLists();
Gilles Debunned88876a2012-03-16 17:34:04 -0700448
449 if (mSpellChecker != null) {
450 mSpellChecker.closeSession();
451 // Forces the creation of a new SpellChecker next time this window is created.
452 // Will handle the cases where the settings has been changed in the meantime.
453 mSpellChecker = null;
454 }
455
Mady Mellora2861452015-06-25 08:40:27 -0700456 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -0800457 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -0700458 }
459
Chris Craik003cc3d2015-10-16 10:24:55 -0700460 private void discardTextDisplayLists() {
Chris Craik956f3402015-04-27 16:41:00 -0700461 if (mTextRenderNodes != null) {
462 for (int i = 0; i < mTextRenderNodes.length; i++) {
463 RenderNode displayList = mTextRenderNodes[i] != null
464 ? mTextRenderNodes[i].renderNode : null;
John Reck7558aa72014-03-05 14:59:59 -0800465 if (displayList != null && displayList.isValid()) {
Chris Craik003cc3d2015-10-16 10:24:55 -0700466 displayList.discardDisplayList();
John Reck7558aa72014-03-05 14:59:59 -0800467 }
468 }
469 }
470 }
471
Gilles Debunned88876a2012-03-16 17:34:04 -0700472 private void showError() {
473 if (mTextView.getWindowToken() == null) {
474 mShowErrorAfterAttach = true;
475 return;
476 }
477
478 if (mErrorPopup == null) {
479 LayoutInflater inflater = LayoutInflater.from(mTextView.getContext());
480 final TextView err = (TextView) inflater.inflate(
481 com.android.internal.R.layout.textview_hint, null);
482
483 final float scale = mTextView.getResources().getDisplayMetrics().density;
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700484 mErrorPopup =
485 new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f));
Gilles Debunned88876a2012-03-16 17:34:04 -0700486 mErrorPopup.setFocusable(false);
487 // The user is entering text, so the input method is needed. We
488 // don't want the popup to be displayed on top of it.
489 mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
490 }
491
492 TextView tv = (TextView) mErrorPopup.getContentView();
493 chooseSize(mErrorPopup, mError, tv);
494 tv.setText(mError);
495
496 mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY());
497 mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor());
498 }
499
500 public void setError(CharSequence error, Drawable icon) {
501 mError = TextUtils.stringOrSpannedString(error);
502 mErrorWasChanged = true;
Romain Guyd1cc1872012-11-05 17:43:25 -0800503
Gilles Debunned88876a2012-03-16 17:34:04 -0700504 if (mError == null) {
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800505 setErrorIcon(null);
Gilles Debunned88876a2012-03-16 17:34:04 -0700506 if (mErrorPopup != null) {
507 if (mErrorPopup.isShowing()) {
508 mErrorPopup.dismiss();
509 }
510
511 mErrorPopup = null;
512 }
Daniel 2 Olofssonf4ecc552013-08-13 10:30:26 +0200513 mShowErrorAfterAttach = false;
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800514 } else {
Romain Guyd1cc1872012-11-05 17:43:25 -0800515 setErrorIcon(icon);
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800516 if (mTextView.isFocused()) {
517 showError();
518 }
Romain Guyd1cc1872012-11-05 17:43:25 -0800519 }
520 }
521
522 private void setErrorIcon(Drawable icon) {
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800523 Drawables dr = mTextView.mDrawables;
524 if (dr == null) {
Fabrice Di Megliof7a5cdf2013-03-15 15:36:51 -0700525 mTextView.mDrawables = dr = new Drawables(mTextView.getContext());
Gilles Debunned88876a2012-03-16 17:34:04 -0700526 }
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800527 dr.setErrorDrawable(icon, mTextView);
528
529 mTextView.resetResolvedDrawables();
530 mTextView.invalidate();
531 mTextView.requestLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -0700532 }
533
534 private void hideError() {
535 if (mErrorPopup != null) {
536 if (mErrorPopup.isShowing()) {
537 mErrorPopup.dismiss();
538 }
539 }
540
541 mShowErrorAfterAttach = false;
542 }
543
544 /**
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800545 * Returns the X offset to make the pointy top of the error point
Gilles Debunned88876a2012-03-16 17:34:04 -0700546 * at the middle of the error icon.
547 */
548 private int getErrorX() {
549 /*
550 * The "25" is the distance between the point and the right edge
551 * of the background
552 */
553 final float scale = mTextView.getResources().getDisplayMetrics().density;
554
555 final Drawables dr = mTextView.mDrawables;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800556
557 final int layoutDirection = mTextView.getLayoutDirection();
558 int errorX;
559 int offset;
560 switch (layoutDirection) {
561 default:
562 case View.LAYOUT_DIRECTION_LTR:
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700563 offset = -(dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
564 errorX = mTextView.getWidth() - mErrorPopup.getWidth()
565 - mTextView.getPaddingRight() + offset;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800566 break;
567 case View.LAYOUT_DIRECTION_RTL:
568 offset = (dr != null ? dr.mDrawableSizeLeft : 0) / 2 - (int) (25 * scale + 0.5f);
569 errorX = mTextView.getPaddingLeft() + offset;
570 break;
571 }
572 return errorX;
Gilles Debunned88876a2012-03-16 17:34:04 -0700573 }
574
575 /**
576 * Returns the Y offset to make the pointy top of the error point
577 * at the bottom of the error icon.
578 */
579 private int getErrorY() {
580 /*
581 * Compound, not extended, because the icon is not clipped
582 * if the text height is smaller.
583 */
584 final int compoundPaddingTop = mTextView.getCompoundPaddingTop();
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700585 int vspace = mTextView.getBottom() - mTextView.getTop()
586 - mTextView.getCompoundPaddingBottom() - compoundPaddingTop;
Gilles Debunned88876a2012-03-16 17:34:04 -0700587
588 final Drawables dr = mTextView.mDrawables;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800589
590 final int layoutDirection = mTextView.getLayoutDirection();
591 int height;
592 switch (layoutDirection) {
593 default:
594 case View.LAYOUT_DIRECTION_LTR:
595 height = (dr != null ? dr.mDrawableHeightRight : 0);
596 break;
597 case View.LAYOUT_DIRECTION_RTL:
598 height = (dr != null ? dr.mDrawableHeightLeft : 0);
599 break;
600 }
601
602 int icontop = compoundPaddingTop + (vspace - height) / 2;
Gilles Debunned88876a2012-03-16 17:34:04 -0700603
604 /*
605 * The "2" is the distance between the point and the top edge
606 * of the background.
607 */
608 final float scale = mTextView.getResources().getDisplayMetrics().density;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800609 return icontop + height - mTextView.getHeight() - (int) (2 * scale + 0.5f);
Gilles Debunned88876a2012-03-16 17:34:04 -0700610 }
611
612 void createInputContentTypeIfNeeded() {
613 if (mInputContentType == null) {
614 mInputContentType = new InputContentType();
615 }
616 }
617
618 void createInputMethodStateIfNeeded() {
619 if (mInputMethodState == null) {
620 mInputMethodState = new InputMethodState();
621 }
622 }
623
624 boolean isCursorVisible() {
625 // The default value is true, even when there is no associated Editor
626 return mCursorVisible && mTextView.isTextEditable();
627 }
628
629 void prepareCursorControllers() {
630 boolean windowSupportsHandles = false;
631
632 ViewGroup.LayoutParams params = mTextView.getRootView().getLayoutParams();
633 if (params instanceof WindowManager.LayoutParams) {
634 WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
635 windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
636 || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
637 }
638
639 boolean enabled = windowSupportsHandles && mTextView.getLayout() != null;
640 mInsertionControllerEnabled = enabled && isCursorVisible();
641 mSelectionControllerEnabled = enabled && mTextView.textCanBeSelected();
642
643 if (!mInsertionControllerEnabled) {
644 hideInsertionPointCursorController();
645 if (mInsertionPointCursorController != null) {
646 mInsertionPointCursorController.onDetached();
647 mInsertionPointCursorController = null;
648 }
649 }
650
651 if (!mSelectionControllerEnabled) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100652 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -0700653 if (mSelectionModifierCursorController != null) {
654 mSelectionModifierCursorController.onDetached();
655 mSelectionModifierCursorController = null;
656 }
657 }
658 }
659
Seigo Nonakabb6a62c2015-03-31 21:59:30 +0900660 void hideInsertionPointCursorController() {
Gilles Debunned88876a2012-03-16 17:34:04 -0700661 if (mInsertionPointCursorController != null) {
662 mInsertionPointCursorController.hide();
663 }
664 }
665
666 /**
Mady Mellora2861452015-06-25 08:40:27 -0700667 * Hides the insertion and span controllers.
Gilles Debunned88876a2012-03-16 17:34:04 -0700668 */
Mady Mellora2861452015-06-25 08:40:27 -0700669 void hideCursorAndSpanControllers() {
Gilles Debunned88876a2012-03-16 17:34:04 -0700670 hideCursorControllers();
671 hideSpanControllers();
672 }
673
674 private void hideSpanControllers() {
Jean Chalardbaf30942013-02-28 16:01:51 -0800675 if (mSpanController != null) {
676 mSpanController.hide();
Gilles Debunned88876a2012-03-16 17:34:04 -0700677 }
678 }
679
680 private void hideCursorControllers() {
Yohei Yukawa85d08f12015-04-29 20:12:37 -0700681 // When mTextView is not ExtractEditText, we need to distinguish two kinds of focus-lost.
682 // One is the true focus lost where suggestions pop-up (if any) should be dismissed, and the
683 // other is an side effect of showing the suggestions pop-up itself. We use isShowingUp()
684 // to distinguish one from the other.
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700685 if (mSuggestionsPopupWindow != null && ((mTextView.isInExtractedMode())
686 || !mSuggestionsPopupWindow.isShowingUp())) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700687 // Should be done before hide insertion point controller since it triggers a show of it
688 mSuggestionsPopupWindow.hide();
689 }
690 hideInsertionPointCursorController();
Gilles Debunned88876a2012-03-16 17:34:04 -0700691 }
692
693 /**
694 * Create new SpellCheckSpans on the modified region.
695 */
696 private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
Satoshi Kataokad7429c12013-06-05 16:30:23 +0900697 // Remove spans whose adjacent characters are text not punctuation
698 mTextView.removeAdjacentSuggestionSpans(start);
699 mTextView.removeAdjacentSuggestionSpans(end);
700
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700701 if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled()
702 && !(mTextView.isInExtractedMode())) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700703 if (mSpellChecker == null && createSpellChecker) {
704 mSpellChecker = new SpellChecker(mTextView);
705 }
706 if (mSpellChecker != null) {
707 mSpellChecker.spellCheck(start, end);
708 }
709 }
710 }
711
712 void onScreenStateChanged(int screenState) {
713 switch (screenState) {
714 case View.SCREEN_STATE_ON:
715 resumeBlink();
716 break;
717 case View.SCREEN_STATE_OFF:
718 suspendBlink();
719 break;
720 }
721 }
722
723 private void suspendBlink() {
724 if (mBlink != null) {
725 mBlink.cancel();
726 }
727 }
728
729 private void resumeBlink() {
730 if (mBlink != null) {
731 mBlink.uncancel();
732 makeBlink();
733 }
734 }
735
736 void adjustInputType(boolean password, boolean passwordInputType,
737 boolean webPasswordInputType, boolean numberPasswordInputType) {
738 // mInputType has been set from inputType, possibly modified by mInputMethod.
739 // Specialize mInputType to [web]password if we have a text class and the original input
740 // type was a password.
741 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
742 if (password || passwordInputType) {
743 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
744 | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
745 }
746 if (webPasswordInputType) {
747 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
748 | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
749 }
750 } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
751 if (numberPasswordInputType) {
752 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
753 | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
754 }
755 }
756 }
757
758 private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
759 int wid = tv.getPaddingLeft() + tv.getPaddingRight();
760 int ht = tv.getPaddingTop() + tv.getPaddingBottom();
761
762 int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize(
763 com.android.internal.R.dimen.textview_error_popup_default_width);
764 Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels,
765 Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
766 float max = 0;
767 for (int i = 0; i < l.getLineCount(); i++) {
768 max = Math.max(max, l.getLineWidth(i));
769 }
770
771 /*
772 * Now set the popup size to be big enough for the text plus the border capped
773 * to DEFAULT_MAX_POPUP_WIDTH
774 */
775 pop.setWidth(wid + (int) Math.ceil(max));
776 pop.setHeight(ht + l.getHeight());
777 }
778
779 void setFrame() {
780 if (mErrorPopup != null) {
781 TextView tv = (TextView) mErrorPopup.getContentView();
782 chooseSize(mErrorPopup, mError, tv);
783 mErrorPopup.update(mTextView, getErrorX(), getErrorY(),
784 mErrorPopup.getWidth(), mErrorPopup.getHeight());
785 }
786 }
787
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800788 private int getWordStart(int offset) {
789 // FIXME - For this and similar methods we're not doing anything to check if there's
790 // a LocaleSpan in the text, this may be something we should try handling or checking for.
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700791 int retOffset = getWordIteratorWithText().prevBoundary(offset);
Mady Mellor58c90872015-05-12 11:09:37 -0700792 if (getWordIteratorWithText().isOnPunctuation(retOffset)) {
793 // On punctuation boundary or within group of punctuation, find punctuation start.
794 retOffset = getWordIteratorWithText().getPunctuationBeginning(offset);
795 } else {
796 // Not on a punctuation boundary, find the word start.
Mady Mellore264ac32015-06-22 16:46:29 -0700797 retOffset = getWordIteratorWithText().getPrevWordBeginningOnTwoWordsBoundary(offset);
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800798 }
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700799 if (retOffset == BreakIterator.DONE) {
800 return offset;
801 }
802 return retOffset;
803 }
804
805 private int getWordEnd(int offset) {
806 int retOffset = getWordIteratorWithText().nextBoundary(offset);
Mady Mellor58c90872015-05-12 11:09:37 -0700807 if (getWordIteratorWithText().isAfterPunctuation(retOffset)) {
808 // On punctuation boundary or within group of punctuation, find punctuation end.
809 retOffset = getWordIteratorWithText().getPunctuationEnd(offset);
810 } else {
811 // Not on a punctuation boundary, find the word end.
Mady Mellore264ac32015-06-22 16:46:29 -0700812 retOffset = getWordIteratorWithText().getNextWordEndOnTwoWordBoundary(offset);
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700813 }
814 if (retOffset == BreakIterator.DONE) {
815 return offset;
816 }
817 return retOffset;
818 }
819
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900820 private boolean needsToSelectAllToSelectWordOrParagraph() {
Andrei Stingaceanu47f82ae2015-04-28 17:43:54 +0100821 if (mTextView.hasPasswordTransformationMethod()) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700822 // Always select all on a password field.
823 // Cut/copy menu entries are not available for passwords, but being able to select all
824 // is however useful to delete or paste to replace the entire content.
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900825 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -0700826 }
827
828 int inputType = mTextView.getInputType();
829 int klass = inputType & InputType.TYPE_MASK_CLASS;
830 int variation = inputType & InputType.TYPE_MASK_VARIATION;
831
832 // Specific text field types: select the entire text for these
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700833 if (klass == InputType.TYPE_CLASS_NUMBER
834 || klass == InputType.TYPE_CLASS_PHONE
835 || klass == InputType.TYPE_CLASS_DATETIME
836 || variation == InputType.TYPE_TEXT_VARIATION_URI
837 || variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
838 || variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS
839 || variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900840 return true;
841 }
842 return false;
843 }
844
845 /**
846 * Adjusts selection to the word under last touch offset. Return true if the operation was
847 * successfully performed.
848 */
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100849 boolean selectCurrentWord() {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900850 if (!mTextView.canSelectText()) {
851 return false;
852 }
853
854 if (needsToSelectAllToSelectWordOrParagraph()) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700855 return mTextView.selectAllText();
856 }
857
858 long lastTouchOffsets = getLastTouchOffsets();
859 final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
860 final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
861
862 // Safety check in case standard touch event handling has been bypassed
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -0800863 if (minOffset < 0 || minOffset > mTextView.getText().length()) return false;
864 if (maxOffset < 0 || maxOffset > mTextView.getText().length()) return false;
Gilles Debunned88876a2012-03-16 17:34:04 -0700865
866 int selectionStart, selectionEnd;
867
868 // If a URLSpan (web address, email, phone...) is found at that position, select it.
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700869 URLSpan[] urlSpans =
870 ((Spanned) mTextView.getText()).getSpans(minOffset, maxOffset, URLSpan.class);
Gilles Debunned88876a2012-03-16 17:34:04 -0700871 if (urlSpans.length >= 1) {
872 URLSpan urlSpan = urlSpans[0];
873 selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan);
874 selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan);
875 } else {
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800876 // FIXME - We should check if there's a LocaleSpan in the text, this may be
877 // something we should try handling or checking for.
Gilles Debunned88876a2012-03-16 17:34:04 -0700878 final WordIterator wordIterator = getWordIterator();
879 wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset);
880
881 selectionStart = wordIterator.getBeginning(minOffset);
882 selectionEnd = wordIterator.getEnd(maxOffset);
883
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700884 if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE
885 || selectionStart == selectionEnd) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700886 // Possible when the word iterator does not properly handle the text's language
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +0900887 long range = getCharClusterRange(minOffset);
Gilles Debunned88876a2012-03-16 17:34:04 -0700888 selectionStart = TextUtils.unpackRangeStartFromLong(range);
889 selectionEnd = TextUtils.unpackRangeEndFromLong(range);
890 }
891 }
892
893 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
894 return selectionEnd > selectionStart;
895 }
896
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900897 /**
898 * Adjusts selection to the paragraph under last touch offset. Return true if the operation was
899 * successfully performed.
900 */
901 private boolean selectCurrentParagraph() {
902 if (!mTextView.canSelectText()) {
903 return false;
904 }
905
906 if (needsToSelectAllToSelectWordOrParagraph()) {
907 return mTextView.selectAllText();
908 }
909
910 long lastTouchOffsets = getLastTouchOffsets();
911 final int minLastTouchOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
912 final int maxLastTouchOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
913
914 final long paragraphsRange = getParagraphsRange(minLastTouchOffset, maxLastTouchOffset);
915 final int start = TextUtils.unpackRangeStartFromLong(paragraphsRange);
916 final int end = TextUtils.unpackRangeEndFromLong(paragraphsRange);
917 if (start < end) {
918 Selection.setSelection((Spannable) mTextView.getText(), start, end);
919 return true;
920 }
921 return false;
922 }
923
924 /**
925 * Get the minimum range of paragraphs that contains startOffset and endOffset.
926 */
927 private long getParagraphsRange(int startOffset, int endOffset) {
928 final Layout layout = mTextView.getLayout();
929 if (layout == null) {
930 return TextUtils.packRangeInLong(-1, -1);
931 }
932 final CharSequence text = mTextView.getText();
933 int minLine = layout.getLineForOffset(startOffset);
934 // Search paragraph start.
935 while (minLine > 0) {
936 final int prevLineEndOffset = layout.getLineEnd(minLine - 1);
937 if (text.charAt(prevLineEndOffset - 1) == '\n') {
938 break;
939 }
940 minLine--;
941 }
942 int maxLine = layout.getLineForOffset(endOffset);
943 // Search paragraph end.
944 while (maxLine < layout.getLineCount() - 1) {
945 final int lineEndOffset = layout.getLineEnd(maxLine);
946 if (text.charAt(lineEndOffset - 1) == '\n') {
947 break;
948 }
949 maxLine++;
950 }
951 return TextUtils.packRangeInLong(layout.getLineStart(minLine), layout.getLineEnd(maxLine));
952 }
953
Gilles Debunned88876a2012-03-16 17:34:04 -0700954 void onLocaleChanged() {
Keisuke Kuroyanagie0ac5ac2016-03-09 15:33:30 +0900955 // Will be re-created on demand in getWordIterator and getWordIteratorWithText with the
956 // proper new locale
Gilles Debunned88876a2012-03-16 17:34:04 -0700957 mWordIterator = null;
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800958 mWordIteratorWithText = null;
Gilles Debunned88876a2012-03-16 17:34:04 -0700959 }
960
Gilles Debunned88876a2012-03-16 17:34:04 -0700961 public WordIterator getWordIterator() {
962 if (mWordIterator == null) {
963 mWordIterator = new WordIterator(mTextView.getTextServicesLocale());
964 }
965 return mWordIterator;
966 }
967
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800968 private WordIterator getWordIteratorWithText() {
969 if (mWordIteratorWithText == null) {
970 mWordIteratorWithText = new WordIterator(mTextView.getTextServicesLocale());
971 mUpdateWordIteratorText = true;
972 }
973 if (mUpdateWordIteratorText) {
974 // FIXME - Shouldn't copy all of the text as only the area of the text relevant
975 // to the user's selection is needed. A possible solution would be to
976 // copy some number N of characters near the selection and then when the
977 // user approaches N then we'd do another copy of the next N characters.
978 CharSequence text = mTextView.getText();
979 mWordIteratorWithText.setCharSequence(text, 0, text.length());
980 mUpdateWordIteratorText = false;
981 }
982 return mWordIteratorWithText;
983 }
984
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +0900985 private int getNextCursorOffset(int offset, boolean findAfterGivenOffset) {
986 final Layout layout = mTextView.getLayout();
987 if (layout == null) return offset;
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700988 return findAfterGivenOffset == layout.isRtlCharAt(offset)
989 ? layout.getOffsetToLeftOf(offset) : layout.getOffsetToRightOf(offset);
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +0900990 }
991
992 private long getCharClusterRange(int offset) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700993 final int textLength = mTextView.getText().length();
Gilles Debunned88876a2012-03-16 17:34:04 -0700994 if (offset < textLength) {
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -0800995 final int clusterEndOffset = getNextCursorOffset(offset, true);
996 return TextUtils.packRangeInLong(
997 getNextCursorOffset(clusterEndOffset, false), clusterEndOffset);
Gilles Debunned88876a2012-03-16 17:34:04 -0700998 }
999 if (offset - 1 >= 0) {
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08001000 final int clusterStartOffset = getNextCursorOffset(offset, false);
1001 return TextUtils.packRangeInLong(clusterStartOffset,
1002 getNextCursorOffset(clusterStartOffset, true));
Gilles Debunned88876a2012-03-16 17:34:04 -07001003 }
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +09001004 return TextUtils.packRangeInLong(offset, offset);
Gilles Debunned88876a2012-03-16 17:34:04 -07001005 }
1006
1007 private boolean touchPositionIsInSelection() {
1008 int selectionStart = mTextView.getSelectionStart();
1009 int selectionEnd = mTextView.getSelectionEnd();
1010
1011 if (selectionStart == selectionEnd) {
1012 return false;
1013 }
1014
1015 if (selectionStart > selectionEnd) {
1016 int tmp = selectionStart;
1017 selectionStart = selectionEnd;
1018 selectionEnd = tmp;
1019 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
1020 }
1021
1022 SelectionModifierCursorController selectionController = getSelectionController();
1023 int minOffset = selectionController.getMinTouchOffset();
1024 int maxOffset = selectionController.getMaxTouchOffset();
1025
1026 return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
1027 }
1028
1029 private PositionListener getPositionListener() {
1030 if (mPositionListener == null) {
1031 mPositionListener = new PositionListener();
1032 }
1033 return mPositionListener;
1034 }
1035
1036 private interface TextViewPositionListener {
1037 public void updatePosition(int parentPositionX, int parentPositionY,
1038 boolean parentPositionChanged, boolean parentScrolled);
1039 }
1040
Gilles Debunned88876a2012-03-16 17:34:04 -07001041 private boolean isOffsetVisible(int offset) {
1042 Layout layout = mTextView.getLayout();
Victoria Leaseb9b77ae2013-10-13 15:12:52 -07001043 if (layout == null) return false;
1044
Gilles Debunned88876a2012-03-16 17:34:04 -07001045 final int line = layout.getLineForOffset(offset);
1046 final int lineBottom = layout.getLineBottom(line);
1047 final int primaryHorizontal = (int) layout.getPrimaryHorizontal(offset);
Phil Weaverc2e28932016-12-08 12:29:25 -08001048 return mTextView.isPositionVisible(
1049 primaryHorizontal + mTextView.viewportToContentHorizontalOffset(),
Gilles Debunned88876a2012-03-16 17:34:04 -07001050 lineBottom + mTextView.viewportToContentVerticalOffset());
1051 }
1052
1053 /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
1054 * in the view. Returns false when the position is in the empty space of left/right of text.
1055 */
1056 private boolean isPositionOnText(float x, float y) {
1057 Layout layout = mTextView.getLayout();
1058 if (layout == null) return false;
1059
1060 final int line = mTextView.getLineAtCoordinate(y);
1061 x = mTextView.convertToLocalHorizontalCoordinate(x);
1062
1063 if (x < layout.getLineLeft(line)) return false;
1064 if (x > layout.getLineRight(line)) return false;
1065 return true;
1066 }
1067
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001068 private void startDragAndDrop() {
Keisuke Kuroyanagifdfc93d2016-03-15 14:47:08 +09001069 // TODO: Fix drag and drop in full screen extracted mode.
1070 if (mTextView.isInExtractedMode()) {
1071 return;
1072 }
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001073 final int start = mTextView.getSelectionStart();
1074 final int end = mTextView.getSelectionEnd();
1075 CharSequence selectedText = mTextView.getTransformedText(start, end);
1076 ClipData data = ClipData.newPlainText(null, selectedText);
1077 DragLocalState localState = new DragLocalState(mTextView, start, end);
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08001078 mTextView.startDragAndDrop(data, getTextThumbnailBuilder(start, end), localState,
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001079 View.DRAG_FLAG_GLOBAL);
1080 stopTextActionMode();
1081 if (hasSelectionController()) {
1082 getSelectionController().resetTouchOffsets();
1083 }
1084 }
1085
Gilles Debunned88876a2012-03-16 17:34:04 -07001086 public boolean performLongClick(boolean handled) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001087 // Long press in empty space moves cursor and starts the insertion action mode.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001088 if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY)
1089 && mInsertionControllerEnabled) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001090 final int offset = mTextView.getOffsetForPosition(mLastDownPositionX,
1091 mLastDownPositionY);
Gilles Debunned88876a2012-03-16 17:34:04 -07001092 Selection.setSelection((Spannable) mTextView.getText(), offset);
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00001093 getInsertionController().show();
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001094 mIsInsertionActionModeStartPending = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001095 handled = true;
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001096 MetricsLogger.action(
1097 mTextView.getContext(),
1098 MetricsEvent.TEXT_LONGPRESS,
1099 TextViewMetrics.SUBTYPE_LONG_PRESS_OTHER);
Gilles Debunned88876a2012-03-16 17:34:04 -07001100 }
1101
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001102 if (!handled && mTextActionMode != null) {
Andrei Stingaceanu2aaeefe2015-10-20 19:11:23 +01001103 if (touchPositionIsInSelection()) {
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001104 startDragAndDrop();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001105 MetricsLogger.action(
1106 mTextView.getContext(),
1107 MetricsEvent.TEXT_LONGPRESS,
1108 TextViewMetrics.SUBTYPE_LONG_PRESS_DRAG_AND_DROP);
Gilles Debunned88876a2012-03-16 17:34:04 -07001109 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001110 stopTextActionMode();
Clara Bayarridfac4432015-05-15 12:18:24 +01001111 selectCurrentWordAndStartDrag();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001112 MetricsLogger.action(
1113 mTextView.getContext(),
1114 MetricsEvent.TEXT_LONGPRESS,
1115 TextViewMetrics.SUBTYPE_LONG_PRESS_SELECTION);
Gilles Debunned88876a2012-03-16 17:34:04 -07001116 }
1117 handled = true;
1118 }
1119
1120 // Start a new selection
1121 if (!handled) {
Clara Bayarridfac4432015-05-15 12:18:24 +01001122 handled = selectCurrentWordAndStartDrag();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001123 if (handled) {
1124 MetricsLogger.action(
1125 mTextView.getContext(),
1126 MetricsEvent.TEXT_LONGPRESS,
1127 TextViewMetrics.SUBTYPE_LONG_PRESS_SELECTION);
1128 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001129 }
1130
1131 return handled;
1132 }
1133
Petar Å egina91df3f92017-08-15 16:20:43 +01001134 float getLastUpPositionX() {
1135 return mLastUpPositionX;
1136 }
1137
1138 float getLastUpPositionY() {
1139 return mLastUpPositionY;
1140 }
1141
Gilles Debunned88876a2012-03-16 17:34:04 -07001142 private long getLastTouchOffsets() {
1143 SelectionModifierCursorController selectionController = getSelectionController();
1144 final int minOffset = selectionController.getMinTouchOffset();
1145 final int maxOffset = selectionController.getMaxTouchOffset();
1146 return TextUtils.packRangeInLong(minOffset, maxOffset);
1147 }
1148
1149 void onFocusChanged(boolean focused, int direction) {
1150 mShowCursor = SystemClock.uptimeMillis();
1151 ensureEndedBatchEdit();
1152
1153 if (focused) {
1154 int selStart = mTextView.getSelectionStart();
1155 int selEnd = mTextView.getSelectionEnd();
1156
1157 // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
1158 // mode for these, unless there was a specific selection already started.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001159 final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0
1160 && selEnd == mTextView.getText().length();
Gilles Debunned88876a2012-03-16 17:34:04 -07001161
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001162 mCreatedWithASelection = mFrozenWithFocus && mTextView.hasSelection()
1163 && !isFocusHighlighted;
Gilles Debunned88876a2012-03-16 17:34:04 -07001164
1165 if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
1166 // If a tap was used to give focus to that view, move cursor at tap position.
1167 // Has to be done before onTakeFocus, which can be overloaded.
1168 final int lastTapPosition = getLastTapPosition();
1169 if (lastTapPosition >= 0) {
1170 Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition);
1171 }
1172
1173 // Note this may have to be moved out of the Editor class
1174 MovementMethod mMovement = mTextView.getMovementMethod();
1175 if (mMovement != null) {
1176 mMovement.onTakeFocus(mTextView, (Spannable) mTextView.getText(), direction);
1177 }
1178
1179 // The DecorView does not have focus when the 'Done' ExtractEditText button is
1180 // pressed. Since it is the ViewAncestor's mView, it requests focus before
1181 // ExtractEditText clears focus, which gives focus to the ExtractEditText.
1182 // This special case ensure that we keep current selection in that case.
1183 // It would be better to know why the DecorView does not have focus at that time.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001184 if (((mTextView.isInExtractedMode()) || mSelectionMoved)
1185 && selStart >= 0 && selEnd >= 0) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001186 /*
1187 * Someone intentionally set the selection, so let them
1188 * do whatever it is that they wanted to do instead of
1189 * the default on-focus behavior. We reset the selection
1190 * here instead of just skipping the onTakeFocus() call
1191 * because some movement methods do something other than
1192 * just setting the selection in theirs and we still
1193 * need to go through that path.
1194 */
1195 Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
1196 }
1197
1198 if (mSelectAllOnFocus) {
1199 mTextView.selectAllText();
1200 }
1201
1202 mTouchFocusSelected = true;
1203 }
1204
1205 mFrozenWithFocus = false;
1206 mSelectionMoved = false;
1207
1208 if (mError != null) {
1209 showError();
1210 }
1211
1212 makeBlink();
1213 } else {
1214 if (mError != null) {
1215 hideError();
1216 }
1217 // Don't leave us in the middle of a batch edit.
1218 mTextView.onEndBatchEdit();
1219
Andrei Stingaceanub1891b32015-06-19 16:44:37 +01001220 if (mTextView.isInExtractedMode()) {
Mady Mellora2861452015-06-25 08:40:27 -07001221 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001222 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -07001223 } else {
Mady Mellora2861452015-06-25 08:40:27 -07001224 hideCursorAndSpanControllers();
Yohei Yukawa24df9312016-03-31 17:15:23 -07001225 if (mTextView.isTemporarilyDetached()) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001226 stopTextActionModeWithPreservingSelection();
1227 } else {
1228 stopTextActionMode();
1229 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001230 downgradeEasyCorrectionSpans();
1231 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001232 // No need to create the controller
1233 if (mSelectionModifierCursorController != null) {
1234 mSelectionModifierCursorController.resetTouchOffsets();
1235 }
1236 }
1237 }
1238
1239 /**
1240 * Downgrades to simple suggestions all the easy correction spans that are not a spell check
1241 * span.
1242 */
1243 private void downgradeEasyCorrectionSpans() {
1244 CharSequence text = mTextView.getText();
1245 if (text instanceof Spannable) {
1246 Spannable spannable = (Spannable) text;
1247 SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
1248 spannable.length(), SuggestionSpan.class);
1249 for (int i = 0; i < suggestionSpans.length; i++) {
1250 int flags = suggestionSpans[i].getFlags();
1251 if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
1252 && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
1253 flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
1254 suggestionSpans[i].setFlags(flags);
1255 }
1256 }
1257 }
1258 }
1259
1260 void sendOnTextChanged(int start, int after) {
1261 updateSpellCheckSpans(start, start + after, false);
1262
Mady Mellor2ff2cd82015-03-02 10:37:01 -08001263 // Flip flag to indicate the word iterator needs to have the text reset.
1264 mUpdateWordIteratorText = true;
1265
Gilles Debunned88876a2012-03-16 17:34:04 -07001266 // Hide the controllers as soon as text is modified (typing, procedural...)
1267 // We do not hide the span controllers, since they can be added when a new text is
1268 // inserted into the text view (voice IME).
1269 hideCursorControllers();
Keisuke Kuroyanagif4e347d2015-06-11 17:41:00 +09001270 // Reset drag accelerator.
1271 if (mSelectionModifierCursorController != null) {
1272 mSelectionModifierCursorController.resetTouchOffsets();
1273 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001274 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07001275 }
1276
1277 private int getLastTapPosition() {
1278 // No need to create the controller at that point, no last tap position saved
1279 if (mSelectionModifierCursorController != null) {
1280 int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
1281 if (lastTapPosition >= 0) {
1282 // Safety check, should not be possible.
1283 if (lastTapPosition > mTextView.getText().length()) {
1284 lastTapPosition = mTextView.getText().length();
1285 }
1286 return lastTapPosition;
1287 }
1288 }
1289
1290 return -1;
1291 }
1292
1293 void onWindowFocusChanged(boolean hasWindowFocus) {
1294 if (hasWindowFocus) {
1295 if (mBlink != null) {
1296 mBlink.uncancel();
1297 makeBlink();
1298 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001299 if (mTextView.hasSelection() && !extractedTextModeWillBeStarted()) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001300 refreshTextActionMode();
Mady Mellora2861452015-06-25 08:40:27 -07001301 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001302 } else {
1303 if (mBlink != null) {
1304 mBlink.cancel();
1305 }
1306 if (mInputContentType != null) {
1307 mInputContentType.enterDown = false;
1308 }
1309 // Order matters! Must be done before onParentLostFocus to rely on isShowingUp
Mady Mellora2861452015-06-25 08:40:27 -07001310 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001311 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -07001312 if (mSuggestionsPopupWindow != null) {
1313 mSuggestionsPopupWindow.onParentLostFocus();
1314 }
1315
Gilles Debunnec72fba82012-06-26 14:47:07 -07001316 // Don't leave us in the middle of a batch edit. Same as in onFocusChanged
1317 ensureEndedBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001318 }
1319 }
1320
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09001321 private void updateTapState(MotionEvent event) {
1322 final int action = event.getActionMasked();
1323 if (action == MotionEvent.ACTION_DOWN) {
1324 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
1325 // Detect double tap and triple click.
1326 if (((mTapState == TAP_STATE_FIRST_TAP)
1327 || ((mTapState == TAP_STATE_DOUBLE_TAP) && isMouse))
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001328 && (SystemClock.uptimeMillis() - mLastTouchUpTime)
1329 <= ViewConfiguration.getDoubleTapTimeout()) {
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09001330 if (mTapState == TAP_STATE_FIRST_TAP) {
1331 mTapState = TAP_STATE_DOUBLE_TAP;
1332 } else {
1333 mTapState = TAP_STATE_TRIPLE_CLICK;
1334 }
1335 } else {
1336 mTapState = TAP_STATE_FIRST_TAP;
1337 }
1338 }
1339 if (action == MotionEvent.ACTION_UP) {
1340 mLastTouchUpTime = SystemClock.uptimeMillis();
1341 }
1342 }
1343
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09001344 private boolean shouldFilterOutTouchEvent(MotionEvent event) {
1345 if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) {
1346 return false;
1347 }
1348 final boolean primaryButtonStateChanged =
1349 ((mLastButtonState ^ event.getButtonState()) & MotionEvent.BUTTON_PRIMARY) != 0;
1350 final int action = event.getActionMasked();
1351 if ((action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_UP)
1352 && !primaryButtonStateChanged) {
1353 return true;
1354 }
1355 if (action == MotionEvent.ACTION_MOVE
1356 && !event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
1357 return true;
1358 }
1359 return false;
1360 }
1361
Gilles Debunned88876a2012-03-16 17:34:04 -07001362 void onTouchEvent(MotionEvent event) {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09001363 final boolean filterOutEvent = shouldFilterOutTouchEvent(event);
1364 mLastButtonState = event.getButtonState();
1365 if (filterOutEvent) {
1366 if (event.getActionMasked() == MotionEvent.ACTION_UP) {
1367 mDiscardNextActionUp = true;
1368 }
1369 return;
1370 }
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09001371 updateTapState(event);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001372 updateFloatingToolbarVisibility(event);
1373
Gilles Debunned88876a2012-03-16 17:34:04 -07001374 if (hasSelectionController()) {
1375 getSelectionController().onTouchEvent(event);
1376 }
1377
1378 if (mShowSuggestionRunnable != null) {
1379 mTextView.removeCallbacks(mShowSuggestionRunnable);
1380 mShowSuggestionRunnable = null;
1381 }
1382
Petar Å egina91df3f92017-08-15 16:20:43 +01001383 if (event.getActionMasked() == MotionEvent.ACTION_UP) {
1384 mLastUpPositionX = event.getX();
1385 mLastUpPositionY = event.getY();
1386 }
1387
Gilles Debunned88876a2012-03-16 17:34:04 -07001388 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
1389 mLastDownPositionX = event.getX();
1390 mLastDownPositionY = event.getY();
1391
1392 // Reset this state; it will be re-set if super.onTouchEvent
1393 // causes focus to move to the view.
1394 mTouchFocusSelected = false;
1395 mIgnoreActionUpEvent = false;
1396 }
1397 }
1398
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001399 private void updateFloatingToolbarVisibility(MotionEvent event) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001400 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001401 switch (event.getActionMasked()) {
1402 case MotionEvent.ACTION_MOVE:
Abodunrinwa Toki4a7aeb32017-07-13 02:06:56 +01001403 hideFloatingToolbar(ActionMode.DEFAULT_HIDE_DURATION);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001404 break;
1405 case MotionEvent.ACTION_UP: // fall through
1406 case MotionEvent.ACTION_CANCEL:
1407 showFloatingToolbar();
1408 }
1409 }
1410 }
1411
Abodunrinwa Toki4a7aeb32017-07-13 02:06:56 +01001412 void hideFloatingToolbar(int duration) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001413 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001414 mTextView.removeCallbacks(mShowFloatingToolbar);
Abodunrinwa Toki4a7aeb32017-07-13 02:06:56 +01001415 mTextActionMode.hide(duration);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001416 }
1417 }
1418
1419 private void showFloatingToolbar() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001420 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001421 // Delay "show" so it doesn't interfere with click confirmations
1422 // or double-clicks that could "dismiss" the floating toolbar.
1423 int delay = ViewConfiguration.getDoubleTapTimeout();
1424 mTextView.postDelayed(mShowFloatingToolbar, delay);
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01001425
1426 // This classifies the text and most likely returns before the toolbar is actually
1427 // shown. If not, it will update the toolbar with the result when classification
1428 // returns. We would rather not wait for a long running classification process.
1429 invalidateActionModeAsync();
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001430 }
1431 }
1432
Gilles Debunned88876a2012-03-16 17:34:04 -07001433 public void beginBatchEdit() {
1434 mInBatchEditControllers = true;
1435 final InputMethodState ims = mInputMethodState;
1436 if (ims != null) {
1437 int nesting = ++ims.mBatchEditNesting;
1438 if (nesting == 1) {
1439 ims.mCursorChanged = false;
1440 ims.mChangedDelta = 0;
1441 if (ims.mContentChanged) {
1442 // We already have a pending change from somewhere else,
1443 // so turn this into a full update.
1444 ims.mChangedStart = 0;
1445 ims.mChangedEnd = mTextView.getText().length();
1446 } else {
1447 ims.mChangedStart = EXTRACT_UNKNOWN;
1448 ims.mChangedEnd = EXTRACT_UNKNOWN;
1449 ims.mContentChanged = false;
1450 }
James Cook48e0fac2015-02-25 15:44:51 -08001451 mUndoInputFilter.beginBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001452 mTextView.onBeginBatchEdit();
1453 }
1454 }
1455 }
1456
1457 public void endBatchEdit() {
1458 mInBatchEditControllers = false;
1459 final InputMethodState ims = mInputMethodState;
1460 if (ims != null) {
1461 int nesting = --ims.mBatchEditNesting;
1462 if (nesting == 0) {
1463 finishBatchEdit(ims);
1464 }
1465 }
1466 }
1467
1468 void ensureEndedBatchEdit() {
1469 final InputMethodState ims = mInputMethodState;
1470 if (ims != null && ims.mBatchEditNesting != 0) {
1471 ims.mBatchEditNesting = 0;
1472 finishBatchEdit(ims);
1473 }
1474 }
1475
1476 void finishBatchEdit(final InputMethodState ims) {
1477 mTextView.onEndBatchEdit();
James Cook48e0fac2015-02-25 15:44:51 -08001478 mUndoInputFilter.endBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001479
1480 if (ims.mContentChanged || ims.mSelectionModeChanged) {
1481 mTextView.updateAfterEdit();
1482 reportExtractedText();
1483 } else if (ims.mCursorChanged) {
Jean Chalardc99d33f2013-02-28 16:39:47 -08001484 // Cheesy way to get us to report the current cursor location.
Gilles Debunned88876a2012-03-16 17:34:04 -07001485 mTextView.invalidateCursor();
1486 }
Jean Chalardc99d33f2013-02-28 16:39:47 -08001487 // sendUpdateSelection knows to avoid sending if the selection did
1488 // not actually change.
1489 sendUpdateSelection();
Keisuke Kuroyanagic6fad962016-05-02 15:11:41 +09001490
1491 // Show drag handles if they were blocked by batch edit mode.
1492 if (mTextActionMode != null) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001493 final CursorController cursorController = mTextView.hasSelection()
1494 ? getSelectionController() : getInsertionController();
Keisuke Kuroyanagic6fad962016-05-02 15:11:41 +09001495 if (cursorController != null && !cursorController.isActive()
1496 && !cursorController.isCursorBeingModified()) {
1497 cursorController.show();
1498 }
1499 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001500 }
1501
1502 static final int EXTRACT_NOTHING = -2;
1503 static final int EXTRACT_UNKNOWN = -1;
1504
1505 boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
1506 return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
1507 EXTRACT_UNKNOWN, outText);
1508 }
1509
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001510 private boolean extractTextInternal(@Nullable ExtractedTextRequest request,
Gilles Debunned88876a2012-03-16 17:34:04 -07001511 int partialStartOffset, int partialEndOffset, int delta,
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001512 @Nullable ExtractedText outText) {
1513 if (request == null || outText == null) {
1514 return false;
Gilles Debunned88876a2012-03-16 17:34:04 -07001515 }
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001516
1517 final CharSequence content = mTextView.getText();
1518 if (content == null) {
1519 return false;
1520 }
1521
1522 if (partialStartOffset != EXTRACT_NOTHING) {
1523 final int N = content.length();
1524 if (partialStartOffset < 0) {
1525 outText.partialStartOffset = outText.partialEndOffset = -1;
1526 partialStartOffset = 0;
1527 partialEndOffset = N;
1528 } else {
1529 // Now use the delta to determine the actual amount of text
1530 // we need.
1531 partialEndOffset += delta;
1532 // Adjust offsets to ensure we contain full spans.
1533 if (content instanceof Spanned) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001534 Spanned spanned = (Spanned) content;
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001535 Object[] spans = spanned.getSpans(partialStartOffset,
1536 partialEndOffset, ParcelableSpan.class);
1537 int i = spans.length;
1538 while (i > 0) {
1539 i--;
1540 int j = spanned.getSpanStart(spans[i]);
1541 if (j < partialStartOffset) partialStartOffset = j;
1542 j = spanned.getSpanEnd(spans[i]);
1543 if (j > partialEndOffset) partialEndOffset = j;
1544 }
1545 }
1546 outText.partialStartOffset = partialStartOffset;
1547 outText.partialEndOffset = partialEndOffset - delta;
1548
1549 if (partialStartOffset > N) {
1550 partialStartOffset = N;
1551 } else if (partialStartOffset < 0) {
1552 partialStartOffset = 0;
1553 }
1554 if (partialEndOffset > N) {
1555 partialEndOffset = N;
1556 } else if (partialEndOffset < 0) {
1557 partialEndOffset = 0;
1558 }
1559 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001560 if ((request.flags & InputConnection.GET_TEXT_WITH_STYLES) != 0) {
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001561 outText.text = content.subSequence(partialStartOffset,
1562 partialEndOffset);
1563 } else {
1564 outText.text = TextUtils.substring(content, partialStartOffset,
1565 partialEndOffset);
1566 }
1567 } else {
1568 outText.partialStartOffset = 0;
1569 outText.partialEndOffset = 0;
1570 outText.text = "";
1571 }
1572 outText.flags = 0;
1573 if (MetaKeyKeyListener.getMetaState(content, MetaKeyKeyListener.META_SELECTING) != 0) {
1574 outText.flags |= ExtractedText.FLAG_SELECTING;
1575 }
1576 if (mTextView.isSingleLine()) {
1577 outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
1578 }
1579 outText.startOffset = 0;
1580 outText.selectionStart = mTextView.getSelectionStart();
1581 outText.selectionEnd = mTextView.getSelectionEnd();
1582 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001583 }
1584
1585 boolean reportExtractedText() {
1586 final Editor.InputMethodState ims = mInputMethodState;
1587 if (ims != null) {
1588 final boolean contentChanged = ims.mContentChanged;
1589 if (contentChanged || ims.mSelectionModeChanged) {
1590 ims.mContentChanged = false;
1591 ims.mSelectionModeChanged = false;
Gilles Debunnec62589c2012-04-12 14:50:23 -07001592 final ExtractedTextRequest req = ims.mExtractedTextRequest;
Gilles Debunned88876a2012-03-16 17:34:04 -07001593 if (req != null) {
1594 InputMethodManager imm = InputMethodManager.peekInstance();
1595 if (imm != null) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001596 if (TextView.DEBUG_EXTRACT) {
1597 Log.v(TextView.LOG_TAG, "Retrieving extracted start="
1598 + ims.mChangedStart
1599 + " end=" + ims.mChangedEnd
1600 + " delta=" + ims.mChangedDelta);
1601 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001602 if (ims.mChangedStart < 0 && !contentChanged) {
1603 ims.mChangedStart = EXTRACT_NOTHING;
1604 }
1605 if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
Gilles Debunnec62589c2012-04-12 14:50:23 -07001606 ims.mChangedDelta, ims.mExtractedText)) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001607 if (TextView.DEBUG_EXTRACT) {
1608 Log.v(TextView.LOG_TAG,
1609 "Reporting extracted start="
1610 + ims.mExtractedText.partialStartOffset
1611 + " end=" + ims.mExtractedText.partialEndOffset
1612 + ": " + ims.mExtractedText.text);
1613 }
Gilles Debunnec62589c2012-04-12 14:50:23 -07001614
1615 imm.updateExtractedText(mTextView, req.token, ims.mExtractedText);
Gilles Debunned88876a2012-03-16 17:34:04 -07001616 ims.mChangedStart = EXTRACT_UNKNOWN;
1617 ims.mChangedEnd = EXTRACT_UNKNOWN;
1618 ims.mChangedDelta = 0;
1619 ims.mContentChanged = false;
1620 return true;
1621 }
1622 }
1623 }
1624 }
1625 }
1626 return false;
1627 }
1628
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001629 private void sendUpdateSelection() {
1630 if (null != mInputMethodState && mInputMethodState.mBatchEditNesting <= 0) {
1631 final InputMethodManager imm = InputMethodManager.peekInstance();
1632 if (null != imm) {
1633 final int selectionStart = mTextView.getSelectionStart();
1634 final int selectionEnd = mTextView.getSelectionEnd();
1635 int candStart = -1;
1636 int candEnd = -1;
1637 if (mTextView.getText() instanceof Spannable) {
1638 final Spannable sp = (Spannable) mTextView.getText();
1639 candStart = EditableInputConnection.getComposingSpanStart(sp);
1640 candEnd = EditableInputConnection.getComposingSpanEnd(sp);
1641 }
Jean Chalardc99d33f2013-02-28 16:39:47 -08001642 // InputMethodManager#updateSelection skips sending the message if
1643 // none of the parameters have changed since the last time we called it.
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001644 imm.updateSelection(mTextView,
1645 selectionStart, selectionEnd, candStart, candEnd);
1646 }
1647 }
1648 }
1649
Gilles Debunned88876a2012-03-16 17:34:04 -07001650 void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint,
1651 int cursorOffsetVertical) {
1652 final int selectionStart = mTextView.getSelectionStart();
1653 final int selectionEnd = mTextView.getSelectionEnd();
1654
1655 final InputMethodState ims = mInputMethodState;
1656 if (ims != null && ims.mBatchEditNesting == 0) {
1657 InputMethodManager imm = InputMethodManager.peekInstance();
1658 if (imm != null) {
1659 if (imm.isActive(mTextView)) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001660 if (ims.mContentChanged || ims.mSelectionModeChanged) {
1661 // We are in extract mode and the content has changed
1662 // in some way... just report complete new text to the
1663 // input method.
Yohei Yukawab6bec1a2015-05-01 16:18:25 -07001664 reportExtractedText();
Gilles Debunned88876a2012-03-16 17:34:04 -07001665 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001666 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001667 }
1668 }
1669
1670 if (mCorrectionHighlighter != null) {
1671 mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
1672 }
1673
Roozbeh Pournader9c133072017-07-26 22:36:27 -07001674 if (highlight != null && selectionStart == selectionEnd && mCursorDrawable != null) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001675 drawCursor(canvas, cursorOffsetVertical);
1676 // Rely on the drawable entirely, do not draw the cursor line.
1677 // Has to be done after the IMM related code above which relies on the highlight.
1678 highlight = null;
1679 }
1680
1681 if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
1682 drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
1683 cursorOffsetVertical);
1684 } else {
1685 layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
1686 }
1687 }
1688
1689 private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
1690 Paint highlightPaint, int cursorOffsetVertical) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001691 final long lineRange = layout.getLineRangeForDraw(canvas);
1692 int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
1693 int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
1694 if (lastLine < 0) return;
1695
1696 layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
1697 firstLine, lastLine);
1698
1699 if (layout instanceof DynamicLayout) {
Chris Craik956f3402015-04-27 16:41:00 -07001700 if (mTextRenderNodes == null) {
1701 mTextRenderNodes = ArrayUtils.emptyArray(TextRenderNode.class);
Gilles Debunned88876a2012-03-16 17:34:04 -07001702 }
1703
1704 DynamicLayout dynamicLayout = (DynamicLayout) layout;
Gilles Debunne157aafc2012-04-19 17:21:57 -07001705 int[] blockEndLines = dynamicLayout.getBlockEndLines();
Gilles Debunned88876a2012-03-16 17:34:04 -07001706 int[] blockIndices = dynamicLayout.getBlockIndices();
1707 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
Sangkyu Lee955beb22012-12-10 15:47:00 +09001708 final int indexFirstChangedBlock = dynamicLayout.getIndexFirstChangedBlock();
Gilles Debunned88876a2012-03-16 17:34:04 -07001709
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001710 final ArraySet<Integer> blockSet = dynamicLayout.getBlocksAlwaysNeedToBeRedrawn();
1711 if (blockSet != null) {
1712 for (int i = 0; i < blockSet.size(); i++) {
1713 final int blockIndex = dynamicLayout.getBlockIndex(blockSet.valueAt(i));
1714 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
1715 && mTextRenderNodes[blockIndex] != null) {
1716 mTextRenderNodes[blockIndex].needsToBeShifted = true;
1717 }
1718 }
1719 }
1720
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001721 int startBlock = Arrays.binarySearch(blockEndLines, 0, numberOfBlocks, firstLine);
1722 if (startBlock < 0) {
1723 startBlock = -(startBlock + 1);
1724 }
1725 startBlock = Math.min(indexFirstChangedBlock, startBlock);
Gilles Debunned88876a2012-03-16 17:34:04 -07001726
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001727 int startIndexToFindAvailableRenderNode = 0;
1728 int lastIndex = numberOfBlocks;
1729
1730 for (int i = startBlock; i < numberOfBlocks; i++) {
1731 final int blockIndex = blockIndices[i];
1732 if (i >= indexFirstChangedBlock
1733 && blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
1734 && mTextRenderNodes[blockIndex] != null) {
1735 mTextRenderNodes[blockIndex].needsToBeShifted = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001736 }
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001737 if (blockEndLines[i] < firstLine) {
1738 // Blocks in [indexFirstChangedBlock, firstLine) are not redrawn here. They will
1739 // be redrawn after they get scrolled into drawing range.
1740 continue;
Gilles Debunned88876a2012-03-16 17:34:04 -07001741 }
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001742 startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas, layout,
1743 highlight, highlightPaint, cursorOffsetVertical, blockEndLines,
1744 blockIndices, i, numberOfBlocks, startIndexToFindAvailableRenderNode);
1745 if (blockEndLines[i] >= lastLine) {
1746 lastIndex = Math.max(indexFirstChangedBlock, i + 1);
1747 break;
Gilles Debunned88876a2012-03-16 17:34:04 -07001748 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001749 }
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001750 if (blockSet != null) {
1751 for (int i = 0; i < blockSet.size(); i++) {
1752 final int block = blockSet.valueAt(i);
1753 final int blockIndex = dynamicLayout.getBlockIndex(block);
1754 if (blockIndex == DynamicLayout.INVALID_BLOCK_INDEX
1755 || mTextRenderNodes[blockIndex] == null
1756 || mTextRenderNodes[blockIndex].needsToBeShifted) {
1757 startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas,
1758 layout, highlight, highlightPaint, cursorOffsetVertical,
1759 blockEndLines, blockIndices, block, numberOfBlocks,
1760 startIndexToFindAvailableRenderNode);
1761 }
1762 }
1763 }
Sangkyu Lee955beb22012-12-10 15:47:00 +09001764
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001765 dynamicLayout.setIndexFirstChangedBlock(lastIndex);
Gilles Debunned88876a2012-03-16 17:34:04 -07001766 } else {
1767 // Boring layout is used for empty and hint text
1768 layout.drawText(canvas, firstLine, lastLine);
1769 }
1770 }
1771
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001772 private int drawHardwareAcceleratedInner(Canvas canvas, Layout layout, Path highlight,
1773 Paint highlightPaint, int cursorOffsetVertical, int[] blockEndLines,
1774 int[] blockIndices, int blockInfoIndex, int numberOfBlocks,
1775 int startIndexToFindAvailableRenderNode) {
1776 final int blockEndLine = blockEndLines[blockInfoIndex];
1777 int blockIndex = blockIndices[blockInfoIndex];
1778
1779 final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
1780 if (blockIsInvalid) {
1781 blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
1782 startIndexToFindAvailableRenderNode);
1783 // Note how dynamic layout's internal block indices get updated from Editor
1784 blockIndices[blockInfoIndex] = blockIndex;
1785 if (mTextRenderNodes[blockIndex] != null) {
1786 mTextRenderNodes[blockIndex].isDirty = true;
1787 }
1788 startIndexToFindAvailableRenderNode = blockIndex + 1;
1789 }
1790
1791 if (mTextRenderNodes[blockIndex] == null) {
1792 mTextRenderNodes[blockIndex] = new TextRenderNode("Text " + blockIndex);
1793 }
1794
1795 final boolean blockDisplayListIsInvalid = mTextRenderNodes[blockIndex].needsRecord();
1796 RenderNode blockDisplayList = mTextRenderNodes[blockIndex].renderNode;
1797 if (mTextRenderNodes[blockIndex].needsToBeShifted || blockDisplayListIsInvalid) {
1798 final int blockBeginLine = blockInfoIndex == 0 ?
1799 0 : blockEndLines[blockInfoIndex - 1] + 1;
1800 final int top = layout.getLineTop(blockBeginLine);
1801 final int bottom = layout.getLineBottom(blockEndLine);
1802 int left = 0;
1803 int right = mTextView.getWidth();
1804 if (mTextView.getHorizontallyScrolling()) {
1805 float min = Float.MAX_VALUE;
1806 float max = Float.MIN_VALUE;
1807 for (int line = blockBeginLine; line <= blockEndLine; line++) {
1808 min = Math.min(min, layout.getLineLeft(line));
1809 max = Math.max(max, layout.getLineRight(line));
1810 }
1811 left = (int) min;
1812 right = (int) (max + 0.5f);
1813 }
1814
1815 // Rebuild display list if it is invalid
1816 if (blockDisplayListIsInvalid) {
1817 final DisplayListCanvas displayListCanvas = blockDisplayList.start(
1818 right - left, bottom - top);
1819 try {
1820 // drawText is always relative to TextView's origin, this translation
1821 // brings this range of text back to the top left corner of the viewport
1822 displayListCanvas.translate(-left, -top);
1823 layout.drawText(displayListCanvas, blockBeginLine, blockEndLine);
1824 mTextRenderNodes[blockIndex].isDirty = false;
1825 // No need to untranslate, previous context is popped after
1826 // drawDisplayList
1827 } finally {
1828 blockDisplayList.end(displayListCanvas);
1829 // Same as drawDisplayList below, handled by our TextView's parent
1830 blockDisplayList.setClipToBounds(false);
1831 }
1832 }
1833
1834 // Valid display list only needs to update its drawing location.
1835 blockDisplayList.setLeftTopRightBottom(left, top, right, bottom);
1836 mTextRenderNodes[blockIndex].needsToBeShifted = false;
1837 }
1838 ((DisplayListCanvas) canvas).drawRenderNode(blockDisplayList);
1839 return startIndexToFindAvailableRenderNode;
1840 }
1841
Gilles Debunned88876a2012-03-16 17:34:04 -07001842 private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
1843 int searchStartIndex) {
Chris Craik956f3402015-04-27 16:41:00 -07001844 int length = mTextRenderNodes.length;
Gilles Debunned88876a2012-03-16 17:34:04 -07001845 for (int i = searchStartIndex; i < length; i++) {
1846 boolean blockIndexFound = false;
1847 for (int j = 0; j < numberOfBlocks; j++) {
1848 if (blockIndices[j] == i) {
1849 blockIndexFound = true;
1850 break;
1851 }
1852 }
1853 if (blockIndexFound) continue;
1854 return i;
1855 }
1856
1857 // No available index found, the pool has to grow
Chris Craik956f3402015-04-27 16:41:00 -07001858 mTextRenderNodes = GrowingArrayUtils.append(mTextRenderNodes, length, null);
Gilles Debunned88876a2012-03-16 17:34:04 -07001859 return length;
1860 }
1861
1862 private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
1863 final boolean translate = cursorOffsetVertical != 0;
1864 if (translate) canvas.translate(0, cursorOffsetVertical);
Roozbeh Pournader9c133072017-07-26 22:36:27 -07001865 if (mCursorDrawable != null) {
1866 mCursorDrawable.draw(canvas);
Gilles Debunned88876a2012-03-16 17:34:04 -07001867 }
1868 if (translate) canvas.translate(0, -cursorOffsetVertical);
1869 }
1870
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09001871 void invalidateHandlesAndActionMode() {
1872 if (mSelectionModifierCursorController != null) {
1873 mSelectionModifierCursorController.invalidateHandles();
1874 }
1875 if (mInsertionPointCursorController != null) {
1876 mInsertionPointCursorController.invalidateHandle();
1877 }
1878 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01001879 invalidateActionMode();
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09001880 }
1881 }
1882
Gilles Debunneebc86af2012-04-20 15:10:47 -07001883 /**
1884 * Invalidates all the sub-display lists that overlap the specified character range
1885 */
1886 void invalidateTextDisplayList(Layout layout, int start, int end) {
Chris Craik956f3402015-04-27 16:41:00 -07001887 if (mTextRenderNodes != null && layout instanceof DynamicLayout) {
Gilles Debunneebc86af2012-04-20 15:10:47 -07001888 final int firstLine = layout.getLineForOffset(start);
1889 final int lastLine = layout.getLineForOffset(end);
1890
1891 DynamicLayout dynamicLayout = (DynamicLayout) layout;
1892 int[] blockEndLines = dynamicLayout.getBlockEndLines();
1893 int[] blockIndices = dynamicLayout.getBlockIndices();
1894 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
1895
1896 int i = 0;
1897 // Skip the blocks before firstLine
1898 while (i < numberOfBlocks) {
1899 if (blockEndLines[i] >= firstLine) break;
1900 i++;
1901 }
1902
1903 // Invalidate all subsequent blocks until lastLine is passed
1904 while (i < numberOfBlocks) {
1905 final int blockIndex = blockIndices[i];
1906 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX) {
Chris Craik956f3402015-04-27 16:41:00 -07001907 mTextRenderNodes[blockIndex].isDirty = true;
Gilles Debunneebc86af2012-04-20 15:10:47 -07001908 }
1909 if (blockEndLines[i] >= lastLine) break;
1910 i++;
1911 }
1912 }
1913 }
1914
Gilles Debunned88876a2012-03-16 17:34:04 -07001915 void invalidateTextDisplayList() {
Chris Craik956f3402015-04-27 16:41:00 -07001916 if (mTextRenderNodes != null) {
1917 for (int i = 0; i < mTextRenderNodes.length; i++) {
1918 if (mTextRenderNodes[i] != null) mTextRenderNodes[i].isDirty = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001919 }
1920 }
1921 }
1922
Roozbeh Pournader9c133072017-07-26 22:36:27 -07001923 void updateCursorPosition() {
Gilles Debunned88876a2012-03-16 17:34:04 -07001924 if (mTextView.mCursorDrawableRes == 0) {
Roozbeh Pournader9c133072017-07-26 22:36:27 -07001925 mCursorDrawable = null;
Gilles Debunned88876a2012-03-16 17:34:04 -07001926 return;
1927 }
1928
Roozbeh Pournader9c133072017-07-26 22:36:27 -07001929 final Layout layout = mTextView.getLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -07001930 final int offset = mTextView.getSelectionStart();
1931 final int line = layout.getLineForOffset(offset);
1932 final int top = layout.getLineTop(line);
Siyamed Sinira60b59d2017-07-26 09:26:41 -07001933 final int bottom = layout.getLineBottomWithoutSpacing(line);
Gilles Debunned88876a2012-03-16 17:34:04 -07001934
Roozbeh Pournader9c133072017-07-26 22:36:27 -07001935 final boolean clamped = layout.shouldClampCursor(line);
1936 updateCursorPosition(top, bottom, layout.getPrimaryHorizontal(offset, clamped));
Gilles Debunned88876a2012-03-16 17:34:04 -07001937 }
1938
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001939 void refreshTextActionMode() {
1940 if (extractedTextModeWillBeStarted()) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001941 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001942 return;
1943 }
1944 final boolean hasSelection = mTextView.hasSelection();
1945 final SelectionModifierCursorController selectionController = getSelectionController();
1946 final InsertionPointCursorController insertionController = getInsertionController();
1947 if ((selectionController != null && selectionController.isCursorBeingModified())
1948 || (insertionController != null && insertionController.isCursorBeingModified())) {
1949 // ActionMode should be managed by the currently active cursor controller.
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001950 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001951 return;
1952 }
1953 if (hasSelection) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001954 hideInsertionPointCursorController();
1955 if (mTextActionMode == null) {
Keisuke Kuroyanagi0fd28c92016-04-04 17:43:06 +09001956 if (mRestartActionModeOnNextRefresh) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001957 // To avoid distraction, newly start action mode only when selection action
Keisuke Kuroyanagi0fd28c92016-04-04 17:43:06 +09001958 // mode is being restarted.
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01001959 startSelectionActionModeAsync(false);
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001960 }
1961 } else if (selectionController == null || !selectionController.isActive()) {
1962 // Insertion action mode is active. Avoid dismissing the selection.
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001963 stopTextActionModeWithPreservingSelection();
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01001964 startSelectionActionModeAsync(false);
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001965 } else {
1966 mTextActionMode.invalidateContentRect();
1967 }
1968 } else {
1969 // Insertion action mode is started only when insertion controller is explicitly
1970 // activated.
1971 if (insertionController == null || !insertionController.isActive()) {
1972 stopTextActionMode();
1973 } else if (mTextActionMode != null) {
1974 mTextActionMode.invalidateContentRect();
1975 }
1976 }
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001977 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001978 }
1979
Gilles Debunned88876a2012-03-16 17:34:04 -07001980 /**
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001981 * Start an Insertion action mode.
Gilles Debunned88876a2012-03-16 17:34:04 -07001982 */
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001983 void startInsertionActionMode() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001984 if (mInsertionActionModeRunnable != null) {
1985 mTextView.removeCallbacks(mInsertionActionModeRunnable);
1986 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01001987 if (extractedTextModeWillBeStarted()) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001988 return;
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01001989 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001990 stopTextActionMode();
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01001991
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001992 ActionMode.Callback actionModeCallback =
1993 new TextActionModeCallback(false /* hasSelection */);
1994 mTextActionMode = mTextView.startActionMode(
Clara Bayarrib8ed5b72015-04-09 15:26:41 +01001995 actionModeCallback, ActionMode.TYPE_FLOATING);
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001996 if (mTextActionMode != null && getInsertionController() != null) {
1997 getInsertionController().show();
1998 }
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00001999 }
2000
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002001 @NonNull
2002 TextView getTextView() {
2003 return mTextView;
2004 }
2005
2006 @Nullable
2007 ActionMode getTextActionMode() {
2008 return mTextActionMode;
2009 }
2010
2011 void setRestartActionModeOnNextRefresh(boolean value) {
2012 mRestartActionModeOnNextRefresh = value;
2013 }
2014
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00002015 /**
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002016 * Asynchronously starts a selection action mode using the TextClassifier.
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00002017 */
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002018 void startSelectionActionModeAsync(boolean adjustSelection) {
2019 getSelectionActionModeHelper().startActionModeAsync(adjustSelection);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002020 }
2021
2022 /**
2023 * Asynchronously invalidates an action mode using the TextClassifier.
2024 */
Abodunrinwa Toki4ce651e2017-05-12 15:37:29 +01002025 void invalidateActionModeAsync() {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002026 getSelectionActionModeHelper().invalidateActionModeAsync();
2027 }
2028
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002029 /**
2030 * Synchronously invalidates an action mode without the TextClassifier.
2031 */
2032 private void invalidateActionMode() {
2033 if (mTextActionMode != null) {
2034 mTextActionMode.invalidate();
2035 }
2036 }
2037
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002038 private SelectionActionModeHelper getSelectionActionModeHelper() {
2039 if (mSelectionActionModeHelper == null) {
2040 mSelectionActionModeHelper = new SelectionActionModeHelper(this);
Clara Bayarri578286f2015-04-10 15:35:31 +01002041 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002042 return mSelectionActionModeHelper;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00002043 }
2044
Clara Bayarridfac4432015-05-15 12:18:24 +01002045 /**
2046 * If the TextView allows text selection, selects the current word when no existing selection
2047 * was available and starts a drag.
2048 *
2049 * @return true if the drag was started.
2050 */
2051 private boolean selectCurrentWordAndStartDrag() {
Clara Bayarri7184c8a2015-06-05 17:34:09 +01002052 if (mInsertionActionModeRunnable != null) {
2053 mTextView.removeCallbacks(mInsertionActionModeRunnable);
2054 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002055 if (extractedTextModeWillBeStarted()) {
Clara Bayarridfac4432015-05-15 12:18:24 +01002056 return false;
2057 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002058 if (!checkField()) {
Clara Bayarridfac4432015-05-15 12:18:24 +01002059 return false;
2060 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002061 if (!mTextView.hasSelection() && !selectCurrentWord()) {
2062 // No selection and cannot select a word.
2063 return false;
2064 }
2065 stopTextActionModeWithPreservingSelection();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08002066 getSelectionController().enterDrag(
2067 SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_WORD);
Clara Bayarridfac4432015-05-15 12:18:24 +01002068 return true;
2069 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002070
Clara Bayarridfac4432015-05-15 12:18:24 +01002071 /**
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002072 * Checks whether a selection can be performed on the current TextView.
Clara Bayarridfac4432015-05-15 12:18:24 +01002073 *
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002074 * @return true if a selection can be performed
Clara Bayarridfac4432015-05-15 12:18:24 +01002075 */
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002076 boolean checkField() {
Clara Bayarridfac4432015-05-15 12:18:24 +01002077 if (!mTextView.canSelectText() || !mTextView.requestFocus()) {
2078 Log.w(TextView.LOG_TAG,
2079 "TextView does not support text selection. Selection cancelled.");
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002080 return false;
2081 }
Clara Bayarridfac4432015-05-15 12:18:24 +01002082 return true;
2083 }
2084
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002085 boolean startSelectionActionModeInternal() {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002086 if (extractedTextModeWillBeStarted()) {
2087 return false;
2088 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002089 if (mTextActionMode != null) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002090 // Text action mode is already started
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002091 invalidateActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07002092 return false;
2093 }
2094
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002095 if (!checkField() || !mTextView.hasSelection()) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002096 return false;
2097 }
2098
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002099 ActionMode.Callback actionModeCallback =
2100 new TextActionModeCallback(true /* hasSelection */);
2101 mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING);
Gilles Debunned88876a2012-03-16 17:34:04 -07002102
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002103 final boolean selectionStarted = mTextActionMode != null;
Gilles Debunne3473b2b2012-04-20 16:21:10 -07002104 if (selectionStarted && !mTextView.isTextSelectable() && mShowSoftInputOnFocus) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002105 // Show the IME to be able to replace text, except when selecting non editable text.
2106 final InputMethodManager imm = InputMethodManager.peekInstance();
2107 if (imm != null) {
2108 imm.showSoftInput(mTextView, 0, null);
2109 }
2110 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002111 return selectionStarted;
2112 }
2113
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09002114 private boolean extractedTextModeWillBeStarted() {
Andrei Stingaceanub1891b32015-06-19 16:44:37 +01002115 if (!(mTextView.isInExtractedMode())) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002116 final InputMethodManager imm = InputMethodManager.peekInstance();
2117 return imm != null && imm.isFullscreenMode();
2118 }
2119 return false;
2120 }
2121
2122 /**
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002123 * @return <code>true</code> if it's reasonable to offer to show suggestions depending on
2124 * the current cursor position or selection range. This method is consistent with the
2125 * method to show suggestions {@link SuggestionsPopupWindow#updateSuggestions}.
Gilles Debunned88876a2012-03-16 17:34:04 -07002126 */
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002127 private boolean shouldOfferToShowSuggestions() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002128 CharSequence text = mTextView.getText();
2129 if (!(text instanceof Spannable)) return false;
2130
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002131 final Spannable spannable = (Spannable) text;
2132 final int selectionStart = mTextView.getSelectionStart();
2133 final int selectionEnd = mTextView.getSelectionEnd();
2134 final SuggestionSpan[] suggestionSpans = spannable.getSpans(selectionStart, selectionEnd,
2135 SuggestionSpan.class);
2136 if (suggestionSpans.length == 0) {
2137 return false;
2138 }
2139 if (selectionStart == selectionEnd) {
2140 // Spans overlap the cursor.
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002141 for (int i = 0; i < suggestionSpans.length; i++) {
2142 if (suggestionSpans[i].getSuggestions().length > 0) {
2143 return true;
2144 }
2145 }
2146 return false;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002147 }
2148 int minSpanStart = mTextView.getText().length();
2149 int maxSpanEnd = 0;
2150 int unionOfSpansCoveringSelectionStartStart = mTextView.getText().length();
2151 int unionOfSpansCoveringSelectionStartEnd = 0;
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002152 boolean hasValidSuggestions = false;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002153 for (int i = 0; i < suggestionSpans.length; i++) {
2154 final int spanStart = spannable.getSpanStart(suggestionSpans[i]);
2155 final int spanEnd = spannable.getSpanEnd(suggestionSpans[i]);
2156 minSpanStart = Math.min(minSpanStart, spanStart);
2157 maxSpanEnd = Math.max(maxSpanEnd, spanEnd);
2158 if (selectionStart < spanStart || selectionStart > spanEnd) {
2159 // The span doesn't cover the current selection start point.
2160 continue;
2161 }
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002162 hasValidSuggestions =
2163 hasValidSuggestions || suggestionSpans[i].getSuggestions().length > 0;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002164 unionOfSpansCoveringSelectionStartStart =
2165 Math.min(unionOfSpansCoveringSelectionStartStart, spanStart);
2166 unionOfSpansCoveringSelectionStartEnd =
2167 Math.max(unionOfSpansCoveringSelectionStartEnd, spanEnd);
2168 }
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002169 if (!hasValidSuggestions) {
2170 return false;
2171 }
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002172 if (unionOfSpansCoveringSelectionStartStart >= unionOfSpansCoveringSelectionStartEnd) {
2173 // No spans cover the selection start point.
2174 return false;
2175 }
2176 if (minSpanStart < unionOfSpansCoveringSelectionStartStart
2177 || maxSpanEnd > unionOfSpansCoveringSelectionStartEnd) {
2178 // There is a span that is not covered by the union. In this case, we soouldn't offer
2179 // to show suggestions as it's confusing.
2180 return false;
2181 }
2182 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07002183 }
2184
2185 /**
2186 * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
2187 * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
2188 */
2189 private boolean isCursorInsideEasyCorrectionSpan() {
2190 Spannable spannable = (Spannable) mTextView.getText();
2191 SuggestionSpan[] suggestionSpans = spannable.getSpans(mTextView.getSelectionStart(),
2192 mTextView.getSelectionEnd(), SuggestionSpan.class);
2193 for (int i = 0; i < suggestionSpans.length; i++) {
2194 if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
2195 return true;
2196 }
2197 }
2198 return false;
2199 }
2200
2201 void onTouchUpEvent(MotionEvent event) {
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +01002202 if (getSelectionActionModeHelper().resetSelection(
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +00002203 getTextView().getOffsetForPosition(event.getX(), event.getY()))) {
2204 return;
2205 }
2206
Gilles Debunned88876a2012-03-16 17:34:04 -07002207 boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect();
Mady Mellora2861452015-06-25 08:40:27 -07002208 hideCursorAndSpanControllers();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002209 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07002210 CharSequence text = mTextView.getText();
2211 if (!selectAllGotFocus && text.length() > 0) {
2212 // Move cursor
2213 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
2214 Selection.setSelection((Spannable) text, offset);
2215 if (mSpellChecker != null) {
2216 // When the cursor moves, the word that was typed may need spell check
2217 mSpellChecker.onSelectionChanged();
2218 }
Andrei Stingaceanu35c550c2015-05-07 16:49:49 +01002219
Gilles Debunned88876a2012-03-16 17:34:04 -07002220 if (!extractedTextModeWillBeStarted()) {
2221 if (isCursorInsideEasyCorrectionSpan()) {
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01002222 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002223 if (mInsertionActionModeRunnable != null) {
2224 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01002225 }
2226
Gilles Debunned88876a2012-03-16 17:34:04 -07002227 mShowSuggestionRunnable = new Runnable() {
2228 public void run() {
Keisuke Kuroyanagi713be062016-02-29 16:07:54 -08002229 replace();
Gilles Debunned88876a2012-03-16 17:34:04 -07002230 }
2231 };
2232 // removeCallbacks is performed on every touch
2233 mTextView.postDelayed(mShowSuggestionRunnable,
2234 ViewConfiguration.getDoubleTapTimeout());
2235 } else if (hasInsertionController()) {
2236 getInsertionController().show();
2237 }
2238 }
2239 }
2240 }
2241
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002242 protected void stopTextActionMode() {
2243 if (mTextActionMode != null) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002244 // This will hide the mSelectionModifierCursorController
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002245 mTextActionMode.finish();
Gilles Debunned88876a2012-03-16 17:34:04 -07002246 }
2247 }
2248
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002249 private void stopTextActionModeWithPreservingSelection() {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002250 if (mTextActionMode != null) {
2251 mRestartActionModeOnNextRefresh = true;
2252 }
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002253 mPreserveSelection = true;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002254 stopTextActionMode();
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002255 mPreserveSelection = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002256 }
2257
Gilles Debunned88876a2012-03-16 17:34:04 -07002258 /**
2259 * @return True if this view supports insertion handles.
2260 */
2261 boolean hasInsertionController() {
2262 return mInsertionControllerEnabled;
2263 }
2264
2265 /**
2266 * @return True if this view supports selection handles.
2267 */
2268 boolean hasSelectionController() {
2269 return mSelectionControllerEnabled;
2270 }
2271
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09002272 private InsertionPointCursorController getInsertionController() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002273 if (!mInsertionControllerEnabled) {
2274 return null;
2275 }
2276
2277 if (mInsertionPointCursorController == null) {
2278 mInsertionPointCursorController = new InsertionPointCursorController();
2279
2280 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
2281 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
2282 }
2283
2284 return mInsertionPointCursorController;
2285 }
2286
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002287 @Nullable
2288 SelectionModifierCursorController getSelectionController() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002289 if (!mSelectionControllerEnabled) {
2290 return null;
2291 }
2292
2293 if (mSelectionModifierCursorController == null) {
2294 mSelectionModifierCursorController = new SelectionModifierCursorController();
2295
2296 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
2297 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
2298 }
2299
2300 return mSelectionModifierCursorController;
2301 }
2302
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002303 @VisibleForTesting
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002304 @Nullable
2305 public Drawable getCursorDrawable() {
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002306 return mCursorDrawable;
2307 }
2308
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002309 private void updateCursorPosition(int top, int bottom, float horizontal) {
2310 if (mCursorDrawable == null) {
2311 mCursorDrawable = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07002312 mTextView.mCursorDrawableRes);
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002313 }
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002314 final int left = clampHorizontalPosition(mCursorDrawable, horizontal);
2315 final int width = mCursorDrawable.getIntrinsicWidth();
2316 mCursorDrawable.setBounds(left, top - mTempRect.top, left + width,
Gilles Debunned88876a2012-03-16 17:34:04 -07002317 bottom + mTempRect.bottom);
2318 }
2319
2320 /**
Siyamed Sinir987ec652016-02-17 19:44:41 -08002321 * Return clamped position for the drawable. If the drawable is within the boundaries of the
2322 * 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 -08002323 * 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 -08002324 * the view boundary. If the drawable is null, horizontal parameter is aligned to left or right
2325 * of the view.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002326 *
Siyamed Sinir987ec652016-02-17 19:44:41 -08002327 * @param drawable Drawable. Can be null.
2328 * @param horizontal Horizontal position for the drawable.
2329 * @return The clamped horizontal position for the drawable.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002330 */
Siyamed Sinir987ec652016-02-17 19:44:41 -08002331 private int clampHorizontalPosition(@Nullable final Drawable drawable, float horizontal) {
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002332 horizontal = Math.max(0.5f, horizontal - 0.5f);
2333 if (mTempRect == null) mTempRect = new Rect();
Siyamed Sinir987ec652016-02-17 19:44:41 -08002334
2335 int drawableWidth = 0;
2336 if (drawable != null) {
2337 drawable.getPadding(mTempRect);
2338 drawableWidth = drawable.getIntrinsicWidth();
2339 } else {
2340 mTempRect.setEmpty();
2341 }
2342
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002343 int scrollX = mTextView.getScrollX();
2344 float horizontalDiff = horizontal - scrollX;
2345 int viewClippedWidth = mTextView.getWidth() - mTextView.getCompoundPaddingLeft()
2346 - mTextView.getCompoundPaddingRight();
2347
2348 final int left;
2349 if (horizontalDiff >= (viewClippedWidth - 1f)) {
2350 // at the rightmost position
Siyamed Sinir987ec652016-02-17 19:44:41 -08002351 left = viewClippedWidth + scrollX - (drawableWidth - mTempRect.right);
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002352 } else if (Math.abs(horizontalDiff) <= 1f
2353 || (TextUtils.isEmpty(mTextView.getText())
Siyamed Sinir987ec652016-02-17 19:44:41 -08002354 && (TextView.VERY_WIDE - scrollX) <= (viewClippedWidth + 1f)
2355 && horizontal <= 1f)) {
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002356 // at the leftmost position
2357 left = scrollX - mTempRect.left;
2358 } else {
2359 left = (int) horizontal - mTempRect.left;
2360 }
2361 return left;
2362 }
2363
2364 /**
Gilles Debunned88876a2012-03-16 17:34:04 -07002365 * 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 -08002366 * a dictionary) from the current input method, provided by it calling
Gilles Debunned88876a2012-03-16 17:34:04 -07002367 * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
2368 * implementation flashes the background of the corrected word to provide feedback to the user.
2369 *
2370 * @param info The auto correct info about the text that was corrected.
2371 */
2372 public void onCommitCorrection(CorrectionInfo info) {
2373 if (mCorrectionHighlighter == null) {
2374 mCorrectionHighlighter = new CorrectionHighlighter();
2375 } else {
2376 mCorrectionHighlighter.invalidate(false);
2377 }
2378
2379 mCorrectionHighlighter.highlight(info);
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002380 mUndoInputFilter.freezeLastEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07002381 }
2382
Gilles Debunned88876a2012-03-16 17:34:04 -07002383 void onScrollChanged() {
Gilles Debunne157aafc2012-04-19 17:21:57 -07002384 if (mPositionListener != null) {
2385 mPositionListener.onScrollChanged();
2386 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002387 if (mTextActionMode != null) {
2388 mTextActionMode.invalidateContentRect();
Abodunrinwa Toki56195db2015-04-22 06:46:54 +01002389 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002390 }
2391
2392 /**
2393 * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
2394 */
2395 private boolean shouldBlink() {
2396 if (!isCursorVisible() || !mTextView.isFocused()) return false;
2397
2398 final int start = mTextView.getSelectionStart();
2399 if (start < 0) return false;
2400
2401 final int end = mTextView.getSelectionEnd();
2402 if (end < 0) return false;
2403
2404 return start == end;
2405 }
2406
2407 void makeBlink() {
2408 if (shouldBlink()) {
2409 mShowCursor = SystemClock.uptimeMillis();
2410 if (mBlink == null) mBlink = new Blink();
John Reckd0374c62015-10-20 13:25:01 -07002411 mTextView.removeCallbacks(mBlink);
2412 mTextView.postDelayed(mBlink, BLINK);
Gilles Debunned88876a2012-03-16 17:34:04 -07002413 } else {
John Reckd0374c62015-10-20 13:25:01 -07002414 if (mBlink != null) mTextView.removeCallbacks(mBlink);
Gilles Debunned88876a2012-03-16 17:34:04 -07002415 }
2416 }
2417
John Reckd0374c62015-10-20 13:25:01 -07002418 private class Blink implements Runnable {
Gilles Debunned88876a2012-03-16 17:34:04 -07002419 private boolean mCancelled;
2420
2421 public void run() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002422 if (mCancelled) {
2423 return;
2424 }
2425
John Reckd0374c62015-10-20 13:25:01 -07002426 mTextView.removeCallbacks(this);
Gilles Debunned88876a2012-03-16 17:34:04 -07002427
2428 if (shouldBlink()) {
2429 if (mTextView.getLayout() != null) {
2430 mTextView.invalidateCursorPath();
2431 }
2432
John Reckd0374c62015-10-20 13:25:01 -07002433 mTextView.postDelayed(this, BLINK);
Gilles Debunned88876a2012-03-16 17:34:04 -07002434 }
2435 }
2436
2437 void cancel() {
2438 if (!mCancelled) {
John Reckd0374c62015-10-20 13:25:01 -07002439 mTextView.removeCallbacks(this);
Gilles Debunned88876a2012-03-16 17:34:04 -07002440 mCancelled = true;
2441 }
2442 }
2443
2444 void uncancel() {
2445 mCancelled = false;
2446 }
2447 }
2448
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002449 private DragShadowBuilder getTextThumbnailBuilder(int start, int end) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002450 TextView shadowView = (TextView) View.inflate(mTextView.getContext(),
2451 com.android.internal.R.layout.text_drag_thumbnail, null);
2452
2453 if (shadowView == null) {
2454 throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
2455 }
2456
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002457 if (end - start > DRAG_SHADOW_MAX_TEXT_LENGTH) {
2458 final long range = getCharClusterRange(start + DRAG_SHADOW_MAX_TEXT_LENGTH);
2459 end = TextUtils.unpackRangeEndFromLong(range);
Gilles Debunned88876a2012-03-16 17:34:04 -07002460 }
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002461 final CharSequence text = mTextView.getTransformedText(start, end);
Gilles Debunned88876a2012-03-16 17:34:04 -07002462 shadowView.setText(text);
2463 shadowView.setTextColor(mTextView.getTextColors());
2464
Alan Viverettebb98ebd2015-05-08 17:17:44 -07002465 shadowView.setTextAppearance(R.styleable.Theme_textAppearanceLarge);
Gilles Debunned88876a2012-03-16 17:34:04 -07002466 shadowView.setGravity(Gravity.CENTER);
2467
2468 shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
2469 ViewGroup.LayoutParams.WRAP_CONTENT));
2470
2471 final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
2472 shadowView.measure(size, size);
2473
2474 shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
2475 shadowView.invalidate();
2476 return new DragShadowBuilder(shadowView);
2477 }
2478
2479 private static class DragLocalState {
2480 public TextView sourceTextView;
2481 public int start, end;
2482
2483 public DragLocalState(TextView sourceTextView, int start, int end) {
2484 this.sourceTextView = sourceTextView;
2485 this.start = start;
2486 this.end = end;
2487 }
2488 }
2489
2490 void onDrop(DragEvent event) {
Ben Murdoch3dac4602017-01-17 11:27:37 +00002491 SpannableStringBuilder content = new SpannableStringBuilder();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002492
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -07002493 final DragAndDropPermissions permissions = DragAndDropPermissions.obtain(event);
2494 if (permissions != null) {
2495 permissions.takeTransient();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002496 }
2497
2498 try {
2499 ClipData clipData = event.getClipData();
2500 final int itemCount = clipData.getItemCount();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002501 for (int i = 0; i < itemCount; i++) {
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002502 Item item = clipData.getItemAt(i);
2503 content.append(item.coerceToStyledText(mTextView.getContext()));
2504 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002505 } finally {
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -07002506 if (permissions != null) {
2507 permissions.release();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002508 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002509 }
2510
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002511 mTextView.beginBatchEdit();
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002512 mUndoInputFilter.freezeLastEdit();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002513 try {
2514 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
2515 Object localState = event.getLocalState();
2516 DragLocalState dragLocalState = null;
2517 if (localState instanceof DragLocalState) {
2518 dragLocalState = (DragLocalState) localState;
Gilles Debunned88876a2012-03-16 17:34:04 -07002519 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002520 boolean dragDropIntoItself = dragLocalState != null
2521 && dragLocalState.sourceTextView == mTextView;
Gilles Debunned88876a2012-03-16 17:34:04 -07002522
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002523 if (dragDropIntoItself) {
2524 if (offset >= dragLocalState.start && offset < dragLocalState.end) {
2525 // A drop inside the original selection discards the drop.
2526 return;
2527 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002528 }
2529
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002530 final int originalLength = mTextView.getText().length();
2531 int min = offset;
2532 int max = offset;
2533
2534 Selection.setSelection((Spannable) mTextView.getText(), max);
2535 mTextView.replaceText_internal(min, max, content);
2536
2537 if (dragDropIntoItself) {
2538 int dragSourceStart = dragLocalState.start;
2539 int dragSourceEnd = dragLocalState.end;
2540 if (max <= dragSourceStart) {
2541 // Inserting text before selection has shifted positions
2542 final int shift = mTextView.getText().length() - originalLength;
2543 dragSourceStart += shift;
2544 dragSourceEnd += shift;
2545 }
2546
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08002547 // Delete original selection
2548 mTextView.deleteText_internal(dragSourceStart, dragSourceEnd);
Gilles Debunned88876a2012-03-16 17:34:04 -07002549
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08002550 // Make sure we do not leave two adjacent spaces.
2551 final int prevCharIdx = Math.max(0, dragSourceStart - 1);
2552 final int nextCharIdx = Math.min(mTextView.getText().length(), dragSourceStart + 1);
2553 if (nextCharIdx > prevCharIdx + 1) {
2554 CharSequence t = mTextView.getTransformedText(prevCharIdx, nextCharIdx);
2555 if (Character.isSpaceChar(t.charAt(0)) && Character.isSpaceChar(t.charAt(1))) {
2556 mTextView.deleteText_internal(prevCharIdx, prevCharIdx + 1);
2557 }
Victoria Lease91373202012-09-07 16:41:59 -07002558 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002559 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002560 } finally {
2561 mTextView.endBatchEdit();
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002562 mUndoInputFilter.freezeLastEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07002563 }
2564 }
2565
Gilles Debunnec62589c2012-04-12 14:50:23 -07002566 public void addSpanWatchers(Spannable text) {
2567 final int textLength = text.length();
2568
2569 if (mKeyListener != null) {
2570 text.setSpan(mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
2571 }
2572
Jean Chalardbaf30942013-02-28 16:01:51 -08002573 if (mSpanController == null) {
2574 mSpanController = new SpanController();
Gilles Debunnec62589c2012-04-12 14:50:23 -07002575 }
Jean Chalardbaf30942013-02-28 16:01:51 -08002576 text.setSpan(mSpanController, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002577 }
2578
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002579 void setContextMenuAnchor(float x, float y) {
2580 mContextMenuAnchorX = x;
2581 mContextMenuAnchorY = y;
2582 }
2583
2584 void onCreateContextMenu(ContextMenu menu) {
2585 if (mIsBeingLongClicked || Float.isNaN(mContextMenuAnchorX)
2586 || Float.isNaN(mContextMenuAnchorY)) {
2587 return;
2588 }
2589 final int offset = mTextView.getOffsetForPosition(mContextMenuAnchorX, mContextMenuAnchorY);
2590 if (offset == -1) {
2591 return;
2592 }
Siyamed Sinir532f3c92017-06-15 18:22:31 -07002593
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002594 stopTextActionModeWithPreservingSelection();
Siyamed Sinir532f3c92017-06-15 18:22:31 -07002595 if (mTextView.canSelectText()) {
2596 final boolean isOnSelection = mTextView.hasSelection()
2597 && offset >= mTextView.getSelectionStart()
2598 && offset <= mTextView.getSelectionEnd();
2599 if (!isOnSelection) {
2600 // Right clicked position is not on the selection. Remove the selection and move the
2601 // cursor to the right clicked position.
2602 Selection.setSelection((Spannable) mTextView.getText(), offset);
2603 stopTextActionMode();
2604 }
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002605 }
2606
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002607 if (shouldOfferToShowSuggestions()) {
Keisuke Kuroyanagi182f5fe2016-03-11 16:31:29 +09002608 final SuggestionInfo[] suggestionInfoArray =
2609 new SuggestionInfo[SuggestionSpan.SUGGESTIONS_MAX_SIZE];
2610 for (int i = 0; i < suggestionInfoArray.length; i++) {
2611 suggestionInfoArray[i] = new SuggestionInfo();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002612 }
2613 final SubMenu subMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, MENU_ITEM_ORDER_REPLACE,
2614 com.android.internal.R.string.replace);
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002615 final int numItems = mSuggestionHelper.getSuggestionInfo(suggestionInfoArray, null);
Keisuke Kuroyanagi182f5fe2016-03-11 16:31:29 +09002616 for (int i = 0; i < numItems; i++) {
2617 final SuggestionInfo info = suggestionInfoArray[i];
2618 subMenu.add(Menu.NONE, Menu.NONE, i, info.mText)
2619 .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
2620 @Override
2621 public boolean onMenuItemClick(MenuItem item) {
2622 replaceWithSuggestion(info);
2623 return true;
2624 }
2625 });
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002626 }
2627 }
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002628
2629 menu.add(Menu.NONE, TextView.ID_UNDO, MENU_ITEM_ORDER_UNDO,
2630 com.android.internal.R.string.undo)
2631 .setAlphabeticShortcut('z')
2632 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2633 .setEnabled(mTextView.canUndo());
2634 menu.add(Menu.NONE, TextView.ID_REDO, MENU_ITEM_ORDER_REDO,
2635 com.android.internal.R.string.redo)
2636 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2637 .setEnabled(mTextView.canRedo());
2638
2639 menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
2640 com.android.internal.R.string.cut)
2641 .setAlphabeticShortcut('x')
2642 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2643 .setEnabled(mTextView.canCut());
2644 menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
2645 com.android.internal.R.string.copy)
2646 .setAlphabeticShortcut('c')
2647 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2648 .setEnabled(mTextView.canCopy());
2649 menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
2650 com.android.internal.R.string.paste)
2651 .setAlphabeticShortcut('v')
2652 .setEnabled(mTextView.canPaste())
2653 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01002654 menu.add(Menu.NONE, TextView.ID_PASTE_AS_PLAIN_TEXT, MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002655 com.android.internal.R.string.paste_as_plain_text)
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01002656 .setEnabled(mTextView.canPasteAsPlainText())
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002657 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2658 menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
2659 com.android.internal.R.string.share)
2660 .setEnabled(mTextView.canShare())
2661 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2662 menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
2663 com.android.internal.R.string.selectAll)
2664 .setAlphabeticShortcut('a')
2665 .setEnabled(mTextView.canSelectAllText())
2666 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Felipe Leme2ac463e2017-03-13 14:06:25 -07002667 menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
Felipe Leme555bcac2017-06-26 12:53:56 -07002668 android.R.string.autofill)
Felipe Leme2ac463e2017-03-13 14:06:25 -07002669 .setEnabled(mTextView.canRequestAutofill())
2670 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002671
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002672 mPreserveSelection = true;
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002673 }
2674
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002675 @Nullable
2676 private SuggestionSpan findEquivalentSuggestionSpan(
2677 @NonNull SuggestionSpanInfo suggestionSpanInfo) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002678 final Editable editable = (Editable) mTextView.getText();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002679 if (editable.getSpanStart(suggestionSpanInfo.mSuggestionSpan) >= 0) {
2680 // Exactly same span is found.
2681 return suggestionSpanInfo.mSuggestionSpan;
2682 }
2683 // Suggestion span couldn't be found. Try to find a suggestion span that has the same
2684 // contents.
2685 final SuggestionSpan[] suggestionSpans = editable.getSpans(suggestionSpanInfo.mSpanStart,
2686 suggestionSpanInfo.mSpanEnd, SuggestionSpan.class);
2687 for (final SuggestionSpan suggestionSpan : suggestionSpans) {
2688 final int start = editable.getSpanStart(suggestionSpan);
2689 if (start != suggestionSpanInfo.mSpanStart) {
2690 continue;
2691 }
2692 final int end = editable.getSpanEnd(suggestionSpan);
2693 if (end != suggestionSpanInfo.mSpanEnd) {
2694 continue;
2695 }
2696 if (suggestionSpan.equals(suggestionSpanInfo.mSuggestionSpan)) {
2697 return suggestionSpan;
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08002698 }
2699 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002700 return null;
2701 }
2702
2703 private void replaceWithSuggestion(@NonNull final SuggestionInfo suggestionInfo) {
2704 final SuggestionSpan targetSuggestionSpan = findEquivalentSuggestionSpan(
2705 suggestionInfo.mSuggestionSpanInfo);
2706 if (targetSuggestionSpan == null) {
2707 // Span has been removed
2708 return;
2709 }
2710 final Editable editable = (Editable) mTextView.getText();
2711 final int spanStart = editable.getSpanStart(targetSuggestionSpan);
2712 final int spanEnd = editable.getSpanEnd(targetSuggestionSpan);
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08002713 if (spanStart < 0 || spanEnd <= spanStart) {
2714 // Span has been removed
2715 return;
2716 }
2717
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002718 final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
2719 // SuggestionSpans are removed by replace: save them before
2720 SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
2721 SuggestionSpan.class);
2722 final int length = suggestionSpans.length;
2723 int[] suggestionSpansStarts = new int[length];
2724 int[] suggestionSpansEnds = new int[length];
2725 int[] suggestionSpansFlags = new int[length];
2726 for (int i = 0; i < length; i++) {
2727 final SuggestionSpan suggestionSpan = suggestionSpans[i];
2728 suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
2729 suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
2730 suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
2731
2732 // Remove potential misspelled flags
2733 int suggestionSpanFlags = suggestionSpan.getFlags();
2734 if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
2735 suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
2736 suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
2737 suggestionSpan.setFlags(suggestionSpanFlags);
2738 }
2739 }
2740
2741 // Notify source IME of the suggestion pick. Do this before swapping texts.
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002742 targetSuggestionSpan.notifySelection(
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002743 mTextView.getContext(), originalText, suggestionInfo.mSuggestionIndex);
2744
2745 // Swap text content between actual text and Suggestion span
2746 final int suggestionStart = suggestionInfo.mSuggestionStart;
2747 final int suggestionEnd = suggestionInfo.mSuggestionEnd;
2748 final String suggestion = suggestionInfo.mText.subSequence(
2749 suggestionStart, suggestionEnd).toString();
2750 mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
2751
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002752 String[] suggestions = targetSuggestionSpan.getSuggestions();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002753 suggestions[suggestionInfo.mSuggestionIndex] = originalText;
2754
2755 // Restore previous SuggestionSpans
2756 final int lengthDelta = suggestion.length() - (spanEnd - spanStart);
2757 for (int i = 0; i < length; i++) {
2758 // Only spans that include the modified region make sense after replacement
2759 // Spans partially included in the replaced region are removed, there is no
2760 // way to assign them a valid range after replacement
2761 if (suggestionSpansStarts[i] <= spanStart && suggestionSpansEnds[i] >= spanEnd) {
2762 mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
2763 suggestionSpansEnds[i] + lengthDelta, suggestionSpansFlags[i]);
2764 }
2765 }
2766 // Move cursor at the end of the replaced word
2767 final int newCursorPosition = spanEnd + lengthDelta;
2768 mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
2769 }
2770
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002771 private final MenuItem.OnMenuItemClickListener mOnContextMenuItemClickListener =
2772 new MenuItem.OnMenuItemClickListener() {
2773 @Override
2774 public boolean onMenuItemClick(MenuItem item) {
2775 if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
2776 return true;
2777 }
2778 return mTextView.onTextContextMenuItem(item.getItemId());
2779 }
2780 };
2781
Gilles Debunned88876a2012-03-16 17:34:04 -07002782 /**
2783 * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
2784 * pop-up should be displayed.
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07002785 * Also monitors {@link Selection} to call back to the attached input method.
Gilles Debunned88876a2012-03-16 17:34:04 -07002786 */
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09002787 private class SpanController implements SpanWatcher {
Gilles Debunned88876a2012-03-16 17:34:04 -07002788
2789 private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
2790
2791 private EasyEditPopupWindow mPopupWindow;
2792
Gilles Debunned88876a2012-03-16 17:34:04 -07002793 private Runnable mHidePopup;
2794
Jean Chalardbaf30942013-02-28 16:01:51 -08002795 // This function is pure but inner classes can't have static functions
2796 private boolean isNonIntermediateSelectionSpan(final Spannable text,
2797 final Object span) {
2798 return (Selection.SELECTION_START == span || Selection.SELECTION_END == span)
2799 && (text.getSpanFlags(span) & Spanned.SPAN_INTERMEDIATE) == 0;
2800 }
2801
Gilles Debunnec62589c2012-04-12 14:50:23 -07002802 @Override
2803 public void onSpanAdded(Spannable text, Object span, int start, int end) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002804 if (isNonIntermediateSelectionSpan(text, span)) {
2805 sendUpdateSelection();
2806 } else if (span instanceof EasyEditSpan) {
Gilles Debunnec62589c2012-04-12 14:50:23 -07002807 if (mPopupWindow == null) {
2808 mPopupWindow = new EasyEditPopupWindow();
2809 mHidePopup = new Runnable() {
2810 @Override
2811 public void run() {
2812 hide();
2813 }
2814 };
2815 }
2816
2817 // Make sure there is only at most one EasyEditSpan in the text
2818 if (mPopupWindow.mEasyEditSpan != null) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002819 mPopupWindow.mEasyEditSpan.setDeleteEnabled(false);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002820 }
2821
2822 mPopupWindow.setEasyEditSpan((EasyEditSpan) span);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002823 mPopupWindow.setOnDeleteListener(new EasyEditDeleteListener() {
2824 @Override
2825 public void onDeleteClick(EasyEditSpan span) {
2826 Editable editable = (Editable) mTextView.getText();
2827 int start = editable.getSpanStart(span);
2828 int end = editable.getSpanEnd(span);
2829 if (start >= 0 && end >= 0) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002830 sendEasySpanNotification(EasyEditSpan.TEXT_DELETED, span);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002831 mTextView.deleteText_internal(start, end);
2832 }
2833 editable.removeSpan(span);
2834 }
2835 });
Gilles Debunnec62589c2012-04-12 14:50:23 -07002836
2837 if (mTextView.getWindowVisibility() != View.VISIBLE) {
2838 // The window is not visible yet, ignore the text change.
2839 return;
2840 }
2841
2842 if (mTextView.getLayout() == null) {
2843 // The view has not been laid out yet, ignore the text change
2844 return;
2845 }
2846
2847 if (extractedTextModeWillBeStarted()) {
2848 // The input is in extract mode. Do not handle the easy edit in
2849 // the original TextView, as the ExtractEditText will do
2850 return;
2851 }
2852
2853 mPopupWindow.show();
2854 mTextView.removeCallbacks(mHidePopup);
2855 mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
2856 }
2857 }
2858
2859 @Override
2860 public void onSpanRemoved(Spannable text, Object span, int start, int end) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002861 if (isNonIntermediateSelectionSpan(text, span)) {
2862 sendUpdateSelection();
2863 } else if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
Gilles Debunnec62589c2012-04-12 14:50:23 -07002864 hide();
2865 }
2866 }
2867
2868 @Override
2869 public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd,
2870 int newStart, int newEnd) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002871 if (isNonIntermediateSelectionSpan(text, span)) {
2872 sendUpdateSelection();
2873 } else if (mPopupWindow != null && span instanceof EasyEditSpan) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002874 EasyEditSpan easyEditSpan = (EasyEditSpan) span;
Jean Chalardbaf30942013-02-28 16:01:51 -08002875 sendEasySpanNotification(EasyEditSpan.TEXT_MODIFIED, easyEditSpan);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002876 text.removeSpan(easyEditSpan);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002877 }
2878 }
2879
Gilles Debunned88876a2012-03-16 17:34:04 -07002880 public void hide() {
2881 if (mPopupWindow != null) {
2882 mPopupWindow.hide();
2883 mTextView.removeCallbacks(mHidePopup);
2884 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002885 }
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002886
Jean Chalardbaf30942013-02-28 16:01:51 -08002887 private void sendEasySpanNotification(int textChangedType, EasyEditSpan span) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002888 try {
2889 PendingIntent pendingIntent = span.getPendingIntent();
2890 if (pendingIntent != null) {
2891 Intent intent = new Intent();
2892 intent.putExtra(EasyEditSpan.EXTRA_TEXT_CHANGED_TYPE, textChangedType);
2893 pendingIntent.send(mTextView.getContext(), 0, intent);
2894 }
2895 } catch (CanceledException e) {
2896 // This should not happen, as we should try to send the intent only once.
2897 Log.w(TAG, "PendingIntent for notification cannot be sent", e);
2898 }
2899 }
2900 }
2901
2902 /**
2903 * Listens for the delete event triggered by {@link EasyEditPopupWindow}.
2904 */
2905 private interface EasyEditDeleteListener {
2906
2907 /**
2908 * Clicks the delete pop-up.
2909 */
2910 void onDeleteClick(EasyEditSpan span);
Gilles Debunned88876a2012-03-16 17:34:04 -07002911 }
2912
2913 /**
2914 * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07002915 * by {@link SpanController}.
Gilles Debunned88876a2012-03-16 17:34:04 -07002916 */
2917 private class EasyEditPopupWindow extends PinnedPopupWindow
2918 implements OnClickListener {
2919 private static final int POPUP_TEXT_LAYOUT =
2920 com.android.internal.R.layout.text_edit_action_popup_text;
2921 private TextView mDeleteTextView;
2922 private EasyEditSpan mEasyEditSpan;
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002923 private EasyEditDeleteListener mOnDeleteListener;
Gilles Debunned88876a2012-03-16 17:34:04 -07002924
2925 @Override
2926 protected void createPopupWindow() {
2927 mPopupWindow = new PopupWindow(mTextView.getContext(), null,
2928 com.android.internal.R.attr.textSelectHandleWindowStyle);
2929 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
2930 mPopupWindow.setClippingEnabled(true);
2931 }
2932
2933 @Override
2934 protected void initContentView() {
2935 LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
2936 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
2937 mContentView = linearLayout;
2938 mContentView.setBackgroundResource(
2939 com.android.internal.R.drawable.text_edit_side_paste_window);
2940
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002941 LayoutInflater inflater = (LayoutInflater) mTextView.getContext()
2942 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
Gilles Debunned88876a2012-03-16 17:34:04 -07002943
2944 LayoutParams wrapContent = new LayoutParams(
2945 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
2946
2947 mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
2948 mDeleteTextView.setLayoutParams(wrapContent);
2949 mDeleteTextView.setText(com.android.internal.R.string.delete);
2950 mDeleteTextView.setOnClickListener(this);
2951 mContentView.addView(mDeleteTextView);
2952 }
2953
Gilles Debunnec62589c2012-04-12 14:50:23 -07002954 public void setEasyEditSpan(EasyEditSpan easyEditSpan) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002955 mEasyEditSpan = easyEditSpan;
Gilles Debunned88876a2012-03-16 17:34:04 -07002956 }
2957
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002958 private void setOnDeleteListener(EasyEditDeleteListener listener) {
2959 mOnDeleteListener = listener;
2960 }
2961
Gilles Debunned88876a2012-03-16 17:34:04 -07002962 @Override
2963 public void onClick(View view) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002964 if (view == mDeleteTextView
2965 && mEasyEditSpan != null && mEasyEditSpan.isDeleteEnabled()
2966 && mOnDeleteListener != null) {
2967 mOnDeleteListener.onDeleteClick(mEasyEditSpan);
Gilles Debunned88876a2012-03-16 17:34:04 -07002968 }
2969 }
2970
2971 @Override
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002972 public void hide() {
2973 if (mEasyEditSpan != null) {
2974 mEasyEditSpan.setDeleteEnabled(false);
2975 }
2976 mOnDeleteListener = null;
2977 super.hide();
2978 }
2979
2980 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07002981 protected int getTextOffset() {
2982 // Place the pop-up at the end of the span
2983 Editable editable = (Editable) mTextView.getText();
2984 return editable.getSpanEnd(mEasyEditSpan);
2985 }
2986
2987 @Override
2988 protected int getVerticalLocalPosition(int line) {
Siyamed Sinira60b59d2017-07-26 09:26:41 -07002989 final Layout layout = mTextView.getLayout();
2990 return layout.getLineBottomWithoutSpacing(line);
Gilles Debunned88876a2012-03-16 17:34:04 -07002991 }
2992
2993 @Override
2994 protected int clipVertically(int positionY) {
2995 // As we display the pop-up below the span, no vertical clipping is required.
2996 return positionY;
2997 }
2998 }
2999
3000 private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
3001 // 3 handles
3002 // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003003 // 1 CursorAnchorInfoNotifier
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003004 private static final int MAXIMUM_NUMBER_OF_LISTENERS = 7;
Gilles Debunned88876a2012-03-16 17:34:04 -07003005 private TextViewPositionListener[] mPositionListeners =
3006 new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003007 private boolean[] mCanMove = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
Gilles Debunned88876a2012-03-16 17:34:04 -07003008 private boolean mPositionHasChanged = true;
3009 // Absolute position of the TextView with respect to its parent window
3010 private int mPositionX, mPositionY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003011 private int mPositionXOnScreen, mPositionYOnScreen;
Gilles Debunned88876a2012-03-16 17:34:04 -07003012 private int mNumberOfListeners;
3013 private boolean mScrollHasChanged;
3014 final int[] mTempCoords = new int[2];
3015
3016 public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
3017 if (mNumberOfListeners == 0) {
3018 updatePosition();
3019 ViewTreeObserver vto = mTextView.getViewTreeObserver();
3020 vto.addOnPreDrawListener(this);
3021 }
3022
3023 int emptySlotIndex = -1;
3024 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3025 TextViewPositionListener listener = mPositionListeners[i];
3026 if (listener == positionListener) {
3027 return;
3028 } else if (emptySlotIndex < 0 && listener == null) {
3029 emptySlotIndex = i;
3030 }
3031 }
3032
3033 mPositionListeners[emptySlotIndex] = positionListener;
3034 mCanMove[emptySlotIndex] = canMove;
3035 mNumberOfListeners++;
3036 }
3037
3038 public void removeSubscriber(TextViewPositionListener positionListener) {
3039 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3040 if (mPositionListeners[i] == positionListener) {
3041 mPositionListeners[i] = null;
3042 mNumberOfListeners--;
3043 break;
3044 }
3045 }
3046
3047 if (mNumberOfListeners == 0) {
3048 ViewTreeObserver vto = mTextView.getViewTreeObserver();
3049 vto.removeOnPreDrawListener(this);
3050 }
3051 }
3052
3053 public int getPositionX() {
3054 return mPositionX;
3055 }
3056
3057 public int getPositionY() {
3058 return mPositionY;
3059 }
3060
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003061 public int getPositionXOnScreen() {
3062 return mPositionXOnScreen;
3063 }
3064
3065 public int getPositionYOnScreen() {
3066 return mPositionYOnScreen;
3067 }
3068
Gilles Debunned88876a2012-03-16 17:34:04 -07003069 @Override
3070 public boolean onPreDraw() {
3071 updatePosition();
3072
3073 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3074 if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
3075 TextViewPositionListener positionListener = mPositionListeners[i];
3076 if (positionListener != null) {
3077 positionListener.updatePosition(mPositionX, mPositionY,
3078 mPositionHasChanged, mScrollHasChanged);
3079 }
3080 }
3081 }
3082
3083 mScrollHasChanged = false;
3084 return true;
3085 }
3086
3087 private void updatePosition() {
3088 mTextView.getLocationInWindow(mTempCoords);
3089
3090 mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
3091
3092 mPositionX = mTempCoords[0];
3093 mPositionY = mTempCoords[1];
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003094
3095 mTextView.getLocationOnScreen(mTempCoords);
3096
3097 mPositionXOnScreen = mTempCoords[0];
3098 mPositionYOnScreen = mTempCoords[1];
Gilles Debunned88876a2012-03-16 17:34:04 -07003099 }
3100
3101 public void onScrollChanged() {
3102 mScrollHasChanged = true;
3103 }
3104 }
3105
3106 private abstract class PinnedPopupWindow implements TextViewPositionListener {
3107 protected PopupWindow mPopupWindow;
3108 protected ViewGroup mContentView;
3109 int mPositionX, mPositionY;
Seigo Nonaka60490d12016-01-28 17:25:18 +09003110 int mClippingLimitLeft, mClippingLimitRight;
Gilles Debunned88876a2012-03-16 17:34:04 -07003111
3112 protected abstract void createPopupWindow();
3113 protected abstract void initContentView();
3114 protected abstract int getTextOffset();
3115 protected abstract int getVerticalLocalPosition(int line);
3116 protected abstract int clipVertically(int positionY);
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003117 protected void setUp() {
3118 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003119
3120 public PinnedPopupWindow() {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003121 // Due to calling subclass methods in base constructor, subclass constructor is not
3122 // called before subclass methods, e.g. createPopupWindow or initContentView. To give
3123 // a chance to initialize subclasses, call setUp() method here.
3124 // TODO: It is good to extract non trivial initialization code from constructor.
3125 setUp();
3126
Gilles Debunned88876a2012-03-16 17:34:04 -07003127 createPopupWindow();
3128
Alan Viverette80ebe0d2015-04-30 15:53:11 -07003129 mPopupWindow.setWindowLayoutType(
3130 WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
Gilles Debunned88876a2012-03-16 17:34:04 -07003131 mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
3132 mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
3133
3134 initContentView();
3135
3136 LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
3137 ViewGroup.LayoutParams.WRAP_CONTENT);
3138 mContentView.setLayoutParams(wrapContent);
3139
3140 mPopupWindow.setContentView(mContentView);
3141 }
3142
3143 public void show() {
3144 getPositionListener().addSubscriber(this, false /* offset is fixed */);
3145
3146 computeLocalPosition();
3147
3148 final PositionListener positionListener = getPositionListener();
3149 updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
3150 }
3151
3152 protected void measureContent() {
3153 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3154 mContentView.measure(
3155 View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
3156 View.MeasureSpec.AT_MOST),
3157 View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
3158 View.MeasureSpec.AT_MOST));
3159 }
3160
3161 /* The popup window will be horizontally centered on the getTextOffset() and vertically
3162 * positioned according to viewportToContentHorizontalOffset.
3163 *
3164 * This method assumes that mContentView has properly been measured from its content. */
3165 private void computeLocalPosition() {
3166 measureContent();
3167 final int width = mContentView.getMeasuredWidth();
3168 final int offset = getTextOffset();
3169 mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f);
3170 mPositionX += mTextView.viewportToContentHorizontalOffset();
3171
3172 final int line = mTextView.getLayout().getLineForOffset(offset);
3173 mPositionY = getVerticalLocalPosition(line);
3174 mPositionY += mTextView.viewportToContentVerticalOffset();
3175 }
3176
3177 private void updatePosition(int parentPositionX, int parentPositionY) {
3178 int positionX = parentPositionX + mPositionX;
3179 int positionY = parentPositionY + mPositionY;
3180
3181 positionY = clipVertically(positionY);
3182
3183 // Horizontal clipping
3184 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3185 final int width = mContentView.getMeasuredWidth();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003186 positionX = Math.min(
3187 displayMetrics.widthPixels - width + mClippingLimitRight, positionX);
3188 positionX = Math.max(-mClippingLimitLeft, positionX);
Gilles Debunned88876a2012-03-16 17:34:04 -07003189
3190 if (isShowing()) {
3191 mPopupWindow.update(positionX, positionY, -1, -1);
3192 } else {
3193 mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
3194 positionX, positionY);
3195 }
3196 }
3197
3198 public void hide() {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09003199 if (!isShowing()) {
3200 return;
3201 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003202 mPopupWindow.dismiss();
3203 getPositionListener().removeSubscriber(this);
3204 }
3205
3206 @Override
3207 public void updatePosition(int parentPositionX, int parentPositionY,
3208 boolean parentPositionChanged, boolean parentScrolled) {
3209 // Either parentPositionChanged or parentScrolled is true, check if still visible
3210 if (isShowing() && isOffsetVisible(getTextOffset())) {
3211 if (parentScrolled) computeLocalPosition();
3212 updatePosition(parentPositionX, parentPositionY);
3213 } else {
3214 hide();
3215 }
3216 }
3217
3218 public boolean isShowing() {
3219 return mPopupWindow.isShowing();
3220 }
3221 }
3222
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003223 private static final class SuggestionInfo {
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003224 // Range of actual suggestion within mText
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003225 int mSuggestionStart, mSuggestionEnd;
3226
3227 // The SuggestionSpan that this TextView represents
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003228 final SuggestionSpanInfo mSuggestionSpanInfo = new SuggestionSpanInfo();
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003229
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003230 // The index of this suggestion inside suggestionSpan
3231 int mSuggestionIndex;
3232
3233 final SpannableStringBuilder mText = new SpannableStringBuilder();
3234
3235 void clear() {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003236 mSuggestionSpanInfo.clear();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003237 mText.clear();
3238 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003239
3240 // Utility method to set attributes about a SuggestionSpan.
3241 void setSpanInfo(SuggestionSpan span, int spanStart, int spanEnd) {
3242 mSuggestionSpanInfo.mSuggestionSpan = span;
3243 mSuggestionSpanInfo.mSpanStart = spanStart;
3244 mSuggestionSpanInfo.mSpanEnd = spanEnd;
3245 }
3246 }
3247
3248 private static final class SuggestionSpanInfo {
3249 // The SuggestionSpan;
3250 @Nullable
3251 SuggestionSpan mSuggestionSpan;
3252
3253 // The SuggestionSpan start position
3254 int mSpanStart;
3255
3256 // The SuggestionSpan end position
3257 int mSpanEnd;
3258
3259 void clear() {
3260 mSuggestionSpan = null;
3261 }
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003262 }
3263
3264 private class SuggestionHelper {
3265 private final Comparator<SuggestionSpan> mSuggestionSpanComparator =
3266 new SuggestionSpanComparator();
3267 private final HashMap<SuggestionSpan, Integer> mSpansLengths =
3268 new HashMap<SuggestionSpan, Integer>();
3269
3270 private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
3271 public int compare(SuggestionSpan span1, SuggestionSpan span2) {
3272 final int flag1 = span1.getFlags();
3273 final int flag2 = span2.getFlags();
3274 if (flag1 != flag2) {
3275 // The order here should match what is used in updateDrawState
3276 final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
3277 final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
3278 final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
3279 final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
3280 if (easy1 && !misspelled1) return -1;
3281 if (easy2 && !misspelled2) return 1;
3282 if (misspelled1) return -1;
3283 if (misspelled2) return 1;
3284 }
3285
3286 return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
3287 }
3288 }
3289
3290 /**
3291 * Returns the suggestion spans that cover the current cursor position. The suggestion
3292 * spans are sorted according to the length of text that they are attached to.
3293 */
3294 private SuggestionSpan[] getSortedSuggestionSpans() {
3295 int pos = mTextView.getSelectionStart();
3296 Spannable spannable = (Spannable) mTextView.getText();
3297 SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
3298
3299 mSpansLengths.clear();
3300 for (SuggestionSpan suggestionSpan : suggestionSpans) {
3301 int start = spannable.getSpanStart(suggestionSpan);
3302 int end = spannable.getSpanEnd(suggestionSpan);
3303 mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
3304 }
3305
3306 // The suggestions are sorted according to their types (easy correction first, then
3307 // misspelled) and to the length of the text that they cover (shorter first).
3308 Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
3309 mSpansLengths.clear();
3310
3311 return suggestionSpans;
3312 }
3313
3314 /**
3315 * Gets the SuggestionInfo list that contains suggestion information at the current cursor
3316 * position.
3317 *
3318 * @param suggestionInfos SuggestionInfo array the results will be set.
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003319 * @param misspelledSpanInfo a struct the misspelled SuggestionSpan info will be set.
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003320 * @return the number of suggestions actually fetched.
3321 */
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003322 public int getSuggestionInfo(SuggestionInfo[] suggestionInfos,
3323 @Nullable SuggestionSpanInfo misspelledSpanInfo) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003324 final Spannable spannable = (Spannable) mTextView.getText();
3325 final SuggestionSpan[] suggestionSpans = getSortedSuggestionSpans();
3326 final int nbSpans = suggestionSpans.length;
3327 if (nbSpans == 0) return 0;
3328
3329 int numberOfSuggestions = 0;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003330 for (final SuggestionSpan suggestionSpan : suggestionSpans) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003331 final int spanStart = spannable.getSpanStart(suggestionSpan);
3332 final int spanEnd = spannable.getSpanEnd(suggestionSpan);
3333
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003334 if (misspelledSpanInfo != null
3335 && (suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
3336 misspelledSpanInfo.mSuggestionSpan = suggestionSpan;
3337 misspelledSpanInfo.mSpanStart = spanStart;
3338 misspelledSpanInfo.mSpanEnd = spanEnd;
3339 }
3340
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003341 final String[] suggestions = suggestionSpan.getSuggestions();
3342 final int nbSuggestions = suggestions.length;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003343 suggestionLoop:
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003344 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
3345 final String suggestion = suggestions[suggestionIndex];
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003346 for (int i = 0; i < numberOfSuggestions; i++) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003347 final SuggestionInfo otherSuggestionInfo = suggestionInfos[i];
3348 if (otherSuggestionInfo.mText.toString().equals(suggestion)) {
3349 final int otherSpanStart =
3350 otherSuggestionInfo.mSuggestionSpanInfo.mSpanStart;
3351 final int otherSpanEnd =
3352 otherSuggestionInfo.mSuggestionSpanInfo.mSpanEnd;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003353 if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003354 continue suggestionLoop;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003355 }
3356 }
3357 }
3358
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003359 SuggestionInfo suggestionInfo = suggestionInfos[numberOfSuggestions];
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003360 suggestionInfo.setSpanInfo(suggestionSpan, spanStart, spanEnd);
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003361 suggestionInfo.mSuggestionIndex = suggestionIndex;
3362 suggestionInfo.mSuggestionStart = 0;
3363 suggestionInfo.mSuggestionEnd = suggestion.length();
3364 suggestionInfo.mText.replace(0, suggestionInfo.mText.length(), suggestion);
3365 numberOfSuggestions++;
3366 if (numberOfSuggestions >= suggestionInfos.length) {
3367 return numberOfSuggestions;
3368 }
3369 }
3370 }
3371 return numberOfSuggestions;
3372 }
3373 }
3374
Seigo Nonakaa60160b2015-08-19 12:38:35 -07003375 @VisibleForTesting
3376 public class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
Gilles Debunned88876a2012-03-16 17:34:04 -07003377 private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003378
3379 // Key of intent extras for inserting new word into user dictionary.
3380 private static final String USER_DICTIONARY_EXTRA_WORD = "word";
3381 private static final String USER_DICTIONARY_EXTRA_LOCALE = "locale";
3382
Gilles Debunned88876a2012-03-16 17:34:04 -07003383 private SuggestionInfo[] mSuggestionInfos;
3384 private int mNumberOfSuggestions;
3385 private boolean mCursorWasVisibleBeforeSuggestions;
3386 private boolean mIsShowingUp = false;
3387 private SuggestionAdapter mSuggestionsAdapter;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003388 private TextAppearanceSpan mHighlightSpan; // TODO: Make mHighlightSpan final.
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003389 private TextView mAddToDictionaryButton;
3390 private TextView mDeleteButton;
Seigo Nonakaf47976e2016-03-01 09:17:37 -08003391 private ListView mSuggestionListView;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003392 private final SuggestionSpanInfo mMisspelledSpanInfo = new SuggestionSpanInfo();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003393 private int mContainerMarginWidth;
3394 private int mContainerMarginTop;
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003395 private LinearLayout mContainerView;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003396 private Context mContext; // TODO: Make mContext final.
Gilles Debunned88876a2012-03-16 17:34:04 -07003397
3398 private class CustomPopupWindow extends PopupWindow {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003399
Gilles Debunned88876a2012-03-16 17:34:04 -07003400 @Override
3401 public void dismiss() {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09003402 if (!isShowing()) {
3403 return;
3404 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003405 super.dismiss();
Gilles Debunned88876a2012-03-16 17:34:04 -07003406 getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
3407
3408 // Safe cast since show() checks that mTextView.getText() is an Editable
3409 ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan);
3410
3411 mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions);
Keisuke Kuroyanagi4a696ac2016-02-23 11:02:07 -08003412 if (hasInsertionController() && !extractedTextModeWillBeStarted()) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003413 getInsertionController().show();
3414 }
3415 }
3416 }
3417
3418 public SuggestionsPopupWindow() {
3419 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
Gilles Debunned88876a2012-03-16 17:34:04 -07003420 }
3421
3422 @Override
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003423 protected void setUp() {
3424 mContext = applyDefaultTheme(mTextView.getContext());
3425 mHighlightSpan = new TextAppearanceSpan(mContext,
3426 mTextView.mTextEditSuggestionHighlightStyle);
3427 }
3428
3429 private Context applyDefaultTheme(Context originalContext) {
3430 TypedArray a = originalContext.obtainStyledAttributes(
3431 new int[]{com.android.internal.R.attr.isLightTheme});
3432 boolean isLightTheme = a.getBoolean(0, true);
3433 int themeId = isLightTheme ? R.style.ThemeOverlay_Material_Light
3434 : R.style.ThemeOverlay_Material_Dark;
3435 a.recycle();
3436 return new ContextThemeWrapper(originalContext, themeId);
3437 }
3438
3439 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07003440 protected void createPopupWindow() {
Seigo Nonaka3ed1b392016-01-19 13:54:59 +09003441 mPopupWindow = new CustomPopupWindow();
Gilles Debunned88876a2012-03-16 17:34:04 -07003442 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
Seigo Nonaka3ed1b392016-01-19 13:54:59 +09003443 mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
Gilles Debunned88876a2012-03-16 17:34:04 -07003444 mPopupWindow.setFocusable(true);
3445 mPopupWindow.setClippingEnabled(false);
3446 }
3447
3448 @Override
3449 protected void initContentView() {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003450 final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
3451 Context.LAYOUT_INFLATER_SERVICE);
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003452 mContentView = (ViewGroup) inflater.inflate(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003453 mTextView.mTextEditSuggestionContainerLayout, null);
Gilles Debunned88876a2012-03-16 17:34:04 -07003454
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003455 mContainerView = (LinearLayout) mContentView.findViewById(
3456 com.android.internal.R.id.suggestionWindowContainer);
Seigo Nonaka60490d12016-01-28 17:25:18 +09003457 ViewGroup.MarginLayoutParams lp =
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003458 (ViewGroup.MarginLayoutParams) mContainerView.getLayoutParams();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003459 mContainerMarginWidth = lp.leftMargin + lp.rightMargin;
3460 mContainerMarginTop = lp.topMargin;
3461 mClippingLimitLeft = lp.leftMargin;
3462 mClippingLimitRight = lp.rightMargin;
3463
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003464 mSuggestionListView = (ListView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003465 com.android.internal.R.id.suggestionContainer);
3466
3467 mSuggestionsAdapter = new SuggestionAdapter();
Seigo Nonakaf47976e2016-03-01 09:17:37 -08003468 mSuggestionListView.setAdapter(mSuggestionsAdapter);
3469 mSuggestionListView.setOnItemClickListener(this);
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003470
3471 // Inflate the suggestion items once and for all.
3472 mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS];
Gilles Debunned88876a2012-03-16 17:34:04 -07003473 for (int i = 0; i < mSuggestionInfos.length; i++) {
3474 mSuggestionInfos[i] = new SuggestionInfo();
3475 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003476
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003477 mAddToDictionaryButton = (TextView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003478 com.android.internal.R.id.addToDictionaryButton);
3479 mAddToDictionaryButton.setOnClickListener(new View.OnClickListener() {
3480 public void onClick(View v) {
Keisuke Kuroyanagi6e0860d2016-03-15 15:40:43 +09003481 final SuggestionSpan misspelledSpan =
3482 findEquivalentSuggestionSpan(mMisspelledSpanInfo);
3483 if (misspelledSpan == null) {
3484 // Span has been removed.
3485 return;
3486 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003487 final Editable editable = (Editable) mTextView.getText();
Keisuke Kuroyanagi6e0860d2016-03-15 15:40:43 +09003488 final int spanStart = editable.getSpanStart(misspelledSpan);
3489 final int spanEnd = editable.getSpanEnd(misspelledSpan);
3490 if (spanStart < 0 || spanEnd <= spanStart) {
3491 return;
3492 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003493 final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
3494
3495 final Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
3496 intent.putExtra(USER_DICTIONARY_EXTRA_WORD, originalText);
3497 intent.putExtra(USER_DICTIONARY_EXTRA_LOCALE,
3498 mTextView.getTextServicesLocale().toString());
3499 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
3500 mTextView.getContext().startActivity(intent);
3501 // There is no way to know if the word was indeed added. Re-check.
3502 // TODO The ExtractEditText should remove the span in the original text instead
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003503 editable.removeSpan(mMisspelledSpanInfo.mSuggestionSpan);
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003504 Selection.setSelection(editable, spanEnd);
3505 updateSpellCheckSpans(spanStart, spanEnd, false);
3506 hideWithCleanUp();
3507 }
3508 });
3509
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003510 mDeleteButton = (TextView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003511 com.android.internal.R.id.deleteButton);
3512 mDeleteButton.setOnClickListener(new View.OnClickListener() {
3513 public void onClick(View v) {
3514 final Editable editable = (Editable) mTextView.getText();
3515
3516 final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
3517 int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
3518 if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
3519 // Do not leave two adjacent spaces after deletion, or one at beginning of
3520 // text
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003521 if (spanUnionEnd < editable.length()
3522 && Character.isSpaceChar(editable.charAt(spanUnionEnd))
3523 && (spanUnionStart == 0
3524 || Character.isSpaceChar(
3525 editable.charAt(spanUnionStart - 1)))) {
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003526 spanUnionEnd = spanUnionEnd + 1;
3527 }
3528 mTextView.deleteText_internal(spanUnionStart, spanUnionEnd);
3529 }
3530 hideWithCleanUp();
3531 }
3532 });
3533
Gilles Debunned88876a2012-03-16 17:34:04 -07003534 }
3535
3536 public boolean isShowingUp() {
3537 return mIsShowingUp;
3538 }
3539
3540 public void onParentLostFocus() {
3541 mIsShowingUp = false;
3542 }
3543
Gilles Debunned88876a2012-03-16 17:34:04 -07003544 private class SuggestionAdapter extends BaseAdapter {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003545 private LayoutInflater mInflater = (LayoutInflater) mContext.getSystemService(
3546 Context.LAYOUT_INFLATER_SERVICE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003547
3548 @Override
3549 public int getCount() {
3550 return mNumberOfSuggestions;
3551 }
3552
3553 @Override
3554 public Object getItem(int position) {
3555 return mSuggestionInfos[position];
3556 }
3557
3558 @Override
3559 public long getItemId(int position) {
3560 return position;
3561 }
3562
3563 @Override
3564 public View getView(int position, View convertView, ViewGroup parent) {
3565 TextView textView = (TextView) convertView;
3566
3567 if (textView == null) {
3568 textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout,
3569 parent, false);
3570 }
3571
3572 final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003573 textView.setText(suggestionInfo.mText);
Gilles Debunned88876a2012-03-16 17:34:04 -07003574 return textView;
3575 }
3576 }
3577
Seigo Nonakaa60160b2015-08-19 12:38:35 -07003578 @VisibleForTesting
3579 public ViewGroup getContentViewForTesting() {
3580 return mContentView;
3581 }
3582
Gilles Debunned88876a2012-03-16 17:34:04 -07003583 @Override
3584 public void show() {
3585 if (!(mTextView.getText() instanceof Editable)) return;
Keisuke Kuroyanagi4a696ac2016-02-23 11:02:07 -08003586 if (extractedTextModeWillBeStarted()) {
3587 return;
3588 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003589
3590 if (updateSuggestions()) {
3591 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
3592 mTextView.setCursorVisible(false);
3593 mIsShowingUp = true;
3594 super.show();
3595 }
3596 }
3597
3598 @Override
3599 protected void measureContent() {
3600 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3601 final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
3602 displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
3603 final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
3604 displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
3605
3606 int width = 0;
3607 View view = null;
3608 for (int i = 0; i < mNumberOfSuggestions; i++) {
3609 view = mSuggestionsAdapter.getView(i, view, mContentView);
3610 view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
3611 view.measure(horizontalMeasure, verticalMeasure);
3612 width = Math.max(width, view.getMeasuredWidth());
3613 }
3614
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003615 if (mAddToDictionaryButton.getVisibility() != View.GONE) {
3616 mAddToDictionaryButton.measure(horizontalMeasure, verticalMeasure);
3617 width = Math.max(width, mAddToDictionaryButton.getMeasuredWidth());
3618 }
3619
3620 mDeleteButton.measure(horizontalMeasure, verticalMeasure);
3621 width = Math.max(width, mDeleteButton.getMeasuredWidth());
3622
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003623 width += mContainerView.getPaddingLeft() + mContainerView.getPaddingRight()
3624 + mContainerMarginWidth;
Seigo Nonaka60490d12016-01-28 17:25:18 +09003625
Gilles Debunned88876a2012-03-16 17:34:04 -07003626 // Enforce the width based on actual text widths
3627 mContentView.measure(
3628 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
3629 verticalMeasure);
3630
3631 Drawable popupBackground = mPopupWindow.getBackground();
3632 if (popupBackground != null) {
3633 if (mTempRect == null) mTempRect = new Rect();
3634 popupBackground.getPadding(mTempRect);
3635 width += mTempRect.left + mTempRect.right;
3636 }
3637 mPopupWindow.setWidth(width);
3638 }
3639
3640 @Override
3641 protected int getTextOffset() {
Keisuke Kuroyanagi713be062016-02-29 16:07:54 -08003642 return (mTextView.getSelectionStart() + mTextView.getSelectionStart()) / 2;
Gilles Debunned88876a2012-03-16 17:34:04 -07003643 }
3644
3645 @Override
3646 protected int getVerticalLocalPosition(int line) {
Siyamed Sinira60b59d2017-07-26 09:26:41 -07003647 final Layout layout = mTextView.getLayout();
3648 return layout.getLineBottomWithoutSpacing(line) - mContainerMarginTop;
Gilles Debunned88876a2012-03-16 17:34:04 -07003649 }
3650
3651 @Override
3652 protected int clipVertically(int positionY) {
3653 final int height = mContentView.getMeasuredHeight();
3654 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3655 return Math.min(positionY, displayMetrics.heightPixels - height);
3656 }
3657
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003658 private void hideWithCleanUp() {
3659 for (final SuggestionInfo info : mSuggestionInfos) {
3660 info.clear();
3661 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003662 mMisspelledSpanInfo.clear();
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003663 hide();
Gilles Debunned88876a2012-03-16 17:34:04 -07003664 }
3665
3666 private boolean updateSuggestions() {
3667 Spannable spannable = (Spannable) mTextView.getText();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003668 mNumberOfSuggestions =
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003669 mSuggestionHelper.getSuggestionInfo(mSuggestionInfos, mMisspelledSpanInfo);
3670 if (mNumberOfSuggestions == 0 && mMisspelledSpanInfo.mSuggestionSpan == null) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003671 return false;
3672 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003673
Gilles Debunned88876a2012-03-16 17:34:04 -07003674 int spanUnionStart = mTextView.getText().length();
3675 int spanUnionEnd = 0;
3676
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003677 for (int i = 0; i < mNumberOfSuggestions; i++) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003678 final SuggestionSpanInfo spanInfo = mSuggestionInfos[i].mSuggestionSpanInfo;
3679 spanUnionStart = Math.min(spanUnionStart, spanInfo.mSpanStart);
3680 spanUnionEnd = Math.max(spanUnionEnd, spanInfo.mSpanEnd);
3681 }
3682 if (mMisspelledSpanInfo.mSuggestionSpan != null) {
3683 spanUnionStart = Math.min(spanUnionStart, mMisspelledSpanInfo.mSpanStart);
3684 spanUnionEnd = Math.max(spanUnionEnd, mMisspelledSpanInfo.mSpanEnd);
Gilles Debunned88876a2012-03-16 17:34:04 -07003685 }
3686
3687 for (int i = 0; i < mNumberOfSuggestions; i++) {
3688 highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
3689 }
3690
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003691 // Make "Add to dictionary" item visible if there is a span with the misspelled flag
3692 int addToDictionaryButtonVisibility = View.GONE;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003693 if (mMisspelledSpanInfo.mSuggestionSpan != null) {
3694 if (mMisspelledSpanInfo.mSpanStart >= 0
3695 && mMisspelledSpanInfo.mSpanEnd > mMisspelledSpanInfo.mSpanStart) {
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003696 addToDictionaryButtonVisibility = View.VISIBLE;
Gilles Debunned88876a2012-03-16 17:34:04 -07003697 }
3698 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003699 mAddToDictionaryButton.setVisibility(addToDictionaryButtonVisibility);
Gilles Debunned88876a2012-03-16 17:34:04 -07003700
3701 if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003702 final int underlineColor;
3703 if (mNumberOfSuggestions != 0) {
3704 underlineColor =
3705 mSuggestionInfos[0].mSuggestionSpanInfo.mSuggestionSpan.getUnderlineColor();
3706 } else {
3707 underlineColor = mMisspelledSpanInfo.mSuggestionSpan.getUnderlineColor();
3708 }
3709
Gilles Debunned88876a2012-03-16 17:34:04 -07003710 if (underlineColor == 0) {
3711 // Fallback on the default highlight color when the first span does not provide one
3712 mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor);
3713 } else {
3714 final float BACKGROUND_TRANSPARENCY = 0.4f;
3715 final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
3716 mSuggestionRangeSpan.setBackgroundColor(
3717 (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
3718 }
3719 spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
3720 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
3721
3722 mSuggestionsAdapter.notifyDataSetChanged();
3723 return true;
3724 }
3725
3726 private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
3727 int unionEnd) {
3728 final Spannable text = (Spannable) mTextView.getText();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003729 final int spanStart = suggestionInfo.mSuggestionSpanInfo.mSpanStart;
3730 final int spanEnd = suggestionInfo.mSuggestionSpanInfo.mSpanEnd;
Gilles Debunned88876a2012-03-16 17:34:04 -07003731
3732 // Adjust the start/end of the suggestion span
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003733 suggestionInfo.mSuggestionStart = spanStart - unionStart;
3734 suggestionInfo.mSuggestionEnd = suggestionInfo.mSuggestionStart
3735 + suggestionInfo.mText.length();
Gilles Debunned88876a2012-03-16 17:34:04 -07003736
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003737 suggestionInfo.mText.setSpan(mHighlightSpan, 0, suggestionInfo.mText.length(),
Seigo Nonakabffbd302015-08-18 18:27:56 -07003738 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003739
3740 // Add the text before and after the span.
3741 final String textAsString = text.toString();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003742 suggestionInfo.mText.insert(0, textAsString.substring(unionStart, spanStart));
3743 suggestionInfo.mText.append(textAsString.substring(spanEnd, unionEnd));
Gilles Debunned88876a2012-03-16 17:34:04 -07003744 }
3745
3746 @Override
3747 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003748 SuggestionInfo suggestionInfo = mSuggestionInfos[position];
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003749 replaceWithSuggestion(suggestionInfo);
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003750 hideWithCleanUp();
Gilles Debunned88876a2012-03-16 17:34:04 -07003751 }
3752 }
3753
3754 /**
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003755 * An ActionMode Callback class that is used to provide actions while in text insertion or
3756 * selection mode.
Gilles Debunned88876a2012-03-16 17:34:04 -07003757 *
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003758 * The default callback provides a subset of Select All, Cut, Copy, Paste, Share and Replace
3759 * actions, depending on which of these this TextView supports and the current selection.
Gilles Debunned88876a2012-03-16 17:34:04 -07003760 */
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003761 private class TextActionModeCallback extends ActionMode.Callback2 {
Clara Bayarriea4f1502015-03-18 00:25:01 +00003762 private final Path mSelectionPath = new Path();
3763 private final RectF mSelectionBounds = new RectF();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003764 private final boolean mHasSelection;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003765 private final int mHandleHeight;
Clara Bayarriea4f1502015-03-18 00:25:01 +00003766
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003767 public TextActionModeCallback(boolean hasSelection) {
3768 mHasSelection = hasSelection;
3769 if (mHasSelection) {
3770 SelectionModifierCursorController selectionController = getSelectionController();
3771 if (selectionController.mStartHandle == null) {
3772 // As these are for initializing selectionController, hide() must be called.
3773 selectionController.initDrawables();
3774 selectionController.initHandles();
3775 selectionController.hide();
3776 }
3777 mHandleHeight = Math.max(
3778 mSelectHandleLeft.getMinimumHeight(),
3779 mSelectHandleRight.getMinimumHeight());
3780 } else {
3781 InsertionPointCursorController insertionController = getInsertionController();
3782 if (insertionController != null) {
3783 insertionController.getHandle();
3784 mHandleHeight = mSelectHandleCenter.getMinimumHeight();
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003785 } else {
3786 mHandleHeight = 0;
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003787 }
Clara Bayarri7fc946e2015-03-31 14:48:33 +01003788 }
Clara Bayarriea4f1502015-03-18 00:25:01 +00003789 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003790
3791 @Override
3792 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003793 mode.setTitle(null);
Clara Bayarri13152d12015-04-09 12:02:04 +01003794 mode.setSubtitle(null);
3795 mode.setTitleOptionalHint(true);
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003796 populateMenuWithItems(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01003797
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003798 Callback customCallback = getCustomCallback();
3799 if (customCallback != null) {
3800 if (!customCallback.onCreateActionMode(mode, menu)) {
Clara Bayarri01243ac2015-06-03 00:46:29 +01003801 // The custom mode can choose to cancel the action mode, dismiss selection.
3802 Selection.setSelection((Spannable) mTextView.getText(),
3803 mTextView.getSelectionEnd());
Clara Bayarri13152d12015-04-09 12:02:04 +01003804 return false;
3805 }
3806 }
3807
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07003808 if (mTextView.canProcessText()) {
3809 mProcessTextIntentActionsHandler.onInitializeMenu(menu);
3810 }
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +00003811
Clara Bayarri13152d12015-04-09 12:02:04 +01003812 if (menu.hasVisibleItems() || mode.getCustomView() != null) {
Keisuke Kuroyanagi183fd502016-04-01 15:00:53 +09003813 if (mHasSelection && !mTextView.hasTransientState()) {
3814 mTextView.setHasTransientState(true);
3815 }
Clara Bayarri13152d12015-04-09 12:02:04 +01003816 return true;
3817 } else {
3818 return false;
3819 }
3820 }
3821
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003822 private Callback getCustomCallback() {
3823 return mHasSelection
3824 ? mCustomSelectionActionModeCallback
3825 : mCustomInsertionActionModeCallback;
3826 }
3827
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003828 private void populateMenuWithItems(Menu menu) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003829 if (mTextView.canCut()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003830 menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003831 com.android.internal.R.string.cut)
3832 .setAlphabeticShortcut('x')
3833 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07003834 }
3835
3836 if (mTextView.canCopy()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003837 menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003838 com.android.internal.R.string.copy)
3839 .setAlphabeticShortcut('c')
3840 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07003841 }
3842
3843 if (mTextView.canPaste()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003844 menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003845 com.android.internal.R.string.paste)
3846 .setAlphabeticShortcut('v')
3847 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07003848 }
3849
Andrei Stingaceanu7f0c5bd2015-04-14 17:12:08 +01003850 if (mTextView.canShare()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003851 menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003852 com.android.internal.R.string.share)
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +00003853 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
Andrei Stingaceanu7f0c5bd2015-04-14 17:12:08 +01003854 }
3855
Felipe Leme2ac463e2017-03-13 14:06:25 -07003856 if (mTextView.canRequestAutofill()) {
Felipe Leme1c1626e2017-06-02 10:53:13 -07003857 final String selected = mTextView.getSelectedText();
3858 if (selected == null || selected.isEmpty()) {
3859 menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
3860 com.android.internal.R.string.autofill)
Siyamed Sinir484c2e22017-06-07 16:26:19 -07003861 .setShowAsAction(MenuItem.SHOW_AS_OVERFLOW_ALWAYS);
Felipe Leme1c1626e2017-06-02 10:53:13 -07003862 }
Felipe Leme2ac463e2017-03-13 14:06:25 -07003863 }
3864
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01003865 if (mTextView.canPasteAsPlainText()) {
3866 menu.add(
3867 Menu.NONE,
3868 TextView.ID_PASTE_AS_PLAIN_TEXT,
3869 MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
3870 com.android.internal.R.string.paste_as_plain_text)
3871 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
3872 }
3873
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003874 updateSelectAllItem(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01003875 updateReplaceItem(menu);
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01003876 updateAssistMenuItem(menu);
Gilles Debunned88876a2012-03-16 17:34:04 -07003877 }
3878
3879 @Override
3880 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003881 updateSelectAllItem(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01003882 updateReplaceItem(menu);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08003883 updateAssistMenuItem(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01003884
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003885 Callback customCallback = getCustomCallback();
3886 if (customCallback != null) {
3887 return customCallback.onPrepareActionMode(mode, menu);
Gilles Debunned88876a2012-03-16 17:34:04 -07003888 }
3889 return true;
3890 }
3891
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003892 private void updateSelectAllItem(Menu menu) {
3893 boolean canSelectAll = mTextView.canSelectAllText();
3894 boolean selectAllItemExists = menu.findItem(TextView.ID_SELECT_ALL) != null;
3895 if (canSelectAll && !selectAllItemExists) {
3896 menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
3897 com.android.internal.R.string.selectAll)
3898 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
3899 } else if (!canSelectAll && selectAllItemExists) {
3900 menu.removeItem(TextView.ID_SELECT_ALL);
3901 }
3902 }
3903
Clara Bayarri13152d12015-04-09 12:02:04 +01003904 private void updateReplaceItem(Menu menu) {
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003905 boolean canReplace = mTextView.isSuggestionsEnabled() && shouldOfferToShowSuggestions();
Clara Bayarri13152d12015-04-09 12:02:04 +01003906 boolean replaceItemExists = menu.findItem(TextView.ID_REPLACE) != null;
3907 if (canReplace && !replaceItemExists) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003908 menu.add(Menu.NONE, TextView.ID_REPLACE, MENU_ITEM_ORDER_REPLACE,
3909 com.android.internal.R.string.replace)
3910 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
Clara Bayarri13152d12015-04-09 12:02:04 +01003911 } else if (!canReplace && replaceItemExists) {
3912 menu.removeItem(TextView.ID_REPLACE);
3913 }
3914 }
3915
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08003916 private void updateAssistMenuItem(Menu menu) {
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003917 menu.removeItem(TextView.ID_ASSIST);
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +01003918 final TextClassification textClassification =
3919 getSelectionActionModeHelper().getTextClassification();
Abodunrinwa Toki9796a1b2017-06-28 02:49:07 +01003920 if (canAssist()) {
3921 menu.add(TextView.ID_ASSIST, TextView.ID_ASSIST, MENU_ITEM_ORDER_ASSIST,
3922 textClassification.getLabel())
3923 .setIcon(textClassification.getIcon())
3924 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
3925 mMetricsLogger.write(
3926 new LogMaker(MetricsEvent.TEXT_SELECTION_MENU_ITEM_ASSIST)
3927 .setType(MetricsEvent.TYPE_OPEN)
3928 .setSubtype(textClassification.getLogType()));
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003929 }
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +00003930 }
3931
Abodunrinwa Toki9796a1b2017-06-28 02:49:07 +01003932 private boolean canAssist() {
3933 final TextClassification textClassification =
3934 getSelectionActionModeHelper().getTextClassification();
3935 return mTextView.isDeviceProvisioned()
3936 && textClassification != null
3937 && (textClassification.getIcon() != null
3938 || !TextUtils.isEmpty(textClassification.getLabel()))
3939 && (textClassification.getOnClickListener() != null
3940 || (textClassification.getIntent() != null
3941 && mTextView.getContext().canStartActivityForResult()));
3942 }
3943
Gilles Debunned88876a2012-03-16 17:34:04 -07003944 @Override
3945 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Abodunrinwa Toki1d775572017-05-08 16:03:01 +01003946 getSelectionActionModeHelper().onSelectionAction();
3947
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07003948 if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +00003949 return true;
3950 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003951 Callback customCallback = getCustomCallback();
3952 if (customCallback != null && customCallback.onActionItemClicked(mode, item)) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003953 return true;
3954 }
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +01003955 final TextClassification textClassification =
3956 getSelectionActionModeHelper().getTextClassification();
3957 if (TextView.ID_ASSIST == item.getItemId() && textClassification != null) {
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +00003958 final OnClickListener onClickListener =
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +01003959 textClassification.getOnClickListener();
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +00003960 if (onClickListener != null) {
3961 onClickListener.onClick(mTextView);
3962 } else {
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +01003963 final Intent intent = textClassification.getIntent();
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +00003964 if (intent != null) {
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +01003965 TextClassification.createStartActivityOnClickListener(
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +00003966 mTextView.getContext(), intent)
3967 .onClick(mTextView);
3968 }
3969 }
Abodunrinwa Toki54486c12017-04-19 21:02:36 +01003970 mMetricsLogger.action(
3971 MetricsEvent.ACTION_TEXT_SELECTION_MENU_ITEM_ASSIST,
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +01003972 textClassification.getLogType());
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003973 stopTextActionMode();
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +00003974 return true;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003975 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003976 return mTextView.onTextContextMenuItem(item.getItemId());
3977 }
3978
3979 @Override
3980 public void onDestroyActionMode(ActionMode mode) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09003981 // Clear mTextActionMode not to recursively destroy action mode by clearing selection.
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +00003982 getSelectionActionModeHelper().onDestroyActionMode();
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09003983 mTextActionMode = null;
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003984 Callback customCallback = getCustomCallback();
3985 if (customCallback != null) {
3986 customCallback.onDestroyActionMode(mode);
Gilles Debunned88876a2012-03-16 17:34:04 -07003987 }
Adam Powell057a5852012-05-11 10:28:38 -07003988
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08003989 if (!mPreserveSelection) {
3990 /*
3991 * Leave current selection when we tentatively destroy action mode for the
3992 * selection. If we're detaching from a window, we'll bring back the selection
3993 * mode when (if) we get reattached.
3994 */
Adam Powell057a5852012-05-11 10:28:38 -07003995 Selection.setSelection((Spannable) mTextView.getText(),
3996 mTextView.getSelectionEnd());
Adam Powell057a5852012-05-11 10:28:38 -07003997 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003998
3999 if (mSelectionModifierCursorController != null) {
4000 mSelectionModifierCursorController.hide();
4001 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004002 }
Clara Bayarriea4f1502015-03-18 00:25:01 +00004003
4004 @Override
4005 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
4006 if (!view.equals(mTextView) || mTextView.getLayout() == null) {
4007 super.onGetContentRect(mode, view, outRect);
4008 return;
4009 }
4010 if (mTextView.getSelectionStart() != mTextView.getSelectionEnd()) {
4011 // We have a selection.
4012 mSelectionPath.reset();
4013 mTextView.getLayout().getSelectionPath(
4014 mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mSelectionPath);
4015 mSelectionPath.computeBounds(mSelectionBounds, true);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004016 mSelectionBounds.bottom += mHandleHeight;
Clara Bayarriea4f1502015-03-18 00:25:01 +00004017 } else {
Roozbeh Pournader9c133072017-07-26 22:36:27 -07004018 // We have a cursor.
Siyamed Sinir987ec652016-02-17 19:44:41 -08004019 Layout layout = mTextView.getLayout();
Mady Mellorff66ca52015-07-08 12:31:45 -07004020 int line = layout.getLineForOffset(mTextView.getSelectionStart());
Siyamed Sinir987ec652016-02-17 19:44:41 -08004021 float primaryHorizontal = clampHorizontalPosition(null,
4022 layout.getPrimaryHorizontal(mTextView.getSelectionStart()));
Clara Bayarriea4f1502015-03-18 00:25:01 +00004023 mSelectionBounds.set(
4024 primaryHorizontal,
Mady Mellorff66ca52015-07-08 12:31:45 -07004025 layout.getLineTop(line),
Clara Bayarrif95ed102015-08-12 19:46:47 +01004026 primaryHorizontal,
Siyamed Sinira60b59d2017-07-26 09:26:41 -07004027 layout.getLineBottom(line) - layout.getLineBottom(line) + mHandleHeight);
Clara Bayarriea4f1502015-03-18 00:25:01 +00004028 }
4029 // Take TextView's padding and scroll into account.
4030 int textHorizontalOffset = mTextView.viewportToContentHorizontalOffset();
4031 int textVerticalOffset = mTextView.viewportToContentVerticalOffset();
4032 outRect.set(
4033 (int) Math.floor(mSelectionBounds.left + textHorizontalOffset),
4034 (int) Math.floor(mSelectionBounds.top + textVerticalOffset),
4035 (int) Math.ceil(mSelectionBounds.right + textHorizontalOffset),
4036 (int) Math.ceil(mSelectionBounds.bottom + textVerticalOffset));
4037 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004038 }
4039
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004040 /**
4041 * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}
4042 * while the input method is requesting the cursor/anchor position. Does nothing as long as
4043 * {@link InputMethodManager#isWatchingCursor(View)} returns false.
4044 */
4045 private final class CursorAnchorInfoNotifier implements TextViewPositionListener {
Yohei Yukawac46b5f02014-06-10 12:26:34 +09004046 final CursorAnchorInfo.Builder mSelectionInfoBuilder = new CursorAnchorInfo.Builder();
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004047 final int[] mTmpIntOffset = new int[2];
4048 final Matrix mViewToScreenMatrix = new Matrix();
4049
4050 @Override
4051 public void updatePosition(int parentPositionX, int parentPositionY,
4052 boolean parentPositionChanged, boolean parentScrolled) {
4053 final InputMethodState ims = mInputMethodState;
4054 if (ims == null || ims.mBatchEditNesting > 0) {
4055 return;
4056 }
4057 final InputMethodManager imm = InputMethodManager.peekInstance();
4058 if (null == imm) {
4059 return;
4060 }
Yohei Yukawa0023d0e2014-07-11 04:13:03 +09004061 if (!imm.isActive(mTextView)) {
4062 return;
4063 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004064 // Skip if the IME has not requested the cursor/anchor position.
Yohei Yukawa0023d0e2014-07-11 04:13:03 +09004065 if (!imm.isCursorAnchorInfoEnabled()) {
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004066 return;
4067 }
4068 Layout layout = mTextView.getLayout();
4069 if (layout == null) {
4070 return;
4071 }
4072
Yohei Yukawac46b5f02014-06-10 12:26:34 +09004073 final CursorAnchorInfo.Builder builder = mSelectionInfoBuilder;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004074 builder.reset();
4075
4076 final int selectionStart = mTextView.getSelectionStart();
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004077 builder.setSelectionRange(selectionStart, mTextView.getSelectionEnd());
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004078
4079 // Construct transformation matrix from view local coordinates to screen coordinates.
4080 mViewToScreenMatrix.set(mTextView.getMatrix());
4081 mTextView.getLocationOnScreen(mTmpIntOffset);
4082 mViewToScreenMatrix.postTranslate(mTmpIntOffset[0], mTmpIntOffset[1]);
4083 builder.setMatrix(mViewToScreenMatrix);
4084
4085 final float viewportToContentHorizontalOffset =
4086 mTextView.viewportToContentHorizontalOffset();
4087 final float viewportToContentVerticalOffset =
4088 mTextView.viewportToContentVerticalOffset();
4089
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004090 final CharSequence text = mTextView.getText();
4091 if (text instanceof Spannable) {
4092 final Spannable sp = (Spannable) text;
4093 int composingTextStart = EditableInputConnection.getComposingSpanStart(sp);
4094 int composingTextEnd = EditableInputConnection.getComposingSpanEnd(sp);
4095 if (composingTextEnd < composingTextStart) {
4096 final int temp = composingTextEnd;
4097 composingTextEnd = composingTextStart;
4098 composingTextStart = temp;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004099 }
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004100 final boolean hasComposingText =
4101 (0 <= composingTextStart) && (composingTextStart < composingTextEnd);
4102 if (hasComposingText) {
4103 final CharSequence composingText = text.subSequence(composingTextStart,
4104 composingTextEnd);
4105 builder.setComposingText(composingTextStart, composingText);
Phil Weaverc2e28932016-12-08 12:29:25 -08004106 mTextView.populateCharacterBounds(builder, composingTextStart,
4107 composingTextEnd, viewportToContentHorizontalOffset,
4108 viewportToContentVerticalOffset);
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004109 }
4110 }
4111
4112 // Treat selectionStart as the insertion point.
4113 if (0 <= selectionStart) {
4114 final int offset = selectionStart;
4115 final int line = layout.getLineForOffset(offset);
4116 final float insertionMarkerX = layout.getPrimaryHorizontal(offset)
4117 + viewportToContentHorizontalOffset;
4118 final float insertionMarkerTop = layout.getLineTop(line)
4119 + viewportToContentVerticalOffset;
4120 final float insertionMarkerBaseline = layout.getLineBaseline(line)
4121 + viewportToContentVerticalOffset;
Siyamed Sinira60b59d2017-07-26 09:26:41 -07004122 final float insertionMarkerBottom = layout.getLineBottomWithoutSpacing(line)
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004123 + viewportToContentVerticalOffset;
Phil Weaverc2e28932016-12-08 12:29:25 -08004124 final boolean isTopVisible = mTextView
4125 .isPositionVisible(insertionMarkerX, insertionMarkerTop);
4126 final boolean isBottomVisible = mTextView
4127 .isPositionVisible(insertionMarkerX, insertionMarkerBottom);
Yohei Yukawacc24e2b2014-08-29 20:21:10 -07004128 int insertionMarkerFlags = 0;
4129 if (isTopVisible || isBottomVisible) {
4130 insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
4131 }
4132 if (!isTopVisible || !isBottomVisible) {
4133 insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
4134 }
Yohei Yukawa5f183f02014-09-02 14:18:40 -07004135 if (layout.isRtlCharAt(offset)) {
4136 insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL;
4137 }
Yohei Yukawa0b01e7f2014-07-08 15:29:51 +09004138 builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop,
Yohei Yukawacc24e2b2014-08-29 20:21:10 -07004139 insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags);
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004140 }
4141
4142 imm.updateCursorAnchorInfo(mTextView, builder.build());
4143 }
4144 }
4145
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004146 @VisibleForTesting
4147 public abstract class HandleView extends View implements TextViewPositionListener {
Gilles Debunned88876a2012-03-16 17:34:04 -07004148 protected Drawable mDrawable;
4149 protected Drawable mDrawableLtr;
4150 protected Drawable mDrawableRtl;
4151 private final PopupWindow mContainer;
4152 // Position with respect to the parent TextView
4153 private int mPositionX, mPositionY;
4154 private boolean mIsDragging;
4155 // Offset from touch position to mPosition
4156 private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
4157 protected int mHotspotX;
Adam Powell3fceabd2014-08-19 18:28:04 -07004158 protected int mHorizontalGravity;
Gilles Debunned88876a2012-03-16 17:34:04 -07004159 // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
4160 private float mTouchOffsetY;
4161 // Where the touch position should be on the handle to ensure a maximum cursor visibility
4162 private float mIdealVerticalOffset;
4163 // Parent's (TextView) previous position in window
4164 private int mLastParentX, mLastParentY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004165 // Parent's (TextView) previous position on screen
4166 private int mLastParentXOnScreen, mLastParentYOnScreen;
Gilles Debunned88876a2012-03-16 17:34:04 -07004167 // Previous text character offset
Mady Mellorc2225b92015-04-01 15:59:20 -07004168 protected int mPreviousOffset = -1;
Gilles Debunned88876a2012-03-16 17:34:04 -07004169 // Previous text character offset
4170 private boolean mPositionHasChanged = true;
Adam Powell3fceabd2014-08-19 18:28:04 -07004171 // Minimum touch target size for handles
4172 private int mMinSize;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004173 // Indicates the line of text that the handle is on.
Mady Mellora6a0f782015-07-10 16:43:32 -07004174 protected int mPrevLine = UNSET_LINE;
4175 // Indicates the line of text that the user was touching. This can differ from mPrevLine
4176 // when selecting text when the handles jump to the end / start of words which may be on
4177 // a different line.
4178 protected int mPreviousLineTouched = UNSET_LINE;
Gilles Debunned88876a2012-03-16 17:34:04 -07004179
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004180 private HandleView(Drawable drawableLtr, Drawable drawableRtl, final int id) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004181 super(mTextView.getContext());
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004182 setId(id);
Gilles Debunned88876a2012-03-16 17:34:04 -07004183 mContainer = new PopupWindow(mTextView.getContext(), null,
4184 com.android.internal.R.attr.textSelectHandleWindowStyle);
4185 mContainer.setSplitTouchEnabled(true);
4186 mContainer.setClippingEnabled(false);
4187 mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
Keisuke Kuroyanagi7340be72015-02-27 17:57:49 +09004188 mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
4189 mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
Gilles Debunned88876a2012-03-16 17:34:04 -07004190 mContainer.setContentView(this);
4191
4192 mDrawableLtr = drawableLtr;
4193 mDrawableRtl = drawableRtl;
Adam Powell3fceabd2014-08-19 18:28:04 -07004194 mMinSize = mTextView.getContext().getResources().getDimensionPixelSize(
4195 com.android.internal.R.dimen.text_handle_min_size);
Gilles Debunned88876a2012-03-16 17:34:04 -07004196
4197 updateDrawable();
4198
Adam Powell3fceabd2014-08-19 18:28:04 -07004199 final int handleHeight = getPreferredHeight();
Gilles Debunned88876a2012-03-16 17:34:04 -07004200 mTouchOffsetY = -0.3f * handleHeight;
4201 mIdealVerticalOffset = 0.7f * handleHeight;
4202 }
4203
Mady Mellor7a936442015-05-20 10:05:52 -07004204 public float getIdealVerticalOffset() {
4205 return mIdealVerticalOffset;
4206 }
4207
Gilles Debunned88876a2012-03-16 17:34:04 -07004208 protected void updateDrawable() {
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004209 if (mIsDragging) {
4210 // Don't update drawable during dragging.
4211 return;
4212 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004213 final Layout layout = mTextView.getLayout();
4214 if (layout == null) {
4215 return;
4216 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004217 final int offset = getCurrentCursorOffset();
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004218 final boolean isRtlCharAtOffset = isAtRtlRun(layout, offset);
Keisuke Kuroyanagi33f81ac2015-05-14 20:10:57 +09004219 final Drawable oldDrawable = mDrawable;
Gilles Debunned88876a2012-03-16 17:34:04 -07004220 mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
4221 mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
Adam Powell3fceabd2014-08-19 18:28:04 -07004222 mHorizontalGravity = getHorizontalGravity(isRtlCharAtOffset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004223 if (oldDrawable != mDrawable && isShowing()) {
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004224 // Update popup window position.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004225 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
4226 - getHorizontalOffset() + getCursorOffset();
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004227 mPositionX += mTextView.viewportToContentHorizontalOffset();
4228 mPositionHasChanged = true;
4229 updatePosition(mLastParentX, mLastParentY, false, false);
Keisuke Kuroyanagi33f81ac2015-05-14 20:10:57 +09004230 postInvalidate();
4231 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004232 }
4233
4234 protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
Adam Powell3fceabd2014-08-19 18:28:04 -07004235 protected abstract int getHorizontalGravity(boolean isRtlRun);
Gilles Debunned88876a2012-03-16 17:34:04 -07004236
4237 // Touch-up filter: number of previous positions remembered
4238 private static final int HISTORY_SIZE = 5;
4239 private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
4240 private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
4241 private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
4242 private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
4243 private int mPreviousOffsetIndex = 0;
4244 private int mNumberPreviousOffsets = 0;
4245
4246 private void startTouchUpFilter(int offset) {
4247 mNumberPreviousOffsets = 0;
4248 addPositionToTouchUpFilter(offset);
4249 }
4250
4251 private void addPositionToTouchUpFilter(int offset) {
4252 mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
4253 mPreviousOffsets[mPreviousOffsetIndex] = offset;
4254 mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
4255 mNumberPreviousOffsets++;
4256 }
4257
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004258 private void filterOnTouchUp(boolean fromTouchScreen) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004259 final long now = SystemClock.uptimeMillis();
4260 int i = 0;
4261 int index = mPreviousOffsetIndex;
4262 final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
4263 while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
4264 i++;
4265 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
4266 }
4267
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004268 if (i > 0 && i < iMax
4269 && (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004270 positionAtCursorOffset(mPreviousOffsets[index], false, fromTouchScreen);
Gilles Debunned88876a2012-03-16 17:34:04 -07004271 }
4272 }
4273
4274 public boolean offsetHasBeenChanged() {
4275 return mNumberPreviousOffsets > 1;
4276 }
4277
4278 @Override
4279 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Adam Powell3fceabd2014-08-19 18:28:04 -07004280 setMeasuredDimension(getPreferredWidth(), getPreferredHeight());
4281 }
4282
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004283 @Override
4284 public void invalidate() {
4285 super.invalidate();
4286 if (isShowing()) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004287 positionAtCursorOffset(getCurrentCursorOffset(), true, false);
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004288 }
4289 };
4290
Adam Powell3fceabd2014-08-19 18:28:04 -07004291 private int getPreferredWidth() {
4292 return Math.max(mDrawable.getIntrinsicWidth(), mMinSize);
4293 }
4294
4295 private int getPreferredHeight() {
4296 return Math.max(mDrawable.getIntrinsicHeight(), mMinSize);
Gilles Debunned88876a2012-03-16 17:34:04 -07004297 }
4298
4299 public void show() {
4300 if (isShowing()) return;
4301
4302 getPositionListener().addSubscriber(this, true /* local position may change */);
4303
4304 // Make sure the offset is always considered new, even when focusing at same position
4305 mPreviousOffset = -1;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004306 positionAtCursorOffset(getCurrentCursorOffset(), false, false);
Gilles Debunned88876a2012-03-16 17:34:04 -07004307 }
4308
4309 protected void dismiss() {
4310 mIsDragging = false;
4311 mContainer.dismiss();
4312 onDetached();
4313 }
4314
4315 public void hide() {
4316 dismiss();
4317
4318 getPositionListener().removeSubscriber(this);
4319 }
4320
Gilles Debunned88876a2012-03-16 17:34:04 -07004321 public boolean isShowing() {
4322 return mContainer.isShowing();
4323 }
4324
4325 private boolean isVisible() {
4326 // Always show a dragging handle.
4327 if (mIsDragging) {
4328 return true;
4329 }
4330
4331 if (mTextView.isInBatchEditMode()) {
4332 return false;
4333 }
4334
Phil Weaverc2e28932016-12-08 12:29:25 -08004335 return mTextView.isPositionVisible(
4336 mPositionX + mHotspotX + getHorizontalOffset(), mPositionY);
Gilles Debunned88876a2012-03-16 17:34:04 -07004337 }
4338
4339 public abstract int getCurrentCursorOffset();
4340
4341 protected abstract void updateSelection(int offset);
4342
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004343 protected abstract void updatePosition(float x, float y, boolean fromTouchScreen);
Gilles Debunned88876a2012-03-16 17:34:04 -07004344
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004345 protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
4346 return layout.isRtlCharAt(offset);
4347 }
4348
4349 @VisibleForTesting
4350 public float getHorizontal(@NonNull Layout layout, int offset) {
4351 return layout.getPrimaryHorizontal(offset);
4352 }
4353
4354 protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
4355 return mTextView.getOffsetAtCoordinate(line, x);
4356 }
4357
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004358 /**
4359 * @param offset Cursor offset. Must be in [-1, length].
4360 * @param forceUpdatePosition whether to force update the position. This should be true
4361 * when If the parent has been scrolled, for example.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004362 * @param fromTouchScreen {@code true} if the cursor is moved with motion events from the
4363 * touch screen.
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004364 */
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004365 protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
4366 boolean fromTouchScreen) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004367 // A HandleView relies on the layout, which may be nulled by external methods
4368 Layout layout = mTextView.getLayout();
4369 if (layout == null) {
4370 // Will update controllers' state, hiding them and stopping selection mode if needed
4371 prepareCursorControllers();
4372 return;
4373 }
Siyamed Sinir987ec652016-02-17 19:44:41 -08004374 layout = mTextView.getLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -07004375
4376 boolean offsetChanged = offset != mPreviousOffset;
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004377 if (offsetChanged || forceUpdatePosition) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004378 if (offsetChanged) {
4379 updateSelection(offset);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004380 if (fromTouchScreen && mHapticTextHandleEnabled) {
4381 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
4382 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004383 addPositionToTouchUpFilter(offset);
4384 }
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07004385 final int line = layout.getLineForOffset(offset);
Mady Mellorb9bbbb12015-03-23 11:50:46 -07004386 mPrevLine = line;
Gilles Debunned88876a2012-03-16 17:34:04 -07004387
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004388 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
4389 - getHorizontalOffset() + getCursorOffset();
Siyamed Sinira60b59d2017-07-26 09:26:41 -07004390 mPositionY = layout.getLineBottomWithoutSpacing(line);
Gilles Debunned88876a2012-03-16 17:34:04 -07004391
4392 // Take TextView's padding and scroll into account.
4393 mPositionX += mTextView.viewportToContentHorizontalOffset();
4394 mPositionY += mTextView.viewportToContentVerticalOffset();
4395
4396 mPreviousOffset = offset;
4397 mPositionHasChanged = true;
4398 }
4399 }
4400
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004401 /**
Roozbeh Pournader9c133072017-07-26 22:36:27 -07004402 * Return the clamped horizontal position for the cursor.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004403 *
4404 * @param layout Text layout.
4405 * @param offset Character offset for the cursor.
4406 * @return The clamped horizontal position for the cursor.
4407 */
4408 int getCursorHorizontalPosition(Layout layout, int offset) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004409 return (int) (getHorizontal(layout, offset) - 0.5f);
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004410 }
4411
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004412 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07004413 public void updatePosition(int parentPositionX, int parentPositionY,
4414 boolean parentPositionChanged, boolean parentScrolled) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004415 positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled, false);
Gilles Debunned88876a2012-03-16 17:34:04 -07004416 if (parentPositionChanged || mPositionHasChanged) {
4417 if (mIsDragging) {
4418 // Update touchToWindow offset in case of parent scrolling while dragging
4419 if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
4420 mTouchToWindowOffsetX += parentPositionX - mLastParentX;
4421 mTouchToWindowOffsetY += parentPositionY - mLastParentY;
4422 mLastParentX = parentPositionX;
4423 mLastParentY = parentPositionY;
4424 }
4425
4426 onHandleMoved();
4427 }
4428
4429 if (isVisible()) {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004430 // Transform to the window coordinates to follow the view tranformation.
4431 final int[] pts = { mPositionX + mHotspotX + getHorizontalOffset(), mPositionY};
4432 mTextView.transformFromViewToWindowSpace(pts);
4433 pts[0] -= mHotspotX + getHorizontalOffset();
4434
Gilles Debunned88876a2012-03-16 17:34:04 -07004435 if (isShowing()) {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004436 mContainer.update(pts[0], pts[1], -1, -1);
Gilles Debunned88876a2012-03-16 17:34:04 -07004437 } else {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004438 mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY, pts[0], pts[1]);
Gilles Debunned88876a2012-03-16 17:34:04 -07004439 }
4440 } else {
4441 if (isShowing()) {
4442 dismiss();
4443 }
4444 }
4445
4446 mPositionHasChanged = false;
4447 }
4448 }
4449
4450 @Override
4451 protected void onDraw(Canvas c) {
Adam Powell3fceabd2014-08-19 18:28:04 -07004452 final int drawWidth = mDrawable.getIntrinsicWidth();
4453 final int left = getHorizontalOffset();
4454
4455 mDrawable.setBounds(left, 0, left + drawWidth, mDrawable.getIntrinsicHeight());
Gilles Debunned88876a2012-03-16 17:34:04 -07004456 mDrawable.draw(c);
4457 }
4458
Adam Powell3fceabd2014-08-19 18:28:04 -07004459 private int getHorizontalOffset() {
4460 final int width = getPreferredWidth();
4461 final int drawWidth = mDrawable.getIntrinsicWidth();
4462 final int left;
4463 switch (mHorizontalGravity) {
4464 case Gravity.LEFT:
4465 left = 0;
4466 break;
4467 default:
4468 case Gravity.CENTER:
4469 left = (width - drawWidth) / 2;
4470 break;
4471 case Gravity.RIGHT:
4472 left = width - drawWidth;
4473 break;
4474 }
4475 return left;
4476 }
4477
4478 protected int getCursorOffset() {
4479 return 0;
4480 }
4481
Gilles Debunned88876a2012-03-16 17:34:04 -07004482 @Override
4483 public boolean onTouchEvent(MotionEvent ev) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01004484 updateFloatingToolbarVisibility(ev);
4485
Gilles Debunned88876a2012-03-16 17:34:04 -07004486 switch (ev.getActionMasked()) {
4487 case MotionEvent.ACTION_DOWN: {
4488 startTouchUpFilter(getCurrentCursorOffset());
Gilles Debunned88876a2012-03-16 17:34:04 -07004489
4490 final PositionListener positionListener = getPositionListener();
4491 mLastParentX = positionListener.getPositionX();
4492 mLastParentY = positionListener.getPositionY();
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004493 mLastParentXOnScreen = positionListener.getPositionXOnScreen();
4494 mLastParentYOnScreen = positionListener.getPositionYOnScreen();
4495
4496 final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
4497 final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
4498 mTouchToWindowOffsetX = xInWindow - mPositionX;
4499 mTouchToWindowOffsetY = yInWindow - mPositionY;
4500
Gilles Debunned88876a2012-03-16 17:34:04 -07004501 mIsDragging = true;
Mady Mellora6a0f782015-07-10 16:43:32 -07004502 mPreviousLineTouched = UNSET_LINE;
Gilles Debunned88876a2012-03-16 17:34:04 -07004503 break;
4504 }
4505
4506 case MotionEvent.ACTION_MOVE: {
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004507 final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
4508 final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
Gilles Debunned88876a2012-03-16 17:34:04 -07004509
4510 // Vertical hysteresis: vertical down movement tends to snap to ideal offset
4511 final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004512 final float currentVerticalOffset = yInWindow - mPositionY - mLastParentY;
Gilles Debunned88876a2012-03-16 17:34:04 -07004513 float newVerticalOffset;
4514 if (previousVerticalOffset < mIdealVerticalOffset) {
4515 newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
4516 newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
4517 } else {
4518 newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
4519 newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
4520 }
4521 mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
4522
Keisuke Kuroyanagibc89a5c2015-05-18 14:49:29 +09004523 final float newPosX =
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004524 xInWindow - mTouchToWindowOffsetX + mHotspotX + getHorizontalOffset();
4525 final float newPosY = yInWindow - mTouchToWindowOffsetY + mTouchOffsetY;
Gilles Debunned88876a2012-03-16 17:34:04 -07004526
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004527 updatePosition(newPosX, newPosY,
4528 ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Gilles Debunned88876a2012-03-16 17:34:04 -07004529 break;
4530 }
4531
4532 case MotionEvent.ACTION_UP:
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004533 filterOnTouchUp(ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Gilles Debunned88876a2012-03-16 17:34:04 -07004534 mIsDragging = false;
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004535 updateDrawable();
Gilles Debunned88876a2012-03-16 17:34:04 -07004536 break;
4537
4538 case MotionEvent.ACTION_CANCEL:
4539 mIsDragging = false;
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004540 updateDrawable();
Gilles Debunned88876a2012-03-16 17:34:04 -07004541 break;
4542 }
4543 return true;
4544 }
4545
4546 public boolean isDragging() {
4547 return mIsDragging;
4548 }
4549
Clara Bayarri6351e662015-03-16 23:17:59 +00004550 void onHandleMoved() {}
Gilles Debunned88876a2012-03-16 17:34:04 -07004551
Clara Bayarri6351e662015-03-16 23:17:59 +00004552 public void onDetached() {}
Gilles Debunned88876a2012-03-16 17:34:04 -07004553 }
4554
4555 private class InsertionHandleView extends HandleView {
4556 private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
4557 private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
4558
Clara Bayarrib71dddd2015-06-04 23:17:30 +01004559 // Used to detect taps on the insertion handle, which will affect the insertion action mode
Gilles Debunned88876a2012-03-16 17:34:04 -07004560 private float mDownPositionX, mDownPositionY;
4561 private Runnable mHider;
4562
4563 public InsertionHandleView(Drawable drawable) {
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004564 super(drawable, drawable, com.android.internal.R.id.insertion_handle);
Gilles Debunned88876a2012-03-16 17:34:04 -07004565 }
4566
4567 @Override
4568 public void show() {
4569 super.show();
4570
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01004571 final long durationSinceCutOrCopy =
Andrei Stingaceanu77b9c382015-05-06 13:25:19 +01004572 SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime;
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01004573
4574 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004575 if (mInsertionActionModeRunnable != null
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09004576 && ((mTapState == TAP_STATE_DOUBLE_TAP)
4577 || (mTapState == TAP_STATE_TRIPLE_CLICK)
4578 || isCursorInsideEasyCorrectionSpan())) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004579 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01004580 }
4581
4582 // Prepare and schedule the single tap runnable to run exactly after the double tap
4583 // timeout has passed.
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09004584 if ((mTapState != TAP_STATE_DOUBLE_TAP) && (mTapState != TAP_STATE_TRIPLE_CLICK)
4585 && !isCursorInsideEasyCorrectionSpan()
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01004586 && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION)) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01004587 if (mTextActionMode == null) {
4588 if (mInsertionActionModeRunnable == null) {
4589 mInsertionActionModeRunnable = new Runnable() {
4590 @Override
4591 public void run() {
4592 startInsertionActionMode();
4593 }
4594 };
4595 }
4596 mTextView.postDelayed(
4597 mInsertionActionModeRunnable,
4598 ViewConfiguration.getDoubleTapTimeout() + 1);
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01004599 }
4600
Gilles Debunned88876a2012-03-16 17:34:04 -07004601 }
4602
4603 hideAfterDelay();
4604 }
4605
Gilles Debunned88876a2012-03-16 17:34:04 -07004606 private void hideAfterDelay() {
4607 if (mHider == null) {
4608 mHider = new Runnable() {
4609 public void run() {
4610 hide();
4611 }
4612 };
4613 } else {
4614 removeHiderCallback();
4615 }
4616 mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
4617 }
4618
4619 private void removeHiderCallback() {
4620 if (mHider != null) {
4621 mTextView.removeCallbacks(mHider);
4622 }
4623 }
4624
4625 @Override
4626 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
4627 return drawable.getIntrinsicWidth() / 2;
4628 }
4629
4630 @Override
Adam Powell3fceabd2014-08-19 18:28:04 -07004631 protected int getHorizontalGravity(boolean isRtlRun) {
4632 return Gravity.CENTER_HORIZONTAL;
4633 }
4634
4635 @Override
4636 protected int getCursorOffset() {
4637 int offset = super.getCursorOffset();
Roozbeh Pournader9c133072017-07-26 22:36:27 -07004638 if (mCursorDrawable != null) {
4639 mCursorDrawable.getPadding(mTempRect);
4640 offset += (mCursorDrawable.getIntrinsicWidth()
4641 - mTempRect.left - mTempRect.right) / 2;
Adam Powell3fceabd2014-08-19 18:28:04 -07004642 }
4643 return offset;
4644 }
4645
4646 @Override
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004647 int getCursorHorizontalPosition(Layout layout, int offset) {
Roozbeh Pournader9c133072017-07-26 22:36:27 -07004648 if (mCursorDrawable != null) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004649 final float horizontal = getHorizontal(layout, offset);
Roozbeh Pournader9c133072017-07-26 22:36:27 -07004650 return clampHorizontalPosition(mCursorDrawable, horizontal) + mTempRect.left;
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004651 }
4652 return super.getCursorHorizontalPosition(layout, offset);
4653 }
4654
4655 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07004656 public boolean onTouchEvent(MotionEvent ev) {
4657 final boolean result = super.onTouchEvent(ev);
4658
4659 switch (ev.getActionMasked()) {
4660 case MotionEvent.ACTION_DOWN:
4661 mDownPositionX = ev.getRawX();
4662 mDownPositionY = ev.getRawY();
4663 break;
4664
4665 case MotionEvent.ACTION_UP:
4666 if (!offsetHasBeenChanged()) {
4667 final float deltaX = mDownPositionX - ev.getRawX();
4668 final float deltaY = mDownPositionY - ev.getRawY();
4669 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
4670
4671 final ViewConfiguration viewConfiguration = ViewConfiguration.get(
4672 mTextView.getContext());
4673 final int touchSlop = viewConfiguration.getScaledTouchSlop();
4674
4675 if (distanceSquared < touchSlop * touchSlop) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01004676 // Tapping on the handle toggles the insertion action mode.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004677 if (mTextActionMode != null) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08004678 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07004679 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004680 startInsertionActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07004681 }
4682 }
Abodunrinwa Tokibcdf0ab2015-04-25 00:11:25 +01004683 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004684 if (mTextActionMode != null) {
4685 mTextActionMode.invalidateContentRect();
Abodunrinwa Tokibcdf0ab2015-04-25 00:11:25 +01004686 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004687 }
4688 hideAfterDelay();
4689 break;
4690
4691 case MotionEvent.ACTION_CANCEL:
4692 hideAfterDelay();
4693 break;
4694
4695 default:
4696 break;
4697 }
4698
4699 return result;
4700 }
4701
4702 @Override
4703 public int getCurrentCursorOffset() {
4704 return mTextView.getSelectionStart();
4705 }
4706
4707 @Override
4708 public void updateSelection(int offset) {
4709 Selection.setSelection((Spannable) mTextView.getText(), offset);
4710 }
4711
4712 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004713 protected void updatePosition(float x, float y, boolean fromTouchScreen) {
Mady Melloree3821e2015-06-05 11:12:01 -07004714 Layout layout = mTextView.getLayout();
4715 int offset;
4716 if (layout != null) {
Mady Mellora6a0f782015-07-10 16:43:32 -07004717 if (mPreviousLineTouched == UNSET_LINE) {
4718 mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
4719 }
4720 int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004721 offset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellora6a0f782015-07-10 16:43:32 -07004722 mPreviousLineTouched = currLine;
Mady Melloree3821e2015-06-05 11:12:01 -07004723 } else {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004724 offset = -1;
Mady Melloree3821e2015-06-05 11:12:01 -07004725 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004726 positionAtCursorOffset(offset, false, fromTouchScreen);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004727 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01004728 invalidateActionMode();
Clara Bayarri1baed512015-05-11 15:29:16 +01004729 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004730 }
4731
4732 @Override
4733 void onHandleMoved() {
4734 super.onHandleMoved();
4735 removeHiderCallback();
4736 }
4737
4738 @Override
4739 public void onDetached() {
4740 super.onDetached();
4741 removeHiderCallback();
4742 }
4743 }
4744
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004745 @Retention(RetentionPolicy.SOURCE)
4746 @IntDef({HANDLE_TYPE_SELECTION_START, HANDLE_TYPE_SELECTION_END})
4747 public @interface HandleType {}
4748 public static final int HANDLE_TYPE_SELECTION_START = 0;
4749 public static final int HANDLE_TYPE_SELECTION_END = 1;
4750
Abodunrinwa Toki4a056a52017-08-05 01:56:40 +01004751 /** For selection handles */
4752 @VisibleForTesting
4753 public final class SelectionHandleView extends HandleView {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004754 // Indicates the handle type, selection start (HANDLE_TYPE_SELECTION_START) or selection
4755 // end (HANDLE_TYPE_SELECTION_END).
4756 @HandleType
4757 private final int mHandleType;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004758 // Indicates whether the cursor is making adjustments within a word.
4759 private boolean mInWord = false;
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004760 // Difference between touch position and word boundary position.
4761 private float mTouchWordDelta;
Mady Mellore264ac32015-06-22 16:46:29 -07004762 // X value of the previous updatePosition call.
4763 private float mPrevX;
4764 // Indicates if the handle has moved a boundary between LTR and RTL text.
4765 private boolean mLanguageDirectionChanged = false;
Mady Mellor42390aa2015-07-24 13:08:42 -07004766 // Distance from edge of horizontally scrolling text view
4767 // to use to switch to character mode.
4768 private final float mTextViewEdgeSlop;
4769 // Used to save text view location.
4770 private final int[] mTextViewLocation = new int[2];
Gilles Debunned88876a2012-03-16 17:34:04 -07004771
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004772 public SelectionHandleView(Drawable drawableLtr, Drawable drawableRtl, int id,
4773 @HandleType int handleType) {
4774 super(drawableLtr, drawableRtl, id);
4775 mHandleType = handleType;
4776 ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext());
Mady Mellor42390aa2015-07-24 13:08:42 -07004777 mTextViewEdgeSlop = viewConfiguration.getScaledTouchSlop() * 4;
Gilles Debunned88876a2012-03-16 17:34:04 -07004778 }
4779
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004780 private boolean isStartHandle() {
4781 return mHandleType == HANDLE_TYPE_SELECTION_START;
4782 }
4783
Gilles Debunned88876a2012-03-16 17:34:04 -07004784 @Override
4785 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004786 if (isRtlRun == isStartHandle()) {
Mady Mellor709386f2015-05-14 12:41:18 -07004787 return drawable.getIntrinsicWidth() / 4;
4788 } else {
4789 return (drawable.getIntrinsicWidth() * 3) / 4;
4790 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004791 }
4792
4793 @Override
Adam Powell3fceabd2014-08-19 18:28:04 -07004794 protected int getHorizontalGravity(boolean isRtlRun) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004795 return (isRtlRun == isStartHandle()) ? Gravity.LEFT : Gravity.RIGHT;
Adam Powell3fceabd2014-08-19 18:28:04 -07004796 }
4797
4798 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07004799 public int getCurrentCursorOffset() {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004800 return isStartHandle() ? mTextView.getSelectionStart() : mTextView.getSelectionEnd();
Gilles Debunned88876a2012-03-16 17:34:04 -07004801 }
4802
4803 @Override
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004804 protected void updateSelection(int offset) {
4805 if (isStartHandle()) {
4806 Selection.setSelection((Spannable) mTextView.getText(), offset,
4807 mTextView.getSelectionEnd());
4808 } else {
4809 Selection.setSelection((Spannable) mTextView.getText(),
4810 mTextView.getSelectionStart(), offset);
4811 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004812 updateDrawable();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004813 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01004814 invalidateActionMode();
Clara Bayarri13152d12015-04-09 12:02:04 +01004815 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004816 }
4817
4818 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004819 protected void updatePosition(float x, float y, boolean fromTouchScreen) {
Mady Mellor81fa3e82015-05-14 09:17:41 -07004820 final Layout layout = mTextView.getLayout();
Mady Mellorcc65c372015-06-17 09:25:19 -07004821 if (layout == null) {
4822 // HandleView will deal appropriately in positionAtCursorOffset when
4823 // layout is null.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004824 positionAndAdjustForCrossingHandles(mTextView.getOffsetForPosition(x, y),
4825 fromTouchScreen);
Mady Mellorcc65c372015-06-17 09:25:19 -07004826 return;
4827 }
4828
Mady Mellora6a0f782015-07-10 16:43:32 -07004829 if (mPreviousLineTouched == UNSET_LINE) {
4830 mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
4831 }
4832
Mady Mellorb9bbbb12015-03-23 11:50:46 -07004833 boolean positionCursor = false;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004834 final int anotherHandleOffset =
4835 isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
Mady Mellora6a0f782015-07-10 16:43:32 -07004836 int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004837 int initialOffset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellor81fa3e82015-05-14 09:17:41 -07004838
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004839 if (isStartHandle() && initialOffset >= anotherHandleOffset
4840 || !isStartHandle() && initialOffset <= anotherHandleOffset) {
4841 // Handles have crossed, bound it to the first selected line and
Mady Mellor81fa3e82015-05-14 09:17:41 -07004842 // adjust by word / char as normal.
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07004843 currLine = layout.getLineForOffset(anotherHandleOffset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004844 initialOffset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellor81fa3e82015-05-14 09:17:41 -07004845 }
4846
4847 int offset = initialOffset;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004848 final int wordEnd = getWordEnd(offset);
4849 final int wordStart = getWordStart(offset);
Gilles Debunned88876a2012-03-16 17:34:04 -07004850
Mady Mellore264ac32015-06-22 16:46:29 -07004851 if (mPrevX == UNSET_X_VALUE) {
4852 mPrevX = x;
4853 }
4854
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004855 final int currentOffset = getCurrentCursorOffset();
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004856 final boolean rtlAtCurrentOffset = isAtRtlRun(layout, currentOffset);
4857 final boolean atRtl = isAtRtlRun(layout, offset);
Mady Mellore264ac32015-06-22 16:46:29 -07004858 final boolean isLvlBoundary = layout.isLevelBoundary(offset);
Mady Mellore264ac32015-06-22 16:46:29 -07004859
4860 // We can't determine if the user is expanding or shrinking the selection if they're
4861 // on a bi-di boundary, so until they've moved past the boundary we'll just place
4862 // the cursor at the current position.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004863 if (isLvlBoundary || (rtlAtCurrentOffset && !atRtl) || (!rtlAtCurrentOffset && atRtl)) {
Mady Mellore264ac32015-06-22 16:46:29 -07004864 // We're on a boundary or this is the first direction change -- just update
4865 // to the current position.
4866 mLanguageDirectionChanged = true;
4867 mTouchWordDelta = 0.0f;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004868 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellore264ac32015-06-22 16:46:29 -07004869 return;
4870 } else if (mLanguageDirectionChanged && !isLvlBoundary) {
4871 // We've just moved past the boundary so update the position. After this we can
4872 // figure out if the user is expanding or shrinking to go by word or character.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004873 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellore264ac32015-06-22 16:46:29 -07004874 mTouchWordDelta = 0.0f;
4875 mLanguageDirectionChanged = false;
4876 return;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004877 }
4878
4879 boolean isExpanding;
4880 final float xDiff = x - mPrevX;
Keisuke Kuroyanagi26454142015-12-02 15:04:57 -08004881 if (isStartHandle()) {
4882 isExpanding = currLine < mPreviousLineTouched;
Mady Mellore264ac32015-06-22 16:46:29 -07004883 } else {
Keisuke Kuroyanagi26454142015-12-02 15:04:57 -08004884 isExpanding = currLine > mPreviousLineTouched;
4885 }
4886 if (atRtl == isStartHandle()) {
4887 isExpanding |= xDiff > 0;
4888 } else {
4889 isExpanding |= xDiff < 0;
Mady Mellore264ac32015-06-22 16:46:29 -07004890 }
4891
Mady Mellor42390aa2015-07-24 13:08:42 -07004892 if (mTextView.getHorizontallyScrolling()) {
4893 if (positionNearEdgeOfScrollingView(x, atRtl)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004894 && ((isStartHandle() && mTextView.getScrollX() != 0)
4895 || (!isStartHandle()
4896 && mTextView.canScrollHorizontally(atRtl ? -1 : 1)))
4897 && ((isExpanding && ((isStartHandle() && offset < currentOffset)
4898 || (!isStartHandle() && offset > currentOffset)))
4899 || !isExpanding)) {
4900 // If we're expanding ensure that the offset is actually expanding compared to
4901 // the current offset, if the handle snapped to the word, the finger position
Mady Mellor42390aa2015-07-24 13:08:42 -07004902 // may be out of sync and we don't want the selection to jump back.
4903 mTouchWordDelta = 0.0f;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004904 final int nextOffset = (atRtl == isStartHandle())
4905 ? layout.getOffsetToRightOf(mPreviousOffset)
Mady Mellor42390aa2015-07-24 13:08:42 -07004906 : layout.getOffsetToLeftOf(mPreviousOffset);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004907 positionAndAdjustForCrossingHandles(nextOffset, fromTouchScreen);
Mady Mellor42390aa2015-07-24 13:08:42 -07004908 return;
4909 }
4910 }
4911
Mady Mellore264ac32015-06-22 16:46:29 -07004912 if (isExpanding) {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004913 // User is increasing the selection.
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004914 int wordBoundary = isStartHandle() ? wordStart : wordEnd;
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07004915 final boolean snapToWord = (!mInWord
4916 || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine))
4917 && atRtl == isAtRtlRun(layout, wordBoundary);
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004918 if (snapToWord) {
Mady Mellora5266832015-06-26 14:28:12 -07004919 // Sometimes words can be broken across lines (Chinese, hyphenation).
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004920 // We still snap to the word boundary but we only use the letters on the
Mady Mellora5266832015-06-26 14:28:12 -07004921 // current line to determine if the user is far enough into the word to snap.
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07004922 if (layout.getLineForOffset(wordBoundary) != currLine) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004923 wordBoundary = isStartHandle()
4924 ? layout.getLineStart(currLine) : layout.getLineEnd(currLine);
Mady Mellora5266832015-06-26 14:28:12 -07004925 }
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004926 final int offsetThresholdToSnap = isStartHandle()
4927 ? wordEnd - ((wordEnd - wordBoundary) / 2)
4928 : wordStart + ((wordBoundary - wordStart) / 2);
4929 if (isStartHandle()
4930 && (offset <= offsetThresholdToSnap || currLine < mPrevLine)) {
4931 // User is far enough into the word or on a different line so we expand by
4932 // word.
4933 offset = wordStart;
4934 } else if (!isStartHandle()
4935 && (offset >= offsetThresholdToSnap || currLine > mPrevLine)) {
4936 // User is far enough into the word or on a different line so we expand by
4937 // word.
4938 offset = wordEnd;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004939 } else {
Mady Mellorc2225b92015-04-01 15:59:20 -07004940 offset = mPreviousOffset;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004941 }
4942 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004943 if ((isStartHandle() && offset < initialOffset)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004944 || (!isStartHandle() && offset > initialOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004945 final float adjustedX = getHorizontal(layout, offset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004946 mTouchWordDelta =
4947 mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
Keisuke Kuroyanagi50a927c2015-05-07 17:34:21 +09004948 } else {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004949 mTouchWordDelta = 0.0f;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004950 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004951 positionCursor = true;
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004952 } else {
4953 final int adjustedOffset =
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004954 getOffsetAtCoordinate(layout, currLine, x - mTouchWordDelta);
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004955 final boolean shrinking = isStartHandle()
4956 ? adjustedOffset > mPreviousOffset || currLine > mPrevLine
4957 : adjustedOffset < mPreviousOffset || currLine < mPrevLine;
4958 if (shrinking) {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004959 // User is shrinking the selection.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004960 if (currLine != mPrevLine) {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004961 // We're on a different line, so we'll snap to word boundaries.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004962 offset = isStartHandle() ? wordStart : wordEnd;
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004963 if ((isStartHandle() && offset < initialOffset)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004964 || (!isStartHandle() && offset > initialOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004965 final float adjustedX = getHorizontal(layout, offset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004966 mTouchWordDelta =
4967 mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
4968 } else {
4969 mTouchWordDelta = 0.0f;
4970 }
4971 } else {
4972 offset = adjustedOffset;
4973 }
4974 positionCursor = true;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004975 } else if ((isStartHandle() && adjustedOffset < mPreviousOffset)
4976 || (!isStartHandle() && adjustedOffset > mPreviousOffset)) {
4977 // Handle has jumped to the word boundary, and the user is moving
Mady Mellor43fd2f42015-06-08 14:03:34 -07004978 // their finger towards the handle, the delta should be updated.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004979 mTouchWordDelta = mTextView.convertToLocalHorizontalCoordinate(x)
4980 - getHorizontal(layout, mPreviousOffset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004981 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004982 }
4983
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004984 if (positionCursor) {
Mady Mellora6a0f782015-07-10 16:43:32 -07004985 mPreviousLineTouched = currLine;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004986 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004987 }
Mady Mellore264ac32015-06-22 16:46:29 -07004988 mPrevX = x;
Gilles Debunned88876a2012-03-16 17:34:04 -07004989 }
4990
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004991 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004992 protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
4993 boolean fromTouchScreen) {
4994 super.positionAtCursorOffset(offset, forceUpdatePosition, fromTouchScreen);
Yoshiki Iguchi9582e152015-10-15 13:34:41 +09004995 mInWord = (offset != -1) && !getWordIteratorWithText().isBoundary(offset);
Mady Mellor36d5a7b2015-05-22 10:31:12 -07004996 }
4997
4998 @Override
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004999 public boolean onTouchEvent(MotionEvent event) {
5000 boolean superResult = super.onTouchEvent(event);
Mady Mellora6a0f782015-07-10 16:43:32 -07005001 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
5002 // Reset the touch word offset and x value when the user
5003 // re-engages the handle.
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005004 mTouchWordDelta = 0.0f;
Mady Mellore264ac32015-06-22 16:46:29 -07005005 mPrevX = UNSET_X_VALUE;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005006 }
5007 return superResult;
5008 }
Mady Mellor42390aa2015-07-24 13:08:42 -07005009
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005010 private void positionAndAdjustForCrossingHandles(int offset, boolean fromTouchScreen) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005011 final int anotherHandleOffset =
5012 isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
5013 if ((isStartHandle() && offset >= anotherHandleOffset)
5014 || (!isStartHandle() && offset <= anotherHandleOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005015 mTouchWordDelta = 0.0f;
5016 final Layout layout = mTextView.getLayout();
5017 if (layout != null && offset != anotherHandleOffset) {
5018 final float horiz = getHorizontal(layout, offset);
5019 final float anotherHandleHoriz = getHorizontal(layout, anotherHandleOffset,
5020 !isStartHandle());
5021 final float currentHoriz = getHorizontal(layout, mPreviousOffset);
5022 if (currentHoriz < anotherHandleHoriz && horiz < anotherHandleHoriz
5023 || currentHoriz > anotherHandleHoriz && horiz > anotherHandleHoriz) {
5024 // This handle passes another one as it crossed a direction boundary.
5025 // Don't minimize the selection, but keep the handle at the run boundary.
5026 final int currentOffset = getCurrentCursorOffset();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005027 final int offsetToGetRunRange = isStartHandle()
5028 ? currentOffset : Math.max(currentOffset - 1, 0);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005029 final long range = layout.getRunRange(offsetToGetRunRange);
5030 if (isStartHandle()) {
5031 offset = TextUtils.unpackRangeStartFromLong(range);
5032 } else {
5033 offset = TextUtils.unpackRangeEndFromLong(range);
5034 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005035 positionAtCursorOffset(offset, false, fromTouchScreen);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005036 return;
5037 }
5038 }
Mady Mellor42390aa2015-07-24 13:08:42 -07005039 // Handles can not cross and selection is at least one character.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005040 offset = getNextCursorOffset(anotherHandleOffset, !isStartHandle());
Mady Mellor42390aa2015-07-24 13:08:42 -07005041 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005042 positionAtCursorOffset(offset, false, fromTouchScreen);
Mady Mellor42390aa2015-07-24 13:08:42 -07005043 }
5044
Mady Mellor42390aa2015-07-24 13:08:42 -07005045 private boolean positionNearEdgeOfScrollingView(float x, boolean atRtl) {
5046 mTextView.getLocationOnScreen(mTextViewLocation);
5047 boolean nearEdge;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005048 if (atRtl == isStartHandle()) {
Mady Mellor42390aa2015-07-24 13:08:42 -07005049 int rightEdge = mTextViewLocation[0] + mTextView.getWidth()
5050 - mTextView.getPaddingRight();
5051 nearEdge = x > rightEdge - mTextViewEdgeSlop;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005052 } else {
5053 int leftEdge = mTextViewLocation[0] + mTextView.getPaddingLeft();
5054 nearEdge = x < leftEdge + mTextViewEdgeSlop;
Mady Mellor42390aa2015-07-24 13:08:42 -07005055 }
5056 return nearEdge;
5057 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005058
5059 @Override
5060 protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
5061 final int offsetToCheck = isStartHandle() ? offset : Math.max(offset - 1, 0);
5062 return layout.isRtlCharAt(offsetToCheck);
5063 }
5064
5065 @Override
5066 public float getHorizontal(@NonNull Layout layout, int offset) {
5067 return getHorizontal(layout, offset, isStartHandle());
5068 }
5069
5070 private float getHorizontal(@NonNull Layout layout, int offset, boolean startHandle) {
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005071 final int line = layout.getLineForOffset(offset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005072 final int offsetToCheck = startHandle ? offset : Math.max(offset - 1, 0);
5073 final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
5074 final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005075 return (isRtlChar == isRtlParagraph)
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005076 ? layout.getPrimaryHorizontal(offset) : layout.getSecondaryHorizontal(offset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005077 }
5078
5079 @Override
5080 protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
Keisuke Kuroyanagib1b88652016-04-05 16:26:16 +09005081 final float localX = mTextView.convertToLocalHorizontalCoordinate(x);
5082 final int primaryOffset = layout.getOffsetForHorizontal(line, localX, true);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005083 if (!layout.isLevelBoundary(primaryOffset)) {
5084 return primaryOffset;
5085 }
Keisuke Kuroyanagib1b88652016-04-05 16:26:16 +09005086 final int secondaryOffset = layout.getOffsetForHorizontal(line, localX, false);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005087 final int currentOffset = getCurrentCursorOffset();
5088 final int primaryDiff = Math.abs(primaryOffset - currentOffset);
5089 final int secondaryDiff = Math.abs(secondaryOffset - currentOffset);
5090 if (primaryDiff < secondaryDiff) {
5091 return primaryOffset;
5092 } else if (primaryDiff > secondaryDiff) {
5093 return secondaryOffset;
5094 } else {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005095 final int offsetToCheck = isStartHandle()
5096 ? currentOffset : Math.max(currentOffset - 1, 0);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005097 final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
5098 final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
5099 return isRtlChar == isRtlParagraph ? primaryOffset : secondaryOffset;
5100 }
5101 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005102 }
5103
Mady Mellorcc65c372015-06-17 09:25:19 -07005104 private int getCurrentLineAdjustedForSlop(Layout layout, int prevLine, float y) {
Mady Mellor80679072015-07-09 16:05:36 -07005105 final int trueLine = mTextView.getLineAtCoordinate(y);
Mady Mellorcc65c372015-06-17 09:25:19 -07005106 if (layout == null || prevLine > layout.getLineCount()
5107 || layout.getLineCount() <= 0 || prevLine < 0) {
5108 // Invalid parameters, just return whatever line is at y.
Mady Mellor80679072015-07-09 16:05:36 -07005109 return trueLine;
5110 }
5111
5112 if (Math.abs(trueLine - prevLine) >= 2) {
5113 // Only stick to lines if we're within a line of the previous selection.
5114 return trueLine;
Mady Mellorcc65c372015-06-17 09:25:19 -07005115 }
5116
5117 final float verticalOffset = mTextView.viewportToContentVerticalOffset();
5118 final int lineCount = layout.getLineCount();
5119 final float slop = mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS;
5120
5121 final float firstLineTop = layout.getLineTop(0) + verticalOffset;
5122 final float prevLineTop = layout.getLineTop(prevLine) + verticalOffset;
5123 final float yTopBound = Math.max(prevLineTop - slop, firstLineTop + slop);
5124
5125 final float lastLineBottom = layout.getLineBottom(lineCount - 1) + verticalOffset;
5126 final float prevLineBottom = layout.getLineBottom(prevLine) + verticalOffset;
5127 final float yBottomBound = Math.min(prevLineBottom + slop, lastLineBottom - slop);
5128
5129 // Determine if we've moved lines based on y position and previous line.
5130 int currLine;
5131 if (y <= yTopBound) {
5132 currLine = Math.max(prevLine - 1, 0);
5133 } else if (y >= yBottomBound) {
5134 currLine = Math.min(prevLine + 1, lineCount - 1);
5135 } else {
5136 currLine = prevLine;
5137 }
5138 return currLine;
5139 }
5140
Gilles Debunned88876a2012-03-16 17:34:04 -07005141 /**
5142 * A CursorController instance can be used to control a cursor in the text.
5143 */
5144 private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
5145 /**
5146 * Makes the cursor controller visible on screen.
5147 * See also {@link #hide()}.
5148 */
5149 public void show();
5150
5151 /**
5152 * Hide the cursor controller from screen.
5153 * See also {@link #show()}.
5154 */
5155 public void hide();
5156
5157 /**
5158 * Called when the view is detached from window. Perform house keeping task, such as
5159 * stopping Runnable thread that would otherwise keep a reference on the context, thus
5160 * preventing the activity from being recycled.
5161 */
5162 public void onDetached();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005163
5164 public boolean isCursorBeingModified();
5165
5166 public boolean isActive();
Gilles Debunned88876a2012-03-16 17:34:04 -07005167 }
5168
5169 private class InsertionPointCursorController implements CursorController {
5170 private InsertionHandleView mHandle;
5171
5172 public void show() {
5173 getHandle().show();
Andrei Stingaceanu35c550c2015-05-07 16:49:49 +01005174
5175 if (mSelectionModifierCursorController != null) {
5176 mSelectionModifierCursorController.hide();
5177 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005178 }
5179
Gilles Debunned88876a2012-03-16 17:34:04 -07005180 public void hide() {
5181 if (mHandle != null) {
5182 mHandle.hide();
5183 }
5184 }
5185
5186 public void onTouchModeChanged(boolean isInTouchMode) {
5187 if (!isInTouchMode) {
5188 hide();
5189 }
5190 }
5191
5192 private InsertionHandleView getHandle() {
5193 if (mSelectHandleCenter == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08005194 mSelectHandleCenter = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07005195 mTextView.mTextSelectHandleRes);
5196 }
5197 if (mHandle == null) {
5198 mHandle = new InsertionHandleView(mSelectHandleCenter);
5199 }
5200 return mHandle;
5201 }
5202
5203 @Override
5204 public void onDetached() {
5205 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
5206 observer.removeOnTouchModeChangeListener(this);
5207
5208 if (mHandle != null) mHandle.onDetached();
5209 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005210
5211 @Override
5212 public boolean isCursorBeingModified() {
5213 return mHandle != null && mHandle.isDragging();
5214 }
5215
5216 @Override
5217 public boolean isActive() {
5218 return mHandle != null && mHandle.isShowing();
5219 }
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09005220
5221 public void invalidateHandle() {
5222 if (mHandle != null) {
5223 mHandle.invalidate();
5224 }
5225 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005226 }
5227
5228 class SelectionModifierCursorController implements CursorController {
Gilles Debunned88876a2012-03-16 17:34:04 -07005229 // The cursor controller handles, lazily created when shown.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005230 private SelectionHandleView mStartHandle;
5231 private SelectionHandleView mEndHandle;
Gilles Debunned88876a2012-03-16 17:34:04 -07005232 // The offsets of that last touch down event. Remembered to start selection there.
5233 private int mMinTouchOffset, mMaxTouchOffset;
5234
Gilles Debunned88876a2012-03-16 17:34:04 -07005235 private float mDownPositionX, mDownPositionY;
5236 private boolean mGestureStayedInTapRegion;
5237
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005238 // Where the user first starts the drag motion.
5239 private int mStartOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005240
Mady Mellor7a936442015-05-20 10:05:52 -07005241 private boolean mHaventMovedEnoughToStartDrag;
Mady Mellorf9f8aeb2015-06-17 09:46:01 -07005242 // The line that a selection happened most recently with the drag accelerator.
5243 private int mLineSelectionIsOn = -1;
5244 // Whether the drag accelerator has selected past the initial line.
5245 private boolean mSwitchedLines = false;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005246
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005247 // Indicates the drag accelerator mode that the user is currently using.
5248 private int mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
5249 // Drag accelerator is inactive.
5250 private static final int DRAG_ACCELERATOR_MODE_INACTIVE = 0;
5251 // Character based selection by dragging. Only for mouse.
5252 private static final int DRAG_ACCELERATOR_MODE_CHARACTER = 1;
5253 // Word based selection by dragging. Enabled after long pressing or double tapping.
5254 private static final int DRAG_ACCELERATOR_MODE_WORD = 2;
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005255 // Paragraph based selection by dragging. Enabled after mouse triple click.
5256 private static final int DRAG_ACCELERATOR_MODE_PARAGRAPH = 3;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005257
Gilles Debunned88876a2012-03-16 17:34:04 -07005258 SelectionModifierCursorController() {
5259 resetTouchOffsets();
5260 }
5261
5262 public void show() {
5263 if (mTextView.isInBatchEditMode()) {
5264 return;
5265 }
5266 initDrawables();
5267 initHandles();
Gilles Debunned88876a2012-03-16 17:34:04 -07005268 }
5269
5270 private void initDrawables() {
5271 if (mSelectHandleLeft == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08005272 mSelectHandleLeft = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07005273 mTextView.mTextSelectHandleLeftRes);
5274 }
5275 if (mSelectHandleRight == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08005276 mSelectHandleRight = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07005277 mTextView.mTextSelectHandleRightRes);
5278 }
5279 }
5280
5281 private void initHandles() {
5282 // Lazy object creation has to be done before updatePosition() is called.
5283 if (mStartHandle == null) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005284 mStartHandle = new SelectionHandleView(mSelectHandleLeft, mSelectHandleRight,
5285 com.android.internal.R.id.selection_start_handle,
5286 HANDLE_TYPE_SELECTION_START);
Gilles Debunned88876a2012-03-16 17:34:04 -07005287 }
5288 if (mEndHandle == null) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005289 mEndHandle = new SelectionHandleView(mSelectHandleRight, mSelectHandleLeft,
5290 com.android.internal.R.id.selection_end_handle,
5291 HANDLE_TYPE_SELECTION_END);
Gilles Debunned88876a2012-03-16 17:34:04 -07005292 }
5293
5294 mStartHandle.show();
5295 mEndHandle.show();
5296
Gilles Debunned88876a2012-03-16 17:34:04 -07005297 hideInsertionPointCursorController();
5298 }
5299
5300 public void hide() {
5301 if (mStartHandle != null) mStartHandle.hide();
5302 if (mEndHandle != null) mEndHandle.hide();
5303 }
5304
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005305 public void enterDrag(int dragAcceleratorMode) {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005306 // Just need to init the handles / hide insertion cursor.
5307 show();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005308 mDragAcceleratorMode = dragAcceleratorMode;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005309 // Start location of selection.
5310 mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX,
5311 mLastDownPositionY);
Mady Mellorf9f8aeb2015-06-17 09:46:01 -07005312 mLineSelectionIsOn = mTextView.getLineAtCoordinate(mLastDownPositionY);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005313 // Don't show the handles until user has lifted finger.
5314 hide();
5315
5316 // This stops scrolling parents from intercepting the touch event, allowing
5317 // the user to continue dragging across the screen to select text; TextView will
5318 // scroll as necessary.
5319 mTextView.getParent().requestDisallowInterceptTouchEvent(true);
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005320 mTextView.cancelLongPress();
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005321 }
5322
Gilles Debunned88876a2012-03-16 17:34:04 -07005323 public void onTouchEvent(MotionEvent event) {
5324 // This is done even when the View does not have focus, so that long presses can start
5325 // selection and tap can move cursor from this tap position.
Mady Mellor7a936442015-05-20 10:05:52 -07005326 final float eventX = event.getX();
5327 final float eventY = event.getY();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005328 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
Gilles Debunned88876a2012-03-16 17:34:04 -07005329 switch (event.getActionMasked()) {
5330 case MotionEvent.ACTION_DOWN:
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005331 if (extractedTextModeWillBeStarted()) {
5332 // Prevent duplicating the selection handles until the mode starts.
5333 hide();
5334 } else {
5335 // Remember finger down position, to be able to start selection from there.
5336 mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(
5337 eventX, eventY);
Gilles Debunned88876a2012-03-16 17:34:04 -07005338
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005339 // Double tap detection
5340 if (mGestureStayedInTapRegion) {
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09005341 if (mTapState == TAP_STATE_DOUBLE_TAP
5342 || mTapState == TAP_STATE_TRIPLE_CLICK) {
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005343 final float deltaX = eventX - mDownPositionX;
5344 final float deltaY = eventY - mDownPositionY;
5345 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005346
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005347 ViewConfiguration viewConfiguration = ViewConfiguration.get(
5348 mTextView.getContext());
5349 int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
5350 boolean stayedInArea =
5351 distanceSquared < doubleTapSlop * doubleTapSlop;
Gilles Debunned88876a2012-03-16 17:34:04 -07005352
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005353 if (stayedInArea && (isMouse || isPositionOnText(eventX, eventY))) {
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09005354 if (mTapState == TAP_STATE_DOUBLE_TAP) {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005355 selectCurrentWordAndStartDrag();
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09005356 } else if (mTapState == TAP_STATE_TRIPLE_CLICK) {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005357 selectCurrentParagraphAndStartDrag();
5358 }
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005359 mDiscardNextActionUp = true;
5360 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005361 }
5362 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005363
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005364 mDownPositionX = eventX;
5365 mDownPositionY = eventY;
5366 mGestureStayedInTapRegion = true;
5367 mHaventMovedEnoughToStartDrag = true;
5368 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005369 break;
5370
5371 case MotionEvent.ACTION_POINTER_DOWN:
5372 case MotionEvent.ACTION_POINTER_UP:
5373 // Handle multi-point gestures. Keep min and max offset positions.
5374 // Only activated for devices that correctly handle multi-touch.
5375 if (mTextView.getContext().getPackageManager().hasSystemFeature(
5376 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
5377 updateMinAndMaxOffsets(event);
5378 }
5379 break;
5380
5381 case MotionEvent.ACTION_MOVE:
Mady Mellor7a936442015-05-20 10:05:52 -07005382 final ViewConfiguration viewConfig = ViewConfiguration.get(
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005383 mTextView.getContext());
Mady Mellor7a936442015-05-20 10:05:52 -07005384 final int touchSlop = viewConfig.getScaledTouchSlop();
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005385
Mady Mellor7a936442015-05-20 10:05:52 -07005386 if (mGestureStayedInTapRegion || mHaventMovedEnoughToStartDrag) {
5387 final float deltaX = eventX - mDownPositionX;
5388 final float deltaY = eventY - mDownPositionY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005389 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
5390
Mady Mellor7a936442015-05-20 10:05:52 -07005391 if (mGestureStayedInTapRegion) {
5392 int doubleTapTouchSlop = viewConfig.getScaledDoubleTapTouchSlop();
5393 mGestureStayedInTapRegion =
5394 distanceSquared <= doubleTapTouchSlop * doubleTapTouchSlop;
5395 }
5396 if (mHaventMovedEnoughToStartDrag) {
5397 // We don't start dragging until the user has moved enough.
5398 mHaventMovedEnoughToStartDrag =
5399 distanceSquared <= touchSlop * touchSlop;
Gilles Debunned88876a2012-03-16 17:34:04 -07005400 }
5401 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005402
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005403 if (isMouse && !isDragAcceleratorActive()) {
5404 final int offset = mTextView.getOffsetForPosition(eventX, eventY);
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09005405 if (mTextView.hasSelection()
5406 && (!mHaventMovedEnoughToStartDrag || mStartOffset != offset)
5407 && offset >= mTextView.getSelectionStart()
5408 && offset <= mTextView.getSelectionEnd()) {
5409 startDragAndDrop();
5410 break;
5411 }
5412
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005413 if (mStartOffset != offset) {
5414 // Start character based drag accelerator.
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005415 stopTextActionMode();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005416 enterDrag(DRAG_ACCELERATOR_MODE_CHARACTER);
5417 mDiscardNextActionUp = true;
5418 mHaventMovedEnoughToStartDrag = false;
5419 }
5420 }
5421
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005422 if (mStartHandle != null && mStartHandle.isShowing()) {
5423 // Don't do the drag if the handles are showing already.
5424 break;
5425 }
5426
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005427 updateSelection(event);
Gilles Debunned88876a2012-03-16 17:34:04 -07005428 break;
5429
5430 case MotionEvent.ACTION_UP:
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005431 if (!isDragAcceleratorActive()) {
5432 break;
5433 }
5434 updateSelection(event);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005435
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005436 // No longer dragging to select text, let the parent intercept events.
5437 mTextView.getParent().requestDisallowInterceptTouchEvent(false);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005438
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005439 // No longer the first dragging motion, reset.
5440 resetDragAcceleratorState();
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09005441
5442 if (mTextView.hasSelection()) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01005443 // Drag selection should not be adjusted by the text classifier.
5444 startSelectionActionModeAsync(mHaventMovedEnoughToStartDrag);
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09005445 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005446 break;
5447 }
5448 }
5449
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005450 private void updateSelection(MotionEvent event) {
5451 if (mTextView.getLayout() != null) {
5452 switch (mDragAcceleratorMode) {
5453 case DRAG_ACCELERATOR_MODE_CHARACTER:
5454 updateCharacterBasedSelection(event);
5455 break;
5456 case DRAG_ACCELERATOR_MODE_WORD:
5457 updateWordBasedSelection(event);
5458 break;
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005459 case DRAG_ACCELERATOR_MODE_PARAGRAPH:
5460 updateParagraphBasedSelection(event);
5461 break;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005462 }
5463 }
5464 }
5465
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005466 /**
5467 * If the TextView allows text selection, selects the current paragraph and starts a drag.
5468 *
5469 * @return true if the drag was started.
5470 */
5471 private boolean selectCurrentParagraphAndStartDrag() {
5472 if (mInsertionActionModeRunnable != null) {
5473 mTextView.removeCallbacks(mInsertionActionModeRunnable);
5474 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005475 stopTextActionMode();
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005476 if (!selectCurrentParagraph()) {
5477 return false;
5478 }
5479 enterDrag(SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_PARAGRAPH);
5480 return true;
5481 }
5482
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005483 private void updateCharacterBasedSelection(MotionEvent event) {
5484 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005485 updateSelectionInternal(mStartOffset, offset,
5486 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005487 }
5488
5489 private void updateWordBasedSelection(MotionEvent event) {
5490 if (mHaventMovedEnoughToStartDrag) {
5491 return;
5492 }
5493 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
5494 final ViewConfiguration viewConfig = ViewConfiguration.get(
5495 mTextView.getContext());
5496 final float eventX = event.getX();
5497 final float eventY = event.getY();
5498 final int currLine;
5499 if (isMouse) {
5500 // No need to offset the y coordinate for mouse input.
5501 currLine = mTextView.getLineAtCoordinate(eventY);
5502 } else {
5503 float y = eventY;
5504 if (mSwitchedLines) {
5505 // Offset the finger by the same vertical offset as the handles.
5506 // This improves visibility of the content being selected by
5507 // shifting the finger below the content, this is applied once
5508 // the user has switched lines.
5509 final int touchSlop = viewConfig.getScaledTouchSlop();
5510 final float fingerOffset = (mStartHandle != null)
5511 ? mStartHandle.getIdealVerticalOffset()
5512 : touchSlop;
5513 y = eventY - fingerOffset;
5514 }
5515
5516 currLine = getCurrentLineAdjustedForSlop(mTextView.getLayout(), mLineSelectionIsOn,
5517 y);
5518 if (!mSwitchedLines && currLine != mLineSelectionIsOn) {
5519 // Break early here, we want to offset the finger position from
5520 // the selection highlight, once the user moved their finger
5521 // to a different line we should apply the offset and *not* switch
5522 // lines until recomputing the position with the finger offset.
5523 mSwitchedLines = true;
5524 return;
5525 }
5526 }
5527
5528 int startOffset;
5529 int offset = mTextView.getOffsetAtCoordinate(currLine, eventX);
5530 // Snap to word boundaries.
5531 if (mStartOffset < offset) {
5532 // Expanding with end handle.
5533 offset = getWordEnd(offset);
5534 startOffset = getWordStart(mStartOffset);
5535 } else {
5536 // Expanding with start handle.
5537 offset = getWordStart(offset);
5538 startOffset = getWordEnd(mStartOffset);
Keisuke Kuroyanagi133dfc02016-07-21 18:07:23 +09005539 if (startOffset == offset) {
5540 offset = getNextCursorOffset(offset, false);
5541 }
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005542 }
5543 mLineSelectionIsOn = currLine;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005544 updateSelectionInternal(startOffset, offset,
5545 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005546 }
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005547
5548 private void updateParagraphBasedSelection(MotionEvent event) {
5549 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
5550
5551 final int start = Math.min(offset, mStartOffset);
5552 final int end = Math.max(offset, mStartOffset);
5553 final long paragraphsRange = getParagraphsRange(start, end);
5554 final int selectionStart = TextUtils.unpackRangeStartFromLong(paragraphsRange);
5555 final int selectionEnd = TextUtils.unpackRangeEndFromLong(paragraphsRange);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005556 updateSelectionInternal(selectionStart, selectionEnd,
5557 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
5558 }
5559
5560 private void updateSelectionInternal(int selectionStart, int selectionEnd,
5561 boolean fromTouchScreen) {
5562 final boolean performHapticFeedback = fromTouchScreen && mHapticTextHandleEnabled
5563 && ((mTextView.getSelectionStart() != selectionStart)
5564 || (mTextView.getSelectionEnd() != selectionEnd));
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005565 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005566 if (performHapticFeedback) {
5567 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
5568 }
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005569 }
5570
Gilles Debunned88876a2012-03-16 17:34:04 -07005571 /**
5572 * @param event
5573 */
5574 private void updateMinAndMaxOffsets(MotionEvent event) {
5575 int pointerCount = event.getPointerCount();
5576 for (int index = 0; index < pointerCount; index++) {
5577 int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
5578 if (offset < mMinTouchOffset) mMinTouchOffset = offset;
5579 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
5580 }
5581 }
5582
5583 public int getMinTouchOffset() {
5584 return mMinTouchOffset;
5585 }
5586
5587 public int getMaxTouchOffset() {
5588 return mMaxTouchOffset;
5589 }
5590
5591 public void resetTouchOffsets() {
5592 mMinTouchOffset = mMaxTouchOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005593 resetDragAcceleratorState();
5594 }
5595
5596 private void resetDragAcceleratorState() {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005597 mStartOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005598 mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
Mady Mellorf9f8aeb2015-06-17 09:46:01 -07005599 mSwitchedLines = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005600 final int selectionStart = mTextView.getSelectionStart();
5601 final int selectionEnd = mTextView.getSelectionEnd();
5602 if (selectionStart > selectionEnd) {
5603 Selection.setSelection((Spannable) mTextView.getText(),
5604 selectionEnd, selectionStart);
5605 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005606 }
5607
5608 /**
5609 * @return true iff this controller is currently used to move the selection start.
5610 */
5611 public boolean isSelectionStartDragged() {
5612 return mStartHandle != null && mStartHandle.isDragging();
5613 }
5614
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005615 @Override
5616 public boolean isCursorBeingModified() {
5617 return isDragAcceleratorActive() || isSelectionStartDragged()
5618 || (mEndHandle != null && mEndHandle.isDragging());
5619 }
5620
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005621 /**
5622 * @return true if the user is selecting text using the drag accelerator.
5623 */
5624 public boolean isDragAcceleratorActive() {
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005625 return mDragAcceleratorMode != DRAG_ACCELERATOR_MODE_INACTIVE;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005626 }
5627
Gilles Debunned88876a2012-03-16 17:34:04 -07005628 public void onTouchModeChanged(boolean isInTouchMode) {
5629 if (!isInTouchMode) {
5630 hide();
5631 }
5632 }
5633
5634 @Override
5635 public void onDetached() {
5636 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
5637 observer.removeOnTouchModeChangeListener(this);
5638
5639 if (mStartHandle != null) mStartHandle.onDetached();
5640 if (mEndHandle != null) mEndHandle.onDetached();
5641 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005642
5643 @Override
5644 public boolean isActive() {
5645 return mStartHandle != null && mStartHandle.isShowing();
5646 }
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09005647
5648 public void invalidateHandles() {
5649 if (mStartHandle != null) {
5650 mStartHandle.invalidate();
5651 }
5652 if (mEndHandle != null) {
5653 mEndHandle.invalidate();
5654 }
5655 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005656 }
5657
5658 private class CorrectionHighlighter {
5659 private final Path mPath = new Path();
Chris Craik6a49dde2015-05-12 10:28:14 -07005660 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
Gilles Debunned88876a2012-03-16 17:34:04 -07005661 private int mStart, mEnd;
5662 private long mFadingStartTime;
5663 private RectF mTempRectF;
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005664 private static final int FADE_OUT_DURATION = 400;
Gilles Debunned88876a2012-03-16 17:34:04 -07005665
5666 public CorrectionHighlighter() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005667 mPaint.setCompatibilityScaling(
5668 mTextView.getResources().getCompatibilityInfo().applicationScale);
Gilles Debunned88876a2012-03-16 17:34:04 -07005669 mPaint.setStyle(Paint.Style.FILL);
5670 }
5671
5672 public void highlight(CorrectionInfo info) {
5673 mStart = info.getOffset();
5674 mEnd = mStart + info.getNewText().length();
5675 mFadingStartTime = SystemClock.uptimeMillis();
5676
5677 if (mStart < 0 || mEnd < 0) {
5678 stopAnimation();
5679 }
5680 }
5681
5682 public void draw(Canvas canvas, int cursorOffsetVertical) {
5683 if (updatePath() && updatePaint()) {
5684 if (cursorOffsetVertical != 0) {
5685 canvas.translate(0, cursorOffsetVertical);
5686 }
5687
5688 canvas.drawPath(mPath, mPaint);
5689
5690 if (cursorOffsetVertical != 0) {
5691 canvas.translate(0, -cursorOffsetVertical);
5692 }
5693 invalidate(true); // TODO invalidate cursor region only
5694 } else {
5695 stopAnimation();
5696 invalidate(false); // TODO invalidate cursor region only
5697 }
5698 }
5699
5700 private boolean updatePaint() {
5701 final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
5702 if (duration > FADE_OUT_DURATION) return false;
5703
5704 final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
5705 final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005706 final int color = (mTextView.mHighlightColor & 0x00FFFFFF)
5707 + ((int) (highlightColorAlpha * coef) << 24);
Gilles Debunned88876a2012-03-16 17:34:04 -07005708 mPaint.setColor(color);
5709 return true;
5710 }
5711
5712 private boolean updatePath() {
5713 final Layout layout = mTextView.getLayout();
5714 if (layout == null) return false;
5715
5716 // Update in case text is edited while the animation is run
5717 final int length = mTextView.getText().length();
5718 int start = Math.min(length, mStart);
5719 int end = Math.min(length, mEnd);
5720
5721 mPath.reset();
5722 layout.getSelectionPath(start, end, mPath);
5723 return true;
5724 }
5725
5726 private void invalidate(boolean delayed) {
5727 if (mTextView.getLayout() == null) return;
5728
5729 if (mTempRectF == null) mTempRectF = new RectF();
5730 mPath.computeBounds(mTempRectF, false);
5731
5732 int left = mTextView.getCompoundPaddingLeft();
5733 int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
5734
5735 if (delayed) {
5736 mTextView.postInvalidateOnAnimation(
5737 left + (int) mTempRectF.left, top + (int) mTempRectF.top,
5738 left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
5739 } else {
5740 mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
5741 (int) mTempRectF.right, (int) mTempRectF.bottom);
5742 }
5743 }
5744
5745 private void stopAnimation() {
5746 Editor.this.mCorrectionHighlighter = null;
5747 }
5748 }
5749
5750 private static class ErrorPopup extends PopupWindow {
5751 private boolean mAbove = false;
5752 private final TextView mView;
5753 private int mPopupInlineErrorBackgroundId = 0;
5754 private int mPopupInlineErrorAboveBackgroundId = 0;
5755
5756 ErrorPopup(TextView v, int width, int height) {
5757 super(v, width, height);
5758 mView = v;
5759 // 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 -08005760 // shown and positioned. Initialized with below background, which should have
Gilles Debunned88876a2012-03-16 17:34:04 -07005761 // dimensions identical to the above version for this to work (and is more likely).
5762 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
5763 com.android.internal.R.styleable.Theme_errorMessageBackground);
5764 mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
5765 }
5766
5767 void fixDirection(boolean above) {
5768 mAbove = above;
5769
5770 if (above) {
5771 mPopupInlineErrorAboveBackgroundId =
5772 getResourceId(mPopupInlineErrorAboveBackgroundId,
5773 com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
5774 } else {
5775 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
5776 com.android.internal.R.styleable.Theme_errorMessageBackground);
5777 }
5778
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005779 mView.setBackgroundResource(
5780 above ? mPopupInlineErrorAboveBackgroundId : mPopupInlineErrorBackgroundId);
Gilles Debunned88876a2012-03-16 17:34:04 -07005781 }
5782
5783 private int getResourceId(int currentId, int index) {
5784 if (currentId == 0) {
5785 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
5786 R.styleable.Theme);
5787 currentId = styledAttributes.getResourceId(index, 0);
5788 styledAttributes.recycle();
5789 }
5790 return currentId;
5791 }
5792
5793 @Override
5794 public void update(int x, int y, int w, int h, boolean force) {
5795 super.update(x, y, w, h, force);
5796
5797 boolean above = isAboveAnchor();
5798 if (above != mAbove) {
5799 fixDirection(above);
5800 }
5801 }
5802 }
5803
5804 static class InputContentType {
5805 int imeOptions = EditorInfo.IME_NULL;
5806 String privateImeOptions;
5807 CharSequence imeActionLabel;
5808 int imeActionId;
5809 Bundle extras;
5810 OnEditorActionListener onEditorActionListener;
5811 boolean enterDown;
Yohei Yukawad469f212016-01-21 12:38:09 -08005812 LocaleList imeHintLocales;
Gilles Debunned88876a2012-03-16 17:34:04 -07005813 }
5814
5815 static class InputMethodState {
Gilles Debunnec62589c2012-04-12 14:50:23 -07005816 ExtractedTextRequest mExtractedTextRequest;
5817 final ExtractedText mExtractedText = new ExtractedText();
Gilles Debunned88876a2012-03-16 17:34:04 -07005818 int mBatchEditNesting;
5819 boolean mCursorChanged;
5820 boolean mSelectionModeChanged;
5821 boolean mContentChanged;
5822 int mChangedStart, mChangedEnd, mChangedDelta;
5823 }
Satoshi Kataoka0e3849a2012-12-13 14:37:19 +09005824
James Cookf59152c2015-02-26 18:03:58 -08005825 /**
James Cook471559f2015-02-27 10:31:20 -08005826 * @return True iff (start, end) is a valid range within the text.
5827 */
5828 private static boolean isValidRange(CharSequence text, int start, int end) {
5829 return 0 <= start && start <= end && end <= text.length();
5830 }
5831
Seigo Nonakaa60160b2015-08-19 12:38:35 -07005832 @VisibleForTesting
5833 public SuggestionsPopupWindow getSuggestionsPopupWindowForTesting() {
5834 return mSuggestionsPopupWindow;
5835 }
5836
James Cook471559f2015-02-27 10:31:20 -08005837 /**
James Cookf59152c2015-02-26 18:03:58 -08005838 * An InputFilter that monitors text input to maintain undo history. It does not modify the
5839 * text being typed (and hence always returns null from the filter() method).
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005840 *
5841 * TODO: Make this span aware.
James Cookf59152c2015-02-26 18:03:58 -08005842 */
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005843 public static class UndoInputFilter implements InputFilter {
James Cookf59152c2015-02-26 18:03:58 -08005844 private final Editor mEditor;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005845
James Cook48e0fac2015-02-25 15:44:51 -08005846 // Whether the current filter pass is directly caused by an end-user text edit.
5847 private boolean mIsUserEdit;
5848
James Cookd2026682015-03-03 14:40:14 -08005849 // Whether the text field is handling an IME composition. Must be parceled in case the user
5850 // rotates the screen during composition.
5851 private boolean mHasComposition;
James Cook48e0fac2015-02-25 15:44:51 -08005852
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005853 // Whether the user is expanding or shortening the text
5854 private boolean mExpanding;
5855
5856 // Whether the previous edit operation was in the current batch edit.
5857 private boolean mPreviousOperationWasInSameBatchEdit;
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08005858
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005859 public UndoInputFilter(Editor editor) {
5860 mEditor = editor;
5861 }
5862
James Cookd2026682015-03-03 14:40:14 -08005863 public void saveInstanceState(Parcel parcel) {
5864 parcel.writeInt(mIsUserEdit ? 1 : 0);
5865 parcel.writeInt(mHasComposition ? 1 : 0);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005866 parcel.writeInt(mExpanding ? 1 : 0);
5867 parcel.writeInt(mPreviousOperationWasInSameBatchEdit ? 1 : 0);
James Cookd2026682015-03-03 14:40:14 -08005868 }
5869
5870 public void restoreInstanceState(Parcel parcel) {
5871 mIsUserEdit = parcel.readInt() != 0;
5872 mHasComposition = parcel.readInt() != 0;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005873 mExpanding = parcel.readInt() != 0;
5874 mPreviousOperationWasInSameBatchEdit = parcel.readInt() != 0;
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08005875 }
5876
James Cook48e0fac2015-02-25 15:44:51 -08005877 /**
5878 * Signals that a user-triggered edit is starting.
5879 */
5880 public void beginBatchEdit() {
5881 if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit");
5882 mIsUserEdit = true;
James Cook48e0fac2015-02-25 15:44:51 -08005883 }
5884
5885 public void endBatchEdit() {
5886 if (DEBUG_UNDO) Log.d(TAG, "endBatchEdit");
5887 mIsUserEdit = false;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005888 mPreviousOperationWasInSameBatchEdit = false;
James Cook48e0fac2015-02-25 15:44:51 -08005889 }
5890
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005891 @Override
5892 public CharSequence filter(CharSequence source, int start, int end,
5893 Spanned dest, int dstart, int dend) {
5894 if (DEBUG_UNDO) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005895 Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ") "
5896 + "dest=" + dest + " (" + dstart + "-" + dend + ")");
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005897 }
James Cookf1dad1e2015-02-27 11:00:01 -08005898
James Cook48e0fac2015-02-25 15:44:51 -08005899 // Check to see if this edit should be tracked for undo.
5900 if (!canUndoEdit(source, start, end, dest, dstart, dend)) {
James Cookf1dad1e2015-02-27 11:00:01 -08005901 return null;
5902 }
5903
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005904 final boolean hadComposition = mHasComposition;
5905 mHasComposition = isComposition(source);
5906 final boolean wasExpanding = mExpanding;
5907 boolean shouldCreateSeparateState = false;
5908 if ((end - start) != (dend - dstart)) {
5909 mExpanding = (end - start) > (dend - dstart);
5910 if (hadComposition && mExpanding != wasExpanding) {
5911 shouldCreateSeparateState = true;
5912 }
James Cookd2026682015-03-03 14:40:14 -08005913 }
5914
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005915 // Handle edit.
5916 handleEdit(source, start, end, dest, dstart, dend, shouldCreateSeparateState);
James Cookd2026682015-03-03 14:40:14 -08005917 return null;
5918 }
5919
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09005920 void freezeLastEdit() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005921 mEditor.mUndoManager.beginUpdate("Edit text");
5922 EditOperation lastEdit = getLastEdit();
5923 if (lastEdit != null) {
5924 lastEdit.mFrozen = true;
James Cookd2026682015-03-03 14:40:14 -08005925 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005926 mEditor.mUndoManager.endUpdate();
James Cookd2026682015-03-03 14:40:14 -08005927 }
5928
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005929 @Retention(RetentionPolicy.SOURCE)
5930 @IntDef({MERGE_EDIT_MODE_FORCE_MERGE, MERGE_EDIT_MODE_NEVER_MERGE, MERGE_EDIT_MODE_NORMAL})
5931 private @interface MergeMode {}
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005932 private static final int MERGE_EDIT_MODE_FORCE_MERGE = 0;
5933 private static final int MERGE_EDIT_MODE_NEVER_MERGE = 1;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005934 /** Use {@link EditOperation#mergeWith} to merge */
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005935 private static final int MERGE_EDIT_MODE_NORMAL = 2;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005936
5937 private void handleEdit(CharSequence source, int start, int end,
5938 Spanned dest, int dstart, int dend, boolean shouldCreateSeparateState) {
James Cook48e0fac2015-02-25 15:44:51 -08005939 // An application may install a TextWatcher to provide additional modifications after
5940 // the initial input filters run (e.g. a credit card formatter that adds spaces to a
5941 // string). This results in multiple filter() calls for what the user considers to be
5942 // a single operation. Always undo the whole set of changes in one step.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005943 @MergeMode
5944 final int mergeMode;
5945 if (isInTextWatcher() || mPreviousOperationWasInSameBatchEdit) {
5946 mergeMode = MERGE_EDIT_MODE_FORCE_MERGE;
5947 } else if (shouldCreateSeparateState) {
5948 mergeMode = MERGE_EDIT_MODE_NEVER_MERGE;
5949 } else {
5950 mergeMode = MERGE_EDIT_MODE_NORMAL;
5951 }
James Cook471559f2015-02-27 10:31:20 -08005952 // Build a new operation with all the information from this edit.
James Cookd2026682015-03-03 14:40:14 -08005953 String newText = TextUtils.substring(source, start, end);
5954 String oldText = TextUtils.substring(dest, dstart, dend);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005955 EditOperation edit = new EditOperation(mEditor, oldText, dstart, newText,
5956 mHasComposition);
5957 if (mHasComposition && TextUtils.equals(edit.mNewText, edit.mOldText)) {
5958 return;
5959 }
5960 recordEdit(edit, mergeMode);
James Cookd2026682015-03-03 14:40:14 -08005961 }
James Cook471559f2015-02-27 10:31:20 -08005962
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005963 private EditOperation getLastEdit() {
5964 final UndoManager um = mEditor.mUndoManager;
5965 return um.getLastOperation(
5966 EditOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE);
5967 }
James Cook22054252015-03-25 14:04:01 -07005968 /**
5969 * Fetches the last undo operation and checks to see if a new edit should be merged into it.
5970 * If forceMerge is true then the new edit is always merged.
5971 */
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005972 private void recordEdit(EditOperation edit, @MergeMode int mergeMode) {
James Cook471559f2015-02-27 10:31:20 -08005973 // Fetch the last edit operation and attempt to merge in the new edit.
James Cook48e0fac2015-02-25 15:44:51 -08005974 final UndoManager um = mEditor.mUndoManager;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005975 um.beginUpdate("Edit text");
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005976 EditOperation lastEdit = getLastEdit();
James Cook471559f2015-02-27 10:31:20 -08005977 if (lastEdit == null) {
5978 // Add this as the first edit.
5979 if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit);
5980 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005981 } else if (mergeMode == MERGE_EDIT_MODE_FORCE_MERGE) {
James Cook22054252015-03-25 14:04:01 -07005982 // Forced merges take priority because they could be the result of a non-user-edit
5983 // change and this case should not create a new undo operation.
5984 if (DEBUG_UNDO) Log.d(TAG, "filter: force merge " + edit);
5985 lastEdit.forceMergeWith(edit);
James Cook48e0fac2015-02-25 15:44:51 -08005986 } else if (!mIsUserEdit) {
5987 // An application directly modified the Editable outside of a text edit. Treat this
5988 // as a new change and don't attempt to merge.
5989 if (DEBUG_UNDO) Log.d(TAG, "non-user edit, new op " + edit);
5990 um.commitState(mEditor.mUndoOwner);
5991 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005992 } else if (mergeMode == MERGE_EDIT_MODE_NORMAL && lastEdit.mergeWith(edit)) {
James Cook471559f2015-02-27 10:31:20 -08005993 // Merge succeeded, nothing else to do.
5994 if (DEBUG_UNDO) Log.d(TAG, "filter: merge succeeded, created " + lastEdit);
James Cook3ac0bcb2015-02-26 10:53:41 -08005995 } else {
James Cook471559f2015-02-27 10:31:20 -08005996 // Could not merge with the last edit, so commit the last edit and add this edit.
5997 if (DEBUG_UNDO) Log.d(TAG, "filter: merge failed, adding " + edit);
5998 um.commitState(mEditor.mUndoOwner);
5999 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
James Cook3ac0bcb2015-02-26 10:53:41 -08006000 }
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09006001 mPreviousOperationWasInSameBatchEdit = mIsUserEdit;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006002 um.endUpdate();
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006003 }
James Cook48e0fac2015-02-25 15:44:51 -08006004
6005 private boolean canUndoEdit(CharSequence source, int start, int end,
6006 Spanned dest, int dstart, int dend) {
6007 if (!mEditor.mAllowUndo) {
6008 if (DEBUG_UNDO) Log.d(TAG, "filter: undo is disabled");
6009 return false;
6010 }
6011
6012 if (mEditor.mUndoManager.isInUndo()) {
6013 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping, currently performing undo/redo");
6014 return false;
6015 }
6016
6017 // Text filters run before input operations are applied. However, some input operations
6018 // are invalid and will throw exceptions when applied. This is common in tests. Don't
6019 // attempt to undo invalid operations.
6020 if (!isValidRange(source, start, end) || !isValidRange(dest, dstart, dend)) {
6021 if (DEBUG_UNDO) Log.d(TAG, "filter: invalid op");
6022 return false;
6023 }
6024
6025 // Earlier filters can rewrite input to be a no-op, for example due to a length limit
6026 // on an input field. Skip no-op changes.
6027 if (start == end && dstart == dend) {
6028 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping no-op");
6029 return false;
6030 }
6031
6032 return true;
6033 }
James Cookd2026682015-03-03 14:40:14 -08006034
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006035 private static boolean isComposition(CharSequence source) {
James Cookd2026682015-03-03 14:40:14 -08006036 if (!(source instanceof Spannable)) {
6037 return false;
6038 }
6039 // This is a composition edit if the source has a non-zero-length composing span.
6040 Spannable text = (Spannable) source;
6041 int composeBegin = EditableInputConnection.getComposingSpanStart(text);
6042 int composeEnd = EditableInputConnection.getComposingSpanEnd(text);
6043 return composeBegin < composeEnd;
6044 }
6045
6046 private boolean isInTextWatcher() {
6047 CharSequence text = mEditor.mTextView.getText();
6048 return (text instanceof SpannableStringBuilder)
6049 && ((SpannableStringBuilder) text).getTextWatcherDepth() > 0;
6050 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006051 }
6052
James Cookf59152c2015-02-26 18:03:58 -08006053 /**
6054 * An operation to undo a single "edit" to a text view.
6055 */
James Cook471559f2015-02-27 10:31:20 -08006056 public static class EditOperation extends UndoOperation<Editor> {
6057 private static final int TYPE_INSERT = 0;
6058 private static final int TYPE_DELETE = 1;
6059 private static final int TYPE_REPLACE = 2;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006060
James Cook471559f2015-02-27 10:31:20 -08006061 private int mType;
6062 private String mOldText;
James Cook471559f2015-02-27 10:31:20 -08006063 private String mNewText;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006064 private int mStart;
James Cook471559f2015-02-27 10:31:20 -08006065
6066 private int mOldCursorPos;
6067 private int mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006068 private boolean mFrozen;
6069 private boolean mIsComposition;
James Cook471559f2015-02-27 10:31:20 -08006070
6071 /**
James Cookd2026682015-03-03 14:40:14 -08006072 * Constructs an edit operation from a text input operation on editor that replaces the
James Cook22054252015-03-25 14:04:01 -07006073 * oldText starting at dstart with newText.
James Cook471559f2015-02-27 10:31:20 -08006074 */
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006075 public EditOperation(Editor editor, String oldText, int dstart, String newText,
6076 boolean isComposition) {
James Cook471559f2015-02-27 10:31:20 -08006077 super(editor.mUndoOwner);
James Cookd2026682015-03-03 14:40:14 -08006078 mOldText = oldText;
6079 mNewText = newText;
James Cook471559f2015-02-27 10:31:20 -08006080
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006081 // Determine the type of the edit.
James Cook471559f2015-02-27 10:31:20 -08006082 if (mNewText.length() > 0 && mOldText.length() == 0) {
6083 mType = TYPE_INSERT;
James Cook471559f2015-02-27 10:31:20 -08006084 } else if (mNewText.length() == 0 && mOldText.length() > 0) {
6085 mType = TYPE_DELETE;
James Cook471559f2015-02-27 10:31:20 -08006086 } else {
6087 mType = TYPE_REPLACE;
James Cook471559f2015-02-27 10:31:20 -08006088 }
6089
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006090 mStart = dstart;
James Cook471559f2015-02-27 10:31:20 -08006091 // Store cursor data.
6092 mOldCursorPos = editor.mTextView.getSelectionStart();
James Cookd2026682015-03-03 14:40:14 -08006093 mNewCursorPos = dstart + mNewText.length();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006094 mIsComposition = isComposition;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006095 }
6096
James Cook471559f2015-02-27 10:31:20 -08006097 public EditOperation(Parcel src, ClassLoader loader) {
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006098 super(src, loader);
James Cook471559f2015-02-27 10:31:20 -08006099 mType = src.readInt();
6100 mOldText = src.readString();
James Cook471559f2015-02-27 10:31:20 -08006101 mNewText = src.readString();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006102 mStart = src.readInt();
James Cook471559f2015-02-27 10:31:20 -08006103 mOldCursorPos = src.readInt();
6104 mNewCursorPos = src.readInt();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006105 mFrozen = src.readInt() == 1;
6106 mIsComposition = src.readInt() == 1;
James Cook471559f2015-02-27 10:31:20 -08006107 }
6108
6109 @Override
6110 public void writeToParcel(Parcel dest, int flags) {
6111 dest.writeInt(mType);
6112 dest.writeString(mOldText);
James Cook471559f2015-02-27 10:31:20 -08006113 dest.writeString(mNewText);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006114 dest.writeInt(mStart);
James Cook471559f2015-02-27 10:31:20 -08006115 dest.writeInt(mOldCursorPos);
6116 dest.writeInt(mNewCursorPos);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006117 dest.writeInt(mFrozen ? 1 : 0);
6118 dest.writeInt(mIsComposition ? 1 : 0);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006119 }
6120
James Cook48e0fac2015-02-25 15:44:51 -08006121 private int getNewTextEnd() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006122 return mStart + mNewText.length();
James Cook48e0fac2015-02-25 15:44:51 -08006123 }
6124
6125 private int getOldTextEnd() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006126 return mStart + mOldText.length();
James Cook48e0fac2015-02-25 15:44:51 -08006127 }
6128
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006129 @Override
6130 public void commit() {
6131 }
6132
6133 @Override
6134 public void undo() {
James Cook471559f2015-02-27 10:31:20 -08006135 if (DEBUG_UNDO) Log.d(TAG, "undo");
6136 // Remove the new text and insert the old.
James Cook48e0fac2015-02-25 15:44:51 -08006137 Editor editor = getOwnerData();
6138 Editable text = (Editable) editor.mTextView.getText();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006139 modifyText(text, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006140 }
6141
6142 @Override
6143 public void redo() {
James Cook471559f2015-02-27 10:31:20 -08006144 if (DEBUG_UNDO) Log.d(TAG, "redo");
6145 // Remove the old text and insert the new.
James Cook48e0fac2015-02-25 15:44:51 -08006146 Editor editor = getOwnerData();
6147 Editable text = (Editable) editor.mTextView.getText();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006148 modifyText(text, mStart, getOldTextEnd(), mNewText, mStart, mNewCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006149 }
6150
James Cook471559f2015-02-27 10:31:20 -08006151 /**
6152 * Attempts to merge this existing operation with a new edit.
6153 * @param edit The new edit operation.
6154 * @return If the merge succeeded, returns true. Otherwise returns false and leaves this
6155 * object unchanged.
6156 */
6157 private boolean mergeWith(EditOperation edit) {
James Cook48e0fac2015-02-25 15:44:51 -08006158 if (DEBUG_UNDO) {
6159 Log.d(TAG, "mergeWith old " + this);
6160 Log.d(TAG, "mergeWith new " + edit);
6161 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006162
6163 if (mFrozen) {
6164 return false;
6165 }
6166
James Cook471559f2015-02-27 10:31:20 -08006167 switch (mType) {
6168 case TYPE_INSERT:
6169 return mergeInsertWith(edit);
6170 case TYPE_DELETE:
6171 return mergeDeleteWith(edit);
6172 case TYPE_REPLACE:
6173 return mergeReplaceWith(edit);
6174 default:
6175 return false;
6176 }
6177 }
6178
6179 private boolean mergeInsertWith(EditOperation edit) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006180 if (edit.mType == TYPE_INSERT) {
6181 // Merge insertions that are contiguous even when it's frozen.
6182 if (getNewTextEnd() != edit.mStart) {
6183 return false;
6184 }
6185 mNewText += edit.mNewText;
6186 mNewCursorPos = edit.mNewCursorPos;
6187 mFrozen = edit.mFrozen;
6188 mIsComposition = edit.mIsComposition;
6189 return true;
James Cook471559f2015-02-27 10:31:20 -08006190 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006191 if (mIsComposition && edit.mType == TYPE_REPLACE
6192 && mStart <= edit.mStart && getNewTextEnd() >= edit.getOldTextEnd()) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006193 // Merge insertion with replace as they can be single insertion.
6194 mNewText = mNewText.substring(0, edit.mStart - mStart) + edit.mNewText
6195 + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
6196 mNewCursorPos = edit.mNewCursorPos;
6197 mIsComposition = edit.mIsComposition;
6198 return true;
James Cook471559f2015-02-27 10:31:20 -08006199 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006200 return false;
James Cook471559f2015-02-27 10:31:20 -08006201 }
6202
6203 // TODO: Support forward delete.
6204 private boolean mergeDeleteWith(EditOperation edit) {
James Cook471559f2015-02-27 10:31:20 -08006205 // Only merge continuous deletes.
6206 if (edit.mType != TYPE_DELETE) {
6207 return false;
6208 }
6209 // Only merge deletions that are contiguous.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006210 if (mStart != edit.getOldTextEnd()) {
James Cook471559f2015-02-27 10:31:20 -08006211 return false;
6212 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006213 mStart = edit.mStart;
James Cook471559f2015-02-27 10:31:20 -08006214 mOldText = edit.mOldText + mOldText;
6215 mNewCursorPos = edit.mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006216 mIsComposition = edit.mIsComposition;
James Cook471559f2015-02-27 10:31:20 -08006217 return true;
6218 }
6219
6220 private boolean mergeReplaceWith(EditOperation edit) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006221 if (edit.mType == TYPE_INSERT && getNewTextEnd() == edit.mStart) {
6222 // Merge with adjacent insert.
6223 mNewText += edit.mNewText;
6224 mNewCursorPos = edit.mNewCursorPos;
6225 return true;
6226 }
6227 if (!mIsComposition) {
James Cook471559f2015-02-27 10:31:20 -08006228 return false;
6229 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006230 if (edit.mType == TYPE_DELETE && mStart <= edit.mStart
6231 && getNewTextEnd() >= edit.getOldTextEnd()) {
6232 // Merge with delete as they can be single operation.
6233 mNewText = mNewText.substring(0, edit.mStart - mStart)
6234 + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
6235 if (mNewText.isEmpty()) {
6236 mType = TYPE_DELETE;
6237 }
6238 mNewCursorPos = edit.mNewCursorPos;
6239 mIsComposition = edit.mIsComposition;
6240 return true;
6241 }
6242 if (edit.mType == TYPE_REPLACE && mStart == edit.mStart
6243 && TextUtils.equals(mNewText, edit.mOldText)) {
6244 // Merge with the replace that replaces the same region.
6245 mNewText = edit.mNewText;
6246 mNewCursorPos = edit.mNewCursorPos;
6247 mIsComposition = edit.mIsComposition;
6248 return true;
6249 }
6250 return false;
James Cook471559f2015-02-27 10:31:20 -08006251 }
6252
James Cook48e0fac2015-02-25 15:44:51 -08006253 /**
6254 * Forcibly creates a single merged edit operation by simulating the entire text
6255 * contents being replaced.
6256 */
James Cook22054252015-03-25 14:04:01 -07006257 public void forceMergeWith(EditOperation edit) {
James Cook48e0fac2015-02-25 15:44:51 -08006258 if (DEBUG_UNDO) Log.d(TAG, "forceMerge");
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006259 if (mergeWith(edit)) {
6260 return;
6261 }
James Cookf59152c2015-02-26 18:03:58 -08006262 Editor editor = getOwnerData();
James Cook48e0fac2015-02-25 15:44:51 -08006263
6264 // Copy the text of the current field.
6265 // NOTE: Using StringBuilder instead of SpannableStringBuilder would be somewhat faster,
6266 // but would require two parallel implementations of modifyText() because Editable and
6267 // StringBuilder do not share an interface for replace/delete/insert.
6268 Editable editable = (Editable) editor.mTextView.getText();
6269 Editable originalText = new SpannableStringBuilder(editable.toString());
6270
6271 // Roll back the last operation.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006272 modifyText(originalText, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
James Cook48e0fac2015-02-25 15:44:51 -08006273
6274 // Clone the text again and apply the new operation.
6275 Editable finalText = new SpannableStringBuilder(editable.toString());
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006276 modifyText(finalText, edit.mStart, edit.getOldTextEnd(),
6277 edit.mNewText, edit.mStart, edit.mNewCursorPos);
James Cook48e0fac2015-02-25 15:44:51 -08006278
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006279 // Convert this operation into a replace operation.
James Cook48e0fac2015-02-25 15:44:51 -08006280 mType = TYPE_REPLACE;
6281 mNewText = finalText.toString();
James Cook48e0fac2015-02-25 15:44:51 -08006282 mOldText = originalText.toString();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006283 mStart = 0;
James Cook48e0fac2015-02-25 15:44:51 -08006284 mNewCursorPos = edit.mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006285 mIsComposition = edit.mIsComposition;
James Cook48e0fac2015-02-25 15:44:51 -08006286 // mOldCursorPos is unchanged.
6287 }
6288
6289 private static void modifyText(Editable text, int deleteFrom, int deleteTo,
6290 CharSequence newText, int newTextInsertAt, int newCursorPos) {
James Cook471559f2015-02-27 10:31:20 -08006291 // Apply the edit if it is still valid.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006292 if (isValidRange(text, deleteFrom, deleteTo)
6293 && newTextInsertAt <= text.length() - (deleteTo - deleteFrom)) {
James Cook471559f2015-02-27 10:31:20 -08006294 if (deleteFrom != deleteTo) {
6295 text.delete(deleteFrom, deleteTo);
6296 }
6297 if (newText.length() != 0) {
6298 text.insert(newTextInsertAt, newText);
6299 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006300 }
James Cook900185d2015-03-10 09:48:11 -07006301 // Restore the cursor position. If there wasn't an old cursor (newCursorPos == -1) then
6302 // don't explicitly set it and rely on SpannableStringBuilder to position it.
James Cook471559f2015-02-27 10:31:20 -08006303 // TODO: Select all the text that was undone.
James Cook900185d2015-03-10 09:48:11 -07006304 if (0 <= newCursorPos && newCursorPos <= text.length()) {
James Cook471559f2015-02-27 10:31:20 -08006305 Selection.setSelection(text, newCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006306 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006307 }
6308
James Cook48e0fac2015-02-25 15:44:51 -08006309 private String getTypeString() {
6310 switch (mType) {
6311 case TYPE_INSERT:
6312 return "insert";
6313 case TYPE_DELETE:
6314 return "delete";
6315 case TYPE_REPLACE:
6316 return "replace";
6317 default:
6318 return "";
6319 }
6320 }
6321
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006322 @Override
James Cook471559f2015-02-27 10:31:20 -08006323 public String toString() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006324 return "[mType=" + getTypeString() + ", "
6325 + "mOldText=" + mOldText + ", "
6326 + "mNewText=" + mNewText + ", "
6327 + "mStart=" + mStart + ", "
6328 + "mOldCursorPos=" + mOldCursorPos + ", "
6329 + "mNewCursorPos=" + mNewCursorPos + ", "
6330 + "mFrozen=" + mFrozen + ", "
6331 + "mIsComposition=" + mIsComposition + "]";
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006332 }
6333
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006334 public static final Parcelable.ClassLoaderCreator<EditOperation> CREATOR =
6335 new Parcelable.ClassLoaderCreator<EditOperation>() {
James Cookf59152c2015-02-26 18:03:58 -08006336 @Override
James Cook471559f2015-02-27 10:31:20 -08006337 public EditOperation createFromParcel(Parcel in) {
6338 return new EditOperation(in, null);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006339 }
6340
James Cookf59152c2015-02-26 18:03:58 -08006341 @Override
James Cook471559f2015-02-27 10:31:20 -08006342 public EditOperation createFromParcel(Parcel in, ClassLoader loader) {
6343 return new EditOperation(in, loader);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006344 }
6345
James Cookf59152c2015-02-26 18:03:58 -08006346 @Override
James Cook471559f2015-02-27 10:31:20 -08006347 public EditOperation[] newArray(int size) {
6348 return new EditOperation[size];
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006349 }
6350 };
6351 }
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006352
6353 /**
6354 * A helper for enabling and handling "PROCESS_TEXT" menu actions.
6355 * These allow external applications to plug into currently selected text.
6356 */
6357 static final class ProcessTextIntentActionsHandler {
6358
6359 private final Editor mEditor;
6360 private final TextView mTextView;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006361 private final Context mContext;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006362 private final PackageManager mPackageManager;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006363 private final String mPackageName;
6364 private final SparseArray<Intent> mAccessibilityIntents = new SparseArray<>();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006365 private final SparseArray<AccessibilityNodeInfo.AccessibilityAction> mAccessibilityActions =
6366 new SparseArray<>();
6367 private final List<ResolveInfo> mSupportedActivities = new ArrayList<>();
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006368
6369 private ProcessTextIntentActionsHandler(Editor editor) {
6370 mEditor = Preconditions.checkNotNull(editor);
6371 mTextView = Preconditions.checkNotNull(mEditor.mTextView);
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006372 mContext = Preconditions.checkNotNull(mTextView.getContext());
6373 mPackageManager = Preconditions.checkNotNull(mContext.getPackageManager());
6374 mPackageName = Preconditions.checkNotNull(mContext.getPackageName());
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006375 }
6376
6377 /**
6378 * Adds "PROCESS_TEXT" menu items to the specified menu.
6379 */
6380 public void onInitializeMenu(Menu menu) {
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +01006381 final int size = mSupportedActivities.size();
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006382 loadSupportedActivities();
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +01006383 for (int i = 0; i < size; i++) {
6384 final ResolveInfo resolveInfo = mSupportedActivities.get(i);
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006385 menu.add(Menu.NONE, Menu.NONE,
6386 Editor.MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i++,
6387 getLabel(resolveInfo))
6388 .setIntent(createProcessTextIntentForResolveInfo(resolveInfo))
6389 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
6390 }
6391 }
6392
6393 /**
6394 * Performs a "PROCESS_TEXT" action if there is one associated with the specified
6395 * menu item.
6396 *
6397 * @return True if the action was performed, false otherwise.
6398 */
6399 public boolean performMenuItemAction(MenuItem item) {
6400 return fireIntent(item.getIntent());
6401 }
6402
6403 /**
6404 * Initializes and caches "PROCESS_TEXT" accessibility actions.
6405 */
6406 public void initializeAccessibilityActions() {
6407 mAccessibilityIntents.clear();
6408 mAccessibilityActions.clear();
6409 int i = 0;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006410 loadSupportedActivities();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006411 for (ResolveInfo resolveInfo : mSupportedActivities) {
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006412 int actionId = TextView.ACCESSIBILITY_ACTION_PROCESS_TEXT_START_ID + i++;
6413 mAccessibilityActions.put(
6414 actionId,
6415 new AccessibilityNodeInfo.AccessibilityAction(
6416 actionId, getLabel(resolveInfo)));
6417 mAccessibilityIntents.put(
6418 actionId, createProcessTextIntentForResolveInfo(resolveInfo));
6419 }
6420 }
6421
6422 /**
6423 * Adds "PROCESS_TEXT" accessibility actions to the specified accessibility node info.
6424 * NOTE: This needs a prior call to {@link #initializeAccessibilityActions()} to make the
6425 * latest accessibility actions available for this call.
6426 */
6427 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) {
6428 for (int i = 0; i < mAccessibilityActions.size(); i++) {
6429 nodeInfo.addAction(mAccessibilityActions.valueAt(i));
6430 }
6431 }
6432
6433 /**
6434 * Performs a "PROCESS_TEXT" action if there is one associated with the specified
6435 * accessibility action id.
6436 *
6437 * @return True if the action was performed, false otherwise.
6438 */
6439 public boolean performAccessibilityAction(int actionId) {
6440 return fireIntent(mAccessibilityIntents.get(actionId));
6441 }
6442
6443 private boolean fireIntent(Intent intent) {
6444 if (intent != null && Intent.ACTION_PROCESS_TEXT.equals(intent.getAction())) {
Siyamed Sinirce3b05a2017-07-18 18:54:31 -07006445 String selectedText = mTextView.getSelectedText();
6446 selectedText = TextUtils.trimToParcelableSize(selectedText);
6447 intent.putExtra(Intent.EXTRA_PROCESS_TEXT, selectedText);
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08006448 mEditor.mPreserveSelection = true;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006449 mTextView.startActivityForResult(intent, TextView.PROCESS_TEXT_REQUEST_CODE);
6450 return true;
6451 }
6452 return false;
6453 }
6454
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006455 private void loadSupportedActivities() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006456 mSupportedActivities.clear();
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006457 PackageManager packageManager = mTextView.getContext().getPackageManager();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006458 List<ResolveInfo> unfiltered =
6459 packageManager.queryIntentActivities(createProcessTextIntent(), 0);
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006460 for (ResolveInfo info : unfiltered) {
6461 if (isSupportedActivity(info)) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006462 mSupportedActivities.add(info);
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006463 }
6464 }
6465 }
6466
6467 private boolean isSupportedActivity(ResolveInfo info) {
6468 return mPackageName.equals(info.activityInfo.packageName)
6469 || info.activityInfo.exported
6470 && (info.activityInfo.permission == null
6471 || mContext.checkSelfPermission(info.activityInfo.permission)
6472 == PackageManager.PERMISSION_GRANTED);
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006473 }
6474
6475 private Intent createProcessTextIntentForResolveInfo(ResolveInfo info) {
6476 return createProcessTextIntent()
6477 .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !mTextView.isTextEditable())
6478 .setClassName(info.activityInfo.packageName, info.activityInfo.name);
6479 }
6480
6481 private Intent createProcessTextIntent() {
6482 return new Intent()
6483 .setAction(Intent.ACTION_PROCESS_TEXT)
6484 .setType("text/plain");
6485 }
6486
6487 private CharSequence getLabel(ResolveInfo resolveInfo) {
6488 return resolveInfo.loadLabel(mPackageManager);
6489 }
6490 }
Gilles Debunned88876a2012-03-16 17:34:04 -07006491}