blob: 04a826514a83308c64a2412a3203218239e9a6d5 [file] [log] [blame]
Gilles Debunned88876a2012-03-16 17:34:04 -07001/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.widget;
18
19import android.R;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +090020import android.annotation.IntDef;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +090021import android.annotation.NonNull;
Yoshiki Iguchiee147722015-04-14 00:12:44 +090022import android.annotation.Nullable;
Luca Zanolin1b15ba52013-02-20 14:31:37 +000023import android.app.PendingIntent;
24import android.app.PendingIntent.CanceledException;
Gilles Debunned88876a2012-03-16 17:34:04 -070025import android.content.ClipData;
26import android.content.ClipData.Item;
27import android.content.Context;
28import android.content.Intent;
Raph Levien26d443a2015-03-30 14:18:32 -070029import android.content.UndoManager;
30import android.content.UndoOperation;
31import android.content.UndoOwner;
Gilles Debunned88876a2012-03-16 17:34:04 -070032import android.content.pm.PackageManager;
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +000033import android.content.pm.ResolveInfo;
Gilles Debunned88876a2012-03-16 17:34:04 -070034import android.content.res.TypedArray;
35import android.graphics.Canvas;
36import android.graphics.Color;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +090037import android.graphics.Matrix;
Gilles Debunned88876a2012-03-16 17:34:04 -070038import android.graphics.Paint;
39import android.graphics.Path;
40import android.graphics.Rect;
41import android.graphics.RectF;
Seigo Nonaka3ed1b392016-01-19 13:54:59 +090042import android.graphics.drawable.ColorDrawable;
Gilles Debunned88876a2012-03-16 17:34:04 -070043import android.graphics.drawable.Drawable;
Abodunrinwa Toki54486c12017-04-19 21:02:36 +010044import android.metrics.LogMaker;
Gilles Debunned88876a2012-03-16 17:34:04 -070045import android.os.Bundle;
Yohei Yukawa23cbe852016-05-17 16:42:58 -070046import android.os.LocaleList;
Raph Levien26d443a2015-03-30 14:18:32 -070047import android.os.Parcel;
48import android.os.Parcelable;
James Cookf59152c2015-02-26 18:03:58 -080049import android.os.ParcelableParcel;
Gilles Debunned88876a2012-03-16 17:34:04 -070050import android.os.SystemClock;
51import android.provider.Settings;
52import android.text.DynamicLayout;
53import android.text.Editable;
Raph Levien26d443a2015-03-30 14:18:32 -070054import android.text.InputFilter;
Gilles Debunned88876a2012-03-16 17:34:04 -070055import android.text.InputType;
56import android.text.Layout;
57import android.text.ParcelableSpan;
58import android.text.Selection;
59import android.text.SpanWatcher;
60import android.text.Spannable;
61import android.text.SpannableStringBuilder;
62import android.text.Spanned;
63import android.text.StaticLayout;
64import android.text.TextUtils;
Gilles Debunned88876a2012-03-16 17:34:04 -070065import android.text.method.KeyListener;
66import android.text.method.MetaKeyKeyListener;
67import android.text.method.MovementMethod;
Gilles Debunned88876a2012-03-16 17:34:04 -070068import android.text.method.WordIterator;
69import android.text.style.EasyEditSpan;
70import android.text.style.SuggestionRangeSpan;
71import android.text.style.SuggestionSpan;
72import android.text.style.TextAppearanceSpan;
73import android.text.style.URLSpan;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +090074import android.util.ArraySet;
Gilles Debunned88876a2012-03-16 17:34:04 -070075import android.util.DisplayMetrics;
76import android.util.Log;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -070077import android.util.SparseArray;
Gilles Debunned88876a2012-03-16 17:34:04 -070078import android.view.ActionMode;
79import android.view.ActionMode.Callback;
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +090080import android.view.ContextMenu;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +090081import android.view.ContextThemeWrapper;
Chris Craikf6829a02015-03-10 10:28:59 -070082import android.view.DisplayListCanvas;
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -070083import android.view.DragAndDropPermissions;
Gilles Debunned88876a2012-03-16 17:34:04 -070084import android.view.DragEvent;
85import android.view.Gravity;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -070086import android.view.HapticFeedbackConstants;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -080087import android.view.InputDevice;
Gilles Debunned88876a2012-03-16 17:34:04 -070088import android.view.LayoutInflater;
89import android.view.Menu;
90import android.view.MenuItem;
91import android.view.MotionEvent;
Chris Craikf6829a02015-03-10 10:28:59 -070092import android.view.RenderNode;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +090093import android.view.SubMenu;
Gilles Debunned88876a2012-03-16 17:34:04 -070094import android.view.View;
Gilles Debunned88876a2012-03-16 17:34:04 -070095import android.view.View.DragShadowBuilder;
96import android.view.View.OnClickListener;
Adam Powell057a5852012-05-11 10:28:38 -070097import android.view.ViewConfiguration;
98import android.view.ViewGroup;
Gilles Debunned88876a2012-03-16 17:34:04 -070099import android.view.ViewGroup.LayoutParams;
Gilles Debunned88876a2012-03-16 17:34:04 -0700100import android.view.ViewTreeObserver;
101import android.view.WindowManager;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700102import android.view.accessibility.AccessibilityNodeInfo;
Gilles Debunned88876a2012-03-16 17:34:04 -0700103import android.view.inputmethod.CorrectionInfo;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900104import android.view.inputmethod.CursorAnchorInfo;
Gilles Debunned88876a2012-03-16 17:34:04 -0700105import android.view.inputmethod.EditorInfo;
106import android.view.inputmethod.ExtractedText;
107import android.view.inputmethod.ExtractedTextRequest;
108import android.view.inputmethod.InputConnection;
109import android.view.inputmethod.InputMethodManager;
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100110import android.view.textclassifier.TextClassification;
Gilles Debunned88876a2012-03-16 17:34:04 -0700111import android.widget.AdapterView.OnItemClickListener;
112import android.widget.TextView.Drawables;
113import android.widget.TextView.OnEditorActionListener;
114
Seigo Nonakaa60160b2015-08-19 12:38:35 -0700115import com.android.internal.annotations.VisibleForTesting;
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +0000116import com.android.internal.logging.MetricsLogger;
117import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
Raph Levien26d443a2015-03-30 14:18:32 -0700118import com.android.internal.util.ArrayUtils;
119import com.android.internal.util.GrowingArrayUtils;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700120import com.android.internal.util.Preconditions;
Raph Levien26d443a2015-03-30 14:18:32 -0700121import com.android.internal.widget.EditableInputConnection;
122
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +0900123import java.lang.annotation.Retention;
124import java.lang.annotation.RetentionPolicy;
Andrei Stingaceanu2aaeefe2015-10-20 19:11:23 +0100125import java.text.BreakIterator;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +0100126import java.util.ArrayList;
Andrei Stingaceanu2aaeefe2015-10-20 19:11:23 +0100127import java.util.Arrays;
128import java.util.Comparator;
129import java.util.HashMap;
130import java.util.List;
131
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700132
Gilles Debunned88876a2012-03-16 17:34:04 -0700133/**
134 * Helper class used by TextView to handle editable text views.
135 *
136 * @hide
137 */
138public class Editor {
Adam Powell057a5852012-05-11 10:28:38 -0700139 private static final String TAG = "Editor";
James Cookf59152c2015-02-26 18:03:58 -0800140 private static final boolean DEBUG_UNDO = false;
Adam Powell057a5852012-05-11 10:28:38 -0700141
Gilles Debunned88876a2012-03-16 17:34:04 -0700142 static final int BLINK = 500;
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700143 private static final int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
Mady Mellorcc65c372015-06-17 09:25:19 -0700144 private static final float LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS = 0.5f;
Mady Mellore264ac32015-06-22 16:46:29 -0700145 private static final int UNSET_X_VALUE = -1;
Mady Mellora6a0f782015-07-10 16:43:32 -0700146 private static final int UNSET_LINE = -1;
James Cookf59152c2015-02-26 18:03:58 -0800147 // Tag used when the Editor maintains its own separate UndoManager.
148 private static final String UNDO_OWNER_TAG = "Editor";
Gilles Debunned88876a2012-03-16 17:34:04 -0700149
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900150 // Ordering constants used to place the Action Mode or context menu items in their menu.
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +0100151 private static final int MENU_ITEM_ORDER_ASSIST = 0;
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +0000152 private static final int MENU_ITEM_ORDER_UNDO = 2;
153 private static final int MENU_ITEM_ORDER_REDO = 3;
Abodunrinwa Toki5fedfb82017-02-06 19:34:00 +0000154 private static final int MENU_ITEM_ORDER_CUT = 4;
155 private static final int MENU_ITEM_ORDER_COPY = 5;
156 private static final int MENU_ITEM_ORDER_PASTE = 6;
157 private static final int MENU_ITEM_ORDER_SHARE = 7;
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +0100158 private static final int MENU_ITEM_ORDER_SELECT_ALL = 8;
159 private static final int MENU_ITEM_ORDER_REPLACE = 9;
160 private static final int MENU_ITEM_ORDER_AUTOFILL = 10;
161 private static final int MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT = 11;
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +0100162 private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 100;
Clara Bayarri3b69fd82015-06-03 21:52:02 +0100163
James Cookf59152c2015-02-26 18:03:58 -0800164 // Each Editor manages its own undo stack.
165 private final UndoManager mUndoManager = new UndoManager();
166 private UndoOwner mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
James Cook48e0fac2015-02-25 15:44:51 -0800167 final UndoInputFilter mUndoInputFilter = new UndoInputFilter(this);
James Cookf1dad1e2015-02-27 11:00:01 -0800168 boolean mAllowUndo = true;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700169
Abodunrinwa Toki54486c12017-04-19 21:02:36 +0100170 private final MetricsLogger mMetricsLogger = new MetricsLogger();
171
Gilles Debunned88876a2012-03-16 17:34:04 -0700172 // Cursor Controllers.
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900173 private InsertionPointCursorController mInsertionPointCursorController;
Gilles Debunned88876a2012-03-16 17:34:04 -0700174 SelectionModifierCursorController mSelectionModifierCursorController;
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100175 // Action mode used when text is selected or when actions on an insertion cursor are triggered.
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800176 private ActionMode mTextActionMode;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900177 private boolean mInsertionControllerEnabled;
178 private boolean mSelectionControllerEnabled;
Gilles Debunned88876a2012-03-16 17:34:04 -0700179
Yohei Yukawac9cd9db2017-06-19 18:27:34 -0700180 private final boolean mHapticTextHandleEnabled;
181
Gilles Debunned88876a2012-03-16 17:34:04 -0700182 // Used to highlight a word when it is corrected by the IME
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900183 private CorrectionHighlighter mCorrectionHighlighter;
Gilles Debunned88876a2012-03-16 17:34:04 -0700184
185 InputContentType mInputContentType;
186 InputMethodState mInputMethodState;
187
Chris Craik956f3402015-04-27 16:41:00 -0700188 private static class TextRenderNode {
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +0900189 // Render node has 3 recording states:
190 // 1. Recorded operations are valid.
191 // #needsRecord() returns false, but needsToBeShifted is false.
192 // 2. Recorded operations are not valid, but just the position needed to be updated.
193 // #needsRecord() returns false, but needsToBeShifted is true.
194 // 3. Recorded operations are not valid. Need to record operations. #needsRecord() returns
195 // true.
Chris Craik956f3402015-04-27 16:41:00 -0700196 RenderNode renderNode;
John Reck7558aa72014-03-05 14:59:59 -0800197 boolean isDirty;
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +0900198 // Becomes true when recorded operations can be reused, but the position has to be updated.
199 boolean needsToBeShifted;
Chris Craik956f3402015-04-27 16:41:00 -0700200 public TextRenderNode(String name) {
Chris Craik956f3402015-04-27 16:41:00 -0700201 renderNode = RenderNode.create(name, null);
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +0900202 isDirty = true;
203 needsToBeShifted = true;
John Reck7558aa72014-03-05 14:59:59 -0800204 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700205 boolean needsRecord() {
206 return isDirty || !renderNode.isValid();
207 }
John Reck7558aa72014-03-05 14:59:59 -0800208 }
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900209 private TextRenderNode[] mTextRenderNodes;
Gilles Debunned88876a2012-03-16 17:34:04 -0700210
211 boolean mFrozenWithFocus;
212 boolean mSelectionMoved;
213 boolean mTouchFocusSelected;
214
215 KeyListener mKeyListener;
216 int mInputType = EditorInfo.TYPE_NULL;
217
218 boolean mDiscardNextActionUp;
219 boolean mIgnoreActionUpEvent;
220
221 long mShowCursor;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900222 private Blink mBlink;
Gilles Debunned88876a2012-03-16 17:34:04 -0700223
224 boolean mCursorVisible = true;
225 boolean mSelectAllOnFocus;
226 boolean mTextIsSelectable;
227
228 CharSequence mError;
229 boolean mErrorWasChanged;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900230 private ErrorPopup mErrorPopup;
Fabrice Di Meglio1957d282012-10-25 17:42:39 -0700231
Gilles Debunned88876a2012-03-16 17:34:04 -0700232 /**
233 * This flag is set if the TextView tries to display an error before it
234 * is attached to the window (so its position is still unknown).
235 * It causes the error to be shown later, when onAttachedToWindow()
236 * is called.
237 */
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900238 private boolean mShowErrorAfterAttach;
Gilles Debunned88876a2012-03-16 17:34:04 -0700239
240 boolean mInBatchEditControllers;
Gilles Debunne3473b2b2012-04-20 16:21:10 -0700241 boolean mShowSoftInputOnFocus = true;
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -0800242 private boolean mPreserveSelection;
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +0900243 private boolean mRestartActionModeOnNextRefresh;
Gilles Debunned88876a2012-03-16 17:34:04 -0700244
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800245 private SelectionActionModeHelper mSelectionActionModeHelper;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +0000246
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900247 boolean mIsBeingLongClicked;
248
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900249 private SuggestionsPopupWindow mSuggestionsPopupWindow;
Gilles Debunned88876a2012-03-16 17:34:04 -0700250 SuggestionRangeSpan mSuggestionRangeSpan;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900251 private Runnable mShowSuggestionRunnable;
Gilles Debunned88876a2012-03-16 17:34:04 -0700252
253 final Drawable[] mCursorDrawable = new Drawable[2];
254 int mCursorCount; // Current number of used mCursorDrawable: 0 (resource=0), 1 or 2 (split)
255
256 private Drawable mSelectHandleLeft;
257 private Drawable mSelectHandleRight;
258 private Drawable mSelectHandleCenter;
259
260 // Global listener that detects changes in the global position of the TextView
261 private PositionListener mPositionListener;
262
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900263 private float mLastDownPositionX, mLastDownPositionY;
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900264 private float mContextMenuAnchorX, mContextMenuAnchorY;
Gilles Debunned88876a2012-03-16 17:34:04 -0700265 Callback mCustomSelectionActionModeCallback;
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100266 Callback mCustomInsertionActionModeCallback;
Gilles Debunned88876a2012-03-16 17:34:04 -0700267
268 // Set when this TextView gained focus with some text selected. Will start selection mode.
269 boolean mCreatedWithASelection;
270
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +0900271 // Indicates the current tap state (first tap, double tap, or triple click).
272 private int mTapState = TAP_STATE_INITIAL;
273 private long mLastTouchUpTime = 0;
274 private static final int TAP_STATE_INITIAL = 0;
275 private static final int TAP_STATE_FIRST_TAP = 1;
276 private static final int TAP_STATE_DOUBLE_TAP = 2;
277 // Only for mouse input.
278 private static final int TAP_STATE_TRIPLE_CLICK = 3;
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100279
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900280 // The button state as of the last time #onTouchEvent is called.
281 private int mLastButtonState;
282
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100283 private Runnable mInsertionActionModeRunnable;
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100284
Jean Chalardbaf30942013-02-28 16:01:51 -0800285 // The span controller helps monitoring the changes to which the Editor needs to react:
286 // - EasyEditSpans, for which we have some UI to display on attach and on hide
287 // - SelectionSpans, for which we need to call updateSelection if an IME is attached
288 private SpanController mSpanController;
Gilles Debunned88876a2012-03-16 17:34:04 -0700289
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900290 private WordIterator mWordIterator;
Gilles Debunned88876a2012-03-16 17:34:04 -0700291 SpellChecker mSpellChecker;
292
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800293 // This word iterator is set with text and used to determine word boundaries
294 // when a user is selecting text.
295 private WordIterator mWordIteratorWithText;
296 // Indicate that the text in the word iterator needs to be updated.
297 private boolean mUpdateWordIteratorText;
298
Gilles Debunned88876a2012-03-16 17:34:04 -0700299 private Rect mTempRect;
300
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800301 private final TextView mTextView;
Gilles Debunned88876a2012-03-16 17:34:04 -0700302
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700303 final ProcessTextIntentActionsHandler mProcessTextIntentActionsHandler;
304
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700305 private final CursorAnchorInfoNotifier mCursorAnchorInfoNotifier =
306 new CursorAnchorInfoNotifier();
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900307
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100308 private final Runnable mShowFloatingToolbar = new Runnable() {
309 @Override
310 public void run() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100311 if (mTextActionMode != null) {
Abodunrinwa Toki9e211282015-06-05 02:46:57 +0100312 mTextActionMode.hide(0); // hide off.
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100313 }
314 }
315 };
316
Clara Bayarrib71dddd2015-06-04 23:17:30 +0100317 boolean mIsInsertionActionModeStartPending = false;
318
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +0900319 private final SuggestionHelper mSuggestionHelper = new SuggestionHelper();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +0900320
Gilles Debunned88876a2012-03-16 17:34:04 -0700321 Editor(TextView textView) {
322 mTextView = textView;
James Cookf59152c2015-02-26 18:03:58 -0800323 // Synchronize the filter list, which places the undo input filter at the end.
324 mTextView.setFilters(mTextView.getFilters());
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700325 mProcessTextIntentActionsHandler = new ProcessTextIntentActionsHandler(this);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -0700326 mHapticTextHandleEnabled = mTextView.getContext().getResources().getBoolean(
327 com.android.internal.R.bool.config_enableHapticTextHandle);
James Cookf59152c2015-02-26 18:03:58 -0800328 }
329
330 ParcelableParcel saveInstanceState() {
James Cookd2026682015-03-03 14:40:14 -0800331 ParcelableParcel state = new ParcelableParcel(getClass().getClassLoader());
332 Parcel parcel = state.getParcel();
333 mUndoManager.saveInstanceState(parcel);
334 mUndoInputFilter.saveInstanceState(parcel);
335 return state;
James Cookf59152c2015-02-26 18:03:58 -0800336 }
337
338 void restoreInstanceState(ParcelableParcel state) {
James Cookd2026682015-03-03 14:40:14 -0800339 Parcel parcel = state.getParcel();
340 mUndoManager.restoreInstanceState(parcel, state.getClassLoader());
341 mUndoInputFilter.restoreInstanceState(parcel);
James Cookf59152c2015-02-26 18:03:58 -0800342 // Re-associate this object as the owner of undo state.
343 mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
344 }
345
James Cook48e0fac2015-02-25 15:44:51 -0800346 /**
347 * Forgets all undo and redo operations for this Editor.
348 */
349 void forgetUndoRedo() {
350 UndoOwner[] owners = { mUndoOwner };
351 mUndoManager.forgetUndos(owners, -1 /* all */);
352 mUndoManager.forgetRedos(owners, -1 /* all */);
353 }
354
James Cookf59152c2015-02-26 18:03:58 -0800355 boolean canUndo() {
356 UndoOwner[] owners = { mUndoOwner };
James Cookf1dad1e2015-02-27 11:00:01 -0800357 return mAllowUndo && mUndoManager.countUndos(owners) > 0;
James Cookf59152c2015-02-26 18:03:58 -0800358 }
359
360 boolean canRedo() {
361 UndoOwner[] owners = { mUndoOwner };
James Cookf1dad1e2015-02-27 11:00:01 -0800362 return mAllowUndo && mUndoManager.countRedos(owners) > 0;
James Cookf59152c2015-02-26 18:03:58 -0800363 }
364
365 void undo() {
James Cookf1dad1e2015-02-27 11:00:01 -0800366 if (!mAllowUndo) {
367 return;
368 }
James Cookf59152c2015-02-26 18:03:58 -0800369 UndoOwner[] owners = { mUndoOwner };
370 mUndoManager.undo(owners, 1); // Undo 1 action.
371 }
372
373 void redo() {
James Cookf1dad1e2015-02-27 11:00:01 -0800374 if (!mAllowUndo) {
375 return;
376 }
James Cookf59152c2015-02-26 18:03:58 -0800377 UndoOwner[] owners = { mUndoOwner };
378 mUndoManager.redo(owners, 1); // Redo 1 action.
Gilles Debunned88876a2012-03-16 17:34:04 -0700379 }
380
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100381 void replace() {
Keisuke Kuroyanagi713be062016-02-29 16:07:54 -0800382 if (mSuggestionsPopupWindow == null) {
383 mSuggestionsPopupWindow = new SuggestionsPopupWindow();
384 }
385 hideCursorAndSpanControllers();
386 mSuggestionsPopupWindow.show();
387
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100388 int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100389 Selection.setSelection((Spannable) mTextView.getText(), middle);
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100390 }
391
Gilles Debunned88876a2012-03-16 17:34:04 -0700392 void onAttachedToWindow() {
393 if (mShowErrorAfterAttach) {
394 showError();
395 mShowErrorAfterAttach = false;
396 }
397
398 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
399 // No need to create the controller.
400 // The get method will add the listener on controller creation.
401 if (mInsertionPointCursorController != null) {
402 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
403 }
404 if (mSelectionModifierCursorController != null) {
Adam Powell057a5852012-05-11 10:28:38 -0700405 mSelectionModifierCursorController.resetTouchOffsets();
Gilles Debunned88876a2012-03-16 17:34:04 -0700406 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
407 }
408 updateSpellCheckSpans(0, mTextView.getText().length(),
409 true /* create the spell checker if needed */);
Adam Powell057a5852012-05-11 10:28:38 -0700410
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +0900411 if (mTextView.hasSelection()) {
412 refreshTextActionMode();
Adam Powell057a5852012-05-11 10:28:38 -0700413 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900414
415 getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true);
Mikael Gullstrand5b734f22013-07-09 14:41:28 +0200416 resumeBlink();
Gilles Debunned88876a2012-03-16 17:34:04 -0700417 }
418
419 void onDetachedFromWindow() {
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900420 getPositionListener().removeSubscriber(mCursorAnchorInfoNotifier);
421
Gilles Debunned88876a2012-03-16 17:34:04 -0700422 if (mError != null) {
423 hideError();
424 }
425
Mikael Gullstrand5b734f22013-07-09 14:41:28 +0200426 suspendBlink();
Gilles Debunned88876a2012-03-16 17:34:04 -0700427
428 if (mInsertionPointCursorController != null) {
429 mInsertionPointCursorController.onDetached();
430 }
431
432 if (mSelectionModifierCursorController != null) {
433 mSelectionModifierCursorController.onDetached();
434 }
435
436 if (mShowSuggestionRunnable != null) {
437 mTextView.removeCallbacks(mShowSuggestionRunnable);
438 }
439
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100440 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100441 if (mInsertionActionModeRunnable != null) {
442 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100443 }
444
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100445 mTextView.removeCallbacks(mShowFloatingToolbar);
446
Chris Craik003cc3d2015-10-16 10:24:55 -0700447 discardTextDisplayLists();
Gilles Debunned88876a2012-03-16 17:34:04 -0700448
449 if (mSpellChecker != null) {
450 mSpellChecker.closeSession();
451 // Forces the creation of a new SpellChecker next time this window is created.
452 // Will handle the cases where the settings has been changed in the meantime.
453 mSpellChecker = null;
454 }
455
Mady Mellora2861452015-06-25 08:40:27 -0700456 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -0800457 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -0700458 }
459
Chris Craik003cc3d2015-10-16 10:24:55 -0700460 private void discardTextDisplayLists() {
Chris Craik956f3402015-04-27 16:41:00 -0700461 if (mTextRenderNodes != null) {
462 for (int i = 0; i < mTextRenderNodes.length; i++) {
463 RenderNode displayList = mTextRenderNodes[i] != null
464 ? mTextRenderNodes[i].renderNode : null;
John Reck7558aa72014-03-05 14:59:59 -0800465 if (displayList != null && displayList.isValid()) {
Chris Craik003cc3d2015-10-16 10:24:55 -0700466 displayList.discardDisplayList();
John Reck7558aa72014-03-05 14:59:59 -0800467 }
468 }
469 }
470 }
471
Gilles Debunned88876a2012-03-16 17:34:04 -0700472 private void showError() {
473 if (mTextView.getWindowToken() == null) {
474 mShowErrorAfterAttach = true;
475 return;
476 }
477
478 if (mErrorPopup == null) {
479 LayoutInflater inflater = LayoutInflater.from(mTextView.getContext());
480 final TextView err = (TextView) inflater.inflate(
481 com.android.internal.R.layout.textview_hint, null);
482
483 final float scale = mTextView.getResources().getDisplayMetrics().density;
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700484 mErrorPopup =
485 new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f));
Gilles Debunned88876a2012-03-16 17:34:04 -0700486 mErrorPopup.setFocusable(false);
487 // The user is entering text, so the input method is needed. We
488 // don't want the popup to be displayed on top of it.
489 mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
490 }
491
492 TextView tv = (TextView) mErrorPopup.getContentView();
493 chooseSize(mErrorPopup, mError, tv);
494 tv.setText(mError);
495
496 mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY());
497 mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor());
498 }
499
500 public void setError(CharSequence error, Drawable icon) {
501 mError = TextUtils.stringOrSpannedString(error);
502 mErrorWasChanged = true;
Romain Guyd1cc1872012-11-05 17:43:25 -0800503
Gilles Debunned88876a2012-03-16 17:34:04 -0700504 if (mError == null) {
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800505 setErrorIcon(null);
Gilles Debunned88876a2012-03-16 17:34:04 -0700506 if (mErrorPopup != null) {
507 if (mErrorPopup.isShowing()) {
508 mErrorPopup.dismiss();
509 }
510
511 mErrorPopup = null;
512 }
Daniel 2 Olofssonf4ecc552013-08-13 10:30:26 +0200513 mShowErrorAfterAttach = false;
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800514 } else {
Romain Guyd1cc1872012-11-05 17:43:25 -0800515 setErrorIcon(icon);
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800516 if (mTextView.isFocused()) {
517 showError();
518 }
Romain Guyd1cc1872012-11-05 17:43:25 -0800519 }
520 }
521
522 private void setErrorIcon(Drawable icon) {
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800523 Drawables dr = mTextView.mDrawables;
524 if (dr == null) {
Fabrice Di Megliof7a5cdf2013-03-15 15:36:51 -0700525 mTextView.mDrawables = dr = new Drawables(mTextView.getContext());
Gilles Debunned88876a2012-03-16 17:34:04 -0700526 }
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800527 dr.setErrorDrawable(icon, mTextView);
528
529 mTextView.resetResolvedDrawables();
530 mTextView.invalidate();
531 mTextView.requestLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -0700532 }
533
534 private void hideError() {
535 if (mErrorPopup != null) {
536 if (mErrorPopup.isShowing()) {
537 mErrorPopup.dismiss();
538 }
539 }
540
541 mShowErrorAfterAttach = false;
542 }
543
544 /**
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800545 * Returns the X offset to make the pointy top of the error point
Gilles Debunned88876a2012-03-16 17:34:04 -0700546 * at the middle of the error icon.
547 */
548 private int getErrorX() {
549 /*
550 * The "25" is the distance between the point and the right edge
551 * of the background
552 */
553 final float scale = mTextView.getResources().getDisplayMetrics().density;
554
555 final Drawables dr = mTextView.mDrawables;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800556
557 final int layoutDirection = mTextView.getLayoutDirection();
558 int errorX;
559 int offset;
560 switch (layoutDirection) {
561 default:
562 case View.LAYOUT_DIRECTION_LTR:
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700563 offset = -(dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
564 errorX = mTextView.getWidth() - mErrorPopup.getWidth()
565 - mTextView.getPaddingRight() + offset;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800566 break;
567 case View.LAYOUT_DIRECTION_RTL:
568 offset = (dr != null ? dr.mDrawableSizeLeft : 0) / 2 - (int) (25 * scale + 0.5f);
569 errorX = mTextView.getPaddingLeft() + offset;
570 break;
571 }
572 return errorX;
Gilles Debunned88876a2012-03-16 17:34:04 -0700573 }
574
575 /**
576 * Returns the Y offset to make the pointy top of the error point
577 * at the bottom of the error icon.
578 */
579 private int getErrorY() {
580 /*
581 * Compound, not extended, because the icon is not clipped
582 * if the text height is smaller.
583 */
584 final int compoundPaddingTop = mTextView.getCompoundPaddingTop();
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700585 int vspace = mTextView.getBottom() - mTextView.getTop()
586 - mTextView.getCompoundPaddingBottom() - compoundPaddingTop;
Gilles Debunned88876a2012-03-16 17:34:04 -0700587
588 final Drawables dr = mTextView.mDrawables;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800589
590 final int layoutDirection = mTextView.getLayoutDirection();
591 int height;
592 switch (layoutDirection) {
593 default:
594 case View.LAYOUT_DIRECTION_LTR:
595 height = (dr != null ? dr.mDrawableHeightRight : 0);
596 break;
597 case View.LAYOUT_DIRECTION_RTL:
598 height = (dr != null ? dr.mDrawableHeightLeft : 0);
599 break;
600 }
601
602 int icontop = compoundPaddingTop + (vspace - height) / 2;
Gilles Debunned88876a2012-03-16 17:34:04 -0700603
604 /*
605 * The "2" is the distance between the point and the top edge
606 * of the background.
607 */
608 final float scale = mTextView.getResources().getDisplayMetrics().density;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800609 return icontop + height - mTextView.getHeight() - (int) (2 * scale + 0.5f);
Gilles Debunned88876a2012-03-16 17:34:04 -0700610 }
611
612 void createInputContentTypeIfNeeded() {
613 if (mInputContentType == null) {
614 mInputContentType = new InputContentType();
615 }
616 }
617
618 void createInputMethodStateIfNeeded() {
619 if (mInputMethodState == null) {
620 mInputMethodState = new InputMethodState();
621 }
622 }
623
624 boolean isCursorVisible() {
625 // The default value is true, even when there is no associated Editor
626 return mCursorVisible && mTextView.isTextEditable();
627 }
628
629 void prepareCursorControllers() {
630 boolean windowSupportsHandles = false;
631
632 ViewGroup.LayoutParams params = mTextView.getRootView().getLayoutParams();
633 if (params instanceof WindowManager.LayoutParams) {
634 WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
635 windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
636 || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
637 }
638
639 boolean enabled = windowSupportsHandles && mTextView.getLayout() != null;
640 mInsertionControllerEnabled = enabled && isCursorVisible();
641 mSelectionControllerEnabled = enabled && mTextView.textCanBeSelected();
642
643 if (!mInsertionControllerEnabled) {
644 hideInsertionPointCursorController();
645 if (mInsertionPointCursorController != null) {
646 mInsertionPointCursorController.onDetached();
647 mInsertionPointCursorController = null;
648 }
649 }
650
651 if (!mSelectionControllerEnabled) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100652 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -0700653 if (mSelectionModifierCursorController != null) {
654 mSelectionModifierCursorController.onDetached();
655 mSelectionModifierCursorController = null;
656 }
657 }
658 }
659
Seigo Nonakabb6a62c2015-03-31 21:59:30 +0900660 void hideInsertionPointCursorController() {
Gilles Debunned88876a2012-03-16 17:34:04 -0700661 if (mInsertionPointCursorController != null) {
662 mInsertionPointCursorController.hide();
663 }
664 }
665
666 /**
Mady Mellora2861452015-06-25 08:40:27 -0700667 * Hides the insertion and span controllers.
Gilles Debunned88876a2012-03-16 17:34:04 -0700668 */
Mady Mellora2861452015-06-25 08:40:27 -0700669 void hideCursorAndSpanControllers() {
Gilles Debunned88876a2012-03-16 17:34:04 -0700670 hideCursorControllers();
671 hideSpanControllers();
672 }
673
674 private void hideSpanControllers() {
Jean Chalardbaf30942013-02-28 16:01:51 -0800675 if (mSpanController != null) {
676 mSpanController.hide();
Gilles Debunned88876a2012-03-16 17:34:04 -0700677 }
678 }
679
680 private void hideCursorControllers() {
Yohei Yukawa85d08f12015-04-29 20:12:37 -0700681 // When mTextView is not ExtractEditText, we need to distinguish two kinds of focus-lost.
682 // One is the true focus lost where suggestions pop-up (if any) should be dismissed, and the
683 // other is an side effect of showing the suggestions pop-up itself. We use isShowingUp()
684 // to distinguish one from the other.
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700685 if (mSuggestionsPopupWindow != null && ((mTextView.isInExtractedMode())
686 || !mSuggestionsPopupWindow.isShowingUp())) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700687 // Should be done before hide insertion point controller since it triggers a show of it
688 mSuggestionsPopupWindow.hide();
689 }
690 hideInsertionPointCursorController();
Gilles Debunned88876a2012-03-16 17:34:04 -0700691 }
692
693 /**
694 * Create new SpellCheckSpans on the modified region.
695 */
696 private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
Satoshi Kataokad7429c12013-06-05 16:30:23 +0900697 // Remove spans whose adjacent characters are text not punctuation
698 mTextView.removeAdjacentSuggestionSpans(start);
699 mTextView.removeAdjacentSuggestionSpans(end);
700
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700701 if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled()
702 && !(mTextView.isInExtractedMode())) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700703 if (mSpellChecker == null && createSpellChecker) {
704 mSpellChecker = new SpellChecker(mTextView);
705 }
706 if (mSpellChecker != null) {
707 mSpellChecker.spellCheck(start, end);
708 }
709 }
710 }
711
712 void onScreenStateChanged(int screenState) {
713 switch (screenState) {
714 case View.SCREEN_STATE_ON:
715 resumeBlink();
716 break;
717 case View.SCREEN_STATE_OFF:
718 suspendBlink();
719 break;
720 }
721 }
722
723 private void suspendBlink() {
724 if (mBlink != null) {
725 mBlink.cancel();
726 }
727 }
728
729 private void resumeBlink() {
730 if (mBlink != null) {
731 mBlink.uncancel();
732 makeBlink();
733 }
734 }
735
736 void adjustInputType(boolean password, boolean passwordInputType,
737 boolean webPasswordInputType, boolean numberPasswordInputType) {
738 // mInputType has been set from inputType, possibly modified by mInputMethod.
739 // Specialize mInputType to [web]password if we have a text class and the original input
740 // type was a password.
741 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
742 if (password || passwordInputType) {
743 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
744 | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
745 }
746 if (webPasswordInputType) {
747 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
748 | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
749 }
750 } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
751 if (numberPasswordInputType) {
752 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
753 | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
754 }
755 }
756 }
757
758 private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
759 int wid = tv.getPaddingLeft() + tv.getPaddingRight();
760 int ht = tv.getPaddingTop() + tv.getPaddingBottom();
761
762 int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize(
763 com.android.internal.R.dimen.textview_error_popup_default_width);
764 Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels,
765 Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
766 float max = 0;
767 for (int i = 0; i < l.getLineCount(); i++) {
768 max = Math.max(max, l.getLineWidth(i));
769 }
770
771 /*
772 * Now set the popup size to be big enough for the text plus the border capped
773 * to DEFAULT_MAX_POPUP_WIDTH
774 */
775 pop.setWidth(wid + (int) Math.ceil(max));
776 pop.setHeight(ht + l.getHeight());
777 }
778
779 void setFrame() {
780 if (mErrorPopup != null) {
781 TextView tv = (TextView) mErrorPopup.getContentView();
782 chooseSize(mErrorPopup, mError, tv);
783 mErrorPopup.update(mTextView, getErrorX(), getErrorY(),
784 mErrorPopup.getWidth(), mErrorPopup.getHeight());
785 }
786 }
787
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800788 private int getWordStart(int offset) {
789 // FIXME - For this and similar methods we're not doing anything to check if there's
790 // a LocaleSpan in the text, this may be something we should try handling or checking for.
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700791 int retOffset = getWordIteratorWithText().prevBoundary(offset);
Mady Mellor58c90872015-05-12 11:09:37 -0700792 if (getWordIteratorWithText().isOnPunctuation(retOffset)) {
793 // On punctuation boundary or within group of punctuation, find punctuation start.
794 retOffset = getWordIteratorWithText().getPunctuationBeginning(offset);
795 } else {
796 // Not on a punctuation boundary, find the word start.
Mady Mellore264ac32015-06-22 16:46:29 -0700797 retOffset = getWordIteratorWithText().getPrevWordBeginningOnTwoWordsBoundary(offset);
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800798 }
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700799 if (retOffset == BreakIterator.DONE) {
800 return offset;
801 }
802 return retOffset;
803 }
804
805 private int getWordEnd(int offset) {
806 int retOffset = getWordIteratorWithText().nextBoundary(offset);
Mady Mellor58c90872015-05-12 11:09:37 -0700807 if (getWordIteratorWithText().isAfterPunctuation(retOffset)) {
808 // On punctuation boundary or within group of punctuation, find punctuation end.
809 retOffset = getWordIteratorWithText().getPunctuationEnd(offset);
810 } else {
811 // Not on a punctuation boundary, find the word end.
Mady Mellore264ac32015-06-22 16:46:29 -0700812 retOffset = getWordIteratorWithText().getNextWordEndOnTwoWordBoundary(offset);
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700813 }
814 if (retOffset == BreakIterator.DONE) {
815 return offset;
816 }
817 return retOffset;
818 }
819
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900820 private boolean needsToSelectAllToSelectWordOrParagraph() {
Andrei Stingaceanu47f82ae2015-04-28 17:43:54 +0100821 if (mTextView.hasPasswordTransformationMethod()) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700822 // Always select all on a password field.
823 // Cut/copy menu entries are not available for passwords, but being able to select all
824 // is however useful to delete or paste to replace the entire content.
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900825 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -0700826 }
827
828 int inputType = mTextView.getInputType();
829 int klass = inputType & InputType.TYPE_MASK_CLASS;
830 int variation = inputType & InputType.TYPE_MASK_VARIATION;
831
832 // Specific text field types: select the entire text for these
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700833 if (klass == InputType.TYPE_CLASS_NUMBER
834 || klass == InputType.TYPE_CLASS_PHONE
835 || klass == InputType.TYPE_CLASS_DATETIME
836 || variation == InputType.TYPE_TEXT_VARIATION_URI
837 || variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
838 || variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS
839 || variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900840 return true;
841 }
842 return false;
843 }
844
845 /**
846 * Adjusts selection to the word under last touch offset. Return true if the operation was
847 * successfully performed.
848 */
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100849 boolean selectCurrentWord() {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900850 if (!mTextView.canSelectText()) {
851 return false;
852 }
853
854 if (needsToSelectAllToSelectWordOrParagraph()) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700855 return mTextView.selectAllText();
856 }
857
858 long lastTouchOffsets = getLastTouchOffsets();
859 final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
860 final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
861
862 // Safety check in case standard touch event handling has been bypassed
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -0800863 if (minOffset < 0 || minOffset > mTextView.getText().length()) return false;
864 if (maxOffset < 0 || maxOffset > mTextView.getText().length()) return false;
Gilles Debunned88876a2012-03-16 17:34:04 -0700865
866 int selectionStart, selectionEnd;
867
868 // If a URLSpan (web address, email, phone...) is found at that position, select it.
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700869 URLSpan[] urlSpans =
870 ((Spanned) mTextView.getText()).getSpans(minOffset, maxOffset, URLSpan.class);
Gilles Debunned88876a2012-03-16 17:34:04 -0700871 if (urlSpans.length >= 1) {
872 URLSpan urlSpan = urlSpans[0];
873 selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan);
874 selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan);
875 } else {
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800876 // FIXME - We should check if there's a LocaleSpan in the text, this may be
877 // something we should try handling or checking for.
Gilles Debunned88876a2012-03-16 17:34:04 -0700878 final WordIterator wordIterator = getWordIterator();
879 wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset);
880
881 selectionStart = wordIterator.getBeginning(minOffset);
882 selectionEnd = wordIterator.getEnd(maxOffset);
883
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700884 if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE
885 || selectionStart == selectionEnd) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700886 // Possible when the word iterator does not properly handle the text's language
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +0900887 long range = getCharClusterRange(minOffset);
Gilles Debunned88876a2012-03-16 17:34:04 -0700888 selectionStart = TextUtils.unpackRangeStartFromLong(range);
889 selectionEnd = TextUtils.unpackRangeEndFromLong(range);
890 }
891 }
892
893 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
894 return selectionEnd > selectionStart;
895 }
896
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900897 /**
898 * Adjusts selection to the paragraph under last touch offset. Return true if the operation was
899 * successfully performed.
900 */
901 private boolean selectCurrentParagraph() {
902 if (!mTextView.canSelectText()) {
903 return false;
904 }
905
906 if (needsToSelectAllToSelectWordOrParagraph()) {
907 return mTextView.selectAllText();
908 }
909
910 long lastTouchOffsets = getLastTouchOffsets();
911 final int minLastTouchOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
912 final int maxLastTouchOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
913
914 final long paragraphsRange = getParagraphsRange(minLastTouchOffset, maxLastTouchOffset);
915 final int start = TextUtils.unpackRangeStartFromLong(paragraphsRange);
916 final int end = TextUtils.unpackRangeEndFromLong(paragraphsRange);
917 if (start < end) {
918 Selection.setSelection((Spannable) mTextView.getText(), start, end);
919 return true;
920 }
921 return false;
922 }
923
924 /**
925 * Get the minimum range of paragraphs that contains startOffset and endOffset.
926 */
927 private long getParagraphsRange(int startOffset, int endOffset) {
928 final Layout layout = mTextView.getLayout();
929 if (layout == null) {
930 return TextUtils.packRangeInLong(-1, -1);
931 }
932 final CharSequence text = mTextView.getText();
933 int minLine = layout.getLineForOffset(startOffset);
934 // Search paragraph start.
935 while (minLine > 0) {
936 final int prevLineEndOffset = layout.getLineEnd(minLine - 1);
937 if (text.charAt(prevLineEndOffset - 1) == '\n') {
938 break;
939 }
940 minLine--;
941 }
942 int maxLine = layout.getLineForOffset(endOffset);
943 // Search paragraph end.
944 while (maxLine < layout.getLineCount() - 1) {
945 final int lineEndOffset = layout.getLineEnd(maxLine);
946 if (text.charAt(lineEndOffset - 1) == '\n') {
947 break;
948 }
949 maxLine++;
950 }
951 return TextUtils.packRangeInLong(layout.getLineStart(minLine), layout.getLineEnd(maxLine));
952 }
953
Gilles Debunned88876a2012-03-16 17:34:04 -0700954 void onLocaleChanged() {
Keisuke Kuroyanagie0ac5ac2016-03-09 15:33:30 +0900955 // Will be re-created on demand in getWordIterator and getWordIteratorWithText with the
956 // proper new locale
Gilles Debunned88876a2012-03-16 17:34:04 -0700957 mWordIterator = null;
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800958 mWordIteratorWithText = null;
Gilles Debunned88876a2012-03-16 17:34:04 -0700959 }
960
Gilles Debunned88876a2012-03-16 17:34:04 -0700961 public WordIterator getWordIterator() {
962 if (mWordIterator == null) {
963 mWordIterator = new WordIterator(mTextView.getTextServicesLocale());
964 }
965 return mWordIterator;
966 }
967
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800968 private WordIterator getWordIteratorWithText() {
969 if (mWordIteratorWithText == null) {
970 mWordIteratorWithText = new WordIterator(mTextView.getTextServicesLocale());
971 mUpdateWordIteratorText = true;
972 }
973 if (mUpdateWordIteratorText) {
974 // FIXME - Shouldn't copy all of the text as only the area of the text relevant
975 // to the user's selection is needed. A possible solution would be to
976 // copy some number N of characters near the selection and then when the
977 // user approaches N then we'd do another copy of the next N characters.
978 CharSequence text = mTextView.getText();
979 mWordIteratorWithText.setCharSequence(text, 0, text.length());
980 mUpdateWordIteratorText = false;
981 }
982 return mWordIteratorWithText;
983 }
984
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +0900985 private int getNextCursorOffset(int offset, boolean findAfterGivenOffset) {
986 final Layout layout = mTextView.getLayout();
987 if (layout == null) return offset;
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700988 return findAfterGivenOffset == layout.isRtlCharAt(offset)
989 ? layout.getOffsetToLeftOf(offset) : layout.getOffsetToRightOf(offset);
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +0900990 }
991
992 private long getCharClusterRange(int offset) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700993 final int textLength = mTextView.getText().length();
Gilles Debunned88876a2012-03-16 17:34:04 -0700994 if (offset < textLength) {
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -0800995 final int clusterEndOffset = getNextCursorOffset(offset, true);
996 return TextUtils.packRangeInLong(
997 getNextCursorOffset(clusterEndOffset, false), clusterEndOffset);
Gilles Debunned88876a2012-03-16 17:34:04 -0700998 }
999 if (offset - 1 >= 0) {
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08001000 final int clusterStartOffset = getNextCursorOffset(offset, false);
1001 return TextUtils.packRangeInLong(clusterStartOffset,
1002 getNextCursorOffset(clusterStartOffset, true));
Gilles Debunned88876a2012-03-16 17:34:04 -07001003 }
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +09001004 return TextUtils.packRangeInLong(offset, offset);
Gilles Debunned88876a2012-03-16 17:34:04 -07001005 }
1006
1007 private boolean touchPositionIsInSelection() {
1008 int selectionStart = mTextView.getSelectionStart();
1009 int selectionEnd = mTextView.getSelectionEnd();
1010
1011 if (selectionStart == selectionEnd) {
1012 return false;
1013 }
1014
1015 if (selectionStart > selectionEnd) {
1016 int tmp = selectionStart;
1017 selectionStart = selectionEnd;
1018 selectionEnd = tmp;
1019 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
1020 }
1021
1022 SelectionModifierCursorController selectionController = getSelectionController();
1023 int minOffset = selectionController.getMinTouchOffset();
1024 int maxOffset = selectionController.getMaxTouchOffset();
1025
1026 return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
1027 }
1028
1029 private PositionListener getPositionListener() {
1030 if (mPositionListener == null) {
1031 mPositionListener = new PositionListener();
1032 }
1033 return mPositionListener;
1034 }
1035
1036 private interface TextViewPositionListener {
1037 public void updatePosition(int parentPositionX, int parentPositionY,
1038 boolean parentPositionChanged, boolean parentScrolled);
1039 }
1040
Gilles Debunned88876a2012-03-16 17:34:04 -07001041 private boolean isOffsetVisible(int offset) {
1042 Layout layout = mTextView.getLayout();
Victoria Leaseb9b77ae2013-10-13 15:12:52 -07001043 if (layout == null) return false;
1044
Gilles Debunned88876a2012-03-16 17:34:04 -07001045 final int line = layout.getLineForOffset(offset);
1046 final int lineBottom = layout.getLineBottom(line);
1047 final int primaryHorizontal = (int) layout.getPrimaryHorizontal(offset);
Phil Weaverc2e28932016-12-08 12:29:25 -08001048 return mTextView.isPositionVisible(
1049 primaryHorizontal + mTextView.viewportToContentHorizontalOffset(),
Gilles Debunned88876a2012-03-16 17:34:04 -07001050 lineBottom + mTextView.viewportToContentVerticalOffset());
1051 }
1052
1053 /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
1054 * in the view. Returns false when the position is in the empty space of left/right of text.
1055 */
1056 private boolean isPositionOnText(float x, float y) {
1057 Layout layout = mTextView.getLayout();
1058 if (layout == null) return false;
1059
1060 final int line = mTextView.getLineAtCoordinate(y);
1061 x = mTextView.convertToLocalHorizontalCoordinate(x);
1062
1063 if (x < layout.getLineLeft(line)) return false;
1064 if (x > layout.getLineRight(line)) return false;
1065 return true;
1066 }
1067
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001068 private void startDragAndDrop() {
Keisuke Kuroyanagifdfc93d2016-03-15 14:47:08 +09001069 // TODO: Fix drag and drop in full screen extracted mode.
1070 if (mTextView.isInExtractedMode()) {
1071 return;
1072 }
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001073 final int start = mTextView.getSelectionStart();
1074 final int end = mTextView.getSelectionEnd();
1075 CharSequence selectedText = mTextView.getTransformedText(start, end);
1076 ClipData data = ClipData.newPlainText(null, selectedText);
1077 DragLocalState localState = new DragLocalState(mTextView, start, end);
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08001078 mTextView.startDragAndDrop(data, getTextThumbnailBuilder(start, end), localState,
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001079 View.DRAG_FLAG_GLOBAL);
1080 stopTextActionMode();
1081 if (hasSelectionController()) {
1082 getSelectionController().resetTouchOffsets();
1083 }
1084 }
1085
Gilles Debunned88876a2012-03-16 17:34:04 -07001086 public boolean performLongClick(boolean handled) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001087 // Long press in empty space moves cursor and starts the insertion action mode.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001088 if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY)
1089 && mInsertionControllerEnabled) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001090 final int offset = mTextView.getOffsetForPosition(mLastDownPositionX,
1091 mLastDownPositionY);
Gilles Debunned88876a2012-03-16 17:34:04 -07001092 Selection.setSelection((Spannable) mTextView.getText(), offset);
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00001093 getInsertionController().show();
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001094 mIsInsertionActionModeStartPending = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001095 handled = true;
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001096 MetricsLogger.action(
1097 mTextView.getContext(),
1098 MetricsEvent.TEXT_LONGPRESS,
1099 TextViewMetrics.SUBTYPE_LONG_PRESS_OTHER);
Gilles Debunned88876a2012-03-16 17:34:04 -07001100 }
1101
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001102 if (!handled && mTextActionMode != null) {
Andrei Stingaceanu2aaeefe2015-10-20 19:11:23 +01001103 if (touchPositionIsInSelection()) {
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001104 startDragAndDrop();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001105 MetricsLogger.action(
1106 mTextView.getContext(),
1107 MetricsEvent.TEXT_LONGPRESS,
1108 TextViewMetrics.SUBTYPE_LONG_PRESS_DRAG_AND_DROP);
Gilles Debunned88876a2012-03-16 17:34:04 -07001109 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001110 stopTextActionMode();
Clara Bayarridfac4432015-05-15 12:18:24 +01001111 selectCurrentWordAndStartDrag();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001112 MetricsLogger.action(
1113 mTextView.getContext(),
1114 MetricsEvent.TEXT_LONGPRESS,
1115 TextViewMetrics.SUBTYPE_LONG_PRESS_SELECTION);
Gilles Debunned88876a2012-03-16 17:34:04 -07001116 }
1117 handled = true;
1118 }
1119
1120 // Start a new selection
1121 if (!handled) {
Clara Bayarridfac4432015-05-15 12:18:24 +01001122 handled = selectCurrentWordAndStartDrag();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001123 if (handled) {
1124 MetricsLogger.action(
1125 mTextView.getContext(),
1126 MetricsEvent.TEXT_LONGPRESS,
1127 TextViewMetrics.SUBTYPE_LONG_PRESS_SELECTION);
1128 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001129 }
1130
1131 return handled;
1132 }
1133
1134 private long getLastTouchOffsets() {
1135 SelectionModifierCursorController selectionController = getSelectionController();
1136 final int minOffset = selectionController.getMinTouchOffset();
1137 final int maxOffset = selectionController.getMaxTouchOffset();
1138 return TextUtils.packRangeInLong(minOffset, maxOffset);
1139 }
1140
1141 void onFocusChanged(boolean focused, int direction) {
1142 mShowCursor = SystemClock.uptimeMillis();
1143 ensureEndedBatchEdit();
1144
1145 if (focused) {
1146 int selStart = mTextView.getSelectionStart();
1147 int selEnd = mTextView.getSelectionEnd();
1148
1149 // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
1150 // mode for these, unless there was a specific selection already started.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001151 final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0
1152 && selEnd == mTextView.getText().length();
Gilles Debunned88876a2012-03-16 17:34:04 -07001153
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001154 mCreatedWithASelection = mFrozenWithFocus && mTextView.hasSelection()
1155 && !isFocusHighlighted;
Gilles Debunned88876a2012-03-16 17:34:04 -07001156
1157 if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
1158 // If a tap was used to give focus to that view, move cursor at tap position.
1159 // Has to be done before onTakeFocus, which can be overloaded.
1160 final int lastTapPosition = getLastTapPosition();
1161 if (lastTapPosition >= 0) {
1162 Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition);
1163 }
1164
1165 // Note this may have to be moved out of the Editor class
1166 MovementMethod mMovement = mTextView.getMovementMethod();
1167 if (mMovement != null) {
1168 mMovement.onTakeFocus(mTextView, (Spannable) mTextView.getText(), direction);
1169 }
1170
1171 // The DecorView does not have focus when the 'Done' ExtractEditText button is
1172 // pressed. Since it is the ViewAncestor's mView, it requests focus before
1173 // ExtractEditText clears focus, which gives focus to the ExtractEditText.
1174 // This special case ensure that we keep current selection in that case.
1175 // It would be better to know why the DecorView does not have focus at that time.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001176 if (((mTextView.isInExtractedMode()) || mSelectionMoved)
1177 && selStart >= 0 && selEnd >= 0) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001178 /*
1179 * Someone intentionally set the selection, so let them
1180 * do whatever it is that they wanted to do instead of
1181 * the default on-focus behavior. We reset the selection
1182 * here instead of just skipping the onTakeFocus() call
1183 * because some movement methods do something other than
1184 * just setting the selection in theirs and we still
1185 * need to go through that path.
1186 */
1187 Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
1188 }
1189
1190 if (mSelectAllOnFocus) {
1191 mTextView.selectAllText();
1192 }
1193
1194 mTouchFocusSelected = true;
1195 }
1196
1197 mFrozenWithFocus = false;
1198 mSelectionMoved = false;
1199
1200 if (mError != null) {
1201 showError();
1202 }
1203
1204 makeBlink();
1205 } else {
1206 if (mError != null) {
1207 hideError();
1208 }
1209 // Don't leave us in the middle of a batch edit.
1210 mTextView.onEndBatchEdit();
1211
Andrei Stingaceanub1891b32015-06-19 16:44:37 +01001212 if (mTextView.isInExtractedMode()) {
Mady Mellora2861452015-06-25 08:40:27 -07001213 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001214 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -07001215 } else {
Mady Mellora2861452015-06-25 08:40:27 -07001216 hideCursorAndSpanControllers();
Yohei Yukawa24df9312016-03-31 17:15:23 -07001217 if (mTextView.isTemporarilyDetached()) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001218 stopTextActionModeWithPreservingSelection();
1219 } else {
1220 stopTextActionMode();
1221 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001222 downgradeEasyCorrectionSpans();
1223 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001224 // No need to create the controller
1225 if (mSelectionModifierCursorController != null) {
1226 mSelectionModifierCursorController.resetTouchOffsets();
1227 }
1228 }
1229 }
1230
1231 /**
1232 * Downgrades to simple suggestions all the easy correction spans that are not a spell check
1233 * span.
1234 */
1235 private void downgradeEasyCorrectionSpans() {
1236 CharSequence text = mTextView.getText();
1237 if (text instanceof Spannable) {
1238 Spannable spannable = (Spannable) text;
1239 SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
1240 spannable.length(), SuggestionSpan.class);
1241 for (int i = 0; i < suggestionSpans.length; i++) {
1242 int flags = suggestionSpans[i].getFlags();
1243 if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
1244 && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
1245 flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
1246 suggestionSpans[i].setFlags(flags);
1247 }
1248 }
1249 }
1250 }
1251
1252 void sendOnTextChanged(int start, int after) {
1253 updateSpellCheckSpans(start, start + after, false);
1254
Mady Mellor2ff2cd82015-03-02 10:37:01 -08001255 // Flip flag to indicate the word iterator needs to have the text reset.
1256 mUpdateWordIteratorText = true;
1257
Gilles Debunned88876a2012-03-16 17:34:04 -07001258 // Hide the controllers as soon as text is modified (typing, procedural...)
1259 // We do not hide the span controllers, since they can be added when a new text is
1260 // inserted into the text view (voice IME).
1261 hideCursorControllers();
Keisuke Kuroyanagif4e347d2015-06-11 17:41:00 +09001262 // Reset drag accelerator.
1263 if (mSelectionModifierCursorController != null) {
1264 mSelectionModifierCursorController.resetTouchOffsets();
1265 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001266 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07001267 }
1268
1269 private int getLastTapPosition() {
1270 // No need to create the controller at that point, no last tap position saved
1271 if (mSelectionModifierCursorController != null) {
1272 int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
1273 if (lastTapPosition >= 0) {
1274 // Safety check, should not be possible.
1275 if (lastTapPosition > mTextView.getText().length()) {
1276 lastTapPosition = mTextView.getText().length();
1277 }
1278 return lastTapPosition;
1279 }
1280 }
1281
1282 return -1;
1283 }
1284
1285 void onWindowFocusChanged(boolean hasWindowFocus) {
1286 if (hasWindowFocus) {
1287 if (mBlink != null) {
1288 mBlink.uncancel();
1289 makeBlink();
1290 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001291 if (mTextView.hasSelection() && !extractedTextModeWillBeStarted()) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001292 refreshTextActionMode();
Mady Mellora2861452015-06-25 08:40:27 -07001293 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001294 } else {
1295 if (mBlink != null) {
1296 mBlink.cancel();
1297 }
1298 if (mInputContentType != null) {
1299 mInputContentType.enterDown = false;
1300 }
1301 // Order matters! Must be done before onParentLostFocus to rely on isShowingUp
Mady Mellora2861452015-06-25 08:40:27 -07001302 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001303 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -07001304 if (mSuggestionsPopupWindow != null) {
1305 mSuggestionsPopupWindow.onParentLostFocus();
1306 }
1307
Gilles Debunnec72fba82012-06-26 14:47:07 -07001308 // Don't leave us in the middle of a batch edit. Same as in onFocusChanged
1309 ensureEndedBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001310 }
1311 }
1312
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09001313 private void updateTapState(MotionEvent event) {
1314 final int action = event.getActionMasked();
1315 if (action == MotionEvent.ACTION_DOWN) {
1316 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
1317 // Detect double tap and triple click.
1318 if (((mTapState == TAP_STATE_FIRST_TAP)
1319 || ((mTapState == TAP_STATE_DOUBLE_TAP) && isMouse))
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001320 && (SystemClock.uptimeMillis() - mLastTouchUpTime)
1321 <= ViewConfiguration.getDoubleTapTimeout()) {
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09001322 if (mTapState == TAP_STATE_FIRST_TAP) {
1323 mTapState = TAP_STATE_DOUBLE_TAP;
1324 } else {
1325 mTapState = TAP_STATE_TRIPLE_CLICK;
1326 }
1327 } else {
1328 mTapState = TAP_STATE_FIRST_TAP;
1329 }
1330 }
1331 if (action == MotionEvent.ACTION_UP) {
1332 mLastTouchUpTime = SystemClock.uptimeMillis();
1333 }
1334 }
1335
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09001336 private boolean shouldFilterOutTouchEvent(MotionEvent event) {
1337 if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) {
1338 return false;
1339 }
1340 final boolean primaryButtonStateChanged =
1341 ((mLastButtonState ^ event.getButtonState()) & MotionEvent.BUTTON_PRIMARY) != 0;
1342 final int action = event.getActionMasked();
1343 if ((action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_UP)
1344 && !primaryButtonStateChanged) {
1345 return true;
1346 }
1347 if (action == MotionEvent.ACTION_MOVE
1348 && !event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
1349 return true;
1350 }
1351 return false;
1352 }
1353
Gilles Debunned88876a2012-03-16 17:34:04 -07001354 void onTouchEvent(MotionEvent event) {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09001355 final boolean filterOutEvent = shouldFilterOutTouchEvent(event);
1356 mLastButtonState = event.getButtonState();
1357 if (filterOutEvent) {
1358 if (event.getActionMasked() == MotionEvent.ACTION_UP) {
1359 mDiscardNextActionUp = true;
1360 }
1361 return;
1362 }
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09001363 updateTapState(event);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001364 updateFloatingToolbarVisibility(event);
1365
Gilles Debunned88876a2012-03-16 17:34:04 -07001366 if (hasSelectionController()) {
1367 getSelectionController().onTouchEvent(event);
1368 }
1369
1370 if (mShowSuggestionRunnable != null) {
1371 mTextView.removeCallbacks(mShowSuggestionRunnable);
1372 mShowSuggestionRunnable = null;
1373 }
1374
1375 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
1376 mLastDownPositionX = event.getX();
1377 mLastDownPositionY = event.getY();
1378
1379 // Reset this state; it will be re-set if super.onTouchEvent
1380 // causes focus to move to the view.
1381 mTouchFocusSelected = false;
1382 mIgnoreActionUpEvent = false;
1383 }
1384 }
1385
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001386 private void updateFloatingToolbarVisibility(MotionEvent event) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001387 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001388 switch (event.getActionMasked()) {
1389 case MotionEvent.ACTION_MOVE:
Abodunrinwa Toki4a7aeb32017-07-13 02:06:56 +01001390 hideFloatingToolbar(ActionMode.DEFAULT_HIDE_DURATION);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001391 break;
1392 case MotionEvent.ACTION_UP: // fall through
1393 case MotionEvent.ACTION_CANCEL:
1394 showFloatingToolbar();
1395 }
1396 }
1397 }
1398
Abodunrinwa Toki4a7aeb32017-07-13 02:06:56 +01001399 void hideFloatingToolbar(int duration) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001400 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001401 mTextView.removeCallbacks(mShowFloatingToolbar);
Abodunrinwa Toki4a7aeb32017-07-13 02:06:56 +01001402 mTextActionMode.hide(duration);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001403 }
1404 }
1405
1406 private void showFloatingToolbar() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001407 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001408 // Delay "show" so it doesn't interfere with click confirmations
1409 // or double-clicks that could "dismiss" the floating toolbar.
1410 int delay = ViewConfiguration.getDoubleTapTimeout();
1411 mTextView.postDelayed(mShowFloatingToolbar, delay);
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01001412
1413 // This classifies the text and most likely returns before the toolbar is actually
1414 // shown. If not, it will update the toolbar with the result when classification
1415 // returns. We would rather not wait for a long running classification process.
1416 invalidateActionModeAsync();
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001417 }
1418 }
1419
Gilles Debunned88876a2012-03-16 17:34:04 -07001420 public void beginBatchEdit() {
1421 mInBatchEditControllers = true;
1422 final InputMethodState ims = mInputMethodState;
1423 if (ims != null) {
1424 int nesting = ++ims.mBatchEditNesting;
1425 if (nesting == 1) {
1426 ims.mCursorChanged = false;
1427 ims.mChangedDelta = 0;
1428 if (ims.mContentChanged) {
1429 // We already have a pending change from somewhere else,
1430 // so turn this into a full update.
1431 ims.mChangedStart = 0;
1432 ims.mChangedEnd = mTextView.getText().length();
1433 } else {
1434 ims.mChangedStart = EXTRACT_UNKNOWN;
1435 ims.mChangedEnd = EXTRACT_UNKNOWN;
1436 ims.mContentChanged = false;
1437 }
James Cook48e0fac2015-02-25 15:44:51 -08001438 mUndoInputFilter.beginBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001439 mTextView.onBeginBatchEdit();
1440 }
1441 }
1442 }
1443
1444 public void endBatchEdit() {
1445 mInBatchEditControllers = false;
1446 final InputMethodState ims = mInputMethodState;
1447 if (ims != null) {
1448 int nesting = --ims.mBatchEditNesting;
1449 if (nesting == 0) {
1450 finishBatchEdit(ims);
1451 }
1452 }
1453 }
1454
1455 void ensureEndedBatchEdit() {
1456 final InputMethodState ims = mInputMethodState;
1457 if (ims != null && ims.mBatchEditNesting != 0) {
1458 ims.mBatchEditNesting = 0;
1459 finishBatchEdit(ims);
1460 }
1461 }
1462
1463 void finishBatchEdit(final InputMethodState ims) {
1464 mTextView.onEndBatchEdit();
James Cook48e0fac2015-02-25 15:44:51 -08001465 mUndoInputFilter.endBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001466
1467 if (ims.mContentChanged || ims.mSelectionModeChanged) {
1468 mTextView.updateAfterEdit();
1469 reportExtractedText();
1470 } else if (ims.mCursorChanged) {
Jean Chalardc99d33f2013-02-28 16:39:47 -08001471 // Cheesy way to get us to report the current cursor location.
Gilles Debunned88876a2012-03-16 17:34:04 -07001472 mTextView.invalidateCursor();
1473 }
Jean Chalardc99d33f2013-02-28 16:39:47 -08001474 // sendUpdateSelection knows to avoid sending if the selection did
1475 // not actually change.
1476 sendUpdateSelection();
Keisuke Kuroyanagic6fad962016-05-02 15:11:41 +09001477
1478 // Show drag handles if they were blocked by batch edit mode.
1479 if (mTextActionMode != null) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001480 final CursorController cursorController = mTextView.hasSelection()
1481 ? getSelectionController() : getInsertionController();
Keisuke Kuroyanagic6fad962016-05-02 15:11:41 +09001482 if (cursorController != null && !cursorController.isActive()
1483 && !cursorController.isCursorBeingModified()) {
1484 cursorController.show();
1485 }
1486 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001487 }
1488
1489 static final int EXTRACT_NOTHING = -2;
1490 static final int EXTRACT_UNKNOWN = -1;
1491
1492 boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
1493 return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
1494 EXTRACT_UNKNOWN, outText);
1495 }
1496
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001497 private boolean extractTextInternal(@Nullable ExtractedTextRequest request,
Gilles Debunned88876a2012-03-16 17:34:04 -07001498 int partialStartOffset, int partialEndOffset, int delta,
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001499 @Nullable ExtractedText outText) {
1500 if (request == null || outText == null) {
1501 return false;
Gilles Debunned88876a2012-03-16 17:34:04 -07001502 }
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001503
1504 final CharSequence content = mTextView.getText();
1505 if (content == null) {
1506 return false;
1507 }
1508
1509 if (partialStartOffset != EXTRACT_NOTHING) {
1510 final int N = content.length();
1511 if (partialStartOffset < 0) {
1512 outText.partialStartOffset = outText.partialEndOffset = -1;
1513 partialStartOffset = 0;
1514 partialEndOffset = N;
1515 } else {
1516 // Now use the delta to determine the actual amount of text
1517 // we need.
1518 partialEndOffset += delta;
1519 // Adjust offsets to ensure we contain full spans.
1520 if (content instanceof Spanned) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001521 Spanned spanned = (Spanned) content;
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001522 Object[] spans = spanned.getSpans(partialStartOffset,
1523 partialEndOffset, ParcelableSpan.class);
1524 int i = spans.length;
1525 while (i > 0) {
1526 i--;
1527 int j = spanned.getSpanStart(spans[i]);
1528 if (j < partialStartOffset) partialStartOffset = j;
1529 j = spanned.getSpanEnd(spans[i]);
1530 if (j > partialEndOffset) partialEndOffset = j;
1531 }
1532 }
1533 outText.partialStartOffset = partialStartOffset;
1534 outText.partialEndOffset = partialEndOffset - delta;
1535
1536 if (partialStartOffset > N) {
1537 partialStartOffset = N;
1538 } else if (partialStartOffset < 0) {
1539 partialStartOffset = 0;
1540 }
1541 if (partialEndOffset > N) {
1542 partialEndOffset = N;
1543 } else if (partialEndOffset < 0) {
1544 partialEndOffset = 0;
1545 }
1546 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001547 if ((request.flags & InputConnection.GET_TEXT_WITH_STYLES) != 0) {
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001548 outText.text = content.subSequence(partialStartOffset,
1549 partialEndOffset);
1550 } else {
1551 outText.text = TextUtils.substring(content, partialStartOffset,
1552 partialEndOffset);
1553 }
1554 } else {
1555 outText.partialStartOffset = 0;
1556 outText.partialEndOffset = 0;
1557 outText.text = "";
1558 }
1559 outText.flags = 0;
1560 if (MetaKeyKeyListener.getMetaState(content, MetaKeyKeyListener.META_SELECTING) != 0) {
1561 outText.flags |= ExtractedText.FLAG_SELECTING;
1562 }
1563 if (mTextView.isSingleLine()) {
1564 outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
1565 }
1566 outText.startOffset = 0;
1567 outText.selectionStart = mTextView.getSelectionStart();
1568 outText.selectionEnd = mTextView.getSelectionEnd();
1569 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001570 }
1571
1572 boolean reportExtractedText() {
1573 final Editor.InputMethodState ims = mInputMethodState;
1574 if (ims != null) {
1575 final boolean contentChanged = ims.mContentChanged;
1576 if (contentChanged || ims.mSelectionModeChanged) {
1577 ims.mContentChanged = false;
1578 ims.mSelectionModeChanged = false;
Gilles Debunnec62589c2012-04-12 14:50:23 -07001579 final ExtractedTextRequest req = ims.mExtractedTextRequest;
Gilles Debunned88876a2012-03-16 17:34:04 -07001580 if (req != null) {
1581 InputMethodManager imm = InputMethodManager.peekInstance();
1582 if (imm != null) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001583 if (TextView.DEBUG_EXTRACT) {
1584 Log.v(TextView.LOG_TAG, "Retrieving extracted start="
1585 + ims.mChangedStart
1586 + " end=" + ims.mChangedEnd
1587 + " delta=" + ims.mChangedDelta);
1588 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001589 if (ims.mChangedStart < 0 && !contentChanged) {
1590 ims.mChangedStart = EXTRACT_NOTHING;
1591 }
1592 if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
Gilles Debunnec62589c2012-04-12 14:50:23 -07001593 ims.mChangedDelta, ims.mExtractedText)) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001594 if (TextView.DEBUG_EXTRACT) {
1595 Log.v(TextView.LOG_TAG,
1596 "Reporting extracted start="
1597 + ims.mExtractedText.partialStartOffset
1598 + " end=" + ims.mExtractedText.partialEndOffset
1599 + ": " + ims.mExtractedText.text);
1600 }
Gilles Debunnec62589c2012-04-12 14:50:23 -07001601
1602 imm.updateExtractedText(mTextView, req.token, ims.mExtractedText);
Gilles Debunned88876a2012-03-16 17:34:04 -07001603 ims.mChangedStart = EXTRACT_UNKNOWN;
1604 ims.mChangedEnd = EXTRACT_UNKNOWN;
1605 ims.mChangedDelta = 0;
1606 ims.mContentChanged = false;
1607 return true;
1608 }
1609 }
1610 }
1611 }
1612 }
1613 return false;
1614 }
1615
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001616 private void sendUpdateSelection() {
1617 if (null != mInputMethodState && mInputMethodState.mBatchEditNesting <= 0) {
1618 final InputMethodManager imm = InputMethodManager.peekInstance();
1619 if (null != imm) {
1620 final int selectionStart = mTextView.getSelectionStart();
1621 final int selectionEnd = mTextView.getSelectionEnd();
1622 int candStart = -1;
1623 int candEnd = -1;
1624 if (mTextView.getText() instanceof Spannable) {
1625 final Spannable sp = (Spannable) mTextView.getText();
1626 candStart = EditableInputConnection.getComposingSpanStart(sp);
1627 candEnd = EditableInputConnection.getComposingSpanEnd(sp);
1628 }
Jean Chalardc99d33f2013-02-28 16:39:47 -08001629 // InputMethodManager#updateSelection skips sending the message if
1630 // none of the parameters have changed since the last time we called it.
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001631 imm.updateSelection(mTextView,
1632 selectionStart, selectionEnd, candStart, candEnd);
1633 }
1634 }
1635 }
1636
Gilles Debunned88876a2012-03-16 17:34:04 -07001637 void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint,
1638 int cursorOffsetVertical) {
1639 final int selectionStart = mTextView.getSelectionStart();
1640 final int selectionEnd = mTextView.getSelectionEnd();
1641
1642 final InputMethodState ims = mInputMethodState;
1643 if (ims != null && ims.mBatchEditNesting == 0) {
1644 InputMethodManager imm = InputMethodManager.peekInstance();
1645 if (imm != null) {
1646 if (imm.isActive(mTextView)) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001647 if (ims.mContentChanged || ims.mSelectionModeChanged) {
1648 // We are in extract mode and the content has changed
1649 // in some way... just report complete new text to the
1650 // input method.
Yohei Yukawab6bec1a2015-05-01 16:18:25 -07001651 reportExtractedText();
Gilles Debunned88876a2012-03-16 17:34:04 -07001652 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001653 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001654 }
1655 }
1656
1657 if (mCorrectionHighlighter != null) {
1658 mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
1659 }
1660
1661 if (highlight != null && selectionStart == selectionEnd && mCursorCount > 0) {
1662 drawCursor(canvas, cursorOffsetVertical);
1663 // Rely on the drawable entirely, do not draw the cursor line.
1664 // Has to be done after the IMM related code above which relies on the highlight.
1665 highlight = null;
1666 }
1667
1668 if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
1669 drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
1670 cursorOffsetVertical);
1671 } else {
1672 layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
1673 }
1674 }
1675
1676 private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
1677 Paint highlightPaint, int cursorOffsetVertical) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001678 final long lineRange = layout.getLineRangeForDraw(canvas);
1679 int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
1680 int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
1681 if (lastLine < 0) return;
1682
1683 layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
1684 firstLine, lastLine);
1685
1686 if (layout instanceof DynamicLayout) {
Chris Craik956f3402015-04-27 16:41:00 -07001687 if (mTextRenderNodes == null) {
1688 mTextRenderNodes = ArrayUtils.emptyArray(TextRenderNode.class);
Gilles Debunned88876a2012-03-16 17:34:04 -07001689 }
1690
1691 DynamicLayout dynamicLayout = (DynamicLayout) layout;
Gilles Debunne157aafc2012-04-19 17:21:57 -07001692 int[] blockEndLines = dynamicLayout.getBlockEndLines();
Gilles Debunned88876a2012-03-16 17:34:04 -07001693 int[] blockIndices = dynamicLayout.getBlockIndices();
1694 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
Sangkyu Lee955beb22012-12-10 15:47:00 +09001695 final int indexFirstChangedBlock = dynamicLayout.getIndexFirstChangedBlock();
Gilles Debunned88876a2012-03-16 17:34:04 -07001696
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001697 final ArraySet<Integer> blockSet = dynamicLayout.getBlocksAlwaysNeedToBeRedrawn();
1698 if (blockSet != null) {
1699 for (int i = 0; i < blockSet.size(); i++) {
1700 final int blockIndex = dynamicLayout.getBlockIndex(blockSet.valueAt(i));
1701 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
1702 && mTextRenderNodes[blockIndex] != null) {
1703 mTextRenderNodes[blockIndex].needsToBeShifted = true;
1704 }
1705 }
1706 }
1707
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001708 int startBlock = Arrays.binarySearch(blockEndLines, 0, numberOfBlocks, firstLine);
1709 if (startBlock < 0) {
1710 startBlock = -(startBlock + 1);
1711 }
1712 startBlock = Math.min(indexFirstChangedBlock, startBlock);
Gilles Debunned88876a2012-03-16 17:34:04 -07001713
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001714 int startIndexToFindAvailableRenderNode = 0;
1715 int lastIndex = numberOfBlocks;
1716
1717 for (int i = startBlock; i < numberOfBlocks; i++) {
1718 final int blockIndex = blockIndices[i];
1719 if (i >= indexFirstChangedBlock
1720 && blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
1721 && mTextRenderNodes[blockIndex] != null) {
1722 mTextRenderNodes[blockIndex].needsToBeShifted = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001723 }
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001724 if (blockEndLines[i] < firstLine) {
1725 // Blocks in [indexFirstChangedBlock, firstLine) are not redrawn here. They will
1726 // be redrawn after they get scrolled into drawing range.
1727 continue;
Gilles Debunned88876a2012-03-16 17:34:04 -07001728 }
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001729 startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas, layout,
1730 highlight, highlightPaint, cursorOffsetVertical, blockEndLines,
1731 blockIndices, i, numberOfBlocks, startIndexToFindAvailableRenderNode);
1732 if (blockEndLines[i] >= lastLine) {
1733 lastIndex = Math.max(indexFirstChangedBlock, i + 1);
1734 break;
Gilles Debunned88876a2012-03-16 17:34:04 -07001735 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001736 }
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001737 if (blockSet != null) {
1738 for (int i = 0; i < blockSet.size(); i++) {
1739 final int block = blockSet.valueAt(i);
1740 final int blockIndex = dynamicLayout.getBlockIndex(block);
1741 if (blockIndex == DynamicLayout.INVALID_BLOCK_INDEX
1742 || mTextRenderNodes[blockIndex] == null
1743 || mTextRenderNodes[blockIndex].needsToBeShifted) {
1744 startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas,
1745 layout, highlight, highlightPaint, cursorOffsetVertical,
1746 blockEndLines, blockIndices, block, numberOfBlocks,
1747 startIndexToFindAvailableRenderNode);
1748 }
1749 }
1750 }
Sangkyu Lee955beb22012-12-10 15:47:00 +09001751
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001752 dynamicLayout.setIndexFirstChangedBlock(lastIndex);
Gilles Debunned88876a2012-03-16 17:34:04 -07001753 } else {
1754 // Boring layout is used for empty and hint text
1755 layout.drawText(canvas, firstLine, lastLine);
1756 }
1757 }
1758
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001759 private int drawHardwareAcceleratedInner(Canvas canvas, Layout layout, Path highlight,
1760 Paint highlightPaint, int cursorOffsetVertical, int[] blockEndLines,
1761 int[] blockIndices, int blockInfoIndex, int numberOfBlocks,
1762 int startIndexToFindAvailableRenderNode) {
1763 final int blockEndLine = blockEndLines[blockInfoIndex];
1764 int blockIndex = blockIndices[blockInfoIndex];
1765
1766 final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
1767 if (blockIsInvalid) {
1768 blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
1769 startIndexToFindAvailableRenderNode);
1770 // Note how dynamic layout's internal block indices get updated from Editor
1771 blockIndices[blockInfoIndex] = blockIndex;
1772 if (mTextRenderNodes[blockIndex] != null) {
1773 mTextRenderNodes[blockIndex].isDirty = true;
1774 }
1775 startIndexToFindAvailableRenderNode = blockIndex + 1;
1776 }
1777
1778 if (mTextRenderNodes[blockIndex] == null) {
1779 mTextRenderNodes[blockIndex] = new TextRenderNode("Text " + blockIndex);
1780 }
1781
1782 final boolean blockDisplayListIsInvalid = mTextRenderNodes[blockIndex].needsRecord();
1783 RenderNode blockDisplayList = mTextRenderNodes[blockIndex].renderNode;
1784 if (mTextRenderNodes[blockIndex].needsToBeShifted || blockDisplayListIsInvalid) {
1785 final int blockBeginLine = blockInfoIndex == 0 ?
1786 0 : blockEndLines[blockInfoIndex - 1] + 1;
1787 final int top = layout.getLineTop(blockBeginLine);
1788 final int bottom = layout.getLineBottom(blockEndLine);
1789 int left = 0;
1790 int right = mTextView.getWidth();
1791 if (mTextView.getHorizontallyScrolling()) {
1792 float min = Float.MAX_VALUE;
1793 float max = Float.MIN_VALUE;
1794 for (int line = blockBeginLine; line <= blockEndLine; line++) {
1795 min = Math.min(min, layout.getLineLeft(line));
1796 max = Math.max(max, layout.getLineRight(line));
1797 }
1798 left = (int) min;
1799 right = (int) (max + 0.5f);
1800 }
1801
1802 // Rebuild display list if it is invalid
1803 if (blockDisplayListIsInvalid) {
1804 final DisplayListCanvas displayListCanvas = blockDisplayList.start(
1805 right - left, bottom - top);
1806 try {
1807 // drawText is always relative to TextView's origin, this translation
1808 // brings this range of text back to the top left corner of the viewport
1809 displayListCanvas.translate(-left, -top);
1810 layout.drawText(displayListCanvas, blockBeginLine, blockEndLine);
1811 mTextRenderNodes[blockIndex].isDirty = false;
1812 // No need to untranslate, previous context is popped after
1813 // drawDisplayList
1814 } finally {
1815 blockDisplayList.end(displayListCanvas);
1816 // Same as drawDisplayList below, handled by our TextView's parent
1817 blockDisplayList.setClipToBounds(false);
1818 }
1819 }
1820
1821 // Valid display list only needs to update its drawing location.
1822 blockDisplayList.setLeftTopRightBottom(left, top, right, bottom);
1823 mTextRenderNodes[blockIndex].needsToBeShifted = false;
1824 }
1825 ((DisplayListCanvas) canvas).drawRenderNode(blockDisplayList);
1826 return startIndexToFindAvailableRenderNode;
1827 }
1828
Gilles Debunned88876a2012-03-16 17:34:04 -07001829 private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
1830 int searchStartIndex) {
Chris Craik956f3402015-04-27 16:41:00 -07001831 int length = mTextRenderNodes.length;
Gilles Debunned88876a2012-03-16 17:34:04 -07001832 for (int i = searchStartIndex; i < length; i++) {
1833 boolean blockIndexFound = false;
1834 for (int j = 0; j < numberOfBlocks; j++) {
1835 if (blockIndices[j] == i) {
1836 blockIndexFound = true;
1837 break;
1838 }
1839 }
1840 if (blockIndexFound) continue;
1841 return i;
1842 }
1843
1844 // No available index found, the pool has to grow
Chris Craik956f3402015-04-27 16:41:00 -07001845 mTextRenderNodes = GrowingArrayUtils.append(mTextRenderNodes, length, null);
Gilles Debunned88876a2012-03-16 17:34:04 -07001846 return length;
1847 }
1848
1849 private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
1850 final boolean translate = cursorOffsetVertical != 0;
1851 if (translate) canvas.translate(0, cursorOffsetVertical);
1852 for (int i = 0; i < mCursorCount; i++) {
1853 mCursorDrawable[i].draw(canvas);
1854 }
1855 if (translate) canvas.translate(0, -cursorOffsetVertical);
1856 }
1857
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09001858 void invalidateHandlesAndActionMode() {
1859 if (mSelectionModifierCursorController != null) {
1860 mSelectionModifierCursorController.invalidateHandles();
1861 }
1862 if (mInsertionPointCursorController != null) {
1863 mInsertionPointCursorController.invalidateHandle();
1864 }
1865 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01001866 invalidateActionMode();
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09001867 }
1868 }
1869
Gilles Debunneebc86af2012-04-20 15:10:47 -07001870 /**
1871 * Invalidates all the sub-display lists that overlap the specified character range
1872 */
1873 void invalidateTextDisplayList(Layout layout, int start, int end) {
Chris Craik956f3402015-04-27 16:41:00 -07001874 if (mTextRenderNodes != null && layout instanceof DynamicLayout) {
Gilles Debunneebc86af2012-04-20 15:10:47 -07001875 final int firstLine = layout.getLineForOffset(start);
1876 final int lastLine = layout.getLineForOffset(end);
1877
1878 DynamicLayout dynamicLayout = (DynamicLayout) layout;
1879 int[] blockEndLines = dynamicLayout.getBlockEndLines();
1880 int[] blockIndices = dynamicLayout.getBlockIndices();
1881 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
1882
1883 int i = 0;
1884 // Skip the blocks before firstLine
1885 while (i < numberOfBlocks) {
1886 if (blockEndLines[i] >= firstLine) break;
1887 i++;
1888 }
1889
1890 // Invalidate all subsequent blocks until lastLine is passed
1891 while (i < numberOfBlocks) {
1892 final int blockIndex = blockIndices[i];
1893 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX) {
Chris Craik956f3402015-04-27 16:41:00 -07001894 mTextRenderNodes[blockIndex].isDirty = true;
Gilles Debunneebc86af2012-04-20 15:10:47 -07001895 }
1896 if (blockEndLines[i] >= lastLine) break;
1897 i++;
1898 }
1899 }
1900 }
1901
Gilles Debunned88876a2012-03-16 17:34:04 -07001902 void invalidateTextDisplayList() {
Chris Craik956f3402015-04-27 16:41:00 -07001903 if (mTextRenderNodes != null) {
1904 for (int i = 0; i < mTextRenderNodes.length; i++) {
1905 if (mTextRenderNodes[i] != null) mTextRenderNodes[i].isDirty = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001906 }
1907 }
1908 }
1909
1910 void updateCursorsPositions() {
1911 if (mTextView.mCursorDrawableRes == 0) {
1912 mCursorCount = 0;
1913 return;
1914 }
1915
Siyamed Sinir987ec652016-02-17 19:44:41 -08001916 Layout layout = mTextView.getLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -07001917 final int offset = mTextView.getSelectionStart();
1918 final int line = layout.getLineForOffset(offset);
1919 final int top = layout.getLineTop(line);
1920 final int bottom = layout.getLineTop(line + 1);
1921
1922 mCursorCount = layout.isLevelBoundary(offset) ? 2 : 1;
1923
1924 int middle = bottom;
1925 if (mCursorCount == 2) {
1926 // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)}
1927 middle = (top + bottom) >> 1;
1928 }
1929
Raph Levienafe8e9b2012-12-19 16:09:32 -08001930 boolean clamped = layout.shouldClampCursor(line);
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07001931 updateCursorPosition(0, top, middle, layout.getPrimaryHorizontal(offset, clamped));
Gilles Debunned88876a2012-03-16 17:34:04 -07001932
1933 if (mCursorCount == 2) {
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07001934 updateCursorPosition(1, middle, bottom, layout.getSecondaryHorizontal(offset, clamped));
Gilles Debunned88876a2012-03-16 17:34:04 -07001935 }
1936 }
1937
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001938 void refreshTextActionMode() {
1939 if (extractedTextModeWillBeStarted()) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001940 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001941 return;
1942 }
1943 final boolean hasSelection = mTextView.hasSelection();
1944 final SelectionModifierCursorController selectionController = getSelectionController();
1945 final InsertionPointCursorController insertionController = getInsertionController();
1946 if ((selectionController != null && selectionController.isCursorBeingModified())
1947 || (insertionController != null && insertionController.isCursorBeingModified())) {
1948 // ActionMode should be managed by the currently active cursor controller.
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001949 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001950 return;
1951 }
1952 if (hasSelection) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001953 hideInsertionPointCursorController();
1954 if (mTextActionMode == null) {
Keisuke Kuroyanagi0fd28c92016-04-04 17:43:06 +09001955 if (mRestartActionModeOnNextRefresh) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001956 // To avoid distraction, newly start action mode only when selection action
Keisuke Kuroyanagi0fd28c92016-04-04 17:43:06 +09001957 // mode is being restarted.
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01001958 startSelectionActionModeAsync(false);
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001959 }
1960 } else if (selectionController == null || !selectionController.isActive()) {
1961 // Insertion action mode is active. Avoid dismissing the selection.
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001962 stopTextActionModeWithPreservingSelection();
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01001963 startSelectionActionModeAsync(false);
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001964 } else {
1965 mTextActionMode.invalidateContentRect();
1966 }
1967 } else {
1968 // Insertion action mode is started only when insertion controller is explicitly
1969 // activated.
1970 if (insertionController == null || !insertionController.isActive()) {
1971 stopTextActionMode();
1972 } else if (mTextActionMode != null) {
1973 mTextActionMode.invalidateContentRect();
1974 }
1975 }
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001976 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001977 }
1978
Gilles Debunned88876a2012-03-16 17:34:04 -07001979 /**
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001980 * Start an Insertion action mode.
Gilles Debunned88876a2012-03-16 17:34:04 -07001981 */
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001982 void startInsertionActionMode() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001983 if (mInsertionActionModeRunnable != null) {
1984 mTextView.removeCallbacks(mInsertionActionModeRunnable);
1985 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01001986 if (extractedTextModeWillBeStarted()) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001987 return;
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01001988 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001989 stopTextActionMode();
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01001990
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001991 ActionMode.Callback actionModeCallback =
1992 new TextActionModeCallback(false /* hasSelection */);
1993 mTextActionMode = mTextView.startActionMode(
Clara Bayarrib8ed5b72015-04-09 15:26:41 +01001994 actionModeCallback, ActionMode.TYPE_FLOATING);
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001995 if (mTextActionMode != null && getInsertionController() != null) {
1996 getInsertionController().show();
1997 }
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00001998 }
1999
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002000 @NonNull
2001 TextView getTextView() {
2002 return mTextView;
2003 }
2004
2005 @Nullable
2006 ActionMode getTextActionMode() {
2007 return mTextActionMode;
2008 }
2009
2010 void setRestartActionModeOnNextRefresh(boolean value) {
2011 mRestartActionModeOnNextRefresh = value;
2012 }
2013
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00002014 /**
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002015 * Asynchronously starts a selection action mode using the TextClassifier.
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00002016 */
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002017 void startSelectionActionModeAsync(boolean adjustSelection) {
2018 getSelectionActionModeHelper().startActionModeAsync(adjustSelection);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002019 }
2020
2021 /**
2022 * Asynchronously invalidates an action mode using the TextClassifier.
2023 */
Abodunrinwa Toki4ce651e2017-05-12 15:37:29 +01002024 void invalidateActionModeAsync() {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002025 getSelectionActionModeHelper().invalidateActionModeAsync();
2026 }
2027
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002028 /**
2029 * Synchronously invalidates an action mode without the TextClassifier.
2030 */
2031 private void invalidateActionMode() {
2032 if (mTextActionMode != null) {
2033 mTextActionMode.invalidate();
2034 }
2035 }
2036
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002037 private SelectionActionModeHelper getSelectionActionModeHelper() {
2038 if (mSelectionActionModeHelper == null) {
2039 mSelectionActionModeHelper = new SelectionActionModeHelper(this);
Clara Bayarri578286f2015-04-10 15:35:31 +01002040 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002041 return mSelectionActionModeHelper;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00002042 }
2043
Clara Bayarridfac4432015-05-15 12:18:24 +01002044 /**
2045 * If the TextView allows text selection, selects the current word when no existing selection
2046 * was available and starts a drag.
2047 *
2048 * @return true if the drag was started.
2049 */
2050 private boolean selectCurrentWordAndStartDrag() {
Clara Bayarri7184c8a2015-06-05 17:34:09 +01002051 if (mInsertionActionModeRunnable != null) {
2052 mTextView.removeCallbacks(mInsertionActionModeRunnable);
2053 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002054 if (extractedTextModeWillBeStarted()) {
Clara Bayarridfac4432015-05-15 12:18:24 +01002055 return false;
2056 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002057 if (!checkField()) {
Clara Bayarridfac4432015-05-15 12:18:24 +01002058 return false;
2059 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002060 if (!mTextView.hasSelection() && !selectCurrentWord()) {
2061 // No selection and cannot select a word.
2062 return false;
2063 }
2064 stopTextActionModeWithPreservingSelection();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08002065 getSelectionController().enterDrag(
2066 SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_WORD);
Clara Bayarridfac4432015-05-15 12:18:24 +01002067 return true;
2068 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002069
Clara Bayarridfac4432015-05-15 12:18:24 +01002070 /**
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002071 * Checks whether a selection can be performed on the current TextView.
Clara Bayarridfac4432015-05-15 12:18:24 +01002072 *
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002073 * @return true if a selection can be performed
Clara Bayarridfac4432015-05-15 12:18:24 +01002074 */
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002075 boolean checkField() {
Clara Bayarridfac4432015-05-15 12:18:24 +01002076 if (!mTextView.canSelectText() || !mTextView.requestFocus()) {
2077 Log.w(TextView.LOG_TAG,
2078 "TextView does not support text selection. Selection cancelled.");
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002079 return false;
2080 }
Clara Bayarridfac4432015-05-15 12:18:24 +01002081 return true;
2082 }
2083
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002084 boolean startSelectionActionModeInternal() {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002085 if (extractedTextModeWillBeStarted()) {
2086 return false;
2087 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002088 if (mTextActionMode != null) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002089 // Text action mode is already started
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002090 invalidateActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07002091 return false;
2092 }
2093
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002094 if (!checkField() || !mTextView.hasSelection()) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002095 return false;
2096 }
2097
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002098 ActionMode.Callback actionModeCallback =
2099 new TextActionModeCallback(true /* hasSelection */);
2100 mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING);
Gilles Debunned88876a2012-03-16 17:34:04 -07002101
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002102 final boolean selectionStarted = mTextActionMode != null;
Gilles Debunne3473b2b2012-04-20 16:21:10 -07002103 if (selectionStarted && !mTextView.isTextSelectable() && mShowSoftInputOnFocus) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002104 // Show the IME to be able to replace text, except when selecting non editable text.
2105 final InputMethodManager imm = InputMethodManager.peekInstance();
2106 if (imm != null) {
2107 imm.showSoftInput(mTextView, 0, null);
2108 }
2109 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002110 return selectionStarted;
2111 }
2112
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09002113 private boolean extractedTextModeWillBeStarted() {
Andrei Stingaceanub1891b32015-06-19 16:44:37 +01002114 if (!(mTextView.isInExtractedMode())) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002115 final InputMethodManager imm = InputMethodManager.peekInstance();
2116 return imm != null && imm.isFullscreenMode();
2117 }
2118 return false;
2119 }
2120
2121 /**
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002122 * @return <code>true</code> if it's reasonable to offer to show suggestions depending on
2123 * the current cursor position or selection range. This method is consistent with the
2124 * method to show suggestions {@link SuggestionsPopupWindow#updateSuggestions}.
Gilles Debunned88876a2012-03-16 17:34:04 -07002125 */
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002126 private boolean shouldOfferToShowSuggestions() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002127 CharSequence text = mTextView.getText();
2128 if (!(text instanceof Spannable)) return false;
2129
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002130 final Spannable spannable = (Spannable) text;
2131 final int selectionStart = mTextView.getSelectionStart();
2132 final int selectionEnd = mTextView.getSelectionEnd();
2133 final SuggestionSpan[] suggestionSpans = spannable.getSpans(selectionStart, selectionEnd,
2134 SuggestionSpan.class);
2135 if (suggestionSpans.length == 0) {
2136 return false;
2137 }
2138 if (selectionStart == selectionEnd) {
2139 // Spans overlap the cursor.
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002140 for (int i = 0; i < suggestionSpans.length; i++) {
2141 if (suggestionSpans[i].getSuggestions().length > 0) {
2142 return true;
2143 }
2144 }
2145 return false;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002146 }
2147 int minSpanStart = mTextView.getText().length();
2148 int maxSpanEnd = 0;
2149 int unionOfSpansCoveringSelectionStartStart = mTextView.getText().length();
2150 int unionOfSpansCoveringSelectionStartEnd = 0;
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002151 boolean hasValidSuggestions = false;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002152 for (int i = 0; i < suggestionSpans.length; i++) {
2153 final int spanStart = spannable.getSpanStart(suggestionSpans[i]);
2154 final int spanEnd = spannable.getSpanEnd(suggestionSpans[i]);
2155 minSpanStart = Math.min(minSpanStart, spanStart);
2156 maxSpanEnd = Math.max(maxSpanEnd, spanEnd);
2157 if (selectionStart < spanStart || selectionStart > spanEnd) {
2158 // The span doesn't cover the current selection start point.
2159 continue;
2160 }
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002161 hasValidSuggestions =
2162 hasValidSuggestions || suggestionSpans[i].getSuggestions().length > 0;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002163 unionOfSpansCoveringSelectionStartStart =
2164 Math.min(unionOfSpansCoveringSelectionStartStart, spanStart);
2165 unionOfSpansCoveringSelectionStartEnd =
2166 Math.max(unionOfSpansCoveringSelectionStartEnd, spanEnd);
2167 }
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002168 if (!hasValidSuggestions) {
2169 return false;
2170 }
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002171 if (unionOfSpansCoveringSelectionStartStart >= unionOfSpansCoveringSelectionStartEnd) {
2172 // No spans cover the selection start point.
2173 return false;
2174 }
2175 if (minSpanStart < unionOfSpansCoveringSelectionStartStart
2176 || maxSpanEnd > unionOfSpansCoveringSelectionStartEnd) {
2177 // There is a span that is not covered by the union. In this case, we soouldn't offer
2178 // to show suggestions as it's confusing.
2179 return false;
2180 }
2181 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07002182 }
2183
2184 /**
2185 * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
2186 * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
2187 */
2188 private boolean isCursorInsideEasyCorrectionSpan() {
2189 Spannable spannable = (Spannable) mTextView.getText();
2190 SuggestionSpan[] suggestionSpans = spannable.getSpans(mTextView.getSelectionStart(),
2191 mTextView.getSelectionEnd(), SuggestionSpan.class);
2192 for (int i = 0; i < suggestionSpans.length; i++) {
2193 if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
2194 return true;
2195 }
2196 }
2197 return false;
2198 }
2199
2200 void onTouchUpEvent(MotionEvent event) {
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +01002201 if (getSelectionActionModeHelper().resetSelection(
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +00002202 getTextView().getOffsetForPosition(event.getX(), event.getY()))) {
2203 return;
2204 }
2205
Gilles Debunned88876a2012-03-16 17:34:04 -07002206 boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect();
Mady Mellora2861452015-06-25 08:40:27 -07002207 hideCursorAndSpanControllers();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002208 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07002209 CharSequence text = mTextView.getText();
2210 if (!selectAllGotFocus && text.length() > 0) {
2211 // Move cursor
2212 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
2213 Selection.setSelection((Spannable) text, offset);
2214 if (mSpellChecker != null) {
2215 // When the cursor moves, the word that was typed may need spell check
2216 mSpellChecker.onSelectionChanged();
2217 }
Andrei Stingaceanu35c550c2015-05-07 16:49:49 +01002218
Gilles Debunned88876a2012-03-16 17:34:04 -07002219 if (!extractedTextModeWillBeStarted()) {
2220 if (isCursorInsideEasyCorrectionSpan()) {
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01002221 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002222 if (mInsertionActionModeRunnable != null) {
2223 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01002224 }
2225
Gilles Debunned88876a2012-03-16 17:34:04 -07002226 mShowSuggestionRunnable = new Runnable() {
2227 public void run() {
Keisuke Kuroyanagi713be062016-02-29 16:07:54 -08002228 replace();
Gilles Debunned88876a2012-03-16 17:34:04 -07002229 }
2230 };
2231 // removeCallbacks is performed on every touch
2232 mTextView.postDelayed(mShowSuggestionRunnable,
2233 ViewConfiguration.getDoubleTapTimeout());
2234 } else if (hasInsertionController()) {
2235 getInsertionController().show();
2236 }
2237 }
2238 }
2239 }
2240
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002241 protected void stopTextActionMode() {
2242 if (mTextActionMode != null) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002243 // This will hide the mSelectionModifierCursorController
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002244 mTextActionMode.finish();
Gilles Debunned88876a2012-03-16 17:34:04 -07002245 }
2246 }
2247
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002248 private void stopTextActionModeWithPreservingSelection() {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002249 if (mTextActionMode != null) {
2250 mRestartActionModeOnNextRefresh = true;
2251 }
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002252 mPreserveSelection = true;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002253 stopTextActionMode();
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002254 mPreserveSelection = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002255 }
2256
Gilles Debunned88876a2012-03-16 17:34:04 -07002257 /**
2258 * @return True if this view supports insertion handles.
2259 */
2260 boolean hasInsertionController() {
2261 return mInsertionControllerEnabled;
2262 }
2263
2264 /**
2265 * @return True if this view supports selection handles.
2266 */
2267 boolean hasSelectionController() {
2268 return mSelectionControllerEnabled;
2269 }
2270
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09002271 private InsertionPointCursorController getInsertionController() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002272 if (!mInsertionControllerEnabled) {
2273 return null;
2274 }
2275
2276 if (mInsertionPointCursorController == null) {
2277 mInsertionPointCursorController = new InsertionPointCursorController();
2278
2279 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
2280 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
2281 }
2282
2283 return mInsertionPointCursorController;
2284 }
2285
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002286 @Nullable
2287 SelectionModifierCursorController getSelectionController() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002288 if (!mSelectionControllerEnabled) {
2289 return null;
2290 }
2291
2292 if (mSelectionModifierCursorController == null) {
2293 mSelectionModifierCursorController = new SelectionModifierCursorController();
2294
2295 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
2296 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
2297 }
2298
2299 return mSelectionModifierCursorController;
2300 }
2301
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002302 @VisibleForTesting
2303 public Drawable[] getCursorDrawable() {
2304 return mCursorDrawable;
2305 }
2306
Gilles Debunned88876a2012-03-16 17:34:04 -07002307 private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002308 if (mCursorDrawable[cursorIndex] == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08002309 mCursorDrawable[cursorIndex] = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07002310 mTextView.mCursorDrawableRes);
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002311 }
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002312 final Drawable drawable = mCursorDrawable[cursorIndex];
Siyamed Sinir987ec652016-02-17 19:44:41 -08002313 final int left = clampHorizontalPosition(drawable, horizontal);
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002314 final int width = drawable.getIntrinsicWidth();
2315 drawable.setBounds(left, top - mTempRect.top, left + width,
Gilles Debunned88876a2012-03-16 17:34:04 -07002316 bottom + mTempRect.bottom);
2317 }
2318
2319 /**
Siyamed Sinir987ec652016-02-17 19:44:41 -08002320 * Return clamped position for the drawable. If the drawable is within the boundaries of the
2321 * 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 -08002322 * 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 -08002323 * the view boundary. If the drawable is null, horizontal parameter is aligned to left or right
2324 * of the view.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002325 *
Siyamed Sinir987ec652016-02-17 19:44:41 -08002326 * @param drawable Drawable. Can be null.
2327 * @param horizontal Horizontal position for the drawable.
2328 * @return The clamped horizontal position for the drawable.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002329 */
Siyamed Sinir987ec652016-02-17 19:44:41 -08002330 private int clampHorizontalPosition(@Nullable final Drawable drawable, float horizontal) {
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002331 horizontal = Math.max(0.5f, horizontal - 0.5f);
2332 if (mTempRect == null) mTempRect = new Rect();
Siyamed Sinir987ec652016-02-17 19:44:41 -08002333
2334 int drawableWidth = 0;
2335 if (drawable != null) {
2336 drawable.getPadding(mTempRect);
2337 drawableWidth = drawable.getIntrinsicWidth();
2338 } else {
2339 mTempRect.setEmpty();
2340 }
2341
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002342 int scrollX = mTextView.getScrollX();
2343 float horizontalDiff = horizontal - scrollX;
2344 int viewClippedWidth = mTextView.getWidth() - mTextView.getCompoundPaddingLeft()
2345 - mTextView.getCompoundPaddingRight();
2346
2347 final int left;
2348 if (horizontalDiff >= (viewClippedWidth - 1f)) {
2349 // at the rightmost position
Siyamed Sinir987ec652016-02-17 19:44:41 -08002350 left = viewClippedWidth + scrollX - (drawableWidth - mTempRect.right);
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002351 } else if (Math.abs(horizontalDiff) <= 1f
2352 || (TextUtils.isEmpty(mTextView.getText())
Siyamed Sinir987ec652016-02-17 19:44:41 -08002353 && (TextView.VERY_WIDE - scrollX) <= (viewClippedWidth + 1f)
2354 && horizontal <= 1f)) {
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002355 // at the leftmost position
2356 left = scrollX - mTempRect.left;
2357 } else {
2358 left = (int) horizontal - mTempRect.left;
2359 }
2360 return left;
2361 }
2362
2363 /**
Gilles Debunned88876a2012-03-16 17:34:04 -07002364 * 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 -08002365 * a dictionary) from the current input method, provided by it calling
Gilles Debunned88876a2012-03-16 17:34:04 -07002366 * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
2367 * implementation flashes the background of the corrected word to provide feedback to the user.
2368 *
2369 * @param info The auto correct info about the text that was corrected.
2370 */
2371 public void onCommitCorrection(CorrectionInfo info) {
2372 if (mCorrectionHighlighter == null) {
2373 mCorrectionHighlighter = new CorrectionHighlighter();
2374 } else {
2375 mCorrectionHighlighter.invalidate(false);
2376 }
2377
2378 mCorrectionHighlighter.highlight(info);
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002379 mUndoInputFilter.freezeLastEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07002380 }
2381
Gilles Debunned88876a2012-03-16 17:34:04 -07002382 void onScrollChanged() {
Gilles Debunne157aafc2012-04-19 17:21:57 -07002383 if (mPositionListener != null) {
2384 mPositionListener.onScrollChanged();
2385 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002386 if (mTextActionMode != null) {
2387 mTextActionMode.invalidateContentRect();
Abodunrinwa Toki56195db2015-04-22 06:46:54 +01002388 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002389 }
2390
2391 /**
2392 * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
2393 */
2394 private boolean shouldBlink() {
2395 if (!isCursorVisible() || !mTextView.isFocused()) return false;
2396
2397 final int start = mTextView.getSelectionStart();
2398 if (start < 0) return false;
2399
2400 final int end = mTextView.getSelectionEnd();
2401 if (end < 0) return false;
2402
2403 return start == end;
2404 }
2405
2406 void makeBlink() {
2407 if (shouldBlink()) {
2408 mShowCursor = SystemClock.uptimeMillis();
2409 if (mBlink == null) mBlink = new Blink();
John Reckd0374c62015-10-20 13:25:01 -07002410 mTextView.removeCallbacks(mBlink);
2411 mTextView.postDelayed(mBlink, BLINK);
Gilles Debunned88876a2012-03-16 17:34:04 -07002412 } else {
John Reckd0374c62015-10-20 13:25:01 -07002413 if (mBlink != null) mTextView.removeCallbacks(mBlink);
Gilles Debunned88876a2012-03-16 17:34:04 -07002414 }
2415 }
2416
John Reckd0374c62015-10-20 13:25:01 -07002417 private class Blink implements Runnable {
Gilles Debunned88876a2012-03-16 17:34:04 -07002418 private boolean mCancelled;
2419
2420 public void run() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002421 if (mCancelled) {
2422 return;
2423 }
2424
John Reckd0374c62015-10-20 13:25:01 -07002425 mTextView.removeCallbacks(this);
Gilles Debunned88876a2012-03-16 17:34:04 -07002426
2427 if (shouldBlink()) {
2428 if (mTextView.getLayout() != null) {
2429 mTextView.invalidateCursorPath();
2430 }
2431
John Reckd0374c62015-10-20 13:25:01 -07002432 mTextView.postDelayed(this, BLINK);
Gilles Debunned88876a2012-03-16 17:34:04 -07002433 }
2434 }
2435
2436 void cancel() {
2437 if (!mCancelled) {
John Reckd0374c62015-10-20 13:25:01 -07002438 mTextView.removeCallbacks(this);
Gilles Debunned88876a2012-03-16 17:34:04 -07002439 mCancelled = true;
2440 }
2441 }
2442
2443 void uncancel() {
2444 mCancelled = false;
2445 }
2446 }
2447
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002448 private DragShadowBuilder getTextThumbnailBuilder(int start, int end) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002449 TextView shadowView = (TextView) View.inflate(mTextView.getContext(),
2450 com.android.internal.R.layout.text_drag_thumbnail, null);
2451
2452 if (shadowView == null) {
2453 throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
2454 }
2455
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002456 if (end - start > DRAG_SHADOW_MAX_TEXT_LENGTH) {
2457 final long range = getCharClusterRange(start + DRAG_SHADOW_MAX_TEXT_LENGTH);
2458 end = TextUtils.unpackRangeEndFromLong(range);
Gilles Debunned88876a2012-03-16 17:34:04 -07002459 }
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002460 final CharSequence text = mTextView.getTransformedText(start, end);
Gilles Debunned88876a2012-03-16 17:34:04 -07002461 shadowView.setText(text);
2462 shadowView.setTextColor(mTextView.getTextColors());
2463
Alan Viverettebb98ebd2015-05-08 17:17:44 -07002464 shadowView.setTextAppearance(R.styleable.Theme_textAppearanceLarge);
Gilles Debunned88876a2012-03-16 17:34:04 -07002465 shadowView.setGravity(Gravity.CENTER);
2466
2467 shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
2468 ViewGroup.LayoutParams.WRAP_CONTENT));
2469
2470 final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
2471 shadowView.measure(size, size);
2472
2473 shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
2474 shadowView.invalidate();
2475 return new DragShadowBuilder(shadowView);
2476 }
2477
2478 private static class DragLocalState {
2479 public TextView sourceTextView;
2480 public int start, end;
2481
2482 public DragLocalState(TextView sourceTextView, int start, int end) {
2483 this.sourceTextView = sourceTextView;
2484 this.start = start;
2485 this.end = end;
2486 }
2487 }
2488
2489 void onDrop(DragEvent event) {
Ben Murdoch3dac4602017-01-17 11:27:37 +00002490 SpannableStringBuilder content = new SpannableStringBuilder();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002491
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -07002492 final DragAndDropPermissions permissions = DragAndDropPermissions.obtain(event);
2493 if (permissions != null) {
2494 permissions.takeTransient();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002495 }
2496
2497 try {
2498 ClipData clipData = event.getClipData();
2499 final int itemCount = clipData.getItemCount();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002500 for (int i = 0; i < itemCount; i++) {
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002501 Item item = clipData.getItemAt(i);
2502 content.append(item.coerceToStyledText(mTextView.getContext()));
2503 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002504 } finally {
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -07002505 if (permissions != null) {
2506 permissions.release();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002507 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002508 }
2509
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002510 mTextView.beginBatchEdit();
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002511 mUndoInputFilter.freezeLastEdit();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002512 try {
2513 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
2514 Object localState = event.getLocalState();
2515 DragLocalState dragLocalState = null;
2516 if (localState instanceof DragLocalState) {
2517 dragLocalState = (DragLocalState) localState;
Gilles Debunned88876a2012-03-16 17:34:04 -07002518 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002519 boolean dragDropIntoItself = dragLocalState != null
2520 && dragLocalState.sourceTextView == mTextView;
Gilles Debunned88876a2012-03-16 17:34:04 -07002521
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002522 if (dragDropIntoItself) {
2523 if (offset >= dragLocalState.start && offset < dragLocalState.end) {
2524 // A drop inside the original selection discards the drop.
2525 return;
2526 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002527 }
2528
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002529 final int originalLength = mTextView.getText().length();
2530 int min = offset;
2531 int max = offset;
2532
2533 Selection.setSelection((Spannable) mTextView.getText(), max);
2534 mTextView.replaceText_internal(min, max, content);
2535
2536 if (dragDropIntoItself) {
2537 int dragSourceStart = dragLocalState.start;
2538 int dragSourceEnd = dragLocalState.end;
2539 if (max <= dragSourceStart) {
2540 // Inserting text before selection has shifted positions
2541 final int shift = mTextView.getText().length() - originalLength;
2542 dragSourceStart += shift;
2543 dragSourceEnd += shift;
2544 }
2545
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08002546 // Delete original selection
2547 mTextView.deleteText_internal(dragSourceStart, dragSourceEnd);
Gilles Debunned88876a2012-03-16 17:34:04 -07002548
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08002549 // Make sure we do not leave two adjacent spaces.
2550 final int prevCharIdx = Math.max(0, dragSourceStart - 1);
2551 final int nextCharIdx = Math.min(mTextView.getText().length(), dragSourceStart + 1);
2552 if (nextCharIdx > prevCharIdx + 1) {
2553 CharSequence t = mTextView.getTransformedText(prevCharIdx, nextCharIdx);
2554 if (Character.isSpaceChar(t.charAt(0)) && Character.isSpaceChar(t.charAt(1))) {
2555 mTextView.deleteText_internal(prevCharIdx, prevCharIdx + 1);
2556 }
Victoria Lease91373202012-09-07 16:41:59 -07002557 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002558 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002559 } finally {
2560 mTextView.endBatchEdit();
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002561 mUndoInputFilter.freezeLastEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07002562 }
2563 }
2564
Gilles Debunnec62589c2012-04-12 14:50:23 -07002565 public void addSpanWatchers(Spannable text) {
2566 final int textLength = text.length();
2567
2568 if (mKeyListener != null) {
2569 text.setSpan(mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
2570 }
2571
Jean Chalardbaf30942013-02-28 16:01:51 -08002572 if (mSpanController == null) {
2573 mSpanController = new SpanController();
Gilles Debunnec62589c2012-04-12 14:50:23 -07002574 }
Jean Chalardbaf30942013-02-28 16:01:51 -08002575 text.setSpan(mSpanController, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002576 }
2577
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002578 void setContextMenuAnchor(float x, float y) {
2579 mContextMenuAnchorX = x;
2580 mContextMenuAnchorY = y;
2581 }
2582
2583 void onCreateContextMenu(ContextMenu menu) {
2584 if (mIsBeingLongClicked || Float.isNaN(mContextMenuAnchorX)
2585 || Float.isNaN(mContextMenuAnchorY)) {
2586 return;
2587 }
2588 final int offset = mTextView.getOffsetForPosition(mContextMenuAnchorX, mContextMenuAnchorY);
2589 if (offset == -1) {
2590 return;
2591 }
Siyamed Sinir532f3c92017-06-15 18:22:31 -07002592
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002593 stopTextActionModeWithPreservingSelection();
Siyamed Sinir532f3c92017-06-15 18:22:31 -07002594 if (mTextView.canSelectText()) {
2595 final boolean isOnSelection = mTextView.hasSelection()
2596 && offset >= mTextView.getSelectionStart()
2597 && offset <= mTextView.getSelectionEnd();
2598 if (!isOnSelection) {
2599 // Right clicked position is not on the selection. Remove the selection and move the
2600 // cursor to the right clicked position.
2601 Selection.setSelection((Spannable) mTextView.getText(), offset);
2602 stopTextActionMode();
2603 }
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002604 }
2605
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002606 if (shouldOfferToShowSuggestions()) {
Keisuke Kuroyanagi182f5fe2016-03-11 16:31:29 +09002607 final SuggestionInfo[] suggestionInfoArray =
2608 new SuggestionInfo[SuggestionSpan.SUGGESTIONS_MAX_SIZE];
2609 for (int i = 0; i < suggestionInfoArray.length; i++) {
2610 suggestionInfoArray[i] = new SuggestionInfo();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002611 }
2612 final SubMenu subMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, MENU_ITEM_ORDER_REPLACE,
2613 com.android.internal.R.string.replace);
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002614 final int numItems = mSuggestionHelper.getSuggestionInfo(suggestionInfoArray, null);
Keisuke Kuroyanagi182f5fe2016-03-11 16:31:29 +09002615 for (int i = 0; i < numItems; i++) {
2616 final SuggestionInfo info = suggestionInfoArray[i];
2617 subMenu.add(Menu.NONE, Menu.NONE, i, info.mText)
2618 .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
2619 @Override
2620 public boolean onMenuItemClick(MenuItem item) {
2621 replaceWithSuggestion(info);
2622 return true;
2623 }
2624 });
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002625 }
2626 }
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002627
2628 menu.add(Menu.NONE, TextView.ID_UNDO, MENU_ITEM_ORDER_UNDO,
2629 com.android.internal.R.string.undo)
2630 .setAlphabeticShortcut('z')
2631 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2632 .setEnabled(mTextView.canUndo());
2633 menu.add(Menu.NONE, TextView.ID_REDO, MENU_ITEM_ORDER_REDO,
2634 com.android.internal.R.string.redo)
2635 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2636 .setEnabled(mTextView.canRedo());
2637
2638 menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
2639 com.android.internal.R.string.cut)
2640 .setAlphabeticShortcut('x')
2641 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2642 .setEnabled(mTextView.canCut());
2643 menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
2644 com.android.internal.R.string.copy)
2645 .setAlphabeticShortcut('c')
2646 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2647 .setEnabled(mTextView.canCopy());
2648 menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
2649 com.android.internal.R.string.paste)
2650 .setAlphabeticShortcut('v')
2651 .setEnabled(mTextView.canPaste())
2652 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01002653 menu.add(Menu.NONE, TextView.ID_PASTE_AS_PLAIN_TEXT, MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002654 com.android.internal.R.string.paste_as_plain_text)
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01002655 .setEnabled(mTextView.canPasteAsPlainText())
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002656 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2657 menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
2658 com.android.internal.R.string.share)
2659 .setEnabled(mTextView.canShare())
2660 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2661 menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
2662 com.android.internal.R.string.selectAll)
2663 .setAlphabeticShortcut('a')
2664 .setEnabled(mTextView.canSelectAllText())
2665 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Felipe Leme2ac463e2017-03-13 14:06:25 -07002666 menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
Felipe Leme555bcac2017-06-26 12:53:56 -07002667 android.R.string.autofill)
Felipe Leme2ac463e2017-03-13 14:06:25 -07002668 .setEnabled(mTextView.canRequestAutofill())
2669 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002670
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002671 mPreserveSelection = true;
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002672 }
2673
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002674 @Nullable
2675 private SuggestionSpan findEquivalentSuggestionSpan(
2676 @NonNull SuggestionSpanInfo suggestionSpanInfo) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002677 final Editable editable = (Editable) mTextView.getText();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002678 if (editable.getSpanStart(suggestionSpanInfo.mSuggestionSpan) >= 0) {
2679 // Exactly same span is found.
2680 return suggestionSpanInfo.mSuggestionSpan;
2681 }
2682 // Suggestion span couldn't be found. Try to find a suggestion span that has the same
2683 // contents.
2684 final SuggestionSpan[] suggestionSpans = editable.getSpans(suggestionSpanInfo.mSpanStart,
2685 suggestionSpanInfo.mSpanEnd, SuggestionSpan.class);
2686 for (final SuggestionSpan suggestionSpan : suggestionSpans) {
2687 final int start = editable.getSpanStart(suggestionSpan);
2688 if (start != suggestionSpanInfo.mSpanStart) {
2689 continue;
2690 }
2691 final int end = editable.getSpanEnd(suggestionSpan);
2692 if (end != suggestionSpanInfo.mSpanEnd) {
2693 continue;
2694 }
2695 if (suggestionSpan.equals(suggestionSpanInfo.mSuggestionSpan)) {
2696 return suggestionSpan;
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08002697 }
2698 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002699 return null;
2700 }
2701
2702 private void replaceWithSuggestion(@NonNull final SuggestionInfo suggestionInfo) {
2703 final SuggestionSpan targetSuggestionSpan = findEquivalentSuggestionSpan(
2704 suggestionInfo.mSuggestionSpanInfo);
2705 if (targetSuggestionSpan == null) {
2706 // Span has been removed
2707 return;
2708 }
2709 final Editable editable = (Editable) mTextView.getText();
2710 final int spanStart = editable.getSpanStart(targetSuggestionSpan);
2711 final int spanEnd = editable.getSpanEnd(targetSuggestionSpan);
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08002712 if (spanStart < 0 || spanEnd <= spanStart) {
2713 // Span has been removed
2714 return;
2715 }
2716
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002717 final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
2718 // SuggestionSpans are removed by replace: save them before
2719 SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
2720 SuggestionSpan.class);
2721 final int length = suggestionSpans.length;
2722 int[] suggestionSpansStarts = new int[length];
2723 int[] suggestionSpansEnds = new int[length];
2724 int[] suggestionSpansFlags = new int[length];
2725 for (int i = 0; i < length; i++) {
2726 final SuggestionSpan suggestionSpan = suggestionSpans[i];
2727 suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
2728 suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
2729 suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
2730
2731 // Remove potential misspelled flags
2732 int suggestionSpanFlags = suggestionSpan.getFlags();
2733 if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
2734 suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
2735 suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
2736 suggestionSpan.setFlags(suggestionSpanFlags);
2737 }
2738 }
2739
2740 // Notify source IME of the suggestion pick. Do this before swapping texts.
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002741 targetSuggestionSpan.notifySelection(
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002742 mTextView.getContext(), originalText, suggestionInfo.mSuggestionIndex);
2743
2744 // Swap text content between actual text and Suggestion span
2745 final int suggestionStart = suggestionInfo.mSuggestionStart;
2746 final int suggestionEnd = suggestionInfo.mSuggestionEnd;
2747 final String suggestion = suggestionInfo.mText.subSequence(
2748 suggestionStart, suggestionEnd).toString();
2749 mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
2750
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002751 String[] suggestions = targetSuggestionSpan.getSuggestions();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002752 suggestions[suggestionInfo.mSuggestionIndex] = originalText;
2753
2754 // Restore previous SuggestionSpans
2755 final int lengthDelta = suggestion.length() - (spanEnd - spanStart);
2756 for (int i = 0; i < length; i++) {
2757 // Only spans that include the modified region make sense after replacement
2758 // Spans partially included in the replaced region are removed, there is no
2759 // way to assign them a valid range after replacement
2760 if (suggestionSpansStarts[i] <= spanStart && suggestionSpansEnds[i] >= spanEnd) {
2761 mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
2762 suggestionSpansEnds[i] + lengthDelta, suggestionSpansFlags[i]);
2763 }
2764 }
2765 // Move cursor at the end of the replaced word
2766 final int newCursorPosition = spanEnd + lengthDelta;
2767 mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
2768 }
2769
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002770 private final MenuItem.OnMenuItemClickListener mOnContextMenuItemClickListener =
2771 new MenuItem.OnMenuItemClickListener() {
2772 @Override
2773 public boolean onMenuItemClick(MenuItem item) {
2774 if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
2775 return true;
2776 }
2777 return mTextView.onTextContextMenuItem(item.getItemId());
2778 }
2779 };
2780
Gilles Debunned88876a2012-03-16 17:34:04 -07002781 /**
2782 * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
2783 * pop-up should be displayed.
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07002784 * Also monitors {@link Selection} to call back to the attached input method.
Gilles Debunned88876a2012-03-16 17:34:04 -07002785 */
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09002786 private class SpanController implements SpanWatcher {
Gilles Debunned88876a2012-03-16 17:34:04 -07002787
2788 private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
2789
2790 private EasyEditPopupWindow mPopupWindow;
2791
Gilles Debunned88876a2012-03-16 17:34:04 -07002792 private Runnable mHidePopup;
2793
Jean Chalardbaf30942013-02-28 16:01:51 -08002794 // This function is pure but inner classes can't have static functions
2795 private boolean isNonIntermediateSelectionSpan(final Spannable text,
2796 final Object span) {
2797 return (Selection.SELECTION_START == span || Selection.SELECTION_END == span)
2798 && (text.getSpanFlags(span) & Spanned.SPAN_INTERMEDIATE) == 0;
2799 }
2800
Gilles Debunnec62589c2012-04-12 14:50:23 -07002801 @Override
2802 public void onSpanAdded(Spannable text, Object span, int start, int end) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002803 if (isNonIntermediateSelectionSpan(text, span)) {
2804 sendUpdateSelection();
2805 } else if (span instanceof EasyEditSpan) {
Gilles Debunnec62589c2012-04-12 14:50:23 -07002806 if (mPopupWindow == null) {
2807 mPopupWindow = new EasyEditPopupWindow();
2808 mHidePopup = new Runnable() {
2809 @Override
2810 public void run() {
2811 hide();
2812 }
2813 };
2814 }
2815
2816 // Make sure there is only at most one EasyEditSpan in the text
2817 if (mPopupWindow.mEasyEditSpan != null) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002818 mPopupWindow.mEasyEditSpan.setDeleteEnabled(false);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002819 }
2820
2821 mPopupWindow.setEasyEditSpan((EasyEditSpan) span);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002822 mPopupWindow.setOnDeleteListener(new EasyEditDeleteListener() {
2823 @Override
2824 public void onDeleteClick(EasyEditSpan span) {
2825 Editable editable = (Editable) mTextView.getText();
2826 int start = editable.getSpanStart(span);
2827 int end = editable.getSpanEnd(span);
2828 if (start >= 0 && end >= 0) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002829 sendEasySpanNotification(EasyEditSpan.TEXT_DELETED, span);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002830 mTextView.deleteText_internal(start, end);
2831 }
2832 editable.removeSpan(span);
2833 }
2834 });
Gilles Debunnec62589c2012-04-12 14:50:23 -07002835
2836 if (mTextView.getWindowVisibility() != View.VISIBLE) {
2837 // The window is not visible yet, ignore the text change.
2838 return;
2839 }
2840
2841 if (mTextView.getLayout() == null) {
2842 // The view has not been laid out yet, ignore the text change
2843 return;
2844 }
2845
2846 if (extractedTextModeWillBeStarted()) {
2847 // The input is in extract mode. Do not handle the easy edit in
2848 // the original TextView, as the ExtractEditText will do
2849 return;
2850 }
2851
2852 mPopupWindow.show();
2853 mTextView.removeCallbacks(mHidePopup);
2854 mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
2855 }
2856 }
2857
2858 @Override
2859 public void onSpanRemoved(Spannable text, Object span, int start, int end) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002860 if (isNonIntermediateSelectionSpan(text, span)) {
2861 sendUpdateSelection();
2862 } else if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
Gilles Debunnec62589c2012-04-12 14:50:23 -07002863 hide();
2864 }
2865 }
2866
2867 @Override
2868 public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd,
2869 int newStart, int newEnd) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002870 if (isNonIntermediateSelectionSpan(text, span)) {
2871 sendUpdateSelection();
2872 } else if (mPopupWindow != null && span instanceof EasyEditSpan) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002873 EasyEditSpan easyEditSpan = (EasyEditSpan) span;
Jean Chalardbaf30942013-02-28 16:01:51 -08002874 sendEasySpanNotification(EasyEditSpan.TEXT_MODIFIED, easyEditSpan);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002875 text.removeSpan(easyEditSpan);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002876 }
2877 }
2878
Gilles Debunned88876a2012-03-16 17:34:04 -07002879 public void hide() {
2880 if (mPopupWindow != null) {
2881 mPopupWindow.hide();
2882 mTextView.removeCallbacks(mHidePopup);
2883 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002884 }
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002885
Jean Chalardbaf30942013-02-28 16:01:51 -08002886 private void sendEasySpanNotification(int textChangedType, EasyEditSpan span) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002887 try {
2888 PendingIntent pendingIntent = span.getPendingIntent();
2889 if (pendingIntent != null) {
2890 Intent intent = new Intent();
2891 intent.putExtra(EasyEditSpan.EXTRA_TEXT_CHANGED_TYPE, textChangedType);
2892 pendingIntent.send(mTextView.getContext(), 0, intent);
2893 }
2894 } catch (CanceledException e) {
2895 // This should not happen, as we should try to send the intent only once.
2896 Log.w(TAG, "PendingIntent for notification cannot be sent", e);
2897 }
2898 }
2899 }
2900
2901 /**
2902 * Listens for the delete event triggered by {@link EasyEditPopupWindow}.
2903 */
2904 private interface EasyEditDeleteListener {
2905
2906 /**
2907 * Clicks the delete pop-up.
2908 */
2909 void onDeleteClick(EasyEditSpan span);
Gilles Debunned88876a2012-03-16 17:34:04 -07002910 }
2911
2912 /**
2913 * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07002914 * by {@link SpanController}.
Gilles Debunned88876a2012-03-16 17:34:04 -07002915 */
2916 private class EasyEditPopupWindow extends PinnedPopupWindow
2917 implements OnClickListener {
2918 private static final int POPUP_TEXT_LAYOUT =
2919 com.android.internal.R.layout.text_edit_action_popup_text;
2920 private TextView mDeleteTextView;
2921 private EasyEditSpan mEasyEditSpan;
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002922 private EasyEditDeleteListener mOnDeleteListener;
Gilles Debunned88876a2012-03-16 17:34:04 -07002923
2924 @Override
2925 protected void createPopupWindow() {
2926 mPopupWindow = new PopupWindow(mTextView.getContext(), null,
2927 com.android.internal.R.attr.textSelectHandleWindowStyle);
2928 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
2929 mPopupWindow.setClippingEnabled(true);
2930 }
2931
2932 @Override
2933 protected void initContentView() {
2934 LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
2935 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
2936 mContentView = linearLayout;
2937 mContentView.setBackgroundResource(
2938 com.android.internal.R.drawable.text_edit_side_paste_window);
2939
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002940 LayoutInflater inflater = (LayoutInflater) mTextView.getContext()
2941 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
Gilles Debunned88876a2012-03-16 17:34:04 -07002942
2943 LayoutParams wrapContent = new LayoutParams(
2944 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
2945
2946 mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
2947 mDeleteTextView.setLayoutParams(wrapContent);
2948 mDeleteTextView.setText(com.android.internal.R.string.delete);
2949 mDeleteTextView.setOnClickListener(this);
2950 mContentView.addView(mDeleteTextView);
2951 }
2952
Gilles Debunnec62589c2012-04-12 14:50:23 -07002953 public void setEasyEditSpan(EasyEditSpan easyEditSpan) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002954 mEasyEditSpan = easyEditSpan;
Gilles Debunned88876a2012-03-16 17:34:04 -07002955 }
2956
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002957 private void setOnDeleteListener(EasyEditDeleteListener listener) {
2958 mOnDeleteListener = listener;
2959 }
2960
Gilles Debunned88876a2012-03-16 17:34:04 -07002961 @Override
2962 public void onClick(View view) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002963 if (view == mDeleteTextView
2964 && mEasyEditSpan != null && mEasyEditSpan.isDeleteEnabled()
2965 && mOnDeleteListener != null) {
2966 mOnDeleteListener.onDeleteClick(mEasyEditSpan);
Gilles Debunned88876a2012-03-16 17:34:04 -07002967 }
2968 }
2969
2970 @Override
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002971 public void hide() {
2972 if (mEasyEditSpan != null) {
2973 mEasyEditSpan.setDeleteEnabled(false);
2974 }
2975 mOnDeleteListener = null;
2976 super.hide();
2977 }
2978
2979 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07002980 protected int getTextOffset() {
2981 // Place the pop-up at the end of the span
2982 Editable editable = (Editable) mTextView.getText();
2983 return editable.getSpanEnd(mEasyEditSpan);
2984 }
2985
2986 @Override
2987 protected int getVerticalLocalPosition(int line) {
2988 return mTextView.getLayout().getLineBottom(line);
2989 }
2990
2991 @Override
2992 protected int clipVertically(int positionY) {
2993 // As we display the pop-up below the span, no vertical clipping is required.
2994 return positionY;
2995 }
2996 }
2997
2998 private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
2999 // 3 handles
3000 // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003001 // 1 CursorAnchorInfoNotifier
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003002 private static final int MAXIMUM_NUMBER_OF_LISTENERS = 7;
Gilles Debunned88876a2012-03-16 17:34:04 -07003003 private TextViewPositionListener[] mPositionListeners =
3004 new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003005 private boolean[] mCanMove = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
Gilles Debunned88876a2012-03-16 17:34:04 -07003006 private boolean mPositionHasChanged = true;
3007 // Absolute position of the TextView with respect to its parent window
3008 private int mPositionX, mPositionY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003009 private int mPositionXOnScreen, mPositionYOnScreen;
Gilles Debunned88876a2012-03-16 17:34:04 -07003010 private int mNumberOfListeners;
3011 private boolean mScrollHasChanged;
3012 final int[] mTempCoords = new int[2];
3013
3014 public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
3015 if (mNumberOfListeners == 0) {
3016 updatePosition();
3017 ViewTreeObserver vto = mTextView.getViewTreeObserver();
3018 vto.addOnPreDrawListener(this);
3019 }
3020
3021 int emptySlotIndex = -1;
3022 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3023 TextViewPositionListener listener = mPositionListeners[i];
3024 if (listener == positionListener) {
3025 return;
3026 } else if (emptySlotIndex < 0 && listener == null) {
3027 emptySlotIndex = i;
3028 }
3029 }
3030
3031 mPositionListeners[emptySlotIndex] = positionListener;
3032 mCanMove[emptySlotIndex] = canMove;
3033 mNumberOfListeners++;
3034 }
3035
3036 public void removeSubscriber(TextViewPositionListener positionListener) {
3037 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3038 if (mPositionListeners[i] == positionListener) {
3039 mPositionListeners[i] = null;
3040 mNumberOfListeners--;
3041 break;
3042 }
3043 }
3044
3045 if (mNumberOfListeners == 0) {
3046 ViewTreeObserver vto = mTextView.getViewTreeObserver();
3047 vto.removeOnPreDrawListener(this);
3048 }
3049 }
3050
3051 public int getPositionX() {
3052 return mPositionX;
3053 }
3054
3055 public int getPositionY() {
3056 return mPositionY;
3057 }
3058
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003059 public int getPositionXOnScreen() {
3060 return mPositionXOnScreen;
3061 }
3062
3063 public int getPositionYOnScreen() {
3064 return mPositionYOnScreen;
3065 }
3066
Gilles Debunned88876a2012-03-16 17:34:04 -07003067 @Override
3068 public boolean onPreDraw() {
3069 updatePosition();
3070
3071 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3072 if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
3073 TextViewPositionListener positionListener = mPositionListeners[i];
3074 if (positionListener != null) {
3075 positionListener.updatePosition(mPositionX, mPositionY,
3076 mPositionHasChanged, mScrollHasChanged);
3077 }
3078 }
3079 }
3080
3081 mScrollHasChanged = false;
3082 return true;
3083 }
3084
3085 private void updatePosition() {
3086 mTextView.getLocationInWindow(mTempCoords);
3087
3088 mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
3089
3090 mPositionX = mTempCoords[0];
3091 mPositionY = mTempCoords[1];
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003092
3093 mTextView.getLocationOnScreen(mTempCoords);
3094
3095 mPositionXOnScreen = mTempCoords[0];
3096 mPositionYOnScreen = mTempCoords[1];
Gilles Debunned88876a2012-03-16 17:34:04 -07003097 }
3098
3099 public void onScrollChanged() {
3100 mScrollHasChanged = true;
3101 }
3102 }
3103
3104 private abstract class PinnedPopupWindow implements TextViewPositionListener {
3105 protected PopupWindow mPopupWindow;
3106 protected ViewGroup mContentView;
3107 int mPositionX, mPositionY;
Seigo Nonaka60490d12016-01-28 17:25:18 +09003108 int mClippingLimitLeft, mClippingLimitRight;
Gilles Debunned88876a2012-03-16 17:34:04 -07003109
3110 protected abstract void createPopupWindow();
3111 protected abstract void initContentView();
3112 protected abstract int getTextOffset();
3113 protected abstract int getVerticalLocalPosition(int line);
3114 protected abstract int clipVertically(int positionY);
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003115 protected void setUp() {
3116 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003117
3118 public PinnedPopupWindow() {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003119 // Due to calling subclass methods in base constructor, subclass constructor is not
3120 // called before subclass methods, e.g. createPopupWindow or initContentView. To give
3121 // a chance to initialize subclasses, call setUp() method here.
3122 // TODO: It is good to extract non trivial initialization code from constructor.
3123 setUp();
3124
Gilles Debunned88876a2012-03-16 17:34:04 -07003125 createPopupWindow();
3126
Alan Viverette80ebe0d2015-04-30 15:53:11 -07003127 mPopupWindow.setWindowLayoutType(
3128 WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
Gilles Debunned88876a2012-03-16 17:34:04 -07003129 mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
3130 mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
3131
3132 initContentView();
3133
3134 LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
3135 ViewGroup.LayoutParams.WRAP_CONTENT);
3136 mContentView.setLayoutParams(wrapContent);
3137
3138 mPopupWindow.setContentView(mContentView);
3139 }
3140
3141 public void show() {
3142 getPositionListener().addSubscriber(this, false /* offset is fixed */);
3143
3144 computeLocalPosition();
3145
3146 final PositionListener positionListener = getPositionListener();
3147 updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
3148 }
3149
3150 protected void measureContent() {
3151 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3152 mContentView.measure(
3153 View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
3154 View.MeasureSpec.AT_MOST),
3155 View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
3156 View.MeasureSpec.AT_MOST));
3157 }
3158
3159 /* The popup window will be horizontally centered on the getTextOffset() and vertically
3160 * positioned according to viewportToContentHorizontalOffset.
3161 *
3162 * This method assumes that mContentView has properly been measured from its content. */
3163 private void computeLocalPosition() {
3164 measureContent();
3165 final int width = mContentView.getMeasuredWidth();
3166 final int offset = getTextOffset();
3167 mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f);
3168 mPositionX += mTextView.viewportToContentHorizontalOffset();
3169
3170 final int line = mTextView.getLayout().getLineForOffset(offset);
3171 mPositionY = getVerticalLocalPosition(line);
3172 mPositionY += mTextView.viewportToContentVerticalOffset();
3173 }
3174
3175 private void updatePosition(int parentPositionX, int parentPositionY) {
3176 int positionX = parentPositionX + mPositionX;
3177 int positionY = parentPositionY + mPositionY;
3178
3179 positionY = clipVertically(positionY);
3180
3181 // Horizontal clipping
3182 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3183 final int width = mContentView.getMeasuredWidth();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003184 positionX = Math.min(
3185 displayMetrics.widthPixels - width + mClippingLimitRight, positionX);
3186 positionX = Math.max(-mClippingLimitLeft, positionX);
Gilles Debunned88876a2012-03-16 17:34:04 -07003187
3188 if (isShowing()) {
3189 mPopupWindow.update(positionX, positionY, -1, -1);
3190 } else {
3191 mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
3192 positionX, positionY);
3193 }
3194 }
3195
3196 public void hide() {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09003197 if (!isShowing()) {
3198 return;
3199 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003200 mPopupWindow.dismiss();
3201 getPositionListener().removeSubscriber(this);
3202 }
3203
3204 @Override
3205 public void updatePosition(int parentPositionX, int parentPositionY,
3206 boolean parentPositionChanged, boolean parentScrolled) {
3207 // Either parentPositionChanged or parentScrolled is true, check if still visible
3208 if (isShowing() && isOffsetVisible(getTextOffset())) {
3209 if (parentScrolled) computeLocalPosition();
3210 updatePosition(parentPositionX, parentPositionY);
3211 } else {
3212 hide();
3213 }
3214 }
3215
3216 public boolean isShowing() {
3217 return mPopupWindow.isShowing();
3218 }
3219 }
3220
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003221 private static final class SuggestionInfo {
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003222 // Range of actual suggestion within mText
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003223 int mSuggestionStart, mSuggestionEnd;
3224
3225 // The SuggestionSpan that this TextView represents
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003226 final SuggestionSpanInfo mSuggestionSpanInfo = new SuggestionSpanInfo();
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003227
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003228 // The index of this suggestion inside suggestionSpan
3229 int mSuggestionIndex;
3230
3231 final SpannableStringBuilder mText = new SpannableStringBuilder();
3232
3233 void clear() {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003234 mSuggestionSpanInfo.clear();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003235 mText.clear();
3236 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003237
3238 // Utility method to set attributes about a SuggestionSpan.
3239 void setSpanInfo(SuggestionSpan span, int spanStart, int spanEnd) {
3240 mSuggestionSpanInfo.mSuggestionSpan = span;
3241 mSuggestionSpanInfo.mSpanStart = spanStart;
3242 mSuggestionSpanInfo.mSpanEnd = spanEnd;
3243 }
3244 }
3245
3246 private static final class SuggestionSpanInfo {
3247 // The SuggestionSpan;
3248 @Nullable
3249 SuggestionSpan mSuggestionSpan;
3250
3251 // The SuggestionSpan start position
3252 int mSpanStart;
3253
3254 // The SuggestionSpan end position
3255 int mSpanEnd;
3256
3257 void clear() {
3258 mSuggestionSpan = null;
3259 }
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003260 }
3261
3262 private class SuggestionHelper {
3263 private final Comparator<SuggestionSpan> mSuggestionSpanComparator =
3264 new SuggestionSpanComparator();
3265 private final HashMap<SuggestionSpan, Integer> mSpansLengths =
3266 new HashMap<SuggestionSpan, Integer>();
3267
3268 private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
3269 public int compare(SuggestionSpan span1, SuggestionSpan span2) {
3270 final int flag1 = span1.getFlags();
3271 final int flag2 = span2.getFlags();
3272 if (flag1 != flag2) {
3273 // The order here should match what is used in updateDrawState
3274 final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
3275 final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
3276 final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
3277 final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
3278 if (easy1 && !misspelled1) return -1;
3279 if (easy2 && !misspelled2) return 1;
3280 if (misspelled1) return -1;
3281 if (misspelled2) return 1;
3282 }
3283
3284 return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
3285 }
3286 }
3287
3288 /**
3289 * Returns the suggestion spans that cover the current cursor position. The suggestion
3290 * spans are sorted according to the length of text that they are attached to.
3291 */
3292 private SuggestionSpan[] getSortedSuggestionSpans() {
3293 int pos = mTextView.getSelectionStart();
3294 Spannable spannable = (Spannable) mTextView.getText();
3295 SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
3296
3297 mSpansLengths.clear();
3298 for (SuggestionSpan suggestionSpan : suggestionSpans) {
3299 int start = spannable.getSpanStart(suggestionSpan);
3300 int end = spannable.getSpanEnd(suggestionSpan);
3301 mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
3302 }
3303
3304 // The suggestions are sorted according to their types (easy correction first, then
3305 // misspelled) and to the length of the text that they cover (shorter first).
3306 Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
3307 mSpansLengths.clear();
3308
3309 return suggestionSpans;
3310 }
3311
3312 /**
3313 * Gets the SuggestionInfo list that contains suggestion information at the current cursor
3314 * position.
3315 *
3316 * @param suggestionInfos SuggestionInfo array the results will be set.
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003317 * @param misspelledSpanInfo a struct the misspelled SuggestionSpan info will be set.
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003318 * @return the number of suggestions actually fetched.
3319 */
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003320 public int getSuggestionInfo(SuggestionInfo[] suggestionInfos,
3321 @Nullable SuggestionSpanInfo misspelledSpanInfo) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003322 final Spannable spannable = (Spannable) mTextView.getText();
3323 final SuggestionSpan[] suggestionSpans = getSortedSuggestionSpans();
3324 final int nbSpans = suggestionSpans.length;
3325 if (nbSpans == 0) return 0;
3326
3327 int numberOfSuggestions = 0;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003328 for (final SuggestionSpan suggestionSpan : suggestionSpans) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003329 final int spanStart = spannable.getSpanStart(suggestionSpan);
3330 final int spanEnd = spannable.getSpanEnd(suggestionSpan);
3331
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003332 if (misspelledSpanInfo != null
3333 && (suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
3334 misspelledSpanInfo.mSuggestionSpan = suggestionSpan;
3335 misspelledSpanInfo.mSpanStart = spanStart;
3336 misspelledSpanInfo.mSpanEnd = spanEnd;
3337 }
3338
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003339 final String[] suggestions = suggestionSpan.getSuggestions();
3340 final int nbSuggestions = suggestions.length;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003341 suggestionLoop:
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003342 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
3343 final String suggestion = suggestions[suggestionIndex];
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003344 for (int i = 0; i < numberOfSuggestions; i++) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003345 final SuggestionInfo otherSuggestionInfo = suggestionInfos[i];
3346 if (otherSuggestionInfo.mText.toString().equals(suggestion)) {
3347 final int otherSpanStart =
3348 otherSuggestionInfo.mSuggestionSpanInfo.mSpanStart;
3349 final int otherSpanEnd =
3350 otherSuggestionInfo.mSuggestionSpanInfo.mSpanEnd;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003351 if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003352 continue suggestionLoop;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003353 }
3354 }
3355 }
3356
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003357 SuggestionInfo suggestionInfo = suggestionInfos[numberOfSuggestions];
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003358 suggestionInfo.setSpanInfo(suggestionSpan, spanStart, spanEnd);
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003359 suggestionInfo.mSuggestionIndex = suggestionIndex;
3360 suggestionInfo.mSuggestionStart = 0;
3361 suggestionInfo.mSuggestionEnd = suggestion.length();
3362 suggestionInfo.mText.replace(0, suggestionInfo.mText.length(), suggestion);
3363 numberOfSuggestions++;
3364 if (numberOfSuggestions >= suggestionInfos.length) {
3365 return numberOfSuggestions;
3366 }
3367 }
3368 }
3369 return numberOfSuggestions;
3370 }
3371 }
3372
Seigo Nonakaa60160b2015-08-19 12:38:35 -07003373 @VisibleForTesting
3374 public class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
Gilles Debunned88876a2012-03-16 17:34:04 -07003375 private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003376
3377 // Key of intent extras for inserting new word into user dictionary.
3378 private static final String USER_DICTIONARY_EXTRA_WORD = "word";
3379 private static final String USER_DICTIONARY_EXTRA_LOCALE = "locale";
3380
Gilles Debunned88876a2012-03-16 17:34:04 -07003381 private SuggestionInfo[] mSuggestionInfos;
3382 private int mNumberOfSuggestions;
3383 private boolean mCursorWasVisibleBeforeSuggestions;
3384 private boolean mIsShowingUp = false;
3385 private SuggestionAdapter mSuggestionsAdapter;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003386 private TextAppearanceSpan mHighlightSpan; // TODO: Make mHighlightSpan final.
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003387 private TextView mAddToDictionaryButton;
3388 private TextView mDeleteButton;
Seigo Nonakaf47976e2016-03-01 09:17:37 -08003389 private ListView mSuggestionListView;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003390 private final SuggestionSpanInfo mMisspelledSpanInfo = new SuggestionSpanInfo();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003391 private int mContainerMarginWidth;
3392 private int mContainerMarginTop;
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003393 private LinearLayout mContainerView;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003394 private Context mContext; // TODO: Make mContext final.
Gilles Debunned88876a2012-03-16 17:34:04 -07003395
3396 private class CustomPopupWindow extends PopupWindow {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003397
Gilles Debunned88876a2012-03-16 17:34:04 -07003398 @Override
3399 public void dismiss() {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09003400 if (!isShowing()) {
3401 return;
3402 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003403 super.dismiss();
Gilles Debunned88876a2012-03-16 17:34:04 -07003404 getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
3405
3406 // Safe cast since show() checks that mTextView.getText() is an Editable
3407 ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan);
3408
3409 mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions);
Keisuke Kuroyanagi4a696ac2016-02-23 11:02:07 -08003410 if (hasInsertionController() && !extractedTextModeWillBeStarted()) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003411 getInsertionController().show();
3412 }
3413 }
3414 }
3415
3416 public SuggestionsPopupWindow() {
3417 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
Gilles Debunned88876a2012-03-16 17:34:04 -07003418 }
3419
3420 @Override
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003421 protected void setUp() {
3422 mContext = applyDefaultTheme(mTextView.getContext());
3423 mHighlightSpan = new TextAppearanceSpan(mContext,
3424 mTextView.mTextEditSuggestionHighlightStyle);
3425 }
3426
3427 private Context applyDefaultTheme(Context originalContext) {
3428 TypedArray a = originalContext.obtainStyledAttributes(
3429 new int[]{com.android.internal.R.attr.isLightTheme});
3430 boolean isLightTheme = a.getBoolean(0, true);
3431 int themeId = isLightTheme ? R.style.ThemeOverlay_Material_Light
3432 : R.style.ThemeOverlay_Material_Dark;
3433 a.recycle();
3434 return new ContextThemeWrapper(originalContext, themeId);
3435 }
3436
3437 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07003438 protected void createPopupWindow() {
Seigo Nonaka3ed1b392016-01-19 13:54:59 +09003439 mPopupWindow = new CustomPopupWindow();
Gilles Debunned88876a2012-03-16 17:34:04 -07003440 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
Seigo Nonaka3ed1b392016-01-19 13:54:59 +09003441 mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
Gilles Debunned88876a2012-03-16 17:34:04 -07003442 mPopupWindow.setFocusable(true);
3443 mPopupWindow.setClippingEnabled(false);
3444 }
3445
3446 @Override
3447 protected void initContentView() {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003448 final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
3449 Context.LAYOUT_INFLATER_SERVICE);
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003450 mContentView = (ViewGroup) inflater.inflate(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003451 mTextView.mTextEditSuggestionContainerLayout, null);
Gilles Debunned88876a2012-03-16 17:34:04 -07003452
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003453 mContainerView = (LinearLayout) mContentView.findViewById(
3454 com.android.internal.R.id.suggestionWindowContainer);
Seigo Nonaka60490d12016-01-28 17:25:18 +09003455 ViewGroup.MarginLayoutParams lp =
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003456 (ViewGroup.MarginLayoutParams) mContainerView.getLayoutParams();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003457 mContainerMarginWidth = lp.leftMargin + lp.rightMargin;
3458 mContainerMarginTop = lp.topMargin;
3459 mClippingLimitLeft = lp.leftMargin;
3460 mClippingLimitRight = lp.rightMargin;
3461
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003462 mSuggestionListView = (ListView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003463 com.android.internal.R.id.suggestionContainer);
3464
3465 mSuggestionsAdapter = new SuggestionAdapter();
Seigo Nonakaf47976e2016-03-01 09:17:37 -08003466 mSuggestionListView.setAdapter(mSuggestionsAdapter);
3467 mSuggestionListView.setOnItemClickListener(this);
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003468
3469 // Inflate the suggestion items once and for all.
3470 mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS];
Gilles Debunned88876a2012-03-16 17:34:04 -07003471 for (int i = 0; i < mSuggestionInfos.length; i++) {
3472 mSuggestionInfos[i] = new SuggestionInfo();
3473 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003474
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003475 mAddToDictionaryButton = (TextView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003476 com.android.internal.R.id.addToDictionaryButton);
3477 mAddToDictionaryButton.setOnClickListener(new View.OnClickListener() {
3478 public void onClick(View v) {
Keisuke Kuroyanagi6e0860d2016-03-15 15:40:43 +09003479 final SuggestionSpan misspelledSpan =
3480 findEquivalentSuggestionSpan(mMisspelledSpanInfo);
3481 if (misspelledSpan == null) {
3482 // Span has been removed.
3483 return;
3484 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003485 final Editable editable = (Editable) mTextView.getText();
Keisuke Kuroyanagi6e0860d2016-03-15 15:40:43 +09003486 final int spanStart = editable.getSpanStart(misspelledSpan);
3487 final int spanEnd = editable.getSpanEnd(misspelledSpan);
3488 if (spanStart < 0 || spanEnd <= spanStart) {
3489 return;
3490 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003491 final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
3492
3493 final Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
3494 intent.putExtra(USER_DICTIONARY_EXTRA_WORD, originalText);
3495 intent.putExtra(USER_DICTIONARY_EXTRA_LOCALE,
3496 mTextView.getTextServicesLocale().toString());
3497 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
3498 mTextView.getContext().startActivity(intent);
3499 // There is no way to know if the word was indeed added. Re-check.
3500 // TODO The ExtractEditText should remove the span in the original text instead
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003501 editable.removeSpan(mMisspelledSpanInfo.mSuggestionSpan);
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003502 Selection.setSelection(editable, spanEnd);
3503 updateSpellCheckSpans(spanStart, spanEnd, false);
3504 hideWithCleanUp();
3505 }
3506 });
3507
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003508 mDeleteButton = (TextView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003509 com.android.internal.R.id.deleteButton);
3510 mDeleteButton.setOnClickListener(new View.OnClickListener() {
3511 public void onClick(View v) {
3512 final Editable editable = (Editable) mTextView.getText();
3513
3514 final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
3515 int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
3516 if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
3517 // Do not leave two adjacent spaces after deletion, or one at beginning of
3518 // text
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003519 if (spanUnionEnd < editable.length()
3520 && Character.isSpaceChar(editable.charAt(spanUnionEnd))
3521 && (spanUnionStart == 0
3522 || Character.isSpaceChar(
3523 editable.charAt(spanUnionStart - 1)))) {
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003524 spanUnionEnd = spanUnionEnd + 1;
3525 }
3526 mTextView.deleteText_internal(spanUnionStart, spanUnionEnd);
3527 }
3528 hideWithCleanUp();
3529 }
3530 });
3531
Gilles Debunned88876a2012-03-16 17:34:04 -07003532 }
3533
3534 public boolean isShowingUp() {
3535 return mIsShowingUp;
3536 }
3537
3538 public void onParentLostFocus() {
3539 mIsShowingUp = false;
3540 }
3541
Gilles Debunned88876a2012-03-16 17:34:04 -07003542 private class SuggestionAdapter extends BaseAdapter {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003543 private LayoutInflater mInflater = (LayoutInflater) mContext.getSystemService(
3544 Context.LAYOUT_INFLATER_SERVICE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003545
3546 @Override
3547 public int getCount() {
3548 return mNumberOfSuggestions;
3549 }
3550
3551 @Override
3552 public Object getItem(int position) {
3553 return mSuggestionInfos[position];
3554 }
3555
3556 @Override
3557 public long getItemId(int position) {
3558 return position;
3559 }
3560
3561 @Override
3562 public View getView(int position, View convertView, ViewGroup parent) {
3563 TextView textView = (TextView) convertView;
3564
3565 if (textView == null) {
3566 textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout,
3567 parent, false);
3568 }
3569
3570 final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003571 textView.setText(suggestionInfo.mText);
Gilles Debunned88876a2012-03-16 17:34:04 -07003572 return textView;
3573 }
3574 }
3575
Seigo Nonakaa60160b2015-08-19 12:38:35 -07003576 @VisibleForTesting
3577 public ViewGroup getContentViewForTesting() {
3578 return mContentView;
3579 }
3580
Gilles Debunned88876a2012-03-16 17:34:04 -07003581 @Override
3582 public void show() {
3583 if (!(mTextView.getText() instanceof Editable)) return;
Keisuke Kuroyanagi4a696ac2016-02-23 11:02:07 -08003584 if (extractedTextModeWillBeStarted()) {
3585 return;
3586 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003587
3588 if (updateSuggestions()) {
3589 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
3590 mTextView.setCursorVisible(false);
3591 mIsShowingUp = true;
3592 super.show();
3593 }
3594 }
3595
3596 @Override
3597 protected void measureContent() {
3598 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3599 final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
3600 displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
3601 final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
3602 displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
3603
3604 int width = 0;
3605 View view = null;
3606 for (int i = 0; i < mNumberOfSuggestions; i++) {
3607 view = mSuggestionsAdapter.getView(i, view, mContentView);
3608 view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
3609 view.measure(horizontalMeasure, verticalMeasure);
3610 width = Math.max(width, view.getMeasuredWidth());
3611 }
3612
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003613 if (mAddToDictionaryButton.getVisibility() != View.GONE) {
3614 mAddToDictionaryButton.measure(horizontalMeasure, verticalMeasure);
3615 width = Math.max(width, mAddToDictionaryButton.getMeasuredWidth());
3616 }
3617
3618 mDeleteButton.measure(horizontalMeasure, verticalMeasure);
3619 width = Math.max(width, mDeleteButton.getMeasuredWidth());
3620
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003621 width += mContainerView.getPaddingLeft() + mContainerView.getPaddingRight()
3622 + mContainerMarginWidth;
Seigo Nonaka60490d12016-01-28 17:25:18 +09003623
Gilles Debunned88876a2012-03-16 17:34:04 -07003624 // Enforce the width based on actual text widths
3625 mContentView.measure(
3626 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
3627 verticalMeasure);
3628
3629 Drawable popupBackground = mPopupWindow.getBackground();
3630 if (popupBackground != null) {
3631 if (mTempRect == null) mTempRect = new Rect();
3632 popupBackground.getPadding(mTempRect);
3633 width += mTempRect.left + mTempRect.right;
3634 }
3635 mPopupWindow.setWidth(width);
3636 }
3637
3638 @Override
3639 protected int getTextOffset() {
Keisuke Kuroyanagi713be062016-02-29 16:07:54 -08003640 return (mTextView.getSelectionStart() + mTextView.getSelectionStart()) / 2;
Gilles Debunned88876a2012-03-16 17:34:04 -07003641 }
3642
3643 @Override
3644 protected int getVerticalLocalPosition(int line) {
Seigo Nonaka60490d12016-01-28 17:25:18 +09003645 return mTextView.getLayout().getLineBottom(line) - mContainerMarginTop;
Gilles Debunned88876a2012-03-16 17:34:04 -07003646 }
3647
3648 @Override
3649 protected int clipVertically(int positionY) {
3650 final int height = mContentView.getMeasuredHeight();
3651 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3652 return Math.min(positionY, displayMetrics.heightPixels - height);
3653 }
3654
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003655 private void hideWithCleanUp() {
3656 for (final SuggestionInfo info : mSuggestionInfos) {
3657 info.clear();
3658 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003659 mMisspelledSpanInfo.clear();
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003660 hide();
Gilles Debunned88876a2012-03-16 17:34:04 -07003661 }
3662
3663 private boolean updateSuggestions() {
3664 Spannable spannable = (Spannable) mTextView.getText();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003665 mNumberOfSuggestions =
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003666 mSuggestionHelper.getSuggestionInfo(mSuggestionInfos, mMisspelledSpanInfo);
3667 if (mNumberOfSuggestions == 0 && mMisspelledSpanInfo.mSuggestionSpan == null) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003668 return false;
3669 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003670
Gilles Debunned88876a2012-03-16 17:34:04 -07003671 int spanUnionStart = mTextView.getText().length();
3672 int spanUnionEnd = 0;
3673
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003674 for (int i = 0; i < mNumberOfSuggestions; i++) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003675 final SuggestionSpanInfo spanInfo = mSuggestionInfos[i].mSuggestionSpanInfo;
3676 spanUnionStart = Math.min(spanUnionStart, spanInfo.mSpanStart);
3677 spanUnionEnd = Math.max(spanUnionEnd, spanInfo.mSpanEnd);
3678 }
3679 if (mMisspelledSpanInfo.mSuggestionSpan != null) {
3680 spanUnionStart = Math.min(spanUnionStart, mMisspelledSpanInfo.mSpanStart);
3681 spanUnionEnd = Math.max(spanUnionEnd, mMisspelledSpanInfo.mSpanEnd);
Gilles Debunned88876a2012-03-16 17:34:04 -07003682 }
3683
3684 for (int i = 0; i < mNumberOfSuggestions; i++) {
3685 highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
3686 }
3687
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003688 // Make "Add to dictionary" item visible if there is a span with the misspelled flag
3689 int addToDictionaryButtonVisibility = View.GONE;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003690 if (mMisspelledSpanInfo.mSuggestionSpan != null) {
3691 if (mMisspelledSpanInfo.mSpanStart >= 0
3692 && mMisspelledSpanInfo.mSpanEnd > mMisspelledSpanInfo.mSpanStart) {
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003693 addToDictionaryButtonVisibility = View.VISIBLE;
Gilles Debunned88876a2012-03-16 17:34:04 -07003694 }
3695 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003696 mAddToDictionaryButton.setVisibility(addToDictionaryButtonVisibility);
Gilles Debunned88876a2012-03-16 17:34:04 -07003697
3698 if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003699 final int underlineColor;
3700 if (mNumberOfSuggestions != 0) {
3701 underlineColor =
3702 mSuggestionInfos[0].mSuggestionSpanInfo.mSuggestionSpan.getUnderlineColor();
3703 } else {
3704 underlineColor = mMisspelledSpanInfo.mSuggestionSpan.getUnderlineColor();
3705 }
3706
Gilles Debunned88876a2012-03-16 17:34:04 -07003707 if (underlineColor == 0) {
3708 // Fallback on the default highlight color when the first span does not provide one
3709 mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor);
3710 } else {
3711 final float BACKGROUND_TRANSPARENCY = 0.4f;
3712 final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
3713 mSuggestionRangeSpan.setBackgroundColor(
3714 (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
3715 }
3716 spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
3717 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
3718
3719 mSuggestionsAdapter.notifyDataSetChanged();
3720 return true;
3721 }
3722
3723 private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
3724 int unionEnd) {
3725 final Spannable text = (Spannable) mTextView.getText();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003726 final int spanStart = suggestionInfo.mSuggestionSpanInfo.mSpanStart;
3727 final int spanEnd = suggestionInfo.mSuggestionSpanInfo.mSpanEnd;
Gilles Debunned88876a2012-03-16 17:34:04 -07003728
3729 // Adjust the start/end of the suggestion span
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003730 suggestionInfo.mSuggestionStart = spanStart - unionStart;
3731 suggestionInfo.mSuggestionEnd = suggestionInfo.mSuggestionStart
3732 + suggestionInfo.mText.length();
Gilles Debunned88876a2012-03-16 17:34:04 -07003733
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003734 suggestionInfo.mText.setSpan(mHighlightSpan, 0, suggestionInfo.mText.length(),
Seigo Nonakabffbd302015-08-18 18:27:56 -07003735 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003736
3737 // Add the text before and after the span.
3738 final String textAsString = text.toString();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003739 suggestionInfo.mText.insert(0, textAsString.substring(unionStart, spanStart));
3740 suggestionInfo.mText.append(textAsString.substring(spanEnd, unionEnd));
Gilles Debunned88876a2012-03-16 17:34:04 -07003741 }
3742
3743 @Override
3744 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003745 SuggestionInfo suggestionInfo = mSuggestionInfos[position];
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003746 replaceWithSuggestion(suggestionInfo);
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003747 hideWithCleanUp();
Gilles Debunned88876a2012-03-16 17:34:04 -07003748 }
3749 }
3750
3751 /**
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003752 * An ActionMode Callback class that is used to provide actions while in text insertion or
3753 * selection mode.
Gilles Debunned88876a2012-03-16 17:34:04 -07003754 *
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003755 * The default callback provides a subset of Select All, Cut, Copy, Paste, Share and Replace
3756 * actions, depending on which of these this TextView supports and the current selection.
Gilles Debunned88876a2012-03-16 17:34:04 -07003757 */
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003758 private class TextActionModeCallback extends ActionMode.Callback2 {
Clara Bayarriea4f1502015-03-18 00:25:01 +00003759 private final Path mSelectionPath = new Path();
3760 private final RectF mSelectionBounds = new RectF();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003761 private final boolean mHasSelection;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003762 private final int mHandleHeight;
Clara Bayarriea4f1502015-03-18 00:25:01 +00003763
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003764 public TextActionModeCallback(boolean hasSelection) {
3765 mHasSelection = hasSelection;
3766 if (mHasSelection) {
3767 SelectionModifierCursorController selectionController = getSelectionController();
3768 if (selectionController.mStartHandle == null) {
3769 // As these are for initializing selectionController, hide() must be called.
3770 selectionController.initDrawables();
3771 selectionController.initHandles();
3772 selectionController.hide();
3773 }
3774 mHandleHeight = Math.max(
3775 mSelectHandleLeft.getMinimumHeight(),
3776 mSelectHandleRight.getMinimumHeight());
3777 } else {
3778 InsertionPointCursorController insertionController = getInsertionController();
3779 if (insertionController != null) {
3780 insertionController.getHandle();
3781 mHandleHeight = mSelectHandleCenter.getMinimumHeight();
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003782 } else {
3783 mHandleHeight = 0;
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003784 }
Clara Bayarri7fc946e2015-03-31 14:48:33 +01003785 }
Clara Bayarriea4f1502015-03-18 00:25:01 +00003786 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003787
3788 @Override
3789 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003790 mode.setTitle(null);
Clara Bayarri13152d12015-04-09 12:02:04 +01003791 mode.setSubtitle(null);
3792 mode.setTitleOptionalHint(true);
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003793 populateMenuWithItems(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01003794
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003795 Callback customCallback = getCustomCallback();
3796 if (customCallback != null) {
3797 if (!customCallback.onCreateActionMode(mode, menu)) {
Clara Bayarri01243ac2015-06-03 00:46:29 +01003798 // The custom mode can choose to cancel the action mode, dismiss selection.
3799 Selection.setSelection((Spannable) mTextView.getText(),
3800 mTextView.getSelectionEnd());
Clara Bayarri13152d12015-04-09 12:02:04 +01003801 return false;
3802 }
3803 }
3804
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07003805 if (mTextView.canProcessText()) {
3806 mProcessTextIntentActionsHandler.onInitializeMenu(menu);
3807 }
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +00003808
Clara Bayarri13152d12015-04-09 12:02:04 +01003809 if (menu.hasVisibleItems() || mode.getCustomView() != null) {
Keisuke Kuroyanagi183fd502016-04-01 15:00:53 +09003810 if (mHasSelection && !mTextView.hasTransientState()) {
3811 mTextView.setHasTransientState(true);
3812 }
Clara Bayarri13152d12015-04-09 12:02:04 +01003813 return true;
3814 } else {
3815 return false;
3816 }
3817 }
3818
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003819 private Callback getCustomCallback() {
3820 return mHasSelection
3821 ? mCustomSelectionActionModeCallback
3822 : mCustomInsertionActionModeCallback;
3823 }
3824
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003825 private void populateMenuWithItems(Menu menu) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003826 if (mTextView.canCut()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003827 menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003828 com.android.internal.R.string.cut)
3829 .setAlphabeticShortcut('x')
3830 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07003831 }
3832
3833 if (mTextView.canCopy()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003834 menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003835 com.android.internal.R.string.copy)
3836 .setAlphabeticShortcut('c')
3837 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07003838 }
3839
3840 if (mTextView.canPaste()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003841 menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003842 com.android.internal.R.string.paste)
3843 .setAlphabeticShortcut('v')
3844 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07003845 }
3846
Andrei Stingaceanu7f0c5bd2015-04-14 17:12:08 +01003847 if (mTextView.canShare()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003848 menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003849 com.android.internal.R.string.share)
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +00003850 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
Andrei Stingaceanu7f0c5bd2015-04-14 17:12:08 +01003851 }
3852
Felipe Leme2ac463e2017-03-13 14:06:25 -07003853 if (mTextView.canRequestAutofill()) {
Felipe Leme1c1626e2017-06-02 10:53:13 -07003854 final String selected = mTextView.getSelectedText();
3855 if (selected == null || selected.isEmpty()) {
3856 menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
3857 com.android.internal.R.string.autofill)
Siyamed Sinir484c2e22017-06-07 16:26:19 -07003858 .setShowAsAction(MenuItem.SHOW_AS_OVERFLOW_ALWAYS);
Felipe Leme1c1626e2017-06-02 10:53:13 -07003859 }
Felipe Leme2ac463e2017-03-13 14:06:25 -07003860 }
3861
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01003862 if (mTextView.canPasteAsPlainText()) {
3863 menu.add(
3864 Menu.NONE,
3865 TextView.ID_PASTE_AS_PLAIN_TEXT,
3866 MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
3867 com.android.internal.R.string.paste_as_plain_text)
3868 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
3869 }
3870
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003871 updateSelectAllItem(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01003872 updateReplaceItem(menu);
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01003873 updateAssistMenuItem(menu);
Gilles Debunned88876a2012-03-16 17:34:04 -07003874 }
3875
3876 @Override
3877 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003878 updateSelectAllItem(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01003879 updateReplaceItem(menu);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08003880 updateAssistMenuItem(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01003881
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003882 Callback customCallback = getCustomCallback();
3883 if (customCallback != null) {
3884 return customCallback.onPrepareActionMode(mode, menu);
Gilles Debunned88876a2012-03-16 17:34:04 -07003885 }
3886 return true;
3887 }
3888
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003889 private void updateSelectAllItem(Menu menu) {
3890 boolean canSelectAll = mTextView.canSelectAllText();
3891 boolean selectAllItemExists = menu.findItem(TextView.ID_SELECT_ALL) != null;
3892 if (canSelectAll && !selectAllItemExists) {
3893 menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
3894 com.android.internal.R.string.selectAll)
3895 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
3896 } else if (!canSelectAll && selectAllItemExists) {
3897 menu.removeItem(TextView.ID_SELECT_ALL);
3898 }
3899 }
3900
Clara Bayarri13152d12015-04-09 12:02:04 +01003901 private void updateReplaceItem(Menu menu) {
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003902 boolean canReplace = mTextView.isSuggestionsEnabled() && shouldOfferToShowSuggestions();
Clara Bayarri13152d12015-04-09 12:02:04 +01003903 boolean replaceItemExists = menu.findItem(TextView.ID_REPLACE) != null;
3904 if (canReplace && !replaceItemExists) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003905 menu.add(Menu.NONE, TextView.ID_REPLACE, MENU_ITEM_ORDER_REPLACE,
3906 com.android.internal.R.string.replace)
3907 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
Clara Bayarri13152d12015-04-09 12:02:04 +01003908 } else if (!canReplace && replaceItemExists) {
3909 menu.removeItem(TextView.ID_REPLACE);
3910 }
3911 }
3912
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08003913 private void updateAssistMenuItem(Menu menu) {
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003914 menu.removeItem(TextView.ID_ASSIST);
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +01003915 final TextClassification textClassification =
3916 getSelectionActionModeHelper().getTextClassification();
Abodunrinwa Toki9796a1b2017-06-28 02:49:07 +01003917 if (canAssist()) {
3918 menu.add(TextView.ID_ASSIST, TextView.ID_ASSIST, MENU_ITEM_ORDER_ASSIST,
3919 textClassification.getLabel())
3920 .setIcon(textClassification.getIcon())
3921 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
3922 mMetricsLogger.write(
3923 new LogMaker(MetricsEvent.TEXT_SELECTION_MENU_ITEM_ASSIST)
3924 .setType(MetricsEvent.TYPE_OPEN)
3925 .setSubtype(textClassification.getLogType()));
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003926 }
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +00003927 }
3928
Abodunrinwa Toki9796a1b2017-06-28 02:49:07 +01003929 private boolean canAssist() {
3930 final TextClassification textClassification =
3931 getSelectionActionModeHelper().getTextClassification();
3932 return mTextView.isDeviceProvisioned()
3933 && textClassification != null
3934 && (textClassification.getIcon() != null
3935 || !TextUtils.isEmpty(textClassification.getLabel()))
3936 && (textClassification.getOnClickListener() != null
3937 || (textClassification.getIntent() != null
3938 && mTextView.getContext().canStartActivityForResult()));
3939 }
3940
Gilles Debunned88876a2012-03-16 17:34:04 -07003941 @Override
3942 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Abodunrinwa Toki1d775572017-05-08 16:03:01 +01003943 getSelectionActionModeHelper().onSelectionAction();
3944
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07003945 if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +00003946 return true;
3947 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003948 Callback customCallback = getCustomCallback();
3949 if (customCallback != null && customCallback.onActionItemClicked(mode, item)) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003950 return true;
3951 }
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +01003952 final TextClassification textClassification =
3953 getSelectionActionModeHelper().getTextClassification();
3954 if (TextView.ID_ASSIST == item.getItemId() && textClassification != null) {
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +00003955 final OnClickListener onClickListener =
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +01003956 textClassification.getOnClickListener();
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +00003957 if (onClickListener != null) {
3958 onClickListener.onClick(mTextView);
3959 } else {
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +01003960 final Intent intent = textClassification.getIntent();
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +00003961 if (intent != null) {
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +01003962 TextClassification.createStartActivityOnClickListener(
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +00003963 mTextView.getContext(), intent)
3964 .onClick(mTextView);
3965 }
3966 }
Abodunrinwa Toki54486c12017-04-19 21:02:36 +01003967 mMetricsLogger.action(
3968 MetricsEvent.ACTION_TEXT_SELECTION_MENU_ITEM_ASSIST,
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +01003969 textClassification.getLogType());
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003970 stopTextActionMode();
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +00003971 return true;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003972 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003973 return mTextView.onTextContextMenuItem(item.getItemId());
3974 }
3975
3976 @Override
3977 public void onDestroyActionMode(ActionMode mode) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09003978 // Clear mTextActionMode not to recursively destroy action mode by clearing selection.
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +00003979 getSelectionActionModeHelper().onDestroyActionMode();
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09003980 mTextActionMode = null;
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003981 Callback customCallback = getCustomCallback();
3982 if (customCallback != null) {
3983 customCallback.onDestroyActionMode(mode);
Gilles Debunned88876a2012-03-16 17:34:04 -07003984 }
Adam Powell057a5852012-05-11 10:28:38 -07003985
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08003986 if (!mPreserveSelection) {
3987 /*
3988 * Leave current selection when we tentatively destroy action mode for the
3989 * selection. If we're detaching from a window, we'll bring back the selection
3990 * mode when (if) we get reattached.
3991 */
Adam Powell057a5852012-05-11 10:28:38 -07003992 Selection.setSelection((Spannable) mTextView.getText(),
3993 mTextView.getSelectionEnd());
Adam Powell057a5852012-05-11 10:28:38 -07003994 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003995
3996 if (mSelectionModifierCursorController != null) {
3997 mSelectionModifierCursorController.hide();
3998 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003999 }
Clara Bayarriea4f1502015-03-18 00:25:01 +00004000
4001 @Override
4002 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
4003 if (!view.equals(mTextView) || mTextView.getLayout() == null) {
4004 super.onGetContentRect(mode, view, outRect);
4005 return;
4006 }
4007 if (mTextView.getSelectionStart() != mTextView.getSelectionEnd()) {
4008 // We have a selection.
4009 mSelectionPath.reset();
4010 mTextView.getLayout().getSelectionPath(
4011 mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mSelectionPath);
4012 mSelectionPath.computeBounds(mSelectionBounds, true);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004013 mSelectionBounds.bottom += mHandleHeight;
Clara Bayarrib7419dd2015-04-09 15:24:58 +01004014 } else if (mCursorCount == 2) {
4015 // We have a split cursor. In this case, we take the rectangle that includes both
4016 // parts of the cursor to ensure we don't obscure either of them.
4017 Rect firstCursorBounds = mCursorDrawable[0].getBounds();
4018 Rect secondCursorBounds = mCursorDrawable[1].getBounds();
4019 mSelectionBounds.set(
4020 Math.min(firstCursorBounds.left, secondCursorBounds.left),
4021 Math.min(firstCursorBounds.top, secondCursorBounds.top),
4022 Math.max(firstCursorBounds.right, secondCursorBounds.right),
4023 Math.max(firstCursorBounds.bottom, secondCursorBounds.bottom)
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004024 + mHandleHeight);
Clara Bayarriea4f1502015-03-18 00:25:01 +00004025 } else {
4026 // We have a single cursor.
Siyamed Sinir987ec652016-02-17 19:44:41 -08004027 Layout layout = mTextView.getLayout();
Mady Mellorff66ca52015-07-08 12:31:45 -07004028 int line = layout.getLineForOffset(mTextView.getSelectionStart());
Siyamed Sinir987ec652016-02-17 19:44:41 -08004029 float primaryHorizontal = clampHorizontalPosition(null,
4030 layout.getPrimaryHorizontal(mTextView.getSelectionStart()));
Clara Bayarriea4f1502015-03-18 00:25:01 +00004031 mSelectionBounds.set(
4032 primaryHorizontal,
Mady Mellorff66ca52015-07-08 12:31:45 -07004033 layout.getLineTop(line),
Clara Bayarrif95ed102015-08-12 19:46:47 +01004034 primaryHorizontal,
Mady Mellorff66ca52015-07-08 12:31:45 -07004035 layout.getLineTop(line + 1) + mHandleHeight);
Clara Bayarriea4f1502015-03-18 00:25:01 +00004036 }
4037 // Take TextView's padding and scroll into account.
4038 int textHorizontalOffset = mTextView.viewportToContentHorizontalOffset();
4039 int textVerticalOffset = mTextView.viewportToContentVerticalOffset();
4040 outRect.set(
4041 (int) Math.floor(mSelectionBounds.left + textHorizontalOffset),
4042 (int) Math.floor(mSelectionBounds.top + textVerticalOffset),
4043 (int) Math.ceil(mSelectionBounds.right + textHorizontalOffset),
4044 (int) Math.ceil(mSelectionBounds.bottom + textVerticalOffset));
4045 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004046 }
4047
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004048 /**
4049 * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}
4050 * while the input method is requesting the cursor/anchor position. Does nothing as long as
4051 * {@link InputMethodManager#isWatchingCursor(View)} returns false.
4052 */
4053 private final class CursorAnchorInfoNotifier implements TextViewPositionListener {
Yohei Yukawac46b5f02014-06-10 12:26:34 +09004054 final CursorAnchorInfo.Builder mSelectionInfoBuilder = new CursorAnchorInfo.Builder();
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004055 final int[] mTmpIntOffset = new int[2];
4056 final Matrix mViewToScreenMatrix = new Matrix();
4057
4058 @Override
4059 public void updatePosition(int parentPositionX, int parentPositionY,
4060 boolean parentPositionChanged, boolean parentScrolled) {
4061 final InputMethodState ims = mInputMethodState;
4062 if (ims == null || ims.mBatchEditNesting > 0) {
4063 return;
4064 }
4065 final InputMethodManager imm = InputMethodManager.peekInstance();
4066 if (null == imm) {
4067 return;
4068 }
Yohei Yukawa0023d0e2014-07-11 04:13:03 +09004069 if (!imm.isActive(mTextView)) {
4070 return;
4071 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004072 // Skip if the IME has not requested the cursor/anchor position.
Yohei Yukawa0023d0e2014-07-11 04:13:03 +09004073 if (!imm.isCursorAnchorInfoEnabled()) {
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004074 return;
4075 }
4076 Layout layout = mTextView.getLayout();
4077 if (layout == null) {
4078 return;
4079 }
4080
Yohei Yukawac46b5f02014-06-10 12:26:34 +09004081 final CursorAnchorInfo.Builder builder = mSelectionInfoBuilder;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004082 builder.reset();
4083
4084 final int selectionStart = mTextView.getSelectionStart();
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004085 builder.setSelectionRange(selectionStart, mTextView.getSelectionEnd());
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004086
4087 // Construct transformation matrix from view local coordinates to screen coordinates.
4088 mViewToScreenMatrix.set(mTextView.getMatrix());
4089 mTextView.getLocationOnScreen(mTmpIntOffset);
4090 mViewToScreenMatrix.postTranslate(mTmpIntOffset[0], mTmpIntOffset[1]);
4091 builder.setMatrix(mViewToScreenMatrix);
4092
4093 final float viewportToContentHorizontalOffset =
4094 mTextView.viewportToContentHorizontalOffset();
4095 final float viewportToContentVerticalOffset =
4096 mTextView.viewportToContentVerticalOffset();
4097
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004098 final CharSequence text = mTextView.getText();
4099 if (text instanceof Spannable) {
4100 final Spannable sp = (Spannable) text;
4101 int composingTextStart = EditableInputConnection.getComposingSpanStart(sp);
4102 int composingTextEnd = EditableInputConnection.getComposingSpanEnd(sp);
4103 if (composingTextEnd < composingTextStart) {
4104 final int temp = composingTextEnd;
4105 composingTextEnd = composingTextStart;
4106 composingTextStart = temp;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004107 }
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004108 final boolean hasComposingText =
4109 (0 <= composingTextStart) && (composingTextStart < composingTextEnd);
4110 if (hasComposingText) {
4111 final CharSequence composingText = text.subSequence(composingTextStart,
4112 composingTextEnd);
4113 builder.setComposingText(composingTextStart, composingText);
Phil Weaverc2e28932016-12-08 12:29:25 -08004114 mTextView.populateCharacterBounds(builder, composingTextStart,
4115 composingTextEnd, viewportToContentHorizontalOffset,
4116 viewportToContentVerticalOffset);
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004117 }
4118 }
4119
4120 // Treat selectionStart as the insertion point.
4121 if (0 <= selectionStart) {
4122 final int offset = selectionStart;
4123 final int line = layout.getLineForOffset(offset);
4124 final float insertionMarkerX = layout.getPrimaryHorizontal(offset)
4125 + viewportToContentHorizontalOffset;
4126 final float insertionMarkerTop = layout.getLineTop(line)
4127 + viewportToContentVerticalOffset;
4128 final float insertionMarkerBaseline = layout.getLineBaseline(line)
4129 + viewportToContentVerticalOffset;
4130 final float insertionMarkerBottom = layout.getLineBottom(line)
4131 + viewportToContentVerticalOffset;
Phil Weaverc2e28932016-12-08 12:29:25 -08004132 final boolean isTopVisible = mTextView
4133 .isPositionVisible(insertionMarkerX, insertionMarkerTop);
4134 final boolean isBottomVisible = mTextView
4135 .isPositionVisible(insertionMarkerX, insertionMarkerBottom);
Yohei Yukawacc24e2b2014-08-29 20:21:10 -07004136 int insertionMarkerFlags = 0;
4137 if (isTopVisible || isBottomVisible) {
4138 insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
4139 }
4140 if (!isTopVisible || !isBottomVisible) {
4141 insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
4142 }
Yohei Yukawa5f183f02014-09-02 14:18:40 -07004143 if (layout.isRtlCharAt(offset)) {
4144 insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL;
4145 }
Yohei Yukawa0b01e7f2014-07-08 15:29:51 +09004146 builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop,
Yohei Yukawacc24e2b2014-08-29 20:21:10 -07004147 insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags);
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004148 }
4149
4150 imm.updateCursorAnchorInfo(mTextView, builder.build());
4151 }
4152 }
4153
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004154 @VisibleForTesting
4155 public abstract class HandleView extends View implements TextViewPositionListener {
Gilles Debunned88876a2012-03-16 17:34:04 -07004156 protected Drawable mDrawable;
4157 protected Drawable mDrawableLtr;
4158 protected Drawable mDrawableRtl;
4159 private final PopupWindow mContainer;
4160 // Position with respect to the parent TextView
4161 private int mPositionX, mPositionY;
4162 private boolean mIsDragging;
4163 // Offset from touch position to mPosition
4164 private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
4165 protected int mHotspotX;
Adam Powell3fceabd2014-08-19 18:28:04 -07004166 protected int mHorizontalGravity;
Gilles Debunned88876a2012-03-16 17:34:04 -07004167 // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
4168 private float mTouchOffsetY;
4169 // Where the touch position should be on the handle to ensure a maximum cursor visibility
4170 private float mIdealVerticalOffset;
4171 // Parent's (TextView) previous position in window
4172 private int mLastParentX, mLastParentY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004173 // Parent's (TextView) previous position on screen
4174 private int mLastParentXOnScreen, mLastParentYOnScreen;
Gilles Debunned88876a2012-03-16 17:34:04 -07004175 // Previous text character offset
Mady Mellorc2225b92015-04-01 15:59:20 -07004176 protected int mPreviousOffset = -1;
Gilles Debunned88876a2012-03-16 17:34:04 -07004177 // Previous text character offset
4178 private boolean mPositionHasChanged = true;
Adam Powell3fceabd2014-08-19 18:28:04 -07004179 // Minimum touch target size for handles
4180 private int mMinSize;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004181 // Indicates the line of text that the handle is on.
Mady Mellora6a0f782015-07-10 16:43:32 -07004182 protected int mPrevLine = UNSET_LINE;
4183 // Indicates the line of text that the user was touching. This can differ from mPrevLine
4184 // when selecting text when the handles jump to the end / start of words which may be on
4185 // a different line.
4186 protected int mPreviousLineTouched = UNSET_LINE;
Gilles Debunned88876a2012-03-16 17:34:04 -07004187
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004188 private HandleView(Drawable drawableLtr, Drawable drawableRtl, final int id) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004189 super(mTextView.getContext());
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004190 setId(id);
Gilles Debunned88876a2012-03-16 17:34:04 -07004191 mContainer = new PopupWindow(mTextView.getContext(), null,
4192 com.android.internal.R.attr.textSelectHandleWindowStyle);
4193 mContainer.setSplitTouchEnabled(true);
4194 mContainer.setClippingEnabled(false);
4195 mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
Keisuke Kuroyanagi7340be72015-02-27 17:57:49 +09004196 mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
4197 mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
Gilles Debunned88876a2012-03-16 17:34:04 -07004198 mContainer.setContentView(this);
4199
4200 mDrawableLtr = drawableLtr;
4201 mDrawableRtl = drawableRtl;
Adam Powell3fceabd2014-08-19 18:28:04 -07004202 mMinSize = mTextView.getContext().getResources().getDimensionPixelSize(
4203 com.android.internal.R.dimen.text_handle_min_size);
Gilles Debunned88876a2012-03-16 17:34:04 -07004204
4205 updateDrawable();
4206
Adam Powell3fceabd2014-08-19 18:28:04 -07004207 final int handleHeight = getPreferredHeight();
Gilles Debunned88876a2012-03-16 17:34:04 -07004208 mTouchOffsetY = -0.3f * handleHeight;
4209 mIdealVerticalOffset = 0.7f * handleHeight;
4210 }
4211
Mady Mellor7a936442015-05-20 10:05:52 -07004212 public float getIdealVerticalOffset() {
4213 return mIdealVerticalOffset;
4214 }
4215
Gilles Debunned88876a2012-03-16 17:34:04 -07004216 protected void updateDrawable() {
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004217 if (mIsDragging) {
4218 // Don't update drawable during dragging.
4219 return;
4220 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004221 final Layout layout = mTextView.getLayout();
4222 if (layout == null) {
4223 return;
4224 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004225 final int offset = getCurrentCursorOffset();
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004226 final boolean isRtlCharAtOffset = isAtRtlRun(layout, offset);
Keisuke Kuroyanagi33f81ac2015-05-14 20:10:57 +09004227 final Drawable oldDrawable = mDrawable;
Gilles Debunned88876a2012-03-16 17:34:04 -07004228 mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
4229 mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
Adam Powell3fceabd2014-08-19 18:28:04 -07004230 mHorizontalGravity = getHorizontalGravity(isRtlCharAtOffset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004231 if (oldDrawable != mDrawable && isShowing()) {
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004232 // Update popup window position.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004233 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
4234 - getHorizontalOffset() + getCursorOffset();
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004235 mPositionX += mTextView.viewportToContentHorizontalOffset();
4236 mPositionHasChanged = true;
4237 updatePosition(mLastParentX, mLastParentY, false, false);
Keisuke Kuroyanagi33f81ac2015-05-14 20:10:57 +09004238 postInvalidate();
4239 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004240 }
4241
4242 protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
Adam Powell3fceabd2014-08-19 18:28:04 -07004243 protected abstract int getHorizontalGravity(boolean isRtlRun);
Gilles Debunned88876a2012-03-16 17:34:04 -07004244
4245 // Touch-up filter: number of previous positions remembered
4246 private static final int HISTORY_SIZE = 5;
4247 private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
4248 private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
4249 private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
4250 private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
4251 private int mPreviousOffsetIndex = 0;
4252 private int mNumberPreviousOffsets = 0;
4253
4254 private void startTouchUpFilter(int offset) {
4255 mNumberPreviousOffsets = 0;
4256 addPositionToTouchUpFilter(offset);
4257 }
4258
4259 private void addPositionToTouchUpFilter(int offset) {
4260 mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
4261 mPreviousOffsets[mPreviousOffsetIndex] = offset;
4262 mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
4263 mNumberPreviousOffsets++;
4264 }
4265
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004266 private void filterOnTouchUp(boolean fromTouchScreen) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004267 final long now = SystemClock.uptimeMillis();
4268 int i = 0;
4269 int index = mPreviousOffsetIndex;
4270 final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
4271 while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
4272 i++;
4273 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
4274 }
4275
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004276 if (i > 0 && i < iMax
4277 && (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004278 positionAtCursorOffset(mPreviousOffsets[index], false, fromTouchScreen);
Gilles Debunned88876a2012-03-16 17:34:04 -07004279 }
4280 }
4281
4282 public boolean offsetHasBeenChanged() {
4283 return mNumberPreviousOffsets > 1;
4284 }
4285
4286 @Override
4287 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Adam Powell3fceabd2014-08-19 18:28:04 -07004288 setMeasuredDimension(getPreferredWidth(), getPreferredHeight());
4289 }
4290
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004291 @Override
4292 public void invalidate() {
4293 super.invalidate();
4294 if (isShowing()) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004295 positionAtCursorOffset(getCurrentCursorOffset(), true, false);
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004296 }
4297 };
4298
Adam Powell3fceabd2014-08-19 18:28:04 -07004299 private int getPreferredWidth() {
4300 return Math.max(mDrawable.getIntrinsicWidth(), mMinSize);
4301 }
4302
4303 private int getPreferredHeight() {
4304 return Math.max(mDrawable.getIntrinsicHeight(), mMinSize);
Gilles Debunned88876a2012-03-16 17:34:04 -07004305 }
4306
4307 public void show() {
4308 if (isShowing()) return;
4309
4310 getPositionListener().addSubscriber(this, true /* local position may change */);
4311
4312 // Make sure the offset is always considered new, even when focusing at same position
4313 mPreviousOffset = -1;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004314 positionAtCursorOffset(getCurrentCursorOffset(), false, false);
Gilles Debunned88876a2012-03-16 17:34:04 -07004315 }
4316
4317 protected void dismiss() {
4318 mIsDragging = false;
4319 mContainer.dismiss();
4320 onDetached();
4321 }
4322
4323 public void hide() {
4324 dismiss();
4325
4326 getPositionListener().removeSubscriber(this);
4327 }
4328
Gilles Debunned88876a2012-03-16 17:34:04 -07004329 public boolean isShowing() {
4330 return mContainer.isShowing();
4331 }
4332
4333 private boolean isVisible() {
4334 // Always show a dragging handle.
4335 if (mIsDragging) {
4336 return true;
4337 }
4338
4339 if (mTextView.isInBatchEditMode()) {
4340 return false;
4341 }
4342
Phil Weaverc2e28932016-12-08 12:29:25 -08004343 return mTextView.isPositionVisible(
4344 mPositionX + mHotspotX + getHorizontalOffset(), mPositionY);
Gilles Debunned88876a2012-03-16 17:34:04 -07004345 }
4346
4347 public abstract int getCurrentCursorOffset();
4348
4349 protected abstract void updateSelection(int offset);
4350
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004351 protected abstract void updatePosition(float x, float y, boolean fromTouchScreen);
Gilles Debunned88876a2012-03-16 17:34:04 -07004352
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004353 protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
4354 return layout.isRtlCharAt(offset);
4355 }
4356
4357 @VisibleForTesting
4358 public float getHorizontal(@NonNull Layout layout, int offset) {
4359 return layout.getPrimaryHorizontal(offset);
4360 }
4361
4362 protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
4363 return mTextView.getOffsetAtCoordinate(line, x);
4364 }
4365
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004366 /**
4367 * @param offset Cursor offset. Must be in [-1, length].
4368 * @param forceUpdatePosition whether to force update the position. This should be true
4369 * when If the parent has been scrolled, for example.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004370 * @param fromTouchScreen {@code true} if the cursor is moved with motion events from the
4371 * touch screen.
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004372 */
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004373 protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
4374 boolean fromTouchScreen) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004375 // A HandleView relies on the layout, which may be nulled by external methods
4376 Layout layout = mTextView.getLayout();
4377 if (layout == null) {
4378 // Will update controllers' state, hiding them and stopping selection mode if needed
4379 prepareCursorControllers();
4380 return;
4381 }
Siyamed Sinir987ec652016-02-17 19:44:41 -08004382 layout = mTextView.getLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -07004383
4384 boolean offsetChanged = offset != mPreviousOffset;
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004385 if (offsetChanged || forceUpdatePosition) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004386 if (offsetChanged) {
4387 updateSelection(offset);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004388 if (fromTouchScreen && mHapticTextHandleEnabled) {
4389 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
4390 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004391 addPositionToTouchUpFilter(offset);
4392 }
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07004393 final int line = layout.getLineForOffset(offset);
Mady Mellorb9bbbb12015-03-23 11:50:46 -07004394 mPrevLine = line;
Gilles Debunned88876a2012-03-16 17:34:04 -07004395
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004396 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
4397 - getHorizontalOffset() + getCursorOffset();
Gilles Debunned88876a2012-03-16 17:34:04 -07004398 mPositionY = layout.getLineBottom(line);
4399
4400 // Take TextView's padding and scroll into account.
4401 mPositionX += mTextView.viewportToContentHorizontalOffset();
4402 mPositionY += mTextView.viewportToContentVerticalOffset();
4403
4404 mPreviousOffset = offset;
4405 mPositionHasChanged = true;
4406 }
4407 }
4408
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004409 /**
4410 * Return the clamped horizontal position for the first cursor.
4411 *
4412 * @param layout Text layout.
4413 * @param offset Character offset for the cursor.
4414 * @return The clamped horizontal position for the cursor.
4415 */
4416 int getCursorHorizontalPosition(Layout layout, int offset) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004417 return (int) (getHorizontal(layout, offset) - 0.5f);
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004418 }
4419
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004420 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07004421 public void updatePosition(int parentPositionX, int parentPositionY,
4422 boolean parentPositionChanged, boolean parentScrolled) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004423 positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled, false);
Gilles Debunned88876a2012-03-16 17:34:04 -07004424 if (parentPositionChanged || mPositionHasChanged) {
4425 if (mIsDragging) {
4426 // Update touchToWindow offset in case of parent scrolling while dragging
4427 if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
4428 mTouchToWindowOffsetX += parentPositionX - mLastParentX;
4429 mTouchToWindowOffsetY += parentPositionY - mLastParentY;
4430 mLastParentX = parentPositionX;
4431 mLastParentY = parentPositionY;
4432 }
4433
4434 onHandleMoved();
4435 }
4436
4437 if (isVisible()) {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004438 // Transform to the window coordinates to follow the view tranformation.
4439 final int[] pts = { mPositionX + mHotspotX + getHorizontalOffset(), mPositionY};
4440 mTextView.transformFromViewToWindowSpace(pts);
4441 pts[0] -= mHotspotX + getHorizontalOffset();
4442
Gilles Debunned88876a2012-03-16 17:34:04 -07004443 if (isShowing()) {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004444 mContainer.update(pts[0], pts[1], -1, -1);
Gilles Debunned88876a2012-03-16 17:34:04 -07004445 } else {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004446 mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY, pts[0], pts[1]);
Gilles Debunned88876a2012-03-16 17:34:04 -07004447 }
4448 } else {
4449 if (isShowing()) {
4450 dismiss();
4451 }
4452 }
4453
4454 mPositionHasChanged = false;
4455 }
4456 }
4457
4458 @Override
4459 protected void onDraw(Canvas c) {
Adam Powell3fceabd2014-08-19 18:28:04 -07004460 final int drawWidth = mDrawable.getIntrinsicWidth();
4461 final int left = getHorizontalOffset();
4462
4463 mDrawable.setBounds(left, 0, left + drawWidth, mDrawable.getIntrinsicHeight());
Gilles Debunned88876a2012-03-16 17:34:04 -07004464 mDrawable.draw(c);
4465 }
4466
Adam Powell3fceabd2014-08-19 18:28:04 -07004467 private int getHorizontalOffset() {
4468 final int width = getPreferredWidth();
4469 final int drawWidth = mDrawable.getIntrinsicWidth();
4470 final int left;
4471 switch (mHorizontalGravity) {
4472 case Gravity.LEFT:
4473 left = 0;
4474 break;
4475 default:
4476 case Gravity.CENTER:
4477 left = (width - drawWidth) / 2;
4478 break;
4479 case Gravity.RIGHT:
4480 left = width - drawWidth;
4481 break;
4482 }
4483 return left;
4484 }
4485
4486 protected int getCursorOffset() {
4487 return 0;
4488 }
4489
Gilles Debunned88876a2012-03-16 17:34:04 -07004490 @Override
4491 public boolean onTouchEvent(MotionEvent ev) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01004492 updateFloatingToolbarVisibility(ev);
4493
Gilles Debunned88876a2012-03-16 17:34:04 -07004494 switch (ev.getActionMasked()) {
4495 case MotionEvent.ACTION_DOWN: {
4496 startTouchUpFilter(getCurrentCursorOffset());
Gilles Debunned88876a2012-03-16 17:34:04 -07004497
4498 final PositionListener positionListener = getPositionListener();
4499 mLastParentX = positionListener.getPositionX();
4500 mLastParentY = positionListener.getPositionY();
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004501 mLastParentXOnScreen = positionListener.getPositionXOnScreen();
4502 mLastParentYOnScreen = positionListener.getPositionYOnScreen();
4503
4504 final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
4505 final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
4506 mTouchToWindowOffsetX = xInWindow - mPositionX;
4507 mTouchToWindowOffsetY = yInWindow - mPositionY;
4508
Gilles Debunned88876a2012-03-16 17:34:04 -07004509 mIsDragging = true;
Mady Mellora6a0f782015-07-10 16:43:32 -07004510 mPreviousLineTouched = UNSET_LINE;
Gilles Debunned88876a2012-03-16 17:34:04 -07004511 break;
4512 }
4513
4514 case MotionEvent.ACTION_MOVE: {
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004515 final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
4516 final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
Gilles Debunned88876a2012-03-16 17:34:04 -07004517
4518 // Vertical hysteresis: vertical down movement tends to snap to ideal offset
4519 final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004520 final float currentVerticalOffset = yInWindow - mPositionY - mLastParentY;
Gilles Debunned88876a2012-03-16 17:34:04 -07004521 float newVerticalOffset;
4522 if (previousVerticalOffset < mIdealVerticalOffset) {
4523 newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
4524 newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
4525 } else {
4526 newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
4527 newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
4528 }
4529 mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
4530
Keisuke Kuroyanagibc89a5c2015-05-18 14:49:29 +09004531 final float newPosX =
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004532 xInWindow - mTouchToWindowOffsetX + mHotspotX + getHorizontalOffset();
4533 final float newPosY = yInWindow - mTouchToWindowOffsetY + mTouchOffsetY;
Gilles Debunned88876a2012-03-16 17:34:04 -07004534
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004535 updatePosition(newPosX, newPosY,
4536 ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Gilles Debunned88876a2012-03-16 17:34:04 -07004537 break;
4538 }
4539
4540 case MotionEvent.ACTION_UP:
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004541 filterOnTouchUp(ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Gilles Debunned88876a2012-03-16 17:34:04 -07004542 mIsDragging = false;
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004543 updateDrawable();
Gilles Debunned88876a2012-03-16 17:34:04 -07004544 break;
4545
4546 case MotionEvent.ACTION_CANCEL:
4547 mIsDragging = false;
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004548 updateDrawable();
Gilles Debunned88876a2012-03-16 17:34:04 -07004549 break;
4550 }
4551 return true;
4552 }
4553
4554 public boolean isDragging() {
4555 return mIsDragging;
4556 }
4557
Clara Bayarri6351e662015-03-16 23:17:59 +00004558 void onHandleMoved() {}
Gilles Debunned88876a2012-03-16 17:34:04 -07004559
Clara Bayarri6351e662015-03-16 23:17:59 +00004560 public void onDetached() {}
Gilles Debunned88876a2012-03-16 17:34:04 -07004561 }
4562
4563 private class InsertionHandleView extends HandleView {
4564 private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
4565 private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
4566
Clara Bayarrib71dddd2015-06-04 23:17:30 +01004567 // Used to detect taps on the insertion handle, which will affect the insertion action mode
Gilles Debunned88876a2012-03-16 17:34:04 -07004568 private float mDownPositionX, mDownPositionY;
4569 private Runnable mHider;
4570
4571 public InsertionHandleView(Drawable drawable) {
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004572 super(drawable, drawable, com.android.internal.R.id.insertion_handle);
Gilles Debunned88876a2012-03-16 17:34:04 -07004573 }
4574
4575 @Override
4576 public void show() {
4577 super.show();
4578
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01004579 final long durationSinceCutOrCopy =
Andrei Stingaceanu77b9c382015-05-06 13:25:19 +01004580 SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime;
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01004581
4582 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004583 if (mInsertionActionModeRunnable != null
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09004584 && ((mTapState == TAP_STATE_DOUBLE_TAP)
4585 || (mTapState == TAP_STATE_TRIPLE_CLICK)
4586 || isCursorInsideEasyCorrectionSpan())) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004587 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01004588 }
4589
4590 // Prepare and schedule the single tap runnable to run exactly after the double tap
4591 // timeout has passed.
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09004592 if ((mTapState != TAP_STATE_DOUBLE_TAP) && (mTapState != TAP_STATE_TRIPLE_CLICK)
4593 && !isCursorInsideEasyCorrectionSpan()
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01004594 && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION)) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01004595 if (mTextActionMode == null) {
4596 if (mInsertionActionModeRunnable == null) {
4597 mInsertionActionModeRunnable = new Runnable() {
4598 @Override
4599 public void run() {
4600 startInsertionActionMode();
4601 }
4602 };
4603 }
4604 mTextView.postDelayed(
4605 mInsertionActionModeRunnable,
4606 ViewConfiguration.getDoubleTapTimeout() + 1);
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01004607 }
4608
Gilles Debunned88876a2012-03-16 17:34:04 -07004609 }
4610
4611 hideAfterDelay();
4612 }
4613
Gilles Debunned88876a2012-03-16 17:34:04 -07004614 private void hideAfterDelay() {
4615 if (mHider == null) {
4616 mHider = new Runnable() {
4617 public void run() {
4618 hide();
4619 }
4620 };
4621 } else {
4622 removeHiderCallback();
4623 }
4624 mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
4625 }
4626
4627 private void removeHiderCallback() {
4628 if (mHider != null) {
4629 mTextView.removeCallbacks(mHider);
4630 }
4631 }
4632
4633 @Override
4634 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
4635 return drawable.getIntrinsicWidth() / 2;
4636 }
4637
4638 @Override
Adam Powell3fceabd2014-08-19 18:28:04 -07004639 protected int getHorizontalGravity(boolean isRtlRun) {
4640 return Gravity.CENTER_HORIZONTAL;
4641 }
4642
4643 @Override
4644 protected int getCursorOffset() {
4645 int offset = super.getCursorOffset();
4646 final Drawable cursor = mCursorCount > 0 ? mCursorDrawable[0] : null;
4647 if (cursor != null) {
4648 cursor.getPadding(mTempRect);
4649 offset += (cursor.getIntrinsicWidth() - mTempRect.left - mTempRect.right) / 2;
4650 }
4651 return offset;
4652 }
4653
4654 @Override
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004655 int getCursorHorizontalPosition(Layout layout, int offset) {
4656 final Drawable drawable = mCursorCount > 0 ? mCursorDrawable[0] : null;
4657 if (drawable != null) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004658 final float horizontal = getHorizontal(layout, offset);
Siyamed Sinir987ec652016-02-17 19:44:41 -08004659 return clampHorizontalPosition(drawable, horizontal) + mTempRect.left;
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004660 }
4661 return super.getCursorHorizontalPosition(layout, offset);
4662 }
4663
4664 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07004665 public boolean onTouchEvent(MotionEvent ev) {
4666 final boolean result = super.onTouchEvent(ev);
4667
4668 switch (ev.getActionMasked()) {
4669 case MotionEvent.ACTION_DOWN:
4670 mDownPositionX = ev.getRawX();
4671 mDownPositionY = ev.getRawY();
4672 break;
4673
4674 case MotionEvent.ACTION_UP:
4675 if (!offsetHasBeenChanged()) {
4676 final float deltaX = mDownPositionX - ev.getRawX();
4677 final float deltaY = mDownPositionY - ev.getRawY();
4678 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
4679
4680 final ViewConfiguration viewConfiguration = ViewConfiguration.get(
4681 mTextView.getContext());
4682 final int touchSlop = viewConfiguration.getScaledTouchSlop();
4683
4684 if (distanceSquared < touchSlop * touchSlop) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01004685 // Tapping on the handle toggles the insertion action mode.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004686 if (mTextActionMode != null) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08004687 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07004688 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004689 startInsertionActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07004690 }
4691 }
Abodunrinwa Tokibcdf0ab2015-04-25 00:11:25 +01004692 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004693 if (mTextActionMode != null) {
4694 mTextActionMode.invalidateContentRect();
Abodunrinwa Tokibcdf0ab2015-04-25 00:11:25 +01004695 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004696 }
4697 hideAfterDelay();
4698 break;
4699
4700 case MotionEvent.ACTION_CANCEL:
4701 hideAfterDelay();
4702 break;
4703
4704 default:
4705 break;
4706 }
4707
4708 return result;
4709 }
4710
4711 @Override
4712 public int getCurrentCursorOffset() {
4713 return mTextView.getSelectionStart();
4714 }
4715
4716 @Override
4717 public void updateSelection(int offset) {
4718 Selection.setSelection((Spannable) mTextView.getText(), offset);
4719 }
4720
4721 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004722 protected void updatePosition(float x, float y, boolean fromTouchScreen) {
Mady Melloree3821e2015-06-05 11:12:01 -07004723 Layout layout = mTextView.getLayout();
4724 int offset;
4725 if (layout != null) {
Mady Mellora6a0f782015-07-10 16:43:32 -07004726 if (mPreviousLineTouched == UNSET_LINE) {
4727 mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
4728 }
4729 int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004730 offset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellora6a0f782015-07-10 16:43:32 -07004731 mPreviousLineTouched = currLine;
Mady Melloree3821e2015-06-05 11:12:01 -07004732 } else {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004733 offset = -1;
Mady Melloree3821e2015-06-05 11:12:01 -07004734 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004735 positionAtCursorOffset(offset, false, fromTouchScreen);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004736 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01004737 invalidateActionMode();
Clara Bayarri1baed512015-05-11 15:29:16 +01004738 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004739 }
4740
4741 @Override
4742 void onHandleMoved() {
4743 super.onHandleMoved();
4744 removeHiderCallback();
4745 }
4746
4747 @Override
4748 public void onDetached() {
4749 super.onDetached();
4750 removeHiderCallback();
4751 }
4752 }
4753
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004754 @Retention(RetentionPolicy.SOURCE)
4755 @IntDef({HANDLE_TYPE_SELECTION_START, HANDLE_TYPE_SELECTION_END})
4756 public @interface HandleType {}
4757 public static final int HANDLE_TYPE_SELECTION_START = 0;
4758 public static final int HANDLE_TYPE_SELECTION_END = 1;
4759
4760 private class SelectionHandleView extends HandleView {
4761 // Indicates the handle type, selection start (HANDLE_TYPE_SELECTION_START) or selection
4762 // end (HANDLE_TYPE_SELECTION_END).
4763 @HandleType
4764 private final int mHandleType;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004765 // Indicates whether the cursor is making adjustments within a word.
4766 private boolean mInWord = false;
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004767 // Difference between touch position and word boundary position.
4768 private float mTouchWordDelta;
Mady Mellore264ac32015-06-22 16:46:29 -07004769 // X value of the previous updatePosition call.
4770 private float mPrevX;
4771 // Indicates if the handle has moved a boundary between LTR and RTL text.
4772 private boolean mLanguageDirectionChanged = false;
Mady Mellor42390aa2015-07-24 13:08:42 -07004773 // Distance from edge of horizontally scrolling text view
4774 // to use to switch to character mode.
4775 private final float mTextViewEdgeSlop;
4776 // Used to save text view location.
4777 private final int[] mTextViewLocation = new int[2];
Gilles Debunned88876a2012-03-16 17:34:04 -07004778
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004779 public SelectionHandleView(Drawable drawableLtr, Drawable drawableRtl, int id,
4780 @HandleType int handleType) {
4781 super(drawableLtr, drawableRtl, id);
4782 mHandleType = handleType;
4783 ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext());
Mady Mellor42390aa2015-07-24 13:08:42 -07004784 mTextViewEdgeSlop = viewConfiguration.getScaledTouchSlop() * 4;
Gilles Debunned88876a2012-03-16 17:34:04 -07004785 }
4786
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004787 private boolean isStartHandle() {
4788 return mHandleType == HANDLE_TYPE_SELECTION_START;
4789 }
4790
Gilles Debunned88876a2012-03-16 17:34:04 -07004791 @Override
4792 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004793 if (isRtlRun == isStartHandle()) {
Mady Mellor709386f2015-05-14 12:41:18 -07004794 return drawable.getIntrinsicWidth() / 4;
4795 } else {
4796 return (drawable.getIntrinsicWidth() * 3) / 4;
4797 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004798 }
4799
4800 @Override
Adam Powell3fceabd2014-08-19 18:28:04 -07004801 protected int getHorizontalGravity(boolean isRtlRun) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004802 return (isRtlRun == isStartHandle()) ? Gravity.LEFT : Gravity.RIGHT;
Adam Powell3fceabd2014-08-19 18:28:04 -07004803 }
4804
4805 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07004806 public int getCurrentCursorOffset() {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004807 return isStartHandle() ? mTextView.getSelectionStart() : mTextView.getSelectionEnd();
Gilles Debunned88876a2012-03-16 17:34:04 -07004808 }
4809
4810 @Override
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004811 protected void updateSelection(int offset) {
4812 if (isStartHandle()) {
4813 Selection.setSelection((Spannable) mTextView.getText(), offset,
4814 mTextView.getSelectionEnd());
4815 } else {
4816 Selection.setSelection((Spannable) mTextView.getText(),
4817 mTextView.getSelectionStart(), offset);
4818 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004819 updateDrawable();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004820 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01004821 invalidateActionMode();
Clara Bayarri13152d12015-04-09 12:02:04 +01004822 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004823 }
4824
4825 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004826 protected void updatePosition(float x, float y, boolean fromTouchScreen) {
Mady Mellor81fa3e82015-05-14 09:17:41 -07004827 final Layout layout = mTextView.getLayout();
Mady Mellorcc65c372015-06-17 09:25:19 -07004828 if (layout == null) {
4829 // HandleView will deal appropriately in positionAtCursorOffset when
4830 // layout is null.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004831 positionAndAdjustForCrossingHandles(mTextView.getOffsetForPosition(x, y),
4832 fromTouchScreen);
Mady Mellorcc65c372015-06-17 09:25:19 -07004833 return;
4834 }
4835
Mady Mellora6a0f782015-07-10 16:43:32 -07004836 if (mPreviousLineTouched == UNSET_LINE) {
4837 mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
4838 }
4839
Mady Mellorb9bbbb12015-03-23 11:50:46 -07004840 boolean positionCursor = false;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004841 final int anotherHandleOffset =
4842 isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
Mady Mellora6a0f782015-07-10 16:43:32 -07004843 int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004844 int initialOffset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellor81fa3e82015-05-14 09:17:41 -07004845
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004846 if (isStartHandle() && initialOffset >= anotherHandleOffset
4847 || !isStartHandle() && initialOffset <= anotherHandleOffset) {
4848 // Handles have crossed, bound it to the first selected line and
Mady Mellor81fa3e82015-05-14 09:17:41 -07004849 // adjust by word / char as normal.
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07004850 currLine = layout.getLineForOffset(anotherHandleOffset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004851 initialOffset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellor81fa3e82015-05-14 09:17:41 -07004852 }
4853
4854 int offset = initialOffset;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004855 final int wordEnd = getWordEnd(offset);
4856 final int wordStart = getWordStart(offset);
Gilles Debunned88876a2012-03-16 17:34:04 -07004857
Mady Mellore264ac32015-06-22 16:46:29 -07004858 if (mPrevX == UNSET_X_VALUE) {
4859 mPrevX = x;
4860 }
4861
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004862 final int currentOffset = getCurrentCursorOffset();
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004863 final boolean rtlAtCurrentOffset = isAtRtlRun(layout, currentOffset);
4864 final boolean atRtl = isAtRtlRun(layout, offset);
Mady Mellore264ac32015-06-22 16:46:29 -07004865 final boolean isLvlBoundary = layout.isLevelBoundary(offset);
Mady Mellore264ac32015-06-22 16:46:29 -07004866
4867 // We can't determine if the user is expanding or shrinking the selection if they're
4868 // on a bi-di boundary, so until they've moved past the boundary we'll just place
4869 // the cursor at the current position.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004870 if (isLvlBoundary || (rtlAtCurrentOffset && !atRtl) || (!rtlAtCurrentOffset && atRtl)) {
Mady Mellore264ac32015-06-22 16:46:29 -07004871 // We're on a boundary or this is the first direction change -- just update
4872 // to the current position.
4873 mLanguageDirectionChanged = true;
4874 mTouchWordDelta = 0.0f;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004875 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellore264ac32015-06-22 16:46:29 -07004876 return;
4877 } else if (mLanguageDirectionChanged && !isLvlBoundary) {
4878 // We've just moved past the boundary so update the position. After this we can
4879 // figure out if the user is expanding or shrinking to go by word or character.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004880 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellore264ac32015-06-22 16:46:29 -07004881 mTouchWordDelta = 0.0f;
4882 mLanguageDirectionChanged = false;
4883 return;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004884 }
4885
4886 boolean isExpanding;
4887 final float xDiff = x - mPrevX;
Keisuke Kuroyanagi26454142015-12-02 15:04:57 -08004888 if (isStartHandle()) {
4889 isExpanding = currLine < mPreviousLineTouched;
Mady Mellore264ac32015-06-22 16:46:29 -07004890 } else {
Keisuke Kuroyanagi26454142015-12-02 15:04:57 -08004891 isExpanding = currLine > mPreviousLineTouched;
4892 }
4893 if (atRtl == isStartHandle()) {
4894 isExpanding |= xDiff > 0;
4895 } else {
4896 isExpanding |= xDiff < 0;
Mady Mellore264ac32015-06-22 16:46:29 -07004897 }
4898
Mady Mellor42390aa2015-07-24 13:08:42 -07004899 if (mTextView.getHorizontallyScrolling()) {
4900 if (positionNearEdgeOfScrollingView(x, atRtl)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004901 && ((isStartHandle() && mTextView.getScrollX() != 0)
4902 || (!isStartHandle()
4903 && mTextView.canScrollHorizontally(atRtl ? -1 : 1)))
4904 && ((isExpanding && ((isStartHandle() && offset < currentOffset)
4905 || (!isStartHandle() && offset > currentOffset)))
4906 || !isExpanding)) {
4907 // If we're expanding ensure that the offset is actually expanding compared to
4908 // the current offset, if the handle snapped to the word, the finger position
Mady Mellor42390aa2015-07-24 13:08:42 -07004909 // may be out of sync and we don't want the selection to jump back.
4910 mTouchWordDelta = 0.0f;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004911 final int nextOffset = (atRtl == isStartHandle())
4912 ? layout.getOffsetToRightOf(mPreviousOffset)
Mady Mellor42390aa2015-07-24 13:08:42 -07004913 : layout.getOffsetToLeftOf(mPreviousOffset);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004914 positionAndAdjustForCrossingHandles(nextOffset, fromTouchScreen);
Mady Mellor42390aa2015-07-24 13:08:42 -07004915 return;
4916 }
4917 }
4918
Mady Mellore264ac32015-06-22 16:46:29 -07004919 if (isExpanding) {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004920 // User is increasing the selection.
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004921 int wordBoundary = isStartHandle() ? wordStart : wordEnd;
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07004922 final boolean snapToWord = (!mInWord
4923 || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine))
4924 && atRtl == isAtRtlRun(layout, wordBoundary);
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004925 if (snapToWord) {
Mady Mellora5266832015-06-26 14:28:12 -07004926 // Sometimes words can be broken across lines (Chinese, hyphenation).
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004927 // We still snap to the word boundary but we only use the letters on the
Mady Mellora5266832015-06-26 14:28:12 -07004928 // current line to determine if the user is far enough into the word to snap.
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07004929 if (layout.getLineForOffset(wordBoundary) != currLine) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004930 wordBoundary = isStartHandle()
4931 ? layout.getLineStart(currLine) : layout.getLineEnd(currLine);
Mady Mellora5266832015-06-26 14:28:12 -07004932 }
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004933 final int offsetThresholdToSnap = isStartHandle()
4934 ? wordEnd - ((wordEnd - wordBoundary) / 2)
4935 : wordStart + ((wordBoundary - wordStart) / 2);
4936 if (isStartHandle()
4937 && (offset <= offsetThresholdToSnap || currLine < mPrevLine)) {
4938 // User is far enough into the word or on a different line so we expand by
4939 // word.
4940 offset = wordStart;
4941 } else if (!isStartHandle()
4942 && (offset >= offsetThresholdToSnap || currLine > mPrevLine)) {
4943 // User is far enough into the word or on a different line so we expand by
4944 // word.
4945 offset = wordEnd;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004946 } else {
Mady Mellorc2225b92015-04-01 15:59:20 -07004947 offset = mPreviousOffset;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004948 }
4949 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004950 if ((isStartHandle() && offset < initialOffset)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004951 || (!isStartHandle() && offset > initialOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004952 final float adjustedX = getHorizontal(layout, offset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004953 mTouchWordDelta =
4954 mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
Keisuke Kuroyanagi50a927c2015-05-07 17:34:21 +09004955 } else {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004956 mTouchWordDelta = 0.0f;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004957 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004958 positionCursor = true;
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004959 } else {
4960 final int adjustedOffset =
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004961 getOffsetAtCoordinate(layout, currLine, x - mTouchWordDelta);
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004962 final boolean shrinking = isStartHandle()
4963 ? adjustedOffset > mPreviousOffset || currLine > mPrevLine
4964 : adjustedOffset < mPreviousOffset || currLine < mPrevLine;
4965 if (shrinking) {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004966 // User is shrinking the selection.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004967 if (currLine != mPrevLine) {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004968 // We're on a different line, so we'll snap to word boundaries.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004969 offset = isStartHandle() ? wordStart : wordEnd;
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004970 if ((isStartHandle() && offset < initialOffset)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004971 || (!isStartHandle() && offset > initialOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004972 final float adjustedX = getHorizontal(layout, offset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004973 mTouchWordDelta =
4974 mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
4975 } else {
4976 mTouchWordDelta = 0.0f;
4977 }
4978 } else {
4979 offset = adjustedOffset;
4980 }
4981 positionCursor = true;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004982 } else if ((isStartHandle() && adjustedOffset < mPreviousOffset)
4983 || (!isStartHandle() && adjustedOffset > mPreviousOffset)) {
4984 // Handle has jumped to the word boundary, and the user is moving
Mady Mellor43fd2f42015-06-08 14:03:34 -07004985 // their finger towards the handle, the delta should be updated.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004986 mTouchWordDelta = mTextView.convertToLocalHorizontalCoordinate(x)
4987 - getHorizontal(layout, mPreviousOffset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004988 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004989 }
4990
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004991 if (positionCursor) {
Mady Mellora6a0f782015-07-10 16:43:32 -07004992 mPreviousLineTouched = currLine;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004993 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004994 }
Mady Mellore264ac32015-06-22 16:46:29 -07004995 mPrevX = x;
Gilles Debunned88876a2012-03-16 17:34:04 -07004996 }
4997
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004998 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004999 protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
5000 boolean fromTouchScreen) {
5001 super.positionAtCursorOffset(offset, forceUpdatePosition, fromTouchScreen);
Yoshiki Iguchi9582e152015-10-15 13:34:41 +09005002 mInWord = (offset != -1) && !getWordIteratorWithText().isBoundary(offset);
Mady Mellor36d5a7b2015-05-22 10:31:12 -07005003 }
5004
5005 @Override
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005006 public boolean onTouchEvent(MotionEvent event) {
5007 boolean superResult = super.onTouchEvent(event);
Mady Mellora6a0f782015-07-10 16:43:32 -07005008 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
5009 // Reset the touch word offset and x value when the user
5010 // re-engages the handle.
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005011 mTouchWordDelta = 0.0f;
Mady Mellore264ac32015-06-22 16:46:29 -07005012 mPrevX = UNSET_X_VALUE;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005013 }
5014 return superResult;
5015 }
Mady Mellor42390aa2015-07-24 13:08:42 -07005016
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005017 private void positionAndAdjustForCrossingHandles(int offset, boolean fromTouchScreen) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005018 final int anotherHandleOffset =
5019 isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
5020 if ((isStartHandle() && offset >= anotherHandleOffset)
5021 || (!isStartHandle() && offset <= anotherHandleOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005022 mTouchWordDelta = 0.0f;
5023 final Layout layout = mTextView.getLayout();
5024 if (layout != null && offset != anotherHandleOffset) {
5025 final float horiz = getHorizontal(layout, offset);
5026 final float anotherHandleHoriz = getHorizontal(layout, anotherHandleOffset,
5027 !isStartHandle());
5028 final float currentHoriz = getHorizontal(layout, mPreviousOffset);
5029 if (currentHoriz < anotherHandleHoriz && horiz < anotherHandleHoriz
5030 || currentHoriz > anotherHandleHoriz && horiz > anotherHandleHoriz) {
5031 // This handle passes another one as it crossed a direction boundary.
5032 // Don't minimize the selection, but keep the handle at the run boundary.
5033 final int currentOffset = getCurrentCursorOffset();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005034 final int offsetToGetRunRange = isStartHandle()
5035 ? currentOffset : Math.max(currentOffset - 1, 0);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005036 final long range = layout.getRunRange(offsetToGetRunRange);
5037 if (isStartHandle()) {
5038 offset = TextUtils.unpackRangeStartFromLong(range);
5039 } else {
5040 offset = TextUtils.unpackRangeEndFromLong(range);
5041 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005042 positionAtCursorOffset(offset, false, fromTouchScreen);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005043 return;
5044 }
5045 }
Mady Mellor42390aa2015-07-24 13:08:42 -07005046 // Handles can not cross and selection is at least one character.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005047 offset = getNextCursorOffset(anotherHandleOffset, !isStartHandle());
Mady Mellor42390aa2015-07-24 13:08:42 -07005048 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005049 positionAtCursorOffset(offset, false, fromTouchScreen);
Mady Mellor42390aa2015-07-24 13:08:42 -07005050 }
5051
Mady Mellor42390aa2015-07-24 13:08:42 -07005052 private boolean positionNearEdgeOfScrollingView(float x, boolean atRtl) {
5053 mTextView.getLocationOnScreen(mTextViewLocation);
5054 boolean nearEdge;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005055 if (atRtl == isStartHandle()) {
Mady Mellor42390aa2015-07-24 13:08:42 -07005056 int rightEdge = mTextViewLocation[0] + mTextView.getWidth()
5057 - mTextView.getPaddingRight();
5058 nearEdge = x > rightEdge - mTextViewEdgeSlop;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005059 } else {
5060 int leftEdge = mTextViewLocation[0] + mTextView.getPaddingLeft();
5061 nearEdge = x < leftEdge + mTextViewEdgeSlop;
Mady Mellor42390aa2015-07-24 13:08:42 -07005062 }
5063 return nearEdge;
5064 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005065
5066 @Override
5067 protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
5068 final int offsetToCheck = isStartHandle() ? offset : Math.max(offset - 1, 0);
5069 return layout.isRtlCharAt(offsetToCheck);
5070 }
5071
5072 @Override
5073 public float getHorizontal(@NonNull Layout layout, int offset) {
5074 return getHorizontal(layout, offset, isStartHandle());
5075 }
5076
5077 private float getHorizontal(@NonNull Layout layout, int offset, boolean startHandle) {
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005078 final int line = layout.getLineForOffset(offset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005079 final int offsetToCheck = startHandle ? offset : Math.max(offset - 1, 0);
5080 final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
5081 final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005082 return (isRtlChar == isRtlParagraph)
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005083 ? layout.getPrimaryHorizontal(offset) : layout.getSecondaryHorizontal(offset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005084 }
5085
5086 @Override
5087 protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
Keisuke Kuroyanagib1b88652016-04-05 16:26:16 +09005088 final float localX = mTextView.convertToLocalHorizontalCoordinate(x);
5089 final int primaryOffset = layout.getOffsetForHorizontal(line, localX, true);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005090 if (!layout.isLevelBoundary(primaryOffset)) {
5091 return primaryOffset;
5092 }
Keisuke Kuroyanagib1b88652016-04-05 16:26:16 +09005093 final int secondaryOffset = layout.getOffsetForHorizontal(line, localX, false);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005094 final int currentOffset = getCurrentCursorOffset();
5095 final int primaryDiff = Math.abs(primaryOffset - currentOffset);
5096 final int secondaryDiff = Math.abs(secondaryOffset - currentOffset);
5097 if (primaryDiff < secondaryDiff) {
5098 return primaryOffset;
5099 } else if (primaryDiff > secondaryDiff) {
5100 return secondaryOffset;
5101 } else {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005102 final int offsetToCheck = isStartHandle()
5103 ? currentOffset : Math.max(currentOffset - 1, 0);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005104 final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
5105 final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
5106 return isRtlChar == isRtlParagraph ? primaryOffset : secondaryOffset;
5107 }
5108 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005109 }
5110
Mady Mellorcc65c372015-06-17 09:25:19 -07005111 private int getCurrentLineAdjustedForSlop(Layout layout, int prevLine, float y) {
Mady Mellor80679072015-07-09 16:05:36 -07005112 final int trueLine = mTextView.getLineAtCoordinate(y);
Mady Mellorcc65c372015-06-17 09:25:19 -07005113 if (layout == null || prevLine > layout.getLineCount()
5114 || layout.getLineCount() <= 0 || prevLine < 0) {
5115 // Invalid parameters, just return whatever line is at y.
Mady Mellor80679072015-07-09 16:05:36 -07005116 return trueLine;
5117 }
5118
5119 if (Math.abs(trueLine - prevLine) >= 2) {
5120 // Only stick to lines if we're within a line of the previous selection.
5121 return trueLine;
Mady Mellorcc65c372015-06-17 09:25:19 -07005122 }
5123
5124 final float verticalOffset = mTextView.viewportToContentVerticalOffset();
5125 final int lineCount = layout.getLineCount();
5126 final float slop = mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS;
5127
5128 final float firstLineTop = layout.getLineTop(0) + verticalOffset;
5129 final float prevLineTop = layout.getLineTop(prevLine) + verticalOffset;
5130 final float yTopBound = Math.max(prevLineTop - slop, firstLineTop + slop);
5131
5132 final float lastLineBottom = layout.getLineBottom(lineCount - 1) + verticalOffset;
5133 final float prevLineBottom = layout.getLineBottom(prevLine) + verticalOffset;
5134 final float yBottomBound = Math.min(prevLineBottom + slop, lastLineBottom - slop);
5135
5136 // Determine if we've moved lines based on y position and previous line.
5137 int currLine;
5138 if (y <= yTopBound) {
5139 currLine = Math.max(prevLine - 1, 0);
5140 } else if (y >= yBottomBound) {
5141 currLine = Math.min(prevLine + 1, lineCount - 1);
5142 } else {
5143 currLine = prevLine;
5144 }
5145 return currLine;
5146 }
5147
Gilles Debunned88876a2012-03-16 17:34:04 -07005148 /**
5149 * A CursorController instance can be used to control a cursor in the text.
5150 */
5151 private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
5152 /**
5153 * Makes the cursor controller visible on screen.
5154 * See also {@link #hide()}.
5155 */
5156 public void show();
5157
5158 /**
5159 * Hide the cursor controller from screen.
5160 * See also {@link #show()}.
5161 */
5162 public void hide();
5163
5164 /**
5165 * Called when the view is detached from window. Perform house keeping task, such as
5166 * stopping Runnable thread that would otherwise keep a reference on the context, thus
5167 * preventing the activity from being recycled.
5168 */
5169 public void onDetached();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005170
5171 public boolean isCursorBeingModified();
5172
5173 public boolean isActive();
Gilles Debunned88876a2012-03-16 17:34:04 -07005174 }
5175
5176 private class InsertionPointCursorController implements CursorController {
5177 private InsertionHandleView mHandle;
5178
5179 public void show() {
5180 getHandle().show();
Andrei Stingaceanu35c550c2015-05-07 16:49:49 +01005181
5182 if (mSelectionModifierCursorController != null) {
5183 mSelectionModifierCursorController.hide();
5184 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005185 }
5186
Gilles Debunned88876a2012-03-16 17:34:04 -07005187 public void hide() {
5188 if (mHandle != null) {
5189 mHandle.hide();
5190 }
5191 }
5192
5193 public void onTouchModeChanged(boolean isInTouchMode) {
5194 if (!isInTouchMode) {
5195 hide();
5196 }
5197 }
5198
5199 private InsertionHandleView getHandle() {
5200 if (mSelectHandleCenter == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08005201 mSelectHandleCenter = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07005202 mTextView.mTextSelectHandleRes);
5203 }
5204 if (mHandle == null) {
5205 mHandle = new InsertionHandleView(mSelectHandleCenter);
5206 }
5207 return mHandle;
5208 }
5209
5210 @Override
5211 public void onDetached() {
5212 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
5213 observer.removeOnTouchModeChangeListener(this);
5214
5215 if (mHandle != null) mHandle.onDetached();
5216 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005217
5218 @Override
5219 public boolean isCursorBeingModified() {
5220 return mHandle != null && mHandle.isDragging();
5221 }
5222
5223 @Override
5224 public boolean isActive() {
5225 return mHandle != null && mHandle.isShowing();
5226 }
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09005227
5228 public void invalidateHandle() {
5229 if (mHandle != null) {
5230 mHandle.invalidate();
5231 }
5232 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005233 }
5234
5235 class SelectionModifierCursorController implements CursorController {
Gilles Debunned88876a2012-03-16 17:34:04 -07005236 // The cursor controller handles, lazily created when shown.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005237 private SelectionHandleView mStartHandle;
5238 private SelectionHandleView mEndHandle;
Gilles Debunned88876a2012-03-16 17:34:04 -07005239 // The offsets of that last touch down event. Remembered to start selection there.
5240 private int mMinTouchOffset, mMaxTouchOffset;
5241
Gilles Debunned88876a2012-03-16 17:34:04 -07005242 private float mDownPositionX, mDownPositionY;
5243 private boolean mGestureStayedInTapRegion;
5244
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005245 // Where the user first starts the drag motion.
5246 private int mStartOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005247
Mady Mellor7a936442015-05-20 10:05:52 -07005248 private boolean mHaventMovedEnoughToStartDrag;
Mady Mellorf9f8aeb2015-06-17 09:46:01 -07005249 // The line that a selection happened most recently with the drag accelerator.
5250 private int mLineSelectionIsOn = -1;
5251 // Whether the drag accelerator has selected past the initial line.
5252 private boolean mSwitchedLines = false;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005253
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005254 // Indicates the drag accelerator mode that the user is currently using.
5255 private int mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
5256 // Drag accelerator is inactive.
5257 private static final int DRAG_ACCELERATOR_MODE_INACTIVE = 0;
5258 // Character based selection by dragging. Only for mouse.
5259 private static final int DRAG_ACCELERATOR_MODE_CHARACTER = 1;
5260 // Word based selection by dragging. Enabled after long pressing or double tapping.
5261 private static final int DRAG_ACCELERATOR_MODE_WORD = 2;
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005262 // Paragraph based selection by dragging. Enabled after mouse triple click.
5263 private static final int DRAG_ACCELERATOR_MODE_PARAGRAPH = 3;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005264
Gilles Debunned88876a2012-03-16 17:34:04 -07005265 SelectionModifierCursorController() {
5266 resetTouchOffsets();
5267 }
5268
5269 public void show() {
5270 if (mTextView.isInBatchEditMode()) {
5271 return;
5272 }
5273 initDrawables();
5274 initHandles();
Gilles Debunned88876a2012-03-16 17:34:04 -07005275 }
5276
5277 private void initDrawables() {
5278 if (mSelectHandleLeft == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08005279 mSelectHandleLeft = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07005280 mTextView.mTextSelectHandleLeftRes);
5281 }
5282 if (mSelectHandleRight == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08005283 mSelectHandleRight = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07005284 mTextView.mTextSelectHandleRightRes);
5285 }
5286 }
5287
5288 private void initHandles() {
5289 // Lazy object creation has to be done before updatePosition() is called.
5290 if (mStartHandle == null) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005291 mStartHandle = new SelectionHandleView(mSelectHandleLeft, mSelectHandleRight,
5292 com.android.internal.R.id.selection_start_handle,
5293 HANDLE_TYPE_SELECTION_START);
Gilles Debunned88876a2012-03-16 17:34:04 -07005294 }
5295 if (mEndHandle == null) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005296 mEndHandle = new SelectionHandleView(mSelectHandleRight, mSelectHandleLeft,
5297 com.android.internal.R.id.selection_end_handle,
5298 HANDLE_TYPE_SELECTION_END);
Gilles Debunned88876a2012-03-16 17:34:04 -07005299 }
5300
5301 mStartHandle.show();
5302 mEndHandle.show();
5303
Gilles Debunned88876a2012-03-16 17:34:04 -07005304 hideInsertionPointCursorController();
5305 }
5306
5307 public void hide() {
5308 if (mStartHandle != null) mStartHandle.hide();
5309 if (mEndHandle != null) mEndHandle.hide();
5310 }
5311
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005312 public void enterDrag(int dragAcceleratorMode) {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005313 // Just need to init the handles / hide insertion cursor.
5314 show();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005315 mDragAcceleratorMode = dragAcceleratorMode;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005316 // Start location of selection.
5317 mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX,
5318 mLastDownPositionY);
Mady Mellorf9f8aeb2015-06-17 09:46:01 -07005319 mLineSelectionIsOn = mTextView.getLineAtCoordinate(mLastDownPositionY);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005320 // Don't show the handles until user has lifted finger.
5321 hide();
5322
5323 // This stops scrolling parents from intercepting the touch event, allowing
5324 // the user to continue dragging across the screen to select text; TextView will
5325 // scroll as necessary.
5326 mTextView.getParent().requestDisallowInterceptTouchEvent(true);
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005327 mTextView.cancelLongPress();
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005328 }
5329
Gilles Debunned88876a2012-03-16 17:34:04 -07005330 public void onTouchEvent(MotionEvent event) {
5331 // This is done even when the View does not have focus, so that long presses can start
5332 // selection and tap can move cursor from this tap position.
Mady Mellor7a936442015-05-20 10:05:52 -07005333 final float eventX = event.getX();
5334 final float eventY = event.getY();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005335 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
Gilles Debunned88876a2012-03-16 17:34:04 -07005336 switch (event.getActionMasked()) {
5337 case MotionEvent.ACTION_DOWN:
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005338 if (extractedTextModeWillBeStarted()) {
5339 // Prevent duplicating the selection handles until the mode starts.
5340 hide();
5341 } else {
5342 // Remember finger down position, to be able to start selection from there.
5343 mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(
5344 eventX, eventY);
Gilles Debunned88876a2012-03-16 17:34:04 -07005345
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005346 // Double tap detection
5347 if (mGestureStayedInTapRegion) {
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09005348 if (mTapState == TAP_STATE_DOUBLE_TAP
5349 || mTapState == TAP_STATE_TRIPLE_CLICK) {
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005350 final float deltaX = eventX - mDownPositionX;
5351 final float deltaY = eventY - mDownPositionY;
5352 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005353
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005354 ViewConfiguration viewConfiguration = ViewConfiguration.get(
5355 mTextView.getContext());
5356 int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
5357 boolean stayedInArea =
5358 distanceSquared < doubleTapSlop * doubleTapSlop;
Gilles Debunned88876a2012-03-16 17:34:04 -07005359
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005360 if (stayedInArea && (isMouse || isPositionOnText(eventX, eventY))) {
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09005361 if (mTapState == TAP_STATE_DOUBLE_TAP) {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005362 selectCurrentWordAndStartDrag();
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09005363 } else if (mTapState == TAP_STATE_TRIPLE_CLICK) {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005364 selectCurrentParagraphAndStartDrag();
5365 }
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005366 mDiscardNextActionUp = true;
5367 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005368 }
5369 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005370
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005371 mDownPositionX = eventX;
5372 mDownPositionY = eventY;
5373 mGestureStayedInTapRegion = true;
5374 mHaventMovedEnoughToStartDrag = true;
5375 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005376 break;
5377
5378 case MotionEvent.ACTION_POINTER_DOWN:
5379 case MotionEvent.ACTION_POINTER_UP:
5380 // Handle multi-point gestures. Keep min and max offset positions.
5381 // Only activated for devices that correctly handle multi-touch.
5382 if (mTextView.getContext().getPackageManager().hasSystemFeature(
5383 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
5384 updateMinAndMaxOffsets(event);
5385 }
5386 break;
5387
5388 case MotionEvent.ACTION_MOVE:
Mady Mellor7a936442015-05-20 10:05:52 -07005389 final ViewConfiguration viewConfig = ViewConfiguration.get(
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005390 mTextView.getContext());
Mady Mellor7a936442015-05-20 10:05:52 -07005391 final int touchSlop = viewConfig.getScaledTouchSlop();
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005392
Mady Mellor7a936442015-05-20 10:05:52 -07005393 if (mGestureStayedInTapRegion || mHaventMovedEnoughToStartDrag) {
5394 final float deltaX = eventX - mDownPositionX;
5395 final float deltaY = eventY - mDownPositionY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005396 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
5397
Mady Mellor7a936442015-05-20 10:05:52 -07005398 if (mGestureStayedInTapRegion) {
5399 int doubleTapTouchSlop = viewConfig.getScaledDoubleTapTouchSlop();
5400 mGestureStayedInTapRegion =
5401 distanceSquared <= doubleTapTouchSlop * doubleTapTouchSlop;
5402 }
5403 if (mHaventMovedEnoughToStartDrag) {
5404 // We don't start dragging until the user has moved enough.
5405 mHaventMovedEnoughToStartDrag =
5406 distanceSquared <= touchSlop * touchSlop;
Gilles Debunned88876a2012-03-16 17:34:04 -07005407 }
5408 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005409
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005410 if (isMouse && !isDragAcceleratorActive()) {
5411 final int offset = mTextView.getOffsetForPosition(eventX, eventY);
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09005412 if (mTextView.hasSelection()
5413 && (!mHaventMovedEnoughToStartDrag || mStartOffset != offset)
5414 && offset >= mTextView.getSelectionStart()
5415 && offset <= mTextView.getSelectionEnd()) {
5416 startDragAndDrop();
5417 break;
5418 }
5419
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005420 if (mStartOffset != offset) {
5421 // Start character based drag accelerator.
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005422 stopTextActionMode();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005423 enterDrag(DRAG_ACCELERATOR_MODE_CHARACTER);
5424 mDiscardNextActionUp = true;
5425 mHaventMovedEnoughToStartDrag = false;
5426 }
5427 }
5428
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005429 if (mStartHandle != null && mStartHandle.isShowing()) {
5430 // Don't do the drag if the handles are showing already.
5431 break;
5432 }
5433
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005434 updateSelection(event);
Gilles Debunned88876a2012-03-16 17:34:04 -07005435 break;
5436
5437 case MotionEvent.ACTION_UP:
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005438 if (!isDragAcceleratorActive()) {
5439 break;
5440 }
5441 updateSelection(event);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005442
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005443 // No longer dragging to select text, let the parent intercept events.
5444 mTextView.getParent().requestDisallowInterceptTouchEvent(false);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005445
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005446 // No longer the first dragging motion, reset.
5447 resetDragAcceleratorState();
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09005448
5449 if (mTextView.hasSelection()) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01005450 // Drag selection should not be adjusted by the text classifier.
5451 startSelectionActionModeAsync(mHaventMovedEnoughToStartDrag);
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09005452 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005453 break;
5454 }
5455 }
5456
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005457 private void updateSelection(MotionEvent event) {
5458 if (mTextView.getLayout() != null) {
5459 switch (mDragAcceleratorMode) {
5460 case DRAG_ACCELERATOR_MODE_CHARACTER:
5461 updateCharacterBasedSelection(event);
5462 break;
5463 case DRAG_ACCELERATOR_MODE_WORD:
5464 updateWordBasedSelection(event);
5465 break;
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005466 case DRAG_ACCELERATOR_MODE_PARAGRAPH:
5467 updateParagraphBasedSelection(event);
5468 break;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005469 }
5470 }
5471 }
5472
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005473 /**
5474 * If the TextView allows text selection, selects the current paragraph and starts a drag.
5475 *
5476 * @return true if the drag was started.
5477 */
5478 private boolean selectCurrentParagraphAndStartDrag() {
5479 if (mInsertionActionModeRunnable != null) {
5480 mTextView.removeCallbacks(mInsertionActionModeRunnable);
5481 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005482 stopTextActionMode();
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005483 if (!selectCurrentParagraph()) {
5484 return false;
5485 }
5486 enterDrag(SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_PARAGRAPH);
5487 return true;
5488 }
5489
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005490 private void updateCharacterBasedSelection(MotionEvent event) {
5491 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005492 updateSelectionInternal(mStartOffset, offset,
5493 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005494 }
5495
5496 private void updateWordBasedSelection(MotionEvent event) {
5497 if (mHaventMovedEnoughToStartDrag) {
5498 return;
5499 }
5500 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
5501 final ViewConfiguration viewConfig = ViewConfiguration.get(
5502 mTextView.getContext());
5503 final float eventX = event.getX();
5504 final float eventY = event.getY();
5505 final int currLine;
5506 if (isMouse) {
5507 // No need to offset the y coordinate for mouse input.
5508 currLine = mTextView.getLineAtCoordinate(eventY);
5509 } else {
5510 float y = eventY;
5511 if (mSwitchedLines) {
5512 // Offset the finger by the same vertical offset as the handles.
5513 // This improves visibility of the content being selected by
5514 // shifting the finger below the content, this is applied once
5515 // the user has switched lines.
5516 final int touchSlop = viewConfig.getScaledTouchSlop();
5517 final float fingerOffset = (mStartHandle != null)
5518 ? mStartHandle.getIdealVerticalOffset()
5519 : touchSlop;
5520 y = eventY - fingerOffset;
5521 }
5522
5523 currLine = getCurrentLineAdjustedForSlop(mTextView.getLayout(), mLineSelectionIsOn,
5524 y);
5525 if (!mSwitchedLines && currLine != mLineSelectionIsOn) {
5526 // Break early here, we want to offset the finger position from
5527 // the selection highlight, once the user moved their finger
5528 // to a different line we should apply the offset and *not* switch
5529 // lines until recomputing the position with the finger offset.
5530 mSwitchedLines = true;
5531 return;
5532 }
5533 }
5534
5535 int startOffset;
5536 int offset = mTextView.getOffsetAtCoordinate(currLine, eventX);
5537 // Snap to word boundaries.
5538 if (mStartOffset < offset) {
5539 // Expanding with end handle.
5540 offset = getWordEnd(offset);
5541 startOffset = getWordStart(mStartOffset);
5542 } else {
5543 // Expanding with start handle.
5544 offset = getWordStart(offset);
5545 startOffset = getWordEnd(mStartOffset);
Keisuke Kuroyanagi133dfc02016-07-21 18:07:23 +09005546 if (startOffset == offset) {
5547 offset = getNextCursorOffset(offset, false);
5548 }
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005549 }
5550 mLineSelectionIsOn = currLine;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005551 updateSelectionInternal(startOffset, offset,
5552 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005553 }
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005554
5555 private void updateParagraphBasedSelection(MotionEvent event) {
5556 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
5557
5558 final int start = Math.min(offset, mStartOffset);
5559 final int end = Math.max(offset, mStartOffset);
5560 final long paragraphsRange = getParagraphsRange(start, end);
5561 final int selectionStart = TextUtils.unpackRangeStartFromLong(paragraphsRange);
5562 final int selectionEnd = TextUtils.unpackRangeEndFromLong(paragraphsRange);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005563 updateSelectionInternal(selectionStart, selectionEnd,
5564 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
5565 }
5566
5567 private void updateSelectionInternal(int selectionStart, int selectionEnd,
5568 boolean fromTouchScreen) {
5569 final boolean performHapticFeedback = fromTouchScreen && mHapticTextHandleEnabled
5570 && ((mTextView.getSelectionStart() != selectionStart)
5571 || (mTextView.getSelectionEnd() != selectionEnd));
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005572 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005573 if (performHapticFeedback) {
5574 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
5575 }
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005576 }
5577
Gilles Debunned88876a2012-03-16 17:34:04 -07005578 /**
5579 * @param event
5580 */
5581 private void updateMinAndMaxOffsets(MotionEvent event) {
5582 int pointerCount = event.getPointerCount();
5583 for (int index = 0; index < pointerCount; index++) {
5584 int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
5585 if (offset < mMinTouchOffset) mMinTouchOffset = offset;
5586 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
5587 }
5588 }
5589
5590 public int getMinTouchOffset() {
5591 return mMinTouchOffset;
5592 }
5593
5594 public int getMaxTouchOffset() {
5595 return mMaxTouchOffset;
5596 }
5597
5598 public void resetTouchOffsets() {
5599 mMinTouchOffset = mMaxTouchOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005600 resetDragAcceleratorState();
5601 }
5602
5603 private void resetDragAcceleratorState() {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005604 mStartOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005605 mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
Mady Mellorf9f8aeb2015-06-17 09:46:01 -07005606 mSwitchedLines = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005607 final int selectionStart = mTextView.getSelectionStart();
5608 final int selectionEnd = mTextView.getSelectionEnd();
5609 if (selectionStart > selectionEnd) {
5610 Selection.setSelection((Spannable) mTextView.getText(),
5611 selectionEnd, selectionStart);
5612 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005613 }
5614
5615 /**
5616 * @return true iff this controller is currently used to move the selection start.
5617 */
5618 public boolean isSelectionStartDragged() {
5619 return mStartHandle != null && mStartHandle.isDragging();
5620 }
5621
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005622 @Override
5623 public boolean isCursorBeingModified() {
5624 return isDragAcceleratorActive() || isSelectionStartDragged()
5625 || (mEndHandle != null && mEndHandle.isDragging());
5626 }
5627
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005628 /**
5629 * @return true if the user is selecting text using the drag accelerator.
5630 */
5631 public boolean isDragAcceleratorActive() {
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005632 return mDragAcceleratorMode != DRAG_ACCELERATOR_MODE_INACTIVE;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005633 }
5634
Gilles Debunned88876a2012-03-16 17:34:04 -07005635 public void onTouchModeChanged(boolean isInTouchMode) {
5636 if (!isInTouchMode) {
5637 hide();
5638 }
5639 }
5640
5641 @Override
5642 public void onDetached() {
5643 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
5644 observer.removeOnTouchModeChangeListener(this);
5645
5646 if (mStartHandle != null) mStartHandle.onDetached();
5647 if (mEndHandle != null) mEndHandle.onDetached();
5648 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005649
5650 @Override
5651 public boolean isActive() {
5652 return mStartHandle != null && mStartHandle.isShowing();
5653 }
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09005654
5655 public void invalidateHandles() {
5656 if (mStartHandle != null) {
5657 mStartHandle.invalidate();
5658 }
5659 if (mEndHandle != null) {
5660 mEndHandle.invalidate();
5661 }
5662 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005663 }
5664
5665 private class CorrectionHighlighter {
5666 private final Path mPath = new Path();
Chris Craik6a49dde2015-05-12 10:28:14 -07005667 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
Gilles Debunned88876a2012-03-16 17:34:04 -07005668 private int mStart, mEnd;
5669 private long mFadingStartTime;
5670 private RectF mTempRectF;
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005671 private static final int FADE_OUT_DURATION = 400;
Gilles Debunned88876a2012-03-16 17:34:04 -07005672
5673 public CorrectionHighlighter() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005674 mPaint.setCompatibilityScaling(
5675 mTextView.getResources().getCompatibilityInfo().applicationScale);
Gilles Debunned88876a2012-03-16 17:34:04 -07005676 mPaint.setStyle(Paint.Style.FILL);
5677 }
5678
5679 public void highlight(CorrectionInfo info) {
5680 mStart = info.getOffset();
5681 mEnd = mStart + info.getNewText().length();
5682 mFadingStartTime = SystemClock.uptimeMillis();
5683
5684 if (mStart < 0 || mEnd < 0) {
5685 stopAnimation();
5686 }
5687 }
5688
5689 public void draw(Canvas canvas, int cursorOffsetVertical) {
5690 if (updatePath() && updatePaint()) {
5691 if (cursorOffsetVertical != 0) {
5692 canvas.translate(0, cursorOffsetVertical);
5693 }
5694
5695 canvas.drawPath(mPath, mPaint);
5696
5697 if (cursorOffsetVertical != 0) {
5698 canvas.translate(0, -cursorOffsetVertical);
5699 }
5700 invalidate(true); // TODO invalidate cursor region only
5701 } else {
5702 stopAnimation();
5703 invalidate(false); // TODO invalidate cursor region only
5704 }
5705 }
5706
5707 private boolean updatePaint() {
5708 final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
5709 if (duration > FADE_OUT_DURATION) return false;
5710
5711 final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
5712 final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005713 final int color = (mTextView.mHighlightColor & 0x00FFFFFF)
5714 + ((int) (highlightColorAlpha * coef) << 24);
Gilles Debunned88876a2012-03-16 17:34:04 -07005715 mPaint.setColor(color);
5716 return true;
5717 }
5718
5719 private boolean updatePath() {
5720 final Layout layout = mTextView.getLayout();
5721 if (layout == null) return false;
5722
5723 // Update in case text is edited while the animation is run
5724 final int length = mTextView.getText().length();
5725 int start = Math.min(length, mStart);
5726 int end = Math.min(length, mEnd);
5727
5728 mPath.reset();
5729 layout.getSelectionPath(start, end, mPath);
5730 return true;
5731 }
5732
5733 private void invalidate(boolean delayed) {
5734 if (mTextView.getLayout() == null) return;
5735
5736 if (mTempRectF == null) mTempRectF = new RectF();
5737 mPath.computeBounds(mTempRectF, false);
5738
5739 int left = mTextView.getCompoundPaddingLeft();
5740 int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
5741
5742 if (delayed) {
5743 mTextView.postInvalidateOnAnimation(
5744 left + (int) mTempRectF.left, top + (int) mTempRectF.top,
5745 left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
5746 } else {
5747 mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
5748 (int) mTempRectF.right, (int) mTempRectF.bottom);
5749 }
5750 }
5751
5752 private void stopAnimation() {
5753 Editor.this.mCorrectionHighlighter = null;
5754 }
5755 }
5756
5757 private static class ErrorPopup extends PopupWindow {
5758 private boolean mAbove = false;
5759 private final TextView mView;
5760 private int mPopupInlineErrorBackgroundId = 0;
5761 private int mPopupInlineErrorAboveBackgroundId = 0;
5762
5763 ErrorPopup(TextView v, int width, int height) {
5764 super(v, width, height);
5765 mView = v;
5766 // 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 -08005767 // shown and positioned. Initialized with below background, which should have
Gilles Debunned88876a2012-03-16 17:34:04 -07005768 // dimensions identical to the above version for this to work (and is more likely).
5769 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
5770 com.android.internal.R.styleable.Theme_errorMessageBackground);
5771 mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
5772 }
5773
5774 void fixDirection(boolean above) {
5775 mAbove = above;
5776
5777 if (above) {
5778 mPopupInlineErrorAboveBackgroundId =
5779 getResourceId(mPopupInlineErrorAboveBackgroundId,
5780 com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
5781 } else {
5782 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
5783 com.android.internal.R.styleable.Theme_errorMessageBackground);
5784 }
5785
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005786 mView.setBackgroundResource(
5787 above ? mPopupInlineErrorAboveBackgroundId : mPopupInlineErrorBackgroundId);
Gilles Debunned88876a2012-03-16 17:34:04 -07005788 }
5789
5790 private int getResourceId(int currentId, int index) {
5791 if (currentId == 0) {
5792 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
5793 R.styleable.Theme);
5794 currentId = styledAttributes.getResourceId(index, 0);
5795 styledAttributes.recycle();
5796 }
5797 return currentId;
5798 }
5799
5800 @Override
5801 public void update(int x, int y, int w, int h, boolean force) {
5802 super.update(x, y, w, h, force);
5803
5804 boolean above = isAboveAnchor();
5805 if (above != mAbove) {
5806 fixDirection(above);
5807 }
5808 }
5809 }
5810
5811 static class InputContentType {
5812 int imeOptions = EditorInfo.IME_NULL;
5813 String privateImeOptions;
5814 CharSequence imeActionLabel;
5815 int imeActionId;
5816 Bundle extras;
5817 OnEditorActionListener onEditorActionListener;
5818 boolean enterDown;
Yohei Yukawad469f212016-01-21 12:38:09 -08005819 LocaleList imeHintLocales;
Gilles Debunned88876a2012-03-16 17:34:04 -07005820 }
5821
5822 static class InputMethodState {
Gilles Debunnec62589c2012-04-12 14:50:23 -07005823 ExtractedTextRequest mExtractedTextRequest;
5824 final ExtractedText mExtractedText = new ExtractedText();
Gilles Debunned88876a2012-03-16 17:34:04 -07005825 int mBatchEditNesting;
5826 boolean mCursorChanged;
5827 boolean mSelectionModeChanged;
5828 boolean mContentChanged;
5829 int mChangedStart, mChangedEnd, mChangedDelta;
5830 }
Satoshi Kataoka0e3849a2012-12-13 14:37:19 +09005831
James Cookf59152c2015-02-26 18:03:58 -08005832 /**
James Cook471559f2015-02-27 10:31:20 -08005833 * @return True iff (start, end) is a valid range within the text.
5834 */
5835 private static boolean isValidRange(CharSequence text, int start, int end) {
5836 return 0 <= start && start <= end && end <= text.length();
5837 }
5838
Seigo Nonakaa60160b2015-08-19 12:38:35 -07005839 @VisibleForTesting
5840 public SuggestionsPopupWindow getSuggestionsPopupWindowForTesting() {
5841 return mSuggestionsPopupWindow;
5842 }
5843
James Cook471559f2015-02-27 10:31:20 -08005844 /**
James Cookf59152c2015-02-26 18:03:58 -08005845 * An InputFilter that monitors text input to maintain undo history. It does not modify the
5846 * text being typed (and hence always returns null from the filter() method).
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005847 *
5848 * TODO: Make this span aware.
James Cookf59152c2015-02-26 18:03:58 -08005849 */
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005850 public static class UndoInputFilter implements InputFilter {
James Cookf59152c2015-02-26 18:03:58 -08005851 private final Editor mEditor;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005852
James Cook48e0fac2015-02-25 15:44:51 -08005853 // Whether the current filter pass is directly caused by an end-user text edit.
5854 private boolean mIsUserEdit;
5855
James Cookd2026682015-03-03 14:40:14 -08005856 // Whether the text field is handling an IME composition. Must be parceled in case the user
5857 // rotates the screen during composition.
5858 private boolean mHasComposition;
James Cook48e0fac2015-02-25 15:44:51 -08005859
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005860 // Whether the user is expanding or shortening the text
5861 private boolean mExpanding;
5862
5863 // Whether the previous edit operation was in the current batch edit.
5864 private boolean mPreviousOperationWasInSameBatchEdit;
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08005865
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005866 public UndoInputFilter(Editor editor) {
5867 mEditor = editor;
5868 }
5869
James Cookd2026682015-03-03 14:40:14 -08005870 public void saveInstanceState(Parcel parcel) {
5871 parcel.writeInt(mIsUserEdit ? 1 : 0);
5872 parcel.writeInt(mHasComposition ? 1 : 0);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005873 parcel.writeInt(mExpanding ? 1 : 0);
5874 parcel.writeInt(mPreviousOperationWasInSameBatchEdit ? 1 : 0);
James Cookd2026682015-03-03 14:40:14 -08005875 }
5876
5877 public void restoreInstanceState(Parcel parcel) {
5878 mIsUserEdit = parcel.readInt() != 0;
5879 mHasComposition = parcel.readInt() != 0;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005880 mExpanding = parcel.readInt() != 0;
5881 mPreviousOperationWasInSameBatchEdit = parcel.readInt() != 0;
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08005882 }
5883
James Cook48e0fac2015-02-25 15:44:51 -08005884 /**
5885 * Signals that a user-triggered edit is starting.
5886 */
5887 public void beginBatchEdit() {
5888 if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit");
5889 mIsUserEdit = true;
James Cook48e0fac2015-02-25 15:44:51 -08005890 }
5891
5892 public void endBatchEdit() {
5893 if (DEBUG_UNDO) Log.d(TAG, "endBatchEdit");
5894 mIsUserEdit = false;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005895 mPreviousOperationWasInSameBatchEdit = false;
James Cook48e0fac2015-02-25 15:44:51 -08005896 }
5897
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005898 @Override
5899 public CharSequence filter(CharSequence source, int start, int end,
5900 Spanned dest, int dstart, int dend) {
5901 if (DEBUG_UNDO) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005902 Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ") "
5903 + "dest=" + dest + " (" + dstart + "-" + dend + ")");
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005904 }
James Cookf1dad1e2015-02-27 11:00:01 -08005905
James Cook48e0fac2015-02-25 15:44:51 -08005906 // Check to see if this edit should be tracked for undo.
5907 if (!canUndoEdit(source, start, end, dest, dstart, dend)) {
James Cookf1dad1e2015-02-27 11:00:01 -08005908 return null;
5909 }
5910
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005911 final boolean hadComposition = mHasComposition;
5912 mHasComposition = isComposition(source);
5913 final boolean wasExpanding = mExpanding;
5914 boolean shouldCreateSeparateState = false;
5915 if ((end - start) != (dend - dstart)) {
5916 mExpanding = (end - start) > (dend - dstart);
5917 if (hadComposition && mExpanding != wasExpanding) {
5918 shouldCreateSeparateState = true;
5919 }
James Cookd2026682015-03-03 14:40:14 -08005920 }
5921
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005922 // Handle edit.
5923 handleEdit(source, start, end, dest, dstart, dend, shouldCreateSeparateState);
James Cookd2026682015-03-03 14:40:14 -08005924 return null;
5925 }
5926
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09005927 void freezeLastEdit() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005928 mEditor.mUndoManager.beginUpdate("Edit text");
5929 EditOperation lastEdit = getLastEdit();
5930 if (lastEdit != null) {
5931 lastEdit.mFrozen = true;
James Cookd2026682015-03-03 14:40:14 -08005932 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005933 mEditor.mUndoManager.endUpdate();
James Cookd2026682015-03-03 14:40:14 -08005934 }
5935
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005936 @Retention(RetentionPolicy.SOURCE)
5937 @IntDef({MERGE_EDIT_MODE_FORCE_MERGE, MERGE_EDIT_MODE_NEVER_MERGE, MERGE_EDIT_MODE_NORMAL})
5938 private @interface MergeMode {}
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005939 private static final int MERGE_EDIT_MODE_FORCE_MERGE = 0;
5940 private static final int MERGE_EDIT_MODE_NEVER_MERGE = 1;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005941 /** Use {@link EditOperation#mergeWith} to merge */
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005942 private static final int MERGE_EDIT_MODE_NORMAL = 2;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005943
5944 private void handleEdit(CharSequence source, int start, int end,
5945 Spanned dest, int dstart, int dend, boolean shouldCreateSeparateState) {
James Cook48e0fac2015-02-25 15:44:51 -08005946 // An application may install a TextWatcher to provide additional modifications after
5947 // the initial input filters run (e.g. a credit card formatter that adds spaces to a
5948 // string). This results in multiple filter() calls for what the user considers to be
5949 // a single operation. Always undo the whole set of changes in one step.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005950 @MergeMode
5951 final int mergeMode;
5952 if (isInTextWatcher() || mPreviousOperationWasInSameBatchEdit) {
5953 mergeMode = MERGE_EDIT_MODE_FORCE_MERGE;
5954 } else if (shouldCreateSeparateState) {
5955 mergeMode = MERGE_EDIT_MODE_NEVER_MERGE;
5956 } else {
5957 mergeMode = MERGE_EDIT_MODE_NORMAL;
5958 }
James Cook471559f2015-02-27 10:31:20 -08005959 // Build a new operation with all the information from this edit.
James Cookd2026682015-03-03 14:40:14 -08005960 String newText = TextUtils.substring(source, start, end);
5961 String oldText = TextUtils.substring(dest, dstart, dend);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005962 EditOperation edit = new EditOperation(mEditor, oldText, dstart, newText,
5963 mHasComposition);
5964 if (mHasComposition && TextUtils.equals(edit.mNewText, edit.mOldText)) {
5965 return;
5966 }
5967 recordEdit(edit, mergeMode);
James Cookd2026682015-03-03 14:40:14 -08005968 }
James Cook471559f2015-02-27 10:31:20 -08005969
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005970 private EditOperation getLastEdit() {
5971 final UndoManager um = mEditor.mUndoManager;
5972 return um.getLastOperation(
5973 EditOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE);
5974 }
James Cook22054252015-03-25 14:04:01 -07005975 /**
5976 * Fetches the last undo operation and checks to see if a new edit should be merged into it.
5977 * If forceMerge is true then the new edit is always merged.
5978 */
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005979 private void recordEdit(EditOperation edit, @MergeMode int mergeMode) {
James Cook471559f2015-02-27 10:31:20 -08005980 // Fetch the last edit operation and attempt to merge in the new edit.
James Cook48e0fac2015-02-25 15:44:51 -08005981 final UndoManager um = mEditor.mUndoManager;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07005982 um.beginUpdate("Edit text");
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005983 EditOperation lastEdit = getLastEdit();
James Cook471559f2015-02-27 10:31:20 -08005984 if (lastEdit == null) {
5985 // Add this as the first edit.
5986 if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit);
5987 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005988 } else if (mergeMode == MERGE_EDIT_MODE_FORCE_MERGE) {
James Cook22054252015-03-25 14:04:01 -07005989 // Forced merges take priority because they could be the result of a non-user-edit
5990 // change and this case should not create a new undo operation.
5991 if (DEBUG_UNDO) Log.d(TAG, "filter: force merge " + edit);
5992 lastEdit.forceMergeWith(edit);
James Cook48e0fac2015-02-25 15:44:51 -08005993 } else if (!mIsUserEdit) {
5994 // An application directly modified the Editable outside of a text edit. Treat this
5995 // as a new change and don't attempt to merge.
5996 if (DEBUG_UNDO) Log.d(TAG, "non-user edit, new op " + edit);
5997 um.commitState(mEditor.mUndoOwner);
5998 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09005999 } else if (mergeMode == MERGE_EDIT_MODE_NORMAL && lastEdit.mergeWith(edit)) {
James Cook471559f2015-02-27 10:31:20 -08006000 // Merge succeeded, nothing else to do.
6001 if (DEBUG_UNDO) Log.d(TAG, "filter: merge succeeded, created " + lastEdit);
James Cook3ac0bcb2015-02-26 10:53:41 -08006002 } else {
James Cook471559f2015-02-27 10:31:20 -08006003 // Could not merge with the last edit, so commit the last edit and add this edit.
6004 if (DEBUG_UNDO) Log.d(TAG, "filter: merge failed, adding " + edit);
6005 um.commitState(mEditor.mUndoOwner);
6006 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
James Cook3ac0bcb2015-02-26 10:53:41 -08006007 }
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09006008 mPreviousOperationWasInSameBatchEdit = mIsUserEdit;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006009 um.endUpdate();
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006010 }
James Cook48e0fac2015-02-25 15:44:51 -08006011
6012 private boolean canUndoEdit(CharSequence source, int start, int end,
6013 Spanned dest, int dstart, int dend) {
6014 if (!mEditor.mAllowUndo) {
6015 if (DEBUG_UNDO) Log.d(TAG, "filter: undo is disabled");
6016 return false;
6017 }
6018
6019 if (mEditor.mUndoManager.isInUndo()) {
6020 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping, currently performing undo/redo");
6021 return false;
6022 }
6023
6024 // Text filters run before input operations are applied. However, some input operations
6025 // are invalid and will throw exceptions when applied. This is common in tests. Don't
6026 // attempt to undo invalid operations.
6027 if (!isValidRange(source, start, end) || !isValidRange(dest, dstart, dend)) {
6028 if (DEBUG_UNDO) Log.d(TAG, "filter: invalid op");
6029 return false;
6030 }
6031
6032 // Earlier filters can rewrite input to be a no-op, for example due to a length limit
6033 // on an input field. Skip no-op changes.
6034 if (start == end && dstart == dend) {
6035 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping no-op");
6036 return false;
6037 }
6038
6039 return true;
6040 }
James Cookd2026682015-03-03 14:40:14 -08006041
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006042 private static boolean isComposition(CharSequence source) {
James Cookd2026682015-03-03 14:40:14 -08006043 if (!(source instanceof Spannable)) {
6044 return false;
6045 }
6046 // This is a composition edit if the source has a non-zero-length composing span.
6047 Spannable text = (Spannable) source;
6048 int composeBegin = EditableInputConnection.getComposingSpanStart(text);
6049 int composeEnd = EditableInputConnection.getComposingSpanEnd(text);
6050 return composeBegin < composeEnd;
6051 }
6052
6053 private boolean isInTextWatcher() {
6054 CharSequence text = mEditor.mTextView.getText();
6055 return (text instanceof SpannableStringBuilder)
6056 && ((SpannableStringBuilder) text).getTextWatcherDepth() > 0;
6057 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006058 }
6059
James Cookf59152c2015-02-26 18:03:58 -08006060 /**
6061 * An operation to undo a single "edit" to a text view.
6062 */
James Cook471559f2015-02-27 10:31:20 -08006063 public static class EditOperation extends UndoOperation<Editor> {
6064 private static final int TYPE_INSERT = 0;
6065 private static final int TYPE_DELETE = 1;
6066 private static final int TYPE_REPLACE = 2;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006067
James Cook471559f2015-02-27 10:31:20 -08006068 private int mType;
6069 private String mOldText;
James Cook471559f2015-02-27 10:31:20 -08006070 private String mNewText;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006071 private int mStart;
James Cook471559f2015-02-27 10:31:20 -08006072
6073 private int mOldCursorPos;
6074 private int mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006075 private boolean mFrozen;
6076 private boolean mIsComposition;
James Cook471559f2015-02-27 10:31:20 -08006077
6078 /**
James Cookd2026682015-03-03 14:40:14 -08006079 * Constructs an edit operation from a text input operation on editor that replaces the
James Cook22054252015-03-25 14:04:01 -07006080 * oldText starting at dstart with newText.
James Cook471559f2015-02-27 10:31:20 -08006081 */
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006082 public EditOperation(Editor editor, String oldText, int dstart, String newText,
6083 boolean isComposition) {
James Cook471559f2015-02-27 10:31:20 -08006084 super(editor.mUndoOwner);
James Cookd2026682015-03-03 14:40:14 -08006085 mOldText = oldText;
6086 mNewText = newText;
James Cook471559f2015-02-27 10:31:20 -08006087
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006088 // Determine the type of the edit.
James Cook471559f2015-02-27 10:31:20 -08006089 if (mNewText.length() > 0 && mOldText.length() == 0) {
6090 mType = TYPE_INSERT;
James Cook471559f2015-02-27 10:31:20 -08006091 } else if (mNewText.length() == 0 && mOldText.length() > 0) {
6092 mType = TYPE_DELETE;
James Cook471559f2015-02-27 10:31:20 -08006093 } else {
6094 mType = TYPE_REPLACE;
James Cook471559f2015-02-27 10:31:20 -08006095 }
6096
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006097 mStart = dstart;
James Cook471559f2015-02-27 10:31:20 -08006098 // Store cursor data.
6099 mOldCursorPos = editor.mTextView.getSelectionStart();
James Cookd2026682015-03-03 14:40:14 -08006100 mNewCursorPos = dstart + mNewText.length();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006101 mIsComposition = isComposition;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006102 }
6103
James Cook471559f2015-02-27 10:31:20 -08006104 public EditOperation(Parcel src, ClassLoader loader) {
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006105 super(src, loader);
James Cook471559f2015-02-27 10:31:20 -08006106 mType = src.readInt();
6107 mOldText = src.readString();
James Cook471559f2015-02-27 10:31:20 -08006108 mNewText = src.readString();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006109 mStart = src.readInt();
James Cook471559f2015-02-27 10:31:20 -08006110 mOldCursorPos = src.readInt();
6111 mNewCursorPos = src.readInt();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006112 mFrozen = src.readInt() == 1;
6113 mIsComposition = src.readInt() == 1;
James Cook471559f2015-02-27 10:31:20 -08006114 }
6115
6116 @Override
6117 public void writeToParcel(Parcel dest, int flags) {
6118 dest.writeInt(mType);
6119 dest.writeString(mOldText);
James Cook471559f2015-02-27 10:31:20 -08006120 dest.writeString(mNewText);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006121 dest.writeInt(mStart);
James Cook471559f2015-02-27 10:31:20 -08006122 dest.writeInt(mOldCursorPos);
6123 dest.writeInt(mNewCursorPos);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006124 dest.writeInt(mFrozen ? 1 : 0);
6125 dest.writeInt(mIsComposition ? 1 : 0);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006126 }
6127
James Cook48e0fac2015-02-25 15:44:51 -08006128 private int getNewTextEnd() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006129 return mStart + mNewText.length();
James Cook48e0fac2015-02-25 15:44:51 -08006130 }
6131
6132 private int getOldTextEnd() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006133 return mStart + mOldText.length();
James Cook48e0fac2015-02-25 15:44:51 -08006134 }
6135
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006136 @Override
6137 public void commit() {
6138 }
6139
6140 @Override
6141 public void undo() {
James Cook471559f2015-02-27 10:31:20 -08006142 if (DEBUG_UNDO) Log.d(TAG, "undo");
6143 // Remove the new text and insert the old.
James Cook48e0fac2015-02-25 15:44:51 -08006144 Editor editor = getOwnerData();
6145 Editable text = (Editable) editor.mTextView.getText();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006146 modifyText(text, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006147 }
6148
6149 @Override
6150 public void redo() {
James Cook471559f2015-02-27 10:31:20 -08006151 if (DEBUG_UNDO) Log.d(TAG, "redo");
6152 // Remove the old text and insert the new.
James Cook48e0fac2015-02-25 15:44:51 -08006153 Editor editor = getOwnerData();
6154 Editable text = (Editable) editor.mTextView.getText();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006155 modifyText(text, mStart, getOldTextEnd(), mNewText, mStart, mNewCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006156 }
6157
James Cook471559f2015-02-27 10:31:20 -08006158 /**
6159 * Attempts to merge this existing operation with a new edit.
6160 * @param edit The new edit operation.
6161 * @return If the merge succeeded, returns true. Otherwise returns false and leaves this
6162 * object unchanged.
6163 */
6164 private boolean mergeWith(EditOperation edit) {
James Cook48e0fac2015-02-25 15:44:51 -08006165 if (DEBUG_UNDO) {
6166 Log.d(TAG, "mergeWith old " + this);
6167 Log.d(TAG, "mergeWith new " + edit);
6168 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006169
6170 if (mFrozen) {
6171 return false;
6172 }
6173
James Cook471559f2015-02-27 10:31:20 -08006174 switch (mType) {
6175 case TYPE_INSERT:
6176 return mergeInsertWith(edit);
6177 case TYPE_DELETE:
6178 return mergeDeleteWith(edit);
6179 case TYPE_REPLACE:
6180 return mergeReplaceWith(edit);
6181 default:
6182 return false;
6183 }
6184 }
6185
6186 private boolean mergeInsertWith(EditOperation edit) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006187 if (edit.mType == TYPE_INSERT) {
6188 // Merge insertions that are contiguous even when it's frozen.
6189 if (getNewTextEnd() != edit.mStart) {
6190 return false;
6191 }
6192 mNewText += edit.mNewText;
6193 mNewCursorPos = edit.mNewCursorPos;
6194 mFrozen = edit.mFrozen;
6195 mIsComposition = edit.mIsComposition;
6196 return true;
James Cook471559f2015-02-27 10:31:20 -08006197 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006198 if (mIsComposition && edit.mType == TYPE_REPLACE
6199 && mStart <= edit.mStart && getNewTextEnd() >= edit.getOldTextEnd()) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006200 // Merge insertion with replace as they can be single insertion.
6201 mNewText = mNewText.substring(0, edit.mStart - mStart) + edit.mNewText
6202 + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
6203 mNewCursorPos = edit.mNewCursorPos;
6204 mIsComposition = edit.mIsComposition;
6205 return true;
James Cook471559f2015-02-27 10:31:20 -08006206 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006207 return false;
James Cook471559f2015-02-27 10:31:20 -08006208 }
6209
6210 // TODO: Support forward delete.
6211 private boolean mergeDeleteWith(EditOperation edit) {
James Cook471559f2015-02-27 10:31:20 -08006212 // Only merge continuous deletes.
6213 if (edit.mType != TYPE_DELETE) {
6214 return false;
6215 }
6216 // Only merge deletions that are contiguous.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006217 if (mStart != edit.getOldTextEnd()) {
James Cook471559f2015-02-27 10:31:20 -08006218 return false;
6219 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006220 mStart = edit.mStart;
James Cook471559f2015-02-27 10:31:20 -08006221 mOldText = edit.mOldText + mOldText;
6222 mNewCursorPos = edit.mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006223 mIsComposition = edit.mIsComposition;
James Cook471559f2015-02-27 10:31:20 -08006224 return true;
6225 }
6226
6227 private boolean mergeReplaceWith(EditOperation edit) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006228 if (edit.mType == TYPE_INSERT && getNewTextEnd() == edit.mStart) {
6229 // Merge with adjacent insert.
6230 mNewText += edit.mNewText;
6231 mNewCursorPos = edit.mNewCursorPos;
6232 return true;
6233 }
6234 if (!mIsComposition) {
James Cook471559f2015-02-27 10:31:20 -08006235 return false;
6236 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006237 if (edit.mType == TYPE_DELETE && mStart <= edit.mStart
6238 && getNewTextEnd() >= edit.getOldTextEnd()) {
6239 // Merge with delete as they can be single operation.
6240 mNewText = mNewText.substring(0, edit.mStart - mStart)
6241 + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
6242 if (mNewText.isEmpty()) {
6243 mType = TYPE_DELETE;
6244 }
6245 mNewCursorPos = edit.mNewCursorPos;
6246 mIsComposition = edit.mIsComposition;
6247 return true;
6248 }
6249 if (edit.mType == TYPE_REPLACE && mStart == edit.mStart
6250 && TextUtils.equals(mNewText, edit.mOldText)) {
6251 // Merge with the replace that replaces the same region.
6252 mNewText = edit.mNewText;
6253 mNewCursorPos = edit.mNewCursorPos;
6254 mIsComposition = edit.mIsComposition;
6255 return true;
6256 }
6257 return false;
James Cook471559f2015-02-27 10:31:20 -08006258 }
6259
James Cook48e0fac2015-02-25 15:44:51 -08006260 /**
6261 * Forcibly creates a single merged edit operation by simulating the entire text
6262 * contents being replaced.
6263 */
James Cook22054252015-03-25 14:04:01 -07006264 public void forceMergeWith(EditOperation edit) {
James Cook48e0fac2015-02-25 15:44:51 -08006265 if (DEBUG_UNDO) Log.d(TAG, "forceMerge");
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006266 if (mergeWith(edit)) {
6267 return;
6268 }
James Cookf59152c2015-02-26 18:03:58 -08006269 Editor editor = getOwnerData();
James Cook48e0fac2015-02-25 15:44:51 -08006270
6271 // Copy the text of the current field.
6272 // NOTE: Using StringBuilder instead of SpannableStringBuilder would be somewhat faster,
6273 // but would require two parallel implementations of modifyText() because Editable and
6274 // StringBuilder do not share an interface for replace/delete/insert.
6275 Editable editable = (Editable) editor.mTextView.getText();
6276 Editable originalText = new SpannableStringBuilder(editable.toString());
6277
6278 // Roll back the last operation.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006279 modifyText(originalText, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
James Cook48e0fac2015-02-25 15:44:51 -08006280
6281 // Clone the text again and apply the new operation.
6282 Editable finalText = new SpannableStringBuilder(editable.toString());
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006283 modifyText(finalText, edit.mStart, edit.getOldTextEnd(),
6284 edit.mNewText, edit.mStart, edit.mNewCursorPos);
James Cook48e0fac2015-02-25 15:44:51 -08006285
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006286 // Convert this operation into a replace operation.
James Cook48e0fac2015-02-25 15:44:51 -08006287 mType = TYPE_REPLACE;
6288 mNewText = finalText.toString();
James Cook48e0fac2015-02-25 15:44:51 -08006289 mOldText = originalText.toString();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006290 mStart = 0;
James Cook48e0fac2015-02-25 15:44:51 -08006291 mNewCursorPos = edit.mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006292 mIsComposition = edit.mIsComposition;
James Cook48e0fac2015-02-25 15:44:51 -08006293 // mOldCursorPos is unchanged.
6294 }
6295
6296 private static void modifyText(Editable text, int deleteFrom, int deleteTo,
6297 CharSequence newText, int newTextInsertAt, int newCursorPos) {
James Cook471559f2015-02-27 10:31:20 -08006298 // Apply the edit if it is still valid.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006299 if (isValidRange(text, deleteFrom, deleteTo)
6300 && newTextInsertAt <= text.length() - (deleteTo - deleteFrom)) {
James Cook471559f2015-02-27 10:31:20 -08006301 if (deleteFrom != deleteTo) {
6302 text.delete(deleteFrom, deleteTo);
6303 }
6304 if (newText.length() != 0) {
6305 text.insert(newTextInsertAt, newText);
6306 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006307 }
James Cook900185d2015-03-10 09:48:11 -07006308 // Restore the cursor position. If there wasn't an old cursor (newCursorPos == -1) then
6309 // don't explicitly set it and rely on SpannableStringBuilder to position it.
James Cook471559f2015-02-27 10:31:20 -08006310 // TODO: Select all the text that was undone.
James Cook900185d2015-03-10 09:48:11 -07006311 if (0 <= newCursorPos && newCursorPos <= text.length()) {
James Cook471559f2015-02-27 10:31:20 -08006312 Selection.setSelection(text, newCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006313 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006314 }
6315
James Cook48e0fac2015-02-25 15:44:51 -08006316 private String getTypeString() {
6317 switch (mType) {
6318 case TYPE_INSERT:
6319 return "insert";
6320 case TYPE_DELETE:
6321 return "delete";
6322 case TYPE_REPLACE:
6323 return "replace";
6324 default:
6325 return "";
6326 }
6327 }
6328
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006329 @Override
James Cook471559f2015-02-27 10:31:20 -08006330 public String toString() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006331 return "[mType=" + getTypeString() + ", "
6332 + "mOldText=" + mOldText + ", "
6333 + "mNewText=" + mNewText + ", "
6334 + "mStart=" + mStart + ", "
6335 + "mOldCursorPos=" + mOldCursorPos + ", "
6336 + "mNewCursorPos=" + mNewCursorPos + ", "
6337 + "mFrozen=" + mFrozen + ", "
6338 + "mIsComposition=" + mIsComposition + "]";
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006339 }
6340
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006341 public static final Parcelable.ClassLoaderCreator<EditOperation> CREATOR =
6342 new Parcelable.ClassLoaderCreator<EditOperation>() {
James Cookf59152c2015-02-26 18:03:58 -08006343 @Override
James Cook471559f2015-02-27 10:31:20 -08006344 public EditOperation createFromParcel(Parcel in) {
6345 return new EditOperation(in, null);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006346 }
6347
James Cookf59152c2015-02-26 18:03:58 -08006348 @Override
James Cook471559f2015-02-27 10:31:20 -08006349 public EditOperation createFromParcel(Parcel in, ClassLoader loader) {
6350 return new EditOperation(in, loader);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006351 }
6352
James Cookf59152c2015-02-26 18:03:58 -08006353 @Override
James Cook471559f2015-02-27 10:31:20 -08006354 public EditOperation[] newArray(int size) {
6355 return new EditOperation[size];
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006356 }
6357 };
6358 }
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006359
6360 /**
6361 * A helper for enabling and handling "PROCESS_TEXT" menu actions.
6362 * These allow external applications to plug into currently selected text.
6363 */
6364 static final class ProcessTextIntentActionsHandler {
6365
6366 private final Editor mEditor;
6367 private final TextView mTextView;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006368 private final Context mContext;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006369 private final PackageManager mPackageManager;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006370 private final String mPackageName;
6371 private final SparseArray<Intent> mAccessibilityIntents = new SparseArray<>();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006372 private final SparseArray<AccessibilityNodeInfo.AccessibilityAction> mAccessibilityActions =
6373 new SparseArray<>();
6374 private final List<ResolveInfo> mSupportedActivities = new ArrayList<>();
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006375
6376 private ProcessTextIntentActionsHandler(Editor editor) {
6377 mEditor = Preconditions.checkNotNull(editor);
6378 mTextView = Preconditions.checkNotNull(mEditor.mTextView);
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006379 mContext = Preconditions.checkNotNull(mTextView.getContext());
6380 mPackageManager = Preconditions.checkNotNull(mContext.getPackageManager());
6381 mPackageName = Preconditions.checkNotNull(mContext.getPackageName());
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006382 }
6383
6384 /**
6385 * Adds "PROCESS_TEXT" menu items to the specified menu.
6386 */
6387 public void onInitializeMenu(Menu menu) {
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +01006388 final int size = mSupportedActivities.size();
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006389 loadSupportedActivities();
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +01006390 for (int i = 0; i < size; i++) {
6391 final ResolveInfo resolveInfo = mSupportedActivities.get(i);
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006392 menu.add(Menu.NONE, Menu.NONE,
6393 Editor.MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i++,
6394 getLabel(resolveInfo))
6395 .setIntent(createProcessTextIntentForResolveInfo(resolveInfo))
6396 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
6397 }
6398 }
6399
6400 /**
6401 * Performs a "PROCESS_TEXT" action if there is one associated with the specified
6402 * menu item.
6403 *
6404 * @return True if the action was performed, false otherwise.
6405 */
6406 public boolean performMenuItemAction(MenuItem item) {
6407 return fireIntent(item.getIntent());
6408 }
6409
6410 /**
6411 * Initializes and caches "PROCESS_TEXT" accessibility actions.
6412 */
6413 public void initializeAccessibilityActions() {
6414 mAccessibilityIntents.clear();
6415 mAccessibilityActions.clear();
6416 int i = 0;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006417 loadSupportedActivities();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006418 for (ResolveInfo resolveInfo : mSupportedActivities) {
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006419 int actionId = TextView.ACCESSIBILITY_ACTION_PROCESS_TEXT_START_ID + i++;
6420 mAccessibilityActions.put(
6421 actionId,
6422 new AccessibilityNodeInfo.AccessibilityAction(
6423 actionId, getLabel(resolveInfo)));
6424 mAccessibilityIntents.put(
6425 actionId, createProcessTextIntentForResolveInfo(resolveInfo));
6426 }
6427 }
6428
6429 /**
6430 * Adds "PROCESS_TEXT" accessibility actions to the specified accessibility node info.
6431 * NOTE: This needs a prior call to {@link #initializeAccessibilityActions()} to make the
6432 * latest accessibility actions available for this call.
6433 */
6434 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) {
6435 for (int i = 0; i < mAccessibilityActions.size(); i++) {
6436 nodeInfo.addAction(mAccessibilityActions.valueAt(i));
6437 }
6438 }
6439
6440 /**
6441 * Performs a "PROCESS_TEXT" action if there is one associated with the specified
6442 * accessibility action id.
6443 *
6444 * @return True if the action was performed, false otherwise.
6445 */
6446 public boolean performAccessibilityAction(int actionId) {
6447 return fireIntent(mAccessibilityIntents.get(actionId));
6448 }
6449
6450 private boolean fireIntent(Intent intent) {
6451 if (intent != null && Intent.ACTION_PROCESS_TEXT.equals(intent.getAction())) {
6452 intent.putExtra(Intent.EXTRA_PROCESS_TEXT, mTextView.getSelectedText());
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08006453 mEditor.mPreserveSelection = true;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006454 mTextView.startActivityForResult(intent, TextView.PROCESS_TEXT_REQUEST_CODE);
6455 return true;
6456 }
6457 return false;
6458 }
6459
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006460 private void loadSupportedActivities() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006461 mSupportedActivities.clear();
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006462 PackageManager packageManager = mTextView.getContext().getPackageManager();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006463 List<ResolveInfo> unfiltered =
6464 packageManager.queryIntentActivities(createProcessTextIntent(), 0);
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006465 for (ResolveInfo info : unfiltered) {
6466 if (isSupportedActivity(info)) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006467 mSupportedActivities.add(info);
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006468 }
6469 }
6470 }
6471
6472 private boolean isSupportedActivity(ResolveInfo info) {
6473 return mPackageName.equals(info.activityInfo.packageName)
6474 || info.activityInfo.exported
6475 && (info.activityInfo.permission == null
6476 || mContext.checkSelfPermission(info.activityInfo.permission)
6477 == PackageManager.PERMISSION_GRANTED);
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006478 }
6479
6480 private Intent createProcessTextIntentForResolveInfo(ResolveInfo info) {
6481 return createProcessTextIntent()
6482 .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !mTextView.isTextEditable())
6483 .setClassName(info.activityInfo.packageName, info.activityInfo.name);
6484 }
6485
6486 private Intent createProcessTextIntent() {
6487 return new Intent()
6488 .setAction(Intent.ACTION_PROCESS_TEXT)
6489 .setType("text/plain");
6490 }
6491
6492 private CharSequence getLabel(ResolveInfo resolveInfo) {
6493 return resolveInfo.loadLabel(mPackageManager);
6494 }
6495 }
Gilles Debunned88876a2012-03-16 17:34:04 -07006496}