blob: e945afce88b62b2109caf6b63a17eb2d47ccc345 [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;
Jan Althaus786a39d2017-09-15 10:41:16 +020044import 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
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700758 private void chooseSize(@NonNull PopupWindow pop, @NonNull CharSequence text,
759 @NonNull TextView tv) {
760 final int wid = tv.getPaddingLeft() + tv.getPaddingRight();
761 final int ht = tv.getPaddingTop() + tv.getPaddingBottom();
Gilles Debunned88876a2012-03-16 17:34:04 -0700762
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700763 final int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize(
Gilles Debunned88876a2012-03-16 17:34:04 -0700764 com.android.internal.R.dimen.textview_error_popup_default_width);
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700765 final StaticLayout l = StaticLayout.Builder.obtain(text, 0, text.length(), tv.getPaint(),
766 defaultWidthInPixels)
767 .setUseLineSpacingFromFallbacks(tv.mUseFallbackLineSpacing)
768 .build();
769
Gilles Debunned88876a2012-03-16 17:34:04 -0700770 float max = 0;
771 for (int i = 0; i < l.getLineCount(); i++) {
772 max = Math.max(max, l.getLineWidth(i));
773 }
774
775 /*
776 * Now set the popup size to be big enough for the text plus the border capped
777 * to DEFAULT_MAX_POPUP_WIDTH
778 */
779 pop.setWidth(wid + (int) Math.ceil(max));
780 pop.setHeight(ht + l.getHeight());
781 }
782
783 void setFrame() {
784 if (mErrorPopup != null) {
785 TextView tv = (TextView) mErrorPopup.getContentView();
786 chooseSize(mErrorPopup, mError, tv);
787 mErrorPopup.update(mTextView, getErrorX(), getErrorY(),
788 mErrorPopup.getWidth(), mErrorPopup.getHeight());
789 }
790 }
791
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800792 private int getWordStart(int offset) {
793 // FIXME - For this and similar methods we're not doing anything to check if there's
794 // a LocaleSpan in the text, this may be something we should try handling or checking for.
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700795 int retOffset = getWordIteratorWithText().prevBoundary(offset);
Mady Mellor58c90872015-05-12 11:09:37 -0700796 if (getWordIteratorWithText().isOnPunctuation(retOffset)) {
797 // On punctuation boundary or within group of punctuation, find punctuation start.
798 retOffset = getWordIteratorWithText().getPunctuationBeginning(offset);
799 } else {
800 // Not on a punctuation boundary, find the word start.
Mady Mellore264ac32015-06-22 16:46:29 -0700801 retOffset = getWordIteratorWithText().getPrevWordBeginningOnTwoWordsBoundary(offset);
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800802 }
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700803 if (retOffset == BreakIterator.DONE) {
804 return offset;
805 }
806 return retOffset;
807 }
808
809 private int getWordEnd(int offset) {
810 int retOffset = getWordIteratorWithText().nextBoundary(offset);
Mady Mellor58c90872015-05-12 11:09:37 -0700811 if (getWordIteratorWithText().isAfterPunctuation(retOffset)) {
812 // On punctuation boundary or within group of punctuation, find punctuation end.
813 retOffset = getWordIteratorWithText().getPunctuationEnd(offset);
814 } else {
815 // Not on a punctuation boundary, find the word end.
Mady Mellore264ac32015-06-22 16:46:29 -0700816 retOffset = getWordIteratorWithText().getNextWordEndOnTwoWordBoundary(offset);
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700817 }
818 if (retOffset == BreakIterator.DONE) {
819 return offset;
820 }
821 return retOffset;
822 }
823
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900824 private boolean needsToSelectAllToSelectWordOrParagraph() {
Andrei Stingaceanu47f82ae2015-04-28 17:43:54 +0100825 if (mTextView.hasPasswordTransformationMethod()) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700826 // Always select all on a password field.
827 // Cut/copy menu entries are not available for passwords, but being able to select all
828 // is however useful to delete or paste to replace the entire content.
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900829 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -0700830 }
831
832 int inputType = mTextView.getInputType();
833 int klass = inputType & InputType.TYPE_MASK_CLASS;
834 int variation = inputType & InputType.TYPE_MASK_VARIATION;
835
836 // Specific text field types: select the entire text for these
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700837 if (klass == InputType.TYPE_CLASS_NUMBER
838 || klass == InputType.TYPE_CLASS_PHONE
839 || klass == InputType.TYPE_CLASS_DATETIME
840 || variation == InputType.TYPE_TEXT_VARIATION_URI
841 || variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
842 || variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS
843 || variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900844 return true;
845 }
846 return false;
847 }
848
849 /**
850 * Adjusts selection to the word under last touch offset. Return true if the operation was
851 * successfully performed.
852 */
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100853 boolean selectCurrentWord() {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900854 if (!mTextView.canSelectText()) {
855 return false;
856 }
857
858 if (needsToSelectAllToSelectWordOrParagraph()) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700859 return mTextView.selectAllText();
860 }
861
862 long lastTouchOffsets = getLastTouchOffsets();
863 final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
864 final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
865
866 // Safety check in case standard touch event handling has been bypassed
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -0800867 if (minOffset < 0 || minOffset > mTextView.getText().length()) return false;
868 if (maxOffset < 0 || maxOffset > mTextView.getText().length()) return false;
Gilles Debunned88876a2012-03-16 17:34:04 -0700869
870 int selectionStart, selectionEnd;
871
872 // If a URLSpan (web address, email, phone...) is found at that position, select it.
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700873 URLSpan[] urlSpans =
874 ((Spanned) mTextView.getText()).getSpans(minOffset, maxOffset, URLSpan.class);
Gilles Debunned88876a2012-03-16 17:34:04 -0700875 if (urlSpans.length >= 1) {
876 URLSpan urlSpan = urlSpans[0];
877 selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan);
878 selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan);
879 } else {
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800880 // FIXME - We should check if there's a LocaleSpan in the text, this may be
881 // something we should try handling or checking for.
Gilles Debunned88876a2012-03-16 17:34:04 -0700882 final WordIterator wordIterator = getWordIterator();
883 wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset);
884
885 selectionStart = wordIterator.getBeginning(minOffset);
886 selectionEnd = wordIterator.getEnd(maxOffset);
887
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700888 if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE
889 || selectionStart == selectionEnd) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700890 // Possible when the word iterator does not properly handle the text's language
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +0900891 long range = getCharClusterRange(minOffset);
Gilles Debunned88876a2012-03-16 17:34:04 -0700892 selectionStart = TextUtils.unpackRangeStartFromLong(range);
893 selectionEnd = TextUtils.unpackRangeEndFromLong(range);
894 }
895 }
896
897 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
898 return selectionEnd > selectionStart;
899 }
900
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900901 /**
902 * Adjusts selection to the paragraph under last touch offset. Return true if the operation was
903 * successfully performed.
904 */
905 private boolean selectCurrentParagraph() {
906 if (!mTextView.canSelectText()) {
907 return false;
908 }
909
910 if (needsToSelectAllToSelectWordOrParagraph()) {
911 return mTextView.selectAllText();
912 }
913
914 long lastTouchOffsets = getLastTouchOffsets();
915 final int minLastTouchOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
916 final int maxLastTouchOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
917
918 final long paragraphsRange = getParagraphsRange(minLastTouchOffset, maxLastTouchOffset);
919 final int start = TextUtils.unpackRangeStartFromLong(paragraphsRange);
920 final int end = TextUtils.unpackRangeEndFromLong(paragraphsRange);
921 if (start < end) {
922 Selection.setSelection((Spannable) mTextView.getText(), start, end);
923 return true;
924 }
925 return false;
926 }
927
928 /**
929 * Get the minimum range of paragraphs that contains startOffset and endOffset.
930 */
931 private long getParagraphsRange(int startOffset, int endOffset) {
932 final Layout layout = mTextView.getLayout();
933 if (layout == null) {
934 return TextUtils.packRangeInLong(-1, -1);
935 }
936 final CharSequence text = mTextView.getText();
937 int minLine = layout.getLineForOffset(startOffset);
938 // Search paragraph start.
939 while (minLine > 0) {
940 final int prevLineEndOffset = layout.getLineEnd(minLine - 1);
941 if (text.charAt(prevLineEndOffset - 1) == '\n') {
942 break;
943 }
944 minLine--;
945 }
946 int maxLine = layout.getLineForOffset(endOffset);
947 // Search paragraph end.
948 while (maxLine < layout.getLineCount() - 1) {
949 final int lineEndOffset = layout.getLineEnd(maxLine);
950 if (text.charAt(lineEndOffset - 1) == '\n') {
951 break;
952 }
953 maxLine++;
954 }
955 return TextUtils.packRangeInLong(layout.getLineStart(minLine), layout.getLineEnd(maxLine));
956 }
957
Gilles Debunned88876a2012-03-16 17:34:04 -0700958 void onLocaleChanged() {
Keisuke Kuroyanagie0ac5ac2016-03-09 15:33:30 +0900959 // Will be re-created on demand in getWordIterator and getWordIteratorWithText with the
960 // proper new locale
Gilles Debunned88876a2012-03-16 17:34:04 -0700961 mWordIterator = null;
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800962 mWordIteratorWithText = null;
Gilles Debunned88876a2012-03-16 17:34:04 -0700963 }
964
Gilles Debunned88876a2012-03-16 17:34:04 -0700965 public WordIterator getWordIterator() {
966 if (mWordIterator == null) {
967 mWordIterator = new WordIterator(mTextView.getTextServicesLocale());
968 }
969 return mWordIterator;
970 }
971
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800972 private WordIterator getWordIteratorWithText() {
973 if (mWordIteratorWithText == null) {
974 mWordIteratorWithText = new WordIterator(mTextView.getTextServicesLocale());
975 mUpdateWordIteratorText = true;
976 }
977 if (mUpdateWordIteratorText) {
978 // FIXME - Shouldn't copy all of the text as only the area of the text relevant
979 // to the user's selection is needed. A possible solution would be to
980 // copy some number N of characters near the selection and then when the
981 // user approaches N then we'd do another copy of the next N characters.
982 CharSequence text = mTextView.getText();
983 mWordIteratorWithText.setCharSequence(text, 0, text.length());
984 mUpdateWordIteratorText = false;
985 }
986 return mWordIteratorWithText;
987 }
988
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +0900989 private int getNextCursorOffset(int offset, boolean findAfterGivenOffset) {
990 final Layout layout = mTextView.getLayout();
991 if (layout == null) return offset;
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700992 return findAfterGivenOffset == layout.isRtlCharAt(offset)
993 ? layout.getOffsetToLeftOf(offset) : layout.getOffsetToRightOf(offset);
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +0900994 }
995
996 private long getCharClusterRange(int offset) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700997 final int textLength = mTextView.getText().length();
Gilles Debunned88876a2012-03-16 17:34:04 -0700998 if (offset < textLength) {
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -0800999 final int clusterEndOffset = getNextCursorOffset(offset, true);
1000 return TextUtils.packRangeInLong(
1001 getNextCursorOffset(clusterEndOffset, false), clusterEndOffset);
Gilles Debunned88876a2012-03-16 17:34:04 -07001002 }
1003 if (offset - 1 >= 0) {
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08001004 final int clusterStartOffset = getNextCursorOffset(offset, false);
1005 return TextUtils.packRangeInLong(clusterStartOffset,
1006 getNextCursorOffset(clusterStartOffset, true));
Gilles Debunned88876a2012-03-16 17:34:04 -07001007 }
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +09001008 return TextUtils.packRangeInLong(offset, offset);
Gilles Debunned88876a2012-03-16 17:34:04 -07001009 }
1010
1011 private boolean touchPositionIsInSelection() {
1012 int selectionStart = mTextView.getSelectionStart();
1013 int selectionEnd = mTextView.getSelectionEnd();
1014
1015 if (selectionStart == selectionEnd) {
1016 return false;
1017 }
1018
1019 if (selectionStart > selectionEnd) {
1020 int tmp = selectionStart;
1021 selectionStart = selectionEnd;
1022 selectionEnd = tmp;
1023 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
1024 }
1025
1026 SelectionModifierCursorController selectionController = getSelectionController();
1027 int minOffset = selectionController.getMinTouchOffset();
1028 int maxOffset = selectionController.getMaxTouchOffset();
1029
1030 return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
1031 }
1032
1033 private PositionListener getPositionListener() {
1034 if (mPositionListener == null) {
1035 mPositionListener = new PositionListener();
1036 }
1037 return mPositionListener;
1038 }
1039
1040 private interface TextViewPositionListener {
1041 public void updatePosition(int parentPositionX, int parentPositionY,
1042 boolean parentPositionChanged, boolean parentScrolled);
1043 }
1044
Gilles Debunned88876a2012-03-16 17:34:04 -07001045 private boolean isOffsetVisible(int offset) {
1046 Layout layout = mTextView.getLayout();
Victoria Leaseb9b77ae2013-10-13 15:12:52 -07001047 if (layout == null) return false;
1048
Gilles Debunned88876a2012-03-16 17:34:04 -07001049 final int line = layout.getLineForOffset(offset);
1050 final int lineBottom = layout.getLineBottom(line);
1051 final int primaryHorizontal = (int) layout.getPrimaryHorizontal(offset);
Phil Weaverc2e28932016-12-08 12:29:25 -08001052 return mTextView.isPositionVisible(
1053 primaryHorizontal + mTextView.viewportToContentHorizontalOffset(),
Gilles Debunned88876a2012-03-16 17:34:04 -07001054 lineBottom + mTextView.viewportToContentVerticalOffset());
1055 }
1056
1057 /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
1058 * in the view. Returns false when the position is in the empty space of left/right of text.
1059 */
1060 private boolean isPositionOnText(float x, float y) {
1061 Layout layout = mTextView.getLayout();
1062 if (layout == null) return false;
1063
1064 final int line = mTextView.getLineAtCoordinate(y);
1065 x = mTextView.convertToLocalHorizontalCoordinate(x);
1066
1067 if (x < layout.getLineLeft(line)) return false;
1068 if (x > layout.getLineRight(line)) return false;
1069 return true;
1070 }
1071
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001072 private void startDragAndDrop() {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001073 getSelectionActionModeHelper().onSelectionDrag();
1074
Keisuke Kuroyanagifdfc93d2016-03-15 14:47:08 +09001075 // TODO: Fix drag and drop in full screen extracted mode.
1076 if (mTextView.isInExtractedMode()) {
1077 return;
1078 }
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001079 final int start = mTextView.getSelectionStart();
1080 final int end = mTextView.getSelectionEnd();
1081 CharSequence selectedText = mTextView.getTransformedText(start, end);
1082 ClipData data = ClipData.newPlainText(null, selectedText);
1083 DragLocalState localState = new DragLocalState(mTextView, start, end);
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08001084 mTextView.startDragAndDrop(data, getTextThumbnailBuilder(start, end), localState,
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001085 View.DRAG_FLAG_GLOBAL);
1086 stopTextActionMode();
1087 if (hasSelectionController()) {
1088 getSelectionController().resetTouchOffsets();
1089 }
1090 }
1091
Gilles Debunned88876a2012-03-16 17:34:04 -07001092 public boolean performLongClick(boolean handled) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001093 // Long press in empty space moves cursor and starts the insertion action mode.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001094 if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY)
1095 && mInsertionControllerEnabled) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001096 final int offset = mTextView.getOffsetForPosition(mLastDownPositionX,
1097 mLastDownPositionY);
Gilles Debunned88876a2012-03-16 17:34:04 -07001098 Selection.setSelection((Spannable) mTextView.getText(), offset);
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00001099 getInsertionController().show();
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001100 mIsInsertionActionModeStartPending = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001101 handled = true;
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001102 MetricsLogger.action(
1103 mTextView.getContext(),
1104 MetricsEvent.TEXT_LONGPRESS,
1105 TextViewMetrics.SUBTYPE_LONG_PRESS_OTHER);
Gilles Debunned88876a2012-03-16 17:34:04 -07001106 }
1107
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001108 if (!handled && mTextActionMode != null) {
Andrei Stingaceanu2aaeefe2015-10-20 19:11:23 +01001109 if (touchPositionIsInSelection()) {
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001110 startDragAndDrop();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001111 MetricsLogger.action(
1112 mTextView.getContext(),
1113 MetricsEvent.TEXT_LONGPRESS,
1114 TextViewMetrics.SUBTYPE_LONG_PRESS_DRAG_AND_DROP);
Gilles Debunned88876a2012-03-16 17:34:04 -07001115 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001116 stopTextActionMode();
Clara Bayarridfac4432015-05-15 12:18:24 +01001117 selectCurrentWordAndStartDrag();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001118 MetricsLogger.action(
1119 mTextView.getContext(),
1120 MetricsEvent.TEXT_LONGPRESS,
1121 TextViewMetrics.SUBTYPE_LONG_PRESS_SELECTION);
Gilles Debunned88876a2012-03-16 17:34:04 -07001122 }
1123 handled = true;
1124 }
1125
1126 // Start a new selection
1127 if (!handled) {
Clara Bayarridfac4432015-05-15 12:18:24 +01001128 handled = selectCurrentWordAndStartDrag();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001129 if (handled) {
1130 MetricsLogger.action(
1131 mTextView.getContext(),
1132 MetricsEvent.TEXT_LONGPRESS,
1133 TextViewMetrics.SUBTYPE_LONG_PRESS_SELECTION);
1134 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001135 }
1136
1137 return handled;
1138 }
1139
Petar Å egina91df3f92017-08-15 16:20:43 +01001140 float getLastUpPositionX() {
1141 return mLastUpPositionX;
1142 }
1143
1144 float getLastUpPositionY() {
1145 return mLastUpPositionY;
1146 }
1147
Gilles Debunned88876a2012-03-16 17:34:04 -07001148 private long getLastTouchOffsets() {
1149 SelectionModifierCursorController selectionController = getSelectionController();
1150 final int minOffset = selectionController.getMinTouchOffset();
1151 final int maxOffset = selectionController.getMaxTouchOffset();
1152 return TextUtils.packRangeInLong(minOffset, maxOffset);
1153 }
1154
1155 void onFocusChanged(boolean focused, int direction) {
1156 mShowCursor = SystemClock.uptimeMillis();
1157 ensureEndedBatchEdit();
1158
1159 if (focused) {
1160 int selStart = mTextView.getSelectionStart();
1161 int selEnd = mTextView.getSelectionEnd();
1162
1163 // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
1164 // mode for these, unless there was a specific selection already started.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001165 final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0
1166 && selEnd == mTextView.getText().length();
Gilles Debunned88876a2012-03-16 17:34:04 -07001167
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001168 mCreatedWithASelection = mFrozenWithFocus && mTextView.hasSelection()
1169 && !isFocusHighlighted;
Gilles Debunned88876a2012-03-16 17:34:04 -07001170
1171 if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
1172 // If a tap was used to give focus to that view, move cursor at tap position.
1173 // Has to be done before onTakeFocus, which can be overloaded.
1174 final int lastTapPosition = getLastTapPosition();
1175 if (lastTapPosition >= 0) {
1176 Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition);
1177 }
1178
1179 // Note this may have to be moved out of the Editor class
1180 MovementMethod mMovement = mTextView.getMovementMethod();
1181 if (mMovement != null) {
1182 mMovement.onTakeFocus(mTextView, (Spannable) mTextView.getText(), direction);
1183 }
1184
1185 // The DecorView does not have focus when the 'Done' ExtractEditText button is
1186 // pressed. Since it is the ViewAncestor's mView, it requests focus before
1187 // ExtractEditText clears focus, which gives focus to the ExtractEditText.
1188 // This special case ensure that we keep current selection in that case.
1189 // It would be better to know why the DecorView does not have focus at that time.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001190 if (((mTextView.isInExtractedMode()) || mSelectionMoved)
1191 && selStart >= 0 && selEnd >= 0) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001192 /*
1193 * Someone intentionally set the selection, so let them
1194 * do whatever it is that they wanted to do instead of
1195 * the default on-focus behavior. We reset the selection
1196 * here instead of just skipping the onTakeFocus() call
1197 * because some movement methods do something other than
1198 * just setting the selection in theirs and we still
1199 * need to go through that path.
1200 */
1201 Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
1202 }
1203
1204 if (mSelectAllOnFocus) {
1205 mTextView.selectAllText();
1206 }
1207
1208 mTouchFocusSelected = true;
1209 }
1210
1211 mFrozenWithFocus = false;
1212 mSelectionMoved = false;
1213
1214 if (mError != null) {
1215 showError();
1216 }
1217
1218 makeBlink();
1219 } else {
1220 if (mError != null) {
1221 hideError();
1222 }
1223 // Don't leave us in the middle of a batch edit.
1224 mTextView.onEndBatchEdit();
1225
Andrei Stingaceanub1891b32015-06-19 16:44:37 +01001226 if (mTextView.isInExtractedMode()) {
Mady Mellora2861452015-06-25 08:40:27 -07001227 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001228 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -07001229 } else {
Mady Mellora2861452015-06-25 08:40:27 -07001230 hideCursorAndSpanControllers();
Yohei Yukawa24df9312016-03-31 17:15:23 -07001231 if (mTextView.isTemporarilyDetached()) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001232 stopTextActionModeWithPreservingSelection();
1233 } else {
1234 stopTextActionMode();
1235 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001236 downgradeEasyCorrectionSpans();
1237 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001238 // No need to create the controller
1239 if (mSelectionModifierCursorController != null) {
1240 mSelectionModifierCursorController.resetTouchOffsets();
1241 }
1242 }
1243 }
1244
1245 /**
1246 * Downgrades to simple suggestions all the easy correction spans that are not a spell check
1247 * span.
1248 */
1249 private void downgradeEasyCorrectionSpans() {
1250 CharSequence text = mTextView.getText();
1251 if (text instanceof Spannable) {
1252 Spannable spannable = (Spannable) text;
1253 SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
1254 spannable.length(), SuggestionSpan.class);
1255 for (int i = 0; i < suggestionSpans.length; i++) {
1256 int flags = suggestionSpans[i].getFlags();
1257 if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
1258 && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
1259 flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
1260 suggestionSpans[i].setFlags(flags);
1261 }
1262 }
1263 }
1264 }
1265
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +01001266 void sendOnTextChanged(int start, int before, int after) {
1267 getSelectionActionModeHelper().onTextChanged(start, start + before);
Gilles Debunned88876a2012-03-16 17:34:04 -07001268 updateSpellCheckSpans(start, start + after, false);
1269
Mady Mellor2ff2cd82015-03-02 10:37:01 -08001270 // Flip flag to indicate the word iterator needs to have the text reset.
1271 mUpdateWordIteratorText = true;
1272
Gilles Debunned88876a2012-03-16 17:34:04 -07001273 // Hide the controllers as soon as text is modified (typing, procedural...)
1274 // We do not hide the span controllers, since they can be added when a new text is
1275 // inserted into the text view (voice IME).
1276 hideCursorControllers();
Keisuke Kuroyanagif4e347d2015-06-11 17:41:00 +09001277 // Reset drag accelerator.
1278 if (mSelectionModifierCursorController != null) {
1279 mSelectionModifierCursorController.resetTouchOffsets();
1280 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001281 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07001282 }
1283
1284 private int getLastTapPosition() {
1285 // No need to create the controller at that point, no last tap position saved
1286 if (mSelectionModifierCursorController != null) {
1287 int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
1288 if (lastTapPosition >= 0) {
1289 // Safety check, should not be possible.
1290 if (lastTapPosition > mTextView.getText().length()) {
1291 lastTapPosition = mTextView.getText().length();
1292 }
1293 return lastTapPosition;
1294 }
1295 }
1296
1297 return -1;
1298 }
1299
1300 void onWindowFocusChanged(boolean hasWindowFocus) {
1301 if (hasWindowFocus) {
1302 if (mBlink != null) {
1303 mBlink.uncancel();
1304 makeBlink();
1305 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001306 if (mTextView.hasSelection() && !extractedTextModeWillBeStarted()) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001307 refreshTextActionMode();
Mady Mellora2861452015-06-25 08:40:27 -07001308 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001309 } else {
1310 if (mBlink != null) {
1311 mBlink.cancel();
1312 }
1313 if (mInputContentType != null) {
1314 mInputContentType.enterDown = false;
1315 }
1316 // Order matters! Must be done before onParentLostFocus to rely on isShowingUp
Mady Mellora2861452015-06-25 08:40:27 -07001317 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001318 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -07001319 if (mSuggestionsPopupWindow != null) {
1320 mSuggestionsPopupWindow.onParentLostFocus();
1321 }
1322
Gilles Debunnec72fba82012-06-26 14:47:07 -07001323 // Don't leave us in the middle of a batch edit. Same as in onFocusChanged
1324 ensureEndedBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001325 }
1326 }
1327
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09001328 private void updateTapState(MotionEvent event) {
1329 final int action = event.getActionMasked();
1330 if (action == MotionEvent.ACTION_DOWN) {
1331 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
1332 // Detect double tap and triple click.
1333 if (((mTapState == TAP_STATE_FIRST_TAP)
1334 || ((mTapState == TAP_STATE_DOUBLE_TAP) && isMouse))
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001335 && (SystemClock.uptimeMillis() - mLastTouchUpTime)
1336 <= ViewConfiguration.getDoubleTapTimeout()) {
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09001337 if (mTapState == TAP_STATE_FIRST_TAP) {
1338 mTapState = TAP_STATE_DOUBLE_TAP;
1339 } else {
1340 mTapState = TAP_STATE_TRIPLE_CLICK;
1341 }
1342 } else {
1343 mTapState = TAP_STATE_FIRST_TAP;
1344 }
1345 }
1346 if (action == MotionEvent.ACTION_UP) {
1347 mLastTouchUpTime = SystemClock.uptimeMillis();
1348 }
1349 }
1350
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09001351 private boolean shouldFilterOutTouchEvent(MotionEvent event) {
1352 if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) {
1353 return false;
1354 }
1355 final boolean primaryButtonStateChanged =
1356 ((mLastButtonState ^ event.getButtonState()) & MotionEvent.BUTTON_PRIMARY) != 0;
1357 final int action = event.getActionMasked();
1358 if ((action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_UP)
1359 && !primaryButtonStateChanged) {
1360 return true;
1361 }
1362 if (action == MotionEvent.ACTION_MOVE
1363 && !event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
1364 return true;
1365 }
1366 return false;
1367 }
1368
Gilles Debunned88876a2012-03-16 17:34:04 -07001369 void onTouchEvent(MotionEvent event) {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09001370 final boolean filterOutEvent = shouldFilterOutTouchEvent(event);
1371 mLastButtonState = event.getButtonState();
1372 if (filterOutEvent) {
1373 if (event.getActionMasked() == MotionEvent.ACTION_UP) {
1374 mDiscardNextActionUp = true;
1375 }
1376 return;
1377 }
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09001378 updateTapState(event);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001379 updateFloatingToolbarVisibility(event);
1380
Gilles Debunned88876a2012-03-16 17:34:04 -07001381 if (hasSelectionController()) {
1382 getSelectionController().onTouchEvent(event);
1383 }
1384
1385 if (mShowSuggestionRunnable != null) {
1386 mTextView.removeCallbacks(mShowSuggestionRunnable);
1387 mShowSuggestionRunnable = null;
1388 }
1389
Petar Å egina91df3f92017-08-15 16:20:43 +01001390 if (event.getActionMasked() == MotionEvent.ACTION_UP) {
1391 mLastUpPositionX = event.getX();
1392 mLastUpPositionY = event.getY();
1393 }
1394
Gilles Debunned88876a2012-03-16 17:34:04 -07001395 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
1396 mLastDownPositionX = event.getX();
1397 mLastDownPositionY = event.getY();
1398
1399 // Reset this state; it will be re-set if super.onTouchEvent
1400 // causes focus to move to the view.
1401 mTouchFocusSelected = false;
1402 mIgnoreActionUpEvent = false;
1403 }
1404 }
1405
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001406 private void updateFloatingToolbarVisibility(MotionEvent event) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001407 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001408 switch (event.getActionMasked()) {
1409 case MotionEvent.ACTION_MOVE:
Abodunrinwa Toki4a7aeb32017-07-13 02:06:56 +01001410 hideFloatingToolbar(ActionMode.DEFAULT_HIDE_DURATION);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001411 break;
1412 case MotionEvent.ACTION_UP: // fall through
1413 case MotionEvent.ACTION_CANCEL:
1414 showFloatingToolbar();
1415 }
1416 }
1417 }
1418
Abodunrinwa Toki4a7aeb32017-07-13 02:06:56 +01001419 void hideFloatingToolbar(int duration) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001420 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001421 mTextView.removeCallbacks(mShowFloatingToolbar);
Abodunrinwa Toki4a7aeb32017-07-13 02:06:56 +01001422 mTextActionMode.hide(duration);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001423 }
1424 }
1425
1426 private void showFloatingToolbar() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001427 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001428 // Delay "show" so it doesn't interfere with click confirmations
1429 // or double-clicks that could "dismiss" the floating toolbar.
1430 int delay = ViewConfiguration.getDoubleTapTimeout();
1431 mTextView.postDelayed(mShowFloatingToolbar, delay);
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01001432
1433 // This classifies the text and most likely returns before the toolbar is actually
1434 // shown. If not, it will update the toolbar with the result when classification
1435 // returns. We would rather not wait for a long running classification process.
1436 invalidateActionModeAsync();
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001437 }
1438 }
1439
Gilles Debunned88876a2012-03-16 17:34:04 -07001440 public void beginBatchEdit() {
1441 mInBatchEditControllers = true;
1442 final InputMethodState ims = mInputMethodState;
1443 if (ims != null) {
1444 int nesting = ++ims.mBatchEditNesting;
1445 if (nesting == 1) {
1446 ims.mCursorChanged = false;
1447 ims.mChangedDelta = 0;
1448 if (ims.mContentChanged) {
1449 // We already have a pending change from somewhere else,
1450 // so turn this into a full update.
1451 ims.mChangedStart = 0;
1452 ims.mChangedEnd = mTextView.getText().length();
1453 } else {
1454 ims.mChangedStart = EXTRACT_UNKNOWN;
1455 ims.mChangedEnd = EXTRACT_UNKNOWN;
1456 ims.mContentChanged = false;
1457 }
James Cook48e0fac2015-02-25 15:44:51 -08001458 mUndoInputFilter.beginBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001459 mTextView.onBeginBatchEdit();
1460 }
1461 }
1462 }
1463
1464 public void endBatchEdit() {
1465 mInBatchEditControllers = false;
1466 final InputMethodState ims = mInputMethodState;
1467 if (ims != null) {
1468 int nesting = --ims.mBatchEditNesting;
1469 if (nesting == 0) {
1470 finishBatchEdit(ims);
1471 }
1472 }
1473 }
1474
1475 void ensureEndedBatchEdit() {
1476 final InputMethodState ims = mInputMethodState;
1477 if (ims != null && ims.mBatchEditNesting != 0) {
1478 ims.mBatchEditNesting = 0;
1479 finishBatchEdit(ims);
1480 }
1481 }
1482
1483 void finishBatchEdit(final InputMethodState ims) {
1484 mTextView.onEndBatchEdit();
James Cook48e0fac2015-02-25 15:44:51 -08001485 mUndoInputFilter.endBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001486
1487 if (ims.mContentChanged || ims.mSelectionModeChanged) {
1488 mTextView.updateAfterEdit();
1489 reportExtractedText();
1490 } else if (ims.mCursorChanged) {
Jean Chalardc99d33f2013-02-28 16:39:47 -08001491 // Cheesy way to get us to report the current cursor location.
Gilles Debunned88876a2012-03-16 17:34:04 -07001492 mTextView.invalidateCursor();
1493 }
Jean Chalardc99d33f2013-02-28 16:39:47 -08001494 // sendUpdateSelection knows to avoid sending if the selection did
1495 // not actually change.
1496 sendUpdateSelection();
Keisuke Kuroyanagic6fad962016-05-02 15:11:41 +09001497
1498 // Show drag handles if they were blocked by batch edit mode.
1499 if (mTextActionMode != null) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001500 final CursorController cursorController = mTextView.hasSelection()
1501 ? getSelectionController() : getInsertionController();
Keisuke Kuroyanagic6fad962016-05-02 15:11:41 +09001502 if (cursorController != null && !cursorController.isActive()
1503 && !cursorController.isCursorBeingModified()) {
1504 cursorController.show();
1505 }
1506 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001507 }
1508
1509 static final int EXTRACT_NOTHING = -2;
1510 static final int EXTRACT_UNKNOWN = -1;
1511
1512 boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
1513 return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
1514 EXTRACT_UNKNOWN, outText);
1515 }
1516
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001517 private boolean extractTextInternal(@Nullable ExtractedTextRequest request,
Gilles Debunned88876a2012-03-16 17:34:04 -07001518 int partialStartOffset, int partialEndOffset, int delta,
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001519 @Nullable ExtractedText outText) {
1520 if (request == null || outText == null) {
1521 return false;
Gilles Debunned88876a2012-03-16 17:34:04 -07001522 }
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001523
1524 final CharSequence content = mTextView.getText();
1525 if (content == null) {
1526 return false;
1527 }
1528
1529 if (partialStartOffset != EXTRACT_NOTHING) {
1530 final int N = content.length();
1531 if (partialStartOffset < 0) {
1532 outText.partialStartOffset = outText.partialEndOffset = -1;
1533 partialStartOffset = 0;
1534 partialEndOffset = N;
1535 } else {
1536 // Now use the delta to determine the actual amount of text
1537 // we need.
1538 partialEndOffset += delta;
1539 // Adjust offsets to ensure we contain full spans.
1540 if (content instanceof Spanned) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001541 Spanned spanned = (Spanned) content;
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001542 Object[] spans = spanned.getSpans(partialStartOffset,
1543 partialEndOffset, ParcelableSpan.class);
1544 int i = spans.length;
1545 while (i > 0) {
1546 i--;
1547 int j = spanned.getSpanStart(spans[i]);
1548 if (j < partialStartOffset) partialStartOffset = j;
1549 j = spanned.getSpanEnd(spans[i]);
1550 if (j > partialEndOffset) partialEndOffset = j;
1551 }
1552 }
1553 outText.partialStartOffset = partialStartOffset;
1554 outText.partialEndOffset = partialEndOffset - delta;
1555
1556 if (partialStartOffset > N) {
1557 partialStartOffset = N;
1558 } else if (partialStartOffset < 0) {
1559 partialStartOffset = 0;
1560 }
1561 if (partialEndOffset > N) {
1562 partialEndOffset = N;
1563 } else if (partialEndOffset < 0) {
1564 partialEndOffset = 0;
1565 }
1566 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001567 if ((request.flags & InputConnection.GET_TEXT_WITH_STYLES) != 0) {
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001568 outText.text = content.subSequence(partialStartOffset,
1569 partialEndOffset);
1570 } else {
1571 outText.text = TextUtils.substring(content, partialStartOffset,
1572 partialEndOffset);
1573 }
1574 } else {
1575 outText.partialStartOffset = 0;
1576 outText.partialEndOffset = 0;
1577 outText.text = "";
1578 }
1579 outText.flags = 0;
1580 if (MetaKeyKeyListener.getMetaState(content, MetaKeyKeyListener.META_SELECTING) != 0) {
1581 outText.flags |= ExtractedText.FLAG_SELECTING;
1582 }
1583 if (mTextView.isSingleLine()) {
1584 outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
1585 }
1586 outText.startOffset = 0;
1587 outText.selectionStart = mTextView.getSelectionStart();
1588 outText.selectionEnd = mTextView.getSelectionEnd();
1589 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001590 }
1591
1592 boolean reportExtractedText() {
1593 final Editor.InputMethodState ims = mInputMethodState;
1594 if (ims != null) {
1595 final boolean contentChanged = ims.mContentChanged;
1596 if (contentChanged || ims.mSelectionModeChanged) {
1597 ims.mContentChanged = false;
1598 ims.mSelectionModeChanged = false;
Gilles Debunnec62589c2012-04-12 14:50:23 -07001599 final ExtractedTextRequest req = ims.mExtractedTextRequest;
Gilles Debunned88876a2012-03-16 17:34:04 -07001600 if (req != null) {
1601 InputMethodManager imm = InputMethodManager.peekInstance();
1602 if (imm != null) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001603 if (TextView.DEBUG_EXTRACT) {
1604 Log.v(TextView.LOG_TAG, "Retrieving extracted start="
1605 + ims.mChangedStart
1606 + " end=" + ims.mChangedEnd
1607 + " delta=" + ims.mChangedDelta);
1608 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001609 if (ims.mChangedStart < 0 && !contentChanged) {
1610 ims.mChangedStart = EXTRACT_NOTHING;
1611 }
1612 if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
Gilles Debunnec62589c2012-04-12 14:50:23 -07001613 ims.mChangedDelta, ims.mExtractedText)) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001614 if (TextView.DEBUG_EXTRACT) {
1615 Log.v(TextView.LOG_TAG,
1616 "Reporting extracted start="
1617 + ims.mExtractedText.partialStartOffset
1618 + " end=" + ims.mExtractedText.partialEndOffset
1619 + ": " + ims.mExtractedText.text);
1620 }
Gilles Debunnec62589c2012-04-12 14:50:23 -07001621
1622 imm.updateExtractedText(mTextView, req.token, ims.mExtractedText);
Gilles Debunned88876a2012-03-16 17:34:04 -07001623 ims.mChangedStart = EXTRACT_UNKNOWN;
1624 ims.mChangedEnd = EXTRACT_UNKNOWN;
1625 ims.mChangedDelta = 0;
1626 ims.mContentChanged = false;
1627 return true;
1628 }
1629 }
1630 }
1631 }
1632 }
1633 return false;
1634 }
1635
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001636 private void sendUpdateSelection() {
1637 if (null != mInputMethodState && mInputMethodState.mBatchEditNesting <= 0) {
1638 final InputMethodManager imm = InputMethodManager.peekInstance();
1639 if (null != imm) {
1640 final int selectionStart = mTextView.getSelectionStart();
1641 final int selectionEnd = mTextView.getSelectionEnd();
1642 int candStart = -1;
1643 int candEnd = -1;
1644 if (mTextView.getText() instanceof Spannable) {
1645 final Spannable sp = (Spannable) mTextView.getText();
1646 candStart = EditableInputConnection.getComposingSpanStart(sp);
1647 candEnd = EditableInputConnection.getComposingSpanEnd(sp);
1648 }
Jean Chalardc99d33f2013-02-28 16:39:47 -08001649 // InputMethodManager#updateSelection skips sending the message if
1650 // none of the parameters have changed since the last time we called it.
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001651 imm.updateSelection(mTextView,
1652 selectionStart, selectionEnd, candStart, candEnd);
1653 }
1654 }
1655 }
1656
Gilles Debunned88876a2012-03-16 17:34:04 -07001657 void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint,
1658 int cursorOffsetVertical) {
1659 final int selectionStart = mTextView.getSelectionStart();
1660 final int selectionEnd = mTextView.getSelectionEnd();
1661
1662 final InputMethodState ims = mInputMethodState;
1663 if (ims != null && ims.mBatchEditNesting == 0) {
1664 InputMethodManager imm = InputMethodManager.peekInstance();
1665 if (imm != null) {
1666 if (imm.isActive(mTextView)) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001667 if (ims.mContentChanged || ims.mSelectionModeChanged) {
1668 // We are in extract mode and the content has changed
1669 // in some way... just report complete new text to the
1670 // input method.
Yohei Yukawab6bec1a2015-05-01 16:18:25 -07001671 reportExtractedText();
Gilles Debunned88876a2012-03-16 17:34:04 -07001672 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001673 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001674 }
1675 }
1676
1677 if (mCorrectionHighlighter != null) {
1678 mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
1679 }
1680
Roozbeh Pournader9c133072017-07-26 22:36:27 -07001681 if (highlight != null && selectionStart == selectionEnd && mCursorDrawable != null) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001682 drawCursor(canvas, cursorOffsetVertical);
1683 // Rely on the drawable entirely, do not draw the cursor line.
1684 // Has to be done after the IMM related code above which relies on the highlight.
1685 highlight = null;
1686 }
1687
1688 if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
1689 drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
1690 cursorOffsetVertical);
1691 } else {
1692 layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
1693 }
Petar Å egina5ab7bb22017-09-05 20:48:42 +01001694
1695 if (mSelectionActionModeHelper != null) {
1696 mSelectionActionModeHelper.onDraw(canvas);
1697 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001698 }
1699
1700 private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
1701 Paint highlightPaint, int cursorOffsetVertical) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001702 final long lineRange = layout.getLineRangeForDraw(canvas);
1703 int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
1704 int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
1705 if (lastLine < 0) return;
1706
1707 layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
1708 firstLine, lastLine);
1709
1710 if (layout instanceof DynamicLayout) {
Chris Craik956f3402015-04-27 16:41:00 -07001711 if (mTextRenderNodes == null) {
1712 mTextRenderNodes = ArrayUtils.emptyArray(TextRenderNode.class);
Gilles Debunned88876a2012-03-16 17:34:04 -07001713 }
1714
1715 DynamicLayout dynamicLayout = (DynamicLayout) layout;
Gilles Debunne157aafc2012-04-19 17:21:57 -07001716 int[] blockEndLines = dynamicLayout.getBlockEndLines();
Gilles Debunned88876a2012-03-16 17:34:04 -07001717 int[] blockIndices = dynamicLayout.getBlockIndices();
1718 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
Sangkyu Lee955beb22012-12-10 15:47:00 +09001719 final int indexFirstChangedBlock = dynamicLayout.getIndexFirstChangedBlock();
Gilles Debunned88876a2012-03-16 17:34:04 -07001720
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001721 final ArraySet<Integer> blockSet = dynamicLayout.getBlocksAlwaysNeedToBeRedrawn();
1722 if (blockSet != null) {
1723 for (int i = 0; i < blockSet.size(); i++) {
1724 final int blockIndex = dynamicLayout.getBlockIndex(blockSet.valueAt(i));
1725 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
1726 && mTextRenderNodes[blockIndex] != null) {
1727 mTextRenderNodes[blockIndex].needsToBeShifted = true;
1728 }
1729 }
1730 }
1731
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001732 int startBlock = Arrays.binarySearch(blockEndLines, 0, numberOfBlocks, firstLine);
1733 if (startBlock < 0) {
1734 startBlock = -(startBlock + 1);
1735 }
1736 startBlock = Math.min(indexFirstChangedBlock, startBlock);
Gilles Debunned88876a2012-03-16 17:34:04 -07001737
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001738 int startIndexToFindAvailableRenderNode = 0;
1739 int lastIndex = numberOfBlocks;
1740
1741 for (int i = startBlock; i < numberOfBlocks; i++) {
1742 final int blockIndex = blockIndices[i];
1743 if (i >= indexFirstChangedBlock
1744 && blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
1745 && mTextRenderNodes[blockIndex] != null) {
1746 mTextRenderNodes[blockIndex].needsToBeShifted = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001747 }
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001748 if (blockEndLines[i] < firstLine) {
1749 // Blocks in [indexFirstChangedBlock, firstLine) are not redrawn here. They will
1750 // be redrawn after they get scrolled into drawing range.
1751 continue;
Gilles Debunned88876a2012-03-16 17:34:04 -07001752 }
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001753 startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas, layout,
1754 highlight, highlightPaint, cursorOffsetVertical, blockEndLines,
1755 blockIndices, i, numberOfBlocks, startIndexToFindAvailableRenderNode);
1756 if (blockEndLines[i] >= lastLine) {
1757 lastIndex = Math.max(indexFirstChangedBlock, i + 1);
1758 break;
Gilles Debunned88876a2012-03-16 17:34:04 -07001759 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001760 }
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001761 if (blockSet != null) {
1762 for (int i = 0; i < blockSet.size(); i++) {
1763 final int block = blockSet.valueAt(i);
1764 final int blockIndex = dynamicLayout.getBlockIndex(block);
1765 if (blockIndex == DynamicLayout.INVALID_BLOCK_INDEX
1766 || mTextRenderNodes[blockIndex] == null
1767 || mTextRenderNodes[blockIndex].needsToBeShifted) {
1768 startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas,
1769 layout, highlight, highlightPaint, cursorOffsetVertical,
1770 blockEndLines, blockIndices, block, numberOfBlocks,
1771 startIndexToFindAvailableRenderNode);
1772 }
1773 }
1774 }
Sangkyu Lee955beb22012-12-10 15:47:00 +09001775
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001776 dynamicLayout.setIndexFirstChangedBlock(lastIndex);
Gilles Debunned88876a2012-03-16 17:34:04 -07001777 } else {
1778 // Boring layout is used for empty and hint text
1779 layout.drawText(canvas, firstLine, lastLine);
1780 }
1781 }
1782
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001783 private int drawHardwareAcceleratedInner(Canvas canvas, Layout layout, Path highlight,
1784 Paint highlightPaint, int cursorOffsetVertical, int[] blockEndLines,
1785 int[] blockIndices, int blockInfoIndex, int numberOfBlocks,
1786 int startIndexToFindAvailableRenderNode) {
1787 final int blockEndLine = blockEndLines[blockInfoIndex];
1788 int blockIndex = blockIndices[blockInfoIndex];
1789
1790 final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
1791 if (blockIsInvalid) {
1792 blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
1793 startIndexToFindAvailableRenderNode);
1794 // Note how dynamic layout's internal block indices get updated from Editor
1795 blockIndices[blockInfoIndex] = blockIndex;
1796 if (mTextRenderNodes[blockIndex] != null) {
1797 mTextRenderNodes[blockIndex].isDirty = true;
1798 }
1799 startIndexToFindAvailableRenderNode = blockIndex + 1;
1800 }
1801
1802 if (mTextRenderNodes[blockIndex] == null) {
1803 mTextRenderNodes[blockIndex] = new TextRenderNode("Text " + blockIndex);
1804 }
1805
1806 final boolean blockDisplayListIsInvalid = mTextRenderNodes[blockIndex].needsRecord();
1807 RenderNode blockDisplayList = mTextRenderNodes[blockIndex].renderNode;
1808 if (mTextRenderNodes[blockIndex].needsToBeShifted || blockDisplayListIsInvalid) {
1809 final int blockBeginLine = blockInfoIndex == 0 ?
1810 0 : blockEndLines[blockInfoIndex - 1] + 1;
1811 final int top = layout.getLineTop(blockBeginLine);
1812 final int bottom = layout.getLineBottom(blockEndLine);
1813 int left = 0;
1814 int right = mTextView.getWidth();
1815 if (mTextView.getHorizontallyScrolling()) {
1816 float min = Float.MAX_VALUE;
1817 float max = Float.MIN_VALUE;
1818 for (int line = blockBeginLine; line <= blockEndLine; line++) {
1819 min = Math.min(min, layout.getLineLeft(line));
1820 max = Math.max(max, layout.getLineRight(line));
1821 }
1822 left = (int) min;
1823 right = (int) (max + 0.5f);
1824 }
1825
1826 // Rebuild display list if it is invalid
1827 if (blockDisplayListIsInvalid) {
1828 final DisplayListCanvas displayListCanvas = blockDisplayList.start(
1829 right - left, bottom - top);
1830 try {
1831 // drawText is always relative to TextView's origin, this translation
1832 // brings this range of text back to the top left corner of the viewport
1833 displayListCanvas.translate(-left, -top);
1834 layout.drawText(displayListCanvas, blockBeginLine, blockEndLine);
1835 mTextRenderNodes[blockIndex].isDirty = false;
1836 // No need to untranslate, previous context is popped after
1837 // drawDisplayList
1838 } finally {
1839 blockDisplayList.end(displayListCanvas);
1840 // Same as drawDisplayList below, handled by our TextView's parent
1841 blockDisplayList.setClipToBounds(false);
1842 }
1843 }
1844
1845 // Valid display list only needs to update its drawing location.
1846 blockDisplayList.setLeftTopRightBottom(left, top, right, bottom);
1847 mTextRenderNodes[blockIndex].needsToBeShifted = false;
1848 }
1849 ((DisplayListCanvas) canvas).drawRenderNode(blockDisplayList);
1850 return startIndexToFindAvailableRenderNode;
1851 }
1852
Gilles Debunned88876a2012-03-16 17:34:04 -07001853 private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
1854 int searchStartIndex) {
Chris Craik956f3402015-04-27 16:41:00 -07001855 int length = mTextRenderNodes.length;
Gilles Debunned88876a2012-03-16 17:34:04 -07001856 for (int i = searchStartIndex; i < length; i++) {
1857 boolean blockIndexFound = false;
1858 for (int j = 0; j < numberOfBlocks; j++) {
1859 if (blockIndices[j] == i) {
1860 blockIndexFound = true;
1861 break;
1862 }
1863 }
1864 if (blockIndexFound) continue;
1865 return i;
1866 }
1867
1868 // No available index found, the pool has to grow
Chris Craik956f3402015-04-27 16:41:00 -07001869 mTextRenderNodes = GrowingArrayUtils.append(mTextRenderNodes, length, null);
Gilles Debunned88876a2012-03-16 17:34:04 -07001870 return length;
1871 }
1872
1873 private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
1874 final boolean translate = cursorOffsetVertical != 0;
1875 if (translate) canvas.translate(0, cursorOffsetVertical);
Roozbeh Pournader9c133072017-07-26 22:36:27 -07001876 if (mCursorDrawable != null) {
1877 mCursorDrawable.draw(canvas);
Gilles Debunned88876a2012-03-16 17:34:04 -07001878 }
1879 if (translate) canvas.translate(0, -cursorOffsetVertical);
1880 }
1881
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09001882 void invalidateHandlesAndActionMode() {
1883 if (mSelectionModifierCursorController != null) {
1884 mSelectionModifierCursorController.invalidateHandles();
1885 }
1886 if (mInsertionPointCursorController != null) {
1887 mInsertionPointCursorController.invalidateHandle();
1888 }
1889 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01001890 invalidateActionMode();
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09001891 }
1892 }
1893
Gilles Debunneebc86af2012-04-20 15:10:47 -07001894 /**
1895 * Invalidates all the sub-display lists that overlap the specified character range
1896 */
1897 void invalidateTextDisplayList(Layout layout, int start, int end) {
Chris Craik956f3402015-04-27 16:41:00 -07001898 if (mTextRenderNodes != null && layout instanceof DynamicLayout) {
Gilles Debunneebc86af2012-04-20 15:10:47 -07001899 final int firstLine = layout.getLineForOffset(start);
1900 final int lastLine = layout.getLineForOffset(end);
1901
1902 DynamicLayout dynamicLayout = (DynamicLayout) layout;
1903 int[] blockEndLines = dynamicLayout.getBlockEndLines();
1904 int[] blockIndices = dynamicLayout.getBlockIndices();
1905 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
1906
1907 int i = 0;
1908 // Skip the blocks before firstLine
1909 while (i < numberOfBlocks) {
1910 if (blockEndLines[i] >= firstLine) break;
1911 i++;
1912 }
1913
1914 // Invalidate all subsequent blocks until lastLine is passed
1915 while (i < numberOfBlocks) {
1916 final int blockIndex = blockIndices[i];
1917 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX) {
Chris Craik956f3402015-04-27 16:41:00 -07001918 mTextRenderNodes[blockIndex].isDirty = true;
Gilles Debunneebc86af2012-04-20 15:10:47 -07001919 }
1920 if (blockEndLines[i] >= lastLine) break;
1921 i++;
1922 }
1923 }
1924 }
1925
Gilles Debunned88876a2012-03-16 17:34:04 -07001926 void invalidateTextDisplayList() {
Chris Craik956f3402015-04-27 16:41:00 -07001927 if (mTextRenderNodes != null) {
1928 for (int i = 0; i < mTextRenderNodes.length; i++) {
1929 if (mTextRenderNodes[i] != null) mTextRenderNodes[i].isDirty = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001930 }
1931 }
1932 }
1933
Roozbeh Pournader9c133072017-07-26 22:36:27 -07001934 void updateCursorPosition() {
Gilles Debunned88876a2012-03-16 17:34:04 -07001935 if (mTextView.mCursorDrawableRes == 0) {
Roozbeh Pournader9c133072017-07-26 22:36:27 -07001936 mCursorDrawable = null;
Gilles Debunned88876a2012-03-16 17:34:04 -07001937 return;
1938 }
1939
Roozbeh Pournader9c133072017-07-26 22:36:27 -07001940 final Layout layout = mTextView.getLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -07001941 final int offset = mTextView.getSelectionStart();
1942 final int line = layout.getLineForOffset(offset);
1943 final int top = layout.getLineTop(line);
Siyamed Sinira60b59d2017-07-26 09:26:41 -07001944 final int bottom = layout.getLineBottomWithoutSpacing(line);
Gilles Debunned88876a2012-03-16 17:34:04 -07001945
Roozbeh Pournader9c133072017-07-26 22:36:27 -07001946 final boolean clamped = layout.shouldClampCursor(line);
1947 updateCursorPosition(top, bottom, layout.getPrimaryHorizontal(offset, clamped));
Gilles Debunned88876a2012-03-16 17:34:04 -07001948 }
1949
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001950 void refreshTextActionMode() {
1951 if (extractedTextModeWillBeStarted()) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001952 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001953 return;
1954 }
1955 final boolean hasSelection = mTextView.hasSelection();
1956 final SelectionModifierCursorController selectionController = getSelectionController();
1957 final InsertionPointCursorController insertionController = getInsertionController();
1958 if ((selectionController != null && selectionController.isCursorBeingModified())
1959 || (insertionController != null && insertionController.isCursorBeingModified())) {
1960 // ActionMode should be managed by the currently active cursor controller.
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001961 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001962 return;
1963 }
1964 if (hasSelection) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001965 hideInsertionPointCursorController();
1966 if (mTextActionMode == null) {
Keisuke Kuroyanagi0fd28c92016-04-04 17:43:06 +09001967 if (mRestartActionModeOnNextRefresh) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001968 // To avoid distraction, newly start action mode only when selection action
Keisuke Kuroyanagi0fd28c92016-04-04 17:43:06 +09001969 // mode is being restarted.
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01001970 startSelectionActionModeAsync(false);
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001971 }
1972 } else if (selectionController == null || !selectionController.isActive()) {
1973 // Insertion action mode is active. Avoid dismissing the selection.
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001974 stopTextActionModeWithPreservingSelection();
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01001975 startSelectionActionModeAsync(false);
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001976 } else {
1977 mTextActionMode.invalidateContentRect();
1978 }
1979 } else {
1980 // Insertion action mode is started only when insertion controller is explicitly
1981 // activated.
1982 if (insertionController == null || !insertionController.isActive()) {
1983 stopTextActionMode();
1984 } else if (mTextActionMode != null) {
1985 mTextActionMode.invalidateContentRect();
1986 }
1987 }
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001988 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001989 }
1990
Gilles Debunned88876a2012-03-16 17:34:04 -07001991 /**
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001992 * Start an Insertion action mode.
Gilles Debunned88876a2012-03-16 17:34:04 -07001993 */
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001994 void startInsertionActionMode() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001995 if (mInsertionActionModeRunnable != null) {
1996 mTextView.removeCallbacks(mInsertionActionModeRunnable);
1997 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01001998 if (extractedTextModeWillBeStarted()) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001999 return;
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002000 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002001 stopTextActionMode();
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002002
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002003 ActionMode.Callback actionModeCallback =
2004 new TextActionModeCallback(false /* hasSelection */);
2005 mTextActionMode = mTextView.startActionMode(
Clara Bayarrib8ed5b72015-04-09 15:26:41 +01002006 actionModeCallback, ActionMode.TYPE_FLOATING);
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002007 if (mTextActionMode != null && getInsertionController() != null) {
2008 getInsertionController().show();
2009 }
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00002010 }
2011
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002012 @NonNull
2013 TextView getTextView() {
2014 return mTextView;
2015 }
2016
2017 @Nullable
2018 ActionMode getTextActionMode() {
2019 return mTextActionMode;
2020 }
2021
2022 void setRestartActionModeOnNextRefresh(boolean value) {
2023 mRestartActionModeOnNextRefresh = value;
2024 }
2025
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00002026 /**
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002027 * Asynchronously starts a selection action mode using the TextClassifier.
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00002028 */
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002029 void startSelectionActionModeAsync(boolean adjustSelection) {
2030 getSelectionActionModeHelper().startActionModeAsync(adjustSelection);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002031 }
2032
2033 /**
2034 * Asynchronously invalidates an action mode using the TextClassifier.
2035 */
Abodunrinwa Toki4ce651e2017-05-12 15:37:29 +01002036 void invalidateActionModeAsync() {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002037 getSelectionActionModeHelper().invalidateActionModeAsync();
2038 }
2039
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002040 /**
2041 * Synchronously invalidates an action mode without the TextClassifier.
2042 */
2043 private void invalidateActionMode() {
2044 if (mTextActionMode != null) {
2045 mTextActionMode.invalidate();
2046 }
2047 }
2048
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002049 private SelectionActionModeHelper getSelectionActionModeHelper() {
2050 if (mSelectionActionModeHelper == null) {
2051 mSelectionActionModeHelper = new SelectionActionModeHelper(this);
Clara Bayarri578286f2015-04-10 15:35:31 +01002052 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002053 return mSelectionActionModeHelper;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00002054 }
2055
Clara Bayarridfac4432015-05-15 12:18:24 +01002056 /**
2057 * If the TextView allows text selection, selects the current word when no existing selection
2058 * was available and starts a drag.
2059 *
2060 * @return true if the drag was started.
2061 */
2062 private boolean selectCurrentWordAndStartDrag() {
Clara Bayarri7184c8a2015-06-05 17:34:09 +01002063 if (mInsertionActionModeRunnable != null) {
2064 mTextView.removeCallbacks(mInsertionActionModeRunnable);
2065 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002066 if (extractedTextModeWillBeStarted()) {
Clara Bayarridfac4432015-05-15 12:18:24 +01002067 return false;
2068 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002069 if (!checkField()) {
Clara Bayarridfac4432015-05-15 12:18:24 +01002070 return false;
2071 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002072 if (!mTextView.hasSelection() && !selectCurrentWord()) {
2073 // No selection and cannot select a word.
2074 return false;
2075 }
2076 stopTextActionModeWithPreservingSelection();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08002077 getSelectionController().enterDrag(
2078 SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_WORD);
Clara Bayarridfac4432015-05-15 12:18:24 +01002079 return true;
2080 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002081
Clara Bayarridfac4432015-05-15 12:18:24 +01002082 /**
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002083 * Checks whether a selection can be performed on the current TextView.
Clara Bayarridfac4432015-05-15 12:18:24 +01002084 *
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002085 * @return true if a selection can be performed
Clara Bayarridfac4432015-05-15 12:18:24 +01002086 */
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002087 boolean checkField() {
Clara Bayarridfac4432015-05-15 12:18:24 +01002088 if (!mTextView.canSelectText() || !mTextView.requestFocus()) {
2089 Log.w(TextView.LOG_TAG,
2090 "TextView does not support text selection. Selection cancelled.");
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002091 return false;
2092 }
Clara Bayarridfac4432015-05-15 12:18:24 +01002093 return true;
2094 }
2095
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002096 boolean startSelectionActionModeInternal() {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002097 if (extractedTextModeWillBeStarted()) {
2098 return false;
2099 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002100 if (mTextActionMode != null) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002101 // Text action mode is already started
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002102 invalidateActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07002103 return false;
2104 }
2105
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002106 if (!checkField() || !mTextView.hasSelection()) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002107 return false;
2108 }
2109
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002110 ActionMode.Callback actionModeCallback =
2111 new TextActionModeCallback(true /* hasSelection */);
2112 mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING);
Gilles Debunned88876a2012-03-16 17:34:04 -07002113
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002114 final boolean selectionStarted = mTextActionMode != null;
Gilles Debunne3473b2b2012-04-20 16:21:10 -07002115 if (selectionStarted && !mTextView.isTextSelectable() && mShowSoftInputOnFocus) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002116 // Show the IME to be able to replace text, except when selecting non editable text.
2117 final InputMethodManager imm = InputMethodManager.peekInstance();
2118 if (imm != null) {
2119 imm.showSoftInput(mTextView, 0, null);
2120 }
2121 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002122 return selectionStarted;
2123 }
2124
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09002125 private boolean extractedTextModeWillBeStarted() {
Andrei Stingaceanub1891b32015-06-19 16:44:37 +01002126 if (!(mTextView.isInExtractedMode())) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002127 final InputMethodManager imm = InputMethodManager.peekInstance();
2128 return imm != null && imm.isFullscreenMode();
2129 }
2130 return false;
2131 }
2132
2133 /**
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002134 * @return <code>true</code> if it's reasonable to offer to show suggestions depending on
2135 * the current cursor position or selection range. This method is consistent with the
2136 * method to show suggestions {@link SuggestionsPopupWindow#updateSuggestions}.
Gilles Debunned88876a2012-03-16 17:34:04 -07002137 */
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002138 private boolean shouldOfferToShowSuggestions() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002139 CharSequence text = mTextView.getText();
2140 if (!(text instanceof Spannable)) return false;
2141
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002142 final Spannable spannable = (Spannable) text;
2143 final int selectionStart = mTextView.getSelectionStart();
2144 final int selectionEnd = mTextView.getSelectionEnd();
2145 final SuggestionSpan[] suggestionSpans = spannable.getSpans(selectionStart, selectionEnd,
2146 SuggestionSpan.class);
2147 if (suggestionSpans.length == 0) {
2148 return false;
2149 }
2150 if (selectionStart == selectionEnd) {
2151 // Spans overlap the cursor.
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002152 for (int i = 0; i < suggestionSpans.length; i++) {
2153 if (suggestionSpans[i].getSuggestions().length > 0) {
2154 return true;
2155 }
2156 }
2157 return false;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002158 }
2159 int minSpanStart = mTextView.getText().length();
2160 int maxSpanEnd = 0;
2161 int unionOfSpansCoveringSelectionStartStart = mTextView.getText().length();
2162 int unionOfSpansCoveringSelectionStartEnd = 0;
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002163 boolean hasValidSuggestions = false;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002164 for (int i = 0; i < suggestionSpans.length; i++) {
2165 final int spanStart = spannable.getSpanStart(suggestionSpans[i]);
2166 final int spanEnd = spannable.getSpanEnd(suggestionSpans[i]);
2167 minSpanStart = Math.min(minSpanStart, spanStart);
2168 maxSpanEnd = Math.max(maxSpanEnd, spanEnd);
2169 if (selectionStart < spanStart || selectionStart > spanEnd) {
2170 // The span doesn't cover the current selection start point.
2171 continue;
2172 }
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002173 hasValidSuggestions =
2174 hasValidSuggestions || suggestionSpans[i].getSuggestions().length > 0;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002175 unionOfSpansCoveringSelectionStartStart =
2176 Math.min(unionOfSpansCoveringSelectionStartStart, spanStart);
2177 unionOfSpansCoveringSelectionStartEnd =
2178 Math.max(unionOfSpansCoveringSelectionStartEnd, spanEnd);
2179 }
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002180 if (!hasValidSuggestions) {
2181 return false;
2182 }
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002183 if (unionOfSpansCoveringSelectionStartStart >= unionOfSpansCoveringSelectionStartEnd) {
2184 // No spans cover the selection start point.
2185 return false;
2186 }
2187 if (minSpanStart < unionOfSpansCoveringSelectionStartStart
2188 || maxSpanEnd > unionOfSpansCoveringSelectionStartEnd) {
2189 // There is a span that is not covered by the union. In this case, we soouldn't offer
2190 // to show suggestions as it's confusing.
2191 return false;
2192 }
2193 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07002194 }
2195
2196 /**
2197 * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
2198 * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
2199 */
2200 private boolean isCursorInsideEasyCorrectionSpan() {
2201 Spannable spannable = (Spannable) mTextView.getText();
2202 SuggestionSpan[] suggestionSpans = spannable.getSpans(mTextView.getSelectionStart(),
2203 mTextView.getSelectionEnd(), SuggestionSpan.class);
2204 for (int i = 0; i < suggestionSpans.length; i++) {
2205 if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
2206 return true;
2207 }
2208 }
2209 return false;
2210 }
2211
2212 void onTouchUpEvent(MotionEvent event) {
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +01002213 if (getSelectionActionModeHelper().resetSelection(
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +00002214 getTextView().getOffsetForPosition(event.getX(), event.getY()))) {
2215 return;
2216 }
2217
Gilles Debunned88876a2012-03-16 17:34:04 -07002218 boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect();
Mady Mellora2861452015-06-25 08:40:27 -07002219 hideCursorAndSpanControllers();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002220 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07002221 CharSequence text = mTextView.getText();
2222 if (!selectAllGotFocus && text.length() > 0) {
2223 // Move cursor
2224 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
2225 Selection.setSelection((Spannable) text, offset);
2226 if (mSpellChecker != null) {
2227 // When the cursor moves, the word that was typed may need spell check
2228 mSpellChecker.onSelectionChanged();
2229 }
Andrei Stingaceanu35c550c2015-05-07 16:49:49 +01002230
Gilles Debunned88876a2012-03-16 17:34:04 -07002231 if (!extractedTextModeWillBeStarted()) {
2232 if (isCursorInsideEasyCorrectionSpan()) {
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01002233 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002234 if (mInsertionActionModeRunnable != null) {
2235 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01002236 }
2237
Gilles Debunned88876a2012-03-16 17:34:04 -07002238 mShowSuggestionRunnable = new Runnable() {
2239 public void run() {
Keisuke Kuroyanagi713be062016-02-29 16:07:54 -08002240 replace();
Gilles Debunned88876a2012-03-16 17:34:04 -07002241 }
2242 };
2243 // removeCallbacks is performed on every touch
2244 mTextView.postDelayed(mShowSuggestionRunnable,
2245 ViewConfiguration.getDoubleTapTimeout());
2246 } else if (hasInsertionController()) {
2247 getInsertionController().show();
2248 }
2249 }
2250 }
2251 }
2252
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002253 protected void stopTextActionMode() {
2254 if (mTextActionMode != null) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002255 // This will hide the mSelectionModifierCursorController
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002256 mTextActionMode.finish();
Gilles Debunned88876a2012-03-16 17:34:04 -07002257 }
2258 }
2259
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002260 private void stopTextActionModeWithPreservingSelection() {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002261 if (mTextActionMode != null) {
2262 mRestartActionModeOnNextRefresh = true;
2263 }
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002264 mPreserveSelection = true;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002265 stopTextActionMode();
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002266 mPreserveSelection = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002267 }
2268
Gilles Debunned88876a2012-03-16 17:34:04 -07002269 /**
2270 * @return True if this view supports insertion handles.
2271 */
2272 boolean hasInsertionController() {
2273 return mInsertionControllerEnabled;
2274 }
2275
2276 /**
2277 * @return True if this view supports selection handles.
2278 */
2279 boolean hasSelectionController() {
2280 return mSelectionControllerEnabled;
2281 }
2282
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09002283 private InsertionPointCursorController getInsertionController() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002284 if (!mInsertionControllerEnabled) {
2285 return null;
2286 }
2287
2288 if (mInsertionPointCursorController == null) {
2289 mInsertionPointCursorController = new InsertionPointCursorController();
2290
2291 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
2292 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
2293 }
2294
2295 return mInsertionPointCursorController;
2296 }
2297
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002298 @Nullable
2299 SelectionModifierCursorController getSelectionController() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002300 if (!mSelectionControllerEnabled) {
2301 return null;
2302 }
2303
2304 if (mSelectionModifierCursorController == null) {
2305 mSelectionModifierCursorController = new SelectionModifierCursorController();
2306
2307 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
2308 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
2309 }
2310
2311 return mSelectionModifierCursorController;
2312 }
2313
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002314 @VisibleForTesting
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002315 @Nullable
2316 public Drawable getCursorDrawable() {
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002317 return mCursorDrawable;
2318 }
2319
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002320 private void updateCursorPosition(int top, int bottom, float horizontal) {
2321 if (mCursorDrawable == null) {
2322 mCursorDrawable = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07002323 mTextView.mCursorDrawableRes);
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002324 }
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002325 final int left = clampHorizontalPosition(mCursorDrawable, horizontal);
2326 final int width = mCursorDrawable.getIntrinsicWidth();
2327 mCursorDrawable.setBounds(left, top - mTempRect.top, left + width,
Gilles Debunned88876a2012-03-16 17:34:04 -07002328 bottom + mTempRect.bottom);
2329 }
2330
2331 /**
Siyamed Sinir987ec652016-02-17 19:44:41 -08002332 * Return clamped position for the drawable. If the drawable is within the boundaries of the
2333 * 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 -08002334 * 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 -08002335 * the view boundary. If the drawable is null, horizontal parameter is aligned to left or right
2336 * of the view.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002337 *
Siyamed Sinir987ec652016-02-17 19:44:41 -08002338 * @param drawable Drawable. Can be null.
2339 * @param horizontal Horizontal position for the drawable.
2340 * @return The clamped horizontal position for the drawable.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002341 */
Siyamed Sinir987ec652016-02-17 19:44:41 -08002342 private int clampHorizontalPosition(@Nullable final Drawable drawable, float horizontal) {
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002343 horizontal = Math.max(0.5f, horizontal - 0.5f);
2344 if (mTempRect == null) mTempRect = new Rect();
Siyamed Sinir987ec652016-02-17 19:44:41 -08002345
2346 int drawableWidth = 0;
2347 if (drawable != null) {
2348 drawable.getPadding(mTempRect);
2349 drawableWidth = drawable.getIntrinsicWidth();
2350 } else {
2351 mTempRect.setEmpty();
2352 }
2353
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002354 int scrollX = mTextView.getScrollX();
2355 float horizontalDiff = horizontal - scrollX;
2356 int viewClippedWidth = mTextView.getWidth() - mTextView.getCompoundPaddingLeft()
2357 - mTextView.getCompoundPaddingRight();
2358
2359 final int left;
2360 if (horizontalDiff >= (viewClippedWidth - 1f)) {
2361 // at the rightmost position
Siyamed Sinir987ec652016-02-17 19:44:41 -08002362 left = viewClippedWidth + scrollX - (drawableWidth - mTempRect.right);
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002363 } else if (Math.abs(horizontalDiff) <= 1f
2364 || (TextUtils.isEmpty(mTextView.getText())
Siyamed Sinir987ec652016-02-17 19:44:41 -08002365 && (TextView.VERY_WIDE - scrollX) <= (viewClippedWidth + 1f)
2366 && horizontal <= 1f)) {
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002367 // at the leftmost position
2368 left = scrollX - mTempRect.left;
2369 } else {
2370 left = (int) horizontal - mTempRect.left;
2371 }
2372 return left;
2373 }
2374
2375 /**
Gilles Debunned88876a2012-03-16 17:34:04 -07002376 * 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 -08002377 * a dictionary) from the current input method, provided by it calling
Gilles Debunned88876a2012-03-16 17:34:04 -07002378 * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
2379 * implementation flashes the background of the corrected word to provide feedback to the user.
2380 *
2381 * @param info The auto correct info about the text that was corrected.
2382 */
2383 public void onCommitCorrection(CorrectionInfo info) {
2384 if (mCorrectionHighlighter == null) {
2385 mCorrectionHighlighter = new CorrectionHighlighter();
2386 } else {
2387 mCorrectionHighlighter.invalidate(false);
2388 }
2389
2390 mCorrectionHighlighter.highlight(info);
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002391 mUndoInputFilter.freezeLastEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07002392 }
2393
Gilles Debunned88876a2012-03-16 17:34:04 -07002394 void onScrollChanged() {
Gilles Debunne157aafc2012-04-19 17:21:57 -07002395 if (mPositionListener != null) {
2396 mPositionListener.onScrollChanged();
2397 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002398 if (mTextActionMode != null) {
2399 mTextActionMode.invalidateContentRect();
Abodunrinwa Toki56195db2015-04-22 06:46:54 +01002400 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002401 }
2402
2403 /**
2404 * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
2405 */
2406 private boolean shouldBlink() {
2407 if (!isCursorVisible() || !mTextView.isFocused()) return false;
2408
2409 final int start = mTextView.getSelectionStart();
2410 if (start < 0) return false;
2411
2412 final int end = mTextView.getSelectionEnd();
2413 if (end < 0) return false;
2414
2415 return start == end;
2416 }
2417
2418 void makeBlink() {
2419 if (shouldBlink()) {
2420 mShowCursor = SystemClock.uptimeMillis();
2421 if (mBlink == null) mBlink = new Blink();
John Reckd0374c62015-10-20 13:25:01 -07002422 mTextView.removeCallbacks(mBlink);
2423 mTextView.postDelayed(mBlink, BLINK);
Gilles Debunned88876a2012-03-16 17:34:04 -07002424 } else {
John Reckd0374c62015-10-20 13:25:01 -07002425 if (mBlink != null) mTextView.removeCallbacks(mBlink);
Gilles Debunned88876a2012-03-16 17:34:04 -07002426 }
2427 }
2428
John Reckd0374c62015-10-20 13:25:01 -07002429 private class Blink implements Runnable {
Gilles Debunned88876a2012-03-16 17:34:04 -07002430 private boolean mCancelled;
2431
2432 public void run() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002433 if (mCancelled) {
2434 return;
2435 }
2436
John Reckd0374c62015-10-20 13:25:01 -07002437 mTextView.removeCallbacks(this);
Gilles Debunned88876a2012-03-16 17:34:04 -07002438
2439 if (shouldBlink()) {
2440 if (mTextView.getLayout() != null) {
2441 mTextView.invalidateCursorPath();
2442 }
2443
John Reckd0374c62015-10-20 13:25:01 -07002444 mTextView.postDelayed(this, BLINK);
Gilles Debunned88876a2012-03-16 17:34:04 -07002445 }
2446 }
2447
2448 void cancel() {
2449 if (!mCancelled) {
John Reckd0374c62015-10-20 13:25:01 -07002450 mTextView.removeCallbacks(this);
Gilles Debunned88876a2012-03-16 17:34:04 -07002451 mCancelled = true;
2452 }
2453 }
2454
2455 void uncancel() {
2456 mCancelled = false;
2457 }
2458 }
2459
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002460 private DragShadowBuilder getTextThumbnailBuilder(int start, int end) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002461 TextView shadowView = (TextView) View.inflate(mTextView.getContext(),
2462 com.android.internal.R.layout.text_drag_thumbnail, null);
2463
2464 if (shadowView == null) {
2465 throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
2466 }
2467
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002468 if (end - start > DRAG_SHADOW_MAX_TEXT_LENGTH) {
2469 final long range = getCharClusterRange(start + DRAG_SHADOW_MAX_TEXT_LENGTH);
2470 end = TextUtils.unpackRangeEndFromLong(range);
Gilles Debunned88876a2012-03-16 17:34:04 -07002471 }
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002472 final CharSequence text = mTextView.getTransformedText(start, end);
Gilles Debunned88876a2012-03-16 17:34:04 -07002473 shadowView.setText(text);
2474 shadowView.setTextColor(mTextView.getTextColors());
2475
Alan Viverettebb98ebd2015-05-08 17:17:44 -07002476 shadowView.setTextAppearance(R.styleable.Theme_textAppearanceLarge);
Gilles Debunned88876a2012-03-16 17:34:04 -07002477 shadowView.setGravity(Gravity.CENTER);
2478
2479 shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
2480 ViewGroup.LayoutParams.WRAP_CONTENT));
2481
2482 final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
2483 shadowView.measure(size, size);
2484
2485 shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
2486 shadowView.invalidate();
2487 return new DragShadowBuilder(shadowView);
2488 }
2489
2490 private static class DragLocalState {
2491 public TextView sourceTextView;
2492 public int start, end;
2493
2494 public DragLocalState(TextView sourceTextView, int start, int end) {
2495 this.sourceTextView = sourceTextView;
2496 this.start = start;
2497 this.end = end;
2498 }
2499 }
2500
2501 void onDrop(DragEvent event) {
Ben Murdoch3dac4602017-01-17 11:27:37 +00002502 SpannableStringBuilder content = new SpannableStringBuilder();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002503
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -07002504 final DragAndDropPermissions permissions = DragAndDropPermissions.obtain(event);
2505 if (permissions != null) {
2506 permissions.takeTransient();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002507 }
2508
2509 try {
2510 ClipData clipData = event.getClipData();
2511 final int itemCount = clipData.getItemCount();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002512 for (int i = 0; i < itemCount; i++) {
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002513 Item item = clipData.getItemAt(i);
2514 content.append(item.coerceToStyledText(mTextView.getContext()));
2515 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002516 } finally {
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -07002517 if (permissions != null) {
2518 permissions.release();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002519 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002520 }
2521
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002522 mTextView.beginBatchEdit();
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002523 mUndoInputFilter.freezeLastEdit();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002524 try {
2525 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
2526 Object localState = event.getLocalState();
2527 DragLocalState dragLocalState = null;
2528 if (localState instanceof DragLocalState) {
2529 dragLocalState = (DragLocalState) localState;
Gilles Debunned88876a2012-03-16 17:34:04 -07002530 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002531 boolean dragDropIntoItself = dragLocalState != null
2532 && dragLocalState.sourceTextView == mTextView;
Gilles Debunned88876a2012-03-16 17:34:04 -07002533
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002534 if (dragDropIntoItself) {
2535 if (offset >= dragLocalState.start && offset < dragLocalState.end) {
2536 // A drop inside the original selection discards the drop.
2537 return;
2538 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002539 }
2540
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002541 final int originalLength = mTextView.getText().length();
2542 int min = offset;
2543 int max = offset;
2544
2545 Selection.setSelection((Spannable) mTextView.getText(), max);
2546 mTextView.replaceText_internal(min, max, content);
2547
2548 if (dragDropIntoItself) {
2549 int dragSourceStart = dragLocalState.start;
2550 int dragSourceEnd = dragLocalState.end;
2551 if (max <= dragSourceStart) {
2552 // Inserting text before selection has shifted positions
2553 final int shift = mTextView.getText().length() - originalLength;
2554 dragSourceStart += shift;
2555 dragSourceEnd += shift;
2556 }
2557
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08002558 // Delete original selection
2559 mTextView.deleteText_internal(dragSourceStart, dragSourceEnd);
Gilles Debunned88876a2012-03-16 17:34:04 -07002560
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08002561 // Make sure we do not leave two adjacent spaces.
2562 final int prevCharIdx = Math.max(0, dragSourceStart - 1);
2563 final int nextCharIdx = Math.min(mTextView.getText().length(), dragSourceStart + 1);
2564 if (nextCharIdx > prevCharIdx + 1) {
2565 CharSequence t = mTextView.getTransformedText(prevCharIdx, nextCharIdx);
2566 if (Character.isSpaceChar(t.charAt(0)) && Character.isSpaceChar(t.charAt(1))) {
2567 mTextView.deleteText_internal(prevCharIdx, prevCharIdx + 1);
2568 }
Victoria Lease91373202012-09-07 16:41:59 -07002569 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002570 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002571 } finally {
2572 mTextView.endBatchEdit();
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002573 mUndoInputFilter.freezeLastEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07002574 }
2575 }
2576
Gilles Debunnec62589c2012-04-12 14:50:23 -07002577 public void addSpanWatchers(Spannable text) {
2578 final int textLength = text.length();
2579
2580 if (mKeyListener != null) {
2581 text.setSpan(mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
2582 }
2583
Jean Chalardbaf30942013-02-28 16:01:51 -08002584 if (mSpanController == null) {
2585 mSpanController = new SpanController();
Gilles Debunnec62589c2012-04-12 14:50:23 -07002586 }
Jean Chalardbaf30942013-02-28 16:01:51 -08002587 text.setSpan(mSpanController, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002588 }
2589
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002590 void setContextMenuAnchor(float x, float y) {
2591 mContextMenuAnchorX = x;
2592 mContextMenuAnchorY = y;
2593 }
2594
2595 void onCreateContextMenu(ContextMenu menu) {
2596 if (mIsBeingLongClicked || Float.isNaN(mContextMenuAnchorX)
2597 || Float.isNaN(mContextMenuAnchorY)) {
2598 return;
2599 }
2600 final int offset = mTextView.getOffsetForPosition(mContextMenuAnchorX, mContextMenuAnchorY);
2601 if (offset == -1) {
2602 return;
2603 }
Siyamed Sinir532f3c92017-06-15 18:22:31 -07002604
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002605 stopTextActionModeWithPreservingSelection();
Siyamed Sinir532f3c92017-06-15 18:22:31 -07002606 if (mTextView.canSelectText()) {
2607 final boolean isOnSelection = mTextView.hasSelection()
2608 && offset >= mTextView.getSelectionStart()
2609 && offset <= mTextView.getSelectionEnd();
2610 if (!isOnSelection) {
2611 // Right clicked position is not on the selection. Remove the selection and move the
2612 // cursor to the right clicked position.
2613 Selection.setSelection((Spannable) mTextView.getText(), offset);
2614 stopTextActionMode();
2615 }
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002616 }
2617
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002618 if (shouldOfferToShowSuggestions()) {
Keisuke Kuroyanagi182f5fe2016-03-11 16:31:29 +09002619 final SuggestionInfo[] suggestionInfoArray =
2620 new SuggestionInfo[SuggestionSpan.SUGGESTIONS_MAX_SIZE];
2621 for (int i = 0; i < suggestionInfoArray.length; i++) {
2622 suggestionInfoArray[i] = new SuggestionInfo();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002623 }
2624 final SubMenu subMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, MENU_ITEM_ORDER_REPLACE,
2625 com.android.internal.R.string.replace);
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002626 final int numItems = mSuggestionHelper.getSuggestionInfo(suggestionInfoArray, null);
Keisuke Kuroyanagi182f5fe2016-03-11 16:31:29 +09002627 for (int i = 0; i < numItems; i++) {
2628 final SuggestionInfo info = suggestionInfoArray[i];
2629 subMenu.add(Menu.NONE, Menu.NONE, i, info.mText)
2630 .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
2631 @Override
2632 public boolean onMenuItemClick(MenuItem item) {
2633 replaceWithSuggestion(info);
2634 return true;
2635 }
2636 });
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002637 }
2638 }
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002639
2640 menu.add(Menu.NONE, TextView.ID_UNDO, MENU_ITEM_ORDER_UNDO,
2641 com.android.internal.R.string.undo)
2642 .setAlphabeticShortcut('z')
2643 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2644 .setEnabled(mTextView.canUndo());
2645 menu.add(Menu.NONE, TextView.ID_REDO, MENU_ITEM_ORDER_REDO,
2646 com.android.internal.R.string.redo)
2647 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2648 .setEnabled(mTextView.canRedo());
2649
2650 menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
2651 com.android.internal.R.string.cut)
2652 .setAlphabeticShortcut('x')
2653 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2654 .setEnabled(mTextView.canCut());
2655 menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
2656 com.android.internal.R.string.copy)
2657 .setAlphabeticShortcut('c')
2658 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2659 .setEnabled(mTextView.canCopy());
2660 menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
2661 com.android.internal.R.string.paste)
2662 .setAlphabeticShortcut('v')
2663 .setEnabled(mTextView.canPaste())
2664 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01002665 menu.add(Menu.NONE, TextView.ID_PASTE_AS_PLAIN_TEXT, MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002666 com.android.internal.R.string.paste_as_plain_text)
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01002667 .setEnabled(mTextView.canPasteAsPlainText())
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002668 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2669 menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
2670 com.android.internal.R.string.share)
2671 .setEnabled(mTextView.canShare())
2672 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2673 menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
2674 com.android.internal.R.string.selectAll)
2675 .setAlphabeticShortcut('a')
2676 .setEnabled(mTextView.canSelectAllText())
2677 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Felipe Leme2ac463e2017-03-13 14:06:25 -07002678 menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
Felipe Leme555bcac2017-06-26 12:53:56 -07002679 android.R.string.autofill)
Felipe Leme2ac463e2017-03-13 14:06:25 -07002680 .setEnabled(mTextView.canRequestAutofill())
2681 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002682
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002683 mPreserveSelection = true;
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002684 }
2685
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002686 @Nullable
2687 private SuggestionSpan findEquivalentSuggestionSpan(
2688 @NonNull SuggestionSpanInfo suggestionSpanInfo) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002689 final Editable editable = (Editable) mTextView.getText();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002690 if (editable.getSpanStart(suggestionSpanInfo.mSuggestionSpan) >= 0) {
2691 // Exactly same span is found.
2692 return suggestionSpanInfo.mSuggestionSpan;
2693 }
2694 // Suggestion span couldn't be found. Try to find a suggestion span that has the same
2695 // contents.
2696 final SuggestionSpan[] suggestionSpans = editable.getSpans(suggestionSpanInfo.mSpanStart,
2697 suggestionSpanInfo.mSpanEnd, SuggestionSpan.class);
2698 for (final SuggestionSpan suggestionSpan : suggestionSpans) {
2699 final int start = editable.getSpanStart(suggestionSpan);
2700 if (start != suggestionSpanInfo.mSpanStart) {
2701 continue;
2702 }
2703 final int end = editable.getSpanEnd(suggestionSpan);
2704 if (end != suggestionSpanInfo.mSpanEnd) {
2705 continue;
2706 }
2707 if (suggestionSpan.equals(suggestionSpanInfo.mSuggestionSpan)) {
2708 return suggestionSpan;
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08002709 }
2710 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002711 return null;
2712 }
2713
2714 private void replaceWithSuggestion(@NonNull final SuggestionInfo suggestionInfo) {
2715 final SuggestionSpan targetSuggestionSpan = findEquivalentSuggestionSpan(
2716 suggestionInfo.mSuggestionSpanInfo);
2717 if (targetSuggestionSpan == null) {
2718 // Span has been removed
2719 return;
2720 }
2721 final Editable editable = (Editable) mTextView.getText();
2722 final int spanStart = editable.getSpanStart(targetSuggestionSpan);
2723 final int spanEnd = editable.getSpanEnd(targetSuggestionSpan);
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08002724 if (spanStart < 0 || spanEnd <= spanStart) {
2725 // Span has been removed
2726 return;
2727 }
2728
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002729 final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
2730 // SuggestionSpans are removed by replace: save them before
2731 SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
2732 SuggestionSpan.class);
2733 final int length = suggestionSpans.length;
2734 int[] suggestionSpansStarts = new int[length];
2735 int[] suggestionSpansEnds = new int[length];
2736 int[] suggestionSpansFlags = new int[length];
2737 for (int i = 0; i < length; i++) {
2738 final SuggestionSpan suggestionSpan = suggestionSpans[i];
2739 suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
2740 suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
2741 suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
2742
2743 // Remove potential misspelled flags
2744 int suggestionSpanFlags = suggestionSpan.getFlags();
2745 if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
2746 suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
2747 suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
2748 suggestionSpan.setFlags(suggestionSpanFlags);
2749 }
2750 }
2751
2752 // Notify source IME of the suggestion pick. Do this before swapping texts.
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002753 targetSuggestionSpan.notifySelection(
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002754 mTextView.getContext(), originalText, suggestionInfo.mSuggestionIndex);
2755
2756 // Swap text content between actual text and Suggestion span
2757 final int suggestionStart = suggestionInfo.mSuggestionStart;
2758 final int suggestionEnd = suggestionInfo.mSuggestionEnd;
2759 final String suggestion = suggestionInfo.mText.subSequence(
2760 suggestionStart, suggestionEnd).toString();
2761 mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
2762
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002763 String[] suggestions = targetSuggestionSpan.getSuggestions();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002764 suggestions[suggestionInfo.mSuggestionIndex] = originalText;
2765
2766 // Restore previous SuggestionSpans
2767 final int lengthDelta = suggestion.length() - (spanEnd - spanStart);
2768 for (int i = 0; i < length; i++) {
2769 // Only spans that include the modified region make sense after replacement
2770 // Spans partially included in the replaced region are removed, there is no
2771 // way to assign them a valid range after replacement
2772 if (suggestionSpansStarts[i] <= spanStart && suggestionSpansEnds[i] >= spanEnd) {
2773 mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
2774 suggestionSpansEnds[i] + lengthDelta, suggestionSpansFlags[i]);
2775 }
2776 }
2777 // Move cursor at the end of the replaced word
2778 final int newCursorPosition = spanEnd + lengthDelta;
2779 mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
2780 }
2781
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002782 private final MenuItem.OnMenuItemClickListener mOnContextMenuItemClickListener =
2783 new MenuItem.OnMenuItemClickListener() {
2784 @Override
2785 public boolean onMenuItemClick(MenuItem item) {
2786 if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
2787 return true;
2788 }
2789 return mTextView.onTextContextMenuItem(item.getItemId());
2790 }
2791 };
2792
Gilles Debunned88876a2012-03-16 17:34:04 -07002793 /**
2794 * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
2795 * pop-up should be displayed.
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07002796 * Also monitors {@link Selection} to call back to the attached input method.
Gilles Debunned88876a2012-03-16 17:34:04 -07002797 */
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09002798 private class SpanController implements SpanWatcher {
Gilles Debunned88876a2012-03-16 17:34:04 -07002799
2800 private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
2801
2802 private EasyEditPopupWindow mPopupWindow;
2803
Gilles Debunned88876a2012-03-16 17:34:04 -07002804 private Runnable mHidePopup;
2805
Jean Chalardbaf30942013-02-28 16:01:51 -08002806 // This function is pure but inner classes can't have static functions
2807 private boolean isNonIntermediateSelectionSpan(final Spannable text,
2808 final Object span) {
2809 return (Selection.SELECTION_START == span || Selection.SELECTION_END == span)
2810 && (text.getSpanFlags(span) & Spanned.SPAN_INTERMEDIATE) == 0;
2811 }
2812
Gilles Debunnec62589c2012-04-12 14:50:23 -07002813 @Override
2814 public void onSpanAdded(Spannable text, Object span, int start, int end) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002815 if (isNonIntermediateSelectionSpan(text, span)) {
2816 sendUpdateSelection();
2817 } else if (span instanceof EasyEditSpan) {
Gilles Debunnec62589c2012-04-12 14:50:23 -07002818 if (mPopupWindow == null) {
2819 mPopupWindow = new EasyEditPopupWindow();
2820 mHidePopup = new Runnable() {
2821 @Override
2822 public void run() {
2823 hide();
2824 }
2825 };
2826 }
2827
2828 // Make sure there is only at most one EasyEditSpan in the text
2829 if (mPopupWindow.mEasyEditSpan != null) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002830 mPopupWindow.mEasyEditSpan.setDeleteEnabled(false);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002831 }
2832
2833 mPopupWindow.setEasyEditSpan((EasyEditSpan) span);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002834 mPopupWindow.setOnDeleteListener(new EasyEditDeleteListener() {
2835 @Override
2836 public void onDeleteClick(EasyEditSpan span) {
2837 Editable editable = (Editable) mTextView.getText();
2838 int start = editable.getSpanStart(span);
2839 int end = editable.getSpanEnd(span);
2840 if (start >= 0 && end >= 0) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002841 sendEasySpanNotification(EasyEditSpan.TEXT_DELETED, span);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002842 mTextView.deleteText_internal(start, end);
2843 }
2844 editable.removeSpan(span);
2845 }
2846 });
Gilles Debunnec62589c2012-04-12 14:50:23 -07002847
2848 if (mTextView.getWindowVisibility() != View.VISIBLE) {
2849 // The window is not visible yet, ignore the text change.
2850 return;
2851 }
2852
2853 if (mTextView.getLayout() == null) {
2854 // The view has not been laid out yet, ignore the text change
2855 return;
2856 }
2857
2858 if (extractedTextModeWillBeStarted()) {
2859 // The input is in extract mode. Do not handle the easy edit in
2860 // the original TextView, as the ExtractEditText will do
2861 return;
2862 }
2863
2864 mPopupWindow.show();
2865 mTextView.removeCallbacks(mHidePopup);
2866 mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
2867 }
2868 }
2869
2870 @Override
2871 public void onSpanRemoved(Spannable text, Object span, int start, int end) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002872 if (isNonIntermediateSelectionSpan(text, span)) {
2873 sendUpdateSelection();
2874 } else if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
Gilles Debunnec62589c2012-04-12 14:50:23 -07002875 hide();
2876 }
2877 }
2878
2879 @Override
2880 public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd,
2881 int newStart, int newEnd) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002882 if (isNonIntermediateSelectionSpan(text, span)) {
2883 sendUpdateSelection();
2884 } else if (mPopupWindow != null && span instanceof EasyEditSpan) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002885 EasyEditSpan easyEditSpan = (EasyEditSpan) span;
Jean Chalardbaf30942013-02-28 16:01:51 -08002886 sendEasySpanNotification(EasyEditSpan.TEXT_MODIFIED, easyEditSpan);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002887 text.removeSpan(easyEditSpan);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002888 }
2889 }
2890
Gilles Debunned88876a2012-03-16 17:34:04 -07002891 public void hide() {
2892 if (mPopupWindow != null) {
2893 mPopupWindow.hide();
2894 mTextView.removeCallbacks(mHidePopup);
2895 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002896 }
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002897
Jean Chalardbaf30942013-02-28 16:01:51 -08002898 private void sendEasySpanNotification(int textChangedType, EasyEditSpan span) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002899 try {
2900 PendingIntent pendingIntent = span.getPendingIntent();
2901 if (pendingIntent != null) {
2902 Intent intent = new Intent();
2903 intent.putExtra(EasyEditSpan.EXTRA_TEXT_CHANGED_TYPE, textChangedType);
2904 pendingIntent.send(mTextView.getContext(), 0, intent);
2905 }
2906 } catch (CanceledException e) {
2907 // This should not happen, as we should try to send the intent only once.
2908 Log.w(TAG, "PendingIntent for notification cannot be sent", e);
2909 }
2910 }
2911 }
2912
2913 /**
2914 * Listens for the delete event triggered by {@link EasyEditPopupWindow}.
2915 */
2916 private interface EasyEditDeleteListener {
2917
2918 /**
2919 * Clicks the delete pop-up.
2920 */
2921 void onDeleteClick(EasyEditSpan span);
Gilles Debunned88876a2012-03-16 17:34:04 -07002922 }
2923
2924 /**
2925 * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07002926 * by {@link SpanController}.
Gilles Debunned88876a2012-03-16 17:34:04 -07002927 */
2928 private class EasyEditPopupWindow extends PinnedPopupWindow
2929 implements OnClickListener {
2930 private static final int POPUP_TEXT_LAYOUT =
2931 com.android.internal.R.layout.text_edit_action_popup_text;
2932 private TextView mDeleteTextView;
2933 private EasyEditSpan mEasyEditSpan;
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002934 private EasyEditDeleteListener mOnDeleteListener;
Gilles Debunned88876a2012-03-16 17:34:04 -07002935
2936 @Override
2937 protected void createPopupWindow() {
2938 mPopupWindow = new PopupWindow(mTextView.getContext(), null,
2939 com.android.internal.R.attr.textSelectHandleWindowStyle);
2940 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
2941 mPopupWindow.setClippingEnabled(true);
2942 }
2943
2944 @Override
2945 protected void initContentView() {
2946 LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
2947 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
2948 mContentView = linearLayout;
2949 mContentView.setBackgroundResource(
2950 com.android.internal.R.drawable.text_edit_side_paste_window);
2951
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002952 LayoutInflater inflater = (LayoutInflater) mTextView.getContext()
2953 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
Gilles Debunned88876a2012-03-16 17:34:04 -07002954
2955 LayoutParams wrapContent = new LayoutParams(
2956 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
2957
2958 mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
2959 mDeleteTextView.setLayoutParams(wrapContent);
2960 mDeleteTextView.setText(com.android.internal.R.string.delete);
2961 mDeleteTextView.setOnClickListener(this);
2962 mContentView.addView(mDeleteTextView);
2963 }
2964
Gilles Debunnec62589c2012-04-12 14:50:23 -07002965 public void setEasyEditSpan(EasyEditSpan easyEditSpan) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002966 mEasyEditSpan = easyEditSpan;
Gilles Debunned88876a2012-03-16 17:34:04 -07002967 }
2968
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002969 private void setOnDeleteListener(EasyEditDeleteListener listener) {
2970 mOnDeleteListener = listener;
2971 }
2972
Gilles Debunned88876a2012-03-16 17:34:04 -07002973 @Override
2974 public void onClick(View view) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002975 if (view == mDeleteTextView
2976 && mEasyEditSpan != null && mEasyEditSpan.isDeleteEnabled()
2977 && mOnDeleteListener != null) {
2978 mOnDeleteListener.onDeleteClick(mEasyEditSpan);
Gilles Debunned88876a2012-03-16 17:34:04 -07002979 }
2980 }
2981
2982 @Override
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002983 public void hide() {
2984 if (mEasyEditSpan != null) {
2985 mEasyEditSpan.setDeleteEnabled(false);
2986 }
2987 mOnDeleteListener = null;
2988 super.hide();
2989 }
2990
2991 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07002992 protected int getTextOffset() {
2993 // Place the pop-up at the end of the span
2994 Editable editable = (Editable) mTextView.getText();
2995 return editable.getSpanEnd(mEasyEditSpan);
2996 }
2997
2998 @Override
2999 protected int getVerticalLocalPosition(int line) {
Siyamed Sinira60b59d2017-07-26 09:26:41 -07003000 final Layout layout = mTextView.getLayout();
3001 return layout.getLineBottomWithoutSpacing(line);
Gilles Debunned88876a2012-03-16 17:34:04 -07003002 }
3003
3004 @Override
3005 protected int clipVertically(int positionY) {
3006 // As we display the pop-up below the span, no vertical clipping is required.
3007 return positionY;
3008 }
3009 }
3010
3011 private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
3012 // 3 handles
3013 // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003014 // 1 CursorAnchorInfoNotifier
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003015 private static final int MAXIMUM_NUMBER_OF_LISTENERS = 7;
Gilles Debunned88876a2012-03-16 17:34:04 -07003016 private TextViewPositionListener[] mPositionListeners =
3017 new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003018 private boolean[] mCanMove = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
Gilles Debunned88876a2012-03-16 17:34:04 -07003019 private boolean mPositionHasChanged = true;
3020 // Absolute position of the TextView with respect to its parent window
3021 private int mPositionX, mPositionY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003022 private int mPositionXOnScreen, mPositionYOnScreen;
Gilles Debunned88876a2012-03-16 17:34:04 -07003023 private int mNumberOfListeners;
3024 private boolean mScrollHasChanged;
3025 final int[] mTempCoords = new int[2];
3026
3027 public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
3028 if (mNumberOfListeners == 0) {
3029 updatePosition();
3030 ViewTreeObserver vto = mTextView.getViewTreeObserver();
3031 vto.addOnPreDrawListener(this);
3032 }
3033
3034 int emptySlotIndex = -1;
3035 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3036 TextViewPositionListener listener = mPositionListeners[i];
3037 if (listener == positionListener) {
3038 return;
3039 } else if (emptySlotIndex < 0 && listener == null) {
3040 emptySlotIndex = i;
3041 }
3042 }
3043
3044 mPositionListeners[emptySlotIndex] = positionListener;
3045 mCanMove[emptySlotIndex] = canMove;
3046 mNumberOfListeners++;
3047 }
3048
3049 public void removeSubscriber(TextViewPositionListener positionListener) {
3050 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3051 if (mPositionListeners[i] == positionListener) {
3052 mPositionListeners[i] = null;
3053 mNumberOfListeners--;
3054 break;
3055 }
3056 }
3057
3058 if (mNumberOfListeners == 0) {
3059 ViewTreeObserver vto = mTextView.getViewTreeObserver();
3060 vto.removeOnPreDrawListener(this);
3061 }
3062 }
3063
3064 public int getPositionX() {
3065 return mPositionX;
3066 }
3067
3068 public int getPositionY() {
3069 return mPositionY;
3070 }
3071
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003072 public int getPositionXOnScreen() {
3073 return mPositionXOnScreen;
3074 }
3075
3076 public int getPositionYOnScreen() {
3077 return mPositionYOnScreen;
3078 }
3079
Gilles Debunned88876a2012-03-16 17:34:04 -07003080 @Override
3081 public boolean onPreDraw() {
3082 updatePosition();
3083
3084 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3085 if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
3086 TextViewPositionListener positionListener = mPositionListeners[i];
3087 if (positionListener != null) {
3088 positionListener.updatePosition(mPositionX, mPositionY,
3089 mPositionHasChanged, mScrollHasChanged);
3090 }
3091 }
3092 }
3093
3094 mScrollHasChanged = false;
3095 return true;
3096 }
3097
3098 private void updatePosition() {
3099 mTextView.getLocationInWindow(mTempCoords);
3100
3101 mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
3102
3103 mPositionX = mTempCoords[0];
3104 mPositionY = mTempCoords[1];
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003105
3106 mTextView.getLocationOnScreen(mTempCoords);
3107
3108 mPositionXOnScreen = mTempCoords[0];
3109 mPositionYOnScreen = mTempCoords[1];
Gilles Debunned88876a2012-03-16 17:34:04 -07003110 }
3111
3112 public void onScrollChanged() {
3113 mScrollHasChanged = true;
3114 }
3115 }
3116
3117 private abstract class PinnedPopupWindow implements TextViewPositionListener {
3118 protected PopupWindow mPopupWindow;
3119 protected ViewGroup mContentView;
3120 int mPositionX, mPositionY;
Seigo Nonaka60490d12016-01-28 17:25:18 +09003121 int mClippingLimitLeft, mClippingLimitRight;
Gilles Debunned88876a2012-03-16 17:34:04 -07003122
3123 protected abstract void createPopupWindow();
3124 protected abstract void initContentView();
3125 protected abstract int getTextOffset();
3126 protected abstract int getVerticalLocalPosition(int line);
3127 protected abstract int clipVertically(int positionY);
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003128 protected void setUp() {
3129 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003130
3131 public PinnedPopupWindow() {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003132 // Due to calling subclass methods in base constructor, subclass constructor is not
3133 // called before subclass methods, e.g. createPopupWindow or initContentView. To give
3134 // a chance to initialize subclasses, call setUp() method here.
3135 // TODO: It is good to extract non trivial initialization code from constructor.
3136 setUp();
3137
Gilles Debunned88876a2012-03-16 17:34:04 -07003138 createPopupWindow();
3139
Alan Viverette80ebe0d2015-04-30 15:53:11 -07003140 mPopupWindow.setWindowLayoutType(
3141 WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
Gilles Debunned88876a2012-03-16 17:34:04 -07003142 mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
3143 mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
3144
3145 initContentView();
3146
3147 LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
3148 ViewGroup.LayoutParams.WRAP_CONTENT);
3149 mContentView.setLayoutParams(wrapContent);
3150
3151 mPopupWindow.setContentView(mContentView);
3152 }
3153
3154 public void show() {
3155 getPositionListener().addSubscriber(this, false /* offset is fixed */);
3156
3157 computeLocalPosition();
3158
3159 final PositionListener positionListener = getPositionListener();
3160 updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
3161 }
3162
3163 protected void measureContent() {
3164 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3165 mContentView.measure(
3166 View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
3167 View.MeasureSpec.AT_MOST),
3168 View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
3169 View.MeasureSpec.AT_MOST));
3170 }
3171
3172 /* The popup window will be horizontally centered on the getTextOffset() and vertically
3173 * positioned according to viewportToContentHorizontalOffset.
3174 *
3175 * This method assumes that mContentView has properly been measured from its content. */
3176 private void computeLocalPosition() {
3177 measureContent();
3178 final int width = mContentView.getMeasuredWidth();
3179 final int offset = getTextOffset();
3180 mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f);
3181 mPositionX += mTextView.viewportToContentHorizontalOffset();
3182
3183 final int line = mTextView.getLayout().getLineForOffset(offset);
3184 mPositionY = getVerticalLocalPosition(line);
3185 mPositionY += mTextView.viewportToContentVerticalOffset();
3186 }
3187
3188 private void updatePosition(int parentPositionX, int parentPositionY) {
3189 int positionX = parentPositionX + mPositionX;
3190 int positionY = parentPositionY + mPositionY;
3191
3192 positionY = clipVertically(positionY);
3193
3194 // Horizontal clipping
3195 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3196 final int width = mContentView.getMeasuredWidth();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003197 positionX = Math.min(
3198 displayMetrics.widthPixels - width + mClippingLimitRight, positionX);
3199 positionX = Math.max(-mClippingLimitLeft, positionX);
Gilles Debunned88876a2012-03-16 17:34:04 -07003200
3201 if (isShowing()) {
3202 mPopupWindow.update(positionX, positionY, -1, -1);
3203 } else {
3204 mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
3205 positionX, positionY);
3206 }
3207 }
3208
3209 public void hide() {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09003210 if (!isShowing()) {
3211 return;
3212 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003213 mPopupWindow.dismiss();
3214 getPositionListener().removeSubscriber(this);
3215 }
3216
3217 @Override
3218 public void updatePosition(int parentPositionX, int parentPositionY,
3219 boolean parentPositionChanged, boolean parentScrolled) {
3220 // Either parentPositionChanged or parentScrolled is true, check if still visible
3221 if (isShowing() && isOffsetVisible(getTextOffset())) {
3222 if (parentScrolled) computeLocalPosition();
3223 updatePosition(parentPositionX, parentPositionY);
3224 } else {
3225 hide();
3226 }
3227 }
3228
3229 public boolean isShowing() {
3230 return mPopupWindow.isShowing();
3231 }
3232 }
3233
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003234 private static final class SuggestionInfo {
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003235 // Range of actual suggestion within mText
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003236 int mSuggestionStart, mSuggestionEnd;
3237
3238 // The SuggestionSpan that this TextView represents
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003239 final SuggestionSpanInfo mSuggestionSpanInfo = new SuggestionSpanInfo();
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003240
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003241 // The index of this suggestion inside suggestionSpan
3242 int mSuggestionIndex;
3243
3244 final SpannableStringBuilder mText = new SpannableStringBuilder();
3245
3246 void clear() {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003247 mSuggestionSpanInfo.clear();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003248 mText.clear();
3249 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003250
3251 // Utility method to set attributes about a SuggestionSpan.
3252 void setSpanInfo(SuggestionSpan span, int spanStart, int spanEnd) {
3253 mSuggestionSpanInfo.mSuggestionSpan = span;
3254 mSuggestionSpanInfo.mSpanStart = spanStart;
3255 mSuggestionSpanInfo.mSpanEnd = spanEnd;
3256 }
3257 }
3258
3259 private static final class SuggestionSpanInfo {
3260 // The SuggestionSpan;
3261 @Nullable
3262 SuggestionSpan mSuggestionSpan;
3263
3264 // The SuggestionSpan start position
3265 int mSpanStart;
3266
3267 // The SuggestionSpan end position
3268 int mSpanEnd;
3269
3270 void clear() {
3271 mSuggestionSpan = null;
3272 }
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003273 }
3274
3275 private class SuggestionHelper {
3276 private final Comparator<SuggestionSpan> mSuggestionSpanComparator =
3277 new SuggestionSpanComparator();
3278 private final HashMap<SuggestionSpan, Integer> mSpansLengths =
3279 new HashMap<SuggestionSpan, Integer>();
3280
3281 private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
3282 public int compare(SuggestionSpan span1, SuggestionSpan span2) {
3283 final int flag1 = span1.getFlags();
3284 final int flag2 = span2.getFlags();
3285 if (flag1 != flag2) {
3286 // The order here should match what is used in updateDrawState
3287 final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
3288 final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
3289 final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
3290 final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
3291 if (easy1 && !misspelled1) return -1;
3292 if (easy2 && !misspelled2) return 1;
3293 if (misspelled1) return -1;
3294 if (misspelled2) return 1;
3295 }
3296
3297 return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
3298 }
3299 }
3300
3301 /**
3302 * Returns the suggestion spans that cover the current cursor position. The suggestion
3303 * spans are sorted according to the length of text that they are attached to.
3304 */
3305 private SuggestionSpan[] getSortedSuggestionSpans() {
3306 int pos = mTextView.getSelectionStart();
3307 Spannable spannable = (Spannable) mTextView.getText();
3308 SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
3309
3310 mSpansLengths.clear();
3311 for (SuggestionSpan suggestionSpan : suggestionSpans) {
3312 int start = spannable.getSpanStart(suggestionSpan);
3313 int end = spannable.getSpanEnd(suggestionSpan);
3314 mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
3315 }
3316
3317 // The suggestions are sorted according to their types (easy correction first, then
3318 // misspelled) and to the length of the text that they cover (shorter first).
3319 Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
3320 mSpansLengths.clear();
3321
3322 return suggestionSpans;
3323 }
3324
3325 /**
3326 * Gets the SuggestionInfo list that contains suggestion information at the current cursor
3327 * position.
3328 *
3329 * @param suggestionInfos SuggestionInfo array the results will be set.
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003330 * @param misspelledSpanInfo a struct the misspelled SuggestionSpan info will be set.
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003331 * @return the number of suggestions actually fetched.
3332 */
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003333 public int getSuggestionInfo(SuggestionInfo[] suggestionInfos,
3334 @Nullable SuggestionSpanInfo misspelledSpanInfo) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003335 final Spannable spannable = (Spannable) mTextView.getText();
3336 final SuggestionSpan[] suggestionSpans = getSortedSuggestionSpans();
3337 final int nbSpans = suggestionSpans.length;
3338 if (nbSpans == 0) return 0;
3339
3340 int numberOfSuggestions = 0;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003341 for (final SuggestionSpan suggestionSpan : suggestionSpans) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003342 final int spanStart = spannable.getSpanStart(suggestionSpan);
3343 final int spanEnd = spannable.getSpanEnd(suggestionSpan);
3344
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003345 if (misspelledSpanInfo != null
3346 && (suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
3347 misspelledSpanInfo.mSuggestionSpan = suggestionSpan;
3348 misspelledSpanInfo.mSpanStart = spanStart;
3349 misspelledSpanInfo.mSpanEnd = spanEnd;
3350 }
3351
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003352 final String[] suggestions = suggestionSpan.getSuggestions();
3353 final int nbSuggestions = suggestions.length;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003354 suggestionLoop:
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003355 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
3356 final String suggestion = suggestions[suggestionIndex];
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003357 for (int i = 0; i < numberOfSuggestions; i++) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003358 final SuggestionInfo otherSuggestionInfo = suggestionInfos[i];
3359 if (otherSuggestionInfo.mText.toString().equals(suggestion)) {
3360 final int otherSpanStart =
3361 otherSuggestionInfo.mSuggestionSpanInfo.mSpanStart;
3362 final int otherSpanEnd =
3363 otherSuggestionInfo.mSuggestionSpanInfo.mSpanEnd;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003364 if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003365 continue suggestionLoop;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003366 }
3367 }
3368 }
3369
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003370 SuggestionInfo suggestionInfo = suggestionInfos[numberOfSuggestions];
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003371 suggestionInfo.setSpanInfo(suggestionSpan, spanStart, spanEnd);
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003372 suggestionInfo.mSuggestionIndex = suggestionIndex;
3373 suggestionInfo.mSuggestionStart = 0;
3374 suggestionInfo.mSuggestionEnd = suggestion.length();
3375 suggestionInfo.mText.replace(0, suggestionInfo.mText.length(), suggestion);
3376 numberOfSuggestions++;
3377 if (numberOfSuggestions >= suggestionInfos.length) {
3378 return numberOfSuggestions;
3379 }
3380 }
3381 }
3382 return numberOfSuggestions;
3383 }
3384 }
3385
Seigo Nonakaa60160b2015-08-19 12:38:35 -07003386 @VisibleForTesting
3387 public class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
Gilles Debunned88876a2012-03-16 17:34:04 -07003388 private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003389
3390 // Key of intent extras for inserting new word into user dictionary.
3391 private static final String USER_DICTIONARY_EXTRA_WORD = "word";
3392 private static final String USER_DICTIONARY_EXTRA_LOCALE = "locale";
3393
Gilles Debunned88876a2012-03-16 17:34:04 -07003394 private SuggestionInfo[] mSuggestionInfos;
3395 private int mNumberOfSuggestions;
3396 private boolean mCursorWasVisibleBeforeSuggestions;
3397 private boolean mIsShowingUp = false;
3398 private SuggestionAdapter mSuggestionsAdapter;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003399 private TextAppearanceSpan mHighlightSpan; // TODO: Make mHighlightSpan final.
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003400 private TextView mAddToDictionaryButton;
3401 private TextView mDeleteButton;
Seigo Nonakaf47976e2016-03-01 09:17:37 -08003402 private ListView mSuggestionListView;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003403 private final SuggestionSpanInfo mMisspelledSpanInfo = new SuggestionSpanInfo();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003404 private int mContainerMarginWidth;
3405 private int mContainerMarginTop;
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003406 private LinearLayout mContainerView;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003407 private Context mContext; // TODO: Make mContext final.
Gilles Debunned88876a2012-03-16 17:34:04 -07003408
3409 private class CustomPopupWindow extends PopupWindow {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003410
Gilles Debunned88876a2012-03-16 17:34:04 -07003411 @Override
3412 public void dismiss() {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09003413 if (!isShowing()) {
3414 return;
3415 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003416 super.dismiss();
Gilles Debunned88876a2012-03-16 17:34:04 -07003417 getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
3418
3419 // Safe cast since show() checks that mTextView.getText() is an Editable
3420 ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan);
3421
3422 mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions);
Keisuke Kuroyanagi4a696ac2016-02-23 11:02:07 -08003423 if (hasInsertionController() && !extractedTextModeWillBeStarted()) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003424 getInsertionController().show();
3425 }
3426 }
3427 }
3428
3429 public SuggestionsPopupWindow() {
3430 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
Gilles Debunned88876a2012-03-16 17:34:04 -07003431 }
3432
3433 @Override
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003434 protected void setUp() {
3435 mContext = applyDefaultTheme(mTextView.getContext());
3436 mHighlightSpan = new TextAppearanceSpan(mContext,
3437 mTextView.mTextEditSuggestionHighlightStyle);
3438 }
3439
3440 private Context applyDefaultTheme(Context originalContext) {
3441 TypedArray a = originalContext.obtainStyledAttributes(
3442 new int[]{com.android.internal.R.attr.isLightTheme});
3443 boolean isLightTheme = a.getBoolean(0, true);
3444 int themeId = isLightTheme ? R.style.ThemeOverlay_Material_Light
3445 : R.style.ThemeOverlay_Material_Dark;
3446 a.recycle();
3447 return new ContextThemeWrapper(originalContext, themeId);
3448 }
3449
3450 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07003451 protected void createPopupWindow() {
Seigo Nonaka3ed1b392016-01-19 13:54:59 +09003452 mPopupWindow = new CustomPopupWindow();
Gilles Debunned88876a2012-03-16 17:34:04 -07003453 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
Seigo Nonaka3ed1b392016-01-19 13:54:59 +09003454 mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
Gilles Debunned88876a2012-03-16 17:34:04 -07003455 mPopupWindow.setFocusable(true);
3456 mPopupWindow.setClippingEnabled(false);
3457 }
3458
3459 @Override
3460 protected void initContentView() {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003461 final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
3462 Context.LAYOUT_INFLATER_SERVICE);
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003463 mContentView = (ViewGroup) inflater.inflate(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003464 mTextView.mTextEditSuggestionContainerLayout, null);
Gilles Debunned88876a2012-03-16 17:34:04 -07003465
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003466 mContainerView = (LinearLayout) mContentView.findViewById(
3467 com.android.internal.R.id.suggestionWindowContainer);
Seigo Nonaka60490d12016-01-28 17:25:18 +09003468 ViewGroup.MarginLayoutParams lp =
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003469 (ViewGroup.MarginLayoutParams) mContainerView.getLayoutParams();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003470 mContainerMarginWidth = lp.leftMargin + lp.rightMargin;
3471 mContainerMarginTop = lp.topMargin;
3472 mClippingLimitLeft = lp.leftMargin;
3473 mClippingLimitRight = lp.rightMargin;
3474
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003475 mSuggestionListView = (ListView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003476 com.android.internal.R.id.suggestionContainer);
3477
3478 mSuggestionsAdapter = new SuggestionAdapter();
Seigo Nonakaf47976e2016-03-01 09:17:37 -08003479 mSuggestionListView.setAdapter(mSuggestionsAdapter);
3480 mSuggestionListView.setOnItemClickListener(this);
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003481
3482 // Inflate the suggestion items once and for all.
3483 mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS];
Gilles Debunned88876a2012-03-16 17:34:04 -07003484 for (int i = 0; i < mSuggestionInfos.length; i++) {
3485 mSuggestionInfos[i] = new SuggestionInfo();
3486 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003487
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003488 mAddToDictionaryButton = (TextView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003489 com.android.internal.R.id.addToDictionaryButton);
3490 mAddToDictionaryButton.setOnClickListener(new View.OnClickListener() {
3491 public void onClick(View v) {
Keisuke Kuroyanagi6e0860d2016-03-15 15:40:43 +09003492 final SuggestionSpan misspelledSpan =
3493 findEquivalentSuggestionSpan(mMisspelledSpanInfo);
3494 if (misspelledSpan == null) {
3495 // Span has been removed.
3496 return;
3497 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003498 final Editable editable = (Editable) mTextView.getText();
Keisuke Kuroyanagi6e0860d2016-03-15 15:40:43 +09003499 final int spanStart = editable.getSpanStart(misspelledSpan);
3500 final int spanEnd = editable.getSpanEnd(misspelledSpan);
3501 if (spanStart < 0 || spanEnd <= spanStart) {
3502 return;
3503 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003504 final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
3505
3506 final Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
3507 intent.putExtra(USER_DICTIONARY_EXTRA_WORD, originalText);
3508 intent.putExtra(USER_DICTIONARY_EXTRA_LOCALE,
3509 mTextView.getTextServicesLocale().toString());
3510 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
3511 mTextView.getContext().startActivity(intent);
3512 // There is no way to know if the word was indeed added. Re-check.
3513 // TODO The ExtractEditText should remove the span in the original text instead
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003514 editable.removeSpan(mMisspelledSpanInfo.mSuggestionSpan);
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003515 Selection.setSelection(editable, spanEnd);
3516 updateSpellCheckSpans(spanStart, spanEnd, false);
3517 hideWithCleanUp();
3518 }
3519 });
3520
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003521 mDeleteButton = (TextView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003522 com.android.internal.R.id.deleteButton);
3523 mDeleteButton.setOnClickListener(new View.OnClickListener() {
3524 public void onClick(View v) {
3525 final Editable editable = (Editable) mTextView.getText();
3526
3527 final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
3528 int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
3529 if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
3530 // Do not leave two adjacent spaces after deletion, or one at beginning of
3531 // text
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003532 if (spanUnionEnd < editable.length()
3533 && Character.isSpaceChar(editable.charAt(spanUnionEnd))
3534 && (spanUnionStart == 0
3535 || Character.isSpaceChar(
3536 editable.charAt(spanUnionStart - 1)))) {
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003537 spanUnionEnd = spanUnionEnd + 1;
3538 }
3539 mTextView.deleteText_internal(spanUnionStart, spanUnionEnd);
3540 }
3541 hideWithCleanUp();
3542 }
3543 });
3544
Gilles Debunned88876a2012-03-16 17:34:04 -07003545 }
3546
3547 public boolean isShowingUp() {
3548 return mIsShowingUp;
3549 }
3550
3551 public void onParentLostFocus() {
3552 mIsShowingUp = false;
3553 }
3554
Gilles Debunned88876a2012-03-16 17:34:04 -07003555 private class SuggestionAdapter extends BaseAdapter {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003556 private LayoutInflater mInflater = (LayoutInflater) mContext.getSystemService(
3557 Context.LAYOUT_INFLATER_SERVICE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003558
3559 @Override
3560 public int getCount() {
3561 return mNumberOfSuggestions;
3562 }
3563
3564 @Override
3565 public Object getItem(int position) {
3566 return mSuggestionInfos[position];
3567 }
3568
3569 @Override
3570 public long getItemId(int position) {
3571 return position;
3572 }
3573
3574 @Override
3575 public View getView(int position, View convertView, ViewGroup parent) {
3576 TextView textView = (TextView) convertView;
3577
3578 if (textView == null) {
3579 textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout,
3580 parent, false);
3581 }
3582
3583 final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003584 textView.setText(suggestionInfo.mText);
Gilles Debunned88876a2012-03-16 17:34:04 -07003585 return textView;
3586 }
3587 }
3588
Seigo Nonakaa60160b2015-08-19 12:38:35 -07003589 @VisibleForTesting
3590 public ViewGroup getContentViewForTesting() {
3591 return mContentView;
3592 }
3593
Gilles Debunned88876a2012-03-16 17:34:04 -07003594 @Override
3595 public void show() {
3596 if (!(mTextView.getText() instanceof Editable)) return;
Keisuke Kuroyanagi4a696ac2016-02-23 11:02:07 -08003597 if (extractedTextModeWillBeStarted()) {
3598 return;
3599 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003600
3601 if (updateSuggestions()) {
3602 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
3603 mTextView.setCursorVisible(false);
3604 mIsShowingUp = true;
3605 super.show();
3606 }
3607 }
3608
3609 @Override
3610 protected void measureContent() {
3611 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3612 final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
3613 displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
3614 final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
3615 displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
3616
3617 int width = 0;
3618 View view = null;
3619 for (int i = 0; i < mNumberOfSuggestions; i++) {
3620 view = mSuggestionsAdapter.getView(i, view, mContentView);
3621 view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
3622 view.measure(horizontalMeasure, verticalMeasure);
3623 width = Math.max(width, view.getMeasuredWidth());
3624 }
3625
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003626 if (mAddToDictionaryButton.getVisibility() != View.GONE) {
3627 mAddToDictionaryButton.measure(horizontalMeasure, verticalMeasure);
3628 width = Math.max(width, mAddToDictionaryButton.getMeasuredWidth());
3629 }
3630
3631 mDeleteButton.measure(horizontalMeasure, verticalMeasure);
3632 width = Math.max(width, mDeleteButton.getMeasuredWidth());
3633
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003634 width += mContainerView.getPaddingLeft() + mContainerView.getPaddingRight()
3635 + mContainerMarginWidth;
Seigo Nonaka60490d12016-01-28 17:25:18 +09003636
Gilles Debunned88876a2012-03-16 17:34:04 -07003637 // Enforce the width based on actual text widths
3638 mContentView.measure(
3639 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
3640 verticalMeasure);
3641
3642 Drawable popupBackground = mPopupWindow.getBackground();
3643 if (popupBackground != null) {
3644 if (mTempRect == null) mTempRect = new Rect();
3645 popupBackground.getPadding(mTempRect);
3646 width += mTempRect.left + mTempRect.right;
3647 }
3648 mPopupWindow.setWidth(width);
3649 }
3650
3651 @Override
3652 protected int getTextOffset() {
Keisuke Kuroyanagi713be062016-02-29 16:07:54 -08003653 return (mTextView.getSelectionStart() + mTextView.getSelectionStart()) / 2;
Gilles Debunned88876a2012-03-16 17:34:04 -07003654 }
3655
3656 @Override
3657 protected int getVerticalLocalPosition(int line) {
Siyamed Sinira60b59d2017-07-26 09:26:41 -07003658 final Layout layout = mTextView.getLayout();
3659 return layout.getLineBottomWithoutSpacing(line) - mContainerMarginTop;
Gilles Debunned88876a2012-03-16 17:34:04 -07003660 }
3661
3662 @Override
3663 protected int clipVertically(int positionY) {
3664 final int height = mContentView.getMeasuredHeight();
3665 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3666 return Math.min(positionY, displayMetrics.heightPixels - height);
3667 }
3668
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003669 private void hideWithCleanUp() {
3670 for (final SuggestionInfo info : mSuggestionInfos) {
3671 info.clear();
3672 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003673 mMisspelledSpanInfo.clear();
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003674 hide();
Gilles Debunned88876a2012-03-16 17:34:04 -07003675 }
3676
3677 private boolean updateSuggestions() {
3678 Spannable spannable = (Spannable) mTextView.getText();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003679 mNumberOfSuggestions =
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003680 mSuggestionHelper.getSuggestionInfo(mSuggestionInfos, mMisspelledSpanInfo);
3681 if (mNumberOfSuggestions == 0 && mMisspelledSpanInfo.mSuggestionSpan == null) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003682 return false;
3683 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003684
Gilles Debunned88876a2012-03-16 17:34:04 -07003685 int spanUnionStart = mTextView.getText().length();
3686 int spanUnionEnd = 0;
3687
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003688 for (int i = 0; i < mNumberOfSuggestions; i++) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003689 final SuggestionSpanInfo spanInfo = mSuggestionInfos[i].mSuggestionSpanInfo;
3690 spanUnionStart = Math.min(spanUnionStart, spanInfo.mSpanStart);
3691 spanUnionEnd = Math.max(spanUnionEnd, spanInfo.mSpanEnd);
3692 }
3693 if (mMisspelledSpanInfo.mSuggestionSpan != null) {
3694 spanUnionStart = Math.min(spanUnionStart, mMisspelledSpanInfo.mSpanStart);
3695 spanUnionEnd = Math.max(spanUnionEnd, mMisspelledSpanInfo.mSpanEnd);
Gilles Debunned88876a2012-03-16 17:34:04 -07003696 }
3697
3698 for (int i = 0; i < mNumberOfSuggestions; i++) {
3699 highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
3700 }
3701
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003702 // Make "Add to dictionary" item visible if there is a span with the misspelled flag
3703 int addToDictionaryButtonVisibility = View.GONE;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003704 if (mMisspelledSpanInfo.mSuggestionSpan != null) {
3705 if (mMisspelledSpanInfo.mSpanStart >= 0
3706 && mMisspelledSpanInfo.mSpanEnd > mMisspelledSpanInfo.mSpanStart) {
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003707 addToDictionaryButtonVisibility = View.VISIBLE;
Gilles Debunned88876a2012-03-16 17:34:04 -07003708 }
3709 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003710 mAddToDictionaryButton.setVisibility(addToDictionaryButtonVisibility);
Gilles Debunned88876a2012-03-16 17:34:04 -07003711
3712 if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003713 final int underlineColor;
3714 if (mNumberOfSuggestions != 0) {
3715 underlineColor =
3716 mSuggestionInfos[0].mSuggestionSpanInfo.mSuggestionSpan.getUnderlineColor();
3717 } else {
3718 underlineColor = mMisspelledSpanInfo.mSuggestionSpan.getUnderlineColor();
3719 }
3720
Gilles Debunned88876a2012-03-16 17:34:04 -07003721 if (underlineColor == 0) {
3722 // Fallback on the default highlight color when the first span does not provide one
3723 mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor);
3724 } else {
3725 final float BACKGROUND_TRANSPARENCY = 0.4f;
3726 final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
3727 mSuggestionRangeSpan.setBackgroundColor(
3728 (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
3729 }
3730 spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
3731 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
3732
3733 mSuggestionsAdapter.notifyDataSetChanged();
3734 return true;
3735 }
3736
3737 private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
3738 int unionEnd) {
3739 final Spannable text = (Spannable) mTextView.getText();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003740 final int spanStart = suggestionInfo.mSuggestionSpanInfo.mSpanStart;
3741 final int spanEnd = suggestionInfo.mSuggestionSpanInfo.mSpanEnd;
Gilles Debunned88876a2012-03-16 17:34:04 -07003742
3743 // Adjust the start/end of the suggestion span
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003744 suggestionInfo.mSuggestionStart = spanStart - unionStart;
3745 suggestionInfo.mSuggestionEnd = suggestionInfo.mSuggestionStart
3746 + suggestionInfo.mText.length();
Gilles Debunned88876a2012-03-16 17:34:04 -07003747
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003748 suggestionInfo.mText.setSpan(mHighlightSpan, 0, suggestionInfo.mText.length(),
Seigo Nonakabffbd302015-08-18 18:27:56 -07003749 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003750
3751 // Add the text before and after the span.
3752 final String textAsString = text.toString();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003753 suggestionInfo.mText.insert(0, textAsString.substring(unionStart, spanStart));
3754 suggestionInfo.mText.append(textAsString.substring(spanEnd, unionEnd));
Gilles Debunned88876a2012-03-16 17:34:04 -07003755 }
3756
3757 @Override
3758 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003759 SuggestionInfo suggestionInfo = mSuggestionInfos[position];
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003760 replaceWithSuggestion(suggestionInfo);
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003761 hideWithCleanUp();
Gilles Debunned88876a2012-03-16 17:34:04 -07003762 }
3763 }
3764
3765 /**
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003766 * An ActionMode Callback class that is used to provide actions while in text insertion or
3767 * selection mode.
Gilles Debunned88876a2012-03-16 17:34:04 -07003768 *
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003769 * The default callback provides a subset of Select All, Cut, Copy, Paste, Share and Replace
3770 * actions, depending on which of these this TextView supports and the current selection.
Gilles Debunned88876a2012-03-16 17:34:04 -07003771 */
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003772 private class TextActionModeCallback extends ActionMode.Callback2 {
Clara Bayarriea4f1502015-03-18 00:25:01 +00003773 private final Path mSelectionPath = new Path();
3774 private final RectF mSelectionBounds = new RectF();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003775 private final boolean mHasSelection;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003776 private final int mHandleHeight;
Clara Bayarriea4f1502015-03-18 00:25:01 +00003777
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003778 public TextActionModeCallback(boolean hasSelection) {
3779 mHasSelection = hasSelection;
3780 if (mHasSelection) {
3781 SelectionModifierCursorController selectionController = getSelectionController();
3782 if (selectionController.mStartHandle == null) {
3783 // As these are for initializing selectionController, hide() must be called.
3784 selectionController.initDrawables();
3785 selectionController.initHandles();
3786 selectionController.hide();
3787 }
3788 mHandleHeight = Math.max(
3789 mSelectHandleLeft.getMinimumHeight(),
3790 mSelectHandleRight.getMinimumHeight());
3791 } else {
3792 InsertionPointCursorController insertionController = getInsertionController();
3793 if (insertionController != null) {
3794 insertionController.getHandle();
3795 mHandleHeight = mSelectHandleCenter.getMinimumHeight();
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003796 } else {
3797 mHandleHeight = 0;
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003798 }
Clara Bayarri7fc946e2015-03-31 14:48:33 +01003799 }
Clara Bayarriea4f1502015-03-18 00:25:01 +00003800 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003801
3802 @Override
3803 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003804 mode.setTitle(null);
Clara Bayarri13152d12015-04-09 12:02:04 +01003805 mode.setSubtitle(null);
3806 mode.setTitleOptionalHint(true);
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003807 populateMenuWithItems(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01003808
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003809 Callback customCallback = getCustomCallback();
3810 if (customCallback != null) {
3811 if (!customCallback.onCreateActionMode(mode, menu)) {
Clara Bayarri01243ac2015-06-03 00:46:29 +01003812 // The custom mode can choose to cancel the action mode, dismiss selection.
3813 Selection.setSelection((Spannable) mTextView.getText(),
3814 mTextView.getSelectionEnd());
Clara Bayarri13152d12015-04-09 12:02:04 +01003815 return false;
3816 }
3817 }
3818
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07003819 if (mTextView.canProcessText()) {
3820 mProcessTextIntentActionsHandler.onInitializeMenu(menu);
3821 }
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +00003822
Clara Bayarri13152d12015-04-09 12:02:04 +01003823 if (menu.hasVisibleItems() || mode.getCustomView() != null) {
Keisuke Kuroyanagi183fd502016-04-01 15:00:53 +09003824 if (mHasSelection && !mTextView.hasTransientState()) {
3825 mTextView.setHasTransientState(true);
3826 }
Clara Bayarri13152d12015-04-09 12:02:04 +01003827 return true;
3828 } else {
3829 return false;
3830 }
3831 }
3832
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003833 private Callback getCustomCallback() {
3834 return mHasSelection
3835 ? mCustomSelectionActionModeCallback
3836 : mCustomInsertionActionModeCallback;
3837 }
3838
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003839 private void populateMenuWithItems(Menu menu) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003840 if (mTextView.canCut()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003841 menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003842 com.android.internal.R.string.cut)
3843 .setAlphabeticShortcut('x')
3844 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07003845 }
3846
3847 if (mTextView.canCopy()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003848 menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003849 com.android.internal.R.string.copy)
3850 .setAlphabeticShortcut('c')
3851 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07003852 }
3853
3854 if (mTextView.canPaste()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003855 menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003856 com.android.internal.R.string.paste)
3857 .setAlphabeticShortcut('v')
3858 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07003859 }
3860
Andrei Stingaceanu7f0c5bd2015-04-14 17:12:08 +01003861 if (mTextView.canShare()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003862 menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003863 com.android.internal.R.string.share)
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +00003864 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
Andrei Stingaceanu7f0c5bd2015-04-14 17:12:08 +01003865 }
3866
Felipe Leme2ac463e2017-03-13 14:06:25 -07003867 if (mTextView.canRequestAutofill()) {
Felipe Leme1c1626e2017-06-02 10:53:13 -07003868 final String selected = mTextView.getSelectedText();
3869 if (selected == null || selected.isEmpty()) {
3870 menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
3871 com.android.internal.R.string.autofill)
Siyamed Sinir484c2e22017-06-07 16:26:19 -07003872 .setShowAsAction(MenuItem.SHOW_AS_OVERFLOW_ALWAYS);
Felipe Leme1c1626e2017-06-02 10:53:13 -07003873 }
Felipe Leme2ac463e2017-03-13 14:06:25 -07003874 }
3875
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01003876 if (mTextView.canPasteAsPlainText()) {
3877 menu.add(
3878 Menu.NONE,
3879 TextView.ID_PASTE_AS_PLAIN_TEXT,
3880 MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
3881 com.android.internal.R.string.paste_as_plain_text)
3882 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
3883 }
3884
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003885 updateSelectAllItem(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01003886 updateReplaceItem(menu);
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01003887 updateAssistMenuItem(menu);
Gilles Debunned88876a2012-03-16 17:34:04 -07003888 }
3889
3890 @Override
3891 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003892 updateSelectAllItem(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01003893 updateReplaceItem(menu);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08003894 updateAssistMenuItem(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01003895
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003896 Callback customCallback = getCustomCallback();
3897 if (customCallback != null) {
3898 return customCallback.onPrepareActionMode(mode, menu);
Gilles Debunned88876a2012-03-16 17:34:04 -07003899 }
3900 return true;
3901 }
3902
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003903 private void updateSelectAllItem(Menu menu) {
3904 boolean canSelectAll = mTextView.canSelectAllText();
3905 boolean selectAllItemExists = menu.findItem(TextView.ID_SELECT_ALL) != null;
3906 if (canSelectAll && !selectAllItemExists) {
3907 menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
3908 com.android.internal.R.string.selectAll)
3909 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
3910 } else if (!canSelectAll && selectAllItemExists) {
3911 menu.removeItem(TextView.ID_SELECT_ALL);
3912 }
3913 }
3914
Clara Bayarri13152d12015-04-09 12:02:04 +01003915 private void updateReplaceItem(Menu menu) {
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003916 boolean canReplace = mTextView.isSuggestionsEnabled() && shouldOfferToShowSuggestions();
Clara Bayarri13152d12015-04-09 12:02:04 +01003917 boolean replaceItemExists = menu.findItem(TextView.ID_REPLACE) != null;
3918 if (canReplace && !replaceItemExists) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003919 menu.add(Menu.NONE, TextView.ID_REPLACE, MENU_ITEM_ORDER_REPLACE,
3920 com.android.internal.R.string.replace)
3921 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
Clara Bayarri13152d12015-04-09 12:02:04 +01003922 } else if (!canReplace && replaceItemExists) {
3923 menu.removeItem(TextView.ID_REPLACE);
3924 }
3925 }
3926
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08003927 private void updateAssistMenuItem(Menu menu) {
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003928 menu.removeItem(TextView.ID_ASSIST);
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +01003929 final TextClassification textClassification =
3930 getSelectionActionModeHelper().getTextClassification();
Abodunrinwa Toki9796a1b2017-06-28 02:49:07 +01003931 if (canAssist()) {
3932 menu.add(TextView.ID_ASSIST, TextView.ID_ASSIST, MENU_ITEM_ORDER_ASSIST,
3933 textClassification.getLabel())
3934 .setIcon(textClassification.getIcon())
3935 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Jan Althaus786a39d2017-09-15 10:41:16 +02003936 mMetricsLogger.write(
3937 new LogMaker(MetricsEvent.TEXT_SELECTION_MENU_ITEM_ASSIST)
3938 .setType(MetricsEvent.TYPE_OPEN)
3939 .setSubtype(textClassification.getLogType()));
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003940 }
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +00003941 }
3942
Abodunrinwa Toki9796a1b2017-06-28 02:49:07 +01003943 private boolean canAssist() {
3944 final TextClassification textClassification =
3945 getSelectionActionModeHelper().getTextClassification();
3946 return mTextView.isDeviceProvisioned()
3947 && textClassification != null
3948 && (textClassification.getIcon() != null
3949 || !TextUtils.isEmpty(textClassification.getLabel()))
3950 && (textClassification.getOnClickListener() != null
3951 || (textClassification.getIntent() != null
3952 && mTextView.getContext().canStartActivityForResult()));
3953 }
3954
Gilles Debunned88876a2012-03-16 17:34:04 -07003955 @Override
3956 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01003957 getSelectionActionModeHelper().onSelectionAction(item.getItemId());
Abodunrinwa Toki1d775572017-05-08 16:03:01 +01003958
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07003959 if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +00003960 return true;
3961 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003962 Callback customCallback = getCustomCallback();
3963 if (customCallback != null && customCallback.onActionItemClicked(mode, item)) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003964 return true;
3965 }
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +01003966 final TextClassification textClassification =
3967 getSelectionActionModeHelper().getTextClassification();
3968 if (TextView.ID_ASSIST == item.getItemId() && textClassification != null) {
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +00003969 final OnClickListener onClickListener =
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +01003970 textClassification.getOnClickListener();
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +00003971 if (onClickListener != null) {
3972 onClickListener.onClick(mTextView);
3973 } else {
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +01003974 final Intent intent = textClassification.getIntent();
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +00003975 if (intent != null) {
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +01003976 TextClassification.createStartActivityOnClickListener(
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +00003977 mTextView.getContext(), intent)
3978 .onClick(mTextView);
3979 }
3980 }
Jan Althaus786a39d2017-09-15 10:41:16 +02003981 mMetricsLogger.action(
3982 MetricsEvent.ACTION_TEXT_SELECTION_MENU_ITEM_ASSIST,
3983 textClassification.getLogType());
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003984 stopTextActionMode();
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +00003985 return true;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003986 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003987 return mTextView.onTextContextMenuItem(item.getItemId());
3988 }
3989
3990 @Override
3991 public void onDestroyActionMode(ActionMode mode) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09003992 // Clear mTextActionMode not to recursively destroy action mode by clearing selection.
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +00003993 getSelectionActionModeHelper().onDestroyActionMode();
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09003994 mTextActionMode = null;
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003995 Callback customCallback = getCustomCallback();
3996 if (customCallback != null) {
3997 customCallback.onDestroyActionMode(mode);
Gilles Debunned88876a2012-03-16 17:34:04 -07003998 }
Adam Powell057a5852012-05-11 10:28:38 -07003999
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08004000 if (!mPreserveSelection) {
4001 /*
4002 * Leave current selection when we tentatively destroy action mode for the
4003 * selection. If we're detaching from a window, we'll bring back the selection
4004 * mode when (if) we get reattached.
4005 */
Adam Powell057a5852012-05-11 10:28:38 -07004006 Selection.setSelection((Spannable) mTextView.getText(),
4007 mTextView.getSelectionEnd());
Adam Powell057a5852012-05-11 10:28:38 -07004008 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004009
4010 if (mSelectionModifierCursorController != null) {
4011 mSelectionModifierCursorController.hide();
4012 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004013 }
Clara Bayarriea4f1502015-03-18 00:25:01 +00004014
4015 @Override
4016 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
4017 if (!view.equals(mTextView) || mTextView.getLayout() == null) {
4018 super.onGetContentRect(mode, view, outRect);
4019 return;
4020 }
4021 if (mTextView.getSelectionStart() != mTextView.getSelectionEnd()) {
4022 // We have a selection.
4023 mSelectionPath.reset();
4024 mTextView.getLayout().getSelectionPath(
4025 mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mSelectionPath);
4026 mSelectionPath.computeBounds(mSelectionBounds, true);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004027 mSelectionBounds.bottom += mHandleHeight;
Clara Bayarriea4f1502015-03-18 00:25:01 +00004028 } else {
Roozbeh Pournader9c133072017-07-26 22:36:27 -07004029 // We have a cursor.
Siyamed Sinir987ec652016-02-17 19:44:41 -08004030 Layout layout = mTextView.getLayout();
Mady Mellorff66ca52015-07-08 12:31:45 -07004031 int line = layout.getLineForOffset(mTextView.getSelectionStart());
Siyamed Sinir987ec652016-02-17 19:44:41 -08004032 float primaryHorizontal = clampHorizontalPosition(null,
4033 layout.getPrimaryHorizontal(mTextView.getSelectionStart()));
Clara Bayarriea4f1502015-03-18 00:25:01 +00004034 mSelectionBounds.set(
4035 primaryHorizontal,
Mady Mellorff66ca52015-07-08 12:31:45 -07004036 layout.getLineTop(line),
Clara Bayarrif95ed102015-08-12 19:46:47 +01004037 primaryHorizontal,
Siyamed Sinira60b59d2017-07-26 09:26:41 -07004038 layout.getLineBottom(line) - layout.getLineBottom(line) + mHandleHeight);
Clara Bayarriea4f1502015-03-18 00:25:01 +00004039 }
4040 // Take TextView's padding and scroll into account.
4041 int textHorizontalOffset = mTextView.viewportToContentHorizontalOffset();
4042 int textVerticalOffset = mTextView.viewportToContentVerticalOffset();
4043 outRect.set(
4044 (int) Math.floor(mSelectionBounds.left + textHorizontalOffset),
4045 (int) Math.floor(mSelectionBounds.top + textVerticalOffset),
4046 (int) Math.ceil(mSelectionBounds.right + textHorizontalOffset),
4047 (int) Math.ceil(mSelectionBounds.bottom + textVerticalOffset));
4048 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004049 }
4050
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004051 /**
4052 * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}
4053 * while the input method is requesting the cursor/anchor position. Does nothing as long as
4054 * {@link InputMethodManager#isWatchingCursor(View)} returns false.
4055 */
4056 private final class CursorAnchorInfoNotifier implements TextViewPositionListener {
Yohei Yukawac46b5f02014-06-10 12:26:34 +09004057 final CursorAnchorInfo.Builder mSelectionInfoBuilder = new CursorAnchorInfo.Builder();
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004058 final int[] mTmpIntOffset = new int[2];
4059 final Matrix mViewToScreenMatrix = new Matrix();
4060
4061 @Override
4062 public void updatePosition(int parentPositionX, int parentPositionY,
4063 boolean parentPositionChanged, boolean parentScrolled) {
4064 final InputMethodState ims = mInputMethodState;
4065 if (ims == null || ims.mBatchEditNesting > 0) {
4066 return;
4067 }
4068 final InputMethodManager imm = InputMethodManager.peekInstance();
4069 if (null == imm) {
4070 return;
4071 }
Yohei Yukawa0023d0e2014-07-11 04:13:03 +09004072 if (!imm.isActive(mTextView)) {
4073 return;
4074 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004075 // Skip if the IME has not requested the cursor/anchor position.
Yohei Yukawa0023d0e2014-07-11 04:13:03 +09004076 if (!imm.isCursorAnchorInfoEnabled()) {
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004077 return;
4078 }
4079 Layout layout = mTextView.getLayout();
4080 if (layout == null) {
4081 return;
4082 }
4083
Yohei Yukawac46b5f02014-06-10 12:26:34 +09004084 final CursorAnchorInfo.Builder builder = mSelectionInfoBuilder;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004085 builder.reset();
4086
4087 final int selectionStart = mTextView.getSelectionStart();
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004088 builder.setSelectionRange(selectionStart, mTextView.getSelectionEnd());
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004089
4090 // Construct transformation matrix from view local coordinates to screen coordinates.
4091 mViewToScreenMatrix.set(mTextView.getMatrix());
4092 mTextView.getLocationOnScreen(mTmpIntOffset);
4093 mViewToScreenMatrix.postTranslate(mTmpIntOffset[0], mTmpIntOffset[1]);
4094 builder.setMatrix(mViewToScreenMatrix);
4095
4096 final float viewportToContentHorizontalOffset =
4097 mTextView.viewportToContentHorizontalOffset();
4098 final float viewportToContentVerticalOffset =
4099 mTextView.viewportToContentVerticalOffset();
4100
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004101 final CharSequence text = mTextView.getText();
4102 if (text instanceof Spannable) {
4103 final Spannable sp = (Spannable) text;
4104 int composingTextStart = EditableInputConnection.getComposingSpanStart(sp);
4105 int composingTextEnd = EditableInputConnection.getComposingSpanEnd(sp);
4106 if (composingTextEnd < composingTextStart) {
4107 final int temp = composingTextEnd;
4108 composingTextEnd = composingTextStart;
4109 composingTextStart = temp;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004110 }
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004111 final boolean hasComposingText =
4112 (0 <= composingTextStart) && (composingTextStart < composingTextEnd);
4113 if (hasComposingText) {
4114 final CharSequence composingText = text.subSequence(composingTextStart,
4115 composingTextEnd);
4116 builder.setComposingText(composingTextStart, composingText);
Phil Weaverc2e28932016-12-08 12:29:25 -08004117 mTextView.populateCharacterBounds(builder, composingTextStart,
4118 composingTextEnd, viewportToContentHorizontalOffset,
4119 viewportToContentVerticalOffset);
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004120 }
4121 }
4122
4123 // Treat selectionStart as the insertion point.
4124 if (0 <= selectionStart) {
4125 final int offset = selectionStart;
4126 final int line = layout.getLineForOffset(offset);
4127 final float insertionMarkerX = layout.getPrimaryHorizontal(offset)
4128 + viewportToContentHorizontalOffset;
4129 final float insertionMarkerTop = layout.getLineTop(line)
4130 + viewportToContentVerticalOffset;
4131 final float insertionMarkerBaseline = layout.getLineBaseline(line)
4132 + viewportToContentVerticalOffset;
Siyamed Sinira60b59d2017-07-26 09:26:41 -07004133 final float insertionMarkerBottom = layout.getLineBottomWithoutSpacing(line)
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004134 + viewportToContentVerticalOffset;
Phil Weaverc2e28932016-12-08 12:29:25 -08004135 final boolean isTopVisible = mTextView
4136 .isPositionVisible(insertionMarkerX, insertionMarkerTop);
4137 final boolean isBottomVisible = mTextView
4138 .isPositionVisible(insertionMarkerX, insertionMarkerBottom);
Yohei Yukawacc24e2b2014-08-29 20:21:10 -07004139 int insertionMarkerFlags = 0;
4140 if (isTopVisible || isBottomVisible) {
4141 insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
4142 }
4143 if (!isTopVisible || !isBottomVisible) {
4144 insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
4145 }
Yohei Yukawa5f183f02014-09-02 14:18:40 -07004146 if (layout.isRtlCharAt(offset)) {
4147 insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL;
4148 }
Yohei Yukawa0b01e7f2014-07-08 15:29:51 +09004149 builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop,
Yohei Yukawacc24e2b2014-08-29 20:21:10 -07004150 insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags);
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004151 }
4152
4153 imm.updateCursorAnchorInfo(mTextView, builder.build());
4154 }
4155 }
4156
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004157 @VisibleForTesting
4158 public abstract class HandleView extends View implements TextViewPositionListener {
Gilles Debunned88876a2012-03-16 17:34:04 -07004159 protected Drawable mDrawable;
4160 protected Drawable mDrawableLtr;
4161 protected Drawable mDrawableRtl;
4162 private final PopupWindow mContainer;
4163 // Position with respect to the parent TextView
4164 private int mPositionX, mPositionY;
4165 private boolean mIsDragging;
4166 // Offset from touch position to mPosition
4167 private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
4168 protected int mHotspotX;
Adam Powell3fceabd2014-08-19 18:28:04 -07004169 protected int mHorizontalGravity;
Gilles Debunned88876a2012-03-16 17:34:04 -07004170 // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
4171 private float mTouchOffsetY;
4172 // Where the touch position should be on the handle to ensure a maximum cursor visibility
4173 private float mIdealVerticalOffset;
4174 // Parent's (TextView) previous position in window
4175 private int mLastParentX, mLastParentY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004176 // Parent's (TextView) previous position on screen
4177 private int mLastParentXOnScreen, mLastParentYOnScreen;
Gilles Debunned88876a2012-03-16 17:34:04 -07004178 // Previous text character offset
Mady Mellorc2225b92015-04-01 15:59:20 -07004179 protected int mPreviousOffset = -1;
Gilles Debunned88876a2012-03-16 17:34:04 -07004180 // Previous text character offset
4181 private boolean mPositionHasChanged = true;
Adam Powell3fceabd2014-08-19 18:28:04 -07004182 // Minimum touch target size for handles
4183 private int mMinSize;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004184 // Indicates the line of text that the handle is on.
Mady Mellora6a0f782015-07-10 16:43:32 -07004185 protected int mPrevLine = UNSET_LINE;
4186 // Indicates the line of text that the user was touching. This can differ from mPrevLine
4187 // when selecting text when the handles jump to the end / start of words which may be on
4188 // a different line.
4189 protected int mPreviousLineTouched = UNSET_LINE;
Gilles Debunned88876a2012-03-16 17:34:04 -07004190
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004191 private HandleView(Drawable drawableLtr, Drawable drawableRtl, final int id) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004192 super(mTextView.getContext());
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004193 setId(id);
Gilles Debunned88876a2012-03-16 17:34:04 -07004194 mContainer = new PopupWindow(mTextView.getContext(), null,
4195 com.android.internal.R.attr.textSelectHandleWindowStyle);
4196 mContainer.setSplitTouchEnabled(true);
4197 mContainer.setClippingEnabled(false);
4198 mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
Keisuke Kuroyanagi7340be72015-02-27 17:57:49 +09004199 mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
4200 mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
Gilles Debunned88876a2012-03-16 17:34:04 -07004201 mContainer.setContentView(this);
4202
4203 mDrawableLtr = drawableLtr;
4204 mDrawableRtl = drawableRtl;
Adam Powell3fceabd2014-08-19 18:28:04 -07004205 mMinSize = mTextView.getContext().getResources().getDimensionPixelSize(
4206 com.android.internal.R.dimen.text_handle_min_size);
Gilles Debunned88876a2012-03-16 17:34:04 -07004207
4208 updateDrawable();
4209
Adam Powell3fceabd2014-08-19 18:28:04 -07004210 final int handleHeight = getPreferredHeight();
Gilles Debunned88876a2012-03-16 17:34:04 -07004211 mTouchOffsetY = -0.3f * handleHeight;
4212 mIdealVerticalOffset = 0.7f * handleHeight;
4213 }
4214
Mady Mellor7a936442015-05-20 10:05:52 -07004215 public float getIdealVerticalOffset() {
4216 return mIdealVerticalOffset;
4217 }
4218
Gilles Debunned88876a2012-03-16 17:34:04 -07004219 protected void updateDrawable() {
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004220 if (mIsDragging) {
4221 // Don't update drawable during dragging.
4222 return;
4223 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004224 final Layout layout = mTextView.getLayout();
4225 if (layout == null) {
4226 return;
4227 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004228 final int offset = getCurrentCursorOffset();
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004229 final boolean isRtlCharAtOffset = isAtRtlRun(layout, offset);
Keisuke Kuroyanagi33f81ac2015-05-14 20:10:57 +09004230 final Drawable oldDrawable = mDrawable;
Gilles Debunned88876a2012-03-16 17:34:04 -07004231 mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
4232 mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
Adam Powell3fceabd2014-08-19 18:28:04 -07004233 mHorizontalGravity = getHorizontalGravity(isRtlCharAtOffset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004234 if (oldDrawable != mDrawable && isShowing()) {
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004235 // Update popup window position.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004236 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
4237 - getHorizontalOffset() + getCursorOffset();
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004238 mPositionX += mTextView.viewportToContentHorizontalOffset();
4239 mPositionHasChanged = true;
4240 updatePosition(mLastParentX, mLastParentY, false, false);
Keisuke Kuroyanagi33f81ac2015-05-14 20:10:57 +09004241 postInvalidate();
4242 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004243 }
4244
4245 protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
Adam Powell3fceabd2014-08-19 18:28:04 -07004246 protected abstract int getHorizontalGravity(boolean isRtlRun);
Gilles Debunned88876a2012-03-16 17:34:04 -07004247
4248 // Touch-up filter: number of previous positions remembered
4249 private static final int HISTORY_SIZE = 5;
4250 private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
4251 private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
4252 private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
4253 private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
4254 private int mPreviousOffsetIndex = 0;
4255 private int mNumberPreviousOffsets = 0;
4256
4257 private void startTouchUpFilter(int offset) {
4258 mNumberPreviousOffsets = 0;
4259 addPositionToTouchUpFilter(offset);
4260 }
4261
4262 private void addPositionToTouchUpFilter(int offset) {
4263 mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
4264 mPreviousOffsets[mPreviousOffsetIndex] = offset;
4265 mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
4266 mNumberPreviousOffsets++;
4267 }
4268
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004269 private void filterOnTouchUp(boolean fromTouchScreen) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004270 final long now = SystemClock.uptimeMillis();
4271 int i = 0;
4272 int index = mPreviousOffsetIndex;
4273 final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
4274 while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
4275 i++;
4276 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
4277 }
4278
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004279 if (i > 0 && i < iMax
4280 && (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004281 positionAtCursorOffset(mPreviousOffsets[index], false, fromTouchScreen);
Gilles Debunned88876a2012-03-16 17:34:04 -07004282 }
4283 }
4284
4285 public boolean offsetHasBeenChanged() {
4286 return mNumberPreviousOffsets > 1;
4287 }
4288
4289 @Override
4290 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Adam Powell3fceabd2014-08-19 18:28:04 -07004291 setMeasuredDimension(getPreferredWidth(), getPreferredHeight());
4292 }
4293
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004294 @Override
4295 public void invalidate() {
4296 super.invalidate();
4297 if (isShowing()) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004298 positionAtCursorOffset(getCurrentCursorOffset(), true, false);
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004299 }
4300 };
4301
Adam Powell3fceabd2014-08-19 18:28:04 -07004302 private int getPreferredWidth() {
4303 return Math.max(mDrawable.getIntrinsicWidth(), mMinSize);
4304 }
4305
4306 private int getPreferredHeight() {
4307 return Math.max(mDrawable.getIntrinsicHeight(), mMinSize);
Gilles Debunned88876a2012-03-16 17:34:04 -07004308 }
4309
4310 public void show() {
4311 if (isShowing()) return;
4312
4313 getPositionListener().addSubscriber(this, true /* local position may change */);
4314
4315 // Make sure the offset is always considered new, even when focusing at same position
4316 mPreviousOffset = -1;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004317 positionAtCursorOffset(getCurrentCursorOffset(), false, false);
Gilles Debunned88876a2012-03-16 17:34:04 -07004318 }
4319
4320 protected void dismiss() {
4321 mIsDragging = false;
4322 mContainer.dismiss();
4323 onDetached();
4324 }
4325
4326 public void hide() {
4327 dismiss();
4328
4329 getPositionListener().removeSubscriber(this);
4330 }
4331
Gilles Debunned88876a2012-03-16 17:34:04 -07004332 public boolean isShowing() {
4333 return mContainer.isShowing();
4334 }
4335
4336 private boolean isVisible() {
4337 // Always show a dragging handle.
4338 if (mIsDragging) {
4339 return true;
4340 }
4341
4342 if (mTextView.isInBatchEditMode()) {
4343 return false;
4344 }
4345
Phil Weaverc2e28932016-12-08 12:29:25 -08004346 return mTextView.isPositionVisible(
4347 mPositionX + mHotspotX + getHorizontalOffset(), mPositionY);
Gilles Debunned88876a2012-03-16 17:34:04 -07004348 }
4349
4350 public abstract int getCurrentCursorOffset();
4351
4352 protected abstract void updateSelection(int offset);
4353
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004354 protected abstract void updatePosition(float x, float y, boolean fromTouchScreen);
Gilles Debunned88876a2012-03-16 17:34:04 -07004355
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004356 protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
4357 return layout.isRtlCharAt(offset);
4358 }
4359
4360 @VisibleForTesting
4361 public float getHorizontal(@NonNull Layout layout, int offset) {
4362 return layout.getPrimaryHorizontal(offset);
4363 }
4364
4365 protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
4366 return mTextView.getOffsetAtCoordinate(line, x);
4367 }
4368
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004369 /**
4370 * @param offset Cursor offset. Must be in [-1, length].
4371 * @param forceUpdatePosition whether to force update the position. This should be true
4372 * when If the parent has been scrolled, for example.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004373 * @param fromTouchScreen {@code true} if the cursor is moved with motion events from the
4374 * touch screen.
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004375 */
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004376 protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
4377 boolean fromTouchScreen) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004378 // A HandleView relies on the layout, which may be nulled by external methods
4379 Layout layout = mTextView.getLayout();
4380 if (layout == null) {
4381 // Will update controllers' state, hiding them and stopping selection mode if needed
4382 prepareCursorControllers();
4383 return;
4384 }
Siyamed Sinir987ec652016-02-17 19:44:41 -08004385 layout = mTextView.getLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -07004386
4387 boolean offsetChanged = offset != mPreviousOffset;
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004388 if (offsetChanged || forceUpdatePosition) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004389 if (offsetChanged) {
4390 updateSelection(offset);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004391 if (fromTouchScreen && mHapticTextHandleEnabled) {
4392 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
4393 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004394 addPositionToTouchUpFilter(offset);
4395 }
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07004396 final int line = layout.getLineForOffset(offset);
Mady Mellorb9bbbb12015-03-23 11:50:46 -07004397 mPrevLine = line;
Gilles Debunned88876a2012-03-16 17:34:04 -07004398
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004399 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
4400 - getHorizontalOffset() + getCursorOffset();
Siyamed Sinira60b59d2017-07-26 09:26:41 -07004401 mPositionY = layout.getLineBottomWithoutSpacing(line);
Gilles Debunned88876a2012-03-16 17:34:04 -07004402
4403 // Take TextView's padding and scroll into account.
4404 mPositionX += mTextView.viewportToContentHorizontalOffset();
4405 mPositionY += mTextView.viewportToContentVerticalOffset();
4406
4407 mPreviousOffset = offset;
4408 mPositionHasChanged = true;
4409 }
4410 }
4411
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004412 /**
Roozbeh Pournader9c133072017-07-26 22:36:27 -07004413 * Return the clamped horizontal position for the cursor.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004414 *
4415 * @param layout Text layout.
4416 * @param offset Character offset for the cursor.
4417 * @return The clamped horizontal position for the cursor.
4418 */
4419 int getCursorHorizontalPosition(Layout layout, int offset) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004420 return (int) (getHorizontal(layout, offset) - 0.5f);
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004421 }
4422
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004423 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07004424 public void updatePosition(int parentPositionX, int parentPositionY,
4425 boolean parentPositionChanged, boolean parentScrolled) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004426 positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled, false);
Gilles Debunned88876a2012-03-16 17:34:04 -07004427 if (parentPositionChanged || mPositionHasChanged) {
4428 if (mIsDragging) {
4429 // Update touchToWindow offset in case of parent scrolling while dragging
4430 if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
4431 mTouchToWindowOffsetX += parentPositionX - mLastParentX;
4432 mTouchToWindowOffsetY += parentPositionY - mLastParentY;
4433 mLastParentX = parentPositionX;
4434 mLastParentY = parentPositionY;
4435 }
4436
4437 onHandleMoved();
4438 }
4439
4440 if (isVisible()) {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004441 // Transform to the window coordinates to follow the view tranformation.
4442 final int[] pts = { mPositionX + mHotspotX + getHorizontalOffset(), mPositionY};
4443 mTextView.transformFromViewToWindowSpace(pts);
4444 pts[0] -= mHotspotX + getHorizontalOffset();
4445
Gilles Debunned88876a2012-03-16 17:34:04 -07004446 if (isShowing()) {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004447 mContainer.update(pts[0], pts[1], -1, -1);
Gilles Debunned88876a2012-03-16 17:34:04 -07004448 } else {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004449 mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY, pts[0], pts[1]);
Gilles Debunned88876a2012-03-16 17:34:04 -07004450 }
4451 } else {
4452 if (isShowing()) {
4453 dismiss();
4454 }
4455 }
4456
4457 mPositionHasChanged = false;
4458 }
4459 }
4460
4461 @Override
4462 protected void onDraw(Canvas c) {
Adam Powell3fceabd2014-08-19 18:28:04 -07004463 final int drawWidth = mDrawable.getIntrinsicWidth();
4464 final int left = getHorizontalOffset();
4465
4466 mDrawable.setBounds(left, 0, left + drawWidth, mDrawable.getIntrinsicHeight());
Gilles Debunned88876a2012-03-16 17:34:04 -07004467 mDrawable.draw(c);
4468 }
4469
Adam Powell3fceabd2014-08-19 18:28:04 -07004470 private int getHorizontalOffset() {
4471 final int width = getPreferredWidth();
4472 final int drawWidth = mDrawable.getIntrinsicWidth();
4473 final int left;
4474 switch (mHorizontalGravity) {
4475 case Gravity.LEFT:
4476 left = 0;
4477 break;
4478 default:
4479 case Gravity.CENTER:
4480 left = (width - drawWidth) / 2;
4481 break;
4482 case Gravity.RIGHT:
4483 left = width - drawWidth;
4484 break;
4485 }
4486 return left;
4487 }
4488
4489 protected int getCursorOffset() {
4490 return 0;
4491 }
4492
Gilles Debunned88876a2012-03-16 17:34:04 -07004493 @Override
4494 public boolean onTouchEvent(MotionEvent ev) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01004495 updateFloatingToolbarVisibility(ev);
4496
Gilles Debunned88876a2012-03-16 17:34:04 -07004497 switch (ev.getActionMasked()) {
4498 case MotionEvent.ACTION_DOWN: {
4499 startTouchUpFilter(getCurrentCursorOffset());
Gilles Debunned88876a2012-03-16 17:34:04 -07004500
4501 final PositionListener positionListener = getPositionListener();
4502 mLastParentX = positionListener.getPositionX();
4503 mLastParentY = positionListener.getPositionY();
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004504 mLastParentXOnScreen = positionListener.getPositionXOnScreen();
4505 mLastParentYOnScreen = positionListener.getPositionYOnScreen();
4506
4507 final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
4508 final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
4509 mTouchToWindowOffsetX = xInWindow - mPositionX;
4510 mTouchToWindowOffsetY = yInWindow - mPositionY;
4511
Gilles Debunned88876a2012-03-16 17:34:04 -07004512 mIsDragging = true;
Mady Mellora6a0f782015-07-10 16:43:32 -07004513 mPreviousLineTouched = UNSET_LINE;
Gilles Debunned88876a2012-03-16 17:34:04 -07004514 break;
4515 }
4516
4517 case MotionEvent.ACTION_MOVE: {
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004518 final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
4519 final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
Gilles Debunned88876a2012-03-16 17:34:04 -07004520
4521 // Vertical hysteresis: vertical down movement tends to snap to ideal offset
4522 final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004523 final float currentVerticalOffset = yInWindow - mPositionY - mLastParentY;
Gilles Debunned88876a2012-03-16 17:34:04 -07004524 float newVerticalOffset;
4525 if (previousVerticalOffset < mIdealVerticalOffset) {
4526 newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
4527 newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
4528 } else {
4529 newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
4530 newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
4531 }
4532 mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
4533
Keisuke Kuroyanagibc89a5c2015-05-18 14:49:29 +09004534 final float newPosX =
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004535 xInWindow - mTouchToWindowOffsetX + mHotspotX + getHorizontalOffset();
4536 final float newPosY = yInWindow - mTouchToWindowOffsetY + mTouchOffsetY;
Gilles Debunned88876a2012-03-16 17:34:04 -07004537
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004538 updatePosition(newPosX, newPosY,
4539 ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Gilles Debunned88876a2012-03-16 17:34:04 -07004540 break;
4541 }
4542
4543 case MotionEvent.ACTION_UP:
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004544 filterOnTouchUp(ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Gilles Debunned88876a2012-03-16 17:34:04 -07004545 mIsDragging = false;
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004546 updateDrawable();
Gilles Debunned88876a2012-03-16 17:34:04 -07004547 break;
4548
4549 case MotionEvent.ACTION_CANCEL:
4550 mIsDragging = false;
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004551 updateDrawable();
Gilles Debunned88876a2012-03-16 17:34:04 -07004552 break;
4553 }
4554 return true;
4555 }
4556
4557 public boolean isDragging() {
4558 return mIsDragging;
4559 }
4560
Clara Bayarri6351e662015-03-16 23:17:59 +00004561 void onHandleMoved() {}
Gilles Debunned88876a2012-03-16 17:34:04 -07004562
Clara Bayarri6351e662015-03-16 23:17:59 +00004563 public void onDetached() {}
Gilles Debunned88876a2012-03-16 17:34:04 -07004564 }
4565
4566 private class InsertionHandleView extends HandleView {
4567 private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
4568 private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
4569
Clara Bayarrib71dddd2015-06-04 23:17:30 +01004570 // Used to detect taps on the insertion handle, which will affect the insertion action mode
Gilles Debunned88876a2012-03-16 17:34:04 -07004571 private float mDownPositionX, mDownPositionY;
4572 private Runnable mHider;
4573
4574 public InsertionHandleView(Drawable drawable) {
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004575 super(drawable, drawable, com.android.internal.R.id.insertion_handle);
Gilles Debunned88876a2012-03-16 17:34:04 -07004576 }
4577
4578 @Override
4579 public void show() {
4580 super.show();
4581
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01004582 final long durationSinceCutOrCopy =
Andrei Stingaceanu77b9c382015-05-06 13:25:19 +01004583 SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime;
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01004584
4585 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004586 if (mInsertionActionModeRunnable != null
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09004587 && ((mTapState == TAP_STATE_DOUBLE_TAP)
4588 || (mTapState == TAP_STATE_TRIPLE_CLICK)
4589 || isCursorInsideEasyCorrectionSpan())) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004590 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01004591 }
4592
4593 // Prepare and schedule the single tap runnable to run exactly after the double tap
4594 // timeout has passed.
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09004595 if ((mTapState != TAP_STATE_DOUBLE_TAP) && (mTapState != TAP_STATE_TRIPLE_CLICK)
4596 && !isCursorInsideEasyCorrectionSpan()
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01004597 && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION)) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01004598 if (mTextActionMode == null) {
4599 if (mInsertionActionModeRunnable == null) {
4600 mInsertionActionModeRunnable = new Runnable() {
4601 @Override
4602 public void run() {
4603 startInsertionActionMode();
4604 }
4605 };
4606 }
4607 mTextView.postDelayed(
4608 mInsertionActionModeRunnable,
4609 ViewConfiguration.getDoubleTapTimeout() + 1);
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01004610 }
4611
Gilles Debunned88876a2012-03-16 17:34:04 -07004612 }
4613
4614 hideAfterDelay();
4615 }
4616
Gilles Debunned88876a2012-03-16 17:34:04 -07004617 private void hideAfterDelay() {
4618 if (mHider == null) {
4619 mHider = new Runnable() {
4620 public void run() {
4621 hide();
4622 }
4623 };
4624 } else {
4625 removeHiderCallback();
4626 }
4627 mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
4628 }
4629
4630 private void removeHiderCallback() {
4631 if (mHider != null) {
4632 mTextView.removeCallbacks(mHider);
4633 }
4634 }
4635
4636 @Override
4637 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
4638 return drawable.getIntrinsicWidth() / 2;
4639 }
4640
4641 @Override
Adam Powell3fceabd2014-08-19 18:28:04 -07004642 protected int getHorizontalGravity(boolean isRtlRun) {
4643 return Gravity.CENTER_HORIZONTAL;
4644 }
4645
4646 @Override
4647 protected int getCursorOffset() {
4648 int offset = super.getCursorOffset();
Roozbeh Pournader9c133072017-07-26 22:36:27 -07004649 if (mCursorDrawable != null) {
4650 mCursorDrawable.getPadding(mTempRect);
4651 offset += (mCursorDrawable.getIntrinsicWidth()
4652 - mTempRect.left - mTempRect.right) / 2;
Adam Powell3fceabd2014-08-19 18:28:04 -07004653 }
4654 return offset;
4655 }
4656
4657 @Override
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004658 int getCursorHorizontalPosition(Layout layout, int offset) {
Roozbeh Pournader9c133072017-07-26 22:36:27 -07004659 if (mCursorDrawable != null) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004660 final float horizontal = getHorizontal(layout, offset);
Roozbeh Pournader9c133072017-07-26 22:36:27 -07004661 return clampHorizontalPosition(mCursorDrawable, horizontal) + mTempRect.left;
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004662 }
4663 return super.getCursorHorizontalPosition(layout, offset);
4664 }
4665
4666 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07004667 public boolean onTouchEvent(MotionEvent ev) {
4668 final boolean result = super.onTouchEvent(ev);
4669
4670 switch (ev.getActionMasked()) {
4671 case MotionEvent.ACTION_DOWN:
4672 mDownPositionX = ev.getRawX();
4673 mDownPositionY = ev.getRawY();
4674 break;
4675
4676 case MotionEvent.ACTION_UP:
4677 if (!offsetHasBeenChanged()) {
4678 final float deltaX = mDownPositionX - ev.getRawX();
4679 final float deltaY = mDownPositionY - ev.getRawY();
4680 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
4681
4682 final ViewConfiguration viewConfiguration = ViewConfiguration.get(
4683 mTextView.getContext());
4684 final int touchSlop = viewConfiguration.getScaledTouchSlop();
4685
4686 if (distanceSquared < touchSlop * touchSlop) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01004687 // Tapping on the handle toggles the insertion action mode.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004688 if (mTextActionMode != null) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08004689 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07004690 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004691 startInsertionActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07004692 }
4693 }
Abodunrinwa Tokibcdf0ab2015-04-25 00:11:25 +01004694 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004695 if (mTextActionMode != null) {
4696 mTextActionMode.invalidateContentRect();
Abodunrinwa Tokibcdf0ab2015-04-25 00:11:25 +01004697 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004698 }
4699 hideAfterDelay();
4700 break;
4701
4702 case MotionEvent.ACTION_CANCEL:
4703 hideAfterDelay();
4704 break;
4705
4706 default:
4707 break;
4708 }
4709
4710 return result;
4711 }
4712
4713 @Override
4714 public int getCurrentCursorOffset() {
4715 return mTextView.getSelectionStart();
4716 }
4717
4718 @Override
4719 public void updateSelection(int offset) {
4720 Selection.setSelection((Spannable) mTextView.getText(), offset);
4721 }
4722
4723 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004724 protected void updatePosition(float x, float y, boolean fromTouchScreen) {
Mady Melloree3821e2015-06-05 11:12:01 -07004725 Layout layout = mTextView.getLayout();
4726 int offset;
4727 if (layout != null) {
Mady Mellora6a0f782015-07-10 16:43:32 -07004728 if (mPreviousLineTouched == UNSET_LINE) {
4729 mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
4730 }
4731 int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004732 offset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellora6a0f782015-07-10 16:43:32 -07004733 mPreviousLineTouched = currLine;
Mady Melloree3821e2015-06-05 11:12:01 -07004734 } else {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004735 offset = -1;
Mady Melloree3821e2015-06-05 11:12:01 -07004736 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004737 positionAtCursorOffset(offset, false, fromTouchScreen);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004738 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01004739 invalidateActionMode();
Clara Bayarri1baed512015-05-11 15:29:16 +01004740 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004741 }
4742
4743 @Override
4744 void onHandleMoved() {
4745 super.onHandleMoved();
4746 removeHiderCallback();
4747 }
4748
4749 @Override
4750 public void onDetached() {
4751 super.onDetached();
4752 removeHiderCallback();
4753 }
4754 }
4755
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004756 @Retention(RetentionPolicy.SOURCE)
4757 @IntDef({HANDLE_TYPE_SELECTION_START, HANDLE_TYPE_SELECTION_END})
4758 public @interface HandleType {}
4759 public static final int HANDLE_TYPE_SELECTION_START = 0;
4760 public static final int HANDLE_TYPE_SELECTION_END = 1;
4761
Abodunrinwa Toki4a056a52017-08-05 01:56:40 +01004762 /** For selection handles */
4763 @VisibleForTesting
4764 public final class SelectionHandleView extends HandleView {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004765 // Indicates the handle type, selection start (HANDLE_TYPE_SELECTION_START) or selection
4766 // end (HANDLE_TYPE_SELECTION_END).
4767 @HandleType
4768 private final int mHandleType;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004769 // Indicates whether the cursor is making adjustments within a word.
4770 private boolean mInWord = false;
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004771 // Difference between touch position and word boundary position.
4772 private float mTouchWordDelta;
Mady Mellore264ac32015-06-22 16:46:29 -07004773 // X value of the previous updatePosition call.
4774 private float mPrevX;
4775 // Indicates if the handle has moved a boundary between LTR and RTL text.
4776 private boolean mLanguageDirectionChanged = false;
Mady Mellor42390aa2015-07-24 13:08:42 -07004777 // Distance from edge of horizontally scrolling text view
4778 // to use to switch to character mode.
4779 private final float mTextViewEdgeSlop;
4780 // Used to save text view location.
4781 private final int[] mTextViewLocation = new int[2];
Gilles Debunned88876a2012-03-16 17:34:04 -07004782
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004783 public SelectionHandleView(Drawable drawableLtr, Drawable drawableRtl, int id,
4784 @HandleType int handleType) {
4785 super(drawableLtr, drawableRtl, id);
4786 mHandleType = handleType;
4787 ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext());
Mady Mellor42390aa2015-07-24 13:08:42 -07004788 mTextViewEdgeSlop = viewConfiguration.getScaledTouchSlop() * 4;
Gilles Debunned88876a2012-03-16 17:34:04 -07004789 }
4790
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004791 private boolean isStartHandle() {
4792 return mHandleType == HANDLE_TYPE_SELECTION_START;
4793 }
4794
Gilles Debunned88876a2012-03-16 17:34:04 -07004795 @Override
4796 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004797 if (isRtlRun == isStartHandle()) {
Mady Mellor709386f2015-05-14 12:41:18 -07004798 return drawable.getIntrinsicWidth() / 4;
4799 } else {
4800 return (drawable.getIntrinsicWidth() * 3) / 4;
4801 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004802 }
4803
4804 @Override
Adam Powell3fceabd2014-08-19 18:28:04 -07004805 protected int getHorizontalGravity(boolean isRtlRun) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004806 return (isRtlRun == isStartHandle()) ? Gravity.LEFT : Gravity.RIGHT;
Adam Powell3fceabd2014-08-19 18:28:04 -07004807 }
4808
4809 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07004810 public int getCurrentCursorOffset() {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004811 return isStartHandle() ? mTextView.getSelectionStart() : mTextView.getSelectionEnd();
Gilles Debunned88876a2012-03-16 17:34:04 -07004812 }
4813
4814 @Override
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004815 protected void updateSelection(int offset) {
4816 if (isStartHandle()) {
4817 Selection.setSelection((Spannable) mTextView.getText(), offset,
4818 mTextView.getSelectionEnd());
4819 } else {
4820 Selection.setSelection((Spannable) mTextView.getText(),
4821 mTextView.getSelectionStart(), offset);
4822 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004823 updateDrawable();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004824 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01004825 invalidateActionMode();
Clara Bayarri13152d12015-04-09 12:02:04 +01004826 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004827 }
4828
4829 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004830 protected void updatePosition(float x, float y, boolean fromTouchScreen) {
Mady Mellor81fa3e82015-05-14 09:17:41 -07004831 final Layout layout = mTextView.getLayout();
Mady Mellorcc65c372015-06-17 09:25:19 -07004832 if (layout == null) {
4833 // HandleView will deal appropriately in positionAtCursorOffset when
4834 // layout is null.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004835 positionAndAdjustForCrossingHandles(mTextView.getOffsetForPosition(x, y),
4836 fromTouchScreen);
Mady Mellorcc65c372015-06-17 09:25:19 -07004837 return;
4838 }
4839
Mady Mellora6a0f782015-07-10 16:43:32 -07004840 if (mPreviousLineTouched == UNSET_LINE) {
4841 mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
4842 }
4843
Mady Mellorb9bbbb12015-03-23 11:50:46 -07004844 boolean positionCursor = false;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004845 final int anotherHandleOffset =
4846 isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
Mady Mellora6a0f782015-07-10 16:43:32 -07004847 int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004848 int initialOffset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellor81fa3e82015-05-14 09:17:41 -07004849
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004850 if (isStartHandle() && initialOffset >= anotherHandleOffset
4851 || !isStartHandle() && initialOffset <= anotherHandleOffset) {
4852 // Handles have crossed, bound it to the first selected line and
Mady Mellor81fa3e82015-05-14 09:17:41 -07004853 // adjust by word / char as normal.
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07004854 currLine = layout.getLineForOffset(anotherHandleOffset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004855 initialOffset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellor81fa3e82015-05-14 09:17:41 -07004856 }
4857
4858 int offset = initialOffset;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004859 final int wordEnd = getWordEnd(offset);
4860 final int wordStart = getWordStart(offset);
Gilles Debunned88876a2012-03-16 17:34:04 -07004861
Mady Mellore264ac32015-06-22 16:46:29 -07004862 if (mPrevX == UNSET_X_VALUE) {
4863 mPrevX = x;
4864 }
4865
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004866 final int currentOffset = getCurrentCursorOffset();
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004867 final boolean rtlAtCurrentOffset = isAtRtlRun(layout, currentOffset);
4868 final boolean atRtl = isAtRtlRun(layout, offset);
Mady Mellore264ac32015-06-22 16:46:29 -07004869 final boolean isLvlBoundary = layout.isLevelBoundary(offset);
Mady Mellore264ac32015-06-22 16:46:29 -07004870
4871 // We can't determine if the user is expanding or shrinking the selection if they're
4872 // on a bi-di boundary, so until they've moved past the boundary we'll just place
4873 // the cursor at the current position.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004874 if (isLvlBoundary || (rtlAtCurrentOffset && !atRtl) || (!rtlAtCurrentOffset && atRtl)) {
Mady Mellore264ac32015-06-22 16:46:29 -07004875 // We're on a boundary or this is the first direction change -- just update
4876 // to the current position.
4877 mLanguageDirectionChanged = true;
4878 mTouchWordDelta = 0.0f;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004879 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellore264ac32015-06-22 16:46:29 -07004880 return;
4881 } else if (mLanguageDirectionChanged && !isLvlBoundary) {
4882 // We've just moved past the boundary so update the position. After this we can
4883 // figure out if the user is expanding or shrinking to go by word or character.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004884 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellore264ac32015-06-22 16:46:29 -07004885 mTouchWordDelta = 0.0f;
4886 mLanguageDirectionChanged = false;
4887 return;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004888 }
4889
4890 boolean isExpanding;
4891 final float xDiff = x - mPrevX;
Keisuke Kuroyanagi26454142015-12-02 15:04:57 -08004892 if (isStartHandle()) {
4893 isExpanding = currLine < mPreviousLineTouched;
Mady Mellore264ac32015-06-22 16:46:29 -07004894 } else {
Keisuke Kuroyanagi26454142015-12-02 15:04:57 -08004895 isExpanding = currLine > mPreviousLineTouched;
4896 }
4897 if (atRtl == isStartHandle()) {
4898 isExpanding |= xDiff > 0;
4899 } else {
4900 isExpanding |= xDiff < 0;
Mady Mellore264ac32015-06-22 16:46:29 -07004901 }
4902
Mady Mellor42390aa2015-07-24 13:08:42 -07004903 if (mTextView.getHorizontallyScrolling()) {
4904 if (positionNearEdgeOfScrollingView(x, atRtl)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004905 && ((isStartHandle() && mTextView.getScrollX() != 0)
4906 || (!isStartHandle()
4907 && mTextView.canScrollHorizontally(atRtl ? -1 : 1)))
4908 && ((isExpanding && ((isStartHandle() && offset < currentOffset)
4909 || (!isStartHandle() && offset > currentOffset)))
4910 || !isExpanding)) {
4911 // If we're expanding ensure that the offset is actually expanding compared to
4912 // the current offset, if the handle snapped to the word, the finger position
Mady Mellor42390aa2015-07-24 13:08:42 -07004913 // may be out of sync and we don't want the selection to jump back.
4914 mTouchWordDelta = 0.0f;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004915 final int nextOffset = (atRtl == isStartHandle())
4916 ? layout.getOffsetToRightOf(mPreviousOffset)
Mady Mellor42390aa2015-07-24 13:08:42 -07004917 : layout.getOffsetToLeftOf(mPreviousOffset);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004918 positionAndAdjustForCrossingHandles(nextOffset, fromTouchScreen);
Mady Mellor42390aa2015-07-24 13:08:42 -07004919 return;
4920 }
4921 }
4922
Mady Mellore264ac32015-06-22 16:46:29 -07004923 if (isExpanding) {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004924 // User is increasing the selection.
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004925 int wordBoundary = isStartHandle() ? wordStart : wordEnd;
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07004926 final boolean snapToWord = (!mInWord
4927 || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine))
4928 && atRtl == isAtRtlRun(layout, wordBoundary);
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004929 if (snapToWord) {
Mady Mellora5266832015-06-26 14:28:12 -07004930 // Sometimes words can be broken across lines (Chinese, hyphenation).
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004931 // We still snap to the word boundary but we only use the letters on the
Mady Mellora5266832015-06-26 14:28:12 -07004932 // current line to determine if the user is far enough into the word to snap.
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07004933 if (layout.getLineForOffset(wordBoundary) != currLine) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004934 wordBoundary = isStartHandle()
4935 ? layout.getLineStart(currLine) : layout.getLineEnd(currLine);
Mady Mellora5266832015-06-26 14:28:12 -07004936 }
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004937 final int offsetThresholdToSnap = isStartHandle()
4938 ? wordEnd - ((wordEnd - wordBoundary) / 2)
4939 : wordStart + ((wordBoundary - wordStart) / 2);
4940 if (isStartHandle()
4941 && (offset <= offsetThresholdToSnap || currLine < mPrevLine)) {
4942 // User is far enough into the word or on a different line so we expand by
4943 // word.
4944 offset = wordStart;
4945 } else if (!isStartHandle()
4946 && (offset >= offsetThresholdToSnap || currLine > mPrevLine)) {
4947 // User is far enough into the word or on a different line so we expand by
4948 // word.
4949 offset = wordEnd;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004950 } else {
Mady Mellorc2225b92015-04-01 15:59:20 -07004951 offset = mPreviousOffset;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004952 }
4953 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004954 if ((isStartHandle() && offset < initialOffset)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004955 || (!isStartHandle() && offset > initialOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004956 final float adjustedX = getHorizontal(layout, offset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004957 mTouchWordDelta =
4958 mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
Keisuke Kuroyanagi50a927c2015-05-07 17:34:21 +09004959 } else {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004960 mTouchWordDelta = 0.0f;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004961 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004962 positionCursor = true;
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004963 } else {
4964 final int adjustedOffset =
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004965 getOffsetAtCoordinate(layout, currLine, x - mTouchWordDelta);
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004966 final boolean shrinking = isStartHandle()
4967 ? adjustedOffset > mPreviousOffset || currLine > mPrevLine
4968 : adjustedOffset < mPreviousOffset || currLine < mPrevLine;
4969 if (shrinking) {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004970 // User is shrinking the selection.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004971 if (currLine != mPrevLine) {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004972 // We're on a different line, so we'll snap to word boundaries.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004973 offset = isStartHandle() ? wordStart : wordEnd;
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004974 if ((isStartHandle() && offset < initialOffset)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004975 || (!isStartHandle() && offset > initialOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004976 final float adjustedX = getHorizontal(layout, offset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004977 mTouchWordDelta =
4978 mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
4979 } else {
4980 mTouchWordDelta = 0.0f;
4981 }
4982 } else {
4983 offset = adjustedOffset;
4984 }
4985 positionCursor = true;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004986 } else if ((isStartHandle() && adjustedOffset < mPreviousOffset)
4987 || (!isStartHandle() && adjustedOffset > mPreviousOffset)) {
4988 // Handle has jumped to the word boundary, and the user is moving
Mady Mellor43fd2f42015-06-08 14:03:34 -07004989 // their finger towards the handle, the delta should be updated.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004990 mTouchWordDelta = mTextView.convertToLocalHorizontalCoordinate(x)
4991 - getHorizontal(layout, mPreviousOffset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004992 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004993 }
4994
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004995 if (positionCursor) {
Mady Mellora6a0f782015-07-10 16:43:32 -07004996 mPreviousLineTouched = currLine;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004997 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004998 }
Mady Mellore264ac32015-06-22 16:46:29 -07004999 mPrevX = x;
Gilles Debunned88876a2012-03-16 17:34:04 -07005000 }
5001
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005002 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005003 protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
5004 boolean fromTouchScreen) {
5005 super.positionAtCursorOffset(offset, forceUpdatePosition, fromTouchScreen);
Yoshiki Iguchi9582e152015-10-15 13:34:41 +09005006 mInWord = (offset != -1) && !getWordIteratorWithText().isBoundary(offset);
Mady Mellor36d5a7b2015-05-22 10:31:12 -07005007 }
5008
5009 @Override
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005010 public boolean onTouchEvent(MotionEvent event) {
5011 boolean superResult = super.onTouchEvent(event);
Mady Mellora6a0f782015-07-10 16:43:32 -07005012 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
5013 // Reset the touch word offset and x value when the user
5014 // re-engages the handle.
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005015 mTouchWordDelta = 0.0f;
Mady Mellore264ac32015-06-22 16:46:29 -07005016 mPrevX = UNSET_X_VALUE;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005017 }
5018 return superResult;
5019 }
Mady Mellor42390aa2015-07-24 13:08:42 -07005020
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005021 private void positionAndAdjustForCrossingHandles(int offset, boolean fromTouchScreen) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005022 final int anotherHandleOffset =
5023 isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
5024 if ((isStartHandle() && offset >= anotherHandleOffset)
5025 || (!isStartHandle() && offset <= anotherHandleOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005026 mTouchWordDelta = 0.0f;
5027 final Layout layout = mTextView.getLayout();
5028 if (layout != null && offset != anotherHandleOffset) {
5029 final float horiz = getHorizontal(layout, offset);
5030 final float anotherHandleHoriz = getHorizontal(layout, anotherHandleOffset,
5031 !isStartHandle());
5032 final float currentHoriz = getHorizontal(layout, mPreviousOffset);
5033 if (currentHoriz < anotherHandleHoriz && horiz < anotherHandleHoriz
5034 || currentHoriz > anotherHandleHoriz && horiz > anotherHandleHoriz) {
5035 // This handle passes another one as it crossed a direction boundary.
5036 // Don't minimize the selection, but keep the handle at the run boundary.
5037 final int currentOffset = getCurrentCursorOffset();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005038 final int offsetToGetRunRange = isStartHandle()
5039 ? currentOffset : Math.max(currentOffset - 1, 0);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005040 final long range = layout.getRunRange(offsetToGetRunRange);
5041 if (isStartHandle()) {
5042 offset = TextUtils.unpackRangeStartFromLong(range);
5043 } else {
5044 offset = TextUtils.unpackRangeEndFromLong(range);
5045 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005046 positionAtCursorOffset(offset, false, fromTouchScreen);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005047 return;
5048 }
5049 }
Mady Mellor42390aa2015-07-24 13:08:42 -07005050 // Handles can not cross and selection is at least one character.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005051 offset = getNextCursorOffset(anotherHandleOffset, !isStartHandle());
Mady Mellor42390aa2015-07-24 13:08:42 -07005052 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005053 positionAtCursorOffset(offset, false, fromTouchScreen);
Mady Mellor42390aa2015-07-24 13:08:42 -07005054 }
5055
Mady Mellor42390aa2015-07-24 13:08:42 -07005056 private boolean positionNearEdgeOfScrollingView(float x, boolean atRtl) {
5057 mTextView.getLocationOnScreen(mTextViewLocation);
5058 boolean nearEdge;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005059 if (atRtl == isStartHandle()) {
Mady Mellor42390aa2015-07-24 13:08:42 -07005060 int rightEdge = mTextViewLocation[0] + mTextView.getWidth()
5061 - mTextView.getPaddingRight();
5062 nearEdge = x > rightEdge - mTextViewEdgeSlop;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005063 } else {
5064 int leftEdge = mTextViewLocation[0] + mTextView.getPaddingLeft();
5065 nearEdge = x < leftEdge + mTextViewEdgeSlop;
Mady Mellor42390aa2015-07-24 13:08:42 -07005066 }
5067 return nearEdge;
5068 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005069
5070 @Override
5071 protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
5072 final int offsetToCheck = isStartHandle() ? offset : Math.max(offset - 1, 0);
5073 return layout.isRtlCharAt(offsetToCheck);
5074 }
5075
5076 @Override
5077 public float getHorizontal(@NonNull Layout layout, int offset) {
5078 return getHorizontal(layout, offset, isStartHandle());
5079 }
5080
5081 private float getHorizontal(@NonNull Layout layout, int offset, boolean startHandle) {
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005082 final int line = layout.getLineForOffset(offset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005083 final int offsetToCheck = startHandle ? offset : Math.max(offset - 1, 0);
5084 final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
5085 final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005086 return (isRtlChar == isRtlParagraph)
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005087 ? layout.getPrimaryHorizontal(offset) : layout.getSecondaryHorizontal(offset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005088 }
5089
5090 @Override
5091 protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
Keisuke Kuroyanagib1b88652016-04-05 16:26:16 +09005092 final float localX = mTextView.convertToLocalHorizontalCoordinate(x);
5093 final int primaryOffset = layout.getOffsetForHorizontal(line, localX, true);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005094 if (!layout.isLevelBoundary(primaryOffset)) {
5095 return primaryOffset;
5096 }
Keisuke Kuroyanagib1b88652016-04-05 16:26:16 +09005097 final int secondaryOffset = layout.getOffsetForHorizontal(line, localX, false);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005098 final int currentOffset = getCurrentCursorOffset();
5099 final int primaryDiff = Math.abs(primaryOffset - currentOffset);
5100 final int secondaryDiff = Math.abs(secondaryOffset - currentOffset);
5101 if (primaryDiff < secondaryDiff) {
5102 return primaryOffset;
5103 } else if (primaryDiff > secondaryDiff) {
5104 return secondaryOffset;
5105 } else {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005106 final int offsetToCheck = isStartHandle()
5107 ? currentOffset : Math.max(currentOffset - 1, 0);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005108 final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
5109 final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
5110 return isRtlChar == isRtlParagraph ? primaryOffset : secondaryOffset;
5111 }
5112 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005113 }
5114
Mady Mellorcc65c372015-06-17 09:25:19 -07005115 private int getCurrentLineAdjustedForSlop(Layout layout, int prevLine, float y) {
Mady Mellor80679072015-07-09 16:05:36 -07005116 final int trueLine = mTextView.getLineAtCoordinate(y);
Mady Mellorcc65c372015-06-17 09:25:19 -07005117 if (layout == null || prevLine > layout.getLineCount()
5118 || layout.getLineCount() <= 0 || prevLine < 0) {
5119 // Invalid parameters, just return whatever line is at y.
Mady Mellor80679072015-07-09 16:05:36 -07005120 return trueLine;
5121 }
5122
5123 if (Math.abs(trueLine - prevLine) >= 2) {
5124 // Only stick to lines if we're within a line of the previous selection.
5125 return trueLine;
Mady Mellorcc65c372015-06-17 09:25:19 -07005126 }
5127
5128 final float verticalOffset = mTextView.viewportToContentVerticalOffset();
5129 final int lineCount = layout.getLineCount();
5130 final float slop = mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS;
5131
5132 final float firstLineTop = layout.getLineTop(0) + verticalOffset;
5133 final float prevLineTop = layout.getLineTop(prevLine) + verticalOffset;
5134 final float yTopBound = Math.max(prevLineTop - slop, firstLineTop + slop);
5135
5136 final float lastLineBottom = layout.getLineBottom(lineCount - 1) + verticalOffset;
5137 final float prevLineBottom = layout.getLineBottom(prevLine) + verticalOffset;
5138 final float yBottomBound = Math.min(prevLineBottom + slop, lastLineBottom - slop);
5139
5140 // Determine if we've moved lines based on y position and previous line.
5141 int currLine;
5142 if (y <= yTopBound) {
5143 currLine = Math.max(prevLine - 1, 0);
5144 } else if (y >= yBottomBound) {
5145 currLine = Math.min(prevLine + 1, lineCount - 1);
5146 } else {
5147 currLine = prevLine;
5148 }
5149 return currLine;
5150 }
5151
Gilles Debunned88876a2012-03-16 17:34:04 -07005152 /**
5153 * A CursorController instance can be used to control a cursor in the text.
5154 */
5155 private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
5156 /**
5157 * Makes the cursor controller visible on screen.
5158 * See also {@link #hide()}.
5159 */
5160 public void show();
5161
5162 /**
5163 * Hide the cursor controller from screen.
5164 * See also {@link #show()}.
5165 */
5166 public void hide();
5167
5168 /**
5169 * Called when the view is detached from window. Perform house keeping task, such as
5170 * stopping Runnable thread that would otherwise keep a reference on the context, thus
5171 * preventing the activity from being recycled.
5172 */
5173 public void onDetached();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005174
5175 public boolean isCursorBeingModified();
5176
5177 public boolean isActive();
Gilles Debunned88876a2012-03-16 17:34:04 -07005178 }
5179
5180 private class InsertionPointCursorController implements CursorController {
5181 private InsertionHandleView mHandle;
5182
5183 public void show() {
5184 getHandle().show();
Andrei Stingaceanu35c550c2015-05-07 16:49:49 +01005185
5186 if (mSelectionModifierCursorController != null) {
5187 mSelectionModifierCursorController.hide();
5188 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005189 }
5190
Gilles Debunned88876a2012-03-16 17:34:04 -07005191 public void hide() {
5192 if (mHandle != null) {
5193 mHandle.hide();
5194 }
5195 }
5196
5197 public void onTouchModeChanged(boolean isInTouchMode) {
5198 if (!isInTouchMode) {
5199 hide();
5200 }
5201 }
5202
5203 private InsertionHandleView getHandle() {
5204 if (mSelectHandleCenter == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08005205 mSelectHandleCenter = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07005206 mTextView.mTextSelectHandleRes);
5207 }
5208 if (mHandle == null) {
5209 mHandle = new InsertionHandleView(mSelectHandleCenter);
5210 }
5211 return mHandle;
5212 }
5213
5214 @Override
5215 public void onDetached() {
5216 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
5217 observer.removeOnTouchModeChangeListener(this);
5218
5219 if (mHandle != null) mHandle.onDetached();
5220 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005221
5222 @Override
5223 public boolean isCursorBeingModified() {
5224 return mHandle != null && mHandle.isDragging();
5225 }
5226
5227 @Override
5228 public boolean isActive() {
5229 return mHandle != null && mHandle.isShowing();
5230 }
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09005231
5232 public void invalidateHandle() {
5233 if (mHandle != null) {
5234 mHandle.invalidate();
5235 }
5236 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005237 }
5238
5239 class SelectionModifierCursorController implements CursorController {
Gilles Debunned88876a2012-03-16 17:34:04 -07005240 // The cursor controller handles, lazily created when shown.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005241 private SelectionHandleView mStartHandle;
5242 private SelectionHandleView mEndHandle;
Gilles Debunned88876a2012-03-16 17:34:04 -07005243 // The offsets of that last touch down event. Remembered to start selection there.
5244 private int mMinTouchOffset, mMaxTouchOffset;
5245
Gilles Debunned88876a2012-03-16 17:34:04 -07005246 private float mDownPositionX, mDownPositionY;
5247 private boolean mGestureStayedInTapRegion;
5248
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005249 // Where the user first starts the drag motion.
5250 private int mStartOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005251
Mady Mellor7a936442015-05-20 10:05:52 -07005252 private boolean mHaventMovedEnoughToStartDrag;
Mady Mellorf9f8aeb2015-06-17 09:46:01 -07005253 // The line that a selection happened most recently with the drag accelerator.
5254 private int mLineSelectionIsOn = -1;
5255 // Whether the drag accelerator has selected past the initial line.
5256 private boolean mSwitchedLines = false;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005257
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005258 // Indicates the drag accelerator mode that the user is currently using.
5259 private int mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
5260 // Drag accelerator is inactive.
5261 private static final int DRAG_ACCELERATOR_MODE_INACTIVE = 0;
5262 // Character based selection by dragging. Only for mouse.
5263 private static final int DRAG_ACCELERATOR_MODE_CHARACTER = 1;
5264 // Word based selection by dragging. Enabled after long pressing or double tapping.
5265 private static final int DRAG_ACCELERATOR_MODE_WORD = 2;
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005266 // Paragraph based selection by dragging. Enabled after mouse triple click.
5267 private static final int DRAG_ACCELERATOR_MODE_PARAGRAPH = 3;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005268
Gilles Debunned88876a2012-03-16 17:34:04 -07005269 SelectionModifierCursorController() {
5270 resetTouchOffsets();
5271 }
5272
5273 public void show() {
5274 if (mTextView.isInBatchEditMode()) {
5275 return;
5276 }
5277 initDrawables();
5278 initHandles();
Gilles Debunned88876a2012-03-16 17:34:04 -07005279 }
5280
5281 private void initDrawables() {
5282 if (mSelectHandleLeft == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08005283 mSelectHandleLeft = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07005284 mTextView.mTextSelectHandleLeftRes);
5285 }
5286 if (mSelectHandleRight == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08005287 mSelectHandleRight = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07005288 mTextView.mTextSelectHandleRightRes);
5289 }
5290 }
5291
5292 private void initHandles() {
5293 // Lazy object creation has to be done before updatePosition() is called.
5294 if (mStartHandle == null) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005295 mStartHandle = new SelectionHandleView(mSelectHandleLeft, mSelectHandleRight,
5296 com.android.internal.R.id.selection_start_handle,
5297 HANDLE_TYPE_SELECTION_START);
Gilles Debunned88876a2012-03-16 17:34:04 -07005298 }
5299 if (mEndHandle == null) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005300 mEndHandle = new SelectionHandleView(mSelectHandleRight, mSelectHandleLeft,
5301 com.android.internal.R.id.selection_end_handle,
5302 HANDLE_TYPE_SELECTION_END);
Gilles Debunned88876a2012-03-16 17:34:04 -07005303 }
5304
5305 mStartHandle.show();
5306 mEndHandle.show();
5307
Gilles Debunned88876a2012-03-16 17:34:04 -07005308 hideInsertionPointCursorController();
5309 }
5310
5311 public void hide() {
5312 if (mStartHandle != null) mStartHandle.hide();
5313 if (mEndHandle != null) mEndHandle.hide();
5314 }
5315
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005316 public void enterDrag(int dragAcceleratorMode) {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005317 // Just need to init the handles / hide insertion cursor.
5318 show();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005319 mDragAcceleratorMode = dragAcceleratorMode;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005320 // Start location of selection.
5321 mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX,
5322 mLastDownPositionY);
Mady Mellorf9f8aeb2015-06-17 09:46:01 -07005323 mLineSelectionIsOn = mTextView.getLineAtCoordinate(mLastDownPositionY);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005324 // Don't show the handles until user has lifted finger.
5325 hide();
5326
5327 // This stops scrolling parents from intercepting the touch event, allowing
5328 // the user to continue dragging across the screen to select text; TextView will
5329 // scroll as necessary.
5330 mTextView.getParent().requestDisallowInterceptTouchEvent(true);
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005331 mTextView.cancelLongPress();
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005332 }
5333
Gilles Debunned88876a2012-03-16 17:34:04 -07005334 public void onTouchEvent(MotionEvent event) {
5335 // This is done even when the View does not have focus, so that long presses can start
5336 // selection and tap can move cursor from this tap position.
Mady Mellor7a936442015-05-20 10:05:52 -07005337 final float eventX = event.getX();
5338 final float eventY = event.getY();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005339 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
Gilles Debunned88876a2012-03-16 17:34:04 -07005340 switch (event.getActionMasked()) {
5341 case MotionEvent.ACTION_DOWN:
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005342 if (extractedTextModeWillBeStarted()) {
5343 // Prevent duplicating the selection handles until the mode starts.
5344 hide();
5345 } else {
5346 // Remember finger down position, to be able to start selection from there.
5347 mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(
5348 eventX, eventY);
Gilles Debunned88876a2012-03-16 17:34:04 -07005349
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005350 // Double tap detection
5351 if (mGestureStayedInTapRegion) {
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09005352 if (mTapState == TAP_STATE_DOUBLE_TAP
5353 || mTapState == TAP_STATE_TRIPLE_CLICK) {
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005354 final float deltaX = eventX - mDownPositionX;
5355 final float deltaY = eventY - mDownPositionY;
5356 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005357
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005358 ViewConfiguration viewConfiguration = ViewConfiguration.get(
5359 mTextView.getContext());
5360 int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
5361 boolean stayedInArea =
5362 distanceSquared < doubleTapSlop * doubleTapSlop;
Gilles Debunned88876a2012-03-16 17:34:04 -07005363
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005364 if (stayedInArea && (isMouse || isPositionOnText(eventX, eventY))) {
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09005365 if (mTapState == TAP_STATE_DOUBLE_TAP) {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005366 selectCurrentWordAndStartDrag();
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09005367 } else if (mTapState == TAP_STATE_TRIPLE_CLICK) {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005368 selectCurrentParagraphAndStartDrag();
5369 }
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005370 mDiscardNextActionUp = true;
5371 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005372 }
5373 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005374
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005375 mDownPositionX = eventX;
5376 mDownPositionY = eventY;
5377 mGestureStayedInTapRegion = true;
5378 mHaventMovedEnoughToStartDrag = true;
5379 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005380 break;
5381
5382 case MotionEvent.ACTION_POINTER_DOWN:
5383 case MotionEvent.ACTION_POINTER_UP:
5384 // Handle multi-point gestures. Keep min and max offset positions.
5385 // Only activated for devices that correctly handle multi-touch.
5386 if (mTextView.getContext().getPackageManager().hasSystemFeature(
5387 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
5388 updateMinAndMaxOffsets(event);
5389 }
5390 break;
5391
5392 case MotionEvent.ACTION_MOVE:
Mady Mellor7a936442015-05-20 10:05:52 -07005393 final ViewConfiguration viewConfig = ViewConfiguration.get(
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005394 mTextView.getContext());
Mady Mellor7a936442015-05-20 10:05:52 -07005395 final int touchSlop = viewConfig.getScaledTouchSlop();
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005396
Mady Mellor7a936442015-05-20 10:05:52 -07005397 if (mGestureStayedInTapRegion || mHaventMovedEnoughToStartDrag) {
5398 final float deltaX = eventX - mDownPositionX;
5399 final float deltaY = eventY - mDownPositionY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005400 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
5401
Mady Mellor7a936442015-05-20 10:05:52 -07005402 if (mGestureStayedInTapRegion) {
5403 int doubleTapTouchSlop = viewConfig.getScaledDoubleTapTouchSlop();
5404 mGestureStayedInTapRegion =
5405 distanceSquared <= doubleTapTouchSlop * doubleTapTouchSlop;
5406 }
5407 if (mHaventMovedEnoughToStartDrag) {
5408 // We don't start dragging until the user has moved enough.
5409 mHaventMovedEnoughToStartDrag =
5410 distanceSquared <= touchSlop * touchSlop;
Gilles Debunned88876a2012-03-16 17:34:04 -07005411 }
5412 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005413
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005414 if (isMouse && !isDragAcceleratorActive()) {
5415 final int offset = mTextView.getOffsetForPosition(eventX, eventY);
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09005416 if (mTextView.hasSelection()
5417 && (!mHaventMovedEnoughToStartDrag || mStartOffset != offset)
5418 && offset >= mTextView.getSelectionStart()
5419 && offset <= mTextView.getSelectionEnd()) {
5420 startDragAndDrop();
5421 break;
5422 }
5423
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005424 if (mStartOffset != offset) {
5425 // Start character based drag accelerator.
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005426 stopTextActionMode();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005427 enterDrag(DRAG_ACCELERATOR_MODE_CHARACTER);
5428 mDiscardNextActionUp = true;
5429 mHaventMovedEnoughToStartDrag = false;
5430 }
5431 }
5432
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005433 if (mStartHandle != null && mStartHandle.isShowing()) {
5434 // Don't do the drag if the handles are showing already.
5435 break;
5436 }
5437
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005438 updateSelection(event);
Gilles Debunned88876a2012-03-16 17:34:04 -07005439 break;
5440
5441 case MotionEvent.ACTION_UP:
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005442 if (!isDragAcceleratorActive()) {
5443 break;
5444 }
5445 updateSelection(event);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005446
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005447 // No longer dragging to select text, let the parent intercept events.
5448 mTextView.getParent().requestDisallowInterceptTouchEvent(false);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005449
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005450 // No longer the first dragging motion, reset.
5451 resetDragAcceleratorState();
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09005452
5453 if (mTextView.hasSelection()) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01005454 // Drag selection should not be adjusted by the text classifier.
5455 startSelectionActionModeAsync(mHaventMovedEnoughToStartDrag);
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09005456 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005457 break;
5458 }
5459 }
5460
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005461 private void updateSelection(MotionEvent event) {
5462 if (mTextView.getLayout() != null) {
5463 switch (mDragAcceleratorMode) {
5464 case DRAG_ACCELERATOR_MODE_CHARACTER:
5465 updateCharacterBasedSelection(event);
5466 break;
5467 case DRAG_ACCELERATOR_MODE_WORD:
5468 updateWordBasedSelection(event);
5469 break;
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005470 case DRAG_ACCELERATOR_MODE_PARAGRAPH:
5471 updateParagraphBasedSelection(event);
5472 break;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005473 }
5474 }
5475 }
5476
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005477 /**
5478 * If the TextView allows text selection, selects the current paragraph and starts a drag.
5479 *
5480 * @return true if the drag was started.
5481 */
5482 private boolean selectCurrentParagraphAndStartDrag() {
5483 if (mInsertionActionModeRunnable != null) {
5484 mTextView.removeCallbacks(mInsertionActionModeRunnable);
5485 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005486 stopTextActionMode();
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005487 if (!selectCurrentParagraph()) {
5488 return false;
5489 }
5490 enterDrag(SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_PARAGRAPH);
5491 return true;
5492 }
5493
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005494 private void updateCharacterBasedSelection(MotionEvent event) {
5495 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005496 updateSelectionInternal(mStartOffset, offset,
5497 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005498 }
5499
5500 private void updateWordBasedSelection(MotionEvent event) {
5501 if (mHaventMovedEnoughToStartDrag) {
5502 return;
5503 }
5504 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
5505 final ViewConfiguration viewConfig = ViewConfiguration.get(
5506 mTextView.getContext());
5507 final float eventX = event.getX();
5508 final float eventY = event.getY();
5509 final int currLine;
5510 if (isMouse) {
5511 // No need to offset the y coordinate for mouse input.
5512 currLine = mTextView.getLineAtCoordinate(eventY);
5513 } else {
5514 float y = eventY;
5515 if (mSwitchedLines) {
5516 // Offset the finger by the same vertical offset as the handles.
5517 // This improves visibility of the content being selected by
5518 // shifting the finger below the content, this is applied once
5519 // the user has switched lines.
5520 final int touchSlop = viewConfig.getScaledTouchSlop();
5521 final float fingerOffset = (mStartHandle != null)
5522 ? mStartHandle.getIdealVerticalOffset()
5523 : touchSlop;
5524 y = eventY - fingerOffset;
5525 }
5526
5527 currLine = getCurrentLineAdjustedForSlop(mTextView.getLayout(), mLineSelectionIsOn,
5528 y);
5529 if (!mSwitchedLines && currLine != mLineSelectionIsOn) {
5530 // Break early here, we want to offset the finger position from
5531 // the selection highlight, once the user moved their finger
5532 // to a different line we should apply the offset and *not* switch
5533 // lines until recomputing the position with the finger offset.
5534 mSwitchedLines = true;
5535 return;
5536 }
5537 }
5538
5539 int startOffset;
5540 int offset = mTextView.getOffsetAtCoordinate(currLine, eventX);
5541 // Snap to word boundaries.
5542 if (mStartOffset < offset) {
5543 // Expanding with end handle.
5544 offset = getWordEnd(offset);
5545 startOffset = getWordStart(mStartOffset);
5546 } else {
5547 // Expanding with start handle.
5548 offset = getWordStart(offset);
5549 startOffset = getWordEnd(mStartOffset);
Keisuke Kuroyanagi133dfc02016-07-21 18:07:23 +09005550 if (startOffset == offset) {
5551 offset = getNextCursorOffset(offset, false);
5552 }
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005553 }
5554 mLineSelectionIsOn = currLine;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005555 updateSelectionInternal(startOffset, offset,
5556 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005557 }
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005558
5559 private void updateParagraphBasedSelection(MotionEvent event) {
5560 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
5561
5562 final int start = Math.min(offset, mStartOffset);
5563 final int end = Math.max(offset, mStartOffset);
5564 final long paragraphsRange = getParagraphsRange(start, end);
5565 final int selectionStart = TextUtils.unpackRangeStartFromLong(paragraphsRange);
5566 final int selectionEnd = TextUtils.unpackRangeEndFromLong(paragraphsRange);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005567 updateSelectionInternal(selectionStart, selectionEnd,
5568 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
5569 }
5570
5571 private void updateSelectionInternal(int selectionStart, int selectionEnd,
5572 boolean fromTouchScreen) {
5573 final boolean performHapticFeedback = fromTouchScreen && mHapticTextHandleEnabled
5574 && ((mTextView.getSelectionStart() != selectionStart)
5575 || (mTextView.getSelectionEnd() != selectionEnd));
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005576 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005577 if (performHapticFeedback) {
5578 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
5579 }
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005580 }
5581
Gilles Debunned88876a2012-03-16 17:34:04 -07005582 /**
5583 * @param event
5584 */
5585 private void updateMinAndMaxOffsets(MotionEvent event) {
5586 int pointerCount = event.getPointerCount();
5587 for (int index = 0; index < pointerCount; index++) {
5588 int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
5589 if (offset < mMinTouchOffset) mMinTouchOffset = offset;
5590 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
5591 }
5592 }
5593
5594 public int getMinTouchOffset() {
5595 return mMinTouchOffset;
5596 }
5597
5598 public int getMaxTouchOffset() {
5599 return mMaxTouchOffset;
5600 }
5601
5602 public void resetTouchOffsets() {
5603 mMinTouchOffset = mMaxTouchOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005604 resetDragAcceleratorState();
5605 }
5606
5607 private void resetDragAcceleratorState() {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005608 mStartOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005609 mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
Mady Mellorf9f8aeb2015-06-17 09:46:01 -07005610 mSwitchedLines = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005611 final int selectionStart = mTextView.getSelectionStart();
5612 final int selectionEnd = mTextView.getSelectionEnd();
5613 if (selectionStart > selectionEnd) {
5614 Selection.setSelection((Spannable) mTextView.getText(),
5615 selectionEnd, selectionStart);
5616 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005617 }
5618
5619 /**
5620 * @return true iff this controller is currently used to move the selection start.
5621 */
5622 public boolean isSelectionStartDragged() {
5623 return mStartHandle != null && mStartHandle.isDragging();
5624 }
5625
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005626 @Override
5627 public boolean isCursorBeingModified() {
5628 return isDragAcceleratorActive() || isSelectionStartDragged()
5629 || (mEndHandle != null && mEndHandle.isDragging());
5630 }
5631
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005632 /**
5633 * @return true if the user is selecting text using the drag accelerator.
5634 */
5635 public boolean isDragAcceleratorActive() {
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005636 return mDragAcceleratorMode != DRAG_ACCELERATOR_MODE_INACTIVE;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005637 }
5638
Gilles Debunned88876a2012-03-16 17:34:04 -07005639 public void onTouchModeChanged(boolean isInTouchMode) {
5640 if (!isInTouchMode) {
5641 hide();
5642 }
5643 }
5644
5645 @Override
5646 public void onDetached() {
5647 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
5648 observer.removeOnTouchModeChangeListener(this);
5649
5650 if (mStartHandle != null) mStartHandle.onDetached();
5651 if (mEndHandle != null) mEndHandle.onDetached();
5652 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005653
5654 @Override
5655 public boolean isActive() {
5656 return mStartHandle != null && mStartHandle.isShowing();
5657 }
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09005658
5659 public void invalidateHandles() {
5660 if (mStartHandle != null) {
5661 mStartHandle.invalidate();
5662 }
5663 if (mEndHandle != null) {
5664 mEndHandle.invalidate();
5665 }
5666 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005667 }
5668
5669 private class CorrectionHighlighter {
5670 private final Path mPath = new Path();
Chris Craik6a49dde2015-05-12 10:28:14 -07005671 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
Gilles Debunned88876a2012-03-16 17:34:04 -07005672 private int mStart, mEnd;
5673 private long mFadingStartTime;
5674 private RectF mTempRectF;
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005675 private static final int FADE_OUT_DURATION = 400;
Gilles Debunned88876a2012-03-16 17:34:04 -07005676
5677 public CorrectionHighlighter() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005678 mPaint.setCompatibilityScaling(
5679 mTextView.getResources().getCompatibilityInfo().applicationScale);
Gilles Debunned88876a2012-03-16 17:34:04 -07005680 mPaint.setStyle(Paint.Style.FILL);
5681 }
5682
5683 public void highlight(CorrectionInfo info) {
5684 mStart = info.getOffset();
5685 mEnd = mStart + info.getNewText().length();
5686 mFadingStartTime = SystemClock.uptimeMillis();
5687
5688 if (mStart < 0 || mEnd < 0) {
5689 stopAnimation();
5690 }
5691 }
5692
5693 public void draw(Canvas canvas, int cursorOffsetVertical) {
5694 if (updatePath() && updatePaint()) {
5695 if (cursorOffsetVertical != 0) {
5696 canvas.translate(0, cursorOffsetVertical);
5697 }
5698
5699 canvas.drawPath(mPath, mPaint);
5700
5701 if (cursorOffsetVertical != 0) {
5702 canvas.translate(0, -cursorOffsetVertical);
5703 }
5704 invalidate(true); // TODO invalidate cursor region only
5705 } else {
5706 stopAnimation();
5707 invalidate(false); // TODO invalidate cursor region only
5708 }
5709 }
5710
5711 private boolean updatePaint() {
5712 final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
5713 if (duration > FADE_OUT_DURATION) return false;
5714
5715 final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
5716 final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005717 final int color = (mTextView.mHighlightColor & 0x00FFFFFF)
5718 + ((int) (highlightColorAlpha * coef) << 24);
Gilles Debunned88876a2012-03-16 17:34:04 -07005719 mPaint.setColor(color);
5720 return true;
5721 }
5722
5723 private boolean updatePath() {
5724 final Layout layout = mTextView.getLayout();
5725 if (layout == null) return false;
5726
5727 // Update in case text is edited while the animation is run
5728 final int length = mTextView.getText().length();
5729 int start = Math.min(length, mStart);
5730 int end = Math.min(length, mEnd);
5731
5732 mPath.reset();
5733 layout.getSelectionPath(start, end, mPath);
5734 return true;
5735 }
5736
5737 private void invalidate(boolean delayed) {
5738 if (mTextView.getLayout() == null) return;
5739
5740 if (mTempRectF == null) mTempRectF = new RectF();
5741 mPath.computeBounds(mTempRectF, false);
5742
5743 int left = mTextView.getCompoundPaddingLeft();
5744 int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
5745
5746 if (delayed) {
5747 mTextView.postInvalidateOnAnimation(
5748 left + (int) mTempRectF.left, top + (int) mTempRectF.top,
5749 left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
5750 } else {
5751 mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
5752 (int) mTempRectF.right, (int) mTempRectF.bottom);
5753 }
5754 }
5755
5756 private void stopAnimation() {
5757 Editor.this.mCorrectionHighlighter = null;
5758 }
5759 }
5760
5761 private static class ErrorPopup extends PopupWindow {
5762 private boolean mAbove = false;
5763 private final TextView mView;
5764 private int mPopupInlineErrorBackgroundId = 0;
5765 private int mPopupInlineErrorAboveBackgroundId = 0;
5766
5767 ErrorPopup(TextView v, int width, int height) {
5768 super(v, width, height);
5769 mView = v;
5770 // 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 -08005771 // shown and positioned. Initialized with below background, which should have
Gilles Debunned88876a2012-03-16 17:34:04 -07005772 // dimensions identical to the above version for this to work (and is more likely).
5773 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
5774 com.android.internal.R.styleable.Theme_errorMessageBackground);
5775 mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
5776 }
5777
5778 void fixDirection(boolean above) {
5779 mAbove = above;
5780
5781 if (above) {
5782 mPopupInlineErrorAboveBackgroundId =
5783 getResourceId(mPopupInlineErrorAboveBackgroundId,
5784 com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
5785 } else {
5786 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
5787 com.android.internal.R.styleable.Theme_errorMessageBackground);
5788 }
5789
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005790 mView.setBackgroundResource(
5791 above ? mPopupInlineErrorAboveBackgroundId : mPopupInlineErrorBackgroundId);
Gilles Debunned88876a2012-03-16 17:34:04 -07005792 }
5793
5794 private int getResourceId(int currentId, int index) {
5795 if (currentId == 0) {
5796 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
5797 R.styleable.Theme);
5798 currentId = styledAttributes.getResourceId(index, 0);
5799 styledAttributes.recycle();
5800 }
5801 return currentId;
5802 }
5803
5804 @Override
5805 public void update(int x, int y, int w, int h, boolean force) {
5806 super.update(x, y, w, h, force);
5807
5808 boolean above = isAboveAnchor();
5809 if (above != mAbove) {
5810 fixDirection(above);
5811 }
5812 }
5813 }
5814
5815 static class InputContentType {
5816 int imeOptions = EditorInfo.IME_NULL;
5817 String privateImeOptions;
5818 CharSequence imeActionLabel;
5819 int imeActionId;
5820 Bundle extras;
5821 OnEditorActionListener onEditorActionListener;
5822 boolean enterDown;
Yohei Yukawad469f212016-01-21 12:38:09 -08005823 LocaleList imeHintLocales;
Gilles Debunned88876a2012-03-16 17:34:04 -07005824 }
5825
5826 static class InputMethodState {
Gilles Debunnec62589c2012-04-12 14:50:23 -07005827 ExtractedTextRequest mExtractedTextRequest;
5828 final ExtractedText mExtractedText = new ExtractedText();
Gilles Debunned88876a2012-03-16 17:34:04 -07005829 int mBatchEditNesting;
5830 boolean mCursorChanged;
5831 boolean mSelectionModeChanged;
5832 boolean mContentChanged;
5833 int mChangedStart, mChangedEnd, mChangedDelta;
5834 }
Satoshi Kataoka0e3849a2012-12-13 14:37:19 +09005835
James Cookf59152c2015-02-26 18:03:58 -08005836 /**
James Cook471559f2015-02-27 10:31:20 -08005837 * @return True iff (start, end) is a valid range within the text.
5838 */
5839 private static boolean isValidRange(CharSequence text, int start, int end) {
5840 return 0 <= start && start <= end && end <= text.length();
5841 }
5842
Seigo Nonakaa60160b2015-08-19 12:38:35 -07005843 @VisibleForTesting
5844 public SuggestionsPopupWindow getSuggestionsPopupWindowForTesting() {
5845 return mSuggestionsPopupWindow;
5846 }
5847
James Cook471559f2015-02-27 10:31:20 -08005848 /**
James Cookf59152c2015-02-26 18:03:58 -08005849 * An InputFilter that monitors text input to maintain undo history. It does not modify the
5850 * text being typed (and hence always returns null from the filter() method).
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005851 *
5852 * TODO: Make this span aware.
James Cookf59152c2015-02-26 18:03:58 -08005853 */
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005854 public static class UndoInputFilter implements InputFilter {
James Cookf59152c2015-02-26 18:03:58 -08005855 private final Editor mEditor;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005856
James Cook48e0fac2015-02-25 15:44:51 -08005857 // Whether the current filter pass is directly caused by an end-user text edit.
5858 private boolean mIsUserEdit;
5859
James Cookd2026682015-03-03 14:40:14 -08005860 // Whether the text field is handling an IME composition. Must be parceled in case the user
5861 // rotates the screen during composition.
5862 private boolean mHasComposition;
James Cook48e0fac2015-02-25 15:44:51 -08005863
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005864 // Whether the user is expanding or shortening the text
5865 private boolean mExpanding;
5866
5867 // Whether the previous edit operation was in the current batch edit.
5868 private boolean mPreviousOperationWasInSameBatchEdit;
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08005869
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005870 public UndoInputFilter(Editor editor) {
5871 mEditor = editor;
5872 }
5873
James Cookd2026682015-03-03 14:40:14 -08005874 public void saveInstanceState(Parcel parcel) {
5875 parcel.writeInt(mIsUserEdit ? 1 : 0);
5876 parcel.writeInt(mHasComposition ? 1 : 0);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005877 parcel.writeInt(mExpanding ? 1 : 0);
5878 parcel.writeInt(mPreviousOperationWasInSameBatchEdit ? 1 : 0);
James Cookd2026682015-03-03 14:40:14 -08005879 }
5880
5881 public void restoreInstanceState(Parcel parcel) {
5882 mIsUserEdit = parcel.readInt() != 0;
5883 mHasComposition = parcel.readInt() != 0;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005884 mExpanding = parcel.readInt() != 0;
5885 mPreviousOperationWasInSameBatchEdit = parcel.readInt() != 0;
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08005886 }
5887
James Cook48e0fac2015-02-25 15:44:51 -08005888 /**
5889 * Signals that a user-triggered edit is starting.
5890 */
5891 public void beginBatchEdit() {
5892 if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit");
5893 mIsUserEdit = true;
James Cook48e0fac2015-02-25 15:44:51 -08005894 }
5895
5896 public void endBatchEdit() {
5897 if (DEBUG_UNDO) Log.d(TAG, "endBatchEdit");
5898 mIsUserEdit = false;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005899 mPreviousOperationWasInSameBatchEdit = false;
James Cook48e0fac2015-02-25 15:44:51 -08005900 }
5901
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005902 @Override
5903 public CharSequence filter(CharSequence source, int start, int end,
5904 Spanned dest, int dstart, int dend) {
5905 if (DEBUG_UNDO) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005906 Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ") "
5907 + "dest=" + dest + " (" + dstart + "-" + dend + ")");
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005908 }
James Cookf1dad1e2015-02-27 11:00:01 -08005909
James Cook48e0fac2015-02-25 15:44:51 -08005910 // Check to see if this edit should be tracked for undo.
5911 if (!canUndoEdit(source, start, end, dest, dstart, dend)) {
James Cookf1dad1e2015-02-27 11:00:01 -08005912 return null;
5913 }
5914
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005915 final boolean hadComposition = mHasComposition;
5916 mHasComposition = isComposition(source);
5917 final boolean wasExpanding = mExpanding;
5918 boolean shouldCreateSeparateState = false;
5919 if ((end - start) != (dend - dstart)) {
5920 mExpanding = (end - start) > (dend - dstart);
5921 if (hadComposition && mExpanding != wasExpanding) {
5922 shouldCreateSeparateState = true;
5923 }
James Cookd2026682015-03-03 14:40:14 -08005924 }
5925
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005926 // Handle edit.
5927 handleEdit(source, start, end, dest, dstart, dend, shouldCreateSeparateState);
James Cookd2026682015-03-03 14:40:14 -08005928 return null;
5929 }
5930
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09005931 void freezeLastEdit() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005932 mEditor.mUndoManager.beginUpdate("Edit text");
5933 EditOperation lastEdit = getLastEdit();
5934 if (lastEdit != null) {
5935 lastEdit.mFrozen = true;
James Cookd2026682015-03-03 14:40:14 -08005936 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005937 mEditor.mUndoManager.endUpdate();
James Cookd2026682015-03-03 14:40:14 -08005938 }
5939
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005940 @Retention(RetentionPolicy.SOURCE)
5941 @IntDef({MERGE_EDIT_MODE_FORCE_MERGE, MERGE_EDIT_MODE_NEVER_MERGE, MERGE_EDIT_MODE_NORMAL})
5942 private @interface MergeMode {}
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005943 private static final int MERGE_EDIT_MODE_FORCE_MERGE = 0;
5944 private static final int MERGE_EDIT_MODE_NEVER_MERGE = 1;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005945 /** Use {@link EditOperation#mergeWith} to merge */
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005946 private static final int MERGE_EDIT_MODE_NORMAL = 2;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005947
5948 private void handleEdit(CharSequence source, int start, int end,
5949 Spanned dest, int dstart, int dend, boolean shouldCreateSeparateState) {
James Cook48e0fac2015-02-25 15:44:51 -08005950 // An application may install a TextWatcher to provide additional modifications after
5951 // the initial input filters run (e.g. a credit card formatter that adds spaces to a
5952 // string). This results in multiple filter() calls for what the user considers to be
5953 // a single operation. Always undo the whole set of changes in one step.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005954 @MergeMode
5955 final int mergeMode;
5956 if (isInTextWatcher() || mPreviousOperationWasInSameBatchEdit) {
5957 mergeMode = MERGE_EDIT_MODE_FORCE_MERGE;
5958 } else if (shouldCreateSeparateState) {
5959 mergeMode = MERGE_EDIT_MODE_NEVER_MERGE;
5960 } else {
5961 mergeMode = MERGE_EDIT_MODE_NORMAL;
5962 }
James Cook471559f2015-02-27 10:31:20 -08005963 // Build a new operation with all the information from this edit.
James Cookd2026682015-03-03 14:40:14 -08005964 String newText = TextUtils.substring(source, start, end);
5965 String oldText = TextUtils.substring(dest, dstart, dend);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005966 EditOperation edit = new EditOperation(mEditor, oldText, dstart, newText,
5967 mHasComposition);
5968 if (mHasComposition && TextUtils.equals(edit.mNewText, edit.mOldText)) {
5969 return;
5970 }
5971 recordEdit(edit, mergeMode);
James Cookd2026682015-03-03 14:40:14 -08005972 }
James Cook471559f2015-02-27 10:31:20 -08005973
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005974 private EditOperation getLastEdit() {
5975 final UndoManager um = mEditor.mUndoManager;
5976 return um.getLastOperation(
5977 EditOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE);
5978 }
James Cook22054252015-03-25 14:04:01 -07005979 /**
5980 * Fetches the last undo operation and checks to see if a new edit should be merged into it.
5981 * If forceMerge is true then the new edit is always merged.
5982 */
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005983 private void recordEdit(EditOperation edit, @MergeMode int mergeMode) {
James Cook471559f2015-02-27 10:31:20 -08005984 // Fetch the last edit operation and attempt to merge in the new edit.
James Cook48e0fac2015-02-25 15:44:51 -08005985 final UndoManager um = mEditor.mUndoManager;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005986 um.beginUpdate("Edit text");
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005987 EditOperation lastEdit = getLastEdit();
James Cook471559f2015-02-27 10:31:20 -08005988 if (lastEdit == null) {
5989 // Add this as the first edit.
5990 if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit);
5991 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005992 } else if (mergeMode == MERGE_EDIT_MODE_FORCE_MERGE) {
James Cook22054252015-03-25 14:04:01 -07005993 // Forced merges take priority because they could be the result of a non-user-edit
5994 // change and this case should not create a new undo operation.
5995 if (DEBUG_UNDO) Log.d(TAG, "filter: force merge " + edit);
5996 lastEdit.forceMergeWith(edit);
James Cook48e0fac2015-02-25 15:44:51 -08005997 } else if (!mIsUserEdit) {
5998 // An application directly modified the Editable outside of a text edit. Treat this
5999 // as a new change and don't attempt to merge.
6000 if (DEBUG_UNDO) Log.d(TAG, "non-user edit, new op " + edit);
6001 um.commitState(mEditor.mUndoOwner);
6002 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006003 } else if (mergeMode == MERGE_EDIT_MODE_NORMAL && lastEdit.mergeWith(edit)) {
James Cook471559f2015-02-27 10:31:20 -08006004 // Merge succeeded, nothing else to do.
6005 if (DEBUG_UNDO) Log.d(TAG, "filter: merge succeeded, created " + lastEdit);
James Cook3ac0bcb2015-02-26 10:53:41 -08006006 } else {
James Cook471559f2015-02-27 10:31:20 -08006007 // Could not merge with the last edit, so commit the last edit and add this edit.
6008 if (DEBUG_UNDO) Log.d(TAG, "filter: merge failed, adding " + edit);
6009 um.commitState(mEditor.mUndoOwner);
6010 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
James Cook3ac0bcb2015-02-26 10:53:41 -08006011 }
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09006012 mPreviousOperationWasInSameBatchEdit = mIsUserEdit;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006013 um.endUpdate();
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006014 }
James Cook48e0fac2015-02-25 15:44:51 -08006015
6016 private boolean canUndoEdit(CharSequence source, int start, int end,
6017 Spanned dest, int dstart, int dend) {
6018 if (!mEditor.mAllowUndo) {
6019 if (DEBUG_UNDO) Log.d(TAG, "filter: undo is disabled");
6020 return false;
6021 }
6022
6023 if (mEditor.mUndoManager.isInUndo()) {
6024 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping, currently performing undo/redo");
6025 return false;
6026 }
6027
6028 // Text filters run before input operations are applied. However, some input operations
6029 // are invalid and will throw exceptions when applied. This is common in tests. Don't
6030 // attempt to undo invalid operations.
6031 if (!isValidRange(source, start, end) || !isValidRange(dest, dstart, dend)) {
6032 if (DEBUG_UNDO) Log.d(TAG, "filter: invalid op");
6033 return false;
6034 }
6035
6036 // Earlier filters can rewrite input to be a no-op, for example due to a length limit
6037 // on an input field. Skip no-op changes.
6038 if (start == end && dstart == dend) {
6039 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping no-op");
6040 return false;
6041 }
6042
6043 return true;
6044 }
James Cookd2026682015-03-03 14:40:14 -08006045
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006046 private static boolean isComposition(CharSequence source) {
James Cookd2026682015-03-03 14:40:14 -08006047 if (!(source instanceof Spannable)) {
6048 return false;
6049 }
6050 // This is a composition edit if the source has a non-zero-length composing span.
6051 Spannable text = (Spannable) source;
6052 int composeBegin = EditableInputConnection.getComposingSpanStart(text);
6053 int composeEnd = EditableInputConnection.getComposingSpanEnd(text);
6054 return composeBegin < composeEnd;
6055 }
6056
6057 private boolean isInTextWatcher() {
6058 CharSequence text = mEditor.mTextView.getText();
6059 return (text instanceof SpannableStringBuilder)
6060 && ((SpannableStringBuilder) text).getTextWatcherDepth() > 0;
6061 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006062 }
6063
James Cookf59152c2015-02-26 18:03:58 -08006064 /**
6065 * An operation to undo a single "edit" to a text view.
6066 */
James Cook471559f2015-02-27 10:31:20 -08006067 public static class EditOperation extends UndoOperation<Editor> {
6068 private static final int TYPE_INSERT = 0;
6069 private static final int TYPE_DELETE = 1;
6070 private static final int TYPE_REPLACE = 2;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006071
James Cook471559f2015-02-27 10:31:20 -08006072 private int mType;
6073 private String mOldText;
James Cook471559f2015-02-27 10:31:20 -08006074 private String mNewText;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006075 private int mStart;
James Cook471559f2015-02-27 10:31:20 -08006076
6077 private int mOldCursorPos;
6078 private int mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006079 private boolean mFrozen;
6080 private boolean mIsComposition;
James Cook471559f2015-02-27 10:31:20 -08006081
6082 /**
James Cookd2026682015-03-03 14:40:14 -08006083 * Constructs an edit operation from a text input operation on editor that replaces the
James Cook22054252015-03-25 14:04:01 -07006084 * oldText starting at dstart with newText.
James Cook471559f2015-02-27 10:31:20 -08006085 */
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006086 public EditOperation(Editor editor, String oldText, int dstart, String newText,
6087 boolean isComposition) {
James Cook471559f2015-02-27 10:31:20 -08006088 super(editor.mUndoOwner);
James Cookd2026682015-03-03 14:40:14 -08006089 mOldText = oldText;
6090 mNewText = newText;
James Cook471559f2015-02-27 10:31:20 -08006091
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006092 // Determine the type of the edit.
James Cook471559f2015-02-27 10:31:20 -08006093 if (mNewText.length() > 0 && mOldText.length() == 0) {
6094 mType = TYPE_INSERT;
James Cook471559f2015-02-27 10:31:20 -08006095 } else if (mNewText.length() == 0 && mOldText.length() > 0) {
6096 mType = TYPE_DELETE;
James Cook471559f2015-02-27 10:31:20 -08006097 } else {
6098 mType = TYPE_REPLACE;
James Cook471559f2015-02-27 10:31:20 -08006099 }
6100
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006101 mStart = dstart;
James Cook471559f2015-02-27 10:31:20 -08006102 // Store cursor data.
6103 mOldCursorPos = editor.mTextView.getSelectionStart();
James Cookd2026682015-03-03 14:40:14 -08006104 mNewCursorPos = dstart + mNewText.length();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006105 mIsComposition = isComposition;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006106 }
6107
James Cook471559f2015-02-27 10:31:20 -08006108 public EditOperation(Parcel src, ClassLoader loader) {
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006109 super(src, loader);
James Cook471559f2015-02-27 10:31:20 -08006110 mType = src.readInt();
6111 mOldText = src.readString();
James Cook471559f2015-02-27 10:31:20 -08006112 mNewText = src.readString();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006113 mStart = src.readInt();
James Cook471559f2015-02-27 10:31:20 -08006114 mOldCursorPos = src.readInt();
6115 mNewCursorPos = src.readInt();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006116 mFrozen = src.readInt() == 1;
6117 mIsComposition = src.readInt() == 1;
James Cook471559f2015-02-27 10:31:20 -08006118 }
6119
6120 @Override
6121 public void writeToParcel(Parcel dest, int flags) {
6122 dest.writeInt(mType);
6123 dest.writeString(mOldText);
James Cook471559f2015-02-27 10:31:20 -08006124 dest.writeString(mNewText);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006125 dest.writeInt(mStart);
James Cook471559f2015-02-27 10:31:20 -08006126 dest.writeInt(mOldCursorPos);
6127 dest.writeInt(mNewCursorPos);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006128 dest.writeInt(mFrozen ? 1 : 0);
6129 dest.writeInt(mIsComposition ? 1 : 0);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006130 }
6131
James Cook48e0fac2015-02-25 15:44:51 -08006132 private int getNewTextEnd() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006133 return mStart + mNewText.length();
James Cook48e0fac2015-02-25 15:44:51 -08006134 }
6135
6136 private int getOldTextEnd() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006137 return mStart + mOldText.length();
James Cook48e0fac2015-02-25 15:44:51 -08006138 }
6139
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006140 @Override
6141 public void commit() {
6142 }
6143
6144 @Override
6145 public void undo() {
James Cook471559f2015-02-27 10:31:20 -08006146 if (DEBUG_UNDO) Log.d(TAG, "undo");
6147 // Remove the new text and insert the old.
James Cook48e0fac2015-02-25 15:44:51 -08006148 Editor editor = getOwnerData();
6149 Editable text = (Editable) editor.mTextView.getText();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006150 modifyText(text, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006151 }
6152
6153 @Override
6154 public void redo() {
James Cook471559f2015-02-27 10:31:20 -08006155 if (DEBUG_UNDO) Log.d(TAG, "redo");
6156 // Remove the old text and insert the new.
James Cook48e0fac2015-02-25 15:44:51 -08006157 Editor editor = getOwnerData();
6158 Editable text = (Editable) editor.mTextView.getText();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006159 modifyText(text, mStart, getOldTextEnd(), mNewText, mStart, mNewCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006160 }
6161
James Cook471559f2015-02-27 10:31:20 -08006162 /**
6163 * Attempts to merge this existing operation with a new edit.
6164 * @param edit The new edit operation.
6165 * @return If the merge succeeded, returns true. Otherwise returns false and leaves this
6166 * object unchanged.
6167 */
6168 private boolean mergeWith(EditOperation edit) {
James Cook48e0fac2015-02-25 15:44:51 -08006169 if (DEBUG_UNDO) {
6170 Log.d(TAG, "mergeWith old " + this);
6171 Log.d(TAG, "mergeWith new " + edit);
6172 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006173
6174 if (mFrozen) {
6175 return false;
6176 }
6177
James Cook471559f2015-02-27 10:31:20 -08006178 switch (mType) {
6179 case TYPE_INSERT:
6180 return mergeInsertWith(edit);
6181 case TYPE_DELETE:
6182 return mergeDeleteWith(edit);
6183 case TYPE_REPLACE:
6184 return mergeReplaceWith(edit);
6185 default:
6186 return false;
6187 }
6188 }
6189
6190 private boolean mergeInsertWith(EditOperation edit) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006191 if (edit.mType == TYPE_INSERT) {
6192 // Merge insertions that are contiguous even when it's frozen.
6193 if (getNewTextEnd() != edit.mStart) {
6194 return false;
6195 }
6196 mNewText += edit.mNewText;
6197 mNewCursorPos = edit.mNewCursorPos;
6198 mFrozen = edit.mFrozen;
6199 mIsComposition = edit.mIsComposition;
6200 return true;
James Cook471559f2015-02-27 10:31:20 -08006201 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006202 if (mIsComposition && edit.mType == TYPE_REPLACE
6203 && mStart <= edit.mStart && getNewTextEnd() >= edit.getOldTextEnd()) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006204 // Merge insertion with replace as they can be single insertion.
6205 mNewText = mNewText.substring(0, edit.mStart - mStart) + edit.mNewText
6206 + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
6207 mNewCursorPos = edit.mNewCursorPos;
6208 mIsComposition = edit.mIsComposition;
6209 return true;
James Cook471559f2015-02-27 10:31:20 -08006210 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006211 return false;
James Cook471559f2015-02-27 10:31:20 -08006212 }
6213
6214 // TODO: Support forward delete.
6215 private boolean mergeDeleteWith(EditOperation edit) {
James Cook471559f2015-02-27 10:31:20 -08006216 // Only merge continuous deletes.
6217 if (edit.mType != TYPE_DELETE) {
6218 return false;
6219 }
6220 // Only merge deletions that are contiguous.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006221 if (mStart != edit.getOldTextEnd()) {
James Cook471559f2015-02-27 10:31:20 -08006222 return false;
6223 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006224 mStart = edit.mStart;
James Cook471559f2015-02-27 10:31:20 -08006225 mOldText = edit.mOldText + mOldText;
6226 mNewCursorPos = edit.mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006227 mIsComposition = edit.mIsComposition;
James Cook471559f2015-02-27 10:31:20 -08006228 return true;
6229 }
6230
6231 private boolean mergeReplaceWith(EditOperation edit) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006232 if (edit.mType == TYPE_INSERT && getNewTextEnd() == edit.mStart) {
6233 // Merge with adjacent insert.
6234 mNewText += edit.mNewText;
6235 mNewCursorPos = edit.mNewCursorPos;
6236 return true;
6237 }
6238 if (!mIsComposition) {
James Cook471559f2015-02-27 10:31:20 -08006239 return false;
6240 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006241 if (edit.mType == TYPE_DELETE && mStart <= edit.mStart
6242 && getNewTextEnd() >= edit.getOldTextEnd()) {
6243 // Merge with delete as they can be single operation.
6244 mNewText = mNewText.substring(0, edit.mStart - mStart)
6245 + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
6246 if (mNewText.isEmpty()) {
6247 mType = TYPE_DELETE;
6248 }
6249 mNewCursorPos = edit.mNewCursorPos;
6250 mIsComposition = edit.mIsComposition;
6251 return true;
6252 }
6253 if (edit.mType == TYPE_REPLACE && mStart == edit.mStart
6254 && TextUtils.equals(mNewText, edit.mOldText)) {
6255 // Merge with the replace that replaces the same region.
6256 mNewText = edit.mNewText;
6257 mNewCursorPos = edit.mNewCursorPos;
6258 mIsComposition = edit.mIsComposition;
6259 return true;
6260 }
6261 return false;
James Cook471559f2015-02-27 10:31:20 -08006262 }
6263
James Cook48e0fac2015-02-25 15:44:51 -08006264 /**
6265 * Forcibly creates a single merged edit operation by simulating the entire text
6266 * contents being replaced.
6267 */
James Cook22054252015-03-25 14:04:01 -07006268 public void forceMergeWith(EditOperation edit) {
James Cook48e0fac2015-02-25 15:44:51 -08006269 if (DEBUG_UNDO) Log.d(TAG, "forceMerge");
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006270 if (mergeWith(edit)) {
6271 return;
6272 }
James Cookf59152c2015-02-26 18:03:58 -08006273 Editor editor = getOwnerData();
James Cook48e0fac2015-02-25 15:44:51 -08006274
6275 // Copy the text of the current field.
6276 // NOTE: Using StringBuilder instead of SpannableStringBuilder would be somewhat faster,
6277 // but would require two parallel implementations of modifyText() because Editable and
6278 // StringBuilder do not share an interface for replace/delete/insert.
6279 Editable editable = (Editable) editor.mTextView.getText();
6280 Editable originalText = new SpannableStringBuilder(editable.toString());
6281
6282 // Roll back the last operation.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006283 modifyText(originalText, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
James Cook48e0fac2015-02-25 15:44:51 -08006284
6285 // Clone the text again and apply the new operation.
6286 Editable finalText = new SpannableStringBuilder(editable.toString());
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006287 modifyText(finalText, edit.mStart, edit.getOldTextEnd(),
6288 edit.mNewText, edit.mStart, edit.mNewCursorPos);
James Cook48e0fac2015-02-25 15:44:51 -08006289
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006290 // Convert this operation into a replace operation.
James Cook48e0fac2015-02-25 15:44:51 -08006291 mType = TYPE_REPLACE;
6292 mNewText = finalText.toString();
James Cook48e0fac2015-02-25 15:44:51 -08006293 mOldText = originalText.toString();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006294 mStart = 0;
James Cook48e0fac2015-02-25 15:44:51 -08006295 mNewCursorPos = edit.mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006296 mIsComposition = edit.mIsComposition;
James Cook48e0fac2015-02-25 15:44:51 -08006297 // mOldCursorPos is unchanged.
6298 }
6299
6300 private static void modifyText(Editable text, int deleteFrom, int deleteTo,
6301 CharSequence newText, int newTextInsertAt, int newCursorPos) {
James Cook471559f2015-02-27 10:31:20 -08006302 // Apply the edit if it is still valid.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006303 if (isValidRange(text, deleteFrom, deleteTo)
6304 && newTextInsertAt <= text.length() - (deleteTo - deleteFrom)) {
James Cook471559f2015-02-27 10:31:20 -08006305 if (deleteFrom != deleteTo) {
6306 text.delete(deleteFrom, deleteTo);
6307 }
6308 if (newText.length() != 0) {
6309 text.insert(newTextInsertAt, newText);
6310 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006311 }
James Cook900185d2015-03-10 09:48:11 -07006312 // Restore the cursor position. If there wasn't an old cursor (newCursorPos == -1) then
6313 // don't explicitly set it and rely on SpannableStringBuilder to position it.
James Cook471559f2015-02-27 10:31:20 -08006314 // TODO: Select all the text that was undone.
James Cook900185d2015-03-10 09:48:11 -07006315 if (0 <= newCursorPos && newCursorPos <= text.length()) {
James Cook471559f2015-02-27 10:31:20 -08006316 Selection.setSelection(text, newCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006317 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006318 }
6319
James Cook48e0fac2015-02-25 15:44:51 -08006320 private String getTypeString() {
6321 switch (mType) {
6322 case TYPE_INSERT:
6323 return "insert";
6324 case TYPE_DELETE:
6325 return "delete";
6326 case TYPE_REPLACE:
6327 return "replace";
6328 default:
6329 return "";
6330 }
6331 }
6332
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006333 @Override
James Cook471559f2015-02-27 10:31:20 -08006334 public String toString() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006335 return "[mType=" + getTypeString() + ", "
6336 + "mOldText=" + mOldText + ", "
6337 + "mNewText=" + mNewText + ", "
6338 + "mStart=" + mStart + ", "
6339 + "mOldCursorPos=" + mOldCursorPos + ", "
6340 + "mNewCursorPos=" + mNewCursorPos + ", "
6341 + "mFrozen=" + mFrozen + ", "
6342 + "mIsComposition=" + mIsComposition + "]";
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006343 }
6344
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006345 public static final Parcelable.ClassLoaderCreator<EditOperation> CREATOR =
6346 new Parcelable.ClassLoaderCreator<EditOperation>() {
James Cookf59152c2015-02-26 18:03:58 -08006347 @Override
James Cook471559f2015-02-27 10:31:20 -08006348 public EditOperation createFromParcel(Parcel in) {
6349 return new EditOperation(in, null);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006350 }
6351
James Cookf59152c2015-02-26 18:03:58 -08006352 @Override
James Cook471559f2015-02-27 10:31:20 -08006353 public EditOperation createFromParcel(Parcel in, ClassLoader loader) {
6354 return new EditOperation(in, loader);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006355 }
6356
James Cookf59152c2015-02-26 18:03:58 -08006357 @Override
James Cook471559f2015-02-27 10:31:20 -08006358 public EditOperation[] newArray(int size) {
6359 return new EditOperation[size];
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006360 }
6361 };
6362 }
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006363
6364 /**
6365 * A helper for enabling and handling "PROCESS_TEXT" menu actions.
6366 * These allow external applications to plug into currently selected text.
6367 */
6368 static final class ProcessTextIntentActionsHandler {
6369
6370 private final Editor mEditor;
6371 private final TextView mTextView;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006372 private final Context mContext;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006373 private final PackageManager mPackageManager;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006374 private final String mPackageName;
6375 private final SparseArray<Intent> mAccessibilityIntents = new SparseArray<>();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006376 private final SparseArray<AccessibilityNodeInfo.AccessibilityAction> mAccessibilityActions =
6377 new SparseArray<>();
6378 private final List<ResolveInfo> mSupportedActivities = new ArrayList<>();
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006379
6380 private ProcessTextIntentActionsHandler(Editor editor) {
6381 mEditor = Preconditions.checkNotNull(editor);
6382 mTextView = Preconditions.checkNotNull(mEditor.mTextView);
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006383 mContext = Preconditions.checkNotNull(mTextView.getContext());
6384 mPackageManager = Preconditions.checkNotNull(mContext.getPackageManager());
6385 mPackageName = Preconditions.checkNotNull(mContext.getPackageName());
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006386 }
6387
6388 /**
6389 * Adds "PROCESS_TEXT" menu items to the specified menu.
6390 */
6391 public void onInitializeMenu(Menu menu) {
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +01006392 final int size = mSupportedActivities.size();
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006393 loadSupportedActivities();
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +01006394 for (int i = 0; i < size; i++) {
6395 final ResolveInfo resolveInfo = mSupportedActivities.get(i);
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006396 menu.add(Menu.NONE, Menu.NONE,
6397 Editor.MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i++,
6398 getLabel(resolveInfo))
6399 .setIntent(createProcessTextIntentForResolveInfo(resolveInfo))
6400 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
6401 }
6402 }
6403
6404 /**
6405 * Performs a "PROCESS_TEXT" action if there is one associated with the specified
6406 * menu item.
6407 *
6408 * @return True if the action was performed, false otherwise.
6409 */
6410 public boolean performMenuItemAction(MenuItem item) {
6411 return fireIntent(item.getIntent());
6412 }
6413
6414 /**
6415 * Initializes and caches "PROCESS_TEXT" accessibility actions.
6416 */
6417 public void initializeAccessibilityActions() {
6418 mAccessibilityIntents.clear();
6419 mAccessibilityActions.clear();
6420 int i = 0;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006421 loadSupportedActivities();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006422 for (ResolveInfo resolveInfo : mSupportedActivities) {
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006423 int actionId = TextView.ACCESSIBILITY_ACTION_PROCESS_TEXT_START_ID + i++;
6424 mAccessibilityActions.put(
6425 actionId,
6426 new AccessibilityNodeInfo.AccessibilityAction(
6427 actionId, getLabel(resolveInfo)));
6428 mAccessibilityIntents.put(
6429 actionId, createProcessTextIntentForResolveInfo(resolveInfo));
6430 }
6431 }
6432
6433 /**
6434 * Adds "PROCESS_TEXT" accessibility actions to the specified accessibility node info.
6435 * NOTE: This needs a prior call to {@link #initializeAccessibilityActions()} to make the
6436 * latest accessibility actions available for this call.
6437 */
6438 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) {
6439 for (int i = 0; i < mAccessibilityActions.size(); i++) {
6440 nodeInfo.addAction(mAccessibilityActions.valueAt(i));
6441 }
6442 }
6443
6444 /**
6445 * Performs a "PROCESS_TEXT" action if there is one associated with the specified
6446 * accessibility action id.
6447 *
6448 * @return True if the action was performed, false otherwise.
6449 */
6450 public boolean performAccessibilityAction(int actionId) {
6451 return fireIntent(mAccessibilityIntents.get(actionId));
6452 }
6453
6454 private boolean fireIntent(Intent intent) {
6455 if (intent != null && Intent.ACTION_PROCESS_TEXT.equals(intent.getAction())) {
Siyamed Sinirce3b05a2017-07-18 18:54:31 -07006456 String selectedText = mTextView.getSelectedText();
6457 selectedText = TextUtils.trimToParcelableSize(selectedText);
6458 intent.putExtra(Intent.EXTRA_PROCESS_TEXT, selectedText);
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08006459 mEditor.mPreserveSelection = true;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006460 mTextView.startActivityForResult(intent, TextView.PROCESS_TEXT_REQUEST_CODE);
6461 return true;
6462 }
6463 return false;
6464 }
6465
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006466 private void loadSupportedActivities() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006467 mSupportedActivities.clear();
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006468 PackageManager packageManager = mTextView.getContext().getPackageManager();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006469 List<ResolveInfo> unfiltered =
6470 packageManager.queryIntentActivities(createProcessTextIntent(), 0);
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006471 for (ResolveInfo info : unfiltered) {
6472 if (isSupportedActivity(info)) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006473 mSupportedActivities.add(info);
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006474 }
6475 }
6476 }
6477
6478 private boolean isSupportedActivity(ResolveInfo info) {
6479 return mPackageName.equals(info.activityInfo.packageName)
6480 || info.activityInfo.exported
6481 && (info.activityInfo.permission == null
6482 || mContext.checkSelfPermission(info.activityInfo.permission)
6483 == PackageManager.PERMISSION_GRANTED);
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006484 }
6485
6486 private Intent createProcessTextIntentForResolveInfo(ResolveInfo info) {
6487 return createProcessTextIntent()
6488 .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !mTextView.isTextEditable())
6489 .setClassName(info.activityInfo.packageName, info.activityInfo.name);
6490 }
6491
6492 private Intent createProcessTextIntent() {
6493 return new Intent()
6494 .setAction(Intent.ACTION_PROCESS_TEXT)
6495 .setType("text/plain");
6496 }
6497
6498 private CharSequence getLabel(ResolveInfo resolveInfo) {
6499 return resolveInfo.loadLabel(mPackageManager);
6500 }
6501 }
Gilles Debunned88876a2012-03-16 17:34:04 -07006502}