blob: b5ac33070f1f2890a7da9f740f0352aa1eab7189 [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;
Gilles Debunned88876a2012-03-16 17:34:04 -070044import android.os.Bundle;
Yohei Yukawa23cbe852016-05-17 16:42:58 -070045import android.os.LocaleList;
Raph Levien26d443a2015-03-30 14:18:32 -070046import android.os.Parcel;
47import android.os.Parcelable;
James Cookf59152c2015-02-26 18:03:58 -080048import android.os.ParcelableParcel;
Gilles Debunned88876a2012-03-16 17:34:04 -070049import android.os.SystemClock;
50import android.provider.Settings;
51import android.text.DynamicLayout;
52import android.text.Editable;
Raph Levien26d443a2015-03-30 14:18:32 -070053import android.text.InputFilter;
Gilles Debunned88876a2012-03-16 17:34:04 -070054import android.text.InputType;
55import android.text.Layout;
56import android.text.ParcelableSpan;
57import android.text.Selection;
58import android.text.SpanWatcher;
59import android.text.Spannable;
60import android.text.SpannableStringBuilder;
61import android.text.Spanned;
62import android.text.StaticLayout;
63import android.text.TextUtils;
Gilles Debunned88876a2012-03-16 17:34:04 -070064import android.text.method.KeyListener;
65import android.text.method.MetaKeyKeyListener;
66import android.text.method.MovementMethod;
Gilles Debunned88876a2012-03-16 17:34:04 -070067import android.text.method.WordIterator;
68import android.text.style.EasyEditSpan;
69import android.text.style.SuggestionRangeSpan;
70import android.text.style.SuggestionSpan;
71import android.text.style.TextAppearanceSpan;
72import android.text.style.URLSpan;
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +090073import android.util.ArraySet;
Gilles Debunned88876a2012-03-16 17:34:04 -070074import android.util.DisplayMetrics;
75import android.util.Log;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -070076import android.util.SparseArray;
Gilles Debunned88876a2012-03-16 17:34:04 -070077import android.view.ActionMode;
78import android.view.ActionMode.Callback;
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +090079import android.view.ContextMenu;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +090080import android.view.ContextThemeWrapper;
Chris Craikf6829a02015-03-10 10:28:59 -070081import android.view.DisplayListCanvas;
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -070082import android.view.DragAndDropPermissions;
Gilles Debunned88876a2012-03-16 17:34:04 -070083import android.view.DragEvent;
84import android.view.Gravity;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -070085import android.view.HapticFeedbackConstants;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -080086import android.view.InputDevice;
Gilles Debunned88876a2012-03-16 17:34:04 -070087import android.view.LayoutInflater;
88import android.view.Menu;
89import android.view.MenuItem;
90import android.view.MotionEvent;
Chris Craikf6829a02015-03-10 10:28:59 -070091import android.view.RenderNode;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +090092import android.view.SubMenu;
Gilles Debunned88876a2012-03-16 17:34:04 -070093import android.view.View;
Gilles Debunned88876a2012-03-16 17:34:04 -070094import android.view.View.DragShadowBuilder;
95import android.view.View.OnClickListener;
Adam Powell057a5852012-05-11 10:28:38 -070096import android.view.ViewConfiguration;
97import android.view.ViewGroup;
Gilles Debunned88876a2012-03-16 17:34:04 -070098import android.view.ViewGroup.LayoutParams;
Gilles Debunned88876a2012-03-16 17:34:04 -070099import android.view.ViewTreeObserver;
100import android.view.WindowManager;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700101import android.view.accessibility.AccessibilityNodeInfo;
Gilles Debunned88876a2012-03-16 17:34:04 -0700102import android.view.inputmethod.CorrectionInfo;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900103import android.view.inputmethod.CursorAnchorInfo;
Gilles Debunned88876a2012-03-16 17:34:04 -0700104import android.view.inputmethod.EditorInfo;
105import android.view.inputmethod.ExtractedText;
106import android.view.inputmethod.ExtractedTextRequest;
107import android.view.inputmethod.InputConnection;
108import android.view.inputmethod.InputMethodManager;
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +0100109import android.view.textclassifier.TextClassification;
Richard Ledley26b87222017-11-30 10:54:08 +0000110import android.view.textclassifier.TextLinks;
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;
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +0100131import java.util.Map;
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;
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100141 // Specifies whether to use or not the magnifier when pressing the insertion or selection
142 // handles.
Andrei Stingaceanu060b3d72017-10-04 11:27:08 +0100143 private static final boolean FLAG_USE_MAGNIFIER = true;
Adam Powell057a5852012-05-11 10:28:38 -0700144
Gilles Debunned88876a2012-03-16 17:34:04 -0700145 static final int BLINK = 500;
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700146 private static final int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
Mady Mellorcc65c372015-06-17 09:25:19 -0700147 private static final float LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS = 0.5f;
Mady Mellore264ac32015-06-22 16:46:29 -0700148 private static final int UNSET_X_VALUE = -1;
Mady Mellora6a0f782015-07-10 16:43:32 -0700149 private static final int UNSET_LINE = -1;
James Cookf59152c2015-02-26 18:03:58 -0800150 // Tag used when the Editor maintains its own separate UndoManager.
151 private static final String UNDO_OWNER_TAG = "Editor";
Gilles Debunned88876a2012-03-16 17:34:04 -0700152
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900153 // Ordering constants used to place the Action Mode or context menu items in their menu.
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +0100154 private static final int MENU_ITEM_ORDER_ASSIST = 0;
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +0000155 private static final int MENU_ITEM_ORDER_UNDO = 2;
156 private static final int MENU_ITEM_ORDER_REDO = 3;
Abodunrinwa Toki5fedfb82017-02-06 19:34:00 +0000157 private static final int MENU_ITEM_ORDER_CUT = 4;
158 private static final int MENU_ITEM_ORDER_COPY = 5;
159 private static final int MENU_ITEM_ORDER_PASTE = 6;
160 private static final int MENU_ITEM_ORDER_SHARE = 7;
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +0100161 private static final int MENU_ITEM_ORDER_SELECT_ALL = 8;
162 private static final int MENU_ITEM_ORDER_REPLACE = 9;
163 private static final int MENU_ITEM_ORDER_AUTOFILL = 10;
164 private static final int MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT = 11;
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +0100165 private static final int MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START = 50;
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +0100166 private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 100;
Clara Bayarri3b69fd82015-06-03 21:52:02 +0100167
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100168 @IntDef({MagnifierHandleTrigger.SELECTION_START,
169 MagnifierHandleTrigger.SELECTION_END,
170 MagnifierHandleTrigger.INSERTION})
171 @Retention(RetentionPolicy.SOURCE)
172 private @interface MagnifierHandleTrigger {
173 int INSERTION = 0;
174 int SELECTION_START = 1;
175 int SELECTION_END = 2;
176 }
177
Richard Ledley26b87222017-11-30 10:54:08 +0000178 @IntDef({TextActionMode.SELECTION, TextActionMode.INSERTION, TextActionMode.TEXT_LINK})
179 @interface TextActionMode {
180 int SELECTION = 0;
181 int INSERTION = 1;
182 int TEXT_LINK = 2;
183 }
184
James Cookf59152c2015-02-26 18:03:58 -0800185 // Each Editor manages its own undo stack.
186 private final UndoManager mUndoManager = new UndoManager();
187 private UndoOwner mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
James Cook48e0fac2015-02-25 15:44:51 -0800188 final UndoInputFilter mUndoInputFilter = new UndoInputFilter(this);
James Cookf1dad1e2015-02-27 11:00:01 -0800189 boolean mAllowUndo = true;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700190
Abodunrinwa Toki54486c12017-04-19 21:02:36 +0100191 private final MetricsLogger mMetricsLogger = new MetricsLogger();
192
Gilles Debunned88876a2012-03-16 17:34:04 -0700193 // Cursor Controllers.
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900194 private InsertionPointCursorController mInsertionPointCursorController;
Gilles Debunned88876a2012-03-16 17:34:04 -0700195 SelectionModifierCursorController mSelectionModifierCursorController;
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100196 // Action mode used when text is selected or when actions on an insertion cursor are triggered.
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800197 private ActionMode mTextActionMode;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900198 private boolean mInsertionControllerEnabled;
199 private boolean mSelectionControllerEnabled;
Gilles Debunned88876a2012-03-16 17:34:04 -0700200
Yohei Yukawac9cd9db2017-06-19 18:27:34 -0700201 private final boolean mHapticTextHandleEnabled;
202
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100203 private final Magnifier mMagnifier;
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000204 private final Runnable mUpdateMagnifierRunnable = new Runnable() {
205 @Override
206 public void run() {
207 mMagnifier.update();
208 }
209 };
210 // Update the magnifier contents whenever anything in the view hierarchy is updated.
211 // Note: this only captures UI thread-visible changes, so it's a known issue that an animating
212 // VectorDrawable or Ripple animation will not trigger capture, since they're owned by
213 // RenderThread.
214 private final ViewTreeObserver.OnDrawListener mMagnifierOnDrawListener =
215 new ViewTreeObserver.OnDrawListener() {
216 @Override
217 public void onDraw() {
218 if (mMagnifier != null) {
219 // Posting the method will ensure that updating the magnifier contents will
220 // happen right after the rendering of the current frame.
221 mTextView.post(mUpdateMagnifierRunnable);
222 }
223 }
224 };
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100225
Gilles Debunned88876a2012-03-16 17:34:04 -0700226 // Used to highlight a word when it is corrected by the IME
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900227 private CorrectionHighlighter mCorrectionHighlighter;
Gilles Debunned88876a2012-03-16 17:34:04 -0700228
229 InputContentType mInputContentType;
230 InputMethodState mInputMethodState;
231
Chris Craik956f3402015-04-27 16:41:00 -0700232 private static class TextRenderNode {
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +0900233 // Render node has 3 recording states:
234 // 1. Recorded operations are valid.
235 // #needsRecord() returns false, but needsToBeShifted is false.
236 // 2. Recorded operations are not valid, but just the position needed to be updated.
237 // #needsRecord() returns false, but needsToBeShifted is true.
238 // 3. Recorded operations are not valid. Need to record operations. #needsRecord() returns
239 // true.
Chris Craik956f3402015-04-27 16:41:00 -0700240 RenderNode renderNode;
John Reck7558aa72014-03-05 14:59:59 -0800241 boolean isDirty;
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +0900242 // Becomes true when recorded operations can be reused, but the position has to be updated.
243 boolean needsToBeShifted;
Chris Craik956f3402015-04-27 16:41:00 -0700244 public TextRenderNode(String name) {
Chris Craik956f3402015-04-27 16:41:00 -0700245 renderNode = RenderNode.create(name, null);
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +0900246 isDirty = true;
247 needsToBeShifted = true;
John Reck7558aa72014-03-05 14:59:59 -0800248 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700249 boolean needsRecord() {
250 return isDirty || !renderNode.isValid();
251 }
John Reck7558aa72014-03-05 14:59:59 -0800252 }
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900253 private TextRenderNode[] mTextRenderNodes;
Gilles Debunned88876a2012-03-16 17:34:04 -0700254
255 boolean mFrozenWithFocus;
256 boolean mSelectionMoved;
257 boolean mTouchFocusSelected;
258
259 KeyListener mKeyListener;
260 int mInputType = EditorInfo.TYPE_NULL;
261
262 boolean mDiscardNextActionUp;
263 boolean mIgnoreActionUpEvent;
264
265 long mShowCursor;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900266 private Blink mBlink;
Gilles Debunned88876a2012-03-16 17:34:04 -0700267
268 boolean mCursorVisible = true;
269 boolean mSelectAllOnFocus;
270 boolean mTextIsSelectable;
271
272 CharSequence mError;
273 boolean mErrorWasChanged;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900274 private ErrorPopup mErrorPopup;
Fabrice Di Meglio1957d282012-10-25 17:42:39 -0700275
Gilles Debunned88876a2012-03-16 17:34:04 -0700276 /**
277 * This flag is set if the TextView tries to display an error before it
278 * is attached to the window (so its position is still unknown).
279 * It causes the error to be shown later, when onAttachedToWindow()
280 * is called.
281 */
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900282 private boolean mShowErrorAfterAttach;
Gilles Debunned88876a2012-03-16 17:34:04 -0700283
284 boolean mInBatchEditControllers;
Gilles Debunne3473b2b2012-04-20 16:21:10 -0700285 boolean mShowSoftInputOnFocus = true;
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -0800286 private boolean mPreserveSelection;
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +0900287 private boolean mRestartActionModeOnNextRefresh;
Gilles Debunned88876a2012-03-16 17:34:04 -0700288
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800289 private SelectionActionModeHelper mSelectionActionModeHelper;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +0000290
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900291 boolean mIsBeingLongClicked;
292
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900293 private SuggestionsPopupWindow mSuggestionsPopupWindow;
Gilles Debunned88876a2012-03-16 17:34:04 -0700294 SuggestionRangeSpan mSuggestionRangeSpan;
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900295 private Runnable mShowSuggestionRunnable;
Gilles Debunned88876a2012-03-16 17:34:04 -0700296
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -0700297 Drawable mDrawableForCursor = null;
Gilles Debunned88876a2012-03-16 17:34:04 -0700298
299 private Drawable mSelectHandleLeft;
300 private Drawable mSelectHandleRight;
301 private Drawable mSelectHandleCenter;
302
303 // Global listener that detects changes in the global position of the TextView
304 private PositionListener mPositionListener;
305
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900306 private float mLastDownPositionX, mLastDownPositionY;
Petar Å egina91df3f92017-08-15 16:20:43 +0100307 private float mLastUpPositionX, mLastUpPositionY;
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900308 private float mContextMenuAnchorX, mContextMenuAnchorY;
Gilles Debunned88876a2012-03-16 17:34:04 -0700309 Callback mCustomSelectionActionModeCallback;
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100310 Callback mCustomInsertionActionModeCallback;
Gilles Debunned88876a2012-03-16 17:34:04 -0700311
312 // Set when this TextView gained focus with some text selected. Will start selection mode.
313 boolean mCreatedWithASelection;
314
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +0900315 // Indicates the current tap state (first tap, double tap, or triple click).
316 private int mTapState = TAP_STATE_INITIAL;
317 private long mLastTouchUpTime = 0;
318 private static final int TAP_STATE_INITIAL = 0;
319 private static final int TAP_STATE_FIRST_TAP = 1;
320 private static final int TAP_STATE_DOUBLE_TAP = 2;
321 // Only for mouse input.
322 private static final int TAP_STATE_TRIPLE_CLICK = 3;
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100323
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +0900324 // The button state as of the last time #onTouchEvent is called.
325 private int mLastButtonState;
326
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100327 private Runnable mInsertionActionModeRunnable;
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100328
Jean Chalardbaf30942013-02-28 16:01:51 -0800329 // The span controller helps monitoring the changes to which the Editor needs to react:
330 // - EasyEditSpans, for which we have some UI to display on attach and on hide
331 // - SelectionSpans, for which we need to call updateSelection if an IME is attached
332 private SpanController mSpanController;
Gilles Debunned88876a2012-03-16 17:34:04 -0700333
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +0900334 private WordIterator mWordIterator;
Gilles Debunned88876a2012-03-16 17:34:04 -0700335 SpellChecker mSpellChecker;
336
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800337 // This word iterator is set with text and used to determine word boundaries
338 // when a user is selecting text.
339 private WordIterator mWordIteratorWithText;
340 // Indicate that the text in the word iterator needs to be updated.
341 private boolean mUpdateWordIteratorText;
342
Gilles Debunned88876a2012-03-16 17:34:04 -0700343 private Rect mTempRect;
344
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -0800345 private final TextView mTextView;
Gilles Debunned88876a2012-03-16 17:34:04 -0700346
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700347 final ProcessTextIntentActionsHandler mProcessTextIntentActionsHandler;
348
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700349 private final CursorAnchorInfoNotifier mCursorAnchorInfoNotifier =
350 new CursorAnchorInfoNotifier();
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900351
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100352 private final Runnable mShowFloatingToolbar = new Runnable() {
353 @Override
354 public void run() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100355 if (mTextActionMode != null) {
Abodunrinwa Toki9e211282015-06-05 02:46:57 +0100356 mTextActionMode.hide(0); // hide off.
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100357 }
358 }
359 };
360
Clara Bayarrib71dddd2015-06-04 23:17:30 +0100361 boolean mIsInsertionActionModeStartPending = false;
362
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +0900363 private final SuggestionHelper mSuggestionHelper = new SuggestionHelper();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +0900364
Gilles Debunned88876a2012-03-16 17:34:04 -0700365 Editor(TextView textView) {
366 mTextView = textView;
James Cookf59152c2015-02-26 18:03:58 -0800367 // Synchronize the filter list, which places the undo input filter at the end.
368 mTextView.setFilters(mTextView.getFilters());
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -0700369 mProcessTextIntentActionsHandler = new ProcessTextIntentActionsHandler(this);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -0700370 mHapticTextHandleEnabled = mTextView.getContext().getResources().getBoolean(
371 com.android.internal.R.bool.config_enableHapticTextHandle);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +0100372
373 mMagnifier = FLAG_USE_MAGNIFIER ? new Magnifier(mTextView) : null;
James Cookf59152c2015-02-26 18:03:58 -0800374 }
375
376 ParcelableParcel saveInstanceState() {
James Cookd2026682015-03-03 14:40:14 -0800377 ParcelableParcel state = new ParcelableParcel(getClass().getClassLoader());
378 Parcel parcel = state.getParcel();
379 mUndoManager.saveInstanceState(parcel);
380 mUndoInputFilter.saveInstanceState(parcel);
381 return state;
James Cookf59152c2015-02-26 18:03:58 -0800382 }
383
384 void restoreInstanceState(ParcelableParcel state) {
James Cookd2026682015-03-03 14:40:14 -0800385 Parcel parcel = state.getParcel();
386 mUndoManager.restoreInstanceState(parcel, state.getClassLoader());
387 mUndoInputFilter.restoreInstanceState(parcel);
James Cookf59152c2015-02-26 18:03:58 -0800388 // Re-associate this object as the owner of undo state.
389 mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
390 }
391
James Cook48e0fac2015-02-25 15:44:51 -0800392 /**
393 * Forgets all undo and redo operations for this Editor.
394 */
395 void forgetUndoRedo() {
396 UndoOwner[] owners = { mUndoOwner };
397 mUndoManager.forgetUndos(owners, -1 /* all */);
398 mUndoManager.forgetRedos(owners, -1 /* all */);
399 }
400
James Cookf59152c2015-02-26 18:03:58 -0800401 boolean canUndo() {
402 UndoOwner[] owners = { mUndoOwner };
James Cookf1dad1e2015-02-27 11:00:01 -0800403 return mAllowUndo && mUndoManager.countUndos(owners) > 0;
James Cookf59152c2015-02-26 18:03:58 -0800404 }
405
406 boolean canRedo() {
407 UndoOwner[] owners = { mUndoOwner };
James Cookf1dad1e2015-02-27 11:00:01 -0800408 return mAllowUndo && mUndoManager.countRedos(owners) > 0;
James Cookf59152c2015-02-26 18:03:58 -0800409 }
410
411 void undo() {
James Cookf1dad1e2015-02-27 11:00:01 -0800412 if (!mAllowUndo) {
413 return;
414 }
James Cookf59152c2015-02-26 18:03:58 -0800415 UndoOwner[] owners = { mUndoOwner };
416 mUndoManager.undo(owners, 1); // Undo 1 action.
417 }
418
419 void redo() {
James Cookf1dad1e2015-02-27 11:00:01 -0800420 if (!mAllowUndo) {
421 return;
422 }
James Cookf59152c2015-02-26 18:03:58 -0800423 UndoOwner[] owners = { mUndoOwner };
424 mUndoManager.redo(owners, 1); // Redo 1 action.
Gilles Debunned88876a2012-03-16 17:34:04 -0700425 }
426
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100427 void replace() {
Keisuke Kuroyanagi713be062016-02-29 16:07:54 -0800428 if (mSuggestionsPopupWindow == null) {
429 mSuggestionsPopupWindow = new SuggestionsPopupWindow();
430 }
431 hideCursorAndSpanControllers();
432 mSuggestionsPopupWindow.show();
433
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100434 int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100435 Selection.setSelection((Spannable) mTextView.getText(), middle);
Andrei Stingaceanueeb9afc2015-05-12 12:39:07 +0100436 }
437
Gilles Debunned88876a2012-03-16 17:34:04 -0700438 void onAttachedToWindow() {
439 if (mShowErrorAfterAttach) {
440 showError();
441 mShowErrorAfterAttach = false;
442 }
443
444 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000445 if (observer.isAlive()) {
446 // No need to create the controller.
447 // The get method will add the listener on controller creation.
448 if (mInsertionPointCursorController != null) {
449 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
450 }
451 if (mSelectionModifierCursorController != null) {
452 mSelectionModifierCursorController.resetTouchOffsets();
453 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
454 }
455 if (FLAG_USE_MAGNIFIER) {
456 observer.addOnDrawListener(mMagnifierOnDrawListener);
457 }
Gilles Debunned88876a2012-03-16 17:34:04 -0700458 }
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000459
Gilles Debunned88876a2012-03-16 17:34:04 -0700460 updateSpellCheckSpans(0, mTextView.getText().length(),
461 true /* create the spell checker if needed */);
Adam Powell057a5852012-05-11 10:28:38 -0700462
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +0900463 if (mTextView.hasSelection()) {
464 refreshTextActionMode();
Adam Powell057a5852012-05-11 10:28:38 -0700465 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900466
467 getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true);
Mikael Gullstrand5b734f22013-07-09 14:41:28 +0200468 resumeBlink();
Gilles Debunned88876a2012-03-16 17:34:04 -0700469 }
470
471 void onDetachedFromWindow() {
Yohei Yukawa83b68ba2014-05-12 15:46:25 +0900472 getPositionListener().removeSubscriber(mCursorAnchorInfoNotifier);
473
Gilles Debunned88876a2012-03-16 17:34:04 -0700474 if (mError != null) {
475 hideError();
476 }
477
Mikael Gullstrand5b734f22013-07-09 14:41:28 +0200478 suspendBlink();
Gilles Debunned88876a2012-03-16 17:34:04 -0700479
480 if (mInsertionPointCursorController != null) {
481 mInsertionPointCursorController.onDetached();
482 }
483
484 if (mSelectionModifierCursorController != null) {
485 mSelectionModifierCursorController.onDetached();
486 }
487
488 if (mShowSuggestionRunnable != null) {
489 mTextView.removeCallbacks(mShowSuggestionRunnable);
490 }
491
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100492 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100493 if (mInsertionActionModeRunnable != null) {
494 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanufae270c2015-04-29 14:39:40 +0100495 }
496
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +0100497 mTextView.removeCallbacks(mShowFloatingToolbar);
498
Chris Craik003cc3d2015-10-16 10:24:55 -0700499 discardTextDisplayLists();
Gilles Debunned88876a2012-03-16 17:34:04 -0700500
501 if (mSpellChecker != null) {
502 mSpellChecker.closeSession();
503 // Forces the creation of a new SpellChecker next time this window is created.
504 // Will handle the cases where the settings has been changed in the meantime.
505 mSpellChecker = null;
506 }
507
Andrei Stingaceanu41589fa2017-11-02 13:54:10 +0000508 if (FLAG_USE_MAGNIFIER) {
509 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
510 if (observer.isAlive()) {
511 observer.removeOnDrawListener(mMagnifierOnDrawListener);
512 }
513 }
514
Mady Mellora2861452015-06-25 08:40:27 -0700515 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -0800516 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -0700517 }
518
Chris Craik003cc3d2015-10-16 10:24:55 -0700519 private void discardTextDisplayLists() {
Chris Craik956f3402015-04-27 16:41:00 -0700520 if (mTextRenderNodes != null) {
521 for (int i = 0; i < mTextRenderNodes.length; i++) {
522 RenderNode displayList = mTextRenderNodes[i] != null
523 ? mTextRenderNodes[i].renderNode : null;
John Reck7558aa72014-03-05 14:59:59 -0800524 if (displayList != null && displayList.isValid()) {
Chris Craik003cc3d2015-10-16 10:24:55 -0700525 displayList.discardDisplayList();
John Reck7558aa72014-03-05 14:59:59 -0800526 }
527 }
528 }
529 }
530
Gilles Debunned88876a2012-03-16 17:34:04 -0700531 private void showError() {
532 if (mTextView.getWindowToken() == null) {
533 mShowErrorAfterAttach = true;
534 return;
535 }
536
537 if (mErrorPopup == null) {
538 LayoutInflater inflater = LayoutInflater.from(mTextView.getContext());
539 final TextView err = (TextView) inflater.inflate(
540 com.android.internal.R.layout.textview_hint, null);
541
542 final float scale = mTextView.getResources().getDisplayMetrics().density;
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700543 mErrorPopup =
544 new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f));
Gilles Debunned88876a2012-03-16 17:34:04 -0700545 mErrorPopup.setFocusable(false);
546 // The user is entering text, so the input method is needed. We
547 // don't want the popup to be displayed on top of it.
548 mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
549 }
550
551 TextView tv = (TextView) mErrorPopup.getContentView();
552 chooseSize(mErrorPopup, mError, tv);
553 tv.setText(mError);
554
Hidehiko Tsuchiyaa0c8c1c2017-11-13 10:52:23 +0900555 mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY(),
556 Gravity.TOP | Gravity.LEFT);
Gilles Debunned88876a2012-03-16 17:34:04 -0700557 mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor());
558 }
559
560 public void setError(CharSequence error, Drawable icon) {
561 mError = TextUtils.stringOrSpannedString(error);
562 mErrorWasChanged = true;
Romain Guyd1cc1872012-11-05 17:43:25 -0800563
Gilles Debunned88876a2012-03-16 17:34:04 -0700564 if (mError == null) {
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800565 setErrorIcon(null);
Gilles Debunned88876a2012-03-16 17:34:04 -0700566 if (mErrorPopup != null) {
567 if (mErrorPopup.isShowing()) {
568 mErrorPopup.dismiss();
569 }
570
571 mErrorPopup = null;
572 }
Daniel 2 Olofssonf4ecc552013-08-13 10:30:26 +0200573 mShowErrorAfterAttach = false;
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800574 } else {
Romain Guyd1cc1872012-11-05 17:43:25 -0800575 setErrorIcon(icon);
Fabrice Di Meglio5acc3792012-11-12 14:08:00 -0800576 if (mTextView.isFocused()) {
577 showError();
578 }
Romain Guyd1cc1872012-11-05 17:43:25 -0800579 }
580 }
581
582 private void setErrorIcon(Drawable icon) {
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800583 Drawables dr = mTextView.mDrawables;
584 if (dr == null) {
Fabrice Di Megliof7a5cdf2013-03-15 15:36:51 -0700585 mTextView.mDrawables = dr = new Drawables(mTextView.getContext());
Gilles Debunned88876a2012-03-16 17:34:04 -0700586 }
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800587 dr.setErrorDrawable(icon, mTextView);
588
589 mTextView.resetResolvedDrawables();
590 mTextView.invalidate();
591 mTextView.requestLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -0700592 }
593
594 private void hideError() {
595 if (mErrorPopup != null) {
596 if (mErrorPopup.isShowing()) {
597 mErrorPopup.dismiss();
598 }
599 }
600
601 mShowErrorAfterAttach = false;
602 }
603
604 /**
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800605 * Returns the X offset to make the pointy top of the error point
Gilles Debunned88876a2012-03-16 17:34:04 -0700606 * at the middle of the error icon.
607 */
608 private int getErrorX() {
609 /*
610 * The "25" is the distance between the point and the right edge
611 * of the background
612 */
613 final float scale = mTextView.getResources().getDisplayMetrics().density;
614
615 final Drawables dr = mTextView.mDrawables;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800616
617 final int layoutDirection = mTextView.getLayoutDirection();
618 int errorX;
619 int offset;
620 switch (layoutDirection) {
621 default:
622 case View.LAYOUT_DIRECTION_LTR:
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700623 offset = -(dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
624 errorX = mTextView.getWidth() - mErrorPopup.getWidth()
625 - mTextView.getPaddingRight() + offset;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800626 break;
627 case View.LAYOUT_DIRECTION_RTL:
628 offset = (dr != null ? dr.mDrawableSizeLeft : 0) / 2 - (int) (25 * scale + 0.5f);
629 errorX = mTextView.getPaddingLeft() + offset;
630 break;
631 }
632 return errorX;
Gilles Debunned88876a2012-03-16 17:34:04 -0700633 }
634
635 /**
636 * Returns the Y offset to make the pointy top of the error point
637 * at the bottom of the error icon.
638 */
639 private int getErrorY() {
640 /*
641 * Compound, not extended, because the icon is not clipped
642 * if the text height is smaller.
643 */
644 final int compoundPaddingTop = mTextView.getCompoundPaddingTop();
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700645 int vspace = mTextView.getBottom() - mTextView.getTop()
646 - mTextView.getCompoundPaddingBottom() - compoundPaddingTop;
Gilles Debunned88876a2012-03-16 17:34:04 -0700647
648 final Drawables dr = mTextView.mDrawables;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800649
650 final int layoutDirection = mTextView.getLayoutDirection();
651 int height;
652 switch (layoutDirection) {
653 default:
654 case View.LAYOUT_DIRECTION_LTR:
655 height = (dr != null ? dr.mDrawableHeightRight : 0);
656 break;
657 case View.LAYOUT_DIRECTION_RTL:
658 height = (dr != null ? dr.mDrawableHeightLeft : 0);
659 break;
660 }
661
662 int icontop = compoundPaddingTop + (vspace - height) / 2;
Gilles Debunned88876a2012-03-16 17:34:04 -0700663
664 /*
665 * The "2" is the distance between the point and the top edge
666 * of the background.
667 */
668 final float scale = mTextView.getResources().getDisplayMetrics().density;
Fabrice Di Megliobb0cbae2012-11-13 20:51:24 -0800669 return icontop + height - mTextView.getHeight() - (int) (2 * scale + 0.5f);
Gilles Debunned88876a2012-03-16 17:34:04 -0700670 }
671
672 void createInputContentTypeIfNeeded() {
673 if (mInputContentType == null) {
674 mInputContentType = new InputContentType();
675 }
676 }
677
678 void createInputMethodStateIfNeeded() {
679 if (mInputMethodState == null) {
680 mInputMethodState = new InputMethodState();
681 }
682 }
683
684 boolean isCursorVisible() {
685 // The default value is true, even when there is no associated Editor
686 return mCursorVisible && mTextView.isTextEditable();
687 }
688
689 void prepareCursorControllers() {
690 boolean windowSupportsHandles = false;
691
692 ViewGroup.LayoutParams params = mTextView.getRootView().getLayoutParams();
693 if (params instanceof WindowManager.LayoutParams) {
694 WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
695 windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
696 || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
697 }
698
699 boolean enabled = windowSupportsHandles && mTextView.getLayout() != null;
700 mInsertionControllerEnabled = enabled && isCursorVisible();
701 mSelectionControllerEnabled = enabled && mTextView.textCanBeSelected();
702
703 if (!mInsertionControllerEnabled) {
704 hideInsertionPointCursorController();
705 if (mInsertionPointCursorController != null) {
706 mInsertionPointCursorController.onDetached();
707 mInsertionPointCursorController = null;
708 }
709 }
710
711 if (!mSelectionControllerEnabled) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +0100712 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -0700713 if (mSelectionModifierCursorController != null) {
714 mSelectionModifierCursorController.onDetached();
715 mSelectionModifierCursorController = null;
716 }
717 }
718 }
719
Seigo Nonakabb6a62c2015-03-31 21:59:30 +0900720 void hideInsertionPointCursorController() {
Gilles Debunned88876a2012-03-16 17:34:04 -0700721 if (mInsertionPointCursorController != null) {
722 mInsertionPointCursorController.hide();
723 }
724 }
725
726 /**
Mady Mellora2861452015-06-25 08:40:27 -0700727 * Hides the insertion and span controllers.
Gilles Debunned88876a2012-03-16 17:34:04 -0700728 */
Mady Mellora2861452015-06-25 08:40:27 -0700729 void hideCursorAndSpanControllers() {
Gilles Debunned88876a2012-03-16 17:34:04 -0700730 hideCursorControllers();
731 hideSpanControllers();
732 }
733
734 private void hideSpanControllers() {
Jean Chalardbaf30942013-02-28 16:01:51 -0800735 if (mSpanController != null) {
736 mSpanController.hide();
Gilles Debunned88876a2012-03-16 17:34:04 -0700737 }
738 }
739
740 private void hideCursorControllers() {
Yohei Yukawa85d08f12015-04-29 20:12:37 -0700741 // When mTextView is not ExtractEditText, we need to distinguish two kinds of focus-lost.
742 // One is the true focus lost where suggestions pop-up (if any) should be dismissed, and the
743 // other is an side effect of showing the suggestions pop-up itself. We use isShowingUp()
744 // to distinguish one from the other.
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700745 if (mSuggestionsPopupWindow != null && ((mTextView.isInExtractedMode())
746 || !mSuggestionsPopupWindow.isShowingUp())) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700747 // Should be done before hide insertion point controller since it triggers a show of it
748 mSuggestionsPopupWindow.hide();
749 }
750 hideInsertionPointCursorController();
Gilles Debunned88876a2012-03-16 17:34:04 -0700751 }
752
753 /**
754 * Create new SpellCheckSpans on the modified region.
755 */
756 private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
Satoshi Kataokad7429c12013-06-05 16:30:23 +0900757 // Remove spans whose adjacent characters are text not punctuation
758 mTextView.removeAdjacentSuggestionSpans(start);
759 mTextView.removeAdjacentSuggestionSpans(end);
760
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700761 if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled()
762 && !(mTextView.isInExtractedMode())) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700763 if (mSpellChecker == null && createSpellChecker) {
764 mSpellChecker = new SpellChecker(mTextView);
765 }
766 if (mSpellChecker != null) {
767 mSpellChecker.spellCheck(start, end);
768 }
769 }
770 }
771
772 void onScreenStateChanged(int screenState) {
773 switch (screenState) {
774 case View.SCREEN_STATE_ON:
775 resumeBlink();
776 break;
777 case View.SCREEN_STATE_OFF:
778 suspendBlink();
779 break;
780 }
781 }
782
783 private void suspendBlink() {
784 if (mBlink != null) {
785 mBlink.cancel();
786 }
787 }
788
789 private void resumeBlink() {
790 if (mBlink != null) {
791 mBlink.uncancel();
792 makeBlink();
793 }
794 }
795
796 void adjustInputType(boolean password, boolean passwordInputType,
797 boolean webPasswordInputType, boolean numberPasswordInputType) {
798 // mInputType has been set from inputType, possibly modified by mInputMethod.
799 // Specialize mInputType to [web]password if we have a text class and the original input
800 // type was a password.
801 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
802 if (password || passwordInputType) {
803 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
804 | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
805 }
806 if (webPasswordInputType) {
807 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
808 | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
809 }
810 } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
811 if (numberPasswordInputType) {
812 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
813 | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
814 }
815 }
816 }
817
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700818 private void chooseSize(@NonNull PopupWindow pop, @NonNull CharSequence text,
819 @NonNull TextView tv) {
820 final int wid = tv.getPaddingLeft() + tv.getPaddingRight();
821 final int ht = tv.getPaddingTop() + tv.getPaddingBottom();
Gilles Debunned88876a2012-03-16 17:34:04 -0700822
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700823 final int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize(
Gilles Debunned88876a2012-03-16 17:34:04 -0700824 com.android.internal.R.dimen.textview_error_popup_default_width);
Roozbeh Pournader5caf5a62017-08-22 18:08:09 -0700825 final StaticLayout l = StaticLayout.Builder.obtain(text, 0, text.length(), tv.getPaint(),
826 defaultWidthInPixels)
827 .setUseLineSpacingFromFallbacks(tv.mUseFallbackLineSpacing)
828 .build();
829
Gilles Debunned88876a2012-03-16 17:34:04 -0700830 float max = 0;
831 for (int i = 0; i < l.getLineCount(); i++) {
832 max = Math.max(max, l.getLineWidth(i));
833 }
834
835 /*
836 * Now set the popup size to be big enough for the text plus the border capped
837 * to DEFAULT_MAX_POPUP_WIDTH
838 */
839 pop.setWidth(wid + (int) Math.ceil(max));
840 pop.setHeight(ht + l.getHeight());
841 }
842
843 void setFrame() {
844 if (mErrorPopup != null) {
845 TextView tv = (TextView) mErrorPopup.getContentView();
846 chooseSize(mErrorPopup, mError, tv);
847 mErrorPopup.update(mTextView, getErrorX(), getErrorY(),
848 mErrorPopup.getWidth(), mErrorPopup.getHeight());
849 }
850 }
851
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800852 private int getWordStart(int offset) {
853 // FIXME - For this and similar methods we're not doing anything to check if there's
854 // a LocaleSpan in the text, this may be something we should try handling or checking for.
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700855 int retOffset = getWordIteratorWithText().prevBoundary(offset);
Mady Mellor58c90872015-05-12 11:09:37 -0700856 if (getWordIteratorWithText().isOnPunctuation(retOffset)) {
857 // On punctuation boundary or within group of punctuation, find punctuation start.
858 retOffset = getWordIteratorWithText().getPunctuationBeginning(offset);
859 } else {
860 // Not on a punctuation boundary, find the word start.
Mady Mellore264ac32015-06-22 16:46:29 -0700861 retOffset = getWordIteratorWithText().getPrevWordBeginningOnTwoWordsBoundary(offset);
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800862 }
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700863 if (retOffset == BreakIterator.DONE) {
864 return offset;
865 }
866 return retOffset;
867 }
868
869 private int getWordEnd(int offset) {
870 int retOffset = getWordIteratorWithText().nextBoundary(offset);
Mady Mellor58c90872015-05-12 11:09:37 -0700871 if (getWordIteratorWithText().isAfterPunctuation(retOffset)) {
872 // On punctuation boundary or within group of punctuation, find punctuation end.
873 retOffset = getWordIteratorWithText().getPunctuationEnd(offset);
874 } else {
875 // Not on a punctuation boundary, find the word end.
Mady Mellore264ac32015-06-22 16:46:29 -0700876 retOffset = getWordIteratorWithText().getNextWordEndOnTwoWordBoundary(offset);
Mady Mellor6c7b4ad2015-04-15 14:23:26 -0700877 }
878 if (retOffset == BreakIterator.DONE) {
879 return offset;
880 }
881 return retOffset;
882 }
883
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900884 private boolean needsToSelectAllToSelectWordOrParagraph() {
Andrei Stingaceanu47f82ae2015-04-28 17:43:54 +0100885 if (mTextView.hasPasswordTransformationMethod()) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700886 // Always select all on a password field.
887 // Cut/copy menu entries are not available for passwords, but being able to select all
888 // is however useful to delete or paste to replace the entire content.
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900889 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -0700890 }
891
892 int inputType = mTextView.getInputType();
893 int klass = inputType & InputType.TYPE_MASK_CLASS;
894 int variation = inputType & InputType.TYPE_MASK_VARIATION;
895
896 // Specific text field types: select the entire text for these
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700897 if (klass == InputType.TYPE_CLASS_NUMBER
898 || klass == InputType.TYPE_CLASS_PHONE
899 || klass == InputType.TYPE_CLASS_DATETIME
900 || variation == InputType.TYPE_TEXT_VARIATION_URI
901 || variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
902 || variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS
903 || variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900904 return true;
905 }
906 return false;
907 }
908
909 /**
910 * Adjusts selection to the word under last touch offset. Return true if the operation was
911 * successfully performed.
912 */
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +0100913 boolean selectCurrentWord() {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900914 if (!mTextView.canSelectText()) {
915 return false;
916 }
917
918 if (needsToSelectAllToSelectWordOrParagraph()) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700919 return mTextView.selectAllText();
920 }
921
922 long lastTouchOffsets = getLastTouchOffsets();
923 final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
924 final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
925
926 // Safety check in case standard touch event handling has been bypassed
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -0800927 if (minOffset < 0 || minOffset > mTextView.getText().length()) return false;
928 if (maxOffset < 0 || maxOffset > mTextView.getText().length()) return false;
Gilles Debunned88876a2012-03-16 17:34:04 -0700929
930 int selectionStart, selectionEnd;
931
932 // If a URLSpan (web address, email, phone...) is found at that position, select it.
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700933 URLSpan[] urlSpans =
934 ((Spanned) mTextView.getText()).getSpans(minOffset, maxOffset, URLSpan.class);
Gilles Debunned88876a2012-03-16 17:34:04 -0700935 if (urlSpans.length >= 1) {
936 URLSpan urlSpan = urlSpans[0];
937 selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan);
938 selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan);
939 } else {
Mady Mellor2ff2cd82015-03-02 10:37:01 -0800940 // FIXME - We should check if there's a LocaleSpan in the text, this may be
941 // something we should try handling or checking for.
Gilles Debunned88876a2012-03-16 17:34:04 -0700942 final WordIterator wordIterator = getWordIterator();
943 wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset);
944
945 selectionStart = wordIterator.getBeginning(minOffset);
946 selectionEnd = wordIterator.getEnd(maxOffset);
947
Aurimas Liutikasee62c292016-07-21 15:05:40 -0700948 if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE
949 || selectionStart == selectionEnd) {
Gilles Debunned88876a2012-03-16 17:34:04 -0700950 // Possible when the word iterator does not properly handle the text's language
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +0900951 long range = getCharClusterRange(minOffset);
Gilles Debunned88876a2012-03-16 17:34:04 -0700952 selectionStart = TextUtils.unpackRangeStartFromLong(range);
953 selectionEnd = TextUtils.unpackRangeEndFromLong(range);
954 }
955 }
956
957 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
958 return selectionEnd > selectionStart;
959 }
960
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +0900961 /**
962 * Adjusts selection to the paragraph under last touch offset. Return true if the operation was
963 * successfully performed.
964 */
965 private boolean selectCurrentParagraph() {
966 if (!mTextView.canSelectText()) {
967 return false;
968 }
969
970 if (needsToSelectAllToSelectWordOrParagraph()) {
971 return mTextView.selectAllText();
972 }
973
974 long lastTouchOffsets = getLastTouchOffsets();
975 final int minLastTouchOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
976 final int maxLastTouchOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
977
978 final long paragraphsRange = getParagraphsRange(minLastTouchOffset, maxLastTouchOffset);
979 final int start = TextUtils.unpackRangeStartFromLong(paragraphsRange);
980 final int end = TextUtils.unpackRangeEndFromLong(paragraphsRange);
981 if (start < end) {
982 Selection.setSelection((Spannable) mTextView.getText(), start, end);
983 return true;
984 }
985 return false;
986 }
987
988 /**
989 * Get the minimum range of paragraphs that contains startOffset and endOffset.
990 */
991 private long getParagraphsRange(int startOffset, int endOffset) {
992 final Layout layout = mTextView.getLayout();
993 if (layout == null) {
994 return TextUtils.packRangeInLong(-1, -1);
995 }
996 final CharSequence text = mTextView.getText();
997 int minLine = layout.getLineForOffset(startOffset);
998 // Search paragraph start.
999 while (minLine > 0) {
1000 final int prevLineEndOffset = layout.getLineEnd(minLine - 1);
1001 if (text.charAt(prevLineEndOffset - 1) == '\n') {
1002 break;
1003 }
1004 minLine--;
1005 }
1006 int maxLine = layout.getLineForOffset(endOffset);
1007 // Search paragraph end.
1008 while (maxLine < layout.getLineCount() - 1) {
1009 final int lineEndOffset = layout.getLineEnd(maxLine);
1010 if (text.charAt(lineEndOffset - 1) == '\n') {
1011 break;
1012 }
1013 maxLine++;
1014 }
1015 return TextUtils.packRangeInLong(layout.getLineStart(minLine), layout.getLineEnd(maxLine));
1016 }
1017
Gilles Debunned88876a2012-03-16 17:34:04 -07001018 void onLocaleChanged() {
Keisuke Kuroyanagie0ac5ac2016-03-09 15:33:30 +09001019 // Will be re-created on demand in getWordIterator and getWordIteratorWithText with the
1020 // proper new locale
Gilles Debunned88876a2012-03-16 17:34:04 -07001021 mWordIterator = null;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08001022 mWordIteratorWithText = null;
Gilles Debunned88876a2012-03-16 17:34:04 -07001023 }
1024
Gilles Debunned88876a2012-03-16 17:34:04 -07001025 public WordIterator getWordIterator() {
1026 if (mWordIterator == null) {
1027 mWordIterator = new WordIterator(mTextView.getTextServicesLocale());
1028 }
1029 return mWordIterator;
1030 }
1031
Mady Mellor2ff2cd82015-03-02 10:37:01 -08001032 private WordIterator getWordIteratorWithText() {
1033 if (mWordIteratorWithText == null) {
1034 mWordIteratorWithText = new WordIterator(mTextView.getTextServicesLocale());
1035 mUpdateWordIteratorText = true;
1036 }
1037 if (mUpdateWordIteratorText) {
1038 // FIXME - Shouldn't copy all of the text as only the area of the text relevant
1039 // to the user's selection is needed. A possible solution would be to
1040 // copy some number N of characters near the selection and then when the
1041 // user approaches N then we'd do another copy of the next N characters.
1042 CharSequence text = mTextView.getText();
1043 mWordIteratorWithText.setCharSequence(text, 0, text.length());
1044 mUpdateWordIteratorText = false;
1045 }
1046 return mWordIteratorWithText;
1047 }
1048
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +09001049 private int getNextCursorOffset(int offset, boolean findAfterGivenOffset) {
1050 final Layout layout = mTextView.getLayout();
1051 if (layout == null) return offset;
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001052 return findAfterGivenOffset == layout.isRtlCharAt(offset)
1053 ? layout.getOffsetToLeftOf(offset) : layout.getOffsetToRightOf(offset);
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +09001054 }
1055
1056 private long getCharClusterRange(int offset) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001057 final int textLength = mTextView.getText().length();
Gilles Debunned88876a2012-03-16 17:34:04 -07001058 if (offset < textLength) {
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08001059 final int clusterEndOffset = getNextCursorOffset(offset, true);
1060 return TextUtils.packRangeInLong(
1061 getNextCursorOffset(clusterEndOffset, false), clusterEndOffset);
Gilles Debunned88876a2012-03-16 17:34:04 -07001062 }
1063 if (offset - 1 >= 0) {
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08001064 final int clusterStartOffset = getNextCursorOffset(offset, false);
1065 return TextUtils.packRangeInLong(clusterStartOffset,
1066 getNextCursorOffset(clusterStartOffset, true));
Gilles Debunned88876a2012-03-16 17:34:04 -07001067 }
Keisuke Kuroyanagi9c68b292015-04-24 18:55:56 +09001068 return TextUtils.packRangeInLong(offset, offset);
Gilles Debunned88876a2012-03-16 17:34:04 -07001069 }
1070
1071 private boolean touchPositionIsInSelection() {
1072 int selectionStart = mTextView.getSelectionStart();
1073 int selectionEnd = mTextView.getSelectionEnd();
1074
1075 if (selectionStart == selectionEnd) {
1076 return false;
1077 }
1078
1079 if (selectionStart > selectionEnd) {
1080 int tmp = selectionStart;
1081 selectionStart = selectionEnd;
1082 selectionEnd = tmp;
1083 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
1084 }
1085
1086 SelectionModifierCursorController selectionController = getSelectionController();
1087 int minOffset = selectionController.getMinTouchOffset();
1088 int maxOffset = selectionController.getMaxTouchOffset();
1089
1090 return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
1091 }
1092
1093 private PositionListener getPositionListener() {
1094 if (mPositionListener == null) {
1095 mPositionListener = new PositionListener();
1096 }
1097 return mPositionListener;
1098 }
1099
1100 private interface TextViewPositionListener {
1101 public void updatePosition(int parentPositionX, int parentPositionY,
1102 boolean parentPositionChanged, boolean parentScrolled);
1103 }
1104
Gilles Debunned88876a2012-03-16 17:34:04 -07001105 private boolean isOffsetVisible(int offset) {
1106 Layout layout = mTextView.getLayout();
Victoria Leaseb9b77ae2013-10-13 15:12:52 -07001107 if (layout == null) return false;
1108
Gilles Debunned88876a2012-03-16 17:34:04 -07001109 final int line = layout.getLineForOffset(offset);
1110 final int lineBottom = layout.getLineBottom(line);
1111 final int primaryHorizontal = (int) layout.getPrimaryHorizontal(offset);
Phil Weaverc2e28932016-12-08 12:29:25 -08001112 return mTextView.isPositionVisible(
1113 primaryHorizontal + mTextView.viewportToContentHorizontalOffset(),
Gilles Debunned88876a2012-03-16 17:34:04 -07001114 lineBottom + mTextView.viewportToContentVerticalOffset());
1115 }
1116
1117 /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
1118 * in the view. Returns false when the position is in the empty space of left/right of text.
1119 */
1120 private boolean isPositionOnText(float x, float y) {
1121 Layout layout = mTextView.getLayout();
1122 if (layout == null) return false;
1123
1124 final int line = mTextView.getLineAtCoordinate(y);
1125 x = mTextView.convertToLocalHorizontalCoordinate(x);
1126
1127 if (x < layout.getLineLeft(line)) return false;
1128 if (x > layout.getLineRight(line)) return false;
1129 return true;
1130 }
1131
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001132 private void startDragAndDrop() {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01001133 getSelectionActionModeHelper().onSelectionDrag();
1134
Keisuke Kuroyanagifdfc93d2016-03-15 14:47:08 +09001135 // TODO: Fix drag and drop in full screen extracted mode.
1136 if (mTextView.isInExtractedMode()) {
1137 return;
1138 }
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001139 final int start = mTextView.getSelectionStart();
1140 final int end = mTextView.getSelectionEnd();
1141 CharSequence selectedText = mTextView.getTransformedText(start, end);
1142 ClipData data = ClipData.newPlainText(null, selectedText);
1143 DragLocalState localState = new DragLocalState(mTextView, start, end);
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08001144 mTextView.startDragAndDrop(data, getTextThumbnailBuilder(start, end), localState,
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001145 View.DRAG_FLAG_GLOBAL);
1146 stopTextActionMode();
1147 if (hasSelectionController()) {
1148 getSelectionController().resetTouchOffsets();
1149 }
1150 }
1151
Gilles Debunned88876a2012-03-16 17:34:04 -07001152 public boolean performLongClick(boolean handled) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001153 // Long press in empty space moves cursor and starts the insertion action mode.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001154 if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY)
1155 && mInsertionControllerEnabled) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001156 final int offset = mTextView.getOffsetForPosition(mLastDownPositionX,
1157 mLastDownPositionY);
Gilles Debunned88876a2012-03-16 17:34:04 -07001158 Selection.setSelection((Spannable) mTextView.getText(), offset);
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00001159 getInsertionController().show();
Clara Bayarrib71dddd2015-06-04 23:17:30 +01001160 mIsInsertionActionModeStartPending = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001161 handled = true;
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001162 MetricsLogger.action(
1163 mTextView.getContext(),
1164 MetricsEvent.TEXT_LONGPRESS,
1165 TextViewMetrics.SUBTYPE_LONG_PRESS_OTHER);
Gilles Debunned88876a2012-03-16 17:34:04 -07001166 }
1167
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001168 if (!handled && mTextActionMode != null) {
Andrei Stingaceanu2aaeefe2015-10-20 19:11:23 +01001169 if (touchPositionIsInSelection()) {
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09001170 startDragAndDrop();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001171 MetricsLogger.action(
1172 mTextView.getContext(),
1173 MetricsEvent.TEXT_LONGPRESS,
1174 TextViewMetrics.SUBTYPE_LONG_PRESS_DRAG_AND_DROP);
Gilles Debunned88876a2012-03-16 17:34:04 -07001175 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001176 stopTextActionMode();
Clara Bayarridfac4432015-05-15 12:18:24 +01001177 selectCurrentWordAndStartDrag();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001178 MetricsLogger.action(
1179 mTextView.getContext(),
1180 MetricsEvent.TEXT_LONGPRESS,
1181 TextViewMetrics.SUBTYPE_LONG_PRESS_SELECTION);
Gilles Debunned88876a2012-03-16 17:34:04 -07001182 }
1183 handled = true;
1184 }
1185
1186 // Start a new selection
1187 if (!handled) {
Clara Bayarridfac4432015-05-15 12:18:24 +01001188 handled = selectCurrentWordAndStartDrag();
Abodunrinwa Toki1b304e42016-11-03 23:27:58 +00001189 if (handled) {
1190 MetricsLogger.action(
1191 mTextView.getContext(),
1192 MetricsEvent.TEXT_LONGPRESS,
1193 TextViewMetrics.SUBTYPE_LONG_PRESS_SELECTION);
1194 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001195 }
1196
1197 return handled;
1198 }
1199
Petar Å egina91df3f92017-08-15 16:20:43 +01001200 float getLastUpPositionX() {
1201 return mLastUpPositionX;
1202 }
1203
1204 float getLastUpPositionY() {
1205 return mLastUpPositionY;
1206 }
1207
Gilles Debunned88876a2012-03-16 17:34:04 -07001208 private long getLastTouchOffsets() {
1209 SelectionModifierCursorController selectionController = getSelectionController();
1210 final int minOffset = selectionController.getMinTouchOffset();
1211 final int maxOffset = selectionController.getMaxTouchOffset();
1212 return TextUtils.packRangeInLong(minOffset, maxOffset);
1213 }
1214
1215 void onFocusChanged(boolean focused, int direction) {
1216 mShowCursor = SystemClock.uptimeMillis();
1217 ensureEndedBatchEdit();
1218
1219 if (focused) {
1220 int selStart = mTextView.getSelectionStart();
1221 int selEnd = mTextView.getSelectionEnd();
1222
1223 // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
1224 // mode for these, unless there was a specific selection already started.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001225 final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0
1226 && selEnd == mTextView.getText().length();
Gilles Debunned88876a2012-03-16 17:34:04 -07001227
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001228 mCreatedWithASelection = mFrozenWithFocus && mTextView.hasSelection()
1229 && !isFocusHighlighted;
Gilles Debunned88876a2012-03-16 17:34:04 -07001230
1231 if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
1232 // If a tap was used to give focus to that view, move cursor at tap position.
1233 // Has to be done before onTakeFocus, which can be overloaded.
1234 final int lastTapPosition = getLastTapPosition();
1235 if (lastTapPosition >= 0) {
1236 Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition);
1237 }
1238
1239 // Note this may have to be moved out of the Editor class
1240 MovementMethod mMovement = mTextView.getMovementMethod();
1241 if (mMovement != null) {
1242 mMovement.onTakeFocus(mTextView, (Spannable) mTextView.getText(), direction);
1243 }
1244
1245 // The DecorView does not have focus when the 'Done' ExtractEditText button is
1246 // pressed. Since it is the ViewAncestor's mView, it requests focus before
1247 // ExtractEditText clears focus, which gives focus to the ExtractEditText.
1248 // This special case ensure that we keep current selection in that case.
1249 // It would be better to know why the DecorView does not have focus at that time.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001250 if (((mTextView.isInExtractedMode()) || mSelectionMoved)
1251 && selStart >= 0 && selEnd >= 0) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001252 /*
1253 * Someone intentionally set the selection, so let them
1254 * do whatever it is that they wanted to do instead of
1255 * the default on-focus behavior. We reset the selection
1256 * here instead of just skipping the onTakeFocus() call
1257 * because some movement methods do something other than
1258 * just setting the selection in theirs and we still
1259 * need to go through that path.
1260 */
1261 Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
1262 }
1263
1264 if (mSelectAllOnFocus) {
1265 mTextView.selectAllText();
1266 }
1267
1268 mTouchFocusSelected = true;
1269 }
1270
1271 mFrozenWithFocus = false;
1272 mSelectionMoved = false;
1273
1274 if (mError != null) {
1275 showError();
1276 }
1277
1278 makeBlink();
1279 } else {
1280 if (mError != null) {
1281 hideError();
1282 }
1283 // Don't leave us in the middle of a batch edit.
1284 mTextView.onEndBatchEdit();
1285
Andrei Stingaceanub1891b32015-06-19 16:44:37 +01001286 if (mTextView.isInExtractedMode()) {
Mady Mellora2861452015-06-25 08:40:27 -07001287 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001288 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -07001289 } else {
Mady Mellora2861452015-06-25 08:40:27 -07001290 hideCursorAndSpanControllers();
Yohei Yukawa24df9312016-03-31 17:15:23 -07001291 if (mTextView.isTemporarilyDetached()) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001292 stopTextActionModeWithPreservingSelection();
1293 } else {
1294 stopTextActionMode();
1295 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001296 downgradeEasyCorrectionSpans();
1297 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001298 // No need to create the controller
1299 if (mSelectionModifierCursorController != null) {
1300 mSelectionModifierCursorController.resetTouchOffsets();
1301 }
1302 }
1303 }
1304
1305 /**
1306 * Downgrades to simple suggestions all the easy correction spans that are not a spell check
1307 * span.
1308 */
1309 private void downgradeEasyCorrectionSpans() {
1310 CharSequence text = mTextView.getText();
1311 if (text instanceof Spannable) {
1312 Spannable spannable = (Spannable) text;
1313 SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
1314 spannable.length(), SuggestionSpan.class);
1315 for (int i = 0; i < suggestionSpans.length; i++) {
1316 int flags = suggestionSpans[i].getFlags();
1317 if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
1318 && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
1319 flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
1320 suggestionSpans[i].setFlags(flags);
1321 }
1322 }
1323 }
1324 }
1325
Abodunrinwa Toki78940eb2017-09-12 02:58:49 +01001326 void sendOnTextChanged(int start, int before, int after) {
1327 getSelectionActionModeHelper().onTextChanged(start, start + before);
Gilles Debunned88876a2012-03-16 17:34:04 -07001328 updateSpellCheckSpans(start, start + after, false);
1329
Mady Mellor2ff2cd82015-03-02 10:37:01 -08001330 // Flip flag to indicate the word iterator needs to have the text reset.
1331 mUpdateWordIteratorText = true;
1332
Gilles Debunned88876a2012-03-16 17:34:04 -07001333 // Hide the controllers as soon as text is modified (typing, procedural...)
1334 // We do not hide the span controllers, since they can be added when a new text is
1335 // inserted into the text view (voice IME).
1336 hideCursorControllers();
Keisuke Kuroyanagif4e347d2015-06-11 17:41:00 +09001337 // Reset drag accelerator.
1338 if (mSelectionModifierCursorController != null) {
1339 mSelectionModifierCursorController.resetTouchOffsets();
1340 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001341 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07001342 }
1343
1344 private int getLastTapPosition() {
1345 // No need to create the controller at that point, no last tap position saved
1346 if (mSelectionModifierCursorController != null) {
1347 int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
1348 if (lastTapPosition >= 0) {
1349 // Safety check, should not be possible.
1350 if (lastTapPosition > mTextView.getText().length()) {
1351 lastTapPosition = mTextView.getText().length();
1352 }
1353 return lastTapPosition;
1354 }
1355 }
1356
1357 return -1;
1358 }
1359
1360 void onWindowFocusChanged(boolean hasWindowFocus) {
1361 if (hasWindowFocus) {
1362 if (mBlink != null) {
1363 mBlink.uncancel();
1364 makeBlink();
1365 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001366 if (mTextView.hasSelection() && !extractedTextModeWillBeStarted()) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09001367 refreshTextActionMode();
Mady Mellora2861452015-06-25 08:40:27 -07001368 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001369 } else {
1370 if (mBlink != null) {
1371 mBlink.cancel();
1372 }
1373 if (mInputContentType != null) {
1374 mInputContentType.enterDown = false;
1375 }
1376 // Order matters! Must be done before onParentLostFocus to rely on isShowingUp
Mady Mellora2861452015-06-25 08:40:27 -07001377 hideCursorAndSpanControllers();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08001378 stopTextActionModeWithPreservingSelection();
Gilles Debunned88876a2012-03-16 17:34:04 -07001379 if (mSuggestionsPopupWindow != null) {
1380 mSuggestionsPopupWindow.onParentLostFocus();
1381 }
1382
Gilles Debunnec72fba82012-06-26 14:47:07 -07001383 // Don't leave us in the middle of a batch edit. Same as in onFocusChanged
1384 ensureEndedBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001385 }
1386 }
1387
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09001388 private void updateTapState(MotionEvent event) {
1389 final int action = event.getActionMasked();
1390 if (action == MotionEvent.ACTION_DOWN) {
1391 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
1392 // Detect double tap and triple click.
1393 if (((mTapState == TAP_STATE_FIRST_TAP)
1394 || ((mTapState == TAP_STATE_DOUBLE_TAP) && isMouse))
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001395 && (SystemClock.uptimeMillis() - mLastTouchUpTime)
1396 <= ViewConfiguration.getDoubleTapTimeout()) {
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09001397 if (mTapState == TAP_STATE_FIRST_TAP) {
1398 mTapState = TAP_STATE_DOUBLE_TAP;
1399 } else {
1400 mTapState = TAP_STATE_TRIPLE_CLICK;
1401 }
1402 } else {
1403 mTapState = TAP_STATE_FIRST_TAP;
1404 }
1405 }
1406 if (action == MotionEvent.ACTION_UP) {
1407 mLastTouchUpTime = SystemClock.uptimeMillis();
1408 }
1409 }
1410
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09001411 private boolean shouldFilterOutTouchEvent(MotionEvent event) {
1412 if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) {
1413 return false;
1414 }
1415 final boolean primaryButtonStateChanged =
1416 ((mLastButtonState ^ event.getButtonState()) & MotionEvent.BUTTON_PRIMARY) != 0;
1417 final int action = event.getActionMasked();
1418 if ((action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_UP)
1419 && !primaryButtonStateChanged) {
1420 return true;
1421 }
1422 if (action == MotionEvent.ACTION_MOVE
1423 && !event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
1424 return true;
1425 }
1426 return false;
1427 }
1428
Gilles Debunned88876a2012-03-16 17:34:04 -07001429 void onTouchEvent(MotionEvent event) {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09001430 final boolean filterOutEvent = shouldFilterOutTouchEvent(event);
1431 mLastButtonState = event.getButtonState();
1432 if (filterOutEvent) {
1433 if (event.getActionMasked() == MotionEvent.ACTION_UP) {
1434 mDiscardNextActionUp = true;
1435 }
1436 return;
1437 }
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09001438 updateTapState(event);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001439 updateFloatingToolbarVisibility(event);
1440
Gilles Debunned88876a2012-03-16 17:34:04 -07001441 if (hasSelectionController()) {
1442 getSelectionController().onTouchEvent(event);
1443 }
1444
1445 if (mShowSuggestionRunnable != null) {
1446 mTextView.removeCallbacks(mShowSuggestionRunnable);
1447 mShowSuggestionRunnable = null;
1448 }
1449
Petar Å egina91df3f92017-08-15 16:20:43 +01001450 if (event.getActionMasked() == MotionEvent.ACTION_UP) {
1451 mLastUpPositionX = event.getX();
1452 mLastUpPositionY = event.getY();
1453 }
1454
Gilles Debunned88876a2012-03-16 17:34:04 -07001455 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
1456 mLastDownPositionX = event.getX();
1457 mLastDownPositionY = event.getY();
1458
1459 // Reset this state; it will be re-set if super.onTouchEvent
1460 // causes focus to move to the view.
1461 mTouchFocusSelected = false;
1462 mIgnoreActionUpEvent = false;
1463 }
1464 }
1465
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001466 private void updateFloatingToolbarVisibility(MotionEvent event) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001467 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001468 switch (event.getActionMasked()) {
1469 case MotionEvent.ACTION_MOVE:
Abodunrinwa Toki4a7aeb32017-07-13 02:06:56 +01001470 hideFloatingToolbar(ActionMode.DEFAULT_HIDE_DURATION);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001471 break;
1472 case MotionEvent.ACTION_UP: // fall through
1473 case MotionEvent.ACTION_CANCEL:
1474 showFloatingToolbar();
1475 }
1476 }
1477 }
1478
Abodunrinwa Toki4a7aeb32017-07-13 02:06:56 +01001479 void hideFloatingToolbar(int duration) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001480 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001481 mTextView.removeCallbacks(mShowFloatingToolbar);
Abodunrinwa Toki4a7aeb32017-07-13 02:06:56 +01001482 mTextActionMode.hide(duration);
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001483 }
1484 }
1485
1486 private void showFloatingToolbar() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01001487 if (mTextActionMode != null) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001488 // Delay "show" so it doesn't interfere with click confirmations
1489 // or double-clicks that could "dismiss" the floating toolbar.
1490 int delay = ViewConfiguration.getDoubleTapTimeout();
1491 mTextView.postDelayed(mShowFloatingToolbar, delay);
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01001492
1493 // This classifies the text and most likely returns before the toolbar is actually
1494 // shown. If not, it will update the toolbar with the result when classification
1495 // returns. We would rather not wait for a long running classification process.
1496 invalidateActionModeAsync();
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01001497 }
1498 }
1499
Gilles Debunned88876a2012-03-16 17:34:04 -07001500 public void beginBatchEdit() {
1501 mInBatchEditControllers = true;
1502 final InputMethodState ims = mInputMethodState;
1503 if (ims != null) {
1504 int nesting = ++ims.mBatchEditNesting;
1505 if (nesting == 1) {
1506 ims.mCursorChanged = false;
1507 ims.mChangedDelta = 0;
1508 if (ims.mContentChanged) {
1509 // We already have a pending change from somewhere else,
1510 // so turn this into a full update.
1511 ims.mChangedStart = 0;
1512 ims.mChangedEnd = mTextView.getText().length();
1513 } else {
1514 ims.mChangedStart = EXTRACT_UNKNOWN;
1515 ims.mChangedEnd = EXTRACT_UNKNOWN;
1516 ims.mContentChanged = false;
1517 }
James Cook48e0fac2015-02-25 15:44:51 -08001518 mUndoInputFilter.beginBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001519 mTextView.onBeginBatchEdit();
1520 }
1521 }
1522 }
1523
1524 public void endBatchEdit() {
1525 mInBatchEditControllers = false;
1526 final InputMethodState ims = mInputMethodState;
1527 if (ims != null) {
1528 int nesting = --ims.mBatchEditNesting;
1529 if (nesting == 0) {
1530 finishBatchEdit(ims);
1531 }
1532 }
1533 }
1534
1535 void ensureEndedBatchEdit() {
1536 final InputMethodState ims = mInputMethodState;
1537 if (ims != null && ims.mBatchEditNesting != 0) {
1538 ims.mBatchEditNesting = 0;
1539 finishBatchEdit(ims);
1540 }
1541 }
1542
1543 void finishBatchEdit(final InputMethodState ims) {
1544 mTextView.onEndBatchEdit();
James Cook48e0fac2015-02-25 15:44:51 -08001545 mUndoInputFilter.endBatchEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07001546
1547 if (ims.mContentChanged || ims.mSelectionModeChanged) {
1548 mTextView.updateAfterEdit();
1549 reportExtractedText();
1550 } else if (ims.mCursorChanged) {
Jean Chalardc99d33f2013-02-28 16:39:47 -08001551 // Cheesy way to get us to report the current cursor location.
Gilles Debunned88876a2012-03-16 17:34:04 -07001552 mTextView.invalidateCursor();
1553 }
Jean Chalardc99d33f2013-02-28 16:39:47 -08001554 // sendUpdateSelection knows to avoid sending if the selection did
1555 // not actually change.
1556 sendUpdateSelection();
Keisuke Kuroyanagic6fad962016-05-02 15:11:41 +09001557
1558 // Show drag handles if they were blocked by batch edit mode.
1559 if (mTextActionMode != null) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001560 final CursorController cursorController = mTextView.hasSelection()
1561 ? getSelectionController() : getInsertionController();
Keisuke Kuroyanagic6fad962016-05-02 15:11:41 +09001562 if (cursorController != null && !cursorController.isActive()
1563 && !cursorController.isCursorBeingModified()) {
1564 cursorController.show();
1565 }
1566 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001567 }
1568
1569 static final int EXTRACT_NOTHING = -2;
1570 static final int EXTRACT_UNKNOWN = -1;
1571
1572 boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
1573 return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
1574 EXTRACT_UNKNOWN, outText);
1575 }
1576
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001577 private boolean extractTextInternal(@Nullable ExtractedTextRequest request,
Gilles Debunned88876a2012-03-16 17:34:04 -07001578 int partialStartOffset, int partialEndOffset, int delta,
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001579 @Nullable ExtractedText outText) {
1580 if (request == null || outText == null) {
1581 return false;
Gilles Debunned88876a2012-03-16 17:34:04 -07001582 }
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001583
1584 final CharSequence content = mTextView.getText();
1585 if (content == null) {
1586 return false;
1587 }
1588
1589 if (partialStartOffset != EXTRACT_NOTHING) {
1590 final int N = content.length();
1591 if (partialStartOffset < 0) {
1592 outText.partialStartOffset = outText.partialEndOffset = -1;
1593 partialStartOffset = 0;
1594 partialEndOffset = N;
1595 } else {
1596 // Now use the delta to determine the actual amount of text
1597 // we need.
1598 partialEndOffset += delta;
1599 // Adjust offsets to ensure we contain full spans.
1600 if (content instanceof Spanned) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001601 Spanned spanned = (Spanned) content;
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001602 Object[] spans = spanned.getSpans(partialStartOffset,
1603 partialEndOffset, ParcelableSpan.class);
1604 int i = spans.length;
1605 while (i > 0) {
1606 i--;
1607 int j = spanned.getSpanStart(spans[i]);
1608 if (j < partialStartOffset) partialStartOffset = j;
1609 j = spanned.getSpanEnd(spans[i]);
1610 if (j > partialEndOffset) partialEndOffset = j;
1611 }
1612 }
1613 outText.partialStartOffset = partialStartOffset;
1614 outText.partialEndOffset = partialEndOffset - delta;
1615
1616 if (partialStartOffset > N) {
1617 partialStartOffset = N;
1618 } else if (partialStartOffset < 0) {
1619 partialStartOffset = 0;
1620 }
1621 if (partialEndOffset > N) {
1622 partialEndOffset = N;
1623 } else if (partialEndOffset < 0) {
1624 partialEndOffset = 0;
1625 }
1626 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07001627 if ((request.flags & InputConnection.GET_TEXT_WITH_STYLES) != 0) {
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001628 outText.text = content.subSequence(partialStartOffset,
1629 partialEndOffset);
1630 } else {
1631 outText.text = TextUtils.substring(content, partialStartOffset,
1632 partialEndOffset);
1633 }
1634 } else {
1635 outText.partialStartOffset = 0;
1636 outText.partialEndOffset = 0;
1637 outText.text = "";
1638 }
1639 outText.flags = 0;
1640 if (MetaKeyKeyListener.getMetaState(content, MetaKeyKeyListener.META_SELECTING) != 0) {
1641 outText.flags |= ExtractedText.FLAG_SELECTING;
1642 }
1643 if (mTextView.isSingleLine()) {
1644 outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
1645 }
1646 outText.startOffset = 0;
1647 outText.selectionStart = mTextView.getSelectionStart();
1648 outText.selectionEnd = mTextView.getSelectionEnd();
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001649 outText.hint = mTextView.getHint();
Yoshiki Iguchiee147722015-04-14 00:12:44 +09001650 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001651 }
1652
1653 boolean reportExtractedText() {
1654 final Editor.InputMethodState ims = mInputMethodState;
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001655 if (ims == null) {
1656 return false;
1657 }
1658 ims.mSelectionModeChanged = false;
1659 final ExtractedTextRequest req = ims.mExtractedTextRequest;
1660 if (req == null) {
1661 return false;
1662 }
1663 final InputMethodManager imm = InputMethodManager.peekInstance();
1664 if (imm == null) {
1665 return false;
1666 }
1667 if (TextView.DEBUG_EXTRACT) {
1668 Log.v(TextView.LOG_TAG, "Retrieving extracted start="
1669 + ims.mChangedStart
1670 + " end=" + ims.mChangedEnd
1671 + " delta=" + ims.mChangedDelta);
1672 }
1673 if (ims.mChangedStart < 0 && !ims.mContentChanged) {
1674 ims.mChangedStart = EXTRACT_NOTHING;
1675 }
1676 if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
1677 ims.mChangedDelta, ims.mExtractedText)) {
1678 if (TextView.DEBUG_EXTRACT) {
1679 Log.v(TextView.LOG_TAG,
1680 "Reporting extracted start="
1681 + ims.mExtractedText.partialStartOffset
1682 + " end=" + ims.mExtractedText.partialEndOffset
1683 + ": " + ims.mExtractedText.text);
Gilles Debunned88876a2012-03-16 17:34:04 -07001684 }
Clara Bayarrid8c5e7f2017-07-24 15:29:58 +01001685
1686 imm.updateExtractedText(mTextView, req.token, ims.mExtractedText);
1687 ims.mChangedStart = EXTRACT_UNKNOWN;
1688 ims.mChangedEnd = EXTRACT_UNKNOWN;
1689 ims.mChangedDelta = 0;
1690 ims.mContentChanged = false;
1691 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001692 }
1693 return false;
1694 }
1695
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001696 private void sendUpdateSelection() {
1697 if (null != mInputMethodState && mInputMethodState.mBatchEditNesting <= 0) {
1698 final InputMethodManager imm = InputMethodManager.peekInstance();
1699 if (null != imm) {
1700 final int selectionStart = mTextView.getSelectionStart();
1701 final int selectionEnd = mTextView.getSelectionEnd();
1702 int candStart = -1;
1703 int candEnd = -1;
1704 if (mTextView.getText() instanceof Spannable) {
1705 final Spannable sp = (Spannable) mTextView.getText();
1706 candStart = EditableInputConnection.getComposingSpanStart(sp);
1707 candEnd = EditableInputConnection.getComposingSpanEnd(sp);
1708 }
Jean Chalardc99d33f2013-02-28 16:39:47 -08001709 // InputMethodManager#updateSelection skips sending the message if
1710 // none of the parameters have changed since the last time we called it.
Jean Chalarddf7c72f2013-02-28 15:28:54 -08001711 imm.updateSelection(mTextView,
1712 selectionStart, selectionEnd, candStart, candEnd);
1713 }
1714 }
1715 }
1716
Gilles Debunned88876a2012-03-16 17:34:04 -07001717 void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint,
1718 int cursorOffsetVertical) {
1719 final int selectionStart = mTextView.getSelectionStart();
1720 final int selectionEnd = mTextView.getSelectionEnd();
1721
1722 final InputMethodState ims = mInputMethodState;
1723 if (ims != null && ims.mBatchEditNesting == 0) {
1724 InputMethodManager imm = InputMethodManager.peekInstance();
1725 if (imm != null) {
1726 if (imm.isActive(mTextView)) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001727 if (ims.mContentChanged || ims.mSelectionModeChanged) {
1728 // We are in extract mode and the content has changed
1729 // in some way... just report complete new text to the
1730 // input method.
Yohei Yukawab6bec1a2015-05-01 16:18:25 -07001731 reportExtractedText();
Gilles Debunned88876a2012-03-16 17:34:04 -07001732 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001733 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001734 }
1735 }
1736
1737 if (mCorrectionHighlighter != null) {
1738 mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
1739 }
1740
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07001741 if (highlight != null && selectionStart == selectionEnd && mDrawableForCursor != null) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001742 drawCursor(canvas, cursorOffsetVertical);
1743 // Rely on the drawable entirely, do not draw the cursor line.
1744 // Has to be done after the IMM related code above which relies on the highlight.
1745 highlight = null;
1746 }
1747
1748 if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
1749 drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
1750 cursorOffsetVertical);
1751 } else {
1752 layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
1753 }
Petar Å egina5ab7bb22017-09-05 20:48:42 +01001754
1755 if (mSelectionActionModeHelper != null) {
1756 mSelectionActionModeHelper.onDraw(canvas);
1757 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001758 }
1759
1760 private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
1761 Paint highlightPaint, int cursorOffsetVertical) {
Gilles Debunned88876a2012-03-16 17:34:04 -07001762 final long lineRange = layout.getLineRangeForDraw(canvas);
1763 int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
1764 int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
1765 if (lastLine < 0) return;
1766
1767 layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
1768 firstLine, lastLine);
1769
1770 if (layout instanceof DynamicLayout) {
Chris Craik956f3402015-04-27 16:41:00 -07001771 if (mTextRenderNodes == null) {
1772 mTextRenderNodes = ArrayUtils.emptyArray(TextRenderNode.class);
Gilles Debunned88876a2012-03-16 17:34:04 -07001773 }
1774
1775 DynamicLayout dynamicLayout = (DynamicLayout) layout;
Gilles Debunne157aafc2012-04-19 17:21:57 -07001776 int[] blockEndLines = dynamicLayout.getBlockEndLines();
Gilles Debunned88876a2012-03-16 17:34:04 -07001777 int[] blockIndices = dynamicLayout.getBlockIndices();
1778 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
Sangkyu Lee955beb22012-12-10 15:47:00 +09001779 final int indexFirstChangedBlock = dynamicLayout.getIndexFirstChangedBlock();
Gilles Debunned88876a2012-03-16 17:34:04 -07001780
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001781 final ArraySet<Integer> blockSet = dynamicLayout.getBlocksAlwaysNeedToBeRedrawn();
1782 if (blockSet != null) {
1783 for (int i = 0; i < blockSet.size(); i++) {
1784 final int blockIndex = dynamicLayout.getBlockIndex(blockSet.valueAt(i));
1785 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
1786 && mTextRenderNodes[blockIndex] != null) {
1787 mTextRenderNodes[blockIndex].needsToBeShifted = true;
1788 }
1789 }
1790 }
1791
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001792 int startBlock = Arrays.binarySearch(blockEndLines, 0, numberOfBlocks, firstLine);
1793 if (startBlock < 0) {
1794 startBlock = -(startBlock + 1);
1795 }
1796 startBlock = Math.min(indexFirstChangedBlock, startBlock);
Gilles Debunned88876a2012-03-16 17:34:04 -07001797
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001798 int startIndexToFindAvailableRenderNode = 0;
1799 int lastIndex = numberOfBlocks;
1800
1801 for (int i = startBlock; i < numberOfBlocks; i++) {
1802 final int blockIndex = blockIndices[i];
1803 if (i >= indexFirstChangedBlock
1804 && blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
1805 && mTextRenderNodes[blockIndex] != null) {
1806 mTextRenderNodes[blockIndex].needsToBeShifted = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001807 }
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001808 if (blockEndLines[i] < firstLine) {
1809 // Blocks in [indexFirstChangedBlock, firstLine) are not redrawn here. They will
1810 // be redrawn after they get scrolled into drawing range.
1811 continue;
Gilles Debunned88876a2012-03-16 17:34:04 -07001812 }
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001813 startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas, layout,
1814 highlight, highlightPaint, cursorOffsetVertical, blockEndLines,
1815 blockIndices, i, numberOfBlocks, startIndexToFindAvailableRenderNode);
1816 if (blockEndLines[i] >= lastLine) {
1817 lastIndex = Math.max(indexFirstChangedBlock, i + 1);
1818 break;
Gilles Debunned88876a2012-03-16 17:34:04 -07001819 }
Gilles Debunned88876a2012-03-16 17:34:04 -07001820 }
Keisuke Kuroyanagif5af4a32016-08-31 21:40:53 +09001821 if (blockSet != null) {
1822 for (int i = 0; i < blockSet.size(); i++) {
1823 final int block = blockSet.valueAt(i);
1824 final int blockIndex = dynamicLayout.getBlockIndex(block);
1825 if (blockIndex == DynamicLayout.INVALID_BLOCK_INDEX
1826 || mTextRenderNodes[blockIndex] == null
1827 || mTextRenderNodes[blockIndex].needsToBeShifted) {
1828 startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas,
1829 layout, highlight, highlightPaint, cursorOffsetVertical,
1830 blockEndLines, blockIndices, block, numberOfBlocks,
1831 startIndexToFindAvailableRenderNode);
1832 }
1833 }
1834 }
Sangkyu Lee955beb22012-12-10 15:47:00 +09001835
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001836 dynamicLayout.setIndexFirstChangedBlock(lastIndex);
Gilles Debunned88876a2012-03-16 17:34:04 -07001837 } else {
1838 // Boring layout is used for empty and hint text
1839 layout.drawText(canvas, firstLine, lastLine);
1840 }
1841 }
1842
Keisuke Kuroyanagi499c1592016-09-05 17:45:30 +09001843 private int drawHardwareAcceleratedInner(Canvas canvas, Layout layout, Path highlight,
1844 Paint highlightPaint, int cursorOffsetVertical, int[] blockEndLines,
1845 int[] blockIndices, int blockInfoIndex, int numberOfBlocks,
1846 int startIndexToFindAvailableRenderNode) {
1847 final int blockEndLine = blockEndLines[blockInfoIndex];
1848 int blockIndex = blockIndices[blockInfoIndex];
1849
1850 final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
1851 if (blockIsInvalid) {
1852 blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
1853 startIndexToFindAvailableRenderNode);
1854 // Note how dynamic layout's internal block indices get updated from Editor
1855 blockIndices[blockInfoIndex] = blockIndex;
1856 if (mTextRenderNodes[blockIndex] != null) {
1857 mTextRenderNodes[blockIndex].isDirty = true;
1858 }
1859 startIndexToFindAvailableRenderNode = blockIndex + 1;
1860 }
1861
1862 if (mTextRenderNodes[blockIndex] == null) {
1863 mTextRenderNodes[blockIndex] = new TextRenderNode("Text " + blockIndex);
1864 }
1865
1866 final boolean blockDisplayListIsInvalid = mTextRenderNodes[blockIndex].needsRecord();
1867 RenderNode blockDisplayList = mTextRenderNodes[blockIndex].renderNode;
1868 if (mTextRenderNodes[blockIndex].needsToBeShifted || blockDisplayListIsInvalid) {
1869 final int blockBeginLine = blockInfoIndex == 0 ?
1870 0 : blockEndLines[blockInfoIndex - 1] + 1;
1871 final int top = layout.getLineTop(blockBeginLine);
1872 final int bottom = layout.getLineBottom(blockEndLine);
1873 int left = 0;
1874 int right = mTextView.getWidth();
1875 if (mTextView.getHorizontallyScrolling()) {
1876 float min = Float.MAX_VALUE;
1877 float max = Float.MIN_VALUE;
1878 for (int line = blockBeginLine; line <= blockEndLine; line++) {
1879 min = Math.min(min, layout.getLineLeft(line));
1880 max = Math.max(max, layout.getLineRight(line));
1881 }
1882 left = (int) min;
1883 right = (int) (max + 0.5f);
1884 }
1885
1886 // Rebuild display list if it is invalid
1887 if (blockDisplayListIsInvalid) {
1888 final DisplayListCanvas displayListCanvas = blockDisplayList.start(
1889 right - left, bottom - top);
1890 try {
1891 // drawText is always relative to TextView's origin, this translation
1892 // brings this range of text back to the top left corner of the viewport
1893 displayListCanvas.translate(-left, -top);
1894 layout.drawText(displayListCanvas, blockBeginLine, blockEndLine);
1895 mTextRenderNodes[blockIndex].isDirty = false;
1896 // No need to untranslate, previous context is popped after
1897 // drawDisplayList
1898 } finally {
1899 blockDisplayList.end(displayListCanvas);
1900 // Same as drawDisplayList below, handled by our TextView's parent
1901 blockDisplayList.setClipToBounds(false);
1902 }
1903 }
1904
1905 // Valid display list only needs to update its drawing location.
1906 blockDisplayList.setLeftTopRightBottom(left, top, right, bottom);
1907 mTextRenderNodes[blockIndex].needsToBeShifted = false;
1908 }
1909 ((DisplayListCanvas) canvas).drawRenderNode(blockDisplayList);
1910 return startIndexToFindAvailableRenderNode;
1911 }
1912
Gilles Debunned88876a2012-03-16 17:34:04 -07001913 private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
1914 int searchStartIndex) {
Chris Craik956f3402015-04-27 16:41:00 -07001915 int length = mTextRenderNodes.length;
Gilles Debunned88876a2012-03-16 17:34:04 -07001916 for (int i = searchStartIndex; i < length; i++) {
1917 boolean blockIndexFound = false;
1918 for (int j = 0; j < numberOfBlocks; j++) {
1919 if (blockIndices[j] == i) {
1920 blockIndexFound = true;
1921 break;
1922 }
1923 }
1924 if (blockIndexFound) continue;
1925 return i;
1926 }
1927
1928 // No available index found, the pool has to grow
Chris Craik956f3402015-04-27 16:41:00 -07001929 mTextRenderNodes = GrowingArrayUtils.append(mTextRenderNodes, length, null);
Gilles Debunned88876a2012-03-16 17:34:04 -07001930 return length;
1931 }
1932
1933 private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
1934 final boolean translate = cursorOffsetVertical != 0;
1935 if (translate) canvas.translate(0, cursorOffsetVertical);
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07001936 if (mDrawableForCursor != null) {
1937 mDrawableForCursor.draw(canvas);
Gilles Debunned88876a2012-03-16 17:34:04 -07001938 }
1939 if (translate) canvas.translate(0, -cursorOffsetVertical);
1940 }
1941
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09001942 void invalidateHandlesAndActionMode() {
1943 if (mSelectionModifierCursorController != null) {
1944 mSelectionModifierCursorController.invalidateHandles();
1945 }
1946 if (mInsertionPointCursorController != null) {
1947 mInsertionPointCursorController.invalidateHandle();
1948 }
1949 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01001950 invalidateActionMode();
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09001951 }
1952 }
1953
Gilles Debunneebc86af2012-04-20 15:10:47 -07001954 /**
1955 * Invalidates all the sub-display lists that overlap the specified character range
1956 */
1957 void invalidateTextDisplayList(Layout layout, int start, int end) {
Chris Craik956f3402015-04-27 16:41:00 -07001958 if (mTextRenderNodes != null && layout instanceof DynamicLayout) {
Gilles Debunneebc86af2012-04-20 15:10:47 -07001959 final int firstLine = layout.getLineForOffset(start);
1960 final int lastLine = layout.getLineForOffset(end);
1961
1962 DynamicLayout dynamicLayout = (DynamicLayout) layout;
1963 int[] blockEndLines = dynamicLayout.getBlockEndLines();
1964 int[] blockIndices = dynamicLayout.getBlockIndices();
1965 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
1966
1967 int i = 0;
1968 // Skip the blocks before firstLine
1969 while (i < numberOfBlocks) {
1970 if (blockEndLines[i] >= firstLine) break;
1971 i++;
1972 }
1973
1974 // Invalidate all subsequent blocks until lastLine is passed
1975 while (i < numberOfBlocks) {
1976 final int blockIndex = blockIndices[i];
1977 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX) {
Chris Craik956f3402015-04-27 16:41:00 -07001978 mTextRenderNodes[blockIndex].isDirty = true;
Gilles Debunneebc86af2012-04-20 15:10:47 -07001979 }
1980 if (blockEndLines[i] >= lastLine) break;
1981 i++;
1982 }
1983 }
1984 }
1985
Gilles Debunned88876a2012-03-16 17:34:04 -07001986 void invalidateTextDisplayList() {
Chris Craik956f3402015-04-27 16:41:00 -07001987 if (mTextRenderNodes != null) {
1988 for (int i = 0; i < mTextRenderNodes.length; i++) {
1989 if (mTextRenderNodes[i] != null) mTextRenderNodes[i].isDirty = true;
Gilles Debunned88876a2012-03-16 17:34:04 -07001990 }
1991 }
1992 }
1993
Roozbeh Pournader9c133072017-07-26 22:36:27 -07001994 void updateCursorPosition() {
Gilles Debunned88876a2012-03-16 17:34:04 -07001995 if (mTextView.mCursorDrawableRes == 0) {
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07001996 mDrawableForCursor = null;
Gilles Debunned88876a2012-03-16 17:34:04 -07001997 return;
1998 }
1999
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002000 final Layout layout = mTextView.getLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -07002001 final int offset = mTextView.getSelectionStart();
2002 final int line = layout.getLineForOffset(offset);
2003 final int top = layout.getLineTop(line);
Siyamed Sinira60b59d2017-07-26 09:26:41 -07002004 final int bottom = layout.getLineBottomWithoutSpacing(line);
Gilles Debunned88876a2012-03-16 17:34:04 -07002005
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002006 final boolean clamped = layout.shouldClampCursor(line);
2007 updateCursorPosition(top, bottom, layout.getPrimaryHorizontal(offset, clamped));
Gilles Debunned88876a2012-03-16 17:34:04 -07002008 }
2009
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002010 void refreshTextActionMode() {
2011 if (extractedTextModeWillBeStarted()) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002012 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002013 return;
2014 }
2015 final boolean hasSelection = mTextView.hasSelection();
2016 final SelectionModifierCursorController selectionController = getSelectionController();
2017 final InsertionPointCursorController insertionController = getInsertionController();
2018 if ((selectionController != null && selectionController.isCursorBeingModified())
2019 || (insertionController != null && insertionController.isCursorBeingModified())) {
2020 // ActionMode should be managed by the currently active cursor controller.
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002021 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002022 return;
2023 }
2024 if (hasSelection) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002025 hideInsertionPointCursorController();
2026 if (mTextActionMode == null) {
Keisuke Kuroyanagi0fd28c92016-04-04 17:43:06 +09002027 if (mRestartActionModeOnNextRefresh) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002028 // To avoid distraction, newly start action mode only when selection action
Keisuke Kuroyanagi0fd28c92016-04-04 17:43:06 +09002029 // mode is being restarted.
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002030 startSelectionActionModeAsync(false);
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002031 }
2032 } else if (selectionController == null || !selectionController.isActive()) {
2033 // Insertion action mode is active. Avoid dismissing the selection.
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002034 stopTextActionModeWithPreservingSelection();
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002035 startSelectionActionModeAsync(false);
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002036 } else {
2037 mTextActionMode.invalidateContentRect();
2038 }
2039 } else {
2040 // Insertion action mode is started only when insertion controller is explicitly
2041 // activated.
2042 if (insertionController == null || !insertionController.isActive()) {
2043 stopTextActionMode();
2044 } else if (mTextActionMode != null) {
2045 mTextActionMode.invalidateContentRect();
2046 }
2047 }
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002048 mRestartActionModeOnNextRefresh = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002049 }
2050
Gilles Debunned88876a2012-03-16 17:34:04 -07002051 /**
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002052 * Start an Insertion action mode.
Gilles Debunned88876a2012-03-16 17:34:04 -07002053 */
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002054 void startInsertionActionMode() {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002055 if (mInsertionActionModeRunnable != null) {
2056 mTextView.removeCallbacks(mInsertionActionModeRunnable);
2057 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002058 if (extractedTextModeWillBeStarted()) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002059 return;
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002060 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002061 stopTextActionMode();
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002062
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002063 ActionMode.Callback actionModeCallback =
Richard Ledley26b87222017-11-30 10:54:08 +00002064 new TextActionModeCallback(TextActionMode.INSERTION);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002065 mTextActionMode = mTextView.startActionMode(
Clara Bayarrib8ed5b72015-04-09 15:26:41 +01002066 actionModeCallback, ActionMode.TYPE_FLOATING);
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002067 if (mTextActionMode != null && getInsertionController() != null) {
2068 getInsertionController().show();
2069 }
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00002070 }
2071
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002072 @NonNull
2073 TextView getTextView() {
2074 return mTextView;
2075 }
2076
2077 @Nullable
2078 ActionMode getTextActionMode() {
2079 return mTextActionMode;
2080 }
2081
2082 void setRestartActionModeOnNextRefresh(boolean value) {
2083 mRestartActionModeOnNextRefresh = value;
2084 }
2085
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00002086 /**
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002087 * Asynchronously starts a selection action mode using the TextClassifier.
Clara Bayarri29d2b5aa2015-03-13 17:41:56 +00002088 */
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002089 void startSelectionActionModeAsync(boolean adjustSelection) {
Richard Ledley26b87222017-11-30 10:54:08 +00002090 getSelectionActionModeHelper().startSelectionActionModeAsync(adjustSelection);
2091 }
2092
2093 void startLinkActionModeAsync(TextLinks.TextLink link) {
2094 Preconditions.checkNotNull(link);
2095 if (!(mTextView.getText() instanceof Spannable)) {
2096 return;
2097 }
Richard Ledley26b87222017-11-30 10:54:08 +00002098 stopTextActionMode();
Richard Ledley26b87222017-11-30 10:54:08 +00002099 getSelectionActionModeHelper().startLinkActionModeAsync(link);
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002100 }
2101
2102 /**
2103 * Asynchronously invalidates an action mode using the TextClassifier.
2104 */
Abodunrinwa Toki4ce651e2017-05-12 15:37:29 +01002105 void invalidateActionModeAsync() {
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002106 getSelectionActionModeHelper().invalidateActionModeAsync();
2107 }
2108
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002109 /**
2110 * Synchronously invalidates an action mode without the TextClassifier.
2111 */
2112 private void invalidateActionMode() {
2113 if (mTextActionMode != null) {
2114 mTextActionMode.invalidate();
2115 }
2116 }
2117
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002118 private SelectionActionModeHelper getSelectionActionModeHelper() {
2119 if (mSelectionActionModeHelper == null) {
2120 mSelectionActionModeHelper = new SelectionActionModeHelper(this);
Clara Bayarri578286f2015-04-10 15:35:31 +01002121 }
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002122 return mSelectionActionModeHelper;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00002123 }
2124
Clara Bayarridfac4432015-05-15 12:18:24 +01002125 /**
2126 * If the TextView allows text selection, selects the current word when no existing selection
2127 * was available and starts a drag.
2128 *
2129 * @return true if the drag was started.
2130 */
2131 private boolean selectCurrentWordAndStartDrag() {
Clara Bayarri7184c8a2015-06-05 17:34:09 +01002132 if (mInsertionActionModeRunnable != null) {
2133 mTextView.removeCallbacks(mInsertionActionModeRunnable);
2134 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002135 if (extractedTextModeWillBeStarted()) {
Clara Bayarridfac4432015-05-15 12:18:24 +01002136 return false;
2137 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002138 if (!checkField()) {
Clara Bayarridfac4432015-05-15 12:18:24 +01002139 return false;
2140 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002141 if (!mTextView.hasSelection() && !selectCurrentWord()) {
2142 // No selection and cannot select a word.
2143 return false;
2144 }
2145 stopTextActionModeWithPreservingSelection();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08002146 getSelectionController().enterDrag(
2147 SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_WORD);
Clara Bayarridfac4432015-05-15 12:18:24 +01002148 return true;
2149 }
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002150
Clara Bayarridfac4432015-05-15 12:18:24 +01002151 /**
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002152 * Checks whether a selection can be performed on the current TextView.
Clara Bayarridfac4432015-05-15 12:18:24 +01002153 *
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002154 * @return true if a selection can be performed
Clara Bayarridfac4432015-05-15 12:18:24 +01002155 */
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002156 boolean checkField() {
Clara Bayarridfac4432015-05-15 12:18:24 +01002157 if (!mTextView.canSelectText() || !mTextView.requestFocus()) {
2158 Log.w(TextView.LOG_TAG,
2159 "TextView does not support text selection. Selection cancelled.");
Andrei Stingaceanu975a8d02015-05-19 17:29:16 +01002160 return false;
2161 }
Clara Bayarridfac4432015-05-15 12:18:24 +01002162 return true;
2163 }
2164
Richard Ledley26b87222017-11-30 10:54:08 +00002165 boolean startActionModeInternal(@TextActionMode int actionMode) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002166 if (extractedTextModeWillBeStarted()) {
2167 return false;
2168 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002169 if (mTextActionMode != null) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01002170 // Text action mode is already started
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01002171 invalidateActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07002172 return false;
2173 }
2174
Richard Ledley724eff92017-12-21 10:11:34 +00002175 if (actionMode != TextActionMode.TEXT_LINK
2176 && (!checkField() || !mTextView.hasSelection())) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002177 return false;
2178 }
2179
Richard Ledley26b87222017-11-30 10:54:08 +00002180 ActionMode.Callback actionModeCallback = new TextActionModeCallback(actionMode);
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002181 mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING);
Gilles Debunned88876a2012-03-16 17:34:04 -07002182
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002183 final boolean selectionStarted = mTextActionMode != null;
Gilles Debunne3473b2b2012-04-20 16:21:10 -07002184 if (selectionStarted && !mTextView.isTextSelectable() && mShowSoftInputOnFocus) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002185 // Show the IME to be able to replace text, except when selecting non editable text.
2186 final InputMethodManager imm = InputMethodManager.peekInstance();
2187 if (imm != null) {
2188 imm.showSoftInput(mTextView, 0, null);
2189 }
2190 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002191 return selectionStarted;
2192 }
2193
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09002194 private boolean extractedTextModeWillBeStarted() {
Andrei Stingaceanub1891b32015-06-19 16:44:37 +01002195 if (!(mTextView.isInExtractedMode())) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002196 final InputMethodManager imm = InputMethodManager.peekInstance();
2197 return imm != null && imm.isFullscreenMode();
2198 }
2199 return false;
2200 }
2201
2202 /**
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002203 * @return <code>true</code> if it's reasonable to offer to show suggestions depending on
2204 * the current cursor position or selection range. This method is consistent with the
2205 * method to show suggestions {@link SuggestionsPopupWindow#updateSuggestions}.
Gilles Debunned88876a2012-03-16 17:34:04 -07002206 */
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002207 private boolean shouldOfferToShowSuggestions() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002208 CharSequence text = mTextView.getText();
2209 if (!(text instanceof Spannable)) return false;
2210
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002211 final Spannable spannable = (Spannable) text;
2212 final int selectionStart = mTextView.getSelectionStart();
2213 final int selectionEnd = mTextView.getSelectionEnd();
2214 final SuggestionSpan[] suggestionSpans = spannable.getSpans(selectionStart, selectionEnd,
2215 SuggestionSpan.class);
2216 if (suggestionSpans.length == 0) {
2217 return false;
2218 }
2219 if (selectionStart == selectionEnd) {
2220 // Spans overlap the cursor.
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002221 for (int i = 0; i < suggestionSpans.length; i++) {
2222 if (suggestionSpans[i].getSuggestions().length > 0) {
2223 return true;
2224 }
2225 }
2226 return false;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002227 }
2228 int minSpanStart = mTextView.getText().length();
2229 int maxSpanEnd = 0;
2230 int unionOfSpansCoveringSelectionStartStart = mTextView.getText().length();
2231 int unionOfSpansCoveringSelectionStartEnd = 0;
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002232 boolean hasValidSuggestions = false;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002233 for (int i = 0; i < suggestionSpans.length; i++) {
2234 final int spanStart = spannable.getSpanStart(suggestionSpans[i]);
2235 final int spanEnd = spannable.getSpanEnd(suggestionSpans[i]);
2236 minSpanStart = Math.min(minSpanStart, spanStart);
2237 maxSpanEnd = Math.max(maxSpanEnd, spanEnd);
2238 if (selectionStart < spanStart || selectionStart > spanEnd) {
2239 // The span doesn't cover the current selection start point.
2240 continue;
2241 }
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002242 hasValidSuggestions =
2243 hasValidSuggestions || suggestionSpans[i].getSuggestions().length > 0;
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002244 unionOfSpansCoveringSelectionStartStart =
2245 Math.min(unionOfSpansCoveringSelectionStartStart, spanStart);
2246 unionOfSpansCoveringSelectionStartEnd =
2247 Math.max(unionOfSpansCoveringSelectionStartEnd, spanEnd);
2248 }
Keisuke Kuroyanagi7e4fbe02015-05-22 14:37:01 +09002249 if (!hasValidSuggestions) {
2250 return false;
2251 }
Keisuke Kuroyanagi05fd8d52015-03-16 17:44:26 +09002252 if (unionOfSpansCoveringSelectionStartStart >= unionOfSpansCoveringSelectionStartEnd) {
2253 // No spans cover the selection start point.
2254 return false;
2255 }
2256 if (minSpanStart < unionOfSpansCoveringSelectionStartStart
2257 || maxSpanEnd > unionOfSpansCoveringSelectionStartEnd) {
2258 // There is a span that is not covered by the union. In this case, we soouldn't offer
2259 // to show suggestions as it's confusing.
2260 return false;
2261 }
2262 return true;
Gilles Debunned88876a2012-03-16 17:34:04 -07002263 }
2264
2265 /**
2266 * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
2267 * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
2268 */
2269 private boolean isCursorInsideEasyCorrectionSpan() {
2270 Spannable spannable = (Spannable) mTextView.getText();
2271 SuggestionSpan[] suggestionSpans = spannable.getSpans(mTextView.getSelectionStart(),
2272 mTextView.getSelectionEnd(), SuggestionSpan.class);
2273 for (int i = 0; i < suggestionSpans.length; i++) {
2274 if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
2275 return true;
2276 }
2277 }
2278 return false;
2279 }
2280
2281 void onTouchUpEvent(MotionEvent event) {
Abodunrinwa Toki8dd3a742017-05-02 18:44:19 +01002282 if (getSelectionActionModeHelper().resetSelection(
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +00002283 getTextView().getOffsetForPosition(event.getX(), event.getY()))) {
2284 return;
2285 }
2286
Gilles Debunned88876a2012-03-16 17:34:04 -07002287 boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect();
Mady Mellora2861452015-06-25 08:40:27 -07002288 hideCursorAndSpanControllers();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002289 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07002290 CharSequence text = mTextView.getText();
2291 if (!selectAllGotFocus && text.length() > 0) {
2292 // Move cursor
2293 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
2294 Selection.setSelection((Spannable) text, offset);
2295 if (mSpellChecker != null) {
2296 // When the cursor moves, the word that was typed may need spell check
2297 mSpellChecker.onSelectionChanged();
2298 }
Andrei Stingaceanu35c550c2015-05-07 16:49:49 +01002299
Gilles Debunned88876a2012-03-16 17:34:04 -07002300 if (!extractedTextModeWillBeStarted()) {
2301 if (isCursorInsideEasyCorrectionSpan()) {
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01002302 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002303 if (mInsertionActionModeRunnable != null) {
2304 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01002305 }
2306
Gilles Debunned88876a2012-03-16 17:34:04 -07002307 mShowSuggestionRunnable = new Runnable() {
2308 public void run() {
Keisuke Kuroyanagi713be062016-02-29 16:07:54 -08002309 replace();
Gilles Debunned88876a2012-03-16 17:34:04 -07002310 }
2311 };
2312 // removeCallbacks is performed on every touch
2313 mTextView.postDelayed(mShowSuggestionRunnable,
2314 ViewConfiguration.getDoubleTapTimeout());
2315 } else if (hasInsertionController()) {
2316 getInsertionController().show();
2317 }
2318 }
2319 }
2320 }
2321
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002322 protected void stopTextActionMode() {
2323 if (mTextActionMode != null) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002324 // This will hide the mSelectionModifierCursorController
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002325 mTextActionMode.finish();
Gilles Debunned88876a2012-03-16 17:34:04 -07002326 }
2327 }
2328
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002329 private void stopTextActionModeWithPreservingSelection() {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09002330 if (mTextActionMode != null) {
2331 mRestartActionModeOnNextRefresh = true;
2332 }
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002333 mPreserveSelection = true;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002334 stopTextActionMode();
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002335 mPreserveSelection = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002336 }
2337
Gilles Debunned88876a2012-03-16 17:34:04 -07002338 /**
2339 * @return True if this view supports insertion handles.
2340 */
2341 boolean hasInsertionController() {
2342 return mInsertionControllerEnabled;
2343 }
2344
2345 /**
2346 * @return True if this view supports selection handles.
2347 */
2348 boolean hasSelectionController() {
2349 return mSelectionControllerEnabled;
2350 }
2351
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09002352 private InsertionPointCursorController getInsertionController() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002353 if (!mInsertionControllerEnabled) {
2354 return null;
2355 }
2356
2357 if (mInsertionPointCursorController == null) {
2358 mInsertionPointCursorController = new InsertionPointCursorController();
2359
2360 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
2361 observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
2362 }
2363
2364 return mInsertionPointCursorController;
2365 }
2366
Abodunrinwa Toki8710ea12017-01-24 10:34:13 -08002367 @Nullable
2368 SelectionModifierCursorController getSelectionController() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002369 if (!mSelectionControllerEnabled) {
2370 return null;
2371 }
2372
2373 if (mSelectionModifierCursorController == null) {
2374 mSelectionModifierCursorController = new SelectionModifierCursorController();
2375
2376 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
2377 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
2378 }
2379
2380 return mSelectionModifierCursorController;
2381 }
2382
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002383 @VisibleForTesting
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002384 @Nullable
2385 public Drawable getCursorDrawable() {
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07002386 return mDrawableForCursor;
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002387 }
2388
Roozbeh Pournader9c133072017-07-26 22:36:27 -07002389 private void updateCursorPosition(int top, int bottom, float horizontal) {
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07002390 if (mDrawableForCursor == null) {
2391 mDrawableForCursor = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07002392 mTextView.mCursorDrawableRes);
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002393 }
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07002394 final int left = clampHorizontalPosition(mDrawableForCursor, horizontal);
2395 final int width = mDrawableForCursor.getIntrinsicWidth();
2396 mDrawableForCursor.setBounds(left, top - mTempRect.top, left + width,
Gilles Debunned88876a2012-03-16 17:34:04 -07002397 bottom + mTempRect.bottom);
2398 }
2399
2400 /**
Siyamed Sinir987ec652016-02-17 19:44:41 -08002401 * Return clamped position for the drawable. If the drawable is within the boundaries of the
2402 * 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 -08002403 * 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 -08002404 * the view boundary. If the drawable is null, horizontal parameter is aligned to left or right
2405 * of the view.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002406 *
Siyamed Sinir987ec652016-02-17 19:44:41 -08002407 * @param drawable Drawable. Can be null.
2408 * @param horizontal Horizontal position for the drawable.
2409 * @return The clamped horizontal position for the drawable.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002410 */
Siyamed Sinir987ec652016-02-17 19:44:41 -08002411 private int clampHorizontalPosition(@Nullable final Drawable drawable, float horizontal) {
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002412 horizontal = Math.max(0.5f, horizontal - 0.5f);
2413 if (mTempRect == null) mTempRect = new Rect();
Siyamed Sinir987ec652016-02-17 19:44:41 -08002414
2415 int drawableWidth = 0;
2416 if (drawable != null) {
2417 drawable.getPadding(mTempRect);
2418 drawableWidth = drawable.getIntrinsicWidth();
2419 } else {
2420 mTempRect.setEmpty();
2421 }
2422
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002423 int scrollX = mTextView.getScrollX();
2424 float horizontalDiff = horizontal - scrollX;
2425 int viewClippedWidth = mTextView.getWidth() - mTextView.getCompoundPaddingLeft()
2426 - mTextView.getCompoundPaddingRight();
2427
2428 final int left;
2429 if (horizontalDiff >= (viewClippedWidth - 1f)) {
2430 // at the rightmost position
Siyamed Sinir987ec652016-02-17 19:44:41 -08002431 left = viewClippedWidth + scrollX - (drawableWidth - mTempRect.right);
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002432 } else if (Math.abs(horizontalDiff) <= 1f
2433 || (TextUtils.isEmpty(mTextView.getText())
Siyamed Sinir987ec652016-02-17 19:44:41 -08002434 && (TextView.VERY_WIDE - scrollX) <= (viewClippedWidth + 1f)
2435 && horizontal <= 1f)) {
Siyamed Sinir217c0f72016-02-01 18:30:02 -08002436 // at the leftmost position
2437 left = scrollX - mTempRect.left;
2438 } else {
2439 left = (int) horizontal - mTempRect.left;
2440 }
2441 return left;
2442 }
2443
2444 /**
Gilles Debunned88876a2012-03-16 17:34:04 -07002445 * 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 -08002446 * a dictionary) from the current input method, provided by it calling
Gilles Debunned88876a2012-03-16 17:34:04 -07002447 * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
2448 * implementation flashes the background of the corrected word to provide feedback to the user.
2449 *
2450 * @param info The auto correct info about the text that was corrected.
2451 */
2452 public void onCommitCorrection(CorrectionInfo info) {
2453 if (mCorrectionHighlighter == null) {
2454 mCorrectionHighlighter = new CorrectionHighlighter();
2455 } else {
2456 mCorrectionHighlighter.invalidate(false);
2457 }
2458
2459 mCorrectionHighlighter.highlight(info);
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002460 mUndoInputFilter.freezeLastEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07002461 }
2462
Gilles Debunned88876a2012-03-16 17:34:04 -07002463 void onScrollChanged() {
Gilles Debunne157aafc2012-04-19 17:21:57 -07002464 if (mPositionListener != null) {
2465 mPositionListener.onScrollChanged();
2466 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01002467 if (mTextActionMode != null) {
2468 mTextActionMode.invalidateContentRect();
Abodunrinwa Toki56195db2015-04-22 06:46:54 +01002469 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002470 }
2471
2472 /**
2473 * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
2474 */
2475 private boolean shouldBlink() {
2476 if (!isCursorVisible() || !mTextView.isFocused()) return false;
2477
2478 final int start = mTextView.getSelectionStart();
2479 if (start < 0) return false;
2480
2481 final int end = mTextView.getSelectionEnd();
2482 if (end < 0) return false;
2483
2484 return start == end;
2485 }
2486
2487 void makeBlink() {
2488 if (shouldBlink()) {
2489 mShowCursor = SystemClock.uptimeMillis();
2490 if (mBlink == null) mBlink = new Blink();
John Reckd0374c62015-10-20 13:25:01 -07002491 mTextView.removeCallbacks(mBlink);
2492 mTextView.postDelayed(mBlink, BLINK);
Gilles Debunned88876a2012-03-16 17:34:04 -07002493 } else {
John Reckd0374c62015-10-20 13:25:01 -07002494 if (mBlink != null) mTextView.removeCallbacks(mBlink);
Gilles Debunned88876a2012-03-16 17:34:04 -07002495 }
2496 }
2497
John Reckd0374c62015-10-20 13:25:01 -07002498 private class Blink implements Runnable {
Gilles Debunned88876a2012-03-16 17:34:04 -07002499 private boolean mCancelled;
2500
2501 public void run() {
Gilles Debunned88876a2012-03-16 17:34:04 -07002502 if (mCancelled) {
2503 return;
2504 }
2505
John Reckd0374c62015-10-20 13:25:01 -07002506 mTextView.removeCallbacks(this);
Gilles Debunned88876a2012-03-16 17:34:04 -07002507
2508 if (shouldBlink()) {
2509 if (mTextView.getLayout() != null) {
2510 mTextView.invalidateCursorPath();
2511 }
2512
John Reckd0374c62015-10-20 13:25:01 -07002513 mTextView.postDelayed(this, BLINK);
Gilles Debunned88876a2012-03-16 17:34:04 -07002514 }
2515 }
2516
2517 void cancel() {
2518 if (!mCancelled) {
John Reckd0374c62015-10-20 13:25:01 -07002519 mTextView.removeCallbacks(this);
Gilles Debunned88876a2012-03-16 17:34:04 -07002520 mCancelled = true;
2521 }
2522 }
2523
2524 void uncancel() {
2525 mCancelled = false;
2526 }
2527 }
2528
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002529 private DragShadowBuilder getTextThumbnailBuilder(int start, int end) {
Gilles Debunned88876a2012-03-16 17:34:04 -07002530 TextView shadowView = (TextView) View.inflate(mTextView.getContext(),
2531 com.android.internal.R.layout.text_drag_thumbnail, null);
2532
2533 if (shadowView == null) {
2534 throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
2535 }
2536
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002537 if (end - start > DRAG_SHADOW_MAX_TEXT_LENGTH) {
2538 final long range = getCharClusterRange(start + DRAG_SHADOW_MAX_TEXT_LENGTH);
2539 end = TextUtils.unpackRangeEndFromLong(range);
Gilles Debunned88876a2012-03-16 17:34:04 -07002540 }
Keisuke Kuroyanagi5396d7e2016-02-19 15:28:26 -08002541 final CharSequence text = mTextView.getTransformedText(start, end);
Gilles Debunned88876a2012-03-16 17:34:04 -07002542 shadowView.setText(text);
2543 shadowView.setTextColor(mTextView.getTextColors());
2544
Alan Viverettebb98ebd2015-05-08 17:17:44 -07002545 shadowView.setTextAppearance(R.styleable.Theme_textAppearanceLarge);
Gilles Debunned88876a2012-03-16 17:34:04 -07002546 shadowView.setGravity(Gravity.CENTER);
2547
2548 shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
2549 ViewGroup.LayoutParams.WRAP_CONTENT));
2550
2551 final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
2552 shadowView.measure(size, size);
2553
2554 shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
2555 shadowView.invalidate();
2556 return new DragShadowBuilder(shadowView);
2557 }
2558
2559 private static class DragLocalState {
2560 public TextView sourceTextView;
2561 public int start, end;
2562
2563 public DragLocalState(TextView sourceTextView, int start, int end) {
2564 this.sourceTextView = sourceTextView;
2565 this.start = start;
2566 this.end = end;
2567 }
2568 }
2569
2570 void onDrop(DragEvent event) {
Ben Murdoch3dac4602017-01-17 11:27:37 +00002571 SpannableStringBuilder content = new SpannableStringBuilder();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002572
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -07002573 final DragAndDropPermissions permissions = DragAndDropPermissions.obtain(event);
2574 if (permissions != null) {
2575 permissions.takeTransient();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002576 }
2577
2578 try {
2579 ClipData clipData = event.getClipData();
2580 final int itemCount = clipData.getItemCount();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002581 for (int i = 0; i < itemCount; i++) {
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002582 Item item = clipData.getItemAt(i);
2583 content.append(item.coerceToStyledText(mTextView.getContext()));
2584 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002585 } finally {
Vladislav Kaznacheev377c3282016-04-20 14:22:23 -07002586 if (permissions != null) {
2587 permissions.release();
Vladislav Kaznacheevc14df8e2016-01-22 11:49:13 -08002588 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002589 }
2590
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002591 mTextView.beginBatchEdit();
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002592 mUndoInputFilter.freezeLastEdit();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002593 try {
2594 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
2595 Object localState = event.getLocalState();
2596 DragLocalState dragLocalState = null;
2597 if (localState instanceof DragLocalState) {
2598 dragLocalState = (DragLocalState) localState;
Gilles Debunned88876a2012-03-16 17:34:04 -07002599 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07002600 boolean dragDropIntoItself = dragLocalState != null
2601 && dragLocalState.sourceTextView == mTextView;
Gilles Debunned88876a2012-03-16 17:34:04 -07002602
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002603 if (dragDropIntoItself) {
2604 if (offset >= dragLocalState.start && offset < dragLocalState.end) {
2605 // A drop inside the original selection discards the drop.
2606 return;
2607 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002608 }
2609
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002610 final int originalLength = mTextView.getText().length();
2611 int min = offset;
2612 int max = offset;
2613
2614 Selection.setSelection((Spannable) mTextView.getText(), max);
2615 mTextView.replaceText_internal(min, max, content);
2616
2617 if (dragDropIntoItself) {
2618 int dragSourceStart = dragLocalState.start;
2619 int dragSourceEnd = dragLocalState.end;
2620 if (max <= dragSourceStart) {
2621 // Inserting text before selection has shifted positions
2622 final int shift = mTextView.getText().length() - originalLength;
2623 dragSourceStart += shift;
2624 dragSourceEnd += shift;
2625 }
2626
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08002627 // Delete original selection
2628 mTextView.deleteText_internal(dragSourceStart, dragSourceEnd);
Gilles Debunned88876a2012-03-16 17:34:04 -07002629
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08002630 // Make sure we do not leave two adjacent spaces.
2631 final int prevCharIdx = Math.max(0, dragSourceStart - 1);
2632 final int nextCharIdx = Math.min(mTextView.getText().length(), dragSourceStart + 1);
2633 if (nextCharIdx > prevCharIdx + 1) {
2634 CharSequence t = mTextView.getTransformedText(prevCharIdx, nextCharIdx);
2635 if (Character.isSpaceChar(t.charAt(0)) && Character.isSpaceChar(t.charAt(1))) {
2636 mTextView.deleteText_internal(prevCharIdx, prevCharIdx + 1);
2637 }
Victoria Lease91373202012-09-07 16:41:59 -07002638 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002639 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09002640 } finally {
2641 mTextView.endBatchEdit();
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09002642 mUndoInputFilter.freezeLastEdit();
Gilles Debunned88876a2012-03-16 17:34:04 -07002643 }
2644 }
2645
Gilles Debunnec62589c2012-04-12 14:50:23 -07002646 public void addSpanWatchers(Spannable text) {
2647 final int textLength = text.length();
2648
2649 if (mKeyListener != null) {
2650 text.setSpan(mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
2651 }
2652
Jean Chalardbaf30942013-02-28 16:01:51 -08002653 if (mSpanController == null) {
2654 mSpanController = new SpanController();
Gilles Debunnec62589c2012-04-12 14:50:23 -07002655 }
Jean Chalardbaf30942013-02-28 16:01:51 -08002656 text.setSpan(mSpanController, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002657 }
2658
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002659 void setContextMenuAnchor(float x, float y) {
2660 mContextMenuAnchorX = x;
2661 mContextMenuAnchorY = y;
2662 }
2663
2664 void onCreateContextMenu(ContextMenu menu) {
2665 if (mIsBeingLongClicked || Float.isNaN(mContextMenuAnchorX)
2666 || Float.isNaN(mContextMenuAnchorY)) {
2667 return;
2668 }
2669 final int offset = mTextView.getOffsetForPosition(mContextMenuAnchorX, mContextMenuAnchorY);
2670 if (offset == -1) {
2671 return;
2672 }
Siyamed Sinir532f3c92017-06-15 18:22:31 -07002673
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08002674 stopTextActionModeWithPreservingSelection();
Siyamed Sinir532f3c92017-06-15 18:22:31 -07002675 if (mTextView.canSelectText()) {
2676 final boolean isOnSelection = mTextView.hasSelection()
2677 && offset >= mTextView.getSelectionStart()
2678 && offset <= mTextView.getSelectionEnd();
2679 if (!isOnSelection) {
2680 // Right clicked position is not on the selection. Remove the selection and move the
2681 // cursor to the right clicked position.
2682 Selection.setSelection((Spannable) mTextView.getText(), offset);
2683 stopTextActionMode();
2684 }
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002685 }
2686
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002687 if (shouldOfferToShowSuggestions()) {
Keisuke Kuroyanagi182f5fe2016-03-11 16:31:29 +09002688 final SuggestionInfo[] suggestionInfoArray =
2689 new SuggestionInfo[SuggestionSpan.SUGGESTIONS_MAX_SIZE];
2690 for (int i = 0; i < suggestionInfoArray.length; i++) {
2691 suggestionInfoArray[i] = new SuggestionInfo();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002692 }
2693 final SubMenu subMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, MENU_ITEM_ORDER_REPLACE,
2694 com.android.internal.R.string.replace);
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002695 final int numItems = mSuggestionHelper.getSuggestionInfo(suggestionInfoArray, null);
Keisuke Kuroyanagi182f5fe2016-03-11 16:31:29 +09002696 for (int i = 0; i < numItems; i++) {
2697 final SuggestionInfo info = suggestionInfoArray[i];
2698 subMenu.add(Menu.NONE, Menu.NONE, i, info.mText)
2699 .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
2700 @Override
2701 public boolean onMenuItemClick(MenuItem item) {
2702 replaceWithSuggestion(info);
2703 return true;
2704 }
2705 });
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002706 }
2707 }
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002708
2709 menu.add(Menu.NONE, TextView.ID_UNDO, MENU_ITEM_ORDER_UNDO,
2710 com.android.internal.R.string.undo)
2711 .setAlphabeticShortcut('z')
2712 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2713 .setEnabled(mTextView.canUndo());
2714 menu.add(Menu.NONE, TextView.ID_REDO, MENU_ITEM_ORDER_REDO,
2715 com.android.internal.R.string.redo)
2716 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2717 .setEnabled(mTextView.canRedo());
2718
2719 menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
2720 com.android.internal.R.string.cut)
2721 .setAlphabeticShortcut('x')
2722 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2723 .setEnabled(mTextView.canCut());
2724 menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
2725 com.android.internal.R.string.copy)
2726 .setAlphabeticShortcut('c')
2727 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2728 .setEnabled(mTextView.canCopy());
2729 menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
2730 com.android.internal.R.string.paste)
2731 .setAlphabeticShortcut('v')
2732 .setEnabled(mTextView.canPaste())
2733 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01002734 menu.add(Menu.NONE, TextView.ID_PASTE_AS_PLAIN_TEXT, MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002735 com.android.internal.R.string.paste_as_plain_text)
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01002736 .setEnabled(mTextView.canPasteAsPlainText())
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002737 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2738 menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
2739 com.android.internal.R.string.share)
2740 .setEnabled(mTextView.canShare())
2741 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2742 menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
2743 com.android.internal.R.string.selectAll)
2744 .setAlphabeticShortcut('a')
2745 .setEnabled(mTextView.canSelectAllText())
2746 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Felipe Leme2ac463e2017-03-13 14:06:25 -07002747 menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
Felipe Leme555bcac2017-06-26 12:53:56 -07002748 android.R.string.autofill)
Felipe Leme2ac463e2017-03-13 14:06:25 -07002749 .setEnabled(mTextView.canRequestAutofill())
2750 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002751
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08002752 mPreserveSelection = true;
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002753 }
2754
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002755 @Nullable
2756 private SuggestionSpan findEquivalentSuggestionSpan(
2757 @NonNull SuggestionSpanInfo suggestionSpanInfo) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002758 final Editable editable = (Editable) mTextView.getText();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002759 if (editable.getSpanStart(suggestionSpanInfo.mSuggestionSpan) >= 0) {
2760 // Exactly same span is found.
2761 return suggestionSpanInfo.mSuggestionSpan;
2762 }
2763 // Suggestion span couldn't be found. Try to find a suggestion span that has the same
2764 // contents.
2765 final SuggestionSpan[] suggestionSpans = editable.getSpans(suggestionSpanInfo.mSpanStart,
2766 suggestionSpanInfo.mSpanEnd, SuggestionSpan.class);
2767 for (final SuggestionSpan suggestionSpan : suggestionSpans) {
2768 final int start = editable.getSpanStart(suggestionSpan);
2769 if (start != suggestionSpanInfo.mSpanStart) {
2770 continue;
2771 }
2772 final int end = editable.getSpanEnd(suggestionSpan);
2773 if (end != suggestionSpanInfo.mSpanEnd) {
2774 continue;
2775 }
2776 if (suggestionSpan.equals(suggestionSpanInfo.mSuggestionSpan)) {
2777 return suggestionSpan;
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08002778 }
2779 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002780 return null;
2781 }
2782
2783 private void replaceWithSuggestion(@NonNull final SuggestionInfo suggestionInfo) {
2784 final SuggestionSpan targetSuggestionSpan = findEquivalentSuggestionSpan(
2785 suggestionInfo.mSuggestionSpanInfo);
2786 if (targetSuggestionSpan == null) {
2787 // Span has been removed
2788 return;
2789 }
2790 final Editable editable = (Editable) mTextView.getText();
2791 final int spanStart = editable.getSpanStart(targetSuggestionSpan);
2792 final int spanEnd = editable.getSpanEnd(targetSuggestionSpan);
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08002793 if (spanStart < 0 || spanEnd <= spanStart) {
2794 // Span has been removed
2795 return;
2796 }
2797
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002798 final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
2799 // SuggestionSpans are removed by replace: save them before
2800 SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
2801 SuggestionSpan.class);
2802 final int length = suggestionSpans.length;
2803 int[] suggestionSpansStarts = new int[length];
2804 int[] suggestionSpansEnds = new int[length];
2805 int[] suggestionSpansFlags = new int[length];
2806 for (int i = 0; i < length; i++) {
2807 final SuggestionSpan suggestionSpan = suggestionSpans[i];
2808 suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
2809 suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
2810 suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
2811
2812 // Remove potential misspelled flags
2813 int suggestionSpanFlags = suggestionSpan.getFlags();
2814 if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
2815 suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
2816 suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
2817 suggestionSpan.setFlags(suggestionSpanFlags);
2818 }
2819 }
2820
2821 // Notify source IME of the suggestion pick. Do this before swapping texts.
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002822 targetSuggestionSpan.notifySelection(
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002823 mTextView.getContext(), originalText, suggestionInfo.mSuggestionIndex);
2824
2825 // Swap text content between actual text and Suggestion span
2826 final int suggestionStart = suggestionInfo.mSuggestionStart;
2827 final int suggestionEnd = suggestionInfo.mSuggestionEnd;
2828 final String suggestion = suggestionInfo.mText.subSequence(
2829 suggestionStart, suggestionEnd).toString();
2830 mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
2831
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09002832 String[] suggestions = targetSuggestionSpan.getSuggestions();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09002833 suggestions[suggestionInfo.mSuggestionIndex] = originalText;
2834
2835 // Restore previous SuggestionSpans
2836 final int lengthDelta = suggestion.length() - (spanEnd - spanStart);
2837 for (int i = 0; i < length; i++) {
2838 // Only spans that include the modified region make sense after replacement
2839 // Spans partially included in the replaced region are removed, there is no
2840 // way to assign them a valid range after replacement
2841 if (suggestionSpansStarts[i] <= spanStart && suggestionSpansEnds[i] >= spanEnd) {
2842 mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
2843 suggestionSpansEnds[i] + lengthDelta, suggestionSpansFlags[i]);
2844 }
2845 }
2846 // Move cursor at the end of the replaced word
2847 final int newCursorPosition = spanEnd + lengthDelta;
2848 mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
2849 }
2850
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09002851 private final MenuItem.OnMenuItemClickListener mOnContextMenuItemClickListener =
2852 new MenuItem.OnMenuItemClickListener() {
2853 @Override
2854 public boolean onMenuItemClick(MenuItem item) {
2855 if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
2856 return true;
2857 }
2858 return mTextView.onTextContextMenuItem(item.getItemId());
2859 }
2860 };
2861
Gilles Debunned88876a2012-03-16 17:34:04 -07002862 /**
2863 * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
2864 * pop-up should be displayed.
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07002865 * Also monitors {@link Selection} to call back to the attached input method.
Gilles Debunned88876a2012-03-16 17:34:04 -07002866 */
Keisuke Kuroyanagic5a43202016-06-10 16:27:27 +09002867 private class SpanController implements SpanWatcher {
Gilles Debunned88876a2012-03-16 17:34:04 -07002868
2869 private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
2870
2871 private EasyEditPopupWindow mPopupWindow;
2872
Gilles Debunned88876a2012-03-16 17:34:04 -07002873 private Runnable mHidePopup;
2874
Jean Chalardbaf30942013-02-28 16:01:51 -08002875 // This function is pure but inner classes can't have static functions
2876 private boolean isNonIntermediateSelectionSpan(final Spannable text,
2877 final Object span) {
2878 return (Selection.SELECTION_START == span || Selection.SELECTION_END == span)
2879 && (text.getSpanFlags(span) & Spanned.SPAN_INTERMEDIATE) == 0;
2880 }
2881
Gilles Debunnec62589c2012-04-12 14:50:23 -07002882 @Override
2883 public void onSpanAdded(Spannable text, Object span, int start, int end) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002884 if (isNonIntermediateSelectionSpan(text, span)) {
2885 sendUpdateSelection();
2886 } else if (span instanceof EasyEditSpan) {
Gilles Debunnec62589c2012-04-12 14:50:23 -07002887 if (mPopupWindow == null) {
2888 mPopupWindow = new EasyEditPopupWindow();
2889 mHidePopup = new Runnable() {
2890 @Override
2891 public void run() {
2892 hide();
2893 }
2894 };
2895 }
2896
2897 // Make sure there is only at most one EasyEditSpan in the text
2898 if (mPopupWindow.mEasyEditSpan != null) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002899 mPopupWindow.mEasyEditSpan.setDeleteEnabled(false);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002900 }
2901
2902 mPopupWindow.setEasyEditSpan((EasyEditSpan) span);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002903 mPopupWindow.setOnDeleteListener(new EasyEditDeleteListener() {
2904 @Override
2905 public void onDeleteClick(EasyEditSpan span) {
2906 Editable editable = (Editable) mTextView.getText();
2907 int start = editable.getSpanStart(span);
2908 int end = editable.getSpanEnd(span);
2909 if (start >= 0 && end >= 0) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002910 sendEasySpanNotification(EasyEditSpan.TEXT_DELETED, span);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002911 mTextView.deleteText_internal(start, end);
2912 }
2913 editable.removeSpan(span);
2914 }
2915 });
Gilles Debunnec62589c2012-04-12 14:50:23 -07002916
2917 if (mTextView.getWindowVisibility() != View.VISIBLE) {
2918 // The window is not visible yet, ignore the text change.
2919 return;
2920 }
2921
2922 if (mTextView.getLayout() == null) {
2923 // The view has not been laid out yet, ignore the text change
2924 return;
2925 }
2926
2927 if (extractedTextModeWillBeStarted()) {
2928 // The input is in extract mode. Do not handle the easy edit in
2929 // the original TextView, as the ExtractEditText will do
2930 return;
2931 }
2932
2933 mPopupWindow.show();
2934 mTextView.removeCallbacks(mHidePopup);
2935 mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
2936 }
2937 }
2938
2939 @Override
2940 public void onSpanRemoved(Spannable text, Object span, int start, int end) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002941 if (isNonIntermediateSelectionSpan(text, span)) {
2942 sendUpdateSelection();
2943 } else if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
Gilles Debunnec62589c2012-04-12 14:50:23 -07002944 hide();
2945 }
2946 }
2947
2948 @Override
2949 public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd,
2950 int newStart, int newEnd) {
Jean Chalardbaf30942013-02-28 16:01:51 -08002951 if (isNonIntermediateSelectionSpan(text, span)) {
2952 sendUpdateSelection();
2953 } else if (mPopupWindow != null && span instanceof EasyEditSpan) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002954 EasyEditSpan easyEditSpan = (EasyEditSpan) span;
Jean Chalardbaf30942013-02-28 16:01:51 -08002955 sendEasySpanNotification(EasyEditSpan.TEXT_MODIFIED, easyEditSpan);
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002956 text.removeSpan(easyEditSpan);
Gilles Debunnec62589c2012-04-12 14:50:23 -07002957 }
2958 }
2959
Gilles Debunned88876a2012-03-16 17:34:04 -07002960 public void hide() {
2961 if (mPopupWindow != null) {
2962 mPopupWindow.hide();
2963 mTextView.removeCallbacks(mHidePopup);
2964 }
Gilles Debunned88876a2012-03-16 17:34:04 -07002965 }
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002966
Jean Chalardbaf30942013-02-28 16:01:51 -08002967 private void sendEasySpanNotification(int textChangedType, EasyEditSpan span) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00002968 try {
2969 PendingIntent pendingIntent = span.getPendingIntent();
2970 if (pendingIntent != null) {
2971 Intent intent = new Intent();
2972 intent.putExtra(EasyEditSpan.EXTRA_TEXT_CHANGED_TYPE, textChangedType);
2973 pendingIntent.send(mTextView.getContext(), 0, intent);
2974 }
2975 } catch (CanceledException e) {
2976 // This should not happen, as we should try to send the intent only once.
2977 Log.w(TAG, "PendingIntent for notification cannot be sent", e);
2978 }
2979 }
2980 }
2981
2982 /**
2983 * Listens for the delete event triggered by {@link EasyEditPopupWindow}.
2984 */
2985 private interface EasyEditDeleteListener {
2986
2987 /**
2988 * Clicks the delete pop-up.
2989 */
2990 void onDeleteClick(EasyEditSpan span);
Gilles Debunned88876a2012-03-16 17:34:04 -07002991 }
2992
2993 /**
2994 * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07002995 * by {@link SpanController}.
Gilles Debunned88876a2012-03-16 17:34:04 -07002996 */
2997 private class EasyEditPopupWindow extends PinnedPopupWindow
2998 implements OnClickListener {
2999 private static final int POPUP_TEXT_LAYOUT =
3000 com.android.internal.R.layout.text_edit_action_popup_text;
3001 private TextView mDeleteTextView;
3002 private EasyEditSpan mEasyEditSpan;
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003003 private EasyEditDeleteListener mOnDeleteListener;
Gilles Debunned88876a2012-03-16 17:34:04 -07003004
3005 @Override
3006 protected void createPopupWindow() {
3007 mPopupWindow = new PopupWindow(mTextView.getContext(), null,
3008 com.android.internal.R.attr.textSelectHandleWindowStyle);
3009 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
3010 mPopupWindow.setClippingEnabled(true);
3011 }
3012
3013 @Override
3014 protected void initContentView() {
3015 LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
3016 linearLayout.setOrientation(LinearLayout.HORIZONTAL);
3017 mContentView = linearLayout;
3018 mContentView.setBackgroundResource(
3019 com.android.internal.R.drawable.text_edit_side_paste_window);
3020
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003021 LayoutInflater inflater = (LayoutInflater) mTextView.getContext()
3022 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003023
3024 LayoutParams wrapContent = new LayoutParams(
3025 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
3026
3027 mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
3028 mDeleteTextView.setLayoutParams(wrapContent);
3029 mDeleteTextView.setText(com.android.internal.R.string.delete);
3030 mDeleteTextView.setOnClickListener(this);
3031 mContentView.addView(mDeleteTextView);
3032 }
3033
Gilles Debunnec62589c2012-04-12 14:50:23 -07003034 public void setEasyEditSpan(EasyEditSpan easyEditSpan) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003035 mEasyEditSpan = easyEditSpan;
Gilles Debunned88876a2012-03-16 17:34:04 -07003036 }
3037
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003038 private void setOnDeleteListener(EasyEditDeleteListener listener) {
3039 mOnDeleteListener = listener;
3040 }
3041
Gilles Debunned88876a2012-03-16 17:34:04 -07003042 @Override
3043 public void onClick(View view) {
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003044 if (view == mDeleteTextView
3045 && mEasyEditSpan != null && mEasyEditSpan.isDeleteEnabled()
3046 && mOnDeleteListener != null) {
3047 mOnDeleteListener.onDeleteClick(mEasyEditSpan);
Gilles Debunned88876a2012-03-16 17:34:04 -07003048 }
3049 }
3050
3051 @Override
Luca Zanolin1b15ba52013-02-20 14:31:37 +00003052 public void hide() {
3053 if (mEasyEditSpan != null) {
3054 mEasyEditSpan.setDeleteEnabled(false);
3055 }
3056 mOnDeleteListener = null;
3057 super.hide();
3058 }
3059
3060 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07003061 protected int getTextOffset() {
3062 // Place the pop-up at the end of the span
3063 Editable editable = (Editable) mTextView.getText();
3064 return editable.getSpanEnd(mEasyEditSpan);
3065 }
3066
3067 @Override
3068 protected int getVerticalLocalPosition(int line) {
Siyamed Sinira60b59d2017-07-26 09:26:41 -07003069 final Layout layout = mTextView.getLayout();
3070 return layout.getLineBottomWithoutSpacing(line);
Gilles Debunned88876a2012-03-16 17:34:04 -07003071 }
3072
3073 @Override
3074 protected int clipVertically(int positionY) {
3075 // As we display the pop-up below the span, no vertical clipping is required.
3076 return positionY;
3077 }
3078 }
3079
3080 private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
3081 // 3 handles
3082 // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09003083 // 1 CursorAnchorInfoNotifier
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003084 private static final int MAXIMUM_NUMBER_OF_LISTENERS = 7;
Gilles Debunned88876a2012-03-16 17:34:04 -07003085 private TextViewPositionListener[] mPositionListeners =
3086 new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003087 private boolean[] mCanMove = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
Gilles Debunned88876a2012-03-16 17:34:04 -07003088 private boolean mPositionHasChanged = true;
3089 // Absolute position of the TextView with respect to its parent window
3090 private int mPositionX, mPositionY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003091 private int mPositionXOnScreen, mPositionYOnScreen;
Gilles Debunned88876a2012-03-16 17:34:04 -07003092 private int mNumberOfListeners;
3093 private boolean mScrollHasChanged;
3094 final int[] mTempCoords = new int[2];
3095
3096 public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
3097 if (mNumberOfListeners == 0) {
3098 updatePosition();
3099 ViewTreeObserver vto = mTextView.getViewTreeObserver();
3100 vto.addOnPreDrawListener(this);
3101 }
3102
3103 int emptySlotIndex = -1;
3104 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3105 TextViewPositionListener listener = mPositionListeners[i];
3106 if (listener == positionListener) {
3107 return;
3108 } else if (emptySlotIndex < 0 && listener == null) {
3109 emptySlotIndex = i;
3110 }
3111 }
3112
3113 mPositionListeners[emptySlotIndex] = positionListener;
3114 mCanMove[emptySlotIndex] = canMove;
3115 mNumberOfListeners++;
3116 }
3117
3118 public void removeSubscriber(TextViewPositionListener positionListener) {
3119 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3120 if (mPositionListeners[i] == positionListener) {
3121 mPositionListeners[i] = null;
3122 mNumberOfListeners--;
3123 break;
3124 }
3125 }
3126
3127 if (mNumberOfListeners == 0) {
3128 ViewTreeObserver vto = mTextView.getViewTreeObserver();
3129 vto.removeOnPreDrawListener(this);
3130 }
3131 }
3132
3133 public int getPositionX() {
3134 return mPositionX;
3135 }
3136
3137 public int getPositionY() {
3138 return mPositionY;
3139 }
3140
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003141 public int getPositionXOnScreen() {
3142 return mPositionXOnScreen;
3143 }
3144
3145 public int getPositionYOnScreen() {
3146 return mPositionYOnScreen;
3147 }
3148
Gilles Debunned88876a2012-03-16 17:34:04 -07003149 @Override
3150 public boolean onPreDraw() {
3151 updatePosition();
3152
3153 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3154 if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
3155 TextViewPositionListener positionListener = mPositionListeners[i];
3156 if (positionListener != null) {
3157 positionListener.updatePosition(mPositionX, mPositionY,
3158 mPositionHasChanged, mScrollHasChanged);
3159 }
3160 }
3161 }
3162
3163 mScrollHasChanged = false;
3164 return true;
3165 }
3166
3167 private void updatePosition() {
3168 mTextView.getLocationInWindow(mTempCoords);
3169
3170 mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
3171
3172 mPositionX = mTempCoords[0];
3173 mPositionY = mTempCoords[1];
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09003174
3175 mTextView.getLocationOnScreen(mTempCoords);
3176
3177 mPositionXOnScreen = mTempCoords[0];
3178 mPositionYOnScreen = mTempCoords[1];
Gilles Debunned88876a2012-03-16 17:34:04 -07003179 }
3180
3181 public void onScrollChanged() {
3182 mScrollHasChanged = true;
3183 }
3184 }
3185
3186 private abstract class PinnedPopupWindow implements TextViewPositionListener {
3187 protected PopupWindow mPopupWindow;
3188 protected ViewGroup mContentView;
3189 int mPositionX, mPositionY;
Seigo Nonaka60490d12016-01-28 17:25:18 +09003190 int mClippingLimitLeft, mClippingLimitRight;
Gilles Debunned88876a2012-03-16 17:34:04 -07003191
3192 protected abstract void createPopupWindow();
3193 protected abstract void initContentView();
3194 protected abstract int getTextOffset();
3195 protected abstract int getVerticalLocalPosition(int line);
3196 protected abstract int clipVertically(int positionY);
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003197 protected void setUp() {
3198 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003199
3200 public PinnedPopupWindow() {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003201 // Due to calling subclass methods in base constructor, subclass constructor is not
3202 // called before subclass methods, e.g. createPopupWindow or initContentView. To give
3203 // a chance to initialize subclasses, call setUp() method here.
3204 // TODO: It is good to extract non trivial initialization code from constructor.
3205 setUp();
3206
Gilles Debunned88876a2012-03-16 17:34:04 -07003207 createPopupWindow();
3208
Alan Viverette80ebe0d2015-04-30 15:53:11 -07003209 mPopupWindow.setWindowLayoutType(
3210 WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
Gilles Debunned88876a2012-03-16 17:34:04 -07003211 mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
3212 mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
3213
3214 initContentView();
3215
3216 LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
3217 ViewGroup.LayoutParams.WRAP_CONTENT);
3218 mContentView.setLayoutParams(wrapContent);
3219
3220 mPopupWindow.setContentView(mContentView);
3221 }
3222
3223 public void show() {
3224 getPositionListener().addSubscriber(this, false /* offset is fixed */);
3225
3226 computeLocalPosition();
3227
3228 final PositionListener positionListener = getPositionListener();
3229 updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
3230 }
3231
3232 protected void measureContent() {
3233 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3234 mContentView.measure(
3235 View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
3236 View.MeasureSpec.AT_MOST),
3237 View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
3238 View.MeasureSpec.AT_MOST));
3239 }
3240
3241 /* The popup window will be horizontally centered on the getTextOffset() and vertically
3242 * positioned according to viewportToContentHorizontalOffset.
3243 *
3244 * This method assumes that mContentView has properly been measured from its content. */
3245 private void computeLocalPosition() {
3246 measureContent();
3247 final int width = mContentView.getMeasuredWidth();
3248 final int offset = getTextOffset();
3249 mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f);
3250 mPositionX += mTextView.viewportToContentHorizontalOffset();
3251
3252 final int line = mTextView.getLayout().getLineForOffset(offset);
3253 mPositionY = getVerticalLocalPosition(line);
3254 mPositionY += mTextView.viewportToContentVerticalOffset();
3255 }
3256
3257 private void updatePosition(int parentPositionX, int parentPositionY) {
3258 int positionX = parentPositionX + mPositionX;
3259 int positionY = parentPositionY + mPositionY;
3260
3261 positionY = clipVertically(positionY);
3262
3263 // Horizontal clipping
3264 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3265 final int width = mContentView.getMeasuredWidth();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003266 positionX = Math.min(
3267 displayMetrics.widthPixels - width + mClippingLimitRight, positionX);
3268 positionX = Math.max(-mClippingLimitLeft, positionX);
Gilles Debunned88876a2012-03-16 17:34:04 -07003269
3270 if (isShowing()) {
3271 mPopupWindow.update(positionX, positionY, -1, -1);
3272 } else {
3273 mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
3274 positionX, positionY);
3275 }
3276 }
3277
3278 public void hide() {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09003279 if (!isShowing()) {
3280 return;
3281 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003282 mPopupWindow.dismiss();
3283 getPositionListener().removeSubscriber(this);
3284 }
3285
3286 @Override
3287 public void updatePosition(int parentPositionX, int parentPositionY,
3288 boolean parentPositionChanged, boolean parentScrolled) {
3289 // Either parentPositionChanged or parentScrolled is true, check if still visible
3290 if (isShowing() && isOffsetVisible(getTextOffset())) {
3291 if (parentScrolled) computeLocalPosition();
3292 updatePosition(parentPositionX, parentPositionY);
3293 } else {
3294 hide();
3295 }
3296 }
3297
3298 public boolean isShowing() {
3299 return mPopupWindow.isShowing();
3300 }
3301 }
3302
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003303 private static final class SuggestionInfo {
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003304 // Range of actual suggestion within mText
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003305 int mSuggestionStart, mSuggestionEnd;
3306
3307 // The SuggestionSpan that this TextView represents
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003308 final SuggestionSpanInfo mSuggestionSpanInfo = new SuggestionSpanInfo();
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003309
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003310 // The index of this suggestion inside suggestionSpan
3311 int mSuggestionIndex;
3312
3313 final SpannableStringBuilder mText = new SpannableStringBuilder();
3314
3315 void clear() {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003316 mSuggestionSpanInfo.clear();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003317 mText.clear();
3318 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003319
3320 // Utility method to set attributes about a SuggestionSpan.
3321 void setSpanInfo(SuggestionSpan span, int spanStart, int spanEnd) {
3322 mSuggestionSpanInfo.mSuggestionSpan = span;
3323 mSuggestionSpanInfo.mSpanStart = spanStart;
3324 mSuggestionSpanInfo.mSpanEnd = spanEnd;
3325 }
3326 }
3327
3328 private static final class SuggestionSpanInfo {
3329 // The SuggestionSpan;
3330 @Nullable
3331 SuggestionSpan mSuggestionSpan;
3332
3333 // The SuggestionSpan start position
3334 int mSpanStart;
3335
3336 // The SuggestionSpan end position
3337 int mSpanEnd;
3338
3339 void clear() {
3340 mSuggestionSpan = null;
3341 }
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003342 }
3343
3344 private class SuggestionHelper {
3345 private final Comparator<SuggestionSpan> mSuggestionSpanComparator =
3346 new SuggestionSpanComparator();
3347 private final HashMap<SuggestionSpan, Integer> mSpansLengths =
3348 new HashMap<SuggestionSpan, Integer>();
3349
3350 private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
3351 public int compare(SuggestionSpan span1, SuggestionSpan span2) {
3352 final int flag1 = span1.getFlags();
3353 final int flag2 = span2.getFlags();
3354 if (flag1 != flag2) {
3355 // The order here should match what is used in updateDrawState
3356 final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
3357 final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
3358 final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
3359 final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
3360 if (easy1 && !misspelled1) return -1;
3361 if (easy2 && !misspelled2) return 1;
3362 if (misspelled1) return -1;
3363 if (misspelled2) return 1;
3364 }
3365
3366 return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
3367 }
3368 }
3369
3370 /**
3371 * Returns the suggestion spans that cover the current cursor position. The suggestion
3372 * spans are sorted according to the length of text that they are attached to.
3373 */
3374 private SuggestionSpan[] getSortedSuggestionSpans() {
3375 int pos = mTextView.getSelectionStart();
3376 Spannable spannable = (Spannable) mTextView.getText();
3377 SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
3378
3379 mSpansLengths.clear();
3380 for (SuggestionSpan suggestionSpan : suggestionSpans) {
3381 int start = spannable.getSpanStart(suggestionSpan);
3382 int end = spannable.getSpanEnd(suggestionSpan);
3383 mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
3384 }
3385
3386 // The suggestions are sorted according to their types (easy correction first, then
3387 // misspelled) and to the length of the text that they cover (shorter first).
3388 Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
3389 mSpansLengths.clear();
3390
3391 return suggestionSpans;
3392 }
3393
3394 /**
3395 * Gets the SuggestionInfo list that contains suggestion information at the current cursor
3396 * position.
3397 *
3398 * @param suggestionInfos SuggestionInfo array the results will be set.
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003399 * @param misspelledSpanInfo a struct the misspelled SuggestionSpan info will be set.
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003400 * @return the number of suggestions actually fetched.
3401 */
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003402 public int getSuggestionInfo(SuggestionInfo[] suggestionInfos,
3403 @Nullable SuggestionSpanInfo misspelledSpanInfo) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003404 final Spannable spannable = (Spannable) mTextView.getText();
3405 final SuggestionSpan[] suggestionSpans = getSortedSuggestionSpans();
3406 final int nbSpans = suggestionSpans.length;
3407 if (nbSpans == 0) return 0;
3408
3409 int numberOfSuggestions = 0;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003410 for (final SuggestionSpan suggestionSpan : suggestionSpans) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003411 final int spanStart = spannable.getSpanStart(suggestionSpan);
3412 final int spanEnd = spannable.getSpanEnd(suggestionSpan);
3413
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003414 if (misspelledSpanInfo != null
3415 && (suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
3416 misspelledSpanInfo.mSuggestionSpan = suggestionSpan;
3417 misspelledSpanInfo.mSpanStart = spanStart;
3418 misspelledSpanInfo.mSpanEnd = spanEnd;
3419 }
3420
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003421 final String[] suggestions = suggestionSpan.getSuggestions();
3422 final int nbSuggestions = suggestions.length;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003423 suggestionLoop:
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003424 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
3425 final String suggestion = suggestions[suggestionIndex];
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003426 for (int i = 0; i < numberOfSuggestions; i++) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003427 final SuggestionInfo otherSuggestionInfo = suggestionInfos[i];
3428 if (otherSuggestionInfo.mText.toString().equals(suggestion)) {
3429 final int otherSpanStart =
3430 otherSuggestionInfo.mSuggestionSpanInfo.mSpanStart;
3431 final int otherSpanEnd =
3432 otherSuggestionInfo.mSuggestionSpanInfo.mSpanEnd;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003433 if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003434 continue suggestionLoop;
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003435 }
3436 }
3437 }
3438
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003439 SuggestionInfo suggestionInfo = suggestionInfos[numberOfSuggestions];
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003440 suggestionInfo.setSpanInfo(suggestionSpan, spanStart, spanEnd);
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003441 suggestionInfo.mSuggestionIndex = suggestionIndex;
3442 suggestionInfo.mSuggestionStart = 0;
3443 suggestionInfo.mSuggestionEnd = suggestion.length();
3444 suggestionInfo.mText.replace(0, suggestionInfo.mText.length(), suggestion);
3445 numberOfSuggestions++;
3446 if (numberOfSuggestions >= suggestionInfos.length) {
3447 return numberOfSuggestions;
3448 }
3449 }
3450 }
3451 return numberOfSuggestions;
3452 }
3453 }
3454
Seigo Nonakaa60160b2015-08-19 12:38:35 -07003455 @VisibleForTesting
3456 public class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
Gilles Debunned88876a2012-03-16 17:34:04 -07003457 private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003458
3459 // Key of intent extras for inserting new word into user dictionary.
3460 private static final String USER_DICTIONARY_EXTRA_WORD = "word";
3461 private static final String USER_DICTIONARY_EXTRA_LOCALE = "locale";
3462
Gilles Debunned88876a2012-03-16 17:34:04 -07003463 private SuggestionInfo[] mSuggestionInfos;
3464 private int mNumberOfSuggestions;
3465 private boolean mCursorWasVisibleBeforeSuggestions;
3466 private boolean mIsShowingUp = false;
3467 private SuggestionAdapter mSuggestionsAdapter;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003468 private TextAppearanceSpan mHighlightSpan; // TODO: Make mHighlightSpan final.
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003469 private TextView mAddToDictionaryButton;
3470 private TextView mDeleteButton;
Seigo Nonakaf47976e2016-03-01 09:17:37 -08003471 private ListView mSuggestionListView;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003472 private final SuggestionSpanInfo mMisspelledSpanInfo = new SuggestionSpanInfo();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003473 private int mContainerMarginWidth;
3474 private int mContainerMarginTop;
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003475 private LinearLayout mContainerView;
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003476 private Context mContext; // TODO: Make mContext final.
Gilles Debunned88876a2012-03-16 17:34:04 -07003477
3478 private class CustomPopupWindow extends PopupWindow {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003479
Gilles Debunned88876a2012-03-16 17:34:04 -07003480 @Override
3481 public void dismiss() {
Keisuke Kuroyanagid0560812015-12-17 17:50:42 +09003482 if (!isShowing()) {
3483 return;
3484 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003485 super.dismiss();
Gilles Debunned88876a2012-03-16 17:34:04 -07003486 getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
3487
3488 // Safe cast since show() checks that mTextView.getText() is an Editable
3489 ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan);
3490
3491 mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions);
Keisuke Kuroyanagi4a696ac2016-02-23 11:02:07 -08003492 if (hasInsertionController() && !extractedTextModeWillBeStarted()) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003493 getInsertionController().show();
3494 }
3495 }
3496 }
3497
3498 public SuggestionsPopupWindow() {
3499 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
Gilles Debunned88876a2012-03-16 17:34:04 -07003500 }
3501
3502 @Override
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003503 protected void setUp() {
3504 mContext = applyDefaultTheme(mTextView.getContext());
3505 mHighlightSpan = new TextAppearanceSpan(mContext,
3506 mTextView.mTextEditSuggestionHighlightStyle);
3507 }
3508
3509 private Context applyDefaultTheme(Context originalContext) {
3510 TypedArray a = originalContext.obtainStyledAttributes(
3511 new int[]{com.android.internal.R.attr.isLightTheme});
3512 boolean isLightTheme = a.getBoolean(0, true);
3513 int themeId = isLightTheme ? R.style.ThemeOverlay_Material_Light
3514 : R.style.ThemeOverlay_Material_Dark;
3515 a.recycle();
3516 return new ContextThemeWrapper(originalContext, themeId);
3517 }
3518
3519 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07003520 protected void createPopupWindow() {
Seigo Nonaka3ed1b392016-01-19 13:54:59 +09003521 mPopupWindow = new CustomPopupWindow();
Gilles Debunned88876a2012-03-16 17:34:04 -07003522 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
Seigo Nonaka3ed1b392016-01-19 13:54:59 +09003523 mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
Gilles Debunned88876a2012-03-16 17:34:04 -07003524 mPopupWindow.setFocusable(true);
3525 mPopupWindow.setClippingEnabled(false);
3526 }
3527
3528 @Override
3529 protected void initContentView() {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003530 final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
3531 Context.LAYOUT_INFLATER_SERVICE);
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003532 mContentView = (ViewGroup) inflater.inflate(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003533 mTextView.mTextEditSuggestionContainerLayout, null);
Gilles Debunned88876a2012-03-16 17:34:04 -07003534
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003535 mContainerView = (LinearLayout) mContentView.findViewById(
3536 com.android.internal.R.id.suggestionWindowContainer);
Seigo Nonaka60490d12016-01-28 17:25:18 +09003537 ViewGroup.MarginLayoutParams lp =
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003538 (ViewGroup.MarginLayoutParams) mContainerView.getLayoutParams();
Seigo Nonaka60490d12016-01-28 17:25:18 +09003539 mContainerMarginWidth = lp.leftMargin + lp.rightMargin;
3540 mContainerMarginTop = lp.topMargin;
3541 mClippingLimitLeft = lp.leftMargin;
3542 mClippingLimitRight = lp.rightMargin;
3543
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003544 mSuggestionListView = (ListView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003545 com.android.internal.R.id.suggestionContainer);
3546
3547 mSuggestionsAdapter = new SuggestionAdapter();
Seigo Nonakaf47976e2016-03-01 09:17:37 -08003548 mSuggestionListView.setAdapter(mSuggestionsAdapter);
3549 mSuggestionListView.setOnItemClickListener(this);
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003550
3551 // Inflate the suggestion items once and for all.
3552 mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS];
Gilles Debunned88876a2012-03-16 17:34:04 -07003553 for (int i = 0; i < mSuggestionInfos.length; i++) {
3554 mSuggestionInfos[i] = new SuggestionInfo();
3555 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003556
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003557 mAddToDictionaryButton = (TextView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003558 com.android.internal.R.id.addToDictionaryButton);
3559 mAddToDictionaryButton.setOnClickListener(new View.OnClickListener() {
3560 public void onClick(View v) {
Keisuke Kuroyanagi6e0860d2016-03-15 15:40:43 +09003561 final SuggestionSpan misspelledSpan =
3562 findEquivalentSuggestionSpan(mMisspelledSpanInfo);
3563 if (misspelledSpan == null) {
3564 // Span has been removed.
3565 return;
3566 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003567 final Editable editable = (Editable) mTextView.getText();
Keisuke Kuroyanagi6e0860d2016-03-15 15:40:43 +09003568 final int spanStart = editable.getSpanStart(misspelledSpan);
3569 final int spanEnd = editable.getSpanEnd(misspelledSpan);
3570 if (spanStart < 0 || spanEnd <= spanStart) {
3571 return;
3572 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003573 final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
3574
3575 final Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
3576 intent.putExtra(USER_DICTIONARY_EXTRA_WORD, originalText);
3577 intent.putExtra(USER_DICTIONARY_EXTRA_LOCALE,
3578 mTextView.getTextServicesLocale().toString());
3579 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
3580 mTextView.getContext().startActivity(intent);
3581 // There is no way to know if the word was indeed added. Re-check.
3582 // TODO The ExtractEditText should remove the span in the original text instead
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003583 editable.removeSpan(mMisspelledSpanInfo.mSuggestionSpan);
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003584 Selection.setSelection(editable, spanEnd);
3585 updateSpellCheckSpans(spanStart, spanEnd, false);
3586 hideWithCleanUp();
3587 }
3588 });
3589
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003590 mDeleteButton = (TextView) mContentView.findViewById(
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003591 com.android.internal.R.id.deleteButton);
3592 mDeleteButton.setOnClickListener(new View.OnClickListener() {
3593 public void onClick(View v) {
3594 final Editable editable = (Editable) mTextView.getText();
3595
3596 final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
3597 int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
3598 if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
3599 // Do not leave two adjacent spaces after deletion, or one at beginning of
3600 // text
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003601 if (spanUnionEnd < editable.length()
3602 && Character.isSpaceChar(editable.charAt(spanUnionEnd))
3603 && (spanUnionStart == 0
3604 || Character.isSpaceChar(
3605 editable.charAt(spanUnionStart - 1)))) {
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003606 spanUnionEnd = spanUnionEnd + 1;
3607 }
3608 mTextView.deleteText_internal(spanUnionStart, spanUnionEnd);
3609 }
3610 hideWithCleanUp();
3611 }
3612 });
3613
Gilles Debunned88876a2012-03-16 17:34:04 -07003614 }
3615
3616 public boolean isShowingUp() {
3617 return mIsShowingUp;
3618 }
3619
3620 public void onParentLostFocus() {
3621 mIsShowingUp = false;
3622 }
3623
Gilles Debunned88876a2012-03-16 17:34:04 -07003624 private class SuggestionAdapter extends BaseAdapter {
Seigo Nonakae9f54bf2016-05-12 18:18:12 +09003625 private LayoutInflater mInflater = (LayoutInflater) mContext.getSystemService(
3626 Context.LAYOUT_INFLATER_SERVICE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003627
3628 @Override
3629 public int getCount() {
3630 return mNumberOfSuggestions;
3631 }
3632
3633 @Override
3634 public Object getItem(int position) {
3635 return mSuggestionInfos[position];
3636 }
3637
3638 @Override
3639 public long getItemId(int position) {
3640 return position;
3641 }
3642
3643 @Override
3644 public View getView(int position, View convertView, ViewGroup parent) {
3645 TextView textView = (TextView) convertView;
3646
3647 if (textView == null) {
3648 textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout,
3649 parent, false);
3650 }
3651
3652 final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003653 textView.setText(suggestionInfo.mText);
Gilles Debunned88876a2012-03-16 17:34:04 -07003654 return textView;
3655 }
3656 }
3657
Seigo Nonakaa60160b2015-08-19 12:38:35 -07003658 @VisibleForTesting
3659 public ViewGroup getContentViewForTesting() {
3660 return mContentView;
3661 }
3662
Gilles Debunned88876a2012-03-16 17:34:04 -07003663 @Override
3664 public void show() {
3665 if (!(mTextView.getText() instanceof Editable)) return;
Keisuke Kuroyanagi4a696ac2016-02-23 11:02:07 -08003666 if (extractedTextModeWillBeStarted()) {
3667 return;
3668 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003669
3670 if (updateSuggestions()) {
3671 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
3672 mTextView.setCursorVisible(false);
3673 mIsShowingUp = true;
3674 super.show();
3675 }
3676 }
3677
3678 @Override
3679 protected void measureContent() {
3680 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3681 final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
3682 displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
3683 final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
3684 displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
3685
3686 int width = 0;
3687 View view = null;
3688 for (int i = 0; i < mNumberOfSuggestions; i++) {
3689 view = mSuggestionsAdapter.getView(i, view, mContentView);
3690 view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
3691 view.measure(horizontalMeasure, verticalMeasure);
3692 width = Math.max(width, view.getMeasuredWidth());
3693 }
3694
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003695 if (mAddToDictionaryButton.getVisibility() != View.GONE) {
3696 mAddToDictionaryButton.measure(horizontalMeasure, verticalMeasure);
3697 width = Math.max(width, mAddToDictionaryButton.getMeasuredWidth());
3698 }
3699
3700 mDeleteButton.measure(horizontalMeasure, verticalMeasure);
3701 width = Math.max(width, mDeleteButton.getMeasuredWidth());
3702
Seigo Nonaka77972bb2016-04-20 15:45:34 +09003703 width += mContainerView.getPaddingLeft() + mContainerView.getPaddingRight()
3704 + mContainerMarginWidth;
Seigo Nonaka60490d12016-01-28 17:25:18 +09003705
Gilles Debunned88876a2012-03-16 17:34:04 -07003706 // Enforce the width based on actual text widths
3707 mContentView.measure(
3708 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
3709 verticalMeasure);
3710
3711 Drawable popupBackground = mPopupWindow.getBackground();
3712 if (popupBackground != null) {
3713 if (mTempRect == null) mTempRect = new Rect();
3714 popupBackground.getPadding(mTempRect);
3715 width += mTempRect.left + mTempRect.right;
3716 }
3717 mPopupWindow.setWidth(width);
3718 }
3719
3720 @Override
3721 protected int getTextOffset() {
Keisuke Kuroyanagi713be062016-02-29 16:07:54 -08003722 return (mTextView.getSelectionStart() + mTextView.getSelectionStart()) / 2;
Gilles Debunned88876a2012-03-16 17:34:04 -07003723 }
3724
3725 @Override
3726 protected int getVerticalLocalPosition(int line) {
Siyamed Sinira60b59d2017-07-26 09:26:41 -07003727 final Layout layout = mTextView.getLayout();
3728 return layout.getLineBottomWithoutSpacing(line) - mContainerMarginTop;
Gilles Debunned88876a2012-03-16 17:34:04 -07003729 }
3730
3731 @Override
3732 protected int clipVertically(int positionY) {
3733 final int height = mContentView.getMeasuredHeight();
3734 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3735 return Math.min(positionY, displayMetrics.heightPixels - height);
3736 }
3737
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003738 private void hideWithCleanUp() {
3739 for (final SuggestionInfo info : mSuggestionInfos) {
3740 info.clear();
3741 }
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003742 mMisspelledSpanInfo.clear();
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003743 hide();
Gilles Debunned88876a2012-03-16 17:34:04 -07003744 }
3745
3746 private boolean updateSuggestions() {
3747 Spannable spannable = (Spannable) mTextView.getText();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003748 mNumberOfSuggestions =
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003749 mSuggestionHelper.getSuggestionInfo(mSuggestionInfos, mMisspelledSpanInfo);
3750 if (mNumberOfSuggestions == 0 && mMisspelledSpanInfo.mSuggestionSpan == null) {
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003751 return false;
3752 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003753
Gilles Debunned88876a2012-03-16 17:34:04 -07003754 int spanUnionStart = mTextView.getText().length();
3755 int spanUnionEnd = 0;
3756
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003757 for (int i = 0; i < mNumberOfSuggestions; i++) {
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003758 final SuggestionSpanInfo spanInfo = mSuggestionInfos[i].mSuggestionSpanInfo;
3759 spanUnionStart = Math.min(spanUnionStart, spanInfo.mSpanStart);
3760 spanUnionEnd = Math.max(spanUnionEnd, spanInfo.mSpanEnd);
3761 }
3762 if (mMisspelledSpanInfo.mSuggestionSpan != null) {
3763 spanUnionStart = Math.min(spanUnionStart, mMisspelledSpanInfo.mSpanStart);
3764 spanUnionEnd = Math.max(spanUnionEnd, mMisspelledSpanInfo.mSpanEnd);
Gilles Debunned88876a2012-03-16 17:34:04 -07003765 }
3766
3767 for (int i = 0; i < mNumberOfSuggestions; i++) {
3768 highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
3769 }
3770
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003771 // Make "Add to dictionary" item visible if there is a span with the misspelled flag
3772 int addToDictionaryButtonVisibility = View.GONE;
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003773 if (mMisspelledSpanInfo.mSuggestionSpan != null) {
3774 if (mMisspelledSpanInfo.mSpanStart >= 0
3775 && mMisspelledSpanInfo.mSpanEnd > mMisspelledSpanInfo.mSpanStart) {
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003776 addToDictionaryButtonVisibility = View.VISIBLE;
Gilles Debunned88876a2012-03-16 17:34:04 -07003777 }
3778 }
Seigo Nonakaa71a2442015-06-19 15:00:43 +09003779 mAddToDictionaryButton.setVisibility(addToDictionaryButtonVisibility);
Gilles Debunned88876a2012-03-16 17:34:04 -07003780
3781 if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003782 final int underlineColor;
3783 if (mNumberOfSuggestions != 0) {
3784 underlineColor =
3785 mSuggestionInfos[0].mSuggestionSpanInfo.mSuggestionSpan.getUnderlineColor();
3786 } else {
3787 underlineColor = mMisspelledSpanInfo.mSuggestionSpan.getUnderlineColor();
3788 }
3789
Gilles Debunned88876a2012-03-16 17:34:04 -07003790 if (underlineColor == 0) {
3791 // Fallback on the default highlight color when the first span does not provide one
3792 mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor);
3793 } else {
3794 final float BACKGROUND_TRANSPARENCY = 0.4f;
3795 final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
3796 mSuggestionRangeSpan.setBackgroundColor(
3797 (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
3798 }
3799 spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
3800 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
3801
3802 mSuggestionsAdapter.notifyDataSetChanged();
3803 return true;
3804 }
3805
3806 private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
3807 int unionEnd) {
3808 final Spannable text = (Spannable) mTextView.getText();
Keisuke Kuroyanagif8e0da22016-03-14 15:10:57 +09003809 final int spanStart = suggestionInfo.mSuggestionSpanInfo.mSpanStart;
3810 final int spanEnd = suggestionInfo.mSuggestionSpanInfo.mSpanEnd;
Gilles Debunned88876a2012-03-16 17:34:04 -07003811
3812 // Adjust the start/end of the suggestion span
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003813 suggestionInfo.mSuggestionStart = spanStart - unionStart;
3814 suggestionInfo.mSuggestionEnd = suggestionInfo.mSuggestionStart
3815 + suggestionInfo.mText.length();
Gilles Debunned88876a2012-03-16 17:34:04 -07003816
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003817 suggestionInfo.mText.setSpan(mHighlightSpan, 0, suggestionInfo.mText.length(),
Seigo Nonakabffbd302015-08-18 18:27:56 -07003818 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunned88876a2012-03-16 17:34:04 -07003819
3820 // Add the text before and after the span.
3821 final String textAsString = text.toString();
Keisuke Kuroyanagi1cd8aac2015-12-21 18:01:40 +09003822 suggestionInfo.mText.insert(0, textAsString.substring(unionStart, spanStart));
3823 suggestionInfo.mText.append(textAsString.substring(spanEnd, unionEnd));
Gilles Debunned88876a2012-03-16 17:34:04 -07003824 }
3825
3826 @Override
3827 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003828 SuggestionInfo suggestionInfo = mSuggestionInfos[position];
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003829 replaceWithSuggestion(suggestionInfo);
Seigo Nonaka7afa67c2015-10-07 17:06:04 +09003830 hideWithCleanUp();
Gilles Debunned88876a2012-03-16 17:34:04 -07003831 }
3832 }
3833
3834 /**
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003835 * An ActionMode Callback class that is used to provide actions while in text insertion or
3836 * selection mode.
Gilles Debunned88876a2012-03-16 17:34:04 -07003837 *
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003838 * The default callback provides a subset of Select All, Cut, Copy, Paste, Share and Replace
3839 * actions, depending on which of these this TextView supports and the current selection.
Gilles Debunned88876a2012-03-16 17:34:04 -07003840 */
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003841 private class TextActionModeCallback extends ActionMode.Callback2 {
Clara Bayarriea4f1502015-03-18 00:25:01 +00003842 private final Path mSelectionPath = new Path();
3843 private final RectF mSelectionBounds = new RectF();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003844 private final boolean mHasSelection;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003845 private final int mHandleHeight;
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01003846 private final Map<MenuItem, OnClickListener> mAssistClickHandlers = new HashMap<>();
Clara Bayarriea4f1502015-03-18 00:25:01 +00003847
Richard Ledley26b87222017-11-30 10:54:08 +00003848 TextActionModeCallback(@TextActionMode int mode) {
3849 mHasSelection = mode == TextActionMode.SELECTION
3850 || (mTextIsSelectable && mode == TextActionMode.TEXT_LINK);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003851 if (mHasSelection) {
3852 SelectionModifierCursorController selectionController = getSelectionController();
3853 if (selectionController.mStartHandle == null) {
3854 // As these are for initializing selectionController, hide() must be called.
3855 selectionController.initDrawables();
3856 selectionController.initHandles();
3857 selectionController.hide();
3858 }
3859 mHandleHeight = Math.max(
3860 mSelectHandleLeft.getMinimumHeight(),
3861 mSelectHandleRight.getMinimumHeight());
3862 } else {
3863 InsertionPointCursorController insertionController = getInsertionController();
3864 if (insertionController != null) {
3865 insertionController.getHandle();
3866 mHandleHeight = mSelectHandleCenter.getMinimumHeight();
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003867 } else {
3868 mHandleHeight = 0;
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003869 }
Clara Bayarri7fc946e2015-03-31 14:48:33 +01003870 }
Clara Bayarriea4f1502015-03-18 00:25:01 +00003871 }
Gilles Debunned88876a2012-03-16 17:34:04 -07003872
3873 @Override
3874 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01003875 mAssistClickHandlers.clear();
3876
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003877 mode.setTitle(null);
Clara Bayarri13152d12015-04-09 12:02:04 +01003878 mode.setSubtitle(null);
3879 mode.setTitleOptionalHint(true);
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003880 populateMenuWithItems(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 if (!customCallback.onCreateActionMode(mode, menu)) {
Clara Bayarri01243ac2015-06-03 00:46:29 +01003885 // The custom mode can choose to cancel the action mode, dismiss selection.
3886 Selection.setSelection((Spannable) mTextView.getText(),
3887 mTextView.getSelectionEnd());
Clara Bayarri13152d12015-04-09 12:02:04 +01003888 return false;
3889 }
3890 }
3891
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07003892 if (mTextView.canProcessText()) {
3893 mProcessTextIntentActionsHandler.onInitializeMenu(menu);
3894 }
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +00003895
Abodunrinwa Tokideb2f492017-11-06 18:55:17 +00003896 if (mHasSelection && !mTextView.hasTransientState()) {
3897 mTextView.setHasTransientState(true);
Clara Bayarri13152d12015-04-09 12:02:04 +01003898 }
Abodunrinwa Tokideb2f492017-11-06 18:55:17 +00003899 return true;
Clara Bayarri13152d12015-04-09 12:02:04 +01003900 }
3901
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003902 private Callback getCustomCallback() {
3903 return mHasSelection
3904 ? mCustomSelectionActionModeCallback
3905 : mCustomInsertionActionModeCallback;
3906 }
3907
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00003908 private void populateMenuWithItems(Menu menu) {
Gilles Debunned88876a2012-03-16 17:34:04 -07003909 if (mTextView.canCut()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003910 menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003911 com.android.internal.R.string.cut)
3912 .setAlphabeticShortcut('x')
3913 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07003914 }
3915
3916 if (mTextView.canCopy()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003917 menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003918 com.android.internal.R.string.copy)
3919 .setAlphabeticShortcut('c')
3920 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07003921 }
3922
3923 if (mTextView.canPaste()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003924 menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003925 com.android.internal.R.string.paste)
3926 .setAlphabeticShortcut('v')
3927 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Gilles Debunned88876a2012-03-16 17:34:04 -07003928 }
3929
Andrei Stingaceanu7f0c5bd2015-04-14 17:12:08 +01003930 if (mTextView.canShare()) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003931 menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
Aurimas Liutikasee62c292016-07-21 15:05:40 -07003932 com.android.internal.R.string.share)
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +00003933 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
Andrei Stingaceanu7f0c5bd2015-04-14 17:12:08 +01003934 }
3935
Felipe Leme2ac463e2017-03-13 14:06:25 -07003936 if (mTextView.canRequestAutofill()) {
Felipe Leme1c1626e2017-06-02 10:53:13 -07003937 final String selected = mTextView.getSelectedText();
3938 if (selected == null || selected.isEmpty()) {
3939 menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
3940 com.android.internal.R.string.autofill)
Abodunrinwa Toki9c881f22017-10-16 21:05:41 +01003941 .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
Felipe Leme1c1626e2017-06-02 10:53:13 -07003942 }
Felipe Leme2ac463e2017-03-13 14:06:25 -07003943 }
3944
Abodunrinwa Tokiea6cb122017-04-28 22:14:13 +01003945 if (mTextView.canPasteAsPlainText()) {
3946 menu.add(
3947 Menu.NONE,
3948 TextView.ID_PASTE_AS_PLAIN_TEXT,
3949 MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
3950 com.android.internal.R.string.paste_as_plain_text)
3951 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
3952 }
3953
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003954 updateSelectAllItem(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01003955 updateReplaceItem(menu);
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01003956 updateAssistMenuItems(menu);
Gilles Debunned88876a2012-03-16 17:34:04 -07003957 }
3958
3959 @Override
3960 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003961 updateSelectAllItem(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01003962 updateReplaceItem(menu);
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01003963 updateAssistMenuItems(menu);
Clara Bayarri13152d12015-04-09 12:02:04 +01003964
Clara Bayarri7938cdb2015-06-02 20:03:45 +01003965 Callback customCallback = getCustomCallback();
3966 if (customCallback != null) {
3967 return customCallback.onPrepareActionMode(mode, menu);
Gilles Debunned88876a2012-03-16 17:34:04 -07003968 }
3969 return true;
3970 }
3971
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003972 private void updateSelectAllItem(Menu menu) {
3973 boolean canSelectAll = mTextView.canSelectAllText();
3974 boolean selectAllItemExists = menu.findItem(TextView.ID_SELECT_ALL) != null;
3975 if (canSelectAll && !selectAllItemExists) {
3976 menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
3977 com.android.internal.R.string.selectAll)
3978 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
3979 } else if (!canSelectAll && selectAllItemExists) {
3980 menu.removeItem(TextView.ID_SELECT_ALL);
3981 }
3982 }
3983
Clara Bayarri13152d12015-04-09 12:02:04 +01003984 private void updateReplaceItem(Menu menu) {
Keisuke Kuroyanagid31945032016-02-26 16:09:12 -08003985 boolean canReplace = mTextView.isSuggestionsEnabled() && shouldOfferToShowSuggestions();
Clara Bayarri13152d12015-04-09 12:02:04 +01003986 boolean replaceItemExists = menu.findItem(TextView.ID_REPLACE) != null;
3987 if (canReplace && !replaceItemExists) {
Clara Bayarri3b69fd82015-06-03 21:52:02 +01003988 menu.add(Menu.NONE, TextView.ID_REPLACE, MENU_ITEM_ORDER_REPLACE,
3989 com.android.internal.R.string.replace)
3990 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
Clara Bayarri13152d12015-04-09 12:02:04 +01003991 } else if (!canReplace && replaceItemExists) {
3992 menu.removeItem(TextView.ID_REPLACE);
3993 }
3994 }
3995
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01003996 private void updateAssistMenuItems(Menu menu) {
3997 clearAssistMenuItems(menu);
3998 if (!mTextView.isDeviceProvisioned()) {
3999 return;
4000 }
Abodunrinwa Tokie0b57892017-04-28 19:59:57 +01004001 final TextClassification textClassification =
4002 getSelectionActionModeHelper().getTextClassification();
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00004003 if (textClassification == null) {
4004 return;
4005 }
4006 if (isValidAssistMenuItem(
4007 textClassification.getIcon(),
4008 textClassification.getLabel(),
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00004009 textClassification.getIntent())) {
4010 final MenuItem item = menu.add(
4011 TextView.ID_ASSIST, TextView.ID_ASSIST, MENU_ITEM_ORDER_ASSIST,
4012 textClassification.getLabel())
4013 .setIcon(textClassification.getIcon())
4014 .setIntent(textClassification.getIntent());
4015 item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
Jan Althaus0d9fbb92017-11-28 12:19:33 +01004016 mAssistClickHandlers.put(
4017 item, TextClassification.createStartActivityOnClickListener(
4018 mTextView.getContext(), textClassification.getIntent()));
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00004019 }
4020 final int count = textClassification.getSecondaryActionsCount();
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004021 for (int i = 0; i < count; i++) {
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00004022 if (!isValidAssistMenuItem(
4023 textClassification.getSecondaryIcon(i),
4024 textClassification.getSecondaryLabel(i),
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00004025 textClassification.getSecondaryIntent(i))) {
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004026 continue;
4027 }
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00004028 final int order = MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START + i;
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004029 final MenuItem item = menu.add(
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00004030 TextView.ID_ASSIST, Menu.NONE, order,
4031 textClassification.getSecondaryLabel(i))
4032 .setIcon(textClassification.getSecondaryIcon(i))
4033 .setIntent(textClassification.getSecondaryIntent(i));
4034 item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
Jan Althaus0d9fbb92017-11-28 12:19:33 +01004035 mAssistClickHandlers.put(item,
4036 TextClassification.createStartActivityOnClickListener(
4037 mTextView.getContext(), textClassification.getSecondaryIntent(i)));
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00004038 }
Abodunrinwa Tokid0d9ceb2016-11-21 18:41:02 +00004039 }
4040
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004041 private void clearAssistMenuItems(Menu menu) {
4042 int i = 0;
4043 while (i < menu.size()) {
4044 final MenuItem menuItem = menu.getItem(i);
4045 if (menuItem.getGroupId() == TextView.ID_ASSIST) {
4046 menu.removeItem(menuItem.getItemId());
4047 continue;
4048 }
4049 i++;
4050 }
4051 }
4052
Jan Althaus0d9fbb92017-11-28 12:19:33 +01004053 private boolean isValidAssistMenuItem(Drawable icon, CharSequence label, Intent intent) {
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004054 final boolean hasUi = icon != null || !TextUtils.isEmpty(label);
Jan Althaus0d9fbb92017-11-28 12:19:33 +01004055 final boolean hasAction = isSupportedIntent(intent);
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004056 return hasUi && hasAction;
4057 }
4058
4059 private boolean isSupportedIntent(Intent intent) {
4060 if (intent == null) {
4061 return false;
4062 }
4063 final Context context = mTextView.getContext();
4064 final ResolveInfo info = context.getPackageManager().resolveActivity(intent, 0);
4065 final boolean samePackage = context.getPackageName().equals(
4066 info.activityInfo.packageName);
4067 if (samePackage) {
4068 return true;
4069 }
4070
4071 final boolean exported = info.activityInfo.exported;
4072 final boolean requiresPermission = info.activityInfo.permission != null;
4073 final boolean hasPermission = !requiresPermission
4074 || context.checkSelfPermission(info.activityInfo.permission)
4075 == PackageManager.PERMISSION_GRANTED;
4076 return exported && hasPermission;
4077 }
4078
4079 private boolean onAssistMenuItemClicked(MenuItem assistMenuItem) {
4080 Preconditions.checkArgument(assistMenuItem.getGroupId() == TextView.ID_ASSIST);
4081
4082 final TextClassification textClassification =
4083 getSelectionActionModeHelper().getTextClassification();
4084 if (!mTextView.isDeviceProvisioned() || textClassification == null) {
4085 // No textClassification result to handle the click. Eat the click.
4086 return true;
4087 }
4088
4089 OnClickListener onClickListener = mAssistClickHandlers.get(assistMenuItem);
4090 if (onClickListener == null) {
4091 final Intent intent = assistMenuItem.getIntent();
4092 if (intent != null) {
4093 onClickListener = TextClassification.createStartActivityOnClickListener(
4094 mTextView.getContext(), intent);
4095 }
4096 }
4097 if (onClickListener != null) {
4098 onClickListener.onClick(mTextView);
4099 stopTextActionMode();
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004100 }
4101 // We tried our best.
4102 return true;
Abodunrinwa Toki9796a1b2017-06-28 02:49:07 +01004103 }
4104
Gilles Debunned88876a2012-03-16 17:34:04 -07004105 @Override
4106 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Abodunrinwa Toki57ec6ea2017-09-04 20:32:31 +01004107 getSelectionActionModeHelper().onSelectionAction(item.getItemId());
Abodunrinwa Toki1d775572017-05-08 16:03:01 +01004108
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07004109 if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
Clara Bayarrid5bf3ed2015-03-27 17:32:45 +00004110 return true;
4111 }
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004112 Callback customCallback = getCustomCallback();
4113 if (customCallback != null && customCallback.onActionItemClicked(mode, item)) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004114 return true;
4115 }
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004116 if (item.getGroupId() == TextView.ID_ASSIST && onAssistMenuItemClicked(item)) {
Abodunrinwa Tokifafdb732017-02-02 11:07:05 +00004117 return true;
Abodunrinwa Tokif001fef2017-01-04 23:51:42 +00004118 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004119 return mTextView.onTextContextMenuItem(item.getItemId());
4120 }
4121
4122 @Override
4123 public void onDestroyActionMode(ActionMode mode) {
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09004124 // Clear mTextActionMode not to recursively destroy action mode by clearing selection.
Abodunrinwa Toki89ba5fb2017-02-23 02:52:05 +00004125 getSelectionActionModeHelper().onDestroyActionMode();
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09004126 mTextActionMode = null;
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004127 Callback customCallback = getCustomCallback();
4128 if (customCallback != null) {
4129 customCallback.onDestroyActionMode(mode);
Gilles Debunned88876a2012-03-16 17:34:04 -07004130 }
Adam Powell057a5852012-05-11 10:28:38 -07004131
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08004132 if (!mPreserveSelection) {
4133 /*
4134 * Leave current selection when we tentatively destroy action mode for the
4135 * selection. If we're detaching from a window, we'll bring back the selection
4136 * mode when (if) we get reattached.
4137 */
Adam Powell057a5852012-05-11 10:28:38 -07004138 Selection.setSelection((Spannable) mTextView.getText(),
4139 mTextView.getSelectionEnd());
Adam Powell057a5852012-05-11 10:28:38 -07004140 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004141
4142 if (mSelectionModifierCursorController != null) {
4143 mSelectionModifierCursorController.hide();
4144 }
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01004145
4146 mAssistClickHandlers.clear();
Gilles Debunned88876a2012-03-16 17:34:04 -07004147 }
Clara Bayarriea4f1502015-03-18 00:25:01 +00004148
4149 @Override
4150 public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
4151 if (!view.equals(mTextView) || mTextView.getLayout() == null) {
4152 super.onGetContentRect(mode, view, outRect);
4153 return;
4154 }
4155 if (mTextView.getSelectionStart() != mTextView.getSelectionEnd()) {
4156 // We have a selection.
4157 mSelectionPath.reset();
4158 mTextView.getLayout().getSelectionPath(
4159 mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mSelectionPath);
4160 mSelectionPath.computeBounds(mSelectionBounds, true);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004161 mSelectionBounds.bottom += mHandleHeight;
Clara Bayarriea4f1502015-03-18 00:25:01 +00004162 } else {
Roozbeh Pournader9c133072017-07-26 22:36:27 -07004163 // We have a cursor.
Siyamed Sinir987ec652016-02-17 19:44:41 -08004164 Layout layout = mTextView.getLayout();
Mady Mellorff66ca52015-07-08 12:31:45 -07004165 int line = layout.getLineForOffset(mTextView.getSelectionStart());
Siyamed Sinir987ec652016-02-17 19:44:41 -08004166 float primaryHorizontal = clampHorizontalPosition(null,
4167 layout.getPrimaryHorizontal(mTextView.getSelectionStart()));
Clara Bayarriea4f1502015-03-18 00:25:01 +00004168 mSelectionBounds.set(
4169 primaryHorizontal,
Mady Mellorff66ca52015-07-08 12:31:45 -07004170 layout.getLineTop(line),
Clara Bayarrif95ed102015-08-12 19:46:47 +01004171 primaryHorizontal,
Siyamed Sinira60b59d2017-07-26 09:26:41 -07004172 layout.getLineBottom(line) - layout.getLineBottom(line) + mHandleHeight);
Clara Bayarriea4f1502015-03-18 00:25:01 +00004173 }
4174 // Take TextView's padding and scroll into account.
4175 int textHorizontalOffset = mTextView.viewportToContentHorizontalOffset();
4176 int textVerticalOffset = mTextView.viewportToContentVerticalOffset();
4177 outRect.set(
4178 (int) Math.floor(mSelectionBounds.left + textHorizontalOffset),
4179 (int) Math.floor(mSelectionBounds.top + textVerticalOffset),
4180 (int) Math.ceil(mSelectionBounds.right + textHorizontalOffset),
4181 (int) Math.ceil(mSelectionBounds.bottom + textVerticalOffset));
4182 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004183 }
4184
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004185 /**
4186 * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}
4187 * while the input method is requesting the cursor/anchor position. Does nothing as long as
4188 * {@link InputMethodManager#isWatchingCursor(View)} returns false.
4189 */
4190 private final class CursorAnchorInfoNotifier implements TextViewPositionListener {
Yohei Yukawac46b5f02014-06-10 12:26:34 +09004191 final CursorAnchorInfo.Builder mSelectionInfoBuilder = new CursorAnchorInfo.Builder();
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004192 final int[] mTmpIntOffset = new int[2];
4193 final Matrix mViewToScreenMatrix = new Matrix();
4194
4195 @Override
4196 public void updatePosition(int parentPositionX, int parentPositionY,
4197 boolean parentPositionChanged, boolean parentScrolled) {
4198 final InputMethodState ims = mInputMethodState;
4199 if (ims == null || ims.mBatchEditNesting > 0) {
4200 return;
4201 }
4202 final InputMethodManager imm = InputMethodManager.peekInstance();
4203 if (null == imm) {
4204 return;
4205 }
Yohei Yukawa0023d0e2014-07-11 04:13:03 +09004206 if (!imm.isActive(mTextView)) {
4207 return;
4208 }
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004209 // Skip if the IME has not requested the cursor/anchor position.
Yohei Yukawa0023d0e2014-07-11 04:13:03 +09004210 if (!imm.isCursorAnchorInfoEnabled()) {
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004211 return;
4212 }
4213 Layout layout = mTextView.getLayout();
4214 if (layout == null) {
4215 return;
4216 }
4217
Yohei Yukawac46b5f02014-06-10 12:26:34 +09004218 final CursorAnchorInfo.Builder builder = mSelectionInfoBuilder;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004219 builder.reset();
4220
4221 final int selectionStart = mTextView.getSelectionStart();
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004222 builder.setSelectionRange(selectionStart, mTextView.getSelectionEnd());
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004223
4224 // Construct transformation matrix from view local coordinates to screen coordinates.
4225 mViewToScreenMatrix.set(mTextView.getMatrix());
4226 mTextView.getLocationOnScreen(mTmpIntOffset);
4227 mViewToScreenMatrix.postTranslate(mTmpIntOffset[0], mTmpIntOffset[1]);
4228 builder.setMatrix(mViewToScreenMatrix);
4229
4230 final float viewportToContentHorizontalOffset =
4231 mTextView.viewportToContentHorizontalOffset();
4232 final float viewportToContentVerticalOffset =
4233 mTextView.viewportToContentVerticalOffset();
4234
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004235 final CharSequence text = mTextView.getText();
4236 if (text instanceof Spannable) {
4237 final Spannable sp = (Spannable) text;
4238 int composingTextStart = EditableInputConnection.getComposingSpanStart(sp);
4239 int composingTextEnd = EditableInputConnection.getComposingSpanEnd(sp);
4240 if (composingTextEnd < composingTextStart) {
4241 final int temp = composingTextEnd;
4242 composingTextEnd = composingTextStart;
4243 composingTextStart = temp;
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004244 }
Yohei Yukawa81f4cb32014-05-13 22:20:35 +09004245 final boolean hasComposingText =
4246 (0 <= composingTextStart) && (composingTextStart < composingTextEnd);
4247 if (hasComposingText) {
4248 final CharSequence composingText = text.subSequence(composingTextStart,
4249 composingTextEnd);
4250 builder.setComposingText(composingTextStart, composingText);
Phil Weaverc2e28932016-12-08 12:29:25 -08004251 mTextView.populateCharacterBounds(builder, composingTextStart,
4252 composingTextEnd, viewportToContentHorizontalOffset,
4253 viewportToContentVerticalOffset);
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004254 }
4255 }
4256
4257 // Treat selectionStart as the insertion point.
4258 if (0 <= selectionStart) {
4259 final int offset = selectionStart;
4260 final int line = layout.getLineForOffset(offset);
4261 final float insertionMarkerX = layout.getPrimaryHorizontal(offset)
4262 + viewportToContentHorizontalOffset;
4263 final float insertionMarkerTop = layout.getLineTop(line)
4264 + viewportToContentVerticalOffset;
4265 final float insertionMarkerBaseline = layout.getLineBaseline(line)
4266 + viewportToContentVerticalOffset;
Siyamed Sinira60b59d2017-07-26 09:26:41 -07004267 final float insertionMarkerBottom = layout.getLineBottomWithoutSpacing(line)
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004268 + viewportToContentVerticalOffset;
Phil Weaverc2e28932016-12-08 12:29:25 -08004269 final boolean isTopVisible = mTextView
4270 .isPositionVisible(insertionMarkerX, insertionMarkerTop);
4271 final boolean isBottomVisible = mTextView
4272 .isPositionVisible(insertionMarkerX, insertionMarkerBottom);
Yohei Yukawacc24e2b2014-08-29 20:21:10 -07004273 int insertionMarkerFlags = 0;
4274 if (isTopVisible || isBottomVisible) {
4275 insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
4276 }
4277 if (!isTopVisible || !isBottomVisible) {
4278 insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
4279 }
Yohei Yukawa5f183f02014-09-02 14:18:40 -07004280 if (layout.isRtlCharAt(offset)) {
4281 insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL;
4282 }
Yohei Yukawa0b01e7f2014-07-08 15:29:51 +09004283 builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop,
Yohei Yukawacc24e2b2014-08-29 20:21:10 -07004284 insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags);
Yohei Yukawa83b68ba2014-05-12 15:46:25 +09004285 }
4286
4287 imm.updateCursorAnchorInfo(mTextView, builder.build());
4288 }
4289 }
4290
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004291 @VisibleForTesting
4292 public abstract class HandleView extends View implements TextViewPositionListener {
Gilles Debunned88876a2012-03-16 17:34:04 -07004293 protected Drawable mDrawable;
4294 protected Drawable mDrawableLtr;
4295 protected Drawable mDrawableRtl;
4296 private final PopupWindow mContainer;
4297 // Position with respect to the parent TextView
4298 private int mPositionX, mPositionY;
4299 private boolean mIsDragging;
4300 // Offset from touch position to mPosition
4301 private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
4302 protected int mHotspotX;
Adam Powell3fceabd2014-08-19 18:28:04 -07004303 protected int mHorizontalGravity;
Gilles Debunned88876a2012-03-16 17:34:04 -07004304 // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
4305 private float mTouchOffsetY;
4306 // Where the touch position should be on the handle to ensure a maximum cursor visibility
4307 private float mIdealVerticalOffset;
4308 // Parent's (TextView) previous position in window
4309 private int mLastParentX, mLastParentY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004310 // Parent's (TextView) previous position on screen
4311 private int mLastParentXOnScreen, mLastParentYOnScreen;
Gilles Debunned88876a2012-03-16 17:34:04 -07004312 // Previous text character offset
Mady Mellorc2225b92015-04-01 15:59:20 -07004313 protected int mPreviousOffset = -1;
Gilles Debunned88876a2012-03-16 17:34:04 -07004314 // Previous text character offset
4315 private boolean mPositionHasChanged = true;
Adam Powell3fceabd2014-08-19 18:28:04 -07004316 // Minimum touch target size for handles
4317 private int mMinSize;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004318 // Indicates the line of text that the handle is on.
Mady Mellora6a0f782015-07-10 16:43:32 -07004319 protected int mPrevLine = UNSET_LINE;
4320 // Indicates the line of text that the user was touching. This can differ from mPrevLine
4321 // when selecting text when the handles jump to the end / start of words which may be on
4322 // a different line.
4323 protected int mPreviousLineTouched = UNSET_LINE;
Gilles Debunned88876a2012-03-16 17:34:04 -07004324
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004325 private HandleView(Drawable drawableLtr, Drawable drawableRtl, final int id) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004326 super(mTextView.getContext());
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004327 setId(id);
Gilles Debunned88876a2012-03-16 17:34:04 -07004328 mContainer = new PopupWindow(mTextView.getContext(), null,
4329 com.android.internal.R.attr.textSelectHandleWindowStyle);
4330 mContainer.setSplitTouchEnabled(true);
4331 mContainer.setClippingEnabled(false);
4332 mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
Keisuke Kuroyanagi7340be72015-02-27 17:57:49 +09004333 mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
4334 mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
Gilles Debunned88876a2012-03-16 17:34:04 -07004335 mContainer.setContentView(this);
4336
4337 mDrawableLtr = drawableLtr;
4338 mDrawableRtl = drawableRtl;
Adam Powell3fceabd2014-08-19 18:28:04 -07004339 mMinSize = mTextView.getContext().getResources().getDimensionPixelSize(
4340 com.android.internal.R.dimen.text_handle_min_size);
Gilles Debunned88876a2012-03-16 17:34:04 -07004341
4342 updateDrawable();
4343
Adam Powell3fceabd2014-08-19 18:28:04 -07004344 final int handleHeight = getPreferredHeight();
Gilles Debunned88876a2012-03-16 17:34:04 -07004345 mTouchOffsetY = -0.3f * handleHeight;
4346 mIdealVerticalOffset = 0.7f * handleHeight;
4347 }
4348
Mady Mellor7a936442015-05-20 10:05:52 -07004349 public float getIdealVerticalOffset() {
4350 return mIdealVerticalOffset;
4351 }
4352
Gilles Debunned88876a2012-03-16 17:34:04 -07004353 protected void updateDrawable() {
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004354 if (mIsDragging) {
4355 // Don't update drawable during dragging.
4356 return;
4357 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004358 final Layout layout = mTextView.getLayout();
4359 if (layout == null) {
4360 return;
4361 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004362 final int offset = getCurrentCursorOffset();
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004363 final boolean isRtlCharAtOffset = isAtRtlRun(layout, offset);
Keisuke Kuroyanagi33f81ac2015-05-14 20:10:57 +09004364 final Drawable oldDrawable = mDrawable;
Gilles Debunned88876a2012-03-16 17:34:04 -07004365 mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
4366 mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
Adam Powell3fceabd2014-08-19 18:28:04 -07004367 mHorizontalGravity = getHorizontalGravity(isRtlCharAtOffset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004368 if (oldDrawable != mDrawable && isShowing()) {
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004369 // Update popup window position.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004370 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
4371 - getHorizontalOffset() + getCursorOffset();
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004372 mPositionX += mTextView.viewportToContentHorizontalOffset();
4373 mPositionHasChanged = true;
4374 updatePosition(mLastParentX, mLastParentY, false, false);
Keisuke Kuroyanagi33f81ac2015-05-14 20:10:57 +09004375 postInvalidate();
4376 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004377 }
4378
4379 protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
Adam Powell3fceabd2014-08-19 18:28:04 -07004380 protected abstract int getHorizontalGravity(boolean isRtlRun);
Gilles Debunned88876a2012-03-16 17:34:04 -07004381
4382 // Touch-up filter: number of previous positions remembered
4383 private static final int HISTORY_SIZE = 5;
4384 private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
4385 private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
4386 private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
4387 private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
4388 private int mPreviousOffsetIndex = 0;
4389 private int mNumberPreviousOffsets = 0;
4390
4391 private void startTouchUpFilter(int offset) {
4392 mNumberPreviousOffsets = 0;
4393 addPositionToTouchUpFilter(offset);
4394 }
4395
4396 private void addPositionToTouchUpFilter(int offset) {
4397 mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
4398 mPreviousOffsets[mPreviousOffsetIndex] = offset;
4399 mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
4400 mNumberPreviousOffsets++;
4401 }
4402
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004403 private void filterOnTouchUp(boolean fromTouchScreen) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004404 final long now = SystemClock.uptimeMillis();
4405 int i = 0;
4406 int index = mPreviousOffsetIndex;
4407 final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
4408 while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
4409 i++;
4410 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
4411 }
4412
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004413 if (i > 0 && i < iMax
4414 && (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004415 positionAtCursorOffset(mPreviousOffsets[index], false, fromTouchScreen);
Gilles Debunned88876a2012-03-16 17:34:04 -07004416 }
4417 }
4418
4419 public boolean offsetHasBeenChanged() {
4420 return mNumberPreviousOffsets > 1;
4421 }
4422
4423 @Override
4424 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Adam Powell3fceabd2014-08-19 18:28:04 -07004425 setMeasuredDimension(getPreferredWidth(), getPreferredHeight());
4426 }
4427
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004428 @Override
4429 public void invalidate() {
4430 super.invalidate();
4431 if (isShowing()) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004432 positionAtCursorOffset(getCurrentCursorOffset(), true, false);
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004433 }
4434 };
4435
Adam Powell3fceabd2014-08-19 18:28:04 -07004436 private int getPreferredWidth() {
4437 return Math.max(mDrawable.getIntrinsicWidth(), mMinSize);
4438 }
4439
4440 private int getPreferredHeight() {
4441 return Math.max(mDrawable.getIntrinsicHeight(), mMinSize);
Gilles Debunned88876a2012-03-16 17:34:04 -07004442 }
4443
4444 public void show() {
4445 if (isShowing()) return;
4446
4447 getPositionListener().addSubscriber(this, true /* local position may change */);
4448
4449 // Make sure the offset is always considered new, even when focusing at same position
4450 mPreviousOffset = -1;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004451 positionAtCursorOffset(getCurrentCursorOffset(), false, false);
Gilles Debunned88876a2012-03-16 17:34:04 -07004452 }
4453
4454 protected void dismiss() {
4455 mIsDragging = false;
4456 mContainer.dismiss();
4457 onDetached();
4458 }
4459
4460 public void hide() {
4461 dismiss();
4462
4463 getPositionListener().removeSubscriber(this);
4464 }
4465
Gilles Debunned88876a2012-03-16 17:34:04 -07004466 public boolean isShowing() {
4467 return mContainer.isShowing();
4468 }
4469
4470 private boolean isVisible() {
4471 // Always show a dragging handle.
4472 if (mIsDragging) {
4473 return true;
4474 }
4475
4476 if (mTextView.isInBatchEditMode()) {
4477 return false;
4478 }
4479
Phil Weaverc2e28932016-12-08 12:29:25 -08004480 return mTextView.isPositionVisible(
4481 mPositionX + mHotspotX + getHorizontalOffset(), mPositionY);
Gilles Debunned88876a2012-03-16 17:34:04 -07004482 }
4483
4484 public abstract int getCurrentCursorOffset();
4485
4486 protected abstract void updateSelection(int offset);
4487
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004488 protected abstract void updatePosition(float x, float y, boolean fromTouchScreen);
Gilles Debunned88876a2012-03-16 17:34:04 -07004489
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004490 @MagnifierHandleTrigger
4491 protected abstract int getMagnifierHandleTrigger();
4492
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004493 protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
4494 return layout.isRtlCharAt(offset);
4495 }
4496
4497 @VisibleForTesting
4498 public float getHorizontal(@NonNull Layout layout, int offset) {
4499 return layout.getPrimaryHorizontal(offset);
4500 }
4501
4502 protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
4503 return mTextView.getOffsetAtCoordinate(line, x);
4504 }
4505
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004506 /**
4507 * @param offset Cursor offset. Must be in [-1, length].
4508 * @param forceUpdatePosition whether to force update the position. This should be true
4509 * when If the parent has been scrolled, for example.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004510 * @param fromTouchScreen {@code true} if the cursor is moved with motion events from the
4511 * touch screen.
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004512 */
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004513 protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
4514 boolean fromTouchScreen) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004515 // A HandleView relies on the layout, which may be nulled by external methods
4516 Layout layout = mTextView.getLayout();
4517 if (layout == null) {
4518 // Will update controllers' state, hiding them and stopping selection mode if needed
4519 prepareCursorControllers();
4520 return;
4521 }
Siyamed Sinir987ec652016-02-17 19:44:41 -08004522 layout = mTextView.getLayout();
Gilles Debunned88876a2012-03-16 17:34:04 -07004523
4524 boolean offsetChanged = offset != mPreviousOffset;
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09004525 if (offsetChanged || forceUpdatePosition) {
Gilles Debunned88876a2012-03-16 17:34:04 -07004526 if (offsetChanged) {
4527 updateSelection(offset);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004528 if (fromTouchScreen && mHapticTextHandleEnabled) {
4529 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
4530 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004531 addPositionToTouchUpFilter(offset);
4532 }
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07004533 final int line = layout.getLineForOffset(offset);
Mady Mellorb9bbbb12015-03-23 11:50:46 -07004534 mPrevLine = line;
Gilles Debunned88876a2012-03-16 17:34:04 -07004535
Aurimas Liutikasee62c292016-07-21 15:05:40 -07004536 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
4537 - getHorizontalOffset() + getCursorOffset();
Siyamed Sinira60b59d2017-07-26 09:26:41 -07004538 mPositionY = layout.getLineBottomWithoutSpacing(line);
Gilles Debunned88876a2012-03-16 17:34:04 -07004539
4540 // Take TextView's padding and scroll into account.
4541 mPositionX += mTextView.viewportToContentHorizontalOffset();
4542 mPositionY += mTextView.viewportToContentVerticalOffset();
4543
4544 mPreviousOffset = offset;
4545 mPositionHasChanged = true;
4546 }
4547 }
4548
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004549 /**
Roozbeh Pournader9c133072017-07-26 22:36:27 -07004550 * Return the clamped horizontal position for the cursor.
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004551 *
4552 * @param layout Text layout.
4553 * @param offset Character offset for the cursor.
4554 * @return The clamped horizontal position for the cursor.
4555 */
4556 int getCursorHorizontalPosition(Layout layout, int offset) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004557 return (int) (getHorizontal(layout, offset) - 0.5f);
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004558 }
4559
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004560 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07004561 public void updatePosition(int parentPositionX, int parentPositionY,
4562 boolean parentPositionChanged, boolean parentScrolled) {
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004563 positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled, false);
Gilles Debunned88876a2012-03-16 17:34:04 -07004564 if (parentPositionChanged || mPositionHasChanged) {
4565 if (mIsDragging) {
4566 // Update touchToWindow offset in case of parent scrolling while dragging
4567 if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
4568 mTouchToWindowOffsetX += parentPositionX - mLastParentX;
4569 mTouchToWindowOffsetY += parentPositionY - mLastParentY;
4570 mLastParentX = parentPositionX;
4571 mLastParentY = parentPositionY;
4572 }
4573
4574 onHandleMoved();
4575 }
4576
4577 if (isVisible()) {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004578 // Transform to the window coordinates to follow the view tranformation.
4579 final int[] pts = { mPositionX + mHotspotX + getHorizontalOffset(), mPositionY};
4580 mTextView.transformFromViewToWindowSpace(pts);
4581 pts[0] -= mHotspotX + getHorizontalOffset();
4582
Gilles Debunned88876a2012-03-16 17:34:04 -07004583 if (isShowing()) {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004584 mContainer.update(pts[0], pts[1], -1, -1);
Gilles Debunned88876a2012-03-16 17:34:04 -07004585 } else {
Seigo Nonaka2f229ca2016-02-18 17:53:54 +09004586 mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY, pts[0], pts[1]);
Gilles Debunned88876a2012-03-16 17:34:04 -07004587 }
4588 } else {
4589 if (isShowing()) {
4590 dismiss();
4591 }
4592 }
4593
4594 mPositionHasChanged = false;
4595 }
4596 }
4597
4598 @Override
4599 protected void onDraw(Canvas c) {
Adam Powell3fceabd2014-08-19 18:28:04 -07004600 final int drawWidth = mDrawable.getIntrinsicWidth();
4601 final int left = getHorizontalOffset();
4602
4603 mDrawable.setBounds(left, 0, left + drawWidth, mDrawable.getIntrinsicHeight());
Gilles Debunned88876a2012-03-16 17:34:04 -07004604 mDrawable.draw(c);
4605 }
4606
Adam Powell3fceabd2014-08-19 18:28:04 -07004607 private int getHorizontalOffset() {
4608 final int width = getPreferredWidth();
4609 final int drawWidth = mDrawable.getIntrinsicWidth();
4610 final int left;
4611 switch (mHorizontalGravity) {
4612 case Gravity.LEFT:
4613 left = 0;
4614 break;
4615 default:
4616 case Gravity.CENTER:
4617 left = (width - drawWidth) / 2;
4618 break;
4619 case Gravity.RIGHT:
4620 left = width - drawWidth;
4621 break;
4622 }
4623 return left;
4624 }
4625
4626 protected int getCursorOffset() {
4627 return 0;
4628 }
4629
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004630 protected final void showMagnifier() {
4631 if (mMagnifier == null) {
4632 return;
4633 }
4634
4635 final int trigger = getMagnifierHandleTrigger();
4636 final int offset;
4637 switch (trigger) {
4638 case MagnifierHandleTrigger.INSERTION: // Fall through.
4639 case MagnifierHandleTrigger.SELECTION_START:
4640 offset = mTextView.getSelectionStart();
4641 break;
4642 case MagnifierHandleTrigger.SELECTION_END:
4643 offset = mTextView.getSelectionEnd();
4644 break;
4645 default:
4646 offset = -1;
4647 break;
4648 }
4649
4650 if (offset == -1) {
4651 dismissMagnifier();
4652 }
4653
4654 final Layout layout = mTextView.getLayout();
4655 final int lineNumber = layout.getLineForOffset(offset);
4656 // Horizontally snap to character offset.
Andrei Stingaceanuca189fe2017-10-19 17:02:22 +01004657 final float xPosInView = getHorizontal(mTextView.getLayout(), offset)
4658 + mTextView.getTotalPaddingLeft() - mTextView.getScrollX();
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004659 // Vertically snap to middle of current line.
4660 final float yPosInView = (mTextView.getLayout().getLineTop(lineNumber)
Andrei Stingaceanuca189fe2017-10-19 17:02:22 +01004661 + mTextView.getLayout().getLineBottom(lineNumber)) / 2.0f
4662 + mTextView.getTotalPaddingTop() - mTextView.getScrollY();
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004663
Andrei Stingaceanu451f9472017-10-13 16:41:28 +01004664 suspendBlink();
Andrei Stingaceanud27c36b2017-10-24 11:17:35 +01004665 mMagnifier.show(xPosInView, yPosInView);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004666 }
4667
4668 protected final void dismissMagnifier() {
4669 if (mMagnifier != null) {
4670 mMagnifier.dismiss();
Andrei Stingaceanu451f9472017-10-13 16:41:28 +01004671 resumeBlink();
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004672 }
4673 }
4674
Gilles Debunned88876a2012-03-16 17:34:04 -07004675 @Override
4676 public boolean onTouchEvent(MotionEvent ev) {
Abodunrinwa Tokifd3a3a12015-05-05 20:04:34 +01004677 updateFloatingToolbarVisibility(ev);
4678
Gilles Debunned88876a2012-03-16 17:34:04 -07004679 switch (ev.getActionMasked()) {
4680 case MotionEvent.ACTION_DOWN: {
4681 startTouchUpFilter(getCurrentCursorOffset());
Gilles Debunned88876a2012-03-16 17:34:04 -07004682
4683 final PositionListener positionListener = getPositionListener();
4684 mLastParentX = positionListener.getPositionX();
4685 mLastParentY = positionListener.getPositionY();
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004686 mLastParentXOnScreen = positionListener.getPositionXOnScreen();
4687 mLastParentYOnScreen = positionListener.getPositionYOnScreen();
4688
4689 final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
4690 final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
4691 mTouchToWindowOffsetX = xInWindow - mPositionX;
4692 mTouchToWindowOffsetY = yInWindow - mPositionY;
4693
Gilles Debunned88876a2012-03-16 17:34:04 -07004694 mIsDragging = true;
Mady Mellora6a0f782015-07-10 16:43:32 -07004695 mPreviousLineTouched = UNSET_LINE;
Gilles Debunned88876a2012-03-16 17:34:04 -07004696 break;
4697 }
4698
4699 case MotionEvent.ACTION_MOVE: {
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004700 final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
4701 final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
Gilles Debunned88876a2012-03-16 17:34:04 -07004702
4703 // Vertical hysteresis: vertical down movement tends to snap to ideal offset
4704 final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004705 final float currentVerticalOffset = yInWindow - mPositionY - mLastParentY;
Gilles Debunned88876a2012-03-16 17:34:04 -07004706 float newVerticalOffset;
4707 if (previousVerticalOffset < mIdealVerticalOffset) {
4708 newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
4709 newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
4710 } else {
4711 newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
4712 newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
4713 }
4714 mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
4715
Keisuke Kuroyanagibc89a5c2015-05-18 14:49:29 +09004716 final float newPosX =
Keisuke Kuroyanagie2a3b1e2016-04-28 12:55:18 +09004717 xInWindow - mTouchToWindowOffsetX + mHotspotX + getHorizontalOffset();
4718 final float newPosY = yInWindow - mTouchToWindowOffsetY + mTouchOffsetY;
Gilles Debunned88876a2012-03-16 17:34:04 -07004719
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004720 updatePosition(newPosX, newPosY,
4721 ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Gilles Debunned88876a2012-03-16 17:34:04 -07004722 break;
4723 }
4724
4725 case MotionEvent.ACTION_UP:
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004726 filterOnTouchUp(ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004727 // Fall through.
Gilles Debunned88876a2012-03-16 17:34:04 -07004728 case MotionEvent.ACTION_CANCEL:
4729 mIsDragging = false;
Keisuke Kuroyanagidbe2c292015-05-27 19:49:34 +09004730 updateDrawable();
Gilles Debunned88876a2012-03-16 17:34:04 -07004731 break;
4732 }
4733 return true;
4734 }
4735
4736 public boolean isDragging() {
4737 return mIsDragging;
4738 }
4739
Clara Bayarri6351e662015-03-16 23:17:59 +00004740 void onHandleMoved() {}
Gilles Debunned88876a2012-03-16 17:34:04 -07004741
Clara Bayarri6351e662015-03-16 23:17:59 +00004742 public void onDetached() {}
Gilles Debunned88876a2012-03-16 17:34:04 -07004743 }
4744
4745 private class InsertionHandleView extends HandleView {
4746 private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
4747 private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
4748
Clara Bayarrib71dddd2015-06-04 23:17:30 +01004749 // Used to detect taps on the insertion handle, which will affect the insertion action mode
Gilles Debunned88876a2012-03-16 17:34:04 -07004750 private float mDownPositionX, mDownPositionY;
4751 private Runnable mHider;
4752
4753 public InsertionHandleView(Drawable drawable) {
Keisuke Kuroyanagida79ee62015-11-25 16:15:15 +09004754 super(drawable, drawable, com.android.internal.R.id.insertion_handle);
Gilles Debunned88876a2012-03-16 17:34:04 -07004755 }
4756
4757 @Override
4758 public void show() {
4759 super.show();
4760
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01004761 final long durationSinceCutOrCopy =
Andrei Stingaceanu77b9c382015-05-06 13:25:19 +01004762 SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime;
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01004763
4764 // Cancel the single tap delayed runnable.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004765 if (mInsertionActionModeRunnable != null
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09004766 && ((mTapState == TAP_STATE_DOUBLE_TAP)
4767 || (mTapState == TAP_STATE_TRIPLE_CLICK)
4768 || isCursorInsideEasyCorrectionSpan())) {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004769 mTextView.removeCallbacks(mInsertionActionModeRunnable);
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01004770 }
4771
4772 // Prepare and schedule the single tap runnable to run exactly after the double tap
4773 // timeout has passed.
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09004774 if ((mTapState != TAP_STATE_DOUBLE_TAP) && (mTapState != TAP_STATE_TRIPLE_CLICK)
4775 && !isCursorInsideEasyCorrectionSpan()
Andrei Stingaceanu373816e2015-05-28 11:26:28 +01004776 && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION)) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01004777 if (mTextActionMode == null) {
4778 if (mInsertionActionModeRunnable == null) {
4779 mInsertionActionModeRunnable = new Runnable() {
4780 @Override
4781 public void run() {
4782 startInsertionActionMode();
4783 }
4784 };
4785 }
4786 mTextView.postDelayed(
4787 mInsertionActionModeRunnable,
4788 ViewConfiguration.getDoubleTapTimeout() + 1);
Andrei Stingaceanufae270c2015-04-29 14:39:40 +01004789 }
4790
Gilles Debunned88876a2012-03-16 17:34:04 -07004791 }
4792
4793 hideAfterDelay();
4794 }
4795
Gilles Debunned88876a2012-03-16 17:34:04 -07004796 private void hideAfterDelay() {
4797 if (mHider == null) {
4798 mHider = new Runnable() {
4799 public void run() {
4800 hide();
4801 }
4802 };
4803 } else {
4804 removeHiderCallback();
4805 }
4806 mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
4807 }
4808
4809 private void removeHiderCallback() {
4810 if (mHider != null) {
4811 mTextView.removeCallbacks(mHider);
4812 }
4813 }
4814
4815 @Override
4816 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
4817 return drawable.getIntrinsicWidth() / 2;
4818 }
4819
4820 @Override
Adam Powell3fceabd2014-08-19 18:28:04 -07004821 protected int getHorizontalGravity(boolean isRtlRun) {
4822 return Gravity.CENTER_HORIZONTAL;
4823 }
4824
4825 @Override
4826 protected int getCursorOffset() {
4827 int offset = super.getCursorOffset();
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07004828 if (mDrawableForCursor != null) {
4829 mDrawableForCursor.getPadding(mTempRect);
4830 offset += (mDrawableForCursor.getIntrinsicWidth()
Roozbeh Pournader9c133072017-07-26 22:36:27 -07004831 - mTempRect.left - mTempRect.right) / 2;
Adam Powell3fceabd2014-08-19 18:28:04 -07004832 }
4833 return offset;
4834 }
4835
4836 @Override
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004837 int getCursorHorizontalPosition(Layout layout, int offset) {
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07004838 if (mDrawableForCursor != null) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004839 final float horizontal = getHorizontal(layout, offset);
Roozbeh Pournadere06eebf2017-10-03 12:13:46 -07004840 return clampHorizontalPosition(mDrawableForCursor, horizontal) + mTempRect.left;
Siyamed Sinir217c0f72016-02-01 18:30:02 -08004841 }
4842 return super.getCursorHorizontalPosition(layout, offset);
4843 }
4844
4845 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07004846 public boolean onTouchEvent(MotionEvent ev) {
4847 final boolean result = super.onTouchEvent(ev);
4848
4849 switch (ev.getActionMasked()) {
4850 case MotionEvent.ACTION_DOWN:
4851 mDownPositionX = ev.getRawX();
4852 mDownPositionY = ev.getRawY();
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004853 showMagnifier();
4854 break;
4855
4856 case MotionEvent.ACTION_MOVE:
4857 showMagnifier();
Gilles Debunned88876a2012-03-16 17:34:04 -07004858 break;
4859
4860 case MotionEvent.ACTION_UP:
4861 if (!offsetHasBeenChanged()) {
4862 final float deltaX = mDownPositionX - ev.getRawX();
4863 final float deltaY = mDownPositionY - ev.getRawY();
4864 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
4865
4866 final ViewConfiguration viewConfiguration = ViewConfiguration.get(
4867 mTextView.getContext());
4868 final int touchSlop = viewConfiguration.getScaledTouchSlop();
4869
4870 if (distanceSquared < touchSlop * touchSlop) {
Clara Bayarrib71dddd2015-06-04 23:17:30 +01004871 // Tapping on the handle toggles the insertion action mode.
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004872 if (mTextActionMode != null) {
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08004873 stopTextActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07004874 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004875 startInsertionActionMode();
Gilles Debunned88876a2012-03-16 17:34:04 -07004876 }
4877 }
Abodunrinwa Tokibcdf0ab2015-04-25 00:11:25 +01004878 } else {
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004879 if (mTextActionMode != null) {
4880 mTextActionMode.invalidateContentRect();
Abodunrinwa Tokibcdf0ab2015-04-25 00:11:25 +01004881 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004882 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004883 // Fall through.
Gilles Debunned88876a2012-03-16 17:34:04 -07004884 case MotionEvent.ACTION_CANCEL:
4885 hideAfterDelay();
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004886 dismissMagnifier();
Gilles Debunned88876a2012-03-16 17:34:04 -07004887 break;
4888
4889 default:
4890 break;
4891 }
4892
4893 return result;
4894 }
4895
4896 @Override
4897 public int getCurrentCursorOffset() {
4898 return mTextView.getSelectionStart();
4899 }
4900
4901 @Override
4902 public void updateSelection(int offset) {
4903 Selection.setSelection((Spannable) mTextView.getText(), offset);
4904 }
4905
4906 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004907 protected void updatePosition(float x, float y, boolean fromTouchScreen) {
Mady Melloree3821e2015-06-05 11:12:01 -07004908 Layout layout = mTextView.getLayout();
4909 int offset;
4910 if (layout != null) {
Mady Mellora6a0f782015-07-10 16:43:32 -07004911 if (mPreviousLineTouched == UNSET_LINE) {
4912 mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
4913 }
4914 int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004915 offset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellora6a0f782015-07-10 16:43:32 -07004916 mPreviousLineTouched = currLine;
Mady Melloree3821e2015-06-05 11:12:01 -07004917 } else {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09004918 offset = -1;
Mady Melloree3821e2015-06-05 11:12:01 -07004919 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07004920 positionAtCursorOffset(offset, false, fromTouchScreen);
Clara Bayarri7938cdb2015-06-02 20:03:45 +01004921 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01004922 invalidateActionMode();
Clara Bayarri1baed512015-05-11 15:29:16 +01004923 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004924 }
4925
4926 @Override
4927 void onHandleMoved() {
4928 super.onHandleMoved();
4929 removeHiderCallback();
4930 }
4931
4932 @Override
4933 public void onDetached() {
4934 super.onDetached();
4935 removeHiderCallback();
4936 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01004937
4938 @Override
4939 @MagnifierHandleTrigger
4940 protected int getMagnifierHandleTrigger() {
4941 return MagnifierHandleTrigger.INSERTION;
4942 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004943 }
4944
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004945 @Retention(RetentionPolicy.SOURCE)
Jeff Sharkeyce8db992017-12-13 20:05:05 -07004946 @IntDef(prefix = { "HANDLE_TYPE_" }, value = {
4947 HANDLE_TYPE_SELECTION_START,
4948 HANDLE_TYPE_SELECTION_END
4949 })
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004950 public @interface HandleType {}
4951 public static final int HANDLE_TYPE_SELECTION_START = 0;
4952 public static final int HANDLE_TYPE_SELECTION_END = 1;
4953
Abodunrinwa Toki4a056a52017-08-05 01:56:40 +01004954 /** For selection handles */
4955 @VisibleForTesting
4956 public final class SelectionHandleView extends HandleView {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004957 // Indicates the handle type, selection start (HANDLE_TYPE_SELECTION_START) or selection
4958 // end (HANDLE_TYPE_SELECTION_END).
4959 @HandleType
4960 private final int mHandleType;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08004961 // Indicates whether the cursor is making adjustments within a word.
4962 private boolean mInWord = false;
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09004963 // Difference between touch position and word boundary position.
4964 private float mTouchWordDelta;
Mady Mellore264ac32015-06-22 16:46:29 -07004965 // X value of the previous updatePosition call.
4966 private float mPrevX;
4967 // Indicates if the handle has moved a boundary between LTR and RTL text.
4968 private boolean mLanguageDirectionChanged = false;
Mady Mellor42390aa2015-07-24 13:08:42 -07004969 // Distance from edge of horizontally scrolling text view
4970 // to use to switch to character mode.
4971 private final float mTextViewEdgeSlop;
4972 // Used to save text view location.
4973 private final int[] mTextViewLocation = new int[2];
Gilles Debunned88876a2012-03-16 17:34:04 -07004974
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004975 public SelectionHandleView(Drawable drawableLtr, Drawable drawableRtl, int id,
4976 @HandleType int handleType) {
4977 super(drawableLtr, drawableRtl, id);
4978 mHandleType = handleType;
4979 ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext());
Mady Mellor42390aa2015-07-24 13:08:42 -07004980 mTextViewEdgeSlop = viewConfiguration.getScaledTouchSlop() * 4;
Gilles Debunned88876a2012-03-16 17:34:04 -07004981 }
4982
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004983 private boolean isStartHandle() {
4984 return mHandleType == HANDLE_TYPE_SELECTION_START;
4985 }
4986
Gilles Debunned88876a2012-03-16 17:34:04 -07004987 @Override
4988 protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004989 if (isRtlRun == isStartHandle()) {
Mady Mellor709386f2015-05-14 12:41:18 -07004990 return drawable.getIntrinsicWidth() / 4;
4991 } else {
4992 return (drawable.getIntrinsicWidth() * 3) / 4;
4993 }
Gilles Debunned88876a2012-03-16 17:34:04 -07004994 }
4995
4996 @Override
Adam Powell3fceabd2014-08-19 18:28:04 -07004997 protected int getHorizontalGravity(boolean isRtlRun) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09004998 return (isRtlRun == isStartHandle()) ? Gravity.LEFT : Gravity.RIGHT;
Adam Powell3fceabd2014-08-19 18:28:04 -07004999 }
5000
5001 @Override
Gilles Debunned88876a2012-03-16 17:34:04 -07005002 public int getCurrentCursorOffset() {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005003 return isStartHandle() ? mTextView.getSelectionStart() : mTextView.getSelectionEnd();
Gilles Debunned88876a2012-03-16 17:34:04 -07005004 }
5005
5006 @Override
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005007 protected void updateSelection(int offset) {
5008 if (isStartHandle()) {
5009 Selection.setSelection((Spannable) mTextView.getText(), offset,
5010 mTextView.getSelectionEnd());
5011 } else {
5012 Selection.setSelection((Spannable) mTextView.getText(),
5013 mTextView.getSelectionStart(), offset);
5014 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005015 updateDrawable();
Clara Bayarri7938cdb2015-06-02 20:03:45 +01005016 if (mTextActionMode != null) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01005017 invalidateActionMode();
Clara Bayarri13152d12015-04-09 12:02:04 +01005018 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005019 }
5020
5021 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005022 protected void updatePosition(float x, float y, boolean fromTouchScreen) {
Mady Mellor81fa3e82015-05-14 09:17:41 -07005023 final Layout layout = mTextView.getLayout();
Mady Mellorcc65c372015-06-17 09:25:19 -07005024 if (layout == null) {
5025 // HandleView will deal appropriately in positionAtCursorOffset when
5026 // layout is null.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005027 positionAndAdjustForCrossingHandles(mTextView.getOffsetForPosition(x, y),
5028 fromTouchScreen);
Mady Mellorcc65c372015-06-17 09:25:19 -07005029 return;
5030 }
5031
Mady Mellora6a0f782015-07-10 16:43:32 -07005032 if (mPreviousLineTouched == UNSET_LINE) {
5033 mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
5034 }
5035
Mady Mellorb9bbbb12015-03-23 11:50:46 -07005036 boolean positionCursor = false;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005037 final int anotherHandleOffset =
5038 isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
Mady Mellora6a0f782015-07-10 16:43:32 -07005039 int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005040 int initialOffset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellor81fa3e82015-05-14 09:17:41 -07005041
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005042 if (isStartHandle() && initialOffset >= anotherHandleOffset
5043 || !isStartHandle() && initialOffset <= anotherHandleOffset) {
5044 // Handles have crossed, bound it to the first selected line and
Mady Mellor81fa3e82015-05-14 09:17:41 -07005045 // adjust by word / char as normal.
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005046 currLine = layout.getLineForOffset(anotherHandleOffset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005047 initialOffset = getOffsetAtCoordinate(layout, currLine, x);
Mady Mellor81fa3e82015-05-14 09:17:41 -07005048 }
5049
5050 int offset = initialOffset;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005051 final int wordEnd = getWordEnd(offset);
5052 final int wordStart = getWordStart(offset);
Gilles Debunned88876a2012-03-16 17:34:04 -07005053
Mady Mellore264ac32015-06-22 16:46:29 -07005054 if (mPrevX == UNSET_X_VALUE) {
5055 mPrevX = x;
5056 }
5057
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005058 final int currentOffset = getCurrentCursorOffset();
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005059 final boolean rtlAtCurrentOffset = isAtRtlRun(layout, currentOffset);
5060 final boolean atRtl = isAtRtlRun(layout, offset);
Mady Mellore264ac32015-06-22 16:46:29 -07005061 final boolean isLvlBoundary = layout.isLevelBoundary(offset);
Mady Mellore264ac32015-06-22 16:46:29 -07005062
5063 // We can't determine if the user is expanding or shrinking the selection if they're
5064 // on a bi-di boundary, so until they've moved past the boundary we'll just place
5065 // the cursor at the current position.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005066 if (isLvlBoundary || (rtlAtCurrentOffset && !atRtl) || (!rtlAtCurrentOffset && atRtl)) {
Mady Mellore264ac32015-06-22 16:46:29 -07005067 // We're on a boundary or this is the first direction change -- just update
5068 // to the current position.
5069 mLanguageDirectionChanged = true;
5070 mTouchWordDelta = 0.0f;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005071 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellore264ac32015-06-22 16:46:29 -07005072 return;
5073 } else if (mLanguageDirectionChanged && !isLvlBoundary) {
5074 // We've just moved past the boundary so update the position. After this we can
5075 // figure out if the user is expanding or shrinking to go by word or character.
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005076 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellore264ac32015-06-22 16:46:29 -07005077 mTouchWordDelta = 0.0f;
5078 mLanguageDirectionChanged = false;
5079 return;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005080 }
5081
5082 boolean isExpanding;
5083 final float xDiff = x - mPrevX;
Keisuke Kuroyanagi26454142015-12-02 15:04:57 -08005084 if (isStartHandle()) {
5085 isExpanding = currLine < mPreviousLineTouched;
Mady Mellore264ac32015-06-22 16:46:29 -07005086 } else {
Keisuke Kuroyanagi26454142015-12-02 15:04:57 -08005087 isExpanding = currLine > mPreviousLineTouched;
5088 }
5089 if (atRtl == isStartHandle()) {
5090 isExpanding |= xDiff > 0;
5091 } else {
5092 isExpanding |= xDiff < 0;
Mady Mellore264ac32015-06-22 16:46:29 -07005093 }
5094
Mady Mellor42390aa2015-07-24 13:08:42 -07005095 if (mTextView.getHorizontallyScrolling()) {
5096 if (positionNearEdgeOfScrollingView(x, atRtl)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005097 && ((isStartHandle() && mTextView.getScrollX() != 0)
5098 || (!isStartHandle()
5099 && mTextView.canScrollHorizontally(atRtl ? -1 : 1)))
5100 && ((isExpanding && ((isStartHandle() && offset < currentOffset)
5101 || (!isStartHandle() && offset > currentOffset)))
5102 || !isExpanding)) {
5103 // If we're expanding ensure that the offset is actually expanding compared to
5104 // the current offset, if the handle snapped to the word, the finger position
Mady Mellor42390aa2015-07-24 13:08:42 -07005105 // may be out of sync and we don't want the selection to jump back.
5106 mTouchWordDelta = 0.0f;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005107 final int nextOffset = (atRtl == isStartHandle())
5108 ? layout.getOffsetToRightOf(mPreviousOffset)
Mady Mellor42390aa2015-07-24 13:08:42 -07005109 : layout.getOffsetToLeftOf(mPreviousOffset);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005110 positionAndAdjustForCrossingHandles(nextOffset, fromTouchScreen);
Mady Mellor42390aa2015-07-24 13:08:42 -07005111 return;
5112 }
5113 }
5114
Mady Mellore264ac32015-06-22 16:46:29 -07005115 if (isExpanding) {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005116 // User is increasing the selection.
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005117 int wordBoundary = isStartHandle() ? wordStart : wordEnd;
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005118 final boolean snapToWord = (!mInWord
5119 || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine))
5120 && atRtl == isAtRtlRun(layout, wordBoundary);
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005121 if (snapToWord) {
Mady Mellora5266832015-06-26 14:28:12 -07005122 // Sometimes words can be broken across lines (Chinese, hyphenation).
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005123 // We still snap to the word boundary but we only use the letters on the
Mady Mellora5266832015-06-26 14:28:12 -07005124 // current line to determine if the user is far enough into the word to snap.
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005125 if (layout.getLineForOffset(wordBoundary) != currLine) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005126 wordBoundary = isStartHandle()
5127 ? layout.getLineStart(currLine) : layout.getLineEnd(currLine);
Mady Mellora5266832015-06-26 14:28:12 -07005128 }
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005129 final int offsetThresholdToSnap = isStartHandle()
5130 ? wordEnd - ((wordEnd - wordBoundary) / 2)
5131 : wordStart + ((wordBoundary - wordStart) / 2);
5132 if (isStartHandle()
5133 && (offset <= offsetThresholdToSnap || currLine < mPrevLine)) {
5134 // User is far enough into the word or on a different line so we expand by
5135 // word.
5136 offset = wordStart;
5137 } else if (!isStartHandle()
5138 && (offset >= offsetThresholdToSnap || currLine > mPrevLine)) {
5139 // User is far enough into the word or on a different line so we expand by
5140 // word.
5141 offset = wordEnd;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005142 } else {
Mady Mellorc2225b92015-04-01 15:59:20 -07005143 offset = mPreviousOffset;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005144 }
5145 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005146 if ((isStartHandle() && offset < initialOffset)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005147 || (!isStartHandle() && offset > initialOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005148 final float adjustedX = getHorizontal(layout, offset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005149 mTouchWordDelta =
5150 mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
Keisuke Kuroyanagi50a927c2015-05-07 17:34:21 +09005151 } else {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005152 mTouchWordDelta = 0.0f;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005153 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005154 positionCursor = true;
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005155 } else {
5156 final int adjustedOffset =
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005157 getOffsetAtCoordinate(layout, currLine, x - mTouchWordDelta);
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005158 final boolean shrinking = isStartHandle()
5159 ? adjustedOffset > mPreviousOffset || currLine > mPrevLine
5160 : adjustedOffset < mPreviousOffset || currLine < mPrevLine;
5161 if (shrinking) {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005162 // User is shrinking the selection.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005163 if (currLine != mPrevLine) {
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005164 // We're on a different line, so we'll snap to word boundaries.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005165 offset = isStartHandle() ? wordStart : wordEnd;
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005166 if ((isStartHandle() && offset < initialOffset)
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005167 || (!isStartHandle() && offset > initialOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005168 final float adjustedX = getHorizontal(layout, offset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005169 mTouchWordDelta =
5170 mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
5171 } else {
5172 mTouchWordDelta = 0.0f;
5173 }
5174 } else {
5175 offset = adjustedOffset;
5176 }
5177 positionCursor = true;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005178 } else if ((isStartHandle() && adjustedOffset < mPreviousOffset)
5179 || (!isStartHandle() && adjustedOffset > mPreviousOffset)) {
5180 // Handle has jumped to the word boundary, and the user is moving
Mady Mellor43fd2f42015-06-08 14:03:34 -07005181 // their finger towards the handle, the delta should be updated.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005182 mTouchWordDelta = mTextView.convertToLocalHorizontalCoordinate(x)
5183 - getHorizontal(layout, mPreviousOffset);
Keisuke Kuroyanagi0138e4c2015-05-12 12:51:26 +09005184 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005185 }
5186
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005187 if (positionCursor) {
Mady Mellora6a0f782015-07-10 16:43:32 -07005188 mPreviousLineTouched = currLine;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005189 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005190 }
Mady Mellore264ac32015-06-22 16:46:29 -07005191 mPrevX = x;
Gilles Debunned88876a2012-03-16 17:34:04 -07005192 }
5193
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005194 @Override
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005195 protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
5196 boolean fromTouchScreen) {
5197 super.positionAtCursorOffset(offset, forceUpdatePosition, fromTouchScreen);
Yoshiki Iguchi9582e152015-10-15 13:34:41 +09005198 mInWord = (offset != -1) && !getWordIteratorWithText().isBoundary(offset);
Mady Mellor36d5a7b2015-05-22 10:31:12 -07005199 }
5200
5201 @Override
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005202 public boolean onTouchEvent(MotionEvent event) {
5203 boolean superResult = super.onTouchEvent(event);
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005204
5205 switch (event.getActionMasked()) {
5206 case MotionEvent.ACTION_DOWN:
5207 // Reset the touch word offset and x value when the user
5208 // re-engages the handle.
5209 mTouchWordDelta = 0.0f;
5210 mPrevX = UNSET_X_VALUE;
5211 showMagnifier();
5212 break;
5213
5214 case MotionEvent.ACTION_MOVE:
5215 showMagnifier();
5216 break;
5217
5218 case MotionEvent.ACTION_UP:
5219 case MotionEvent.ACTION_CANCEL:
5220 dismissMagnifier();
5221 break;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005222 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005223
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005224 return superResult;
5225 }
Mady Mellor42390aa2015-07-24 13:08:42 -07005226
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005227 private void positionAndAdjustForCrossingHandles(int offset, boolean fromTouchScreen) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005228 final int anotherHandleOffset =
5229 isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
5230 if ((isStartHandle() && offset >= anotherHandleOffset)
5231 || (!isStartHandle() && offset <= anotherHandleOffset)) {
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005232 mTouchWordDelta = 0.0f;
5233 final Layout layout = mTextView.getLayout();
5234 if (layout != null && offset != anotherHandleOffset) {
5235 final float horiz = getHorizontal(layout, offset);
5236 final float anotherHandleHoriz = getHorizontal(layout, anotherHandleOffset,
5237 !isStartHandle());
5238 final float currentHoriz = getHorizontal(layout, mPreviousOffset);
5239 if (currentHoriz < anotherHandleHoriz && horiz < anotherHandleHoriz
5240 || currentHoriz > anotherHandleHoriz && horiz > anotherHandleHoriz) {
5241 // This handle passes another one as it crossed a direction boundary.
5242 // Don't minimize the selection, but keep the handle at the run boundary.
5243 final int currentOffset = getCurrentCursorOffset();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005244 final int offsetToGetRunRange = isStartHandle()
5245 ? currentOffset : Math.max(currentOffset - 1, 0);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005246 final long range = layout.getRunRange(offsetToGetRunRange);
5247 if (isStartHandle()) {
5248 offset = TextUtils.unpackRangeStartFromLong(range);
5249 } else {
5250 offset = TextUtils.unpackRangeEndFromLong(range);
5251 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005252 positionAtCursorOffset(offset, false, fromTouchScreen);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005253 return;
5254 }
5255 }
Mady Mellor42390aa2015-07-24 13:08:42 -07005256 // Handles can not cross and selection is at least one character.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005257 offset = getNextCursorOffset(anotherHandleOffset, !isStartHandle());
Mady Mellor42390aa2015-07-24 13:08:42 -07005258 }
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005259 positionAtCursorOffset(offset, false, fromTouchScreen);
Mady Mellor42390aa2015-07-24 13:08:42 -07005260 }
5261
Mady Mellor42390aa2015-07-24 13:08:42 -07005262 private boolean positionNearEdgeOfScrollingView(float x, boolean atRtl) {
5263 mTextView.getLocationOnScreen(mTextViewLocation);
5264 boolean nearEdge;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005265 if (atRtl == isStartHandle()) {
Mady Mellor42390aa2015-07-24 13:08:42 -07005266 int rightEdge = mTextViewLocation[0] + mTextView.getWidth()
5267 - mTextView.getPaddingRight();
5268 nearEdge = x > rightEdge - mTextViewEdgeSlop;
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005269 } else {
5270 int leftEdge = mTextViewLocation[0] + mTextView.getPaddingLeft();
5271 nearEdge = x < leftEdge + mTextViewEdgeSlop;
Mady Mellor42390aa2015-07-24 13:08:42 -07005272 }
5273 return nearEdge;
5274 }
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005275
5276 @Override
5277 protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
5278 final int offsetToCheck = isStartHandle() ? offset : Math.max(offset - 1, 0);
5279 return layout.isRtlCharAt(offsetToCheck);
5280 }
5281
5282 @Override
5283 public float getHorizontal(@NonNull Layout layout, int offset) {
5284 return getHorizontal(layout, offset, isStartHandle());
5285 }
5286
5287 private float getHorizontal(@NonNull Layout layout, int offset, boolean startHandle) {
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005288 final int line = layout.getLineForOffset(offset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005289 final int offsetToCheck = startHandle ? offset : Math.max(offset - 1, 0);
5290 final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
5291 final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005292 return (isRtlChar == isRtlParagraph)
Roozbeh Pournader7557a5a2017-04-11 18:34:42 -07005293 ? layout.getPrimaryHorizontal(offset) : layout.getSecondaryHorizontal(offset);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005294 }
5295
5296 @Override
5297 protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
Keisuke Kuroyanagib1b88652016-04-05 16:26:16 +09005298 final float localX = mTextView.convertToLocalHorizontalCoordinate(x);
5299 final int primaryOffset = layout.getOffsetForHorizontal(line, localX, true);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005300 if (!layout.isLevelBoundary(primaryOffset)) {
5301 return primaryOffset;
5302 }
Keisuke Kuroyanagib1b88652016-04-05 16:26:16 +09005303 final int secondaryOffset = layout.getOffsetForHorizontal(line, localX, false);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005304 final int currentOffset = getCurrentCursorOffset();
5305 final int primaryDiff = Math.abs(primaryOffset - currentOffset);
5306 final int secondaryDiff = Math.abs(secondaryOffset - currentOffset);
5307 if (primaryDiff < secondaryDiff) {
5308 return primaryOffset;
5309 } else if (primaryDiff > secondaryDiff) {
5310 return secondaryOffset;
5311 } else {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005312 final int offsetToCheck = isStartHandle()
5313 ? currentOffset : Math.max(currentOffset - 1, 0);
Keisuke Kuroyanagif0bb87b72016-02-08 23:52:55 +09005314 final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
5315 final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
5316 return isRtlChar == isRtlParagraph ? primaryOffset : secondaryOffset;
5317 }
5318 }
Andrei Stingaceanud2eadfa2017-09-22 15:32:13 +01005319
5320 @MagnifierHandleTrigger
5321 protected int getMagnifierHandleTrigger() {
5322 return isStartHandle()
5323 ? MagnifierHandleTrigger.SELECTION_START
5324 : MagnifierHandleTrigger.SELECTION_END;
5325 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005326 }
5327
Mady Mellorcc65c372015-06-17 09:25:19 -07005328 private int getCurrentLineAdjustedForSlop(Layout layout, int prevLine, float y) {
Mady Mellor80679072015-07-09 16:05:36 -07005329 final int trueLine = mTextView.getLineAtCoordinate(y);
Mady Mellorcc65c372015-06-17 09:25:19 -07005330 if (layout == null || prevLine > layout.getLineCount()
5331 || layout.getLineCount() <= 0 || prevLine < 0) {
5332 // Invalid parameters, just return whatever line is at y.
Mady Mellor80679072015-07-09 16:05:36 -07005333 return trueLine;
5334 }
5335
5336 if (Math.abs(trueLine - prevLine) >= 2) {
5337 // Only stick to lines if we're within a line of the previous selection.
5338 return trueLine;
Mady Mellorcc65c372015-06-17 09:25:19 -07005339 }
5340
5341 final float verticalOffset = mTextView.viewportToContentVerticalOffset();
5342 final int lineCount = layout.getLineCount();
5343 final float slop = mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS;
5344
5345 final float firstLineTop = layout.getLineTop(0) + verticalOffset;
5346 final float prevLineTop = layout.getLineTop(prevLine) + verticalOffset;
5347 final float yTopBound = Math.max(prevLineTop - slop, firstLineTop + slop);
5348
5349 final float lastLineBottom = layout.getLineBottom(lineCount - 1) + verticalOffset;
5350 final float prevLineBottom = layout.getLineBottom(prevLine) + verticalOffset;
5351 final float yBottomBound = Math.min(prevLineBottom + slop, lastLineBottom - slop);
5352
5353 // Determine if we've moved lines based on y position and previous line.
5354 int currLine;
5355 if (y <= yTopBound) {
5356 currLine = Math.max(prevLine - 1, 0);
5357 } else if (y >= yBottomBound) {
5358 currLine = Math.min(prevLine + 1, lineCount - 1);
5359 } else {
5360 currLine = prevLine;
5361 }
5362 return currLine;
5363 }
5364
Gilles Debunned88876a2012-03-16 17:34:04 -07005365 /**
5366 * A CursorController instance can be used to control a cursor in the text.
5367 */
5368 private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
5369 /**
5370 * Makes the cursor controller visible on screen.
5371 * See also {@link #hide()}.
5372 */
5373 public void show();
5374
5375 /**
5376 * Hide the cursor controller from screen.
5377 * See also {@link #show()}.
5378 */
5379 public void hide();
5380
5381 /**
5382 * Called when the view is detached from window. Perform house keeping task, such as
5383 * stopping Runnable thread that would otherwise keep a reference on the context, thus
5384 * preventing the activity from being recycled.
5385 */
5386 public void onDetached();
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005387
5388 public boolean isCursorBeingModified();
5389
5390 public boolean isActive();
Gilles Debunned88876a2012-03-16 17:34:04 -07005391 }
5392
5393 private class InsertionPointCursorController implements CursorController {
5394 private InsertionHandleView mHandle;
5395
5396 public void show() {
5397 getHandle().show();
Andrei Stingaceanu35c550c2015-05-07 16:49:49 +01005398
5399 if (mSelectionModifierCursorController != null) {
5400 mSelectionModifierCursorController.hide();
5401 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005402 }
5403
Gilles Debunned88876a2012-03-16 17:34:04 -07005404 public void hide() {
5405 if (mHandle != null) {
5406 mHandle.hide();
5407 }
5408 }
5409
5410 public void onTouchModeChanged(boolean isInTouchMode) {
5411 if (!isInTouchMode) {
5412 hide();
5413 }
5414 }
5415
5416 private InsertionHandleView getHandle() {
5417 if (mSelectHandleCenter == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08005418 mSelectHandleCenter = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07005419 mTextView.mTextSelectHandleRes);
5420 }
5421 if (mHandle == null) {
5422 mHandle = new InsertionHandleView(mSelectHandleCenter);
5423 }
5424 return mHandle;
5425 }
5426
5427 @Override
5428 public void onDetached() {
5429 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
5430 observer.removeOnTouchModeChangeListener(this);
5431
5432 if (mHandle != null) mHandle.onDetached();
5433 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005434
5435 @Override
5436 public boolean isCursorBeingModified() {
5437 return mHandle != null && mHandle.isDragging();
5438 }
5439
5440 @Override
5441 public boolean isActive() {
5442 return mHandle != null && mHandle.isShowing();
5443 }
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09005444
5445 public void invalidateHandle() {
5446 if (mHandle != null) {
5447 mHandle.invalidate();
5448 }
5449 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005450 }
5451
5452 class SelectionModifierCursorController implements CursorController {
Gilles Debunned88876a2012-03-16 17:34:04 -07005453 // The cursor controller handles, lazily created when shown.
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005454 private SelectionHandleView mStartHandle;
5455 private SelectionHandleView mEndHandle;
Gilles Debunned88876a2012-03-16 17:34:04 -07005456 // The offsets of that last touch down event. Remembered to start selection there.
5457 private int mMinTouchOffset, mMaxTouchOffset;
5458
Gilles Debunned88876a2012-03-16 17:34:04 -07005459 private float mDownPositionX, mDownPositionY;
5460 private boolean mGestureStayedInTapRegion;
5461
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005462 // Where the user first starts the drag motion.
5463 private int mStartOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005464
Mady Mellor7a936442015-05-20 10:05:52 -07005465 private boolean mHaventMovedEnoughToStartDrag;
Mady Mellorf9f8aeb2015-06-17 09:46:01 -07005466 // The line that a selection happened most recently with the drag accelerator.
5467 private int mLineSelectionIsOn = -1;
5468 // Whether the drag accelerator has selected past the initial line.
5469 private boolean mSwitchedLines = false;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005470
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005471 // Indicates the drag accelerator mode that the user is currently using.
5472 private int mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
5473 // Drag accelerator is inactive.
5474 private static final int DRAG_ACCELERATOR_MODE_INACTIVE = 0;
5475 // Character based selection by dragging. Only for mouse.
5476 private static final int DRAG_ACCELERATOR_MODE_CHARACTER = 1;
5477 // Word based selection by dragging. Enabled after long pressing or double tapping.
5478 private static final int DRAG_ACCELERATOR_MODE_WORD = 2;
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005479 // Paragraph based selection by dragging. Enabled after mouse triple click.
5480 private static final int DRAG_ACCELERATOR_MODE_PARAGRAPH = 3;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005481
Gilles Debunned88876a2012-03-16 17:34:04 -07005482 SelectionModifierCursorController() {
5483 resetTouchOffsets();
5484 }
5485
5486 public void show() {
5487 if (mTextView.isInBatchEditMode()) {
5488 return;
5489 }
5490 initDrawables();
5491 initHandles();
Gilles Debunned88876a2012-03-16 17:34:04 -07005492 }
5493
5494 private void initDrawables() {
5495 if (mSelectHandleLeft == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08005496 mSelectHandleLeft = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07005497 mTextView.mTextSelectHandleLeftRes);
5498 }
5499 if (mSelectHandleRight == null) {
Alan Viverette8eea3ea2014-02-03 18:40:20 -08005500 mSelectHandleRight = mTextView.getContext().getDrawable(
Gilles Debunned88876a2012-03-16 17:34:04 -07005501 mTextView.mTextSelectHandleRightRes);
5502 }
5503 }
5504
5505 private void initHandles() {
5506 // Lazy object creation has to be done before updatePosition() is called.
5507 if (mStartHandle == null) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005508 mStartHandle = new SelectionHandleView(mSelectHandleLeft, mSelectHandleRight,
5509 com.android.internal.R.id.selection_start_handle,
5510 HANDLE_TYPE_SELECTION_START);
Gilles Debunned88876a2012-03-16 17:34:04 -07005511 }
5512 if (mEndHandle == null) {
Keisuke Kuroyanagi5d7657e2015-11-26 13:59:05 +09005513 mEndHandle = new SelectionHandleView(mSelectHandleRight, mSelectHandleLeft,
5514 com.android.internal.R.id.selection_end_handle,
5515 HANDLE_TYPE_SELECTION_END);
Gilles Debunned88876a2012-03-16 17:34:04 -07005516 }
5517
5518 mStartHandle.show();
5519 mEndHandle.show();
5520
Gilles Debunned88876a2012-03-16 17:34:04 -07005521 hideInsertionPointCursorController();
5522 }
5523
5524 public void hide() {
5525 if (mStartHandle != null) mStartHandle.hide();
5526 if (mEndHandle != null) mEndHandle.hide();
5527 }
5528
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005529 public void enterDrag(int dragAcceleratorMode) {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005530 // Just need to init the handles / hide insertion cursor.
5531 show();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005532 mDragAcceleratorMode = dragAcceleratorMode;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005533 // Start location of selection.
5534 mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX,
5535 mLastDownPositionY);
Mady Mellorf9f8aeb2015-06-17 09:46:01 -07005536 mLineSelectionIsOn = mTextView.getLineAtCoordinate(mLastDownPositionY);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005537 // Don't show the handles until user has lifted finger.
5538 hide();
5539
5540 // This stops scrolling parents from intercepting the touch event, allowing
5541 // the user to continue dragging across the screen to select text; TextView will
5542 // scroll as necessary.
5543 mTextView.getParent().requestDisallowInterceptTouchEvent(true);
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005544 mTextView.cancelLongPress();
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005545 }
5546
Gilles Debunned88876a2012-03-16 17:34:04 -07005547 public void onTouchEvent(MotionEvent event) {
5548 // This is done even when the View does not have focus, so that long presses can start
5549 // selection and tap can move cursor from this tap position.
Mady Mellor7a936442015-05-20 10:05:52 -07005550 final float eventX = event.getX();
5551 final float eventY = event.getY();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005552 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
Gilles Debunned88876a2012-03-16 17:34:04 -07005553 switch (event.getActionMasked()) {
5554 case MotionEvent.ACTION_DOWN:
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005555 if (extractedTextModeWillBeStarted()) {
5556 // Prevent duplicating the selection handles until the mode starts.
5557 hide();
5558 } else {
5559 // Remember finger down position, to be able to start selection from there.
5560 mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(
5561 eventX, eventY);
Gilles Debunned88876a2012-03-16 17:34:04 -07005562
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005563 // Double tap detection
5564 if (mGestureStayedInTapRegion) {
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09005565 if (mTapState == TAP_STATE_DOUBLE_TAP
5566 || mTapState == TAP_STATE_TRIPLE_CLICK) {
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005567 final float deltaX = eventX - mDownPositionX;
5568 final float deltaY = eventY - mDownPositionY;
5569 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005570
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005571 ViewConfiguration viewConfiguration = ViewConfiguration.get(
5572 mTextView.getContext());
5573 int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
5574 boolean stayedInArea =
5575 distanceSquared < doubleTapSlop * doubleTapSlop;
Gilles Debunned88876a2012-03-16 17:34:04 -07005576
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005577 if (stayedInArea && (isMouse || isPositionOnText(eventX, eventY))) {
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09005578 if (mTapState == TAP_STATE_DOUBLE_TAP) {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005579 selectCurrentWordAndStartDrag();
Keisuke Kuroyanagi155aecb2015-11-05 19:10:07 +09005580 } else if (mTapState == TAP_STATE_TRIPLE_CLICK) {
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005581 selectCurrentParagraphAndStartDrag();
5582 }
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005583 mDiscardNextActionUp = true;
5584 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005585 }
5586 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005587
Andrei Stingaceanu838307272015-06-19 17:58:47 +01005588 mDownPositionX = eventX;
5589 mDownPositionY = eventY;
5590 mGestureStayedInTapRegion = true;
5591 mHaventMovedEnoughToStartDrag = true;
5592 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005593 break;
5594
5595 case MotionEvent.ACTION_POINTER_DOWN:
5596 case MotionEvent.ACTION_POINTER_UP:
5597 // Handle multi-point gestures. Keep min and max offset positions.
5598 // Only activated for devices that correctly handle multi-touch.
5599 if (mTextView.getContext().getPackageManager().hasSystemFeature(
5600 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
5601 updateMinAndMaxOffsets(event);
5602 }
5603 break;
5604
5605 case MotionEvent.ACTION_MOVE:
Mady Mellor7a936442015-05-20 10:05:52 -07005606 final ViewConfiguration viewConfig = ViewConfiguration.get(
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005607 mTextView.getContext());
Mady Mellor7a936442015-05-20 10:05:52 -07005608 final int touchSlop = viewConfig.getScaledTouchSlop();
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005609
Mady Mellor7a936442015-05-20 10:05:52 -07005610 if (mGestureStayedInTapRegion || mHaventMovedEnoughToStartDrag) {
5611 final float deltaX = eventX - mDownPositionX;
5612 final float deltaY = eventY - mDownPositionY;
Gilles Debunned88876a2012-03-16 17:34:04 -07005613 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
5614
Mady Mellor7a936442015-05-20 10:05:52 -07005615 if (mGestureStayedInTapRegion) {
5616 int doubleTapTouchSlop = viewConfig.getScaledDoubleTapTouchSlop();
5617 mGestureStayedInTapRegion =
5618 distanceSquared <= doubleTapTouchSlop * doubleTapTouchSlop;
5619 }
5620 if (mHaventMovedEnoughToStartDrag) {
5621 // We don't start dragging until the user has moved enough.
5622 mHaventMovedEnoughToStartDrag =
5623 distanceSquared <= touchSlop * touchSlop;
Gilles Debunned88876a2012-03-16 17:34:04 -07005624 }
5625 }
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005626
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005627 if (isMouse && !isDragAcceleratorActive()) {
5628 final int offset = mTextView.getOffsetForPosition(eventX, eventY);
Keisuke Kuroyanagie8760852015-12-21 18:30:19 +09005629 if (mTextView.hasSelection()
5630 && (!mHaventMovedEnoughToStartDrag || mStartOffset != offset)
5631 && offset >= mTextView.getSelectionStart()
5632 && offset <= mTextView.getSelectionEnd()) {
5633 startDragAndDrop();
5634 break;
5635 }
5636
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005637 if (mStartOffset != offset) {
5638 // Start character based drag accelerator.
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005639 stopTextActionMode();
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005640 enterDrag(DRAG_ACCELERATOR_MODE_CHARACTER);
5641 mDiscardNextActionUp = true;
5642 mHaventMovedEnoughToStartDrag = false;
5643 }
5644 }
5645
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005646 if (mStartHandle != null && mStartHandle.isShowing()) {
5647 // Don't do the drag if the handles are showing already.
5648 break;
5649 }
5650
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005651 updateSelection(event);
Gilles Debunned88876a2012-03-16 17:34:04 -07005652 break;
5653
5654 case MotionEvent.ACTION_UP:
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005655 if (!isDragAcceleratorActive()) {
5656 break;
5657 }
5658 updateSelection(event);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005659
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005660 // No longer dragging to select text, let the parent intercept events.
5661 mTextView.getParent().requestDisallowInterceptTouchEvent(false);
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005662
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005663 // No longer the first dragging motion, reset.
5664 resetDragAcceleratorState();
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09005665
5666 if (mTextView.hasSelection()) {
Abodunrinwa Toki66c16272017-05-03 20:22:55 +01005667 // Drag selection should not be adjusted by the text classifier.
5668 startSelectionActionModeAsync(mHaventMovedEnoughToStartDrag);
Keisuke Kuroyanagic477b582016-03-15 15:38:40 +09005669 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005670 break;
5671 }
5672 }
5673
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005674 private void updateSelection(MotionEvent event) {
5675 if (mTextView.getLayout() != null) {
5676 switch (mDragAcceleratorMode) {
5677 case DRAG_ACCELERATOR_MODE_CHARACTER:
5678 updateCharacterBasedSelection(event);
5679 break;
5680 case DRAG_ACCELERATOR_MODE_WORD:
5681 updateWordBasedSelection(event);
5682 break;
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005683 case DRAG_ACCELERATOR_MODE_PARAGRAPH:
5684 updateParagraphBasedSelection(event);
5685 break;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005686 }
5687 }
5688 }
5689
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005690 /**
5691 * If the TextView allows text selection, selects the current paragraph and starts a drag.
5692 *
5693 * @return true if the drag was started.
5694 */
5695 private boolean selectCurrentParagraphAndStartDrag() {
5696 if (mInsertionActionModeRunnable != null) {
5697 mTextView.removeCallbacks(mInsertionActionModeRunnable);
5698 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005699 stopTextActionMode();
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005700 if (!selectCurrentParagraph()) {
5701 return false;
5702 }
5703 enterDrag(SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_PARAGRAPH);
5704 return true;
5705 }
5706
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005707 private void updateCharacterBasedSelection(MotionEvent event) {
5708 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005709 updateSelectionInternal(mStartOffset, offset,
5710 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005711 }
5712
5713 private void updateWordBasedSelection(MotionEvent event) {
5714 if (mHaventMovedEnoughToStartDrag) {
5715 return;
5716 }
5717 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
5718 final ViewConfiguration viewConfig = ViewConfiguration.get(
5719 mTextView.getContext());
5720 final float eventX = event.getX();
5721 final float eventY = event.getY();
5722 final int currLine;
5723 if (isMouse) {
5724 // No need to offset the y coordinate for mouse input.
5725 currLine = mTextView.getLineAtCoordinate(eventY);
5726 } else {
5727 float y = eventY;
5728 if (mSwitchedLines) {
5729 // Offset the finger by the same vertical offset as the handles.
5730 // This improves visibility of the content being selected by
5731 // shifting the finger below the content, this is applied once
5732 // the user has switched lines.
5733 final int touchSlop = viewConfig.getScaledTouchSlop();
5734 final float fingerOffset = (mStartHandle != null)
5735 ? mStartHandle.getIdealVerticalOffset()
5736 : touchSlop;
5737 y = eventY - fingerOffset;
5738 }
5739
5740 currLine = getCurrentLineAdjustedForSlop(mTextView.getLayout(), mLineSelectionIsOn,
5741 y);
5742 if (!mSwitchedLines && currLine != mLineSelectionIsOn) {
5743 // Break early here, we want to offset the finger position from
5744 // the selection highlight, once the user moved their finger
5745 // to a different line we should apply the offset and *not* switch
5746 // lines until recomputing the position with the finger offset.
5747 mSwitchedLines = true;
5748 return;
5749 }
5750 }
5751
5752 int startOffset;
5753 int offset = mTextView.getOffsetAtCoordinate(currLine, eventX);
5754 // Snap to word boundaries.
5755 if (mStartOffset < offset) {
5756 // Expanding with end handle.
5757 offset = getWordEnd(offset);
5758 startOffset = getWordStart(mStartOffset);
5759 } else {
5760 // Expanding with start handle.
5761 offset = getWordStart(offset);
5762 startOffset = getWordEnd(mStartOffset);
Keisuke Kuroyanagi133dfc02016-07-21 18:07:23 +09005763 if (startOffset == offset) {
5764 offset = getNextCursorOffset(offset, false);
5765 }
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005766 }
5767 mLineSelectionIsOn = currLine;
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005768 updateSelectionInternal(startOffset, offset,
5769 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005770 }
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005771
5772 private void updateParagraphBasedSelection(MotionEvent event) {
5773 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
5774
5775 final int start = Math.min(offset, mStartOffset);
5776 final int end = Math.max(offset, mStartOffset);
5777 final long paragraphsRange = getParagraphsRange(start, end);
5778 final int selectionStart = TextUtils.unpackRangeStartFromLong(paragraphsRange);
5779 final int selectionEnd = TextUtils.unpackRangeEndFromLong(paragraphsRange);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005780 updateSelectionInternal(selectionStart, selectionEnd,
5781 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
5782 }
5783
5784 private void updateSelectionInternal(int selectionStart, int selectionEnd,
5785 boolean fromTouchScreen) {
5786 final boolean performHapticFeedback = fromTouchScreen && mHapticTextHandleEnabled
5787 && ((mTextView.getSelectionStart() != selectionStart)
5788 || (mTextView.getSelectionEnd() != selectionEnd));
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005789 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
Yohei Yukawac9cd9db2017-06-19 18:27:34 -07005790 if (performHapticFeedback) {
5791 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
5792 }
Keisuke Kuroyanagi4368d052015-11-05 18:51:00 +09005793 }
5794
Gilles Debunned88876a2012-03-16 17:34:04 -07005795 /**
5796 * @param event
5797 */
5798 private void updateMinAndMaxOffsets(MotionEvent event) {
5799 int pointerCount = event.getPointerCount();
5800 for (int index = 0; index < pointerCount; index++) {
5801 int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
5802 if (offset < mMinTouchOffset) mMinTouchOffset = offset;
5803 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
5804 }
5805 }
5806
5807 public int getMinTouchOffset() {
5808 return mMinTouchOffset;
5809 }
5810
5811 public int getMaxTouchOffset() {
5812 return mMaxTouchOffset;
5813 }
5814
5815 public void resetTouchOffsets() {
5816 mMinTouchOffset = mMaxTouchOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005817 resetDragAcceleratorState();
5818 }
5819
5820 private void resetDragAcceleratorState() {
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005821 mStartOffset = -1;
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005822 mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
Mady Mellorf9f8aeb2015-06-17 09:46:01 -07005823 mSwitchedLines = false;
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005824 final int selectionStart = mTextView.getSelectionStart();
5825 final int selectionEnd = mTextView.getSelectionEnd();
5826 if (selectionStart > selectionEnd) {
5827 Selection.setSelection((Spannable) mTextView.getText(),
5828 selectionEnd, selectionStart);
5829 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005830 }
5831
5832 /**
5833 * @return true iff this controller is currently used to move the selection start.
5834 */
5835 public boolean isSelectionStartDragged() {
5836 return mStartHandle != null && mStartHandle.isDragging();
5837 }
5838
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005839 @Override
5840 public boolean isCursorBeingModified() {
5841 return isDragAcceleratorActive() || isSelectionStartDragged()
5842 || (mEndHandle != null && mEndHandle.isDragging());
5843 }
5844
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005845 /**
5846 * @return true if the user is selecting text using the drag accelerator.
5847 */
5848 public boolean isDragAcceleratorActive() {
Keisuke Kuroyanagi97af6732015-12-04 16:56:38 -08005849 return mDragAcceleratorMode != DRAG_ACCELERATOR_MODE_INACTIVE;
Mady Mellor2ff2cd82015-03-02 10:37:01 -08005850 }
5851
Gilles Debunned88876a2012-03-16 17:34:04 -07005852 public void onTouchModeChanged(boolean isInTouchMode) {
5853 if (!isInTouchMode) {
5854 hide();
5855 }
5856 }
5857
5858 @Override
5859 public void onDetached() {
5860 final ViewTreeObserver observer = mTextView.getViewTreeObserver();
5861 observer.removeOnTouchModeChangeListener(this);
5862
5863 if (mStartHandle != null) mStartHandle.onDetached();
5864 if (mEndHandle != null) mEndHandle.onDetached();
5865 }
Keisuke Kuroyanagibec97152016-02-24 18:40:09 -08005866
5867 @Override
5868 public boolean isActive() {
5869 return mStartHandle != null && mStartHandle.isShowing();
5870 }
Keisuke Kuroyanagic14e1272016-04-05 15:39:24 +09005871
5872 public void invalidateHandles() {
5873 if (mStartHandle != null) {
5874 mStartHandle.invalidate();
5875 }
5876 if (mEndHandle != null) {
5877 mEndHandle.invalidate();
5878 }
5879 }
Gilles Debunned88876a2012-03-16 17:34:04 -07005880 }
5881
5882 private class CorrectionHighlighter {
5883 private final Path mPath = new Path();
Chris Craik6a49dde2015-05-12 10:28:14 -07005884 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
Gilles Debunned88876a2012-03-16 17:34:04 -07005885 private int mStart, mEnd;
5886 private long mFadingStartTime;
5887 private RectF mTempRectF;
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005888 private static final int FADE_OUT_DURATION = 400;
Gilles Debunned88876a2012-03-16 17:34:04 -07005889
5890 public CorrectionHighlighter() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005891 mPaint.setCompatibilityScaling(
5892 mTextView.getResources().getCompatibilityInfo().applicationScale);
Gilles Debunned88876a2012-03-16 17:34:04 -07005893 mPaint.setStyle(Paint.Style.FILL);
5894 }
5895
5896 public void highlight(CorrectionInfo info) {
5897 mStart = info.getOffset();
5898 mEnd = mStart + info.getNewText().length();
5899 mFadingStartTime = SystemClock.uptimeMillis();
5900
5901 if (mStart < 0 || mEnd < 0) {
5902 stopAnimation();
5903 }
5904 }
5905
5906 public void draw(Canvas canvas, int cursorOffsetVertical) {
5907 if (updatePath() && updatePaint()) {
5908 if (cursorOffsetVertical != 0) {
5909 canvas.translate(0, cursorOffsetVertical);
5910 }
5911
5912 canvas.drawPath(mPath, mPaint);
5913
5914 if (cursorOffsetVertical != 0) {
5915 canvas.translate(0, -cursorOffsetVertical);
5916 }
5917 invalidate(true); // TODO invalidate cursor region only
5918 } else {
5919 stopAnimation();
5920 invalidate(false); // TODO invalidate cursor region only
5921 }
5922 }
5923
5924 private boolean updatePaint() {
5925 final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
5926 if (duration > FADE_OUT_DURATION) return false;
5927
5928 final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
5929 final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
Aurimas Liutikasee62c292016-07-21 15:05:40 -07005930 final int color = (mTextView.mHighlightColor & 0x00FFFFFF)
5931 + ((int) (highlightColorAlpha * coef) << 24);
Gilles Debunned88876a2012-03-16 17:34:04 -07005932 mPaint.setColor(color);
5933 return true;
5934 }
5935
5936 private boolean updatePath() {
5937 final Layout layout = mTextView.getLayout();
5938 if (layout == null) return false;
5939
5940 // Update in case text is edited while the animation is run
5941 final int length = mTextView.getText().length();
5942 int start = Math.min(length, mStart);
5943 int end = Math.min(length, mEnd);
5944
5945 mPath.reset();
5946 layout.getSelectionPath(start, end, mPath);
5947 return true;
5948 }
5949
5950 private void invalidate(boolean delayed) {
5951 if (mTextView.getLayout() == null) return;
5952
5953 if (mTempRectF == null) mTempRectF = new RectF();
5954 mPath.computeBounds(mTempRectF, false);
5955
5956 int left = mTextView.getCompoundPaddingLeft();
5957 int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
5958
5959 if (delayed) {
5960 mTextView.postInvalidateOnAnimation(
5961 left + (int) mTempRectF.left, top + (int) mTempRectF.top,
5962 left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
5963 } else {
5964 mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
5965 (int) mTempRectF.right, (int) mTempRectF.bottom);
5966 }
5967 }
5968
5969 private void stopAnimation() {
5970 Editor.this.mCorrectionHighlighter = null;
5971 }
5972 }
5973
5974 private static class ErrorPopup extends PopupWindow {
5975 private boolean mAbove = false;
5976 private final TextView mView;
5977 private int mPopupInlineErrorBackgroundId = 0;
5978 private int mPopupInlineErrorAboveBackgroundId = 0;
5979
5980 ErrorPopup(TextView v, int width, int height) {
5981 super(v, width, height);
5982 mView = v;
5983 // 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 -08005984 // shown and positioned. Initialized with below background, which should have
Gilles Debunned88876a2012-03-16 17:34:04 -07005985 // dimensions identical to the above version for this to work (and is more likely).
5986 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
5987 com.android.internal.R.styleable.Theme_errorMessageBackground);
5988 mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
5989 }
5990
5991 void fixDirection(boolean above) {
5992 mAbove = above;
5993
5994 if (above) {
5995 mPopupInlineErrorAboveBackgroundId =
5996 getResourceId(mPopupInlineErrorAboveBackgroundId,
5997 com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
5998 } else {
5999 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
6000 com.android.internal.R.styleable.Theme_errorMessageBackground);
6001 }
6002
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006003 mView.setBackgroundResource(
6004 above ? mPopupInlineErrorAboveBackgroundId : mPopupInlineErrorBackgroundId);
Gilles Debunned88876a2012-03-16 17:34:04 -07006005 }
6006
6007 private int getResourceId(int currentId, int index) {
6008 if (currentId == 0) {
6009 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
6010 R.styleable.Theme);
6011 currentId = styledAttributes.getResourceId(index, 0);
6012 styledAttributes.recycle();
6013 }
6014 return currentId;
6015 }
6016
6017 @Override
6018 public void update(int x, int y, int w, int h, boolean force) {
6019 super.update(x, y, w, h, force);
6020
6021 boolean above = isAboveAnchor();
6022 if (above != mAbove) {
6023 fixDirection(above);
6024 }
6025 }
6026 }
6027
6028 static class InputContentType {
6029 int imeOptions = EditorInfo.IME_NULL;
6030 String privateImeOptions;
6031 CharSequence imeActionLabel;
6032 int imeActionId;
6033 Bundle extras;
6034 OnEditorActionListener onEditorActionListener;
6035 boolean enterDown;
Yohei Yukawad469f212016-01-21 12:38:09 -08006036 LocaleList imeHintLocales;
Gilles Debunned88876a2012-03-16 17:34:04 -07006037 }
6038
6039 static class InputMethodState {
Gilles Debunnec62589c2012-04-12 14:50:23 -07006040 ExtractedTextRequest mExtractedTextRequest;
6041 final ExtractedText mExtractedText = new ExtractedText();
Gilles Debunned88876a2012-03-16 17:34:04 -07006042 int mBatchEditNesting;
6043 boolean mCursorChanged;
6044 boolean mSelectionModeChanged;
6045 boolean mContentChanged;
6046 int mChangedStart, mChangedEnd, mChangedDelta;
6047 }
Satoshi Kataoka0e3849a2012-12-13 14:37:19 +09006048
James Cookf59152c2015-02-26 18:03:58 -08006049 /**
James Cook471559f2015-02-27 10:31:20 -08006050 * @return True iff (start, end) is a valid range within the text.
6051 */
6052 private static boolean isValidRange(CharSequence text, int start, int end) {
6053 return 0 <= start && start <= end && end <= text.length();
6054 }
6055
Seigo Nonakaa60160b2015-08-19 12:38:35 -07006056 @VisibleForTesting
6057 public SuggestionsPopupWindow getSuggestionsPopupWindowForTesting() {
6058 return mSuggestionsPopupWindow;
6059 }
6060
James Cook471559f2015-02-27 10:31:20 -08006061 /**
James Cookf59152c2015-02-26 18:03:58 -08006062 * An InputFilter that monitors text input to maintain undo history. It does not modify the
6063 * text being typed (and hence always returns null from the filter() method).
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006064 *
6065 * TODO: Make this span aware.
James Cookf59152c2015-02-26 18:03:58 -08006066 */
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006067 public static class UndoInputFilter implements InputFilter {
James Cookf59152c2015-02-26 18:03:58 -08006068 private final Editor mEditor;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006069
James Cook48e0fac2015-02-25 15:44:51 -08006070 // Whether the current filter pass is directly caused by an end-user text edit.
6071 private boolean mIsUserEdit;
6072
James Cookd2026682015-03-03 14:40:14 -08006073 // Whether the text field is handling an IME composition. Must be parceled in case the user
6074 // rotates the screen during composition.
6075 private boolean mHasComposition;
James Cook48e0fac2015-02-25 15:44:51 -08006076
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006077 // Whether the user is expanding or shortening the text
6078 private boolean mExpanding;
6079
6080 // Whether the previous edit operation was in the current batch edit.
6081 private boolean mPreviousOperationWasInSameBatchEdit;
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08006082
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006083 public UndoInputFilter(Editor editor) {
6084 mEditor = editor;
6085 }
6086
James Cookd2026682015-03-03 14:40:14 -08006087 public void saveInstanceState(Parcel parcel) {
6088 parcel.writeInt(mIsUserEdit ? 1 : 0);
6089 parcel.writeInt(mHasComposition ? 1 : 0);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006090 parcel.writeInt(mExpanding ? 1 : 0);
6091 parcel.writeInt(mPreviousOperationWasInSameBatchEdit ? 1 : 0);
James Cookd2026682015-03-03 14:40:14 -08006092 }
6093
6094 public void restoreInstanceState(Parcel parcel) {
6095 mIsUserEdit = parcel.readInt() != 0;
6096 mHasComposition = parcel.readInt() != 0;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006097 mExpanding = parcel.readInt() != 0;
6098 mPreviousOperationWasInSameBatchEdit = parcel.readInt() != 0;
Keisuke Kuroyanagifae45782016-02-24 18:53:00 -08006099 }
6100
James Cook48e0fac2015-02-25 15:44:51 -08006101 /**
6102 * Signals that a user-triggered edit is starting.
6103 */
6104 public void beginBatchEdit() {
6105 if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit");
6106 mIsUserEdit = true;
James Cook48e0fac2015-02-25 15:44:51 -08006107 }
6108
6109 public void endBatchEdit() {
6110 if (DEBUG_UNDO) Log.d(TAG, "endBatchEdit");
6111 mIsUserEdit = false;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006112 mPreviousOperationWasInSameBatchEdit = false;
James Cook48e0fac2015-02-25 15:44:51 -08006113 }
6114
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006115 @Override
6116 public CharSequence filter(CharSequence source, int start, int end,
6117 Spanned dest, int dstart, int dend) {
6118 if (DEBUG_UNDO) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006119 Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ") "
6120 + "dest=" + dest + " (" + dstart + "-" + dend + ")");
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006121 }
James Cookf1dad1e2015-02-27 11:00:01 -08006122
James Cook48e0fac2015-02-25 15:44:51 -08006123 // Check to see if this edit should be tracked for undo.
6124 if (!canUndoEdit(source, start, end, dest, dstart, dend)) {
James Cookf1dad1e2015-02-27 11:00:01 -08006125 return null;
6126 }
6127
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006128 final boolean hadComposition = mHasComposition;
6129 mHasComposition = isComposition(source);
6130 final boolean wasExpanding = mExpanding;
6131 boolean shouldCreateSeparateState = false;
6132 if ((end - start) != (dend - dstart)) {
6133 mExpanding = (end - start) > (dend - dstart);
6134 if (hadComposition && mExpanding != wasExpanding) {
6135 shouldCreateSeparateState = true;
6136 }
James Cookd2026682015-03-03 14:40:14 -08006137 }
6138
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006139 // Handle edit.
6140 handleEdit(source, start, end, dest, dstart, dend, shouldCreateSeparateState);
James Cookd2026682015-03-03 14:40:14 -08006141 return null;
6142 }
6143
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09006144 void freezeLastEdit() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006145 mEditor.mUndoManager.beginUpdate("Edit text");
6146 EditOperation lastEdit = getLastEdit();
6147 if (lastEdit != null) {
6148 lastEdit.mFrozen = true;
James Cookd2026682015-03-03 14:40:14 -08006149 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006150 mEditor.mUndoManager.endUpdate();
James Cookd2026682015-03-03 14:40:14 -08006151 }
6152
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006153 @Retention(RetentionPolicy.SOURCE)
Jeff Sharkeyce8db992017-12-13 20:05:05 -07006154 @IntDef(prefix = { "MERGE_EDIT_MODE_" }, value = {
6155 MERGE_EDIT_MODE_FORCE_MERGE,
6156 MERGE_EDIT_MODE_NEVER_MERGE,
6157 MERGE_EDIT_MODE_NORMAL
6158 })
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006159 private @interface MergeMode {}
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006160 private static final int MERGE_EDIT_MODE_FORCE_MERGE = 0;
6161 private static final int MERGE_EDIT_MODE_NEVER_MERGE = 1;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006162 /** Use {@link EditOperation#mergeWith} to merge */
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006163 private static final int MERGE_EDIT_MODE_NORMAL = 2;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006164
6165 private void handleEdit(CharSequence source, int start, int end,
6166 Spanned dest, int dstart, int dend, boolean shouldCreateSeparateState) {
James Cook48e0fac2015-02-25 15:44:51 -08006167 // An application may install a TextWatcher to provide additional modifications after
6168 // the initial input filters run (e.g. a credit card formatter that adds spaces to a
6169 // string). This results in multiple filter() calls for what the user considers to be
6170 // a single operation. Always undo the whole set of changes in one step.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006171 @MergeMode
6172 final int mergeMode;
6173 if (isInTextWatcher() || mPreviousOperationWasInSameBatchEdit) {
6174 mergeMode = MERGE_EDIT_MODE_FORCE_MERGE;
6175 } else if (shouldCreateSeparateState) {
6176 mergeMode = MERGE_EDIT_MODE_NEVER_MERGE;
6177 } else {
6178 mergeMode = MERGE_EDIT_MODE_NORMAL;
6179 }
James Cook471559f2015-02-27 10:31:20 -08006180 // Build a new operation with all the information from this edit.
James Cookd2026682015-03-03 14:40:14 -08006181 String newText = TextUtils.substring(source, start, end);
6182 String oldText = TextUtils.substring(dest, dstart, dend);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006183 EditOperation edit = new EditOperation(mEditor, oldText, dstart, newText,
6184 mHasComposition);
6185 if (mHasComposition && TextUtils.equals(edit.mNewText, edit.mOldText)) {
6186 return;
6187 }
6188 recordEdit(edit, mergeMode);
James Cookd2026682015-03-03 14:40:14 -08006189 }
James Cook471559f2015-02-27 10:31:20 -08006190
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006191 private EditOperation getLastEdit() {
6192 final UndoManager um = mEditor.mUndoManager;
6193 return um.getLastOperation(
6194 EditOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE);
6195 }
James Cook22054252015-03-25 14:04:01 -07006196 /**
6197 * Fetches the last undo operation and checks to see if a new edit should be merged into it.
6198 * If forceMerge is true then the new edit is always merged.
6199 */
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006200 private void recordEdit(EditOperation edit, @MergeMode int mergeMode) {
James Cook471559f2015-02-27 10:31:20 -08006201 // Fetch the last edit operation and attempt to merge in the new edit.
James Cook48e0fac2015-02-25 15:44:51 -08006202 final UndoManager um = mEditor.mUndoManager;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006203 um.beginUpdate("Edit text");
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006204 EditOperation lastEdit = getLastEdit();
James Cook471559f2015-02-27 10:31:20 -08006205 if (lastEdit == null) {
6206 // Add this as the first edit.
6207 if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit);
6208 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006209 } else if (mergeMode == MERGE_EDIT_MODE_FORCE_MERGE) {
James Cook22054252015-03-25 14:04:01 -07006210 // Forced merges take priority because they could be the result of a non-user-edit
6211 // change and this case should not create a new undo operation.
6212 if (DEBUG_UNDO) Log.d(TAG, "filter: force merge " + edit);
6213 lastEdit.forceMergeWith(edit);
James Cook48e0fac2015-02-25 15:44:51 -08006214 } else if (!mIsUserEdit) {
6215 // An application directly modified the Editable outside of a text edit. Treat this
6216 // as a new change and don't attempt to merge.
6217 if (DEBUG_UNDO) Log.d(TAG, "non-user edit, new op " + edit);
6218 um.commitState(mEditor.mUndoOwner);
6219 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006220 } else if (mergeMode == MERGE_EDIT_MODE_NORMAL && lastEdit.mergeWith(edit)) {
James Cook471559f2015-02-27 10:31:20 -08006221 // Merge succeeded, nothing else to do.
6222 if (DEBUG_UNDO) Log.d(TAG, "filter: merge succeeded, created " + lastEdit);
James Cook3ac0bcb2015-02-26 10:53:41 -08006223 } else {
James Cook471559f2015-02-27 10:31:20 -08006224 // Could not merge with the last edit, so commit the last edit and add this edit.
6225 if (DEBUG_UNDO) Log.d(TAG, "filter: merge failed, adding " + edit);
6226 um.commitState(mEditor.mUndoOwner);
6227 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
James Cook3ac0bcb2015-02-26 10:53:41 -08006228 }
Keisuke Kuroyanagifa2d7b12016-07-22 17:09:48 +09006229 mPreviousOperationWasInSameBatchEdit = mIsUserEdit;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006230 um.endUpdate();
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006231 }
James Cook48e0fac2015-02-25 15:44:51 -08006232
6233 private boolean canUndoEdit(CharSequence source, int start, int end,
6234 Spanned dest, int dstart, int dend) {
6235 if (!mEditor.mAllowUndo) {
6236 if (DEBUG_UNDO) Log.d(TAG, "filter: undo is disabled");
6237 return false;
6238 }
6239
6240 if (mEditor.mUndoManager.isInUndo()) {
6241 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping, currently performing undo/redo");
6242 return false;
6243 }
6244
6245 // Text filters run before input operations are applied. However, some input operations
6246 // are invalid and will throw exceptions when applied. This is common in tests. Don't
6247 // attempt to undo invalid operations.
6248 if (!isValidRange(source, start, end) || !isValidRange(dest, dstart, dend)) {
6249 if (DEBUG_UNDO) Log.d(TAG, "filter: invalid op");
6250 return false;
6251 }
6252
6253 // Earlier filters can rewrite input to be a no-op, for example due to a length limit
6254 // on an input field. Skip no-op changes.
6255 if (start == end && dstart == dend) {
6256 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping no-op");
6257 return false;
6258 }
6259
6260 return true;
6261 }
James Cookd2026682015-03-03 14:40:14 -08006262
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006263 private static boolean isComposition(CharSequence source) {
James Cookd2026682015-03-03 14:40:14 -08006264 if (!(source instanceof Spannable)) {
6265 return false;
6266 }
6267 // This is a composition edit if the source has a non-zero-length composing span.
6268 Spannable text = (Spannable) source;
6269 int composeBegin = EditableInputConnection.getComposingSpanStart(text);
6270 int composeEnd = EditableInputConnection.getComposingSpanEnd(text);
6271 return composeBegin < composeEnd;
6272 }
6273
6274 private boolean isInTextWatcher() {
6275 CharSequence text = mEditor.mTextView.getText();
6276 return (text instanceof SpannableStringBuilder)
6277 && ((SpannableStringBuilder) text).getTextWatcherDepth() > 0;
6278 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006279 }
6280
James Cookf59152c2015-02-26 18:03:58 -08006281 /**
6282 * An operation to undo a single "edit" to a text view.
6283 */
James Cook471559f2015-02-27 10:31:20 -08006284 public static class EditOperation extends UndoOperation<Editor> {
6285 private static final int TYPE_INSERT = 0;
6286 private static final int TYPE_DELETE = 1;
6287 private static final int TYPE_REPLACE = 2;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006288
James Cook471559f2015-02-27 10:31:20 -08006289 private int mType;
6290 private String mOldText;
James Cook471559f2015-02-27 10:31:20 -08006291 private String mNewText;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006292 private int mStart;
James Cook471559f2015-02-27 10:31:20 -08006293
6294 private int mOldCursorPos;
6295 private int mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006296 private boolean mFrozen;
6297 private boolean mIsComposition;
James Cook471559f2015-02-27 10:31:20 -08006298
6299 /**
James Cookd2026682015-03-03 14:40:14 -08006300 * Constructs an edit operation from a text input operation on editor that replaces the
James Cook22054252015-03-25 14:04:01 -07006301 * oldText starting at dstart with newText.
James Cook471559f2015-02-27 10:31:20 -08006302 */
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006303 public EditOperation(Editor editor, String oldText, int dstart, String newText,
6304 boolean isComposition) {
James Cook471559f2015-02-27 10:31:20 -08006305 super(editor.mUndoOwner);
James Cookd2026682015-03-03 14:40:14 -08006306 mOldText = oldText;
6307 mNewText = newText;
James Cook471559f2015-02-27 10:31:20 -08006308
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006309 // Determine the type of the edit.
James Cook471559f2015-02-27 10:31:20 -08006310 if (mNewText.length() > 0 && mOldText.length() == 0) {
6311 mType = TYPE_INSERT;
James Cook471559f2015-02-27 10:31:20 -08006312 } else if (mNewText.length() == 0 && mOldText.length() > 0) {
6313 mType = TYPE_DELETE;
James Cook471559f2015-02-27 10:31:20 -08006314 } else {
6315 mType = TYPE_REPLACE;
James Cook471559f2015-02-27 10:31:20 -08006316 }
6317
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006318 mStart = dstart;
James Cook471559f2015-02-27 10:31:20 -08006319 // Store cursor data.
6320 mOldCursorPos = editor.mTextView.getSelectionStart();
James Cookd2026682015-03-03 14:40:14 -08006321 mNewCursorPos = dstart + mNewText.length();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006322 mIsComposition = isComposition;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006323 }
6324
James Cook471559f2015-02-27 10:31:20 -08006325 public EditOperation(Parcel src, ClassLoader loader) {
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006326 super(src, loader);
James Cook471559f2015-02-27 10:31:20 -08006327 mType = src.readInt();
6328 mOldText = src.readString();
James Cook471559f2015-02-27 10:31:20 -08006329 mNewText = src.readString();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006330 mStart = src.readInt();
James Cook471559f2015-02-27 10:31:20 -08006331 mOldCursorPos = src.readInt();
6332 mNewCursorPos = src.readInt();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006333 mFrozen = src.readInt() == 1;
6334 mIsComposition = src.readInt() == 1;
James Cook471559f2015-02-27 10:31:20 -08006335 }
6336
6337 @Override
6338 public void writeToParcel(Parcel dest, int flags) {
6339 dest.writeInt(mType);
6340 dest.writeString(mOldText);
James Cook471559f2015-02-27 10:31:20 -08006341 dest.writeString(mNewText);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006342 dest.writeInt(mStart);
James Cook471559f2015-02-27 10:31:20 -08006343 dest.writeInt(mOldCursorPos);
6344 dest.writeInt(mNewCursorPos);
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006345 dest.writeInt(mFrozen ? 1 : 0);
6346 dest.writeInt(mIsComposition ? 1 : 0);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006347 }
6348
James Cook48e0fac2015-02-25 15:44:51 -08006349 private int getNewTextEnd() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006350 return mStart + mNewText.length();
James Cook48e0fac2015-02-25 15:44:51 -08006351 }
6352
6353 private int getOldTextEnd() {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006354 return mStart + mOldText.length();
James Cook48e0fac2015-02-25 15:44:51 -08006355 }
6356
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006357 @Override
6358 public void commit() {
6359 }
6360
6361 @Override
6362 public void undo() {
James Cook471559f2015-02-27 10:31:20 -08006363 if (DEBUG_UNDO) Log.d(TAG, "undo");
6364 // Remove the new text and insert the old.
James Cook48e0fac2015-02-25 15:44:51 -08006365 Editor editor = getOwnerData();
6366 Editable text = (Editable) editor.mTextView.getText();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006367 modifyText(text, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006368 }
6369
6370 @Override
6371 public void redo() {
James Cook471559f2015-02-27 10:31:20 -08006372 if (DEBUG_UNDO) Log.d(TAG, "redo");
6373 // Remove the old text and insert the new.
James Cook48e0fac2015-02-25 15:44:51 -08006374 Editor editor = getOwnerData();
6375 Editable text = (Editable) editor.mTextView.getText();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006376 modifyText(text, mStart, getOldTextEnd(), mNewText, mStart, mNewCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006377 }
6378
James Cook471559f2015-02-27 10:31:20 -08006379 /**
6380 * Attempts to merge this existing operation with a new edit.
6381 * @param edit The new edit operation.
6382 * @return If the merge succeeded, returns true. Otherwise returns false and leaves this
6383 * object unchanged.
6384 */
6385 private boolean mergeWith(EditOperation edit) {
James Cook48e0fac2015-02-25 15:44:51 -08006386 if (DEBUG_UNDO) {
6387 Log.d(TAG, "mergeWith old " + this);
6388 Log.d(TAG, "mergeWith new " + edit);
6389 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006390
6391 if (mFrozen) {
6392 return false;
6393 }
6394
James Cook471559f2015-02-27 10:31:20 -08006395 switch (mType) {
6396 case TYPE_INSERT:
6397 return mergeInsertWith(edit);
6398 case TYPE_DELETE:
6399 return mergeDeleteWith(edit);
6400 case TYPE_REPLACE:
6401 return mergeReplaceWith(edit);
6402 default:
6403 return false;
6404 }
6405 }
6406
6407 private boolean mergeInsertWith(EditOperation edit) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006408 if (edit.mType == TYPE_INSERT) {
6409 // Merge insertions that are contiguous even when it's frozen.
6410 if (getNewTextEnd() != edit.mStart) {
6411 return false;
6412 }
6413 mNewText += edit.mNewText;
6414 mNewCursorPos = edit.mNewCursorPos;
6415 mFrozen = edit.mFrozen;
6416 mIsComposition = edit.mIsComposition;
6417 return true;
James Cook471559f2015-02-27 10:31:20 -08006418 }
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006419 if (mIsComposition && edit.mType == TYPE_REPLACE
6420 && mStart <= edit.mStart && getNewTextEnd() >= edit.getOldTextEnd()) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006421 // Merge insertion with replace as they can be single insertion.
6422 mNewText = mNewText.substring(0, edit.mStart - mStart) + edit.mNewText
6423 + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
6424 mNewCursorPos = edit.mNewCursorPos;
6425 mIsComposition = edit.mIsComposition;
6426 return true;
James Cook471559f2015-02-27 10:31:20 -08006427 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006428 return false;
James Cook471559f2015-02-27 10:31:20 -08006429 }
6430
6431 // TODO: Support forward delete.
6432 private boolean mergeDeleteWith(EditOperation edit) {
James Cook471559f2015-02-27 10:31:20 -08006433 // Only merge continuous deletes.
6434 if (edit.mType != TYPE_DELETE) {
6435 return false;
6436 }
6437 // Only merge deletions that are contiguous.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006438 if (mStart != edit.getOldTextEnd()) {
James Cook471559f2015-02-27 10:31:20 -08006439 return false;
6440 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006441 mStart = edit.mStart;
James Cook471559f2015-02-27 10:31:20 -08006442 mOldText = edit.mOldText + mOldText;
6443 mNewCursorPos = edit.mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006444 mIsComposition = edit.mIsComposition;
James Cook471559f2015-02-27 10:31:20 -08006445 return true;
6446 }
6447
6448 private boolean mergeReplaceWith(EditOperation edit) {
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006449 if (edit.mType == TYPE_INSERT && getNewTextEnd() == edit.mStart) {
6450 // Merge with adjacent insert.
6451 mNewText += edit.mNewText;
6452 mNewCursorPos = edit.mNewCursorPos;
6453 return true;
6454 }
6455 if (!mIsComposition) {
James Cook471559f2015-02-27 10:31:20 -08006456 return false;
6457 }
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006458 if (edit.mType == TYPE_DELETE && mStart <= edit.mStart
6459 && getNewTextEnd() >= edit.getOldTextEnd()) {
6460 // Merge with delete as they can be single operation.
6461 mNewText = mNewText.substring(0, edit.mStart - mStart)
6462 + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
6463 if (mNewText.isEmpty()) {
6464 mType = TYPE_DELETE;
6465 }
6466 mNewCursorPos = edit.mNewCursorPos;
6467 mIsComposition = edit.mIsComposition;
6468 return true;
6469 }
6470 if (edit.mType == TYPE_REPLACE && mStart == edit.mStart
6471 && TextUtils.equals(mNewText, edit.mOldText)) {
6472 // Merge with the replace that replaces the same region.
6473 mNewText = edit.mNewText;
6474 mNewCursorPos = edit.mNewCursorPos;
6475 mIsComposition = edit.mIsComposition;
6476 return true;
6477 }
6478 return false;
James Cook471559f2015-02-27 10:31:20 -08006479 }
6480
James Cook48e0fac2015-02-25 15:44:51 -08006481 /**
6482 * Forcibly creates a single merged edit operation by simulating the entire text
6483 * contents being replaced.
6484 */
James Cook22054252015-03-25 14:04:01 -07006485 public void forceMergeWith(EditOperation edit) {
James Cook48e0fac2015-02-25 15:44:51 -08006486 if (DEBUG_UNDO) Log.d(TAG, "forceMerge");
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006487 if (mergeWith(edit)) {
6488 return;
6489 }
James Cookf59152c2015-02-26 18:03:58 -08006490 Editor editor = getOwnerData();
James Cook48e0fac2015-02-25 15:44:51 -08006491
6492 // Copy the text of the current field.
6493 // NOTE: Using StringBuilder instead of SpannableStringBuilder would be somewhat faster,
6494 // but would require two parallel implementations of modifyText() because Editable and
6495 // StringBuilder do not share an interface for replace/delete/insert.
6496 Editable editable = (Editable) editor.mTextView.getText();
6497 Editable originalText = new SpannableStringBuilder(editable.toString());
6498
6499 // Roll back the last operation.
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006500 modifyText(originalText, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
James Cook48e0fac2015-02-25 15:44:51 -08006501
6502 // Clone the text again and apply the new operation.
6503 Editable finalText = new SpannableStringBuilder(editable.toString());
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006504 modifyText(finalText, edit.mStart, edit.getOldTextEnd(),
6505 edit.mNewText, edit.mStart, edit.mNewCursorPos);
James Cook48e0fac2015-02-25 15:44:51 -08006506
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006507 // Convert this operation into a replace operation.
James Cook48e0fac2015-02-25 15:44:51 -08006508 mType = TYPE_REPLACE;
6509 mNewText = finalText.toString();
James Cook48e0fac2015-02-25 15:44:51 -08006510 mOldText = originalText.toString();
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006511 mStart = 0;
James Cook48e0fac2015-02-25 15:44:51 -08006512 mNewCursorPos = edit.mNewCursorPos;
Keisuke Kuroyanagi113c0042016-07-01 18:42:05 +09006513 mIsComposition = edit.mIsComposition;
James Cook48e0fac2015-02-25 15:44:51 -08006514 // mOldCursorPos is unchanged.
6515 }
6516
6517 private static void modifyText(Editable text, int deleteFrom, int deleteTo,
6518 CharSequence newText, int newTextInsertAt, int newCursorPos) {
James Cook471559f2015-02-27 10:31:20 -08006519 // Apply the edit if it is still valid.
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006520 if (isValidRange(text, deleteFrom, deleteTo)
6521 && newTextInsertAt <= text.length() - (deleteTo - deleteFrom)) {
James Cook471559f2015-02-27 10:31:20 -08006522 if (deleteFrom != deleteTo) {
6523 text.delete(deleteFrom, deleteTo);
6524 }
6525 if (newText.length() != 0) {
6526 text.insert(newTextInsertAt, newText);
6527 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006528 }
James Cook900185d2015-03-10 09:48:11 -07006529 // Restore the cursor position. If there wasn't an old cursor (newCursorPos == -1) then
6530 // don't explicitly set it and rely on SpannableStringBuilder to position it.
James Cook471559f2015-02-27 10:31:20 -08006531 // TODO: Select all the text that was undone.
James Cook900185d2015-03-10 09:48:11 -07006532 if (0 <= newCursorPos && newCursorPos <= text.length()) {
James Cook471559f2015-02-27 10:31:20 -08006533 Selection.setSelection(text, newCursorPos);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006534 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006535 }
6536
James Cook48e0fac2015-02-25 15:44:51 -08006537 private String getTypeString() {
6538 switch (mType) {
6539 case TYPE_INSERT:
6540 return "insert";
6541 case TYPE_DELETE:
6542 return "delete";
6543 case TYPE_REPLACE:
6544 return "replace";
6545 default:
6546 return "";
6547 }
6548 }
6549
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006550 @Override
James Cook471559f2015-02-27 10:31:20 -08006551 public String toString() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006552 return "[mType=" + getTypeString() + ", "
6553 + "mOldText=" + mOldText + ", "
6554 + "mNewText=" + mNewText + ", "
6555 + "mStart=" + mStart + ", "
6556 + "mOldCursorPos=" + mOldCursorPos + ", "
6557 + "mNewCursorPos=" + mNewCursorPos + ", "
6558 + "mFrozen=" + mFrozen + ", "
6559 + "mIsComposition=" + mIsComposition + "]";
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006560 }
6561
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006562 public static final Parcelable.ClassLoaderCreator<EditOperation> CREATOR =
6563 new Parcelable.ClassLoaderCreator<EditOperation>() {
James Cookf59152c2015-02-26 18:03:58 -08006564 @Override
James Cook471559f2015-02-27 10:31:20 -08006565 public EditOperation createFromParcel(Parcel in) {
6566 return new EditOperation(in, null);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006567 }
6568
James Cookf59152c2015-02-26 18:03:58 -08006569 @Override
James Cook471559f2015-02-27 10:31:20 -08006570 public EditOperation createFromParcel(Parcel in, ClassLoader loader) {
6571 return new EditOperation(in, loader);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006572 }
6573
James Cookf59152c2015-02-26 18:03:58 -08006574 @Override
James Cook471559f2015-02-27 10:31:20 -08006575 public EditOperation[] newArray(int size) {
6576 return new EditOperation[size];
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07006577 }
6578 };
6579 }
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006580
6581 /**
6582 * A helper for enabling and handling "PROCESS_TEXT" menu actions.
6583 * These allow external applications to plug into currently selected text.
6584 */
6585 static final class ProcessTextIntentActionsHandler {
6586
6587 private final Editor mEditor;
6588 private final TextView mTextView;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006589 private final Context mContext;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006590 private final PackageManager mPackageManager;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006591 private final String mPackageName;
6592 private final SparseArray<Intent> mAccessibilityIntents = new SparseArray<>();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006593 private final SparseArray<AccessibilityNodeInfo.AccessibilityAction> mAccessibilityActions =
6594 new SparseArray<>();
6595 private final List<ResolveInfo> mSupportedActivities = new ArrayList<>();
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006596
6597 private ProcessTextIntentActionsHandler(Editor editor) {
6598 mEditor = Preconditions.checkNotNull(editor);
6599 mTextView = Preconditions.checkNotNull(mEditor.mTextView);
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006600 mContext = Preconditions.checkNotNull(mTextView.getContext());
6601 mPackageManager = Preconditions.checkNotNull(mContext.getPackageManager());
6602 mPackageName = Preconditions.checkNotNull(mContext.getPackageName());
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006603 }
6604
6605 /**
6606 * Adds "PROCESS_TEXT" menu items to the specified menu.
6607 */
6608 public void onInitializeMenu(Menu menu) {
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006609 loadSupportedActivities();
Abodunrinwa Tokic28be382017-11-07 18:46:50 +00006610 final int size = mSupportedActivities.size();
Abodunrinwa Toki6eecdc92017-04-11 05:15:25 +01006611 for (int i = 0; i < size; i++) {
6612 final ResolveInfo resolveInfo = mSupportedActivities.get(i);
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006613 menu.add(Menu.NONE, Menu.NONE,
Abodunrinwa Tokic28be382017-11-07 18:46:50 +00006614 Editor.MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i,
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006615 getLabel(resolveInfo))
6616 .setIntent(createProcessTextIntentForResolveInfo(resolveInfo))
Abodunrinwa Tokiba385622017-11-29 19:30:32 +00006617 .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006618 }
6619 }
6620
6621 /**
6622 * Performs a "PROCESS_TEXT" action if there is one associated with the specified
6623 * menu item.
6624 *
6625 * @return True if the action was performed, false otherwise.
6626 */
6627 public boolean performMenuItemAction(MenuItem item) {
6628 return fireIntent(item.getIntent());
6629 }
6630
6631 /**
6632 * Initializes and caches "PROCESS_TEXT" accessibility actions.
6633 */
6634 public void initializeAccessibilityActions() {
6635 mAccessibilityIntents.clear();
6636 mAccessibilityActions.clear();
6637 int i = 0;
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006638 loadSupportedActivities();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006639 for (ResolveInfo resolveInfo : mSupportedActivities) {
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006640 int actionId = TextView.ACCESSIBILITY_ACTION_PROCESS_TEXT_START_ID + i++;
6641 mAccessibilityActions.put(
6642 actionId,
6643 new AccessibilityNodeInfo.AccessibilityAction(
6644 actionId, getLabel(resolveInfo)));
6645 mAccessibilityIntents.put(
6646 actionId, createProcessTextIntentForResolveInfo(resolveInfo));
6647 }
6648 }
6649
6650 /**
6651 * Adds "PROCESS_TEXT" accessibility actions to the specified accessibility node info.
6652 * NOTE: This needs a prior call to {@link #initializeAccessibilityActions()} to make the
6653 * latest accessibility actions available for this call.
6654 */
6655 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) {
6656 for (int i = 0; i < mAccessibilityActions.size(); i++) {
6657 nodeInfo.addAction(mAccessibilityActions.valueAt(i));
6658 }
6659 }
6660
6661 /**
6662 * Performs a "PROCESS_TEXT" action if there is one associated with the specified
6663 * accessibility action id.
6664 *
6665 * @return True if the action was performed, false otherwise.
6666 */
6667 public boolean performAccessibilityAction(int actionId) {
6668 return fireIntent(mAccessibilityIntents.get(actionId));
6669 }
6670
6671 private boolean fireIntent(Intent intent) {
6672 if (intent != null && Intent.ACTION_PROCESS_TEXT.equals(intent.getAction())) {
Siyamed Sinirce3b05a2017-07-18 18:54:31 -07006673 String selectedText = mTextView.getSelectedText();
6674 selectedText = TextUtils.trimToParcelableSize(selectedText);
6675 intent.putExtra(Intent.EXTRA_PROCESS_TEXT, selectedText);
Keisuke Kuroyanagiaf4caa62016-02-29 12:53:58 -08006676 mEditor.mPreserveSelection = true;
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006677 mTextView.startActivityForResult(intent, TextView.PROCESS_TEXT_REQUEST_CODE);
6678 return true;
6679 }
6680 return false;
6681 }
6682
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006683 private void loadSupportedActivities() {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006684 mSupportedActivities.clear();
Abodunrinwa Toki3049d8c2017-10-17 20:33:00 +01006685 if (!mContext.canStartActivityForResult()) {
6686 return;
6687 }
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006688 PackageManager packageManager = mTextView.getContext().getPackageManager();
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006689 List<ResolveInfo> unfiltered =
6690 packageManager.queryIntentActivities(createProcessTextIntent(), 0);
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006691 for (ResolveInfo info : unfiltered) {
6692 if (isSupportedActivity(info)) {
Aurimas Liutikasee62c292016-07-21 15:05:40 -07006693 mSupportedActivities.add(info);
Abodunrinwa Toki5ba060b2016-05-24 18:34:40 +01006694 }
6695 }
6696 }
6697
6698 private boolean isSupportedActivity(ResolveInfo info) {
6699 return mPackageName.equals(info.activityInfo.packageName)
6700 || info.activityInfo.exported
6701 && (info.activityInfo.permission == null
6702 || mContext.checkSelfPermission(info.activityInfo.permission)
6703 == PackageManager.PERMISSION_GRANTED);
Abodunrinwa Tokideaf0db2015-06-26 18:21:30 -07006704 }
6705
6706 private Intent createProcessTextIntentForResolveInfo(ResolveInfo info) {
6707 return createProcessTextIntent()
6708 .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !mTextView.isTextEditable())
6709 .setClassName(info.activityInfo.packageName, info.activityInfo.name);
6710 }
6711
6712 private Intent createProcessTextIntent() {
6713 return new Intent()
6714 .setAction(Intent.ACTION_PROCESS_TEXT)
6715 .setType("text/plain");
6716 }
6717
6718 private CharSequence getLabel(ResolveInfo resolveInfo) {
6719 return resolveInfo.loadLabel(mPackageManager);
6720 }
6721 }
Gilles Debunned88876a2012-03-16 17:34:04 -07006722}