Editor.java revision 97af673e1f2e33a3cc48a2ab7816bbf082a2e9f4
155bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync/*
255bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync * Copyright (C) 2012 The Android Open Source Project
355bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync *
455bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync * Licensed under the Apache License, Version 2.0 (the "License");
555bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync * you may not use this file except in compliance with the License.
655bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync * You may obtain a copy of the License at
755bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync *
855bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync *      http://www.apache.org/licenses/LICENSE-2.0
955bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync *
1055bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync * Unless required by applicable law or agreed to in writing, software
1155bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync * distributed under the License is distributed on an "AS IS" BASIS,
1255bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1355bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync * See the License for the specific language governing permissions and
1455bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync * limitations under the License.
1555bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync */
1655bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync
1755bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncpackage android.widget;
1855bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync
1955bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.R;
2055bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.annotation.IntDef;
2155bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.annotation.Nullable;
2257e42f4117e92c03d39f1e1e572f53ef5bb821b8Irfan Sheriffimport android.app.PendingIntent;
2355bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.app.PendingIntent.CanceledException;
2455bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.content.ClipData;
2555bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.content.ClipData.Item;
2655bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.content.Context;
2721ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriffimport android.content.Intent;
2855bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.content.UndoManager;
2955bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.content.UndoOperation;
30bfb27bbefb013220af699881d486cc04be5ec1f5Yoshihiko Ikenagaimport android.content.UndoOwner;
316f7d385d964949e507dcc9c88012372f48d0bce7Irfan Sheriffimport android.content.pm.PackageManager;
32bfb27bbefb013220af699881d486cc04be5ec1f5Yoshihiko Ikenagaimport android.content.pm.ResolveInfo;
336f7d385d964949e507dcc9c88012372f48d0bce7Irfan Sheriffimport android.content.res.TypedArray;
3455bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.graphics.Canvas;
3555bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.graphics.Color;
3655bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.graphics.Matrix;
37d8544a51482c86b12da3ac82ea77b83045f689b7Jeff Brownimport android.graphics.Paint;
3855bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.graphics.Path;
39651cdfcbac6245f570475991588ddc2d30265e8dIrfan Sheriffimport android.graphics.Rect;
4055bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.graphics.RectF;
4155bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.graphics.drawable.Drawable;
42651cdfcbac6245f570475991588ddc2d30265e8dIrfan Sheriffimport android.os.Bundle;
4355bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.os.Parcel;
4455bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.os.Parcelable;
4521ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriffimport android.os.ParcelableParcel;
46ab3b9fbfa0523e036ec71888d0da5dc55cd3301bIrfan Sheriffimport android.os.SystemClock;
4721ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriffimport android.provider.Settings;
4855bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.text.DynamicLayout;
4955bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.text.Editable;
5055bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.text.InputFilter;
516f7d385d964949e507dcc9c88012372f48d0bce7Irfan Sheriffimport android.text.InputType;
5255bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.text.Layout;
5355bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.text.ParcelableSpan;
5421ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriffimport android.text.Selection;
55ab3b9fbfa0523e036ec71888d0da5dc55cd3301bIrfan Sheriffimport android.text.SpanWatcher;
5621ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriffimport android.text.Spannable;
5755bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.text.SpannableStringBuilder;
5855bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.text.Spanned;
5955bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.text.StaticLayout;
60f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriffimport android.text.TextUtils;
61f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriffimport android.text.method.KeyListener;
62f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriffimport android.text.method.MetaKeyKeyListener;
63f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriffimport android.text.method.MovementMethod;
64f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriffimport android.text.method.WordIterator;
65f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriffimport android.text.style.EasyEditSpan;
66f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriffimport android.text.style.SuggestionRangeSpan;
67f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriffimport android.text.style.SuggestionSpan;
68f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriffimport android.text.style.TextAppearanceSpan;
69f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriffimport android.text.style.URLSpan;
70f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriffimport android.util.DisplayMetrics;
71f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriffimport android.util.Log;
72f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriffimport android.util.SparseArray;
733a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriffimport android.view.ActionMode;
74ea5b16ac5751022de73e8f1225407eb01e7f1824Irfan Sheriffimport android.view.ActionMode.Callback;
75ea5b16ac5751022de73e8f1225407eb01e7f1824Irfan Sheriffimport android.view.DisplayListCanvas;
7655bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.view.DragEvent;
77ea5b16ac5751022de73e8f1225407eb01e7f1824Irfan Sheriffimport android.view.Gravity;
7855bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.view.InputDevice;
7955bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo syncimport android.view.LayoutInflater;
803a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriffimport android.view.Menu;
813a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriffimport android.view.MenuItem;
823a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriffimport android.view.MotionEvent;
833a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriffimport android.view.RenderNode;
843a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriffimport android.view.View;
853a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriffimport android.view.View.DragShadowBuilder;
864be4d31f34a0fd0e23de1cbda311c07412f8d0b8Irfan Sheriffimport android.view.View.OnClickListener;
873a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriffimport android.view.ViewConfiguration;
883a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriffimport android.view.ViewGroup;
893a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriffimport android.view.ViewGroup.LayoutParams;
903a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriffimport android.view.ViewParent;
913a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriffimport android.view.ViewTreeObserver;
92ffadfb9ffdced62db215319d3edc7717802088fbVinit Deshapndeimport android.view.WindowManager;
93f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriffimport android.view.accessibility.AccessibilityNodeInfo;
9421ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriffimport android.view.inputmethod.CorrectionInfo;
9521ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriffimport android.view.inputmethod.CursorAnchorInfo;
9621ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriffimport android.view.inputmethod.EditorInfo;
9721ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriffimport android.view.inputmethod.ExtractedText;
9821ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriffimport android.view.inputmethod.ExtractedTextRequest;
9921ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriffimport android.view.inputmethod.InputConnection;
10021ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriffimport android.view.inputmethod.InputMethodManager;
10121ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriffimport android.widget.AdapterView.OnItemClickListener;
102e298d884580006cbcd4aec8fd7877dae3f081eecIrfan Sheriffimport android.widget.TextView.Drawables;
10321ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriffimport android.widget.TextView.OnEditorActionListener;
1044be4d31f34a0fd0e23de1cbda311c07412f8d0b8Irfan Sheriff
1054be4d31f34a0fd0e23de1cbda311c07412f8d0b8Irfan Sheriffimport com.android.internal.annotations.VisibleForTesting;
10621ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriffimport com.android.internal.util.ArrayUtils;
10721ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriffimport com.android.internal.util.GrowingArrayUtils;
10821ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriffimport com.android.internal.util.Preconditions;
10921ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriffimport com.android.internal.widget.EditableInputConnection;
110ffadfb9ffdced62db215319d3edc7717802088fbVinit Deshapnde
111f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriffimport java.lang.annotation.Retention;
11257e42f4117e92c03d39f1e1e572f53ef5bb821b8Irfan Sheriffimport java.lang.annotation.RetentionPolicy;
11357e42f4117e92c03d39f1e1e572f53ef5bb821b8Irfan Sheriffimport java.text.BreakIterator;
11457e42f4117e92c03d39f1e1e572f53ef5bb821b8Irfan Sheriffimport java.util.Arrays;
11557e42f4117e92c03d39f1e1e572f53ef5bb821b8Irfan Sheriffimport java.util.Comparator;
11657e42f4117e92c03d39f1e1e572f53ef5bb821b8Irfan Sheriffimport java.util.HashMap;
11757e42f4117e92c03d39f1e1e572f53ef5bb821b8Irfan Sheriffimport java.util.List;
11857e42f4117e92c03d39f1e1e572f53ef5bb821b8Irfan Sheriff
119ffadfb9ffdced62db215319d3edc7717802088fbVinit Deshapnde
120f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriff/**
121bfb27bbefb013220af699881d486cc04be5ec1f5Yoshihiko Ikenaga * Helper class used by TextView to handle editable text views.
12257e42f4117e92c03d39f1e1e572f53ef5bb821b8Irfan Sheriff *
12357e42f4117e92c03d39f1e1e572f53ef5bb821b8Irfan Sheriff * @hide
12457e42f4117e92c03d39f1e1e572f53ef5bb821b8Irfan Sheriff */
125bfb27bbefb013220af699881d486cc04be5ec1f5Yoshihiko Ikenagapublic class Editor {
126bfb27bbefb013220af699881d486cc04be5ec1f5Yoshihiko Ikenaga    private static final String TAG = "Editor";
1273a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriff    private static final boolean DEBUG_UNDO = false;
1283a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriff
1293a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriff    static final int BLINK = 500;
1303a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriff    private static final float[] TEMP_POSITION = new float[2];
1313a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriff    private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
13221ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriff    private static final float LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS = 0.5f;
133f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriff    private static final int UNSET_X_VALUE = -1;
13421ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriff    private static final int UNSET_LINE = -1;
13555bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    // Tag used when the Editor maintains its own separate UndoManager.
13655bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    private static final String UNDO_OWNER_TAG = "Editor";
1376f7d385d964949e507dcc9c88012372f48d0bce7Irfan Sheriff
13855bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    // Ordering constants used to place the Action Mode items in their menu.
139f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriff    private static final int MENU_ITEM_ORDER_CUT = 1;
14021ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriff    private static final int MENU_ITEM_ORDER_COPY = 2;
14155bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    private static final int MENU_ITEM_ORDER_PASTE = 3;
14255bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    private static final int MENU_ITEM_ORDER_SHARE = 4;
1433a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriff    private static final int MENU_ITEM_ORDER_SELECT_ALL = 5;
1443a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriff    private static final int MENU_ITEM_ORDER_REPLACE = 6;
1453a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriff    private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 10;
1463a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriff
1473a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriff    // Each Editor manages its own undo stack.
1483a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriff    private final UndoManager mUndoManager = new UndoManager();
1493a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriff    private UndoOwner mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
150f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriff    final UndoInputFilter mUndoInputFilter = new UndoInputFilter(this);
1513a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriff    boolean mAllowUndo = true;
1523a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriff
1533a67e2515bff73fab57621b1f9966662e83b7881Irfan Sheriff    // Cursor Controllers.
15441de2404658c7c6faf6c78e777ba50af11784f5cIrfan Sheriff    InsertionPointCursorController mInsertionPointCursorController;
15541de2404658c7c6faf6c78e777ba50af11784f5cIrfan Sheriff    SelectionModifierCursorController mSelectionModifierCursorController;
15641de2404658c7c6faf6c78e777ba50af11784f5cIrfan Sheriff    // Action mode used when text is selected or when actions on an insertion cursor are triggered.
15741de2404658c7c6faf6c78e777ba50af11784f5cIrfan Sheriff    ActionMode mTextActionMode;
15841de2404658c7c6faf6c78e777ba50af11784f5cIrfan Sheriff    boolean mInsertionControllerEnabled;
15941de2404658c7c6faf6c78e777ba50af11784f5cIrfan Sheriff    boolean mSelectionControllerEnabled;
16041de2404658c7c6faf6c78e777ba50af11784f5cIrfan Sheriff
16141de2404658c7c6faf6c78e777ba50af11784f5cIrfan Sheriff    // Used to highlight a word when it is corrected by the IME
16241de2404658c7c6faf6c78e777ba50af11784f5cIrfan Sheriff    CorrectionHighlighter mCorrectionHighlighter;
1636f7d385d964949e507dcc9c88012372f48d0bce7Irfan Sheriff
16455bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    InputContentType mInputContentType;
16521ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriff    InputMethodState mInputMethodState;
16655bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync
16755bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    private static class TextRenderNode {
16810ca870d3b58ec6fd62b85466ec1211fca77d33eIrfan Sheriff        RenderNode renderNode;
16910ca870d3b58ec6fd62b85466ec1211fca77d33eIrfan Sheriff        boolean isDirty;
170f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriff        public TextRenderNode(String name) {
171f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriff            isDirty = true;
172f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriff            renderNode = RenderNode.create(name, null);
173f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriff        }
17410ca870d3b58ec6fd62b85466ec1211fca77d33eIrfan Sheriff        boolean needsRecord() { return isDirty || !renderNode.isValid(); }
175f118043ca726c65f04cddc6321c9e820b577fc6fIrfan Sheriff    }
17610ca870d3b58ec6fd62b85466ec1211fca77d33eIrfan Sheriff    TextRenderNode[] mTextRenderNodes;
17710ca870d3b58ec6fd62b85466ec1211fca77d33eIrfan Sheriff
17855bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    boolean mFrozenWithFocus;
17955bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    boolean mSelectionMoved;
18021ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriff    boolean mTouchFocusSelected;
18155bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync
18255bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    KeyListener mKeyListener;
18355bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    int mInputType = EditorInfo.TYPE_NULL;
18455bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync
18555bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    boolean mDiscardNextActionUp;
186651cdfcbac6245f570475991588ddc2d30265e8dIrfan Sheriff    boolean mIgnoreActionUpEvent;
18755bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync
18855bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    long mShowCursor;
18955bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    Blink mBlink;
19055bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync
191651cdfcbac6245f570475991588ddc2d30265e8dIrfan Sheriff    boolean mCursorVisible = true;
19255bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    boolean mSelectAllOnFocus;
19355bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    boolean mTextIsSelectable;
19421ba8153325e010224c6bc75a0acdc98b6ca82e8Irfan Sheriff
19555bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    CharSequence mError;
19655bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    boolean mErrorWasChanged;
19755bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    ErrorPopup mErrorPopup;
19855bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync
199651cdfcbac6245f570475991588ddc2d30265e8dIrfan Sheriff    /**
20055bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync     * This flag is set if the TextView tries to display an error before it
20155bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync     * is attached to the window (so its position is still unknown).
20255bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync     * It causes the error to be shown later, when onAttachedToWindow()
20355bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync     * is called.
20455bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync     */
20555bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    boolean mShowErrorAfterAttach;
20655bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync
2074be4d31f34a0fd0e23de1cbda311c07412f8d0b8Irfan Sheriff    boolean mInBatchEditControllers;
20855bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    boolean mShowSoftInputOnFocus = true;
20955bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    boolean mPreserveDetachedSelection;
21055bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    boolean mTemporaryDetach;
21155bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync
21255bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    SuggestionsPopupWindow mSuggestionsPopupWindow;
21355bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    SuggestionRangeSpan mSuggestionRangeSpan;
21455bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    Runnable mShowSuggestionRunnable;
21555bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync
21655bc5f3e0408bcb5a39a6732de0b2d1aa99a55berepo sync    final Drawable[] mCursorDrawable = new Drawable[2];
217    int mCursorCount; // Current number of used mCursorDrawable: 0 (resource=0), 1 or 2 (split)
218
219    private Drawable mSelectHandleLeft;
220    private Drawable mSelectHandleRight;
221    private Drawable mSelectHandleCenter;
222
223    // Global listener that detects changes in the global position of the TextView
224    private PositionListener mPositionListener;
225
226    float mLastDownPositionX, mLastDownPositionY;
227    Callback mCustomSelectionActionModeCallback;
228    Callback mCustomInsertionActionModeCallback;
229
230    // Set when this TextView gained focus with some text selected. Will start selection mode.
231    boolean mCreatedWithASelection;
232
233    boolean mDoubleTap = false;
234
235    private Runnable mInsertionActionModeRunnable;
236
237    // The span controller helps monitoring the changes to which the Editor needs to react:
238    // - EasyEditSpans, for which we have some UI to display on attach and on hide
239    // - SelectionSpans, for which we need to call updateSelection if an IME is attached
240    private SpanController mSpanController;
241
242    WordIterator mWordIterator;
243    SpellChecker mSpellChecker;
244
245    // This word iterator is set with text and used to determine word boundaries
246    // when a user is selecting text.
247    private WordIterator mWordIteratorWithText;
248    // Indicate that the text in the word iterator needs to be updated.
249    private boolean mUpdateWordIteratorText;
250
251    private Rect mTempRect;
252
253    private TextView mTextView;
254
255    final ProcessTextIntentActionsHandler mProcessTextIntentActionsHandler;
256
257    final CursorAnchorInfoNotifier mCursorAnchorInfoNotifier = new CursorAnchorInfoNotifier();
258
259    private final Runnable mShowFloatingToolbar = new Runnable() {
260        @Override
261        public void run() {
262            if (mTextActionMode != null) {
263                mTextActionMode.hide(0);  // hide off.
264            }
265        }
266    };
267
268    boolean mIsInsertionActionModeStartPending = false;
269
270    Editor(TextView textView) {
271        mTextView = textView;
272        // Synchronize the filter list, which places the undo input filter at the end.
273        mTextView.setFilters(mTextView.getFilters());
274        mProcessTextIntentActionsHandler = new ProcessTextIntentActionsHandler(this);
275    }
276
277    ParcelableParcel saveInstanceState() {
278        ParcelableParcel state = new ParcelableParcel(getClass().getClassLoader());
279        Parcel parcel = state.getParcel();
280        mUndoManager.saveInstanceState(parcel);
281        mUndoInputFilter.saveInstanceState(parcel);
282        return state;
283    }
284
285    void restoreInstanceState(ParcelableParcel state) {
286        Parcel parcel = state.getParcel();
287        mUndoManager.restoreInstanceState(parcel, state.getClassLoader());
288        mUndoInputFilter.restoreInstanceState(parcel);
289        // Re-associate this object as the owner of undo state.
290        mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
291    }
292
293    /**
294     * Forgets all undo and redo operations for this Editor.
295     */
296    void forgetUndoRedo() {
297        UndoOwner[] owners = { mUndoOwner };
298        mUndoManager.forgetUndos(owners, -1 /* all */);
299        mUndoManager.forgetRedos(owners, -1 /* all */);
300    }
301
302    boolean canUndo() {
303        UndoOwner[] owners = { mUndoOwner };
304        return mAllowUndo && mUndoManager.countUndos(owners) > 0;
305    }
306
307    boolean canRedo() {
308        UndoOwner[] owners = { mUndoOwner };
309        return mAllowUndo && mUndoManager.countRedos(owners) > 0;
310    }
311
312    void undo() {
313        if (!mAllowUndo) {
314            return;
315        }
316        UndoOwner[] owners = { mUndoOwner };
317        mUndoManager.undo(owners, 1);  // Undo 1 action.
318    }
319
320    void redo() {
321        if (!mAllowUndo) {
322            return;
323        }
324        UndoOwner[] owners = { mUndoOwner };
325        mUndoManager.redo(owners, 1);  // Redo 1 action.
326    }
327
328    void replace() {
329        int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
330        stopTextActionMode();
331        Selection.setSelection((Spannable) mTextView.getText(), middle);
332        showSuggestions();
333    }
334
335    void onAttachedToWindow() {
336        if (mShowErrorAfterAttach) {
337            showError();
338            mShowErrorAfterAttach = false;
339        }
340        mTemporaryDetach = false;
341
342        final ViewTreeObserver observer = mTextView.getViewTreeObserver();
343        // No need to create the controller.
344        // The get method will add the listener on controller creation.
345        if (mInsertionPointCursorController != null) {
346            observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
347        }
348        if (mSelectionModifierCursorController != null) {
349            mSelectionModifierCursorController.resetTouchOffsets();
350            observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
351        }
352        updateSpellCheckSpans(0, mTextView.getText().length(),
353                true /* create the spell checker if needed */);
354
355        if (mTextView.hasTransientState() &&
356                mTextView.getSelectionStart() != mTextView.getSelectionEnd()) {
357            // Since transient state is reference counted make sure it stays matched
358            // with our own calls to it for managing selection.
359            // The action mode callback will set this back again when/if the action mode starts.
360            mTextView.setHasTransientState(false);
361
362            // We had an active selection from before, start the selection mode.
363            startSelectionActionMode();
364        }
365
366        getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true);
367        resumeBlink();
368    }
369
370    void onDetachedFromWindow() {
371        getPositionListener().removeSubscriber(mCursorAnchorInfoNotifier);
372
373        if (mError != null) {
374            hideError();
375        }
376
377        suspendBlink();
378
379        if (mInsertionPointCursorController != null) {
380            mInsertionPointCursorController.onDetached();
381        }
382
383        if (mSelectionModifierCursorController != null) {
384            mSelectionModifierCursorController.onDetached();
385        }
386
387        if (mShowSuggestionRunnable != null) {
388            mTextView.removeCallbacks(mShowSuggestionRunnable);
389        }
390
391        // Cancel the single tap delayed runnable.
392        if (mInsertionActionModeRunnable != null) {
393            mTextView.removeCallbacks(mInsertionActionModeRunnable);
394        }
395
396        mTextView.removeCallbacks(mShowFloatingToolbar);
397
398        discardTextDisplayLists();
399
400        if (mSpellChecker != null) {
401            mSpellChecker.closeSession();
402            // Forces the creation of a new SpellChecker next time this window is created.
403            // Will handle the cases where the settings has been changed in the meantime.
404            mSpellChecker = null;
405        }
406
407        mPreserveDetachedSelection = true;
408        hideCursorAndSpanControllers();
409        stopTextActionMode();
410        mPreserveDetachedSelection = false;
411        mTemporaryDetach = false;
412    }
413
414    private void discardTextDisplayLists() {
415        if (mTextRenderNodes != null) {
416            for (int i = 0; i < mTextRenderNodes.length; i++) {
417                RenderNode displayList = mTextRenderNodes[i] != null
418                        ? mTextRenderNodes[i].renderNode : null;
419                if (displayList != null && displayList.isValid()) {
420                    displayList.discardDisplayList();
421                }
422            }
423        }
424    }
425
426    private void showError() {
427        if (mTextView.getWindowToken() == null) {
428            mShowErrorAfterAttach = true;
429            return;
430        }
431
432        if (mErrorPopup == null) {
433            LayoutInflater inflater = LayoutInflater.from(mTextView.getContext());
434            final TextView err = (TextView) inflater.inflate(
435                    com.android.internal.R.layout.textview_hint, null);
436
437            final float scale = mTextView.getResources().getDisplayMetrics().density;
438            mErrorPopup = new ErrorPopup(err, (int)(200 * scale + 0.5f), (int)(50 * scale + 0.5f));
439            mErrorPopup.setFocusable(false);
440            // The user is entering text, so the input method is needed.  We
441            // don't want the popup to be displayed on top of it.
442            mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
443        }
444
445        TextView tv = (TextView) mErrorPopup.getContentView();
446        chooseSize(mErrorPopup, mError, tv);
447        tv.setText(mError);
448
449        mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY());
450        mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor());
451    }
452
453    public void setError(CharSequence error, Drawable icon) {
454        mError = TextUtils.stringOrSpannedString(error);
455        mErrorWasChanged = true;
456
457        if (mError == null) {
458            setErrorIcon(null);
459            if (mErrorPopup != null) {
460                if (mErrorPopup.isShowing()) {
461                    mErrorPopup.dismiss();
462                }
463
464                mErrorPopup = null;
465            }
466            mShowErrorAfterAttach = false;
467        } else {
468            setErrorIcon(icon);
469            if (mTextView.isFocused()) {
470                showError();
471            }
472        }
473    }
474
475    private void setErrorIcon(Drawable icon) {
476        Drawables dr = mTextView.mDrawables;
477        if (dr == null) {
478            mTextView.mDrawables = dr = new Drawables(mTextView.getContext());
479        }
480        dr.setErrorDrawable(icon, mTextView);
481
482        mTextView.resetResolvedDrawables();
483        mTextView.invalidate();
484        mTextView.requestLayout();
485    }
486
487    private void hideError() {
488        if (mErrorPopup != null) {
489            if (mErrorPopup.isShowing()) {
490                mErrorPopup.dismiss();
491            }
492        }
493
494        mShowErrorAfterAttach = false;
495    }
496
497    /**
498     * Returns the X offset to make the pointy top of the error point
499     * at the middle of the error icon.
500     */
501    private int getErrorX() {
502        /*
503         * The "25" is the distance between the point and the right edge
504         * of the background
505         */
506        final float scale = mTextView.getResources().getDisplayMetrics().density;
507
508        final Drawables dr = mTextView.mDrawables;
509
510        final int layoutDirection = mTextView.getLayoutDirection();
511        int errorX;
512        int offset;
513        switch (layoutDirection) {
514            default:
515            case View.LAYOUT_DIRECTION_LTR:
516                offset = - (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
517                errorX = mTextView.getWidth() - mErrorPopup.getWidth() -
518                        mTextView.getPaddingRight() + offset;
519                break;
520            case View.LAYOUT_DIRECTION_RTL:
521                offset = (dr != null ? dr.mDrawableSizeLeft : 0) / 2 - (int) (25 * scale + 0.5f);
522                errorX = mTextView.getPaddingLeft() + offset;
523                break;
524        }
525        return errorX;
526    }
527
528    /**
529     * Returns the Y offset to make the pointy top of the error point
530     * at the bottom of the error icon.
531     */
532    private int getErrorY() {
533        /*
534         * Compound, not extended, because the icon is not clipped
535         * if the text height is smaller.
536         */
537        final int compoundPaddingTop = mTextView.getCompoundPaddingTop();
538        int vspace = mTextView.getBottom() - mTextView.getTop() -
539                mTextView.getCompoundPaddingBottom() - compoundPaddingTop;
540
541        final Drawables dr = mTextView.mDrawables;
542
543        final int layoutDirection = mTextView.getLayoutDirection();
544        int height;
545        switch (layoutDirection) {
546            default:
547            case View.LAYOUT_DIRECTION_LTR:
548                height = (dr != null ? dr.mDrawableHeightRight : 0);
549                break;
550            case View.LAYOUT_DIRECTION_RTL:
551                height = (dr != null ? dr.mDrawableHeightLeft : 0);
552                break;
553        }
554
555        int icontop = compoundPaddingTop + (vspace - height) / 2;
556
557        /*
558         * The "2" is the distance between the point and the top edge
559         * of the background.
560         */
561        final float scale = mTextView.getResources().getDisplayMetrics().density;
562        return icontop + height - mTextView.getHeight() - (int) (2 * scale + 0.5f);
563    }
564
565    void createInputContentTypeIfNeeded() {
566        if (mInputContentType == null) {
567            mInputContentType = new InputContentType();
568        }
569    }
570
571    void createInputMethodStateIfNeeded() {
572        if (mInputMethodState == null) {
573            mInputMethodState = new InputMethodState();
574        }
575    }
576
577    boolean isCursorVisible() {
578        // The default value is true, even when there is no associated Editor
579        return mCursorVisible && mTextView.isTextEditable();
580    }
581
582    void prepareCursorControllers() {
583        boolean windowSupportsHandles = false;
584
585        ViewGroup.LayoutParams params = mTextView.getRootView().getLayoutParams();
586        if (params instanceof WindowManager.LayoutParams) {
587            WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
588            windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
589                    || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
590        }
591
592        boolean enabled = windowSupportsHandles && mTextView.getLayout() != null;
593        mInsertionControllerEnabled = enabled && isCursorVisible();
594        mSelectionControllerEnabled = enabled && mTextView.textCanBeSelected();
595
596        if (!mInsertionControllerEnabled) {
597            hideInsertionPointCursorController();
598            if (mInsertionPointCursorController != null) {
599                mInsertionPointCursorController.onDetached();
600                mInsertionPointCursorController = null;
601            }
602        }
603
604        if (!mSelectionControllerEnabled) {
605            stopTextActionMode();
606            if (mSelectionModifierCursorController != null) {
607                mSelectionModifierCursorController.onDetached();
608                mSelectionModifierCursorController = null;
609            }
610        }
611    }
612
613    void hideInsertionPointCursorController() {
614        if (mInsertionPointCursorController != null) {
615            mInsertionPointCursorController.hide();
616        }
617    }
618
619    /**
620     * Hides the insertion and span controllers.
621     */
622    void hideCursorAndSpanControllers() {
623        hideCursorControllers();
624        hideSpanControllers();
625    }
626
627    private void hideSpanControllers() {
628        if (mSpanController != null) {
629            mSpanController.hide();
630        }
631    }
632
633    private void hideCursorControllers() {
634        // When mTextView is not ExtractEditText, we need to distinguish two kinds of focus-lost.
635        // One is the true focus lost where suggestions pop-up (if any) should be dismissed, and the
636        // other is an side effect of showing the suggestions pop-up itself. We use isShowingUp()
637        // to distinguish one from the other.
638        if (mSuggestionsPopupWindow != null && ((mTextView.isInExtractedMode()) ||
639                !mSuggestionsPopupWindow.isShowingUp())) {
640            // Should be done before hide insertion point controller since it triggers a show of it
641            mSuggestionsPopupWindow.hide();
642        }
643        hideInsertionPointCursorController();
644    }
645
646    /**
647     * Create new SpellCheckSpans on the modified region.
648     */
649    private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
650        // Remove spans whose adjacent characters are text not punctuation
651        mTextView.removeAdjacentSuggestionSpans(start);
652        mTextView.removeAdjacentSuggestionSpans(end);
653
654        if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled() &&
655                !(mTextView.isInExtractedMode())) {
656            if (mSpellChecker == null && createSpellChecker) {
657                mSpellChecker = new SpellChecker(mTextView);
658            }
659            if (mSpellChecker != null) {
660                mSpellChecker.spellCheck(start, end);
661            }
662        }
663    }
664
665    void onScreenStateChanged(int screenState) {
666        switch (screenState) {
667            case View.SCREEN_STATE_ON:
668                resumeBlink();
669                break;
670            case View.SCREEN_STATE_OFF:
671                suspendBlink();
672                break;
673        }
674    }
675
676    private void suspendBlink() {
677        if (mBlink != null) {
678            mBlink.cancel();
679        }
680    }
681
682    private void resumeBlink() {
683        if (mBlink != null) {
684            mBlink.uncancel();
685            makeBlink();
686        }
687    }
688
689    void adjustInputType(boolean password, boolean passwordInputType,
690            boolean webPasswordInputType, boolean numberPasswordInputType) {
691        // mInputType has been set from inputType, possibly modified by mInputMethod.
692        // Specialize mInputType to [web]password if we have a text class and the original input
693        // type was a password.
694        if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
695            if (password || passwordInputType) {
696                mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
697                        | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
698            }
699            if (webPasswordInputType) {
700                mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
701                        | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
702            }
703        } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
704            if (numberPasswordInputType) {
705                mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
706                        | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
707            }
708        }
709    }
710
711    private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
712        int wid = tv.getPaddingLeft() + tv.getPaddingRight();
713        int ht = tv.getPaddingTop() + tv.getPaddingBottom();
714
715        int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize(
716                com.android.internal.R.dimen.textview_error_popup_default_width);
717        Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels,
718                                    Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
719        float max = 0;
720        for (int i = 0; i < l.getLineCount(); i++) {
721            max = Math.max(max, l.getLineWidth(i));
722        }
723
724        /*
725         * Now set the popup size to be big enough for the text plus the border capped
726         * to DEFAULT_MAX_POPUP_WIDTH
727         */
728        pop.setWidth(wid + (int) Math.ceil(max));
729        pop.setHeight(ht + l.getHeight());
730    }
731
732    void setFrame() {
733        if (mErrorPopup != null) {
734            TextView tv = (TextView) mErrorPopup.getContentView();
735            chooseSize(mErrorPopup, mError, tv);
736            mErrorPopup.update(mTextView, getErrorX(), getErrorY(),
737                    mErrorPopup.getWidth(), mErrorPopup.getHeight());
738        }
739    }
740
741    private int getWordStart(int offset) {
742        // FIXME - For this and similar methods we're not doing anything to check if there's
743        // a LocaleSpan in the text, this may be something we should try handling or checking for.
744        int retOffset = getWordIteratorWithText().prevBoundary(offset);
745        if (getWordIteratorWithText().isOnPunctuation(retOffset)) {
746            // On punctuation boundary or within group of punctuation, find punctuation start.
747            retOffset = getWordIteratorWithText().getPunctuationBeginning(offset);
748        } else {
749            // Not on a punctuation boundary, find the word start.
750            retOffset = getWordIteratorWithText().getPrevWordBeginningOnTwoWordsBoundary(offset);
751        }
752        if (retOffset == BreakIterator.DONE) {
753            return offset;
754        }
755        return retOffset;
756    }
757
758    private int getWordEnd(int offset) {
759        int retOffset = getWordIteratorWithText().nextBoundary(offset);
760        if (getWordIteratorWithText().isAfterPunctuation(retOffset)) {
761            // On punctuation boundary or within group of punctuation, find punctuation end.
762            retOffset = getWordIteratorWithText().getPunctuationEnd(offset);
763        } else {
764            // Not on a punctuation boundary, find the word end.
765            retOffset = getWordIteratorWithText().getNextWordEndOnTwoWordBoundary(offset);
766        }
767        if (retOffset == BreakIterator.DONE) {
768            return offset;
769        }
770        return retOffset;
771    }
772
773    /**
774     * Adjusts selection to the word under last touch offset. Return true if the operation was
775     * successfully performed.
776     */
777    private boolean selectCurrentWord() {
778        if (!mTextView.canSelectText()) {
779            return false;
780        }
781
782        if (mTextView.hasPasswordTransformationMethod()) {
783            // Always select all on a password field.
784            // Cut/copy menu entries are not available for passwords, but being able to select all
785            // is however useful to delete or paste to replace the entire content.
786            return mTextView.selectAllText();
787        }
788
789        int inputType = mTextView.getInputType();
790        int klass = inputType & InputType.TYPE_MASK_CLASS;
791        int variation = inputType & InputType.TYPE_MASK_VARIATION;
792
793        // Specific text field types: select the entire text for these
794        if (klass == InputType.TYPE_CLASS_NUMBER ||
795                klass == InputType.TYPE_CLASS_PHONE ||
796                klass == InputType.TYPE_CLASS_DATETIME ||
797                variation == InputType.TYPE_TEXT_VARIATION_URI ||
798                variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
799                variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ||
800                variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
801            return mTextView.selectAllText();
802        }
803
804        long lastTouchOffsets = getLastTouchOffsets();
805        final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
806        final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
807
808        // Safety check in case standard touch event handling has been bypassed
809        if (minOffset < 0 || minOffset > mTextView.getText().length()) return false;
810        if (maxOffset < 0 || maxOffset > mTextView.getText().length()) return false;
811
812        int selectionStart, selectionEnd;
813
814        // If a URLSpan (web address, email, phone...) is found at that position, select it.
815        URLSpan[] urlSpans = ((Spanned) mTextView.getText()).
816                getSpans(minOffset, maxOffset, URLSpan.class);
817        if (urlSpans.length >= 1) {
818            URLSpan urlSpan = urlSpans[0];
819            selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan);
820            selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan);
821        } else {
822            // FIXME - We should check if there's a LocaleSpan in the text, this may be
823            // something we should try handling or checking for.
824            final WordIterator wordIterator = getWordIterator();
825            wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset);
826
827            selectionStart = wordIterator.getBeginning(minOffset);
828            selectionEnd = wordIterator.getEnd(maxOffset);
829
830            if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE ||
831                    selectionStart == selectionEnd) {
832                // Possible when the word iterator does not properly handle the text's language
833                long range = getCharClusterRange(minOffset);
834                selectionStart = TextUtils.unpackRangeStartFromLong(range);
835                selectionEnd = TextUtils.unpackRangeEndFromLong(range);
836            }
837        }
838
839        Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
840        return selectionEnd > selectionStart;
841    }
842
843    void onLocaleChanged() {
844        // Will be re-created on demand in getWordIterator with the proper new locale
845        mWordIterator = null;
846        mWordIteratorWithText = null;
847    }
848
849    /**
850     * @hide
851     */
852    public WordIterator getWordIterator() {
853        if (mWordIterator == null) {
854            mWordIterator = new WordIterator(mTextView.getTextServicesLocale());
855        }
856        return mWordIterator;
857    }
858
859    private WordIterator getWordIteratorWithText() {
860        if (mWordIteratorWithText == null) {
861            mWordIteratorWithText = new WordIterator(mTextView.getTextServicesLocale());
862            mUpdateWordIteratorText = true;
863        }
864        if (mUpdateWordIteratorText) {
865            // FIXME - Shouldn't copy all of the text as only the area of the text relevant
866            // to the user's selection is needed. A possible solution would be to
867            // copy some number N of characters near the selection and then when the
868            // user approaches N then we'd do another copy of the next N characters.
869            CharSequence text = mTextView.getText();
870            mWordIteratorWithText.setCharSequence(text, 0, text.length());
871            mUpdateWordIteratorText = false;
872        }
873        return mWordIteratorWithText;
874    }
875
876    private int getNextCursorOffset(int offset, boolean findAfterGivenOffset) {
877        final Layout layout = mTextView.getLayout();
878        if (layout == null) return offset;
879        final CharSequence text = mTextView.getText();
880        final int nextOffset = layout.getPaint().getTextRunCursor(text, 0, text.length(),
881                layout.isRtlCharAt(offset) ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR,
882                offset, findAfterGivenOffset ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE);
883        return nextOffset == -1 ? offset : nextOffset;
884    }
885
886    private long getCharClusterRange(int offset) {
887        final int textLength = mTextView.getText().length();
888        if (offset < textLength) {
889            return TextUtils.packRangeInLong(offset, getNextCursorOffset(offset, true));
890        }
891        if (offset - 1 >= 0) {
892            return TextUtils.packRangeInLong(getNextCursorOffset(offset, false), offset);
893        }
894        return TextUtils.packRangeInLong(offset, offset);
895    }
896
897    private boolean touchPositionIsInSelection() {
898        int selectionStart = mTextView.getSelectionStart();
899        int selectionEnd = mTextView.getSelectionEnd();
900
901        if (selectionStart == selectionEnd) {
902            return false;
903        }
904
905        if (selectionStart > selectionEnd) {
906            int tmp = selectionStart;
907            selectionStart = selectionEnd;
908            selectionEnd = tmp;
909            Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
910        }
911
912        SelectionModifierCursorController selectionController = getSelectionController();
913        int minOffset = selectionController.getMinTouchOffset();
914        int maxOffset = selectionController.getMaxTouchOffset();
915
916        return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
917    }
918
919    private PositionListener getPositionListener() {
920        if (mPositionListener == null) {
921            mPositionListener = new PositionListener();
922        }
923        return mPositionListener;
924    }
925
926    private interface TextViewPositionListener {
927        public void updatePosition(int parentPositionX, int parentPositionY,
928                boolean parentPositionChanged, boolean parentScrolled);
929    }
930
931    private boolean isPositionVisible(final float positionX, final float positionY) {
932        synchronized (TEMP_POSITION) {
933            final float[] position = TEMP_POSITION;
934            position[0] = positionX;
935            position[1] = positionY;
936            View view = mTextView;
937
938            while (view != null) {
939                if (view != mTextView) {
940                    // Local scroll is already taken into account in positionX/Y
941                    position[0] -= view.getScrollX();
942                    position[1] -= view.getScrollY();
943                }
944
945                if (position[0] < 0 || position[1] < 0 ||
946                        position[0] > view.getWidth() || position[1] > view.getHeight()) {
947                    return false;
948                }
949
950                if (!view.getMatrix().isIdentity()) {
951                    view.getMatrix().mapPoints(position);
952                }
953
954                position[0] += view.getLeft();
955                position[1] += view.getTop();
956
957                final ViewParent parent = view.getParent();
958                if (parent instanceof View) {
959                    view = (View) parent;
960                } else {
961                    // We've reached the ViewRoot, stop iterating
962                    view = null;
963                }
964            }
965        }
966
967        // We've been able to walk up the view hierarchy and the position was never clipped
968        return true;
969    }
970
971    private boolean isOffsetVisible(int offset) {
972        Layout layout = mTextView.getLayout();
973        if (layout == null) return false;
974
975        final int line = layout.getLineForOffset(offset);
976        final int lineBottom = layout.getLineBottom(line);
977        final int primaryHorizontal = (int) layout.getPrimaryHorizontal(offset);
978        return isPositionVisible(primaryHorizontal + mTextView.viewportToContentHorizontalOffset(),
979                lineBottom + mTextView.viewportToContentVerticalOffset());
980    }
981
982    /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
983     * in the view. Returns false when the position is in the empty space of left/right of text.
984     */
985    private boolean isPositionOnText(float x, float y) {
986        Layout layout = mTextView.getLayout();
987        if (layout == null) return false;
988
989        final int line = mTextView.getLineAtCoordinate(y);
990        x = mTextView.convertToLocalHorizontalCoordinate(x);
991
992        if (x < layout.getLineLeft(line)) return false;
993        if (x > layout.getLineRight(line)) return false;
994        return true;
995    }
996
997    public boolean performLongClick(boolean handled) {
998        // Long press in empty space moves cursor and starts the insertion action mode.
999        if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY) &&
1000                mInsertionControllerEnabled) {
1001            final int offset = mTextView.getOffsetForPosition(mLastDownPositionX,
1002                    mLastDownPositionY);
1003            stopTextActionMode();
1004            Selection.setSelection((Spannable) mTextView.getText(), offset);
1005            getInsertionController().show();
1006            mIsInsertionActionModeStartPending = true;
1007            handled = true;
1008        }
1009
1010        if (!handled && mTextActionMode != null) {
1011            if (touchPositionIsInSelection()) {
1012                // Start a drag
1013                final int start = mTextView.getSelectionStart();
1014                final int end = mTextView.getSelectionEnd();
1015                CharSequence selectedText = mTextView.getTransformedText(start, end);
1016                ClipData data = ClipData.newPlainText(null, selectedText);
1017                DragLocalState localState = new DragLocalState(mTextView, start, end);
1018                mTextView.startDrag(data, getTextThumbnailBuilder(selectedText), localState,
1019                        View.DRAG_FLAG_GLOBAL);
1020                stopTextActionMode();
1021            } else {
1022                stopTextActionMode();
1023                selectCurrentWordAndStartDrag();
1024            }
1025            handled = true;
1026        }
1027
1028        // Start a new selection
1029        if (!handled) {
1030            handled = selectCurrentWordAndStartDrag();
1031        }
1032
1033        return handled;
1034    }
1035
1036    private long getLastTouchOffsets() {
1037        SelectionModifierCursorController selectionController = getSelectionController();
1038        final int minOffset = selectionController.getMinTouchOffset();
1039        final int maxOffset = selectionController.getMaxTouchOffset();
1040        return TextUtils.packRangeInLong(minOffset, maxOffset);
1041    }
1042
1043    void onFocusChanged(boolean focused, int direction) {
1044        mShowCursor = SystemClock.uptimeMillis();
1045        ensureEndedBatchEdit();
1046
1047        if (focused) {
1048            int selStart = mTextView.getSelectionStart();
1049            int selEnd = mTextView.getSelectionEnd();
1050
1051            // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
1052            // mode for these, unless there was a specific selection already started.
1053            final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0 &&
1054                    selEnd == mTextView.getText().length();
1055
1056            mCreatedWithASelection = mFrozenWithFocus && mTextView.hasSelection() &&
1057                    !isFocusHighlighted;
1058
1059            if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
1060                // If a tap was used to give focus to that view, move cursor at tap position.
1061                // Has to be done before onTakeFocus, which can be overloaded.
1062                final int lastTapPosition = getLastTapPosition();
1063                if (lastTapPosition >= 0) {
1064                    Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition);
1065                }
1066
1067                // Note this may have to be moved out of the Editor class
1068                MovementMethod mMovement = mTextView.getMovementMethod();
1069                if (mMovement != null) {
1070                    mMovement.onTakeFocus(mTextView, (Spannable) mTextView.getText(), direction);
1071                }
1072
1073                // The DecorView does not have focus when the 'Done' ExtractEditText button is
1074                // pressed. Since it is the ViewAncestor's mView, it requests focus before
1075                // ExtractEditText clears focus, which gives focus to the ExtractEditText.
1076                // This special case ensure that we keep current selection in that case.
1077                // It would be better to know why the DecorView does not have focus at that time.
1078                if (((mTextView.isInExtractedMode()) || mSelectionMoved) &&
1079                        selStart >= 0 && selEnd >= 0) {
1080                    /*
1081                     * Someone intentionally set the selection, so let them
1082                     * do whatever it is that they wanted to do instead of
1083                     * the default on-focus behavior.  We reset the selection
1084                     * here instead of just skipping the onTakeFocus() call
1085                     * because some movement methods do something other than
1086                     * just setting the selection in theirs and we still
1087                     * need to go through that path.
1088                     */
1089                    Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
1090                }
1091
1092                if (mSelectAllOnFocus) {
1093                    mTextView.selectAllText();
1094                }
1095
1096                mTouchFocusSelected = true;
1097            }
1098
1099            mFrozenWithFocus = false;
1100            mSelectionMoved = false;
1101
1102            if (mError != null) {
1103                showError();
1104            }
1105
1106            makeBlink();
1107        } else {
1108            if (mError != null) {
1109                hideError();
1110            }
1111            // Don't leave us in the middle of a batch edit.
1112            mTextView.onEndBatchEdit();
1113
1114            if (mTextView.isInExtractedMode()) {
1115                // terminateTextSelectionMode removes selection, which we want to keep when
1116                // ExtractEditText goes out of focus.
1117                final int selStart = mTextView.getSelectionStart();
1118                final int selEnd = mTextView.getSelectionEnd();
1119                hideCursorAndSpanControllers();
1120                stopTextActionMode();
1121                Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
1122            } else {
1123                if (mTemporaryDetach) mPreserveDetachedSelection = true;
1124                hideCursorAndSpanControllers();
1125                stopTextActionMode();
1126                if (mTemporaryDetach) mPreserveDetachedSelection = false;
1127                downgradeEasyCorrectionSpans();
1128            }
1129            // No need to create the controller
1130            if (mSelectionModifierCursorController != null) {
1131                mSelectionModifierCursorController.resetTouchOffsets();
1132            }
1133        }
1134    }
1135
1136    /**
1137     * Downgrades to simple suggestions all the easy correction spans that are not a spell check
1138     * span.
1139     */
1140    private void downgradeEasyCorrectionSpans() {
1141        CharSequence text = mTextView.getText();
1142        if (text instanceof Spannable) {
1143            Spannable spannable = (Spannable) text;
1144            SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
1145                    spannable.length(), SuggestionSpan.class);
1146            for (int i = 0; i < suggestionSpans.length; i++) {
1147                int flags = suggestionSpans[i].getFlags();
1148                if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
1149                        && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
1150                    flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
1151                    suggestionSpans[i].setFlags(flags);
1152                }
1153            }
1154        }
1155    }
1156
1157    void sendOnTextChanged(int start, int after) {
1158        updateSpellCheckSpans(start, start + after, false);
1159
1160        // Flip flag to indicate the word iterator needs to have the text reset.
1161        mUpdateWordIteratorText = true;
1162
1163        // Hide the controllers as soon as text is modified (typing, procedural...)
1164        // We do not hide the span controllers, since they can be added when a new text is
1165        // inserted into the text view (voice IME).
1166        hideCursorControllers();
1167        // Reset drag accelerator.
1168        if (mSelectionModifierCursorController != null) {
1169            mSelectionModifierCursorController.resetTouchOffsets();
1170        }
1171        stopTextActionMode();
1172    }
1173
1174    private int getLastTapPosition() {
1175        // No need to create the controller at that point, no last tap position saved
1176        if (mSelectionModifierCursorController != null) {
1177            int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
1178            if (lastTapPosition >= 0) {
1179                // Safety check, should not be possible.
1180                if (lastTapPosition > mTextView.getText().length()) {
1181                    lastTapPosition = mTextView.getText().length();
1182                }
1183                return lastTapPosition;
1184            }
1185        }
1186
1187        return -1;
1188    }
1189
1190    void onWindowFocusChanged(boolean hasWindowFocus) {
1191        if (hasWindowFocus) {
1192            if (mBlink != null) {
1193                mBlink.uncancel();
1194                makeBlink();
1195            }
1196            final InputMethodManager imm = InputMethodManager.peekInstance();
1197            final boolean immFullScreen = (imm != null && imm.isFullscreenMode());
1198            if (mSelectionModifierCursorController != null && mTextView.hasSelection()
1199                    && !immFullScreen && mTextActionMode != null) {
1200                mSelectionModifierCursorController.show();
1201            }
1202        } else {
1203            if (mBlink != null) {
1204                mBlink.cancel();
1205            }
1206            if (mInputContentType != null) {
1207                mInputContentType.enterDown = false;
1208            }
1209            // Order matters! Must be done before onParentLostFocus to rely on isShowingUp
1210            hideCursorAndSpanControllers();
1211            if (mSelectionModifierCursorController != null) {
1212                mSelectionModifierCursorController.hide();
1213            }
1214            if (mSuggestionsPopupWindow != null) {
1215                mSuggestionsPopupWindow.onParentLostFocus();
1216            }
1217
1218            // Don't leave us in the middle of a batch edit. Same as in onFocusChanged
1219            ensureEndedBatchEdit();
1220        }
1221    }
1222
1223    void onTouchEvent(MotionEvent event) {
1224        updateFloatingToolbarVisibility(event);
1225
1226        if (hasSelectionController()) {
1227            getSelectionController().onTouchEvent(event);
1228        }
1229
1230        if (mShowSuggestionRunnable != null) {
1231            mTextView.removeCallbacks(mShowSuggestionRunnable);
1232            mShowSuggestionRunnable = null;
1233        }
1234
1235        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
1236            mLastDownPositionX = event.getX();
1237            mLastDownPositionY = event.getY();
1238
1239            // Reset this state; it will be re-set if super.onTouchEvent
1240            // causes focus to move to the view.
1241            mTouchFocusSelected = false;
1242            mIgnoreActionUpEvent = false;
1243        }
1244    }
1245
1246    private void updateFloatingToolbarVisibility(MotionEvent event) {
1247        if (mTextActionMode != null) {
1248            switch (event.getActionMasked()) {
1249                case MotionEvent.ACTION_MOVE:
1250                    hideFloatingToolbar();
1251                    break;
1252                case MotionEvent.ACTION_UP:  // fall through
1253                case MotionEvent.ACTION_CANCEL:
1254                    showFloatingToolbar();
1255            }
1256        }
1257    }
1258
1259    private void hideFloatingToolbar() {
1260        if (mTextActionMode != null) {
1261            mTextView.removeCallbacks(mShowFloatingToolbar);
1262            mTextActionMode.hide(ActionMode.DEFAULT_HIDE_DURATION);
1263        }
1264    }
1265
1266    private void showFloatingToolbar() {
1267        if (mTextActionMode != null) {
1268            // Delay "show" so it doesn't interfere with click confirmations
1269            // or double-clicks that could "dismiss" the floating toolbar.
1270            int delay = ViewConfiguration.getDoubleTapTimeout();
1271            mTextView.postDelayed(mShowFloatingToolbar, delay);
1272        }
1273    }
1274
1275    public void beginBatchEdit() {
1276        mInBatchEditControllers = true;
1277        final InputMethodState ims = mInputMethodState;
1278        if (ims != null) {
1279            int nesting = ++ims.mBatchEditNesting;
1280            if (nesting == 1) {
1281                ims.mCursorChanged = false;
1282                ims.mChangedDelta = 0;
1283                if (ims.mContentChanged) {
1284                    // We already have a pending change from somewhere else,
1285                    // so turn this into a full update.
1286                    ims.mChangedStart = 0;
1287                    ims.mChangedEnd = mTextView.getText().length();
1288                } else {
1289                    ims.mChangedStart = EXTRACT_UNKNOWN;
1290                    ims.mChangedEnd = EXTRACT_UNKNOWN;
1291                    ims.mContentChanged = false;
1292                }
1293                mUndoInputFilter.beginBatchEdit();
1294                mTextView.onBeginBatchEdit();
1295            }
1296        }
1297    }
1298
1299    public void endBatchEdit() {
1300        mInBatchEditControllers = false;
1301        final InputMethodState ims = mInputMethodState;
1302        if (ims != null) {
1303            int nesting = --ims.mBatchEditNesting;
1304            if (nesting == 0) {
1305                finishBatchEdit(ims);
1306            }
1307        }
1308    }
1309
1310    void ensureEndedBatchEdit() {
1311        final InputMethodState ims = mInputMethodState;
1312        if (ims != null && ims.mBatchEditNesting != 0) {
1313            ims.mBatchEditNesting = 0;
1314            finishBatchEdit(ims);
1315        }
1316    }
1317
1318    void finishBatchEdit(final InputMethodState ims) {
1319        mTextView.onEndBatchEdit();
1320        mUndoInputFilter.endBatchEdit();
1321
1322        if (ims.mContentChanged || ims.mSelectionModeChanged) {
1323            mTextView.updateAfterEdit();
1324            reportExtractedText();
1325        } else if (ims.mCursorChanged) {
1326            // Cheesy way to get us to report the current cursor location.
1327            mTextView.invalidateCursor();
1328        }
1329        // sendUpdateSelection knows to avoid sending if the selection did
1330        // not actually change.
1331        sendUpdateSelection();
1332    }
1333
1334    static final int EXTRACT_NOTHING = -2;
1335    static final int EXTRACT_UNKNOWN = -1;
1336
1337    boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
1338        return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
1339                EXTRACT_UNKNOWN, outText);
1340    }
1341
1342    private boolean extractTextInternal(@Nullable ExtractedTextRequest request,
1343            int partialStartOffset, int partialEndOffset, int delta,
1344            @Nullable ExtractedText outText) {
1345        if (request == null || outText == null) {
1346            return false;
1347        }
1348
1349        final CharSequence content = mTextView.getText();
1350        if (content == null) {
1351            return false;
1352        }
1353
1354        if (partialStartOffset != EXTRACT_NOTHING) {
1355            final int N = content.length();
1356            if (partialStartOffset < 0) {
1357                outText.partialStartOffset = outText.partialEndOffset = -1;
1358                partialStartOffset = 0;
1359                partialEndOffset = N;
1360            } else {
1361                // Now use the delta to determine the actual amount of text
1362                // we need.
1363                partialEndOffset += delta;
1364                // Adjust offsets to ensure we contain full spans.
1365                if (content instanceof Spanned) {
1366                    Spanned spanned = (Spanned)content;
1367                    Object[] spans = spanned.getSpans(partialStartOffset,
1368                            partialEndOffset, ParcelableSpan.class);
1369                    int i = spans.length;
1370                    while (i > 0) {
1371                        i--;
1372                        int j = spanned.getSpanStart(spans[i]);
1373                        if (j < partialStartOffset) partialStartOffset = j;
1374                        j = spanned.getSpanEnd(spans[i]);
1375                        if (j > partialEndOffset) partialEndOffset = j;
1376                    }
1377                }
1378                outText.partialStartOffset = partialStartOffset;
1379                outText.partialEndOffset = partialEndOffset - delta;
1380
1381                if (partialStartOffset > N) {
1382                    partialStartOffset = N;
1383                } else if (partialStartOffset < 0) {
1384                    partialStartOffset = 0;
1385                }
1386                if (partialEndOffset > N) {
1387                    partialEndOffset = N;
1388                } else if (partialEndOffset < 0) {
1389                    partialEndOffset = 0;
1390                }
1391            }
1392            if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) {
1393                outText.text = content.subSequence(partialStartOffset,
1394                        partialEndOffset);
1395            } else {
1396                outText.text = TextUtils.substring(content, partialStartOffset,
1397                        partialEndOffset);
1398            }
1399        } else {
1400            outText.partialStartOffset = 0;
1401            outText.partialEndOffset = 0;
1402            outText.text = "";
1403        }
1404        outText.flags = 0;
1405        if (MetaKeyKeyListener.getMetaState(content, MetaKeyKeyListener.META_SELECTING) != 0) {
1406            outText.flags |= ExtractedText.FLAG_SELECTING;
1407        }
1408        if (mTextView.isSingleLine()) {
1409            outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
1410        }
1411        outText.startOffset = 0;
1412        outText.selectionStart = mTextView.getSelectionStart();
1413        outText.selectionEnd = mTextView.getSelectionEnd();
1414        return true;
1415    }
1416
1417    boolean reportExtractedText() {
1418        final Editor.InputMethodState ims = mInputMethodState;
1419        if (ims != null) {
1420            final boolean contentChanged = ims.mContentChanged;
1421            if (contentChanged || ims.mSelectionModeChanged) {
1422                ims.mContentChanged = false;
1423                ims.mSelectionModeChanged = false;
1424                final ExtractedTextRequest req = ims.mExtractedTextRequest;
1425                if (req != null) {
1426                    InputMethodManager imm = InputMethodManager.peekInstance();
1427                    if (imm != null) {
1428                        if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG,
1429                                "Retrieving extracted start=" + ims.mChangedStart +
1430                                " end=" + ims.mChangedEnd +
1431                                " delta=" + ims.mChangedDelta);
1432                        if (ims.mChangedStart < 0 && !contentChanged) {
1433                            ims.mChangedStart = EXTRACT_NOTHING;
1434                        }
1435                        if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
1436                                ims.mChangedDelta, ims.mExtractedText)) {
1437                            if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG,
1438                                    "Reporting extracted start=" +
1439                                    ims.mExtractedText.partialStartOffset +
1440                                    " end=" + ims.mExtractedText.partialEndOffset +
1441                                    ": " + ims.mExtractedText.text);
1442
1443                            imm.updateExtractedText(mTextView, req.token, ims.mExtractedText);
1444                            ims.mChangedStart = EXTRACT_UNKNOWN;
1445                            ims.mChangedEnd = EXTRACT_UNKNOWN;
1446                            ims.mChangedDelta = 0;
1447                            ims.mContentChanged = false;
1448                            return true;
1449                        }
1450                    }
1451                }
1452            }
1453        }
1454        return false;
1455    }
1456
1457    private void sendUpdateSelection() {
1458        if (null != mInputMethodState && mInputMethodState.mBatchEditNesting <= 0) {
1459            final InputMethodManager imm = InputMethodManager.peekInstance();
1460            if (null != imm) {
1461                final int selectionStart = mTextView.getSelectionStart();
1462                final int selectionEnd = mTextView.getSelectionEnd();
1463                int candStart = -1;
1464                int candEnd = -1;
1465                if (mTextView.getText() instanceof Spannable) {
1466                    final Spannable sp = (Spannable) mTextView.getText();
1467                    candStart = EditableInputConnection.getComposingSpanStart(sp);
1468                    candEnd = EditableInputConnection.getComposingSpanEnd(sp);
1469                }
1470                // InputMethodManager#updateSelection skips sending the message if
1471                // none of the parameters have changed since the last time we called it.
1472                imm.updateSelection(mTextView,
1473                        selectionStart, selectionEnd, candStart, candEnd);
1474            }
1475        }
1476    }
1477
1478    void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint,
1479            int cursorOffsetVertical) {
1480        final int selectionStart = mTextView.getSelectionStart();
1481        final int selectionEnd = mTextView.getSelectionEnd();
1482
1483        final InputMethodState ims = mInputMethodState;
1484        if (ims != null && ims.mBatchEditNesting == 0) {
1485            InputMethodManager imm = InputMethodManager.peekInstance();
1486            if (imm != null) {
1487                if (imm.isActive(mTextView)) {
1488                    if (ims.mContentChanged || ims.mSelectionModeChanged) {
1489                        // We are in extract mode and the content has changed
1490                        // in some way... just report complete new text to the
1491                        // input method.
1492                        reportExtractedText();
1493                    }
1494                }
1495            }
1496        }
1497
1498        if (mCorrectionHighlighter != null) {
1499            mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
1500        }
1501
1502        if (highlight != null && selectionStart == selectionEnd && mCursorCount > 0) {
1503            drawCursor(canvas, cursorOffsetVertical);
1504            // Rely on the drawable entirely, do not draw the cursor line.
1505            // Has to be done after the IMM related code above which relies on the highlight.
1506            highlight = null;
1507        }
1508
1509        if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
1510            drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
1511                    cursorOffsetVertical);
1512        } else {
1513            layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
1514        }
1515    }
1516
1517    private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
1518            Paint highlightPaint, int cursorOffsetVertical) {
1519        final long lineRange = layout.getLineRangeForDraw(canvas);
1520        int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
1521        int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
1522        if (lastLine < 0) return;
1523
1524        layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
1525                firstLine, lastLine);
1526
1527        if (layout instanceof DynamicLayout) {
1528            if (mTextRenderNodes == null) {
1529                mTextRenderNodes = ArrayUtils.emptyArray(TextRenderNode.class);
1530            }
1531
1532            DynamicLayout dynamicLayout = (DynamicLayout) layout;
1533            int[] blockEndLines = dynamicLayout.getBlockEndLines();
1534            int[] blockIndices = dynamicLayout.getBlockIndices();
1535            final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
1536            final int indexFirstChangedBlock = dynamicLayout.getIndexFirstChangedBlock();
1537
1538            int endOfPreviousBlock = -1;
1539            int searchStartIndex = 0;
1540            for (int i = 0; i < numberOfBlocks; i++) {
1541                int blockEndLine = blockEndLines[i];
1542                int blockIndex = blockIndices[i];
1543
1544                final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
1545                if (blockIsInvalid) {
1546                    blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
1547                            searchStartIndex);
1548                    // Note how dynamic layout's internal block indices get updated from Editor
1549                    blockIndices[i] = blockIndex;
1550                    if (mTextRenderNodes[blockIndex] != null) {
1551                        mTextRenderNodes[blockIndex].isDirty = true;
1552                    }
1553                    searchStartIndex = blockIndex + 1;
1554                }
1555
1556                if (mTextRenderNodes[blockIndex] == null) {
1557                    mTextRenderNodes[blockIndex] =
1558                            new TextRenderNode("Text " + blockIndex);
1559                }
1560
1561                final boolean blockDisplayListIsInvalid = mTextRenderNodes[blockIndex].needsRecord();
1562                RenderNode blockDisplayList = mTextRenderNodes[blockIndex].renderNode;
1563                if (i >= indexFirstChangedBlock || blockDisplayListIsInvalid) {
1564                    final int blockBeginLine = endOfPreviousBlock + 1;
1565                    final int top = layout.getLineTop(blockBeginLine);
1566                    final int bottom = layout.getLineBottom(blockEndLine);
1567                    int left = 0;
1568                    int right = mTextView.getWidth();
1569                    if (mTextView.getHorizontallyScrolling()) {
1570                        float min = Float.MAX_VALUE;
1571                        float max = Float.MIN_VALUE;
1572                        for (int line = blockBeginLine; line <= blockEndLine; line++) {
1573                            min = Math.min(min, layout.getLineLeft(line));
1574                            max = Math.max(max, layout.getLineRight(line));
1575                        }
1576                        left = (int) min;
1577                        right = (int) (max + 0.5f);
1578                    }
1579
1580                    // Rebuild display list if it is invalid
1581                    if (blockDisplayListIsInvalid) {
1582                        final DisplayListCanvas displayListCanvas = blockDisplayList.start(
1583                                right - left, bottom - top);
1584                        try {
1585                            // drawText is always relative to TextView's origin, this translation
1586                            // brings this range of text back to the top left corner of the viewport
1587                            displayListCanvas.translate(-left, -top);
1588                            layout.drawText(displayListCanvas, blockBeginLine, blockEndLine);
1589                            mTextRenderNodes[blockIndex].isDirty = false;
1590                            // No need to untranslate, previous context is popped after
1591                            // drawDisplayList
1592                        } finally {
1593                            blockDisplayList.end(displayListCanvas);
1594                            // Same as drawDisplayList below, handled by our TextView's parent
1595                            blockDisplayList.setClipToBounds(false);
1596                        }
1597                    }
1598
1599                    // Valid disply list whose index is >= indexFirstChangedBlock
1600                    // only needs to update its drawing location.
1601                    blockDisplayList.setLeftTopRightBottom(left, top, right, bottom);
1602                }
1603
1604                ((DisplayListCanvas) canvas).drawRenderNode(blockDisplayList);
1605
1606                endOfPreviousBlock = blockEndLine;
1607            }
1608
1609            dynamicLayout.setIndexFirstChangedBlock(numberOfBlocks);
1610        } else {
1611            // Boring layout is used for empty and hint text
1612            layout.drawText(canvas, firstLine, lastLine);
1613        }
1614    }
1615
1616    private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
1617            int searchStartIndex) {
1618        int length = mTextRenderNodes.length;
1619        for (int i = searchStartIndex; i < length; i++) {
1620            boolean blockIndexFound = false;
1621            for (int j = 0; j < numberOfBlocks; j++) {
1622                if (blockIndices[j] == i) {
1623                    blockIndexFound = true;
1624                    break;
1625                }
1626            }
1627            if (blockIndexFound) continue;
1628            return i;
1629        }
1630
1631        // No available index found, the pool has to grow
1632        mTextRenderNodes = GrowingArrayUtils.append(mTextRenderNodes, length, null);
1633        return length;
1634    }
1635
1636    private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
1637        final boolean translate = cursorOffsetVertical != 0;
1638        if (translate) canvas.translate(0, cursorOffsetVertical);
1639        for (int i = 0; i < mCursorCount; i++) {
1640            mCursorDrawable[i].draw(canvas);
1641        }
1642        if (translate) canvas.translate(0, -cursorOffsetVertical);
1643    }
1644
1645    /**
1646     * Invalidates all the sub-display lists that overlap the specified character range
1647     */
1648    void invalidateTextDisplayList(Layout layout, int start, int end) {
1649        if (mTextRenderNodes != null && layout instanceof DynamicLayout) {
1650            final int firstLine = layout.getLineForOffset(start);
1651            final int lastLine = layout.getLineForOffset(end);
1652
1653            DynamicLayout dynamicLayout = (DynamicLayout) layout;
1654            int[] blockEndLines = dynamicLayout.getBlockEndLines();
1655            int[] blockIndices = dynamicLayout.getBlockIndices();
1656            final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
1657
1658            int i = 0;
1659            // Skip the blocks before firstLine
1660            while (i < numberOfBlocks) {
1661                if (blockEndLines[i] >= firstLine) break;
1662                i++;
1663            }
1664
1665            // Invalidate all subsequent blocks until lastLine is passed
1666            while (i < numberOfBlocks) {
1667                final int blockIndex = blockIndices[i];
1668                if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX) {
1669                    mTextRenderNodes[blockIndex].isDirty = true;
1670                }
1671                if (blockEndLines[i] >= lastLine) break;
1672                i++;
1673            }
1674        }
1675    }
1676
1677    void invalidateTextDisplayList() {
1678        if (mTextRenderNodes != null) {
1679            for (int i = 0; i < mTextRenderNodes.length; i++) {
1680                if (mTextRenderNodes[i] != null) mTextRenderNodes[i].isDirty = true;
1681            }
1682        }
1683    }
1684
1685    void updateCursorsPositions() {
1686        if (mTextView.mCursorDrawableRes == 0) {
1687            mCursorCount = 0;
1688            return;
1689        }
1690
1691        Layout layout = getActiveLayout();
1692        final int offset = mTextView.getSelectionStart();
1693        final int line = layout.getLineForOffset(offset);
1694        final int top = layout.getLineTop(line);
1695        final int bottom = layout.getLineTop(line + 1);
1696
1697        mCursorCount = layout.isLevelBoundary(offset) ? 2 : 1;
1698
1699        int middle = bottom;
1700        if (mCursorCount == 2) {
1701            // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)}
1702            middle = (top + bottom) >> 1;
1703        }
1704
1705        boolean clamped = layout.shouldClampCursor(line);
1706        updateCursorPosition(0, top, middle, layout.getPrimaryHorizontal(offset, clamped));
1707
1708        if (mCursorCount == 2) {
1709            updateCursorPosition(1, middle, bottom,
1710                    layout.getSecondaryHorizontal(offset, clamped));
1711        }
1712    }
1713
1714    /**
1715     * Start an Insertion action mode.
1716     */
1717    void startInsertionActionMode() {
1718        if (mInsertionActionModeRunnable != null) {
1719            mTextView.removeCallbacks(mInsertionActionModeRunnable);
1720        }
1721        if (extractedTextModeWillBeStarted()) {
1722            return;
1723        }
1724        stopTextActionMode();
1725
1726        ActionMode.Callback actionModeCallback =
1727                new TextActionModeCallback(false /* hasSelection */);
1728        mTextActionMode = mTextView.startActionMode(
1729                actionModeCallback, ActionMode.TYPE_FLOATING);
1730        if (mTextActionMode != null && getInsertionController() != null) {
1731            getInsertionController().show();
1732        }
1733    }
1734
1735    /**
1736     * Starts a Selection Action Mode with the current selection and ensures the selection handles
1737     * are shown if there is a selection, otherwise the insertion handle is shown. This should be
1738     * used when the mode is started from a non-touch event.
1739     *
1740     * @return true if the selection mode was actually started.
1741     */
1742    boolean startSelectionActionMode() {
1743        boolean selectionStarted = startSelectionActionModeInternal();
1744        if (selectionStarted) {
1745            getSelectionController().show();
1746        } else if (getInsertionController() != null) {
1747            getInsertionController().show();
1748        }
1749        return selectionStarted;
1750    }
1751
1752    /**
1753     * If the TextView allows text selection, selects the current word when no existing selection
1754     * was available and starts a drag.
1755     *
1756     * @return true if the drag was started.
1757     */
1758    private boolean selectCurrentWordAndStartDrag() {
1759        if (mInsertionActionModeRunnable != null) {
1760            mTextView.removeCallbacks(mInsertionActionModeRunnable);
1761        }
1762        if (extractedTextModeWillBeStarted()) {
1763            return false;
1764        }
1765        if (mTextActionMode != null) {
1766            mTextActionMode.finish();
1767        }
1768        if (!checkFieldAndSelectCurrentWord()) {
1769            return false;
1770        }
1771
1772        // Avoid dismissing the selection if it exists.
1773        mPreserveDetachedSelection = true;
1774        stopTextActionMode();
1775        mPreserveDetachedSelection = false;
1776
1777        getSelectionController().enterDrag(
1778                SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_WORD);
1779        return true;
1780    }
1781
1782    /**
1783     * Checks whether a selection can be performed on the current TextView and if so selects
1784     * the current word.
1785     *
1786     * @return true if there already was a selection or if the current word was selected.
1787     */
1788    boolean checkFieldAndSelectCurrentWord() {
1789        if (!mTextView.canSelectText() || !mTextView.requestFocus()) {
1790            Log.w(TextView.LOG_TAG,
1791                    "TextView does not support text selection. Selection cancelled.");
1792            return false;
1793        }
1794
1795        if (!mTextView.hasSelection()) {
1796            // There may already be a selection on device rotation
1797            return selectCurrentWord();
1798        }
1799        return true;
1800    }
1801
1802    private boolean startSelectionActionModeInternal() {
1803        if (mTextActionMode != null) {
1804            // Text action mode is already started
1805            mTextActionMode.invalidate();
1806            return false;
1807        }
1808
1809        if (!checkFieldAndSelectCurrentWord()) {
1810            return false;
1811        }
1812
1813        boolean willExtract = extractedTextModeWillBeStarted();
1814
1815        // Do not start the action mode when extracted text will show up full screen, which would
1816        // immediately hide the newly created action bar and would be visually distracting.
1817        if (!willExtract) {
1818            ActionMode.Callback actionModeCallback =
1819                    new TextActionModeCallback(true /* hasSelection */);
1820            mTextActionMode = mTextView.startActionMode(
1821                    actionModeCallback, ActionMode.TYPE_FLOATING);
1822        }
1823
1824        final boolean selectionStarted = mTextActionMode != null || willExtract;
1825        if (selectionStarted && !mTextView.isTextSelectable() && mShowSoftInputOnFocus) {
1826            // Show the IME to be able to replace text, except when selecting non editable text.
1827            final InputMethodManager imm = InputMethodManager.peekInstance();
1828            if (imm != null) {
1829                imm.showSoftInput(mTextView, 0, null);
1830            }
1831        }
1832        return selectionStarted;
1833    }
1834
1835    boolean extractedTextModeWillBeStarted() {
1836        if (!(mTextView.isInExtractedMode())) {
1837            final InputMethodManager imm = InputMethodManager.peekInstance();
1838            return  imm != null && imm.isFullscreenMode();
1839        }
1840        return false;
1841    }
1842
1843    /**
1844     * @return <code>true</code> if it's reasonable to offer to show suggestions depending on
1845     * the current cursor position or selection range. This method is consistent with the
1846     * method to show suggestions {@link SuggestionsPopupWindow#updateSuggestions}.
1847     */
1848    private boolean shouldOfferToShowSuggestions() {
1849        CharSequence text = mTextView.getText();
1850        if (!(text instanceof Spannable)) return false;
1851
1852        final Spannable spannable = (Spannable) text;
1853        final int selectionStart = mTextView.getSelectionStart();
1854        final int selectionEnd = mTextView.getSelectionEnd();
1855        final SuggestionSpan[] suggestionSpans = spannable.getSpans(selectionStart, selectionEnd,
1856                SuggestionSpan.class);
1857        if (suggestionSpans.length == 0) {
1858            return false;
1859        }
1860        if (selectionStart == selectionEnd) {
1861            // Spans overlap the cursor.
1862            for (int i = 0; i < suggestionSpans.length; i++) {
1863                if (suggestionSpans[i].getSuggestions().length > 0) {
1864                    return true;
1865                }
1866            }
1867            return false;
1868        }
1869        int minSpanStart = mTextView.getText().length();
1870        int maxSpanEnd = 0;
1871        int unionOfSpansCoveringSelectionStartStart = mTextView.getText().length();
1872        int unionOfSpansCoveringSelectionStartEnd = 0;
1873        boolean hasValidSuggestions = false;
1874        for (int i = 0; i < suggestionSpans.length; i++) {
1875            final int spanStart = spannable.getSpanStart(suggestionSpans[i]);
1876            final int spanEnd = spannable.getSpanEnd(suggestionSpans[i]);
1877            minSpanStart = Math.min(minSpanStart, spanStart);
1878            maxSpanEnd = Math.max(maxSpanEnd, spanEnd);
1879            if (selectionStart < spanStart || selectionStart > spanEnd) {
1880                // The span doesn't cover the current selection start point.
1881                continue;
1882            }
1883            hasValidSuggestions =
1884                    hasValidSuggestions || suggestionSpans[i].getSuggestions().length > 0;
1885            unionOfSpansCoveringSelectionStartStart =
1886                    Math.min(unionOfSpansCoveringSelectionStartStart, spanStart);
1887            unionOfSpansCoveringSelectionStartEnd =
1888                    Math.max(unionOfSpansCoveringSelectionStartEnd, spanEnd);
1889        }
1890        if (!hasValidSuggestions) {
1891            return false;
1892        }
1893        if (unionOfSpansCoveringSelectionStartStart >= unionOfSpansCoveringSelectionStartEnd) {
1894            // No spans cover the selection start point.
1895            return false;
1896        }
1897        if (minSpanStart < unionOfSpansCoveringSelectionStartStart
1898                || maxSpanEnd > unionOfSpansCoveringSelectionStartEnd) {
1899            // There is a span that is not covered by the union. In this case, we soouldn't offer
1900            // to show suggestions as it's confusing.
1901            return false;
1902        }
1903        return true;
1904    }
1905
1906    /**
1907     * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
1908     * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
1909     */
1910    private boolean isCursorInsideEasyCorrectionSpan() {
1911        Spannable spannable = (Spannable) mTextView.getText();
1912        SuggestionSpan[] suggestionSpans = spannable.getSpans(mTextView.getSelectionStart(),
1913                mTextView.getSelectionEnd(), SuggestionSpan.class);
1914        for (int i = 0; i < suggestionSpans.length; i++) {
1915            if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
1916                return true;
1917            }
1918        }
1919        return false;
1920    }
1921
1922    void onTouchUpEvent(MotionEvent event) {
1923        boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect();
1924        hideCursorAndSpanControllers();
1925        stopTextActionMode();
1926        CharSequence text = mTextView.getText();
1927        if (!selectAllGotFocus && text.length() > 0) {
1928            // Move cursor
1929            final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
1930            Selection.setSelection((Spannable) text, offset);
1931            if (mSpellChecker != null) {
1932                // When the cursor moves, the word that was typed may need spell check
1933                mSpellChecker.onSelectionChanged();
1934            }
1935
1936            if (!extractedTextModeWillBeStarted()) {
1937                if (isCursorInsideEasyCorrectionSpan()) {
1938                    // Cancel the single tap delayed runnable.
1939                    if (mInsertionActionModeRunnable != null) {
1940                        mTextView.removeCallbacks(mInsertionActionModeRunnable);
1941                    }
1942
1943                    mShowSuggestionRunnable = new Runnable() {
1944                        public void run() {
1945                            showSuggestions();
1946                        }
1947                    };
1948                    // removeCallbacks is performed on every touch
1949                    mTextView.postDelayed(mShowSuggestionRunnable,
1950                            ViewConfiguration.getDoubleTapTimeout());
1951                } else if (hasInsertionController()) {
1952                    getInsertionController().show();
1953                }
1954            }
1955        }
1956    }
1957
1958    protected void stopTextActionMode() {
1959        if (mTextActionMode != null) {
1960            // This will hide the mSelectionModifierCursorController
1961            mTextActionMode.finish();
1962        }
1963    }
1964
1965    /**
1966     * @return True if this view supports insertion handles.
1967     */
1968    boolean hasInsertionController() {
1969        return mInsertionControllerEnabled;
1970    }
1971
1972    /**
1973     * @return True if this view supports selection handles.
1974     */
1975    boolean hasSelectionController() {
1976        return mSelectionControllerEnabled;
1977    }
1978
1979    InsertionPointCursorController getInsertionController() {
1980        if (!mInsertionControllerEnabled) {
1981            return null;
1982        }
1983
1984        if (mInsertionPointCursorController == null) {
1985            mInsertionPointCursorController = new InsertionPointCursorController();
1986
1987            final ViewTreeObserver observer = mTextView.getViewTreeObserver();
1988            observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
1989        }
1990
1991        return mInsertionPointCursorController;
1992    }
1993
1994    SelectionModifierCursorController getSelectionController() {
1995        if (!mSelectionControllerEnabled) {
1996            return null;
1997        }
1998
1999        if (mSelectionModifierCursorController == null) {
2000            mSelectionModifierCursorController = new SelectionModifierCursorController();
2001
2002            final ViewTreeObserver observer = mTextView.getViewTreeObserver();
2003            observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
2004        }
2005
2006        return mSelectionModifierCursorController;
2007    }
2008
2009    private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
2010        if (mCursorDrawable[cursorIndex] == null)
2011            mCursorDrawable[cursorIndex] = mTextView.getContext().getDrawable(
2012                    mTextView.mCursorDrawableRes);
2013
2014        if (mTempRect == null) mTempRect = new Rect();
2015        mCursorDrawable[cursorIndex].getPadding(mTempRect);
2016        final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth();
2017        horizontal = Math.max(0.5f, horizontal - 0.5f);
2018        final int left = (int) (horizontal) - mTempRect.left;
2019        mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width,
2020                bottom + mTempRect.bottom);
2021    }
2022
2023    /**
2024     * Called by the framework in response to a text auto-correction (such as fixing a typo using a
2025     * a dictionary) from the current input method, provided by it calling
2026     * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
2027     * implementation flashes the background of the corrected word to provide feedback to the user.
2028     *
2029     * @param info The auto correct info about the text that was corrected.
2030     */
2031    public void onCommitCorrection(CorrectionInfo info) {
2032        if (mCorrectionHighlighter == null) {
2033            mCorrectionHighlighter = new CorrectionHighlighter();
2034        } else {
2035            mCorrectionHighlighter.invalidate(false);
2036        }
2037
2038        mCorrectionHighlighter.highlight(info);
2039    }
2040
2041    void showSuggestions() {
2042        if (mSuggestionsPopupWindow == null) {
2043            mSuggestionsPopupWindow = new SuggestionsPopupWindow();
2044        }
2045        hideCursorAndSpanControllers();
2046        stopTextActionMode();
2047        mSuggestionsPopupWindow.show();
2048    }
2049
2050    void onScrollChanged() {
2051        if (mPositionListener != null) {
2052            mPositionListener.onScrollChanged();
2053        }
2054        if (mTextActionMode != null) {
2055            mTextActionMode.invalidateContentRect();
2056        }
2057    }
2058
2059    /**
2060     * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
2061     */
2062    private boolean shouldBlink() {
2063        if (!isCursorVisible() || !mTextView.isFocused()) return false;
2064
2065        final int start = mTextView.getSelectionStart();
2066        if (start < 0) return false;
2067
2068        final int end = mTextView.getSelectionEnd();
2069        if (end < 0) return false;
2070
2071        return start == end;
2072    }
2073
2074    void makeBlink() {
2075        if (shouldBlink()) {
2076            mShowCursor = SystemClock.uptimeMillis();
2077            if (mBlink == null) mBlink = new Blink();
2078            mTextView.removeCallbacks(mBlink);
2079            mTextView.postDelayed(mBlink, BLINK);
2080        } else {
2081            if (mBlink != null) mTextView.removeCallbacks(mBlink);
2082        }
2083    }
2084
2085    private class Blink implements Runnable {
2086        private boolean mCancelled;
2087
2088        public void run() {
2089            if (mCancelled) {
2090                return;
2091            }
2092
2093            mTextView.removeCallbacks(this);
2094
2095            if (shouldBlink()) {
2096                if (mTextView.getLayout() != null) {
2097                    mTextView.invalidateCursorPath();
2098                }
2099
2100                mTextView.postDelayed(this, BLINK);
2101            }
2102        }
2103
2104        void cancel() {
2105            if (!mCancelled) {
2106                mTextView.removeCallbacks(this);
2107                mCancelled = true;
2108            }
2109        }
2110
2111        void uncancel() {
2112            mCancelled = false;
2113        }
2114    }
2115
2116    private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) {
2117        TextView shadowView = (TextView) View.inflate(mTextView.getContext(),
2118                com.android.internal.R.layout.text_drag_thumbnail, null);
2119
2120        if (shadowView == null) {
2121            throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
2122        }
2123
2124        if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) {
2125            text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH);
2126        }
2127        shadowView.setText(text);
2128        shadowView.setTextColor(mTextView.getTextColors());
2129
2130        shadowView.setTextAppearance(R.styleable.Theme_textAppearanceLarge);
2131        shadowView.setGravity(Gravity.CENTER);
2132
2133        shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
2134                ViewGroup.LayoutParams.WRAP_CONTENT));
2135
2136        final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
2137        shadowView.measure(size, size);
2138
2139        shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
2140        shadowView.invalidate();
2141        return new DragShadowBuilder(shadowView);
2142    }
2143
2144    private static class DragLocalState {
2145        public TextView sourceTextView;
2146        public int start, end;
2147
2148        public DragLocalState(TextView sourceTextView, int start, int end) {
2149            this.sourceTextView = sourceTextView;
2150            this.start = start;
2151            this.end = end;
2152        }
2153    }
2154
2155    void onDrop(DragEvent event) {
2156        StringBuilder content = new StringBuilder("");
2157        ClipData clipData = event.getClipData();
2158        final int itemCount = clipData.getItemCount();
2159        for (int i=0; i < itemCount; i++) {
2160            Item item = clipData.getItemAt(i);
2161            content.append(item.coerceToStyledText(mTextView.getContext()));
2162        }
2163
2164        final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
2165
2166        Object localState = event.getLocalState();
2167        DragLocalState dragLocalState = null;
2168        if (localState instanceof DragLocalState) {
2169            dragLocalState = (DragLocalState) localState;
2170        }
2171        boolean dragDropIntoItself = dragLocalState != null &&
2172                dragLocalState.sourceTextView == mTextView;
2173
2174        if (dragDropIntoItself) {
2175            if (offset >= dragLocalState.start && offset < dragLocalState.end) {
2176                // A drop inside the original selection discards the drop.
2177                return;
2178            }
2179        }
2180
2181        final int originalLength = mTextView.getText().length();
2182        int min = offset;
2183        int max = offset;
2184
2185        Selection.setSelection((Spannable) mTextView.getText(), max);
2186        mTextView.replaceText_internal(min, max, content);
2187
2188        if (dragDropIntoItself) {
2189            int dragSourceStart = dragLocalState.start;
2190            int dragSourceEnd = dragLocalState.end;
2191            if (max <= dragSourceStart) {
2192                // Inserting text before selection has shifted positions
2193                final int shift = mTextView.getText().length() - originalLength;
2194                dragSourceStart += shift;
2195                dragSourceEnd += shift;
2196            }
2197
2198            // Delete original selection
2199            mTextView.deleteText_internal(dragSourceStart, dragSourceEnd);
2200
2201            // Make sure we do not leave two adjacent spaces.
2202            final int prevCharIdx = Math.max(0,  dragSourceStart - 1);
2203            final int nextCharIdx = Math.min(mTextView.getText().length(), dragSourceStart + 1);
2204            if (nextCharIdx > prevCharIdx + 1) {
2205                CharSequence t = mTextView.getTransformedText(prevCharIdx, nextCharIdx);
2206                if (Character.isSpaceChar(t.charAt(0)) && Character.isSpaceChar(t.charAt(1))) {
2207                    mTextView.deleteText_internal(prevCharIdx, prevCharIdx + 1);
2208                }
2209            }
2210        }
2211    }
2212
2213    public void addSpanWatchers(Spannable text) {
2214        final int textLength = text.length();
2215
2216        if (mKeyListener != null) {
2217            text.setSpan(mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
2218        }
2219
2220        if (mSpanController == null) {
2221            mSpanController = new SpanController();
2222        }
2223        text.setSpan(mSpanController, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
2224    }
2225
2226    /**
2227     * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
2228     * pop-up should be displayed.
2229     * Also monitors {@link Selection} to call back to the attached input method.
2230     */
2231    class SpanController implements SpanWatcher {
2232
2233        private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
2234
2235        private EasyEditPopupWindow mPopupWindow;
2236
2237        private Runnable mHidePopup;
2238
2239        // This function is pure but inner classes can't have static functions
2240        private boolean isNonIntermediateSelectionSpan(final Spannable text,
2241                final Object span) {
2242            return (Selection.SELECTION_START == span || Selection.SELECTION_END == span)
2243                    && (text.getSpanFlags(span) & Spanned.SPAN_INTERMEDIATE) == 0;
2244        }
2245
2246        @Override
2247        public void onSpanAdded(Spannable text, Object span, int start, int end) {
2248            if (isNonIntermediateSelectionSpan(text, span)) {
2249                sendUpdateSelection();
2250            } else if (span instanceof EasyEditSpan) {
2251                if (mPopupWindow == null) {
2252                    mPopupWindow = new EasyEditPopupWindow();
2253                    mHidePopup = new Runnable() {
2254                        @Override
2255                        public void run() {
2256                            hide();
2257                        }
2258                    };
2259                }
2260
2261                // Make sure there is only at most one EasyEditSpan in the text
2262                if (mPopupWindow.mEasyEditSpan != null) {
2263                    mPopupWindow.mEasyEditSpan.setDeleteEnabled(false);
2264                }
2265
2266                mPopupWindow.setEasyEditSpan((EasyEditSpan) span);
2267                mPopupWindow.setOnDeleteListener(new EasyEditDeleteListener() {
2268                    @Override
2269                    public void onDeleteClick(EasyEditSpan span) {
2270                        Editable editable = (Editable) mTextView.getText();
2271                        int start = editable.getSpanStart(span);
2272                        int end = editable.getSpanEnd(span);
2273                        if (start >= 0 && end >= 0) {
2274                            sendEasySpanNotification(EasyEditSpan.TEXT_DELETED, span);
2275                            mTextView.deleteText_internal(start, end);
2276                        }
2277                        editable.removeSpan(span);
2278                    }
2279                });
2280
2281                if (mTextView.getWindowVisibility() != View.VISIBLE) {
2282                    // The window is not visible yet, ignore the text change.
2283                    return;
2284                }
2285
2286                if (mTextView.getLayout() == null) {
2287                    // The view has not been laid out yet, ignore the text change
2288                    return;
2289                }
2290
2291                if (extractedTextModeWillBeStarted()) {
2292                    // The input is in extract mode. Do not handle the easy edit in
2293                    // the original TextView, as the ExtractEditText will do
2294                    return;
2295                }
2296
2297                mPopupWindow.show();
2298                mTextView.removeCallbacks(mHidePopup);
2299                mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
2300            }
2301        }
2302
2303        @Override
2304        public void onSpanRemoved(Spannable text, Object span, int start, int end) {
2305            if (isNonIntermediateSelectionSpan(text, span)) {
2306                sendUpdateSelection();
2307            } else if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
2308                hide();
2309            }
2310        }
2311
2312        @Override
2313        public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd,
2314                int newStart, int newEnd) {
2315            if (isNonIntermediateSelectionSpan(text, span)) {
2316                sendUpdateSelection();
2317            } else if (mPopupWindow != null && span instanceof EasyEditSpan) {
2318                EasyEditSpan easyEditSpan = (EasyEditSpan) span;
2319                sendEasySpanNotification(EasyEditSpan.TEXT_MODIFIED, easyEditSpan);
2320                text.removeSpan(easyEditSpan);
2321            }
2322        }
2323
2324        public void hide() {
2325            if (mPopupWindow != null) {
2326                mPopupWindow.hide();
2327                mTextView.removeCallbacks(mHidePopup);
2328            }
2329        }
2330
2331        private void sendEasySpanNotification(int textChangedType, EasyEditSpan span) {
2332            try {
2333                PendingIntent pendingIntent = span.getPendingIntent();
2334                if (pendingIntent != null) {
2335                    Intent intent = new Intent();
2336                    intent.putExtra(EasyEditSpan.EXTRA_TEXT_CHANGED_TYPE, textChangedType);
2337                    pendingIntent.send(mTextView.getContext(), 0, intent);
2338                }
2339            } catch (CanceledException e) {
2340                // This should not happen, as we should try to send the intent only once.
2341                Log.w(TAG, "PendingIntent for notification cannot be sent", e);
2342            }
2343        }
2344    }
2345
2346    /**
2347     * Listens for the delete event triggered by {@link EasyEditPopupWindow}.
2348     */
2349    private interface EasyEditDeleteListener {
2350
2351        /**
2352         * Clicks the delete pop-up.
2353         */
2354        void onDeleteClick(EasyEditSpan span);
2355    }
2356
2357    /**
2358     * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
2359     * by {@link SpanController}.
2360     */
2361    private class EasyEditPopupWindow extends PinnedPopupWindow
2362            implements OnClickListener {
2363        private static final int POPUP_TEXT_LAYOUT =
2364                com.android.internal.R.layout.text_edit_action_popup_text;
2365        private TextView mDeleteTextView;
2366        private EasyEditSpan mEasyEditSpan;
2367        private EasyEditDeleteListener mOnDeleteListener;
2368
2369        @Override
2370        protected void createPopupWindow() {
2371            mPopupWindow = new PopupWindow(mTextView.getContext(), null,
2372                    com.android.internal.R.attr.textSelectHandleWindowStyle);
2373            mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
2374            mPopupWindow.setClippingEnabled(true);
2375        }
2376
2377        @Override
2378        protected void initContentView() {
2379            LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
2380            linearLayout.setOrientation(LinearLayout.HORIZONTAL);
2381            mContentView = linearLayout;
2382            mContentView.setBackgroundResource(
2383                    com.android.internal.R.drawable.text_edit_side_paste_window);
2384
2385            LayoutInflater inflater = (LayoutInflater)mTextView.getContext().
2386                    getSystemService(Context.LAYOUT_INFLATER_SERVICE);
2387
2388            LayoutParams wrapContent = new LayoutParams(
2389                    ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
2390
2391            mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
2392            mDeleteTextView.setLayoutParams(wrapContent);
2393            mDeleteTextView.setText(com.android.internal.R.string.delete);
2394            mDeleteTextView.setOnClickListener(this);
2395            mContentView.addView(mDeleteTextView);
2396        }
2397
2398        public void setEasyEditSpan(EasyEditSpan easyEditSpan) {
2399            mEasyEditSpan = easyEditSpan;
2400        }
2401
2402        private void setOnDeleteListener(EasyEditDeleteListener listener) {
2403            mOnDeleteListener = listener;
2404        }
2405
2406        @Override
2407        public void onClick(View view) {
2408            if (view == mDeleteTextView
2409                    && mEasyEditSpan != null && mEasyEditSpan.isDeleteEnabled()
2410                    && mOnDeleteListener != null) {
2411                mOnDeleteListener.onDeleteClick(mEasyEditSpan);
2412            }
2413        }
2414
2415        @Override
2416        public void hide() {
2417            if (mEasyEditSpan != null) {
2418                mEasyEditSpan.setDeleteEnabled(false);
2419            }
2420            mOnDeleteListener = null;
2421            super.hide();
2422        }
2423
2424        @Override
2425        protected int getTextOffset() {
2426            // Place the pop-up at the end of the span
2427            Editable editable = (Editable) mTextView.getText();
2428            return editable.getSpanEnd(mEasyEditSpan);
2429        }
2430
2431        @Override
2432        protected int getVerticalLocalPosition(int line) {
2433            return mTextView.getLayout().getLineBottom(line);
2434        }
2435
2436        @Override
2437        protected int clipVertically(int positionY) {
2438            // As we display the pop-up below the span, no vertical clipping is required.
2439            return positionY;
2440        }
2441    }
2442
2443    private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
2444        // 3 handles
2445        // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
2446        // 1 CursorAnchorInfoNotifier
2447        private final int MAXIMUM_NUMBER_OF_LISTENERS = 7;
2448        private TextViewPositionListener[] mPositionListeners =
2449                new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
2450        private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
2451        private boolean mPositionHasChanged = true;
2452        // Absolute position of the TextView with respect to its parent window
2453        private int mPositionX, mPositionY;
2454        private int mNumberOfListeners;
2455        private boolean mScrollHasChanged;
2456        final int[] mTempCoords = new int[2];
2457
2458        public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
2459            if (mNumberOfListeners == 0) {
2460                updatePosition();
2461                ViewTreeObserver vto = mTextView.getViewTreeObserver();
2462                vto.addOnPreDrawListener(this);
2463            }
2464
2465            int emptySlotIndex = -1;
2466            for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
2467                TextViewPositionListener listener = mPositionListeners[i];
2468                if (listener == positionListener) {
2469                    return;
2470                } else if (emptySlotIndex < 0 && listener == null) {
2471                    emptySlotIndex = i;
2472                }
2473            }
2474
2475            mPositionListeners[emptySlotIndex] = positionListener;
2476            mCanMove[emptySlotIndex] = canMove;
2477            mNumberOfListeners++;
2478        }
2479
2480        public void removeSubscriber(TextViewPositionListener positionListener) {
2481            for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
2482                if (mPositionListeners[i] == positionListener) {
2483                    mPositionListeners[i] = null;
2484                    mNumberOfListeners--;
2485                    break;
2486                }
2487            }
2488
2489            if (mNumberOfListeners == 0) {
2490                ViewTreeObserver vto = mTextView.getViewTreeObserver();
2491                vto.removeOnPreDrawListener(this);
2492            }
2493        }
2494
2495        public int getPositionX() {
2496            return mPositionX;
2497        }
2498
2499        public int getPositionY() {
2500            return mPositionY;
2501        }
2502
2503        @Override
2504        public boolean onPreDraw() {
2505            updatePosition();
2506
2507            for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
2508                if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
2509                    TextViewPositionListener positionListener = mPositionListeners[i];
2510                    if (positionListener != null) {
2511                        positionListener.updatePosition(mPositionX, mPositionY,
2512                                mPositionHasChanged, mScrollHasChanged);
2513                    }
2514                }
2515            }
2516
2517            mScrollHasChanged = false;
2518            return true;
2519        }
2520
2521        private void updatePosition() {
2522            mTextView.getLocationInWindow(mTempCoords);
2523
2524            mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
2525
2526            mPositionX = mTempCoords[0];
2527            mPositionY = mTempCoords[1];
2528        }
2529
2530        public void onScrollChanged() {
2531            mScrollHasChanged = true;
2532        }
2533    }
2534
2535    private abstract class PinnedPopupWindow implements TextViewPositionListener {
2536        protected PopupWindow mPopupWindow;
2537        protected ViewGroup mContentView;
2538        int mPositionX, mPositionY;
2539
2540        protected abstract void createPopupWindow();
2541        protected abstract void initContentView();
2542        protected abstract int getTextOffset();
2543        protected abstract int getVerticalLocalPosition(int line);
2544        protected abstract int clipVertically(int positionY);
2545
2546        public PinnedPopupWindow() {
2547            createPopupWindow();
2548
2549            mPopupWindow.setWindowLayoutType(
2550                    WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
2551            mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
2552            mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
2553
2554            initContentView();
2555
2556            LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
2557                    ViewGroup.LayoutParams.WRAP_CONTENT);
2558            mContentView.setLayoutParams(wrapContent);
2559
2560            mPopupWindow.setContentView(mContentView);
2561        }
2562
2563        public void show() {
2564            getPositionListener().addSubscriber(this, false /* offset is fixed */);
2565
2566            computeLocalPosition();
2567
2568            final PositionListener positionListener = getPositionListener();
2569            updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
2570        }
2571
2572        protected void measureContent() {
2573            final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
2574            mContentView.measure(
2575                    View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
2576                            View.MeasureSpec.AT_MOST),
2577                    View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
2578                            View.MeasureSpec.AT_MOST));
2579        }
2580
2581        /* The popup window will be horizontally centered on the getTextOffset() and vertically
2582         * positioned according to viewportToContentHorizontalOffset.
2583         *
2584         * This method assumes that mContentView has properly been measured from its content. */
2585        private void computeLocalPosition() {
2586            measureContent();
2587            final int width = mContentView.getMeasuredWidth();
2588            final int offset = getTextOffset();
2589            mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f);
2590            mPositionX += mTextView.viewportToContentHorizontalOffset();
2591
2592            final int line = mTextView.getLayout().getLineForOffset(offset);
2593            mPositionY = getVerticalLocalPosition(line);
2594            mPositionY += mTextView.viewportToContentVerticalOffset();
2595        }
2596
2597        private void updatePosition(int parentPositionX, int parentPositionY) {
2598            int positionX = parentPositionX + mPositionX;
2599            int positionY = parentPositionY + mPositionY;
2600
2601            positionY = clipVertically(positionY);
2602
2603            // Horizontal clipping
2604            final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
2605            final int width = mContentView.getMeasuredWidth();
2606            positionX = Math.min(displayMetrics.widthPixels - width, positionX);
2607            positionX = Math.max(0, positionX);
2608
2609            if (isShowing()) {
2610                mPopupWindow.update(positionX, positionY, -1, -1);
2611            } else {
2612                mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
2613                        positionX, positionY);
2614            }
2615        }
2616
2617        public void hide() {
2618            mPopupWindow.dismiss();
2619            getPositionListener().removeSubscriber(this);
2620        }
2621
2622        @Override
2623        public void updatePosition(int parentPositionX, int parentPositionY,
2624                boolean parentPositionChanged, boolean parentScrolled) {
2625            // Either parentPositionChanged or parentScrolled is true, check if still visible
2626            if (isShowing() && isOffsetVisible(getTextOffset())) {
2627                if (parentScrolled) computeLocalPosition();
2628                updatePosition(parentPositionX, parentPositionY);
2629            } else {
2630                hide();
2631            }
2632        }
2633
2634        public boolean isShowing() {
2635            return mPopupWindow.isShowing();
2636        }
2637    }
2638
2639    @VisibleForTesting
2640    public class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
2641        private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
2642
2643        // Key of intent extras for inserting new word into user dictionary.
2644        private static final String USER_DICTIONARY_EXTRA_WORD = "word";
2645        private static final String USER_DICTIONARY_EXTRA_LOCALE = "locale";
2646
2647        private SuggestionInfo[] mSuggestionInfos;
2648        private int mNumberOfSuggestions;
2649        private boolean mCursorWasVisibleBeforeSuggestions;
2650        private boolean mIsShowingUp = false;
2651        private SuggestionAdapter mSuggestionsAdapter;
2652        private final Comparator<SuggestionSpan> mSuggestionSpanComparator;
2653        private final HashMap<SuggestionSpan, Integer> mSpansLengths;
2654        private final TextAppearanceSpan mHighlightSpan = new TextAppearanceSpan(
2655                mTextView.getContext(), mTextView.mTextEditSuggestionHighlightStyle);
2656        private TextView mAddToDictionaryButton;
2657        private TextView mDeleteButton;
2658        private SuggestionSpan mMisspelledSpan;
2659
2660        private class CustomPopupWindow extends PopupWindow {
2661            public CustomPopupWindow(Context context, int defStyleAttr) {
2662                super(context, null, defStyleAttr);
2663            }
2664
2665            @Override
2666            public void dismiss() {
2667                super.dismiss();
2668
2669                getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
2670
2671                // Safe cast since show() checks that mTextView.getText() is an Editable
2672                ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan);
2673
2674                mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions);
2675                if (hasInsertionController()) {
2676                    getInsertionController().show();
2677                }
2678            }
2679        }
2680
2681        public SuggestionsPopupWindow() {
2682            mCursorWasVisibleBeforeSuggestions = mCursorVisible;
2683            mSuggestionSpanComparator = new SuggestionSpanComparator();
2684            mSpansLengths = new HashMap<SuggestionSpan, Integer>();
2685        }
2686
2687        @Override
2688        protected void createPopupWindow() {
2689            mPopupWindow = new CustomPopupWindow(mTextView.getContext(),
2690                com.android.internal.R.attr.textSuggestionsWindowStyle);
2691            mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
2692            mPopupWindow.setFocusable(true);
2693            mPopupWindow.setClippingEnabled(false);
2694        }
2695
2696        @Override
2697        protected void initContentView() {
2698            final LayoutInflater inflater = (LayoutInflater) mTextView.getContext().
2699                    getSystemService(Context.LAYOUT_INFLATER_SERVICE);
2700            final LinearLayout linearLayout = (LinearLayout) inflater.inflate(
2701                    mTextView.mTextEditSuggestionContainerLayout, null);
2702
2703            final ListView suggestionListView = (ListView) linearLayout.findViewById(
2704                    com.android.internal.R.id.suggestionContainer);
2705
2706            mSuggestionsAdapter = new SuggestionAdapter();
2707            suggestionListView.setAdapter(mSuggestionsAdapter);
2708            suggestionListView.setOnItemClickListener(this);
2709
2710            // Inflate the suggestion items once and for all.
2711            mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS];
2712            for (int i = 0; i < mSuggestionInfos.length; i++) {
2713                mSuggestionInfos[i] = new SuggestionInfo();
2714            }
2715
2716            mContentView = linearLayout;
2717
2718            mAddToDictionaryButton = (TextView) linearLayout.findViewById(
2719                    com.android.internal.R.id.addToDictionaryButton);
2720            mAddToDictionaryButton.setOnClickListener(new View.OnClickListener() {
2721                public void onClick(View v) {
2722                    final Editable editable = (Editable) mTextView.getText();
2723                    final int spanStart = editable.getSpanStart(mMisspelledSpan);
2724                    final int spanEnd = editable.getSpanEnd(mMisspelledSpan);
2725                    final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
2726
2727                    final Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
2728                    intent.putExtra(USER_DICTIONARY_EXTRA_WORD, originalText);
2729                    intent.putExtra(USER_DICTIONARY_EXTRA_LOCALE,
2730                            mTextView.getTextServicesLocale().toString());
2731                    intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
2732                    mTextView.getContext().startActivity(intent);
2733                    // There is no way to know if the word was indeed added. Re-check.
2734                    // TODO The ExtractEditText should remove the span in the original text instead
2735                    editable.removeSpan(mMisspelledSpan);
2736                    Selection.setSelection(editable, spanEnd);
2737                    updateSpellCheckSpans(spanStart, spanEnd, false);
2738                    hideWithCleanUp();
2739                }
2740            });
2741
2742            mDeleteButton = (TextView) linearLayout.findViewById(
2743                    com.android.internal.R.id.deleteButton);
2744            mDeleteButton.setOnClickListener(new View.OnClickListener() {
2745                public void onClick(View v) {
2746                    final Editable editable = (Editable) mTextView.getText();
2747
2748                    final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
2749                    int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
2750                    if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
2751                        // Do not leave two adjacent spaces after deletion, or one at beginning of
2752                        // text
2753                        if (spanUnionEnd < editable.length() &&
2754                                Character.isSpaceChar(editable.charAt(spanUnionEnd)) &&
2755                                (spanUnionStart == 0 ||
2756                                Character.isSpaceChar(editable.charAt(spanUnionStart - 1)))) {
2757                            spanUnionEnd = spanUnionEnd + 1;
2758                        }
2759                        mTextView.deleteText_internal(spanUnionStart, spanUnionEnd);
2760                    }
2761                    hideWithCleanUp();
2762                }
2763            });
2764
2765        }
2766
2767        public boolean isShowingUp() {
2768            return mIsShowingUp;
2769        }
2770
2771        public void onParentLostFocus() {
2772            mIsShowingUp = false;
2773        }
2774
2775        private final class SuggestionInfo {
2776            int suggestionStart, suggestionEnd; // range of actual suggestion within text
2777
2778            // the SuggestionSpan that this TextView represents
2779            @Nullable
2780            SuggestionSpan suggestionSpan;
2781
2782            int suggestionIndex; // the index of this suggestion inside suggestionSpan
2783
2784            @Nullable
2785            final SpannableStringBuilder text = new SpannableStringBuilder();
2786
2787            void clear() {
2788                suggestionSpan = null;
2789                text.clear();
2790            }
2791        }
2792
2793        private class SuggestionAdapter extends BaseAdapter {
2794            private LayoutInflater mInflater = (LayoutInflater) mTextView.getContext().
2795                    getSystemService(Context.LAYOUT_INFLATER_SERVICE);
2796
2797            @Override
2798            public int getCount() {
2799                return mNumberOfSuggestions;
2800            }
2801
2802            @Override
2803            public Object getItem(int position) {
2804                return mSuggestionInfos[position];
2805            }
2806
2807            @Override
2808            public long getItemId(int position) {
2809                return position;
2810            }
2811
2812            @Override
2813            public View getView(int position, View convertView, ViewGroup parent) {
2814                TextView textView = (TextView) convertView;
2815
2816                if (textView == null) {
2817                    textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout,
2818                            parent, false);
2819                }
2820
2821                final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
2822                textView.setText(suggestionInfo.text);
2823                return textView;
2824            }
2825        }
2826
2827        private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
2828            public int compare(SuggestionSpan span1, SuggestionSpan span2) {
2829                final int flag1 = span1.getFlags();
2830                final int flag2 = span2.getFlags();
2831                if (flag1 != flag2) {
2832                    // The order here should match what is used in updateDrawState
2833                    final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
2834                    final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
2835                    final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
2836                    final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
2837                    if (easy1 && !misspelled1) return -1;
2838                    if (easy2 && !misspelled2) return 1;
2839                    if (misspelled1) return -1;
2840                    if (misspelled2) return 1;
2841                }
2842
2843                return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
2844            }
2845        }
2846
2847        /**
2848         * Returns the suggestion spans that cover the current cursor position. The suggestion
2849         * spans are sorted according to the length of text that they are attached to.
2850         */
2851        private SuggestionSpan[] getSuggestionSpans() {
2852            int pos = mTextView.getSelectionStart();
2853            Spannable spannable = (Spannable) mTextView.getText();
2854            SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
2855
2856            mSpansLengths.clear();
2857            for (SuggestionSpan suggestionSpan : suggestionSpans) {
2858                int start = spannable.getSpanStart(suggestionSpan);
2859                int end = spannable.getSpanEnd(suggestionSpan);
2860                mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
2861            }
2862
2863            // The suggestions are sorted according to their types (easy correction first, then
2864            // misspelled) and to the length of the text that they cover (shorter first).
2865            Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
2866            mSpansLengths.clear();
2867
2868            return suggestionSpans;
2869        }
2870
2871        @VisibleForTesting
2872        public ViewGroup getContentViewForTesting() {
2873            return mContentView;
2874        }
2875
2876        @Override
2877        public void show() {
2878            if (!(mTextView.getText() instanceof Editable)) return;
2879
2880            if (updateSuggestions()) {
2881                mCursorWasVisibleBeforeSuggestions = mCursorVisible;
2882                mTextView.setCursorVisible(false);
2883                mIsShowingUp = true;
2884                super.show();
2885            }
2886        }
2887
2888        @Override
2889        protected void measureContent() {
2890            final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
2891            final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
2892                    displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
2893            final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
2894                    displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
2895
2896            int width = 0;
2897            View view = null;
2898            for (int i = 0; i < mNumberOfSuggestions; i++) {
2899                view = mSuggestionsAdapter.getView(i, view, mContentView);
2900                view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
2901                view.measure(horizontalMeasure, verticalMeasure);
2902                width = Math.max(width, view.getMeasuredWidth());
2903            }
2904
2905            if (mAddToDictionaryButton.getVisibility() != View.GONE) {
2906                mAddToDictionaryButton.measure(horizontalMeasure, verticalMeasure);
2907                width = Math.max(width, mAddToDictionaryButton.getMeasuredWidth());
2908            }
2909
2910            mDeleteButton.measure(horizontalMeasure, verticalMeasure);
2911            width = Math.max(width, mDeleteButton.getMeasuredWidth());
2912
2913            // Enforce the width based on actual text widths
2914            mContentView.measure(
2915                    View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
2916                    verticalMeasure);
2917
2918            Drawable popupBackground = mPopupWindow.getBackground();
2919            if (popupBackground != null) {
2920                if (mTempRect == null) mTempRect = new Rect();
2921                popupBackground.getPadding(mTempRect);
2922                width += mTempRect.left + mTempRect.right;
2923            }
2924            mPopupWindow.setWidth(width);
2925        }
2926
2927        @Override
2928        protected int getTextOffset() {
2929            return mTextView.getSelectionStart();
2930        }
2931
2932        @Override
2933        protected int getVerticalLocalPosition(int line) {
2934            return mTextView.getLayout().getLineBottom(line);
2935        }
2936
2937        @Override
2938        protected int clipVertically(int positionY) {
2939            final int height = mContentView.getMeasuredHeight();
2940            final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
2941            return Math.min(positionY, displayMetrics.heightPixels - height);
2942        }
2943
2944        private void hideWithCleanUp() {
2945            for (final SuggestionInfo info : mSuggestionInfos) {
2946                info.clear();
2947            }
2948            mMisspelledSpan = null;
2949            hide();
2950        }
2951
2952        private boolean updateSuggestions() {
2953            Spannable spannable = (Spannable) mTextView.getText();
2954            SuggestionSpan[] suggestionSpans = getSuggestionSpans();
2955
2956            final int nbSpans = suggestionSpans.length;
2957            // Suggestions are shown after a delay: the underlying spans may have been removed
2958            if (nbSpans == 0) return false;
2959
2960            mNumberOfSuggestions = 0;
2961            int spanUnionStart = mTextView.getText().length();
2962            int spanUnionEnd = 0;
2963
2964            mMisspelledSpan = null;
2965            int underlineColor = 0;
2966
2967            for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) {
2968                SuggestionSpan suggestionSpan = suggestionSpans[spanIndex];
2969                final int spanStart = spannable.getSpanStart(suggestionSpan);
2970                final int spanEnd = spannable.getSpanEnd(suggestionSpan);
2971                spanUnionStart = Math.min(spanStart, spanUnionStart);
2972                spanUnionEnd = Math.max(spanEnd, spanUnionEnd);
2973
2974                if ((suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
2975                    mMisspelledSpan = suggestionSpan;
2976                }
2977
2978                // The first span dictates the background color of the highlighted text
2979                if (spanIndex == 0) underlineColor = suggestionSpan.getUnderlineColor();
2980
2981                String[] suggestions = suggestionSpan.getSuggestions();
2982                int nbSuggestions = suggestions.length;
2983                for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
2984                    String suggestion = suggestions[suggestionIndex];
2985
2986                    boolean suggestionIsDuplicate = false;
2987                    for (int i = 0; i < mNumberOfSuggestions; i++) {
2988                        if (mSuggestionInfos[i].text.toString().equals(suggestion)) {
2989                            SuggestionSpan otherSuggestionSpan = mSuggestionInfos[i].suggestionSpan;
2990                            final int otherSpanStart = spannable.getSpanStart(otherSuggestionSpan);
2991                            final int otherSpanEnd = spannable.getSpanEnd(otherSuggestionSpan);
2992                            if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
2993                                suggestionIsDuplicate = true;
2994                                break;
2995                            }
2996                        }
2997                    }
2998
2999                    if (!suggestionIsDuplicate) {
3000                        SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
3001                        suggestionInfo.suggestionSpan = suggestionSpan;
3002                        suggestionInfo.suggestionIndex = suggestionIndex;
3003                        suggestionInfo.text.replace(0, suggestionInfo.text.length(), suggestion);
3004
3005                        mNumberOfSuggestions++;
3006
3007                        if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) {
3008                            // Also end outer for loop
3009                            spanIndex = nbSpans;
3010                            break;
3011                        }
3012                    }
3013                }
3014            }
3015
3016            for (int i = 0; i < mNumberOfSuggestions; i++) {
3017                highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
3018            }
3019
3020            // Make "Add to dictionary" item visible if there is a span with the misspelled flag
3021            int addToDictionaryButtonVisibility = View.GONE;
3022            if (mMisspelledSpan != null) {
3023                final int misspelledStart = spannable.getSpanStart(mMisspelledSpan);
3024                final int misspelledEnd = spannable.getSpanEnd(mMisspelledSpan);
3025                if (misspelledStart >= 0 && misspelledEnd > misspelledStart) {
3026                    addToDictionaryButtonVisibility = View.VISIBLE;
3027                }
3028            }
3029            mAddToDictionaryButton.setVisibility(addToDictionaryButtonVisibility);
3030
3031            if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
3032            if (underlineColor == 0) {
3033                // Fallback on the default highlight color when the first span does not provide one
3034                mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor);
3035            } else {
3036                final float BACKGROUND_TRANSPARENCY = 0.4f;
3037                final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
3038                mSuggestionRangeSpan.setBackgroundColor(
3039                        (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
3040            }
3041            spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
3042                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
3043
3044            mSuggestionsAdapter.notifyDataSetChanged();
3045            return true;
3046        }
3047
3048        private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
3049                int unionEnd) {
3050            final Spannable text = (Spannable) mTextView.getText();
3051            final int spanStart = text.getSpanStart(suggestionInfo.suggestionSpan);
3052            final int spanEnd = text.getSpanEnd(suggestionInfo.suggestionSpan);
3053
3054            // Adjust the start/end of the suggestion span
3055            suggestionInfo.suggestionStart = spanStart - unionStart;
3056            suggestionInfo.suggestionEnd = suggestionInfo.suggestionStart
3057                    + suggestionInfo.text.length();
3058
3059            suggestionInfo.text.setSpan(mHighlightSpan, 0, suggestionInfo.text.length(),
3060                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
3061
3062            // Add the text before and after the span.
3063            final String textAsString = text.toString();
3064            suggestionInfo.text.insert(0, textAsString.substring(unionStart, spanStart));
3065            suggestionInfo.text.append(textAsString.substring(spanEnd, unionEnd));
3066        }
3067
3068        @Override
3069        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
3070            Editable editable = (Editable) mTextView.getText();
3071            SuggestionInfo suggestionInfo = mSuggestionInfos[position];
3072
3073            final int spanStart = editable.getSpanStart(suggestionInfo.suggestionSpan);
3074            final int spanEnd = editable.getSpanEnd(suggestionInfo.suggestionSpan);
3075            if (spanStart < 0 || spanEnd <= spanStart) {
3076                // Span has been removed
3077                hideWithCleanUp();
3078                return;
3079            }
3080
3081            final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
3082
3083            // SuggestionSpans are removed by replace: save them before
3084            final SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
3085                    SuggestionSpan.class);
3086            final int length = suggestionSpans.length;
3087            final int[] suggestionSpansStarts = new int[length];
3088            final int[] suggestionSpansEnds = new int[length];
3089            final int[] suggestionSpansFlags = new int[length];
3090            for (int i = 0; i < length; i++) {
3091                final SuggestionSpan suggestionSpan = suggestionSpans[i];
3092                suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
3093                suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
3094                suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
3095
3096                // Remove potential misspelled flags
3097                int suggestionSpanFlags = suggestionSpan.getFlags();
3098                if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) > 0) {
3099                    suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
3100                    suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
3101                    suggestionSpan.setFlags(suggestionSpanFlags);
3102                }
3103            }
3104
3105            final int suggestionStart = suggestionInfo.suggestionStart;
3106            final int suggestionEnd = suggestionInfo.suggestionEnd;
3107            final String suggestion = suggestionInfo.text.subSequence(
3108                    suggestionStart, suggestionEnd).toString();
3109            mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
3110
3111            // Notify source IME of the suggestion pick. Do this before
3112            // swaping texts.
3113            suggestionInfo.suggestionSpan.notifySelection(
3114                    mTextView.getContext(), originalText, suggestionInfo.suggestionIndex);
3115
3116            // Swap text content between actual text and Suggestion span
3117            final String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions();
3118            suggestions[suggestionInfo.suggestionIndex] = originalText;
3119
3120            // Restore previous SuggestionSpans
3121            final int lengthDifference = suggestion.length() - (spanEnd - spanStart);
3122            for (int i = 0; i < length; i++) {
3123                // Only spans that include the modified region make sense after replacement
3124                // Spans partially included in the replaced region are removed, there is no
3125                // way to assign them a valid range after replacement
3126                if (suggestionSpansStarts[i] <= spanStart &&
3127                        suggestionSpansEnds[i] >= spanEnd) {
3128                    mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
3129                            suggestionSpansEnds[i] + lengthDifference, suggestionSpansFlags[i]);
3130                }
3131            }
3132
3133            // Move cursor at the end of the replaced word
3134            final int newCursorPosition = spanEnd + lengthDifference;
3135            mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
3136            hideWithCleanUp();
3137        }
3138    }
3139
3140    /**
3141     * An ActionMode Callback class that is used to provide actions while in text insertion or
3142     * selection mode.
3143     *
3144     * The default callback provides a subset of Select All, Cut, Copy, Paste, Share and Replace
3145     * actions, depending on which of these this TextView supports and the current selection.
3146     */
3147    private class TextActionModeCallback extends ActionMode.Callback2 {
3148        private final Path mSelectionPath = new Path();
3149        private final RectF mSelectionBounds = new RectF();
3150        private final boolean mHasSelection;
3151
3152        private int mHandleHeight;
3153
3154        public TextActionModeCallback(boolean hasSelection) {
3155            mHasSelection = hasSelection;
3156            if (mHasSelection) {
3157                SelectionModifierCursorController selectionController = getSelectionController();
3158                if (selectionController.mStartHandle == null) {
3159                    // As these are for initializing selectionController, hide() must be called.
3160                    selectionController.initDrawables();
3161                    selectionController.initHandles();
3162                    selectionController.hide();
3163                }
3164                mHandleHeight = Math.max(
3165                        mSelectHandleLeft.getMinimumHeight(),
3166                        mSelectHandleRight.getMinimumHeight());
3167            } else {
3168                InsertionPointCursorController insertionController = getInsertionController();
3169                if (insertionController != null) {
3170                    insertionController.getHandle();
3171                    mHandleHeight = mSelectHandleCenter.getMinimumHeight();
3172                }
3173            }
3174        }
3175
3176        @Override
3177        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
3178            mode.setTitle(null);
3179            mode.setSubtitle(null);
3180            mode.setTitleOptionalHint(true);
3181            populateMenuWithItems(menu);
3182
3183            Callback customCallback = getCustomCallback();
3184            if (customCallback != null) {
3185                if (!customCallback.onCreateActionMode(mode, menu)) {
3186                    // The custom mode can choose to cancel the action mode, dismiss selection.
3187                    Selection.setSelection((Spannable) mTextView.getText(),
3188                            mTextView.getSelectionEnd());
3189                    return false;
3190                }
3191            }
3192
3193            if (mTextView.canProcessText()) {
3194                mProcessTextIntentActionsHandler.onInitializeMenu(menu);
3195            }
3196
3197            if (menu.hasVisibleItems() || mode.getCustomView() != null) {
3198                mTextView.setHasTransientState(true);
3199                return true;
3200            } else {
3201                return false;
3202            }
3203        }
3204
3205        private Callback getCustomCallback() {
3206            return mHasSelection
3207                    ? mCustomSelectionActionModeCallback
3208                    : mCustomInsertionActionModeCallback;
3209        }
3210
3211        private void populateMenuWithItems(Menu menu) {
3212            if (mTextView.canCut()) {
3213                menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
3214                        com.android.internal.R.string.cut).
3215                    setAlphabeticShortcut('x').
3216                    setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
3217            }
3218
3219            if (mTextView.canCopy()) {
3220                menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
3221                        com.android.internal.R.string.copy).
3222                    setAlphabeticShortcut('c').
3223                    setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
3224            }
3225
3226            if (mTextView.canPaste()) {
3227                menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
3228                        com.android.internal.R.string.paste).
3229                    setAlphabeticShortcut('v').
3230                    setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
3231            }
3232
3233            if (mTextView.canShare()) {
3234                menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
3235                        com.android.internal.R.string.share).
3236                    setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
3237            }
3238
3239            updateSelectAllItem(menu);
3240            updateReplaceItem(menu);
3241        }
3242
3243        @Override
3244        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
3245            updateSelectAllItem(menu);
3246            updateReplaceItem(menu);
3247
3248            Callback customCallback = getCustomCallback();
3249            if (customCallback != null) {
3250                return customCallback.onPrepareActionMode(mode, menu);
3251            }
3252            return true;
3253        }
3254
3255        private void updateSelectAllItem(Menu menu) {
3256            boolean canSelectAll = mTextView.canSelectAllText();
3257            boolean selectAllItemExists = menu.findItem(TextView.ID_SELECT_ALL) != null;
3258            if (canSelectAll && !selectAllItemExists) {
3259                menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
3260                        com.android.internal.R.string.selectAll)
3261                    .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
3262            } else if (!canSelectAll && selectAllItemExists) {
3263                menu.removeItem(TextView.ID_SELECT_ALL);
3264            }
3265        }
3266
3267        private void updateReplaceItem(Menu menu) {
3268            boolean canReplace = mTextView.isSuggestionsEnabled() && shouldOfferToShowSuggestions()
3269                    && !(mTextView.isInExtractedMode() && mTextView.hasSelection());
3270            boolean replaceItemExists = menu.findItem(TextView.ID_REPLACE) != null;
3271            if (canReplace && !replaceItemExists) {
3272                menu.add(Menu.NONE, TextView.ID_REPLACE, MENU_ITEM_ORDER_REPLACE,
3273                        com.android.internal.R.string.replace)
3274                    .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
3275            } else if (!canReplace && replaceItemExists) {
3276                menu.removeItem(TextView.ID_REPLACE);
3277            }
3278        }
3279
3280        @Override
3281        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
3282            if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
3283                return true;
3284            }
3285            Callback customCallback = getCustomCallback();
3286            if (customCallback != null && customCallback.onActionItemClicked(mode, item)) {
3287                return true;
3288            }
3289            return mTextView.onTextContextMenuItem(item.getItemId());
3290        }
3291
3292        @Override
3293        public void onDestroyActionMode(ActionMode mode) {
3294            Callback customCallback = getCustomCallback();
3295            if (customCallback != null) {
3296                customCallback.onDestroyActionMode(mode);
3297            }
3298
3299            /*
3300             * If we're ending this mode because we're detaching from a window,
3301             * we still have selection state to preserve. Don't clear it, we'll
3302             * bring back the selection mode when (if) we get reattached.
3303             */
3304            if (!mPreserveDetachedSelection) {
3305                Selection.setSelection((Spannable) mTextView.getText(),
3306                        mTextView.getSelectionEnd());
3307                mTextView.setHasTransientState(false);
3308            }
3309
3310            if (mSelectionModifierCursorController != null) {
3311                mSelectionModifierCursorController.hide();
3312            }
3313
3314            mTextActionMode = null;
3315        }
3316
3317        @Override
3318        public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
3319            if (!view.equals(mTextView) || mTextView.getLayout() == null) {
3320                super.onGetContentRect(mode, view, outRect);
3321                return;
3322            }
3323            if (mTextView.getSelectionStart() != mTextView.getSelectionEnd()) {
3324                // We have a selection.
3325                mSelectionPath.reset();
3326                mTextView.getLayout().getSelectionPath(
3327                        mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mSelectionPath);
3328                mSelectionPath.computeBounds(mSelectionBounds, true);
3329                mSelectionBounds.bottom += mHandleHeight;
3330            } else if (mCursorCount == 2) {
3331                // We have a split cursor. In this case, we take the rectangle that includes both
3332                // parts of the cursor to ensure we don't obscure either of them.
3333                Rect firstCursorBounds = mCursorDrawable[0].getBounds();
3334                Rect secondCursorBounds = mCursorDrawable[1].getBounds();
3335                mSelectionBounds.set(
3336                        Math.min(firstCursorBounds.left, secondCursorBounds.left),
3337                        Math.min(firstCursorBounds.top, secondCursorBounds.top),
3338                        Math.max(firstCursorBounds.right, secondCursorBounds.right),
3339                        Math.max(firstCursorBounds.bottom, secondCursorBounds.bottom)
3340                                + mHandleHeight);
3341            } else {
3342                // We have a single cursor.
3343                Layout layout = getActiveLayout();
3344                int line = layout.getLineForOffset(mTextView.getSelectionStart());
3345                float primaryHorizontal =
3346                        layout.getPrimaryHorizontal(mTextView.getSelectionStart());
3347                mSelectionBounds.set(
3348                        primaryHorizontal,
3349                        layout.getLineTop(line),
3350                        primaryHorizontal,
3351                        layout.getLineTop(line + 1) + mHandleHeight);
3352            }
3353            // Take TextView's padding and scroll into account.
3354            int textHorizontalOffset = mTextView.viewportToContentHorizontalOffset();
3355            int textVerticalOffset = mTextView.viewportToContentVerticalOffset();
3356            outRect.set(
3357                    (int) Math.floor(mSelectionBounds.left + textHorizontalOffset),
3358                    (int) Math.floor(mSelectionBounds.top + textVerticalOffset),
3359                    (int) Math.ceil(mSelectionBounds.right + textHorizontalOffset),
3360                    (int) Math.ceil(mSelectionBounds.bottom + textVerticalOffset));
3361        }
3362    }
3363
3364    /**
3365     * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}
3366     * while the input method is requesting the cursor/anchor position. Does nothing as long as
3367     * {@link InputMethodManager#isWatchingCursor(View)} returns false.
3368     */
3369    private final class CursorAnchorInfoNotifier implements TextViewPositionListener {
3370        final CursorAnchorInfo.Builder mSelectionInfoBuilder = new CursorAnchorInfo.Builder();
3371        final int[] mTmpIntOffset = new int[2];
3372        final Matrix mViewToScreenMatrix = new Matrix();
3373
3374        @Override
3375        public void updatePosition(int parentPositionX, int parentPositionY,
3376                boolean parentPositionChanged, boolean parentScrolled) {
3377            final InputMethodState ims = mInputMethodState;
3378            if (ims == null || ims.mBatchEditNesting > 0) {
3379                return;
3380            }
3381            final InputMethodManager imm = InputMethodManager.peekInstance();
3382            if (null == imm) {
3383                return;
3384            }
3385            if (!imm.isActive(mTextView)) {
3386                return;
3387            }
3388            // Skip if the IME has not requested the cursor/anchor position.
3389            if (!imm.isCursorAnchorInfoEnabled()) {
3390                return;
3391            }
3392            Layout layout = mTextView.getLayout();
3393            if (layout == null) {
3394                return;
3395            }
3396
3397            final CursorAnchorInfo.Builder builder = mSelectionInfoBuilder;
3398            builder.reset();
3399
3400            final int selectionStart = mTextView.getSelectionStart();
3401            builder.setSelectionRange(selectionStart, mTextView.getSelectionEnd());
3402
3403            // Construct transformation matrix from view local coordinates to screen coordinates.
3404            mViewToScreenMatrix.set(mTextView.getMatrix());
3405            mTextView.getLocationOnScreen(mTmpIntOffset);
3406            mViewToScreenMatrix.postTranslate(mTmpIntOffset[0], mTmpIntOffset[1]);
3407            builder.setMatrix(mViewToScreenMatrix);
3408
3409            final float viewportToContentHorizontalOffset =
3410                    mTextView.viewportToContentHorizontalOffset();
3411            final float viewportToContentVerticalOffset =
3412                    mTextView.viewportToContentVerticalOffset();
3413
3414            final CharSequence text = mTextView.getText();
3415            if (text instanceof Spannable) {
3416                final Spannable sp = (Spannable) text;
3417                int composingTextStart = EditableInputConnection.getComposingSpanStart(sp);
3418                int composingTextEnd = EditableInputConnection.getComposingSpanEnd(sp);
3419                if (composingTextEnd < composingTextStart) {
3420                    final int temp = composingTextEnd;
3421                    composingTextEnd = composingTextStart;
3422                    composingTextStart = temp;
3423                }
3424                final boolean hasComposingText =
3425                        (0 <= composingTextStart) && (composingTextStart < composingTextEnd);
3426                if (hasComposingText) {
3427                    final CharSequence composingText = text.subSequence(composingTextStart,
3428                            composingTextEnd);
3429                    builder.setComposingText(composingTextStart, composingText);
3430
3431                    final int minLine = layout.getLineForOffset(composingTextStart);
3432                    final int maxLine = layout.getLineForOffset(composingTextEnd - 1);
3433                    for (int line = minLine; line <= maxLine; ++line) {
3434                        final int lineStart = layout.getLineStart(line);
3435                        final int lineEnd = layout.getLineEnd(line);
3436                        final int offsetStart = Math.max(lineStart, composingTextStart);
3437                        final int offsetEnd = Math.min(lineEnd, composingTextEnd);
3438                        final boolean ltrLine =
3439                                layout.getParagraphDirection(line) == Layout.DIR_LEFT_TO_RIGHT;
3440                        final float[] widths = new float[offsetEnd - offsetStart];
3441                        layout.getPaint().getTextWidths(text, offsetStart, offsetEnd, widths);
3442                        final float top = layout.getLineTop(line);
3443                        final float bottom = layout.getLineBottom(line);
3444                        for (int offset = offsetStart; offset < offsetEnd; ++offset) {
3445                            final float charWidth = widths[offset - offsetStart];
3446                            final boolean isRtl = layout.isRtlCharAt(offset);
3447                            final float primary = layout.getPrimaryHorizontal(offset);
3448                            final float secondary = layout.getSecondaryHorizontal(offset);
3449                            // TODO: This doesn't work perfectly for text with custom styles and
3450                            // TAB chars.
3451                            final float left;
3452                            final float right;
3453                            if (ltrLine) {
3454                                if (isRtl) {
3455                                    left = secondary - charWidth;
3456                                    right = secondary;
3457                                } else {
3458                                    left = primary;
3459                                    right = primary + charWidth;
3460                                }
3461                            } else {
3462                                if (!isRtl) {
3463                                    left = secondary;
3464                                    right = secondary + charWidth;
3465                                } else {
3466                                    left = primary - charWidth;
3467                                    right = primary;
3468                                }
3469                            }
3470                            // TODO: Check top-right and bottom-left as well.
3471                            final float localLeft = left + viewportToContentHorizontalOffset;
3472                            final float localRight = right + viewportToContentHorizontalOffset;
3473                            final float localTop = top + viewportToContentVerticalOffset;
3474                            final float localBottom = bottom + viewportToContentVerticalOffset;
3475                            final boolean isTopLeftVisible = isPositionVisible(localLeft, localTop);
3476                            final boolean isBottomRightVisible =
3477                                    isPositionVisible(localRight, localBottom);
3478                            int characterBoundsFlags = 0;
3479                            if (isTopLeftVisible || isBottomRightVisible) {
3480                                characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
3481                            }
3482                            if (!isTopLeftVisible || !isBottomRightVisible) {
3483                                characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
3484                            }
3485                            if (isRtl) {
3486                                characterBoundsFlags |= CursorAnchorInfo.FLAG_IS_RTL;
3487                            }
3488                            // Here offset is the index in Java chars.
3489                            builder.addCharacterBounds(offset, localLeft, localTop, localRight,
3490                                    localBottom, characterBoundsFlags);
3491                        }
3492                    }
3493                }
3494            }
3495
3496            // Treat selectionStart as the insertion point.
3497            if (0 <= selectionStart) {
3498                final int offset = selectionStart;
3499                final int line = layout.getLineForOffset(offset);
3500                final float insertionMarkerX = layout.getPrimaryHorizontal(offset)
3501                        + viewportToContentHorizontalOffset;
3502                final float insertionMarkerTop = layout.getLineTop(line)
3503                        + viewportToContentVerticalOffset;
3504                final float insertionMarkerBaseline = layout.getLineBaseline(line)
3505                        + viewportToContentVerticalOffset;
3506                final float insertionMarkerBottom = layout.getLineBottom(line)
3507                        + viewportToContentVerticalOffset;
3508                final boolean isTopVisible =
3509                        isPositionVisible(insertionMarkerX, insertionMarkerTop);
3510                final boolean isBottomVisible =
3511                        isPositionVisible(insertionMarkerX, insertionMarkerBottom);
3512                int insertionMarkerFlags = 0;
3513                if (isTopVisible || isBottomVisible) {
3514                    insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
3515                }
3516                if (!isTopVisible || !isBottomVisible) {
3517                    insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
3518                }
3519                if (layout.isRtlCharAt(offset)) {
3520                    insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL;
3521                }
3522                builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop,
3523                        insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags);
3524            }
3525
3526            imm.updateCursorAnchorInfo(mTextView, builder.build());
3527        }
3528    }
3529
3530    @VisibleForTesting
3531    public abstract class HandleView extends View implements TextViewPositionListener {
3532        protected Drawable mDrawable;
3533        protected Drawable mDrawableLtr;
3534        protected Drawable mDrawableRtl;
3535        private final PopupWindow mContainer;
3536        // Position with respect to the parent TextView
3537        private int mPositionX, mPositionY;
3538        private boolean mIsDragging;
3539        // Offset from touch position to mPosition
3540        private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
3541        protected int mHotspotX;
3542        protected int mHorizontalGravity;
3543        // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
3544        private float mTouchOffsetY;
3545        // Where the touch position should be on the handle to ensure a maximum cursor visibility
3546        private float mIdealVerticalOffset;
3547        // Parent's (TextView) previous position in window
3548        private int mLastParentX, mLastParentY;
3549        // Previous text character offset
3550        protected int mPreviousOffset = -1;
3551        // Previous text character offset
3552        private boolean mPositionHasChanged = true;
3553        // Minimum touch target size for handles
3554        private int mMinSize;
3555        // Indicates the line of text that the handle is on.
3556        protected int mPrevLine = UNSET_LINE;
3557        // Indicates the line of text that the user was touching. This can differ from mPrevLine
3558        // when selecting text when the handles jump to the end / start of words which may be on
3559        // a different line.
3560        protected int mPreviousLineTouched = UNSET_LINE;
3561
3562        private HandleView(Drawable drawableLtr, Drawable drawableRtl, final int id) {
3563            super(mTextView.getContext());
3564            setId(id);
3565            mContainer = new PopupWindow(mTextView.getContext(), null,
3566                    com.android.internal.R.attr.textSelectHandleWindowStyle);
3567            mContainer.setSplitTouchEnabled(true);
3568            mContainer.setClippingEnabled(false);
3569            mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
3570            mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
3571            mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
3572            mContainer.setContentView(this);
3573
3574            mDrawableLtr = drawableLtr;
3575            mDrawableRtl = drawableRtl;
3576            mMinSize = mTextView.getContext().getResources().getDimensionPixelSize(
3577                    com.android.internal.R.dimen.text_handle_min_size);
3578
3579            updateDrawable();
3580
3581            final int handleHeight = getPreferredHeight();
3582            mTouchOffsetY = -0.3f * handleHeight;
3583            mIdealVerticalOffset = 0.7f * handleHeight;
3584        }
3585
3586        public float getIdealVerticalOffset() {
3587            return mIdealVerticalOffset;
3588        }
3589
3590        protected void updateDrawable() {
3591            if (mIsDragging) {
3592                // Don't update drawable during dragging.
3593                return;
3594            }
3595            final int offset = getCurrentCursorOffset();
3596            final boolean isRtlCharAtOffset = mTextView.getLayout().isRtlCharAt(offset);
3597            final Drawable oldDrawable = mDrawable;
3598            mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
3599            mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
3600            mHorizontalGravity = getHorizontalGravity(isRtlCharAtOffset);
3601            final Layout layout = mTextView.getLayout();
3602            if (layout != null && oldDrawable != mDrawable && isShowing()) {
3603                // Update popup window position.
3604                mPositionX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX -
3605                        getHorizontalOffset() + getCursorOffset());
3606                mPositionX += mTextView.viewportToContentHorizontalOffset();
3607                mPositionHasChanged = true;
3608                updatePosition(mLastParentX, mLastParentY, false, false);
3609                postInvalidate();
3610            }
3611        }
3612
3613        protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
3614        protected abstract int getHorizontalGravity(boolean isRtlRun);
3615
3616        // Touch-up filter: number of previous positions remembered
3617        private static final int HISTORY_SIZE = 5;
3618        private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
3619        private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
3620        private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
3621        private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
3622        private int mPreviousOffsetIndex = 0;
3623        private int mNumberPreviousOffsets = 0;
3624
3625        private void startTouchUpFilter(int offset) {
3626            mNumberPreviousOffsets = 0;
3627            addPositionToTouchUpFilter(offset);
3628        }
3629
3630        private void addPositionToTouchUpFilter(int offset) {
3631            mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
3632            mPreviousOffsets[mPreviousOffsetIndex] = offset;
3633            mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
3634            mNumberPreviousOffsets++;
3635        }
3636
3637        private void filterOnTouchUp() {
3638            final long now = SystemClock.uptimeMillis();
3639            int i = 0;
3640            int index = mPreviousOffsetIndex;
3641            final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
3642            while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
3643                i++;
3644                index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
3645            }
3646
3647            if (i > 0 && i < iMax &&
3648                    (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
3649                positionAtCursorOffset(mPreviousOffsets[index], false);
3650            }
3651        }
3652
3653        public boolean offsetHasBeenChanged() {
3654            return mNumberPreviousOffsets > 1;
3655        }
3656
3657        @Override
3658        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
3659            setMeasuredDimension(getPreferredWidth(), getPreferredHeight());
3660        }
3661
3662        private int getPreferredWidth() {
3663            return Math.max(mDrawable.getIntrinsicWidth(), mMinSize);
3664        }
3665
3666        private int getPreferredHeight() {
3667            return Math.max(mDrawable.getIntrinsicHeight(), mMinSize);
3668        }
3669
3670        public void show() {
3671            if (isShowing()) return;
3672
3673            getPositionListener().addSubscriber(this, true /* local position may change */);
3674
3675            // Make sure the offset is always considered new, even when focusing at same position
3676            mPreviousOffset = -1;
3677            positionAtCursorOffset(getCurrentCursorOffset(), false);
3678        }
3679
3680        protected void dismiss() {
3681            mIsDragging = false;
3682            mContainer.dismiss();
3683            onDetached();
3684        }
3685
3686        public void hide() {
3687            dismiss();
3688
3689            getPositionListener().removeSubscriber(this);
3690        }
3691
3692        public boolean isShowing() {
3693            return mContainer.isShowing();
3694        }
3695
3696        private boolean isVisible() {
3697            // Always show a dragging handle.
3698            if (mIsDragging) {
3699                return true;
3700            }
3701
3702            if (mTextView.isInBatchEditMode()) {
3703                return false;
3704            }
3705
3706            return isPositionVisible(mPositionX + mHotspotX + getHorizontalOffset(), mPositionY);
3707        }
3708
3709        public abstract int getCurrentCursorOffset();
3710
3711        protected abstract void updateSelection(int offset);
3712
3713        public abstract void updatePosition(float x, float y);
3714
3715        protected void positionAtCursorOffset(int offset, boolean parentScrolled) {
3716            // A HandleView relies on the layout, which may be nulled by external methods
3717            Layout layout = mTextView.getLayout();
3718            if (layout == null) {
3719                // Will update controllers' state, hiding them and stopping selection mode if needed
3720                prepareCursorControllers();
3721                return;
3722            }
3723            layout = getActiveLayout();
3724
3725            boolean offsetChanged = offset != mPreviousOffset;
3726            if (offsetChanged || parentScrolled) {
3727                if (offsetChanged) {
3728                    updateSelection(offset);
3729                    addPositionToTouchUpFilter(offset);
3730                }
3731                final int line = layout.getLineForOffset(offset);
3732                mPrevLine = line;
3733
3734                mPositionX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX -
3735                        getHorizontalOffset() + getCursorOffset());
3736                mPositionY = layout.getLineBottom(line);
3737
3738                // Take TextView's padding and scroll into account.
3739                mPositionX += mTextView.viewportToContentHorizontalOffset();
3740                mPositionY += mTextView.viewportToContentVerticalOffset();
3741
3742                mPreviousOffset = offset;
3743                mPositionHasChanged = true;
3744            }
3745        }
3746
3747        public void updatePosition(int parentPositionX, int parentPositionY,
3748                boolean parentPositionChanged, boolean parentScrolled) {
3749            positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled);
3750            if (parentPositionChanged || mPositionHasChanged) {
3751                if (mIsDragging) {
3752                    // Update touchToWindow offset in case of parent scrolling while dragging
3753                    if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
3754                        mTouchToWindowOffsetX += parentPositionX - mLastParentX;
3755                        mTouchToWindowOffsetY += parentPositionY - mLastParentY;
3756                        mLastParentX = parentPositionX;
3757                        mLastParentY = parentPositionY;
3758                    }
3759
3760                    onHandleMoved();
3761                }
3762
3763                if (isVisible()) {
3764                    final int positionX = parentPositionX + mPositionX;
3765                    final int positionY = parentPositionY + mPositionY;
3766                    if (isShowing()) {
3767                        mContainer.update(positionX, positionY, -1, -1);
3768                    } else {
3769                        mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY,
3770                                positionX, positionY);
3771                    }
3772                } else {
3773                    if (isShowing()) {
3774                        dismiss();
3775                    }
3776                }
3777
3778                mPositionHasChanged = false;
3779            }
3780        }
3781
3782        public void showAtLocation(int offset) {
3783            // TODO - investigate if there's a better way to show the handles
3784            // after the drag accelerator has occured.
3785            int[] tmpCords = new int[2];
3786            mTextView.getLocationInWindow(tmpCords);
3787
3788            Layout layout = mTextView.getLayout();
3789            int posX = tmpCords[0];
3790            int posY = tmpCords[1];
3791
3792            final int line = layout.getLineForOffset(offset);
3793
3794            int startX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f
3795                    - mHotspotX - getHorizontalOffset() + getCursorOffset());
3796            int startY = layout.getLineBottom(line);
3797
3798            // Take TextView's padding and scroll into account.
3799            startX += mTextView.viewportToContentHorizontalOffset();
3800            startY += mTextView.viewportToContentVerticalOffset();
3801
3802            mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY,
3803                    startX + posX, startY + posY);
3804        }
3805
3806        @Override
3807        protected void onDraw(Canvas c) {
3808            final int drawWidth = mDrawable.getIntrinsicWidth();
3809            final int left = getHorizontalOffset();
3810
3811            mDrawable.setBounds(left, 0, left + drawWidth, mDrawable.getIntrinsicHeight());
3812            mDrawable.draw(c);
3813        }
3814
3815        private int getHorizontalOffset() {
3816            final int width = getPreferredWidth();
3817            final int drawWidth = mDrawable.getIntrinsicWidth();
3818            final int left;
3819            switch (mHorizontalGravity) {
3820                case Gravity.LEFT:
3821                    left = 0;
3822                    break;
3823                default:
3824                case Gravity.CENTER:
3825                    left = (width - drawWidth) / 2;
3826                    break;
3827                case Gravity.RIGHT:
3828                    left = width - drawWidth;
3829                    break;
3830            }
3831            return left;
3832        }
3833
3834        protected int getCursorOffset() {
3835            return 0;
3836        }
3837
3838        @Override
3839        public boolean onTouchEvent(MotionEvent ev) {
3840            updateFloatingToolbarVisibility(ev);
3841
3842            switch (ev.getActionMasked()) {
3843                case MotionEvent.ACTION_DOWN: {
3844                    startTouchUpFilter(getCurrentCursorOffset());
3845                    mTouchToWindowOffsetX = ev.getRawX() - mPositionX;
3846                    mTouchToWindowOffsetY = ev.getRawY() - mPositionY;
3847
3848                    final PositionListener positionListener = getPositionListener();
3849                    mLastParentX = positionListener.getPositionX();
3850                    mLastParentY = positionListener.getPositionY();
3851                    mIsDragging = true;
3852                    mPreviousLineTouched = UNSET_LINE;
3853                    break;
3854                }
3855
3856                case MotionEvent.ACTION_MOVE: {
3857                    final float rawX = ev.getRawX();
3858                    final float rawY = ev.getRawY();
3859
3860                    // Vertical hysteresis: vertical down movement tends to snap to ideal offset
3861                    final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
3862                    final float currentVerticalOffset = rawY - mPositionY - mLastParentY;
3863                    float newVerticalOffset;
3864                    if (previousVerticalOffset < mIdealVerticalOffset) {
3865                        newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
3866                        newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
3867                    } else {
3868                        newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
3869                        newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
3870                    }
3871                    mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
3872
3873                    final float newPosX =
3874                            rawX - mTouchToWindowOffsetX + mHotspotX + getHorizontalOffset();
3875                    final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY;
3876
3877                    updatePosition(newPosX, newPosY);
3878                    break;
3879                }
3880
3881                case MotionEvent.ACTION_UP:
3882                    filterOnTouchUp();
3883                    mIsDragging = false;
3884                    updateDrawable();
3885                    break;
3886
3887                case MotionEvent.ACTION_CANCEL:
3888                    mIsDragging = false;
3889                    updateDrawable();
3890                    break;
3891            }
3892            return true;
3893        }
3894
3895        public boolean isDragging() {
3896            return mIsDragging;
3897        }
3898
3899        void onHandleMoved() {}
3900
3901        public void onDetached() {}
3902    }
3903
3904    /**
3905     * Returns the active layout (hint or text layout). Note that the text layout can be null.
3906     */
3907    private Layout getActiveLayout() {
3908        Layout layout = mTextView.getLayout();
3909        Layout hintLayout = mTextView.getHintLayout();
3910        if (TextUtils.isEmpty(layout.getText()) && hintLayout != null &&
3911                !TextUtils.isEmpty(hintLayout.getText())) {
3912            layout = hintLayout;
3913        }
3914        return layout;
3915    }
3916
3917    private class InsertionHandleView extends HandleView {
3918        private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
3919        private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
3920
3921        // Used to detect taps on the insertion handle, which will affect the insertion action mode
3922        private float mDownPositionX, mDownPositionY;
3923        private Runnable mHider;
3924
3925        public InsertionHandleView(Drawable drawable) {
3926            super(drawable, drawable, com.android.internal.R.id.insertion_handle);
3927        }
3928
3929        @Override
3930        public void show() {
3931            super.show();
3932
3933            final long durationSinceCutOrCopy =
3934                    SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime;
3935
3936            // Cancel the single tap delayed runnable.
3937            if (mInsertionActionModeRunnable != null
3938                    && (mDoubleTap || isCursorInsideEasyCorrectionSpan())) {
3939                mTextView.removeCallbacks(mInsertionActionModeRunnable);
3940            }
3941
3942            // Prepare and schedule the single tap runnable to run exactly after the double tap
3943            // timeout has passed.
3944            if (!mDoubleTap && !isCursorInsideEasyCorrectionSpan()
3945                    && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION)) {
3946                if (mTextActionMode == null) {
3947                    if (mInsertionActionModeRunnable == null) {
3948                        mInsertionActionModeRunnable = new Runnable() {
3949                            @Override
3950                            public void run() {
3951                                startInsertionActionMode();
3952                            }
3953                        };
3954                    }
3955                    mTextView.postDelayed(
3956                            mInsertionActionModeRunnable,
3957                            ViewConfiguration.getDoubleTapTimeout() + 1);
3958                }
3959
3960            }
3961
3962            hideAfterDelay();
3963        }
3964
3965        private void hideAfterDelay() {
3966            if (mHider == null) {
3967                mHider = new Runnable() {
3968                    public void run() {
3969                        hide();
3970                    }
3971                };
3972            } else {
3973                removeHiderCallback();
3974            }
3975            mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
3976        }
3977
3978        private void removeHiderCallback() {
3979            if (mHider != null) {
3980                mTextView.removeCallbacks(mHider);
3981            }
3982        }
3983
3984        @Override
3985        protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
3986            return drawable.getIntrinsicWidth() / 2;
3987        }
3988
3989        @Override
3990        protected int getHorizontalGravity(boolean isRtlRun) {
3991            return Gravity.CENTER_HORIZONTAL;
3992        }
3993
3994        @Override
3995        protected int getCursorOffset() {
3996            int offset = super.getCursorOffset();
3997            final Drawable cursor = mCursorCount > 0 ? mCursorDrawable[0] : null;
3998            if (cursor != null) {
3999                cursor.getPadding(mTempRect);
4000                offset += (cursor.getIntrinsicWidth() - mTempRect.left - mTempRect.right) / 2;
4001            }
4002            return offset;
4003        }
4004
4005        @Override
4006        public boolean onTouchEvent(MotionEvent ev) {
4007            final boolean result = super.onTouchEvent(ev);
4008
4009            switch (ev.getActionMasked()) {
4010                case MotionEvent.ACTION_DOWN:
4011                    mDownPositionX = ev.getRawX();
4012                    mDownPositionY = ev.getRawY();
4013                    break;
4014
4015                case MotionEvent.ACTION_UP:
4016                    if (!offsetHasBeenChanged()) {
4017                        final float deltaX = mDownPositionX - ev.getRawX();
4018                        final float deltaY = mDownPositionY - ev.getRawY();
4019                        final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
4020
4021                        final ViewConfiguration viewConfiguration = ViewConfiguration.get(
4022                                mTextView.getContext());
4023                        final int touchSlop = viewConfiguration.getScaledTouchSlop();
4024
4025                        if (distanceSquared < touchSlop * touchSlop) {
4026                            // Tapping on the handle toggles the insertion action mode.
4027                            if (mTextActionMode != null) {
4028                                mTextActionMode.finish();
4029                            } else {
4030                                startInsertionActionMode();
4031                            }
4032                        }
4033                    } else {
4034                        if (mTextActionMode != null) {
4035                            mTextActionMode.invalidateContentRect();
4036                        }
4037                    }
4038                    hideAfterDelay();
4039                    break;
4040
4041                case MotionEvent.ACTION_CANCEL:
4042                    hideAfterDelay();
4043                    break;
4044
4045                default:
4046                    break;
4047            }
4048
4049            return result;
4050        }
4051
4052        @Override
4053        public int getCurrentCursorOffset() {
4054            return mTextView.getSelectionStart();
4055        }
4056
4057        @Override
4058        public void updateSelection(int offset) {
4059            Selection.setSelection((Spannable) mTextView.getText(), offset);
4060        }
4061
4062        @Override
4063        public void updatePosition(float x, float y) {
4064            Layout layout = mTextView.getLayout();
4065            int offset;
4066            if (layout != null) {
4067                if (mPreviousLineTouched == UNSET_LINE) {
4068                    mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
4069                }
4070                int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
4071                offset = mTextView.getOffsetAtCoordinate(currLine, x);
4072                mPreviousLineTouched = currLine;
4073            } else {
4074                offset = mTextView.getOffsetForPosition(x, y);
4075            }
4076            positionAtCursorOffset(offset, false);
4077            if (mTextActionMode != null) {
4078                mTextActionMode.invalidate();
4079            }
4080        }
4081
4082        @Override
4083        void onHandleMoved() {
4084            super.onHandleMoved();
4085            removeHiderCallback();
4086        }
4087
4088        @Override
4089        public void onDetached() {
4090            super.onDetached();
4091            removeHiderCallback();
4092        }
4093    }
4094
4095    @Retention(RetentionPolicy.SOURCE)
4096    @IntDef({HANDLE_TYPE_SELECTION_START, HANDLE_TYPE_SELECTION_END})
4097    public @interface HandleType {}
4098    public static final int HANDLE_TYPE_SELECTION_START = 0;
4099    public static final int HANDLE_TYPE_SELECTION_END = 1;
4100
4101    private class SelectionHandleView extends HandleView {
4102        // Indicates the handle type, selection start (HANDLE_TYPE_SELECTION_START) or selection
4103        // end (HANDLE_TYPE_SELECTION_END).
4104        @HandleType
4105        private final int mHandleType;
4106        // Indicates whether the cursor is making adjustments within a word.
4107        private boolean mInWord = false;
4108        // Difference between touch position and word boundary position.
4109        private float mTouchWordDelta;
4110        // X value of the previous updatePosition call.
4111        private float mPrevX;
4112        // Indicates if the handle has moved a boundary between LTR and RTL text.
4113        private boolean mLanguageDirectionChanged = false;
4114        // Distance from edge of horizontally scrolling text view
4115        // to use to switch to character mode.
4116        private final float mTextViewEdgeSlop;
4117        // Used to save text view location.
4118        private final int[] mTextViewLocation = new int[2];
4119
4120        public SelectionHandleView(Drawable drawableLtr, Drawable drawableRtl, int id,
4121                @HandleType int handleType) {
4122            super(drawableLtr, drawableRtl, id);
4123            mHandleType = handleType;
4124            ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext());
4125            mTextViewEdgeSlop = viewConfiguration.getScaledTouchSlop() * 4;
4126        }
4127
4128        private boolean isStartHandle() {
4129            return mHandleType == HANDLE_TYPE_SELECTION_START;
4130        }
4131
4132        @Override
4133        protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
4134            if (isRtlRun == isStartHandle()) {
4135                return drawable.getIntrinsicWidth() / 4;
4136            } else {
4137                return (drawable.getIntrinsicWidth() * 3) / 4;
4138            }
4139        }
4140
4141        @Override
4142        protected int getHorizontalGravity(boolean isRtlRun) {
4143            return (isRtlRun == isStartHandle()) ? Gravity.LEFT : Gravity.RIGHT;
4144        }
4145
4146        @Override
4147        public int getCurrentCursorOffset() {
4148            return isStartHandle() ? mTextView.getSelectionStart() : mTextView.getSelectionEnd();
4149        }
4150
4151        @Override
4152        protected void updateSelection(int offset) {
4153            if (isStartHandle()) {
4154                Selection.setSelection((Spannable) mTextView.getText(), offset,
4155                        mTextView.getSelectionEnd());
4156            } else {
4157                Selection.setSelection((Spannable) mTextView.getText(),
4158                        mTextView.getSelectionStart(), offset);
4159            }
4160            updateDrawable();
4161            if (mTextActionMode != null) {
4162                mTextActionMode.invalidate();
4163            }
4164        }
4165
4166        @Override
4167        public void updatePosition(float x, float y) {
4168            final Layout layout = mTextView.getLayout();
4169            if (layout == null) {
4170                // HandleView will deal appropriately in positionAtCursorOffset when
4171                // layout is null.
4172                positionAndAdjustForCrossingHandles(mTextView.getOffsetForPosition(x, y));
4173                return;
4174            }
4175
4176            if (mPreviousLineTouched == UNSET_LINE) {
4177                mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
4178            }
4179
4180            boolean positionCursor = false;
4181            final int anotherHandleOffset =
4182                    isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
4183            int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
4184            int initialOffset = mTextView.getOffsetAtCoordinate(currLine, x);
4185
4186            if (isStartHandle() && initialOffset >= anotherHandleOffset
4187                    || !isStartHandle() && initialOffset <= anotherHandleOffset) {
4188                // Handles have crossed, bound it to the first selected line and
4189                // adjust by word / char as normal.
4190                currLine = layout.getLineForOffset(anotherHandleOffset);
4191                initialOffset = mTextView.getOffsetAtCoordinate(currLine, x);
4192            }
4193
4194            int offset = initialOffset;
4195            final int wordEnd = getWordEnd(offset);
4196            final int wordStart = getWordStart(offset);
4197
4198            if (mPrevX == UNSET_X_VALUE) {
4199                mPrevX = x;
4200            }
4201
4202            final int currentOffset = getCurrentCursorOffset();
4203            final boolean rtlAtCurrentOffset = layout.isRtlCharAt(currentOffset);
4204            final boolean atRtl = layout.isRtlCharAt(offset);
4205            final boolean isLvlBoundary = layout.isLevelBoundary(offset);
4206
4207            // We can't determine if the user is expanding or shrinking the selection if they're
4208            // on a bi-di boundary, so until they've moved past the boundary we'll just place
4209            // the cursor at the current position.
4210            if (isLvlBoundary || (rtlAtCurrentOffset && !atRtl) || (!rtlAtCurrentOffset && atRtl)) {
4211                // We're on a boundary or this is the first direction change -- just update
4212                // to the current position.
4213                mLanguageDirectionChanged = true;
4214                mTouchWordDelta = 0.0f;
4215                positionAndAdjustForCrossingHandles(offset);
4216                return;
4217            } else if (mLanguageDirectionChanged && !isLvlBoundary) {
4218                // We've just moved past the boundary so update the position. After this we can
4219                // figure out if the user is expanding or shrinking to go by word or character.
4220                positionAndAdjustForCrossingHandles(offset);
4221                mTouchWordDelta = 0.0f;
4222                mLanguageDirectionChanged = false;
4223                return;
4224            }
4225
4226            boolean isExpanding;
4227            final float xDiff = x - mPrevX;
4228            if (atRtl == isStartHandle()) {
4229                isExpanding = xDiff > 0 || currLine > mPreviousLineTouched;
4230            } else {
4231                isExpanding = xDiff < 0 || currLine < mPreviousLineTouched;
4232            }
4233
4234            if (mTextView.getHorizontallyScrolling()) {
4235                if (positionNearEdgeOfScrollingView(x, atRtl)
4236                        && ((isStartHandle() && mTextView.getScrollX() != 0)
4237                                || (!isStartHandle()
4238                                        && mTextView.canScrollHorizontally(atRtl ? -1 : 1)))
4239                        && ((isExpanding && ((isStartHandle() && offset < currentOffset)
4240                                || (!isStartHandle() && offset > currentOffset)))
4241                                        || !isExpanding)) {
4242                    // If we're expanding ensure that the offset is actually expanding compared to
4243                    // the current offset, if the handle snapped to the word, the finger position
4244                    // may be out of sync and we don't want the selection to jump back.
4245                    mTouchWordDelta = 0.0f;
4246                    final int nextOffset = (atRtl == isStartHandle())
4247                            ? layout.getOffsetToRightOf(mPreviousOffset)
4248                            : layout.getOffsetToLeftOf(mPreviousOffset);
4249                    positionAndAdjustForCrossingHandles(nextOffset);
4250                    return;
4251                }
4252            }
4253
4254            if (isExpanding) {
4255                // User is increasing the selection.
4256                final boolean snapToWord = !mInWord
4257                        || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine);
4258                if (snapToWord) {
4259                    // Sometimes words can be broken across lines (Chinese, hyphenation).
4260                    // We still snap to the word boundary but we only use the letters on the
4261                    // current line to determine if the user is far enough into the word to snap.
4262                    int wordBoundary = isStartHandle() ? wordStart : wordEnd;
4263                    if (layout != null && layout.getLineForOffset(wordBoundary) != currLine) {
4264                        wordBoundary = isStartHandle() ?
4265                                layout.getLineStart(currLine) : layout.getLineEnd(currLine);
4266                    }
4267                    final int offsetThresholdToSnap = isStartHandle()
4268                            ? wordEnd - ((wordEnd - wordBoundary) / 2)
4269                            : wordStart + ((wordBoundary - wordStart) / 2);
4270                    if (isStartHandle()
4271                            && (offset <= offsetThresholdToSnap || currLine < mPrevLine)) {
4272                        // User is far enough into the word or on a different line so we expand by
4273                        // word.
4274                        offset = wordStart;
4275                    } else if (!isStartHandle()
4276                            && (offset >= offsetThresholdToSnap || currLine > mPrevLine)) {
4277                        // User is far enough into the word or on a different line so we expand by
4278                        // word.
4279                        offset = wordEnd;
4280                    } else {
4281                        offset = mPreviousOffset;
4282                    }
4283                }
4284                if (layout != null && (isStartHandle() && offset < initialOffset)
4285                        || (!isStartHandle() && offset > initialOffset)) {
4286                    final float adjustedX = layout.getPrimaryHorizontal(offset);
4287                    mTouchWordDelta =
4288                            mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
4289                } else {
4290                    mTouchWordDelta = 0.0f;
4291                }
4292                positionCursor = true;
4293            } else {
4294                final int adjustedOffset =
4295                        mTextView.getOffsetAtCoordinate(currLine, x - mTouchWordDelta);
4296                final boolean shrinking = isStartHandle()
4297                        ? adjustedOffset > mPreviousOffset || currLine > mPrevLine
4298                        : adjustedOffset < mPreviousOffset || currLine < mPrevLine;
4299                if (shrinking) {
4300                    // User is shrinking the selection.
4301                    if (currLine != mPrevLine) {
4302                        // We're on a different line, so we'll snap to word boundaries.
4303                        offset = isStartHandle() ? wordStart : wordEnd;
4304                        if (layout != null && (isStartHandle() && offset < initialOffset)
4305                                || (!isStartHandle() && offset > initialOffset)) {
4306                            final float adjustedX = layout.getPrimaryHorizontal(offset);
4307                            mTouchWordDelta =
4308                                    mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
4309                        } else {
4310                            mTouchWordDelta = 0.0f;
4311                        }
4312                    } else {
4313                        offset = adjustedOffset;
4314                    }
4315                    positionCursor = true;
4316                } else if ((isStartHandle() && adjustedOffset < mPreviousOffset)
4317                        || (!isStartHandle() && adjustedOffset > mPreviousOffset)) {
4318                    // Handle has jumped to the word boundary, and the user is moving
4319                    // their finger towards the handle, the delta should be updated.
4320                    mTouchWordDelta = mTextView.convertToLocalHorizontalCoordinate(x) -
4321                            layout.getPrimaryHorizontal(mPreviousOffset);
4322                }
4323            }
4324
4325            if (positionCursor) {
4326                mPreviousLineTouched = currLine;
4327                positionAndAdjustForCrossingHandles(offset);
4328            }
4329            mPrevX = x;
4330        }
4331
4332        /**
4333         * @param offset Cursor offset. Must be in [-1, length].
4334         * @param parentScrolled If the parent has been scrolled or not.
4335         */
4336        @Override
4337        protected void positionAtCursorOffset(int offset, boolean parentScrolled) {
4338            super.positionAtCursorOffset(offset, parentScrolled);
4339            mInWord = (offset != -1) && !getWordIteratorWithText().isBoundary(offset);
4340        }
4341
4342        @Override
4343        public boolean onTouchEvent(MotionEvent event) {
4344            boolean superResult = super.onTouchEvent(event);
4345            if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
4346                // Reset the touch word offset and x value when the user
4347                // re-engages the handle.
4348                mTouchWordDelta = 0.0f;
4349                mPrevX = UNSET_X_VALUE;
4350            }
4351            return superResult;
4352        }
4353
4354        private void positionAndAdjustForCrossingHandles(int offset) {
4355            final int anotherHandleOffset =
4356                    isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
4357            if ((isStartHandle() && offset >= anotherHandleOffset)
4358                    || (!isStartHandle() && offset <= anotherHandleOffset)) {
4359                // Handles can not cross and selection is at least one character.
4360                offset = getNextCursorOffset(anotherHandleOffset, !isStartHandle());
4361                mTouchWordDelta = 0.0f;
4362            }
4363            positionAtCursorOffset(offset, false);
4364        }
4365
4366        private boolean positionNearEdgeOfScrollingView(float x, boolean atRtl) {
4367            mTextView.getLocationOnScreen(mTextViewLocation);
4368            boolean nearEdge;
4369            if (atRtl == isStartHandle()) {
4370                int rightEdge = mTextViewLocation[0] + mTextView.getWidth()
4371                        - mTextView.getPaddingRight();
4372                nearEdge = x > rightEdge - mTextViewEdgeSlop;
4373            } else {
4374                int leftEdge = mTextViewLocation[0] + mTextView.getPaddingLeft();
4375                nearEdge = x < leftEdge + mTextViewEdgeSlop;
4376            }
4377            return nearEdge;
4378        }
4379    }
4380
4381    private int getCurrentLineAdjustedForSlop(Layout layout, int prevLine, float y) {
4382        final int trueLine = mTextView.getLineAtCoordinate(y);
4383        if (layout == null || prevLine > layout.getLineCount()
4384                || layout.getLineCount() <= 0 || prevLine < 0) {
4385            // Invalid parameters, just return whatever line is at y.
4386            return trueLine;
4387        }
4388
4389        if (Math.abs(trueLine - prevLine) >= 2) {
4390            // Only stick to lines if we're within a line of the previous selection.
4391            return trueLine;
4392        }
4393
4394        final float verticalOffset = mTextView.viewportToContentVerticalOffset();
4395        final int lineCount = layout.getLineCount();
4396        final float slop = mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS;
4397
4398        final float firstLineTop = layout.getLineTop(0) + verticalOffset;
4399        final float prevLineTop = layout.getLineTop(prevLine) + verticalOffset;
4400        final float yTopBound = Math.max(prevLineTop - slop, firstLineTop + slop);
4401
4402        final float lastLineBottom = layout.getLineBottom(lineCount - 1) + verticalOffset;
4403        final float prevLineBottom = layout.getLineBottom(prevLine) + verticalOffset;
4404        final float yBottomBound = Math.min(prevLineBottom + slop, lastLineBottom - slop);
4405
4406        // Determine if we've moved lines based on y position and previous line.
4407        int currLine;
4408        if (y <= yTopBound) {
4409            currLine = Math.max(prevLine - 1, 0);
4410        } else if (y >= yBottomBound) {
4411            currLine = Math.min(prevLine + 1, lineCount - 1);
4412        } else {
4413            currLine = prevLine;
4414        }
4415        return currLine;
4416    }
4417
4418    /**
4419     * A CursorController instance can be used to control a cursor in the text.
4420     */
4421    private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
4422        /**
4423         * Makes the cursor controller visible on screen.
4424         * See also {@link #hide()}.
4425         */
4426        public void show();
4427
4428        /**
4429         * Hide the cursor controller from screen.
4430         * See also {@link #show()}.
4431         */
4432        public void hide();
4433
4434        /**
4435         * Called when the view is detached from window. Perform house keeping task, such as
4436         * stopping Runnable thread that would otherwise keep a reference on the context, thus
4437         * preventing the activity from being recycled.
4438         */
4439        public void onDetached();
4440    }
4441
4442    private class InsertionPointCursorController implements CursorController {
4443        private InsertionHandleView mHandle;
4444
4445        public void show() {
4446            getHandle().show();
4447
4448            if (mSelectionModifierCursorController != null) {
4449                mSelectionModifierCursorController.hide();
4450            }
4451        }
4452
4453        public void hide() {
4454            if (mHandle != null) {
4455                mHandle.hide();
4456            }
4457        }
4458
4459        public void onTouchModeChanged(boolean isInTouchMode) {
4460            if (!isInTouchMode) {
4461                hide();
4462            }
4463        }
4464
4465        private InsertionHandleView getHandle() {
4466            if (mSelectHandleCenter == null) {
4467                mSelectHandleCenter = mTextView.getContext().getDrawable(
4468                        mTextView.mTextSelectHandleRes);
4469            }
4470            if (mHandle == null) {
4471                mHandle = new InsertionHandleView(mSelectHandleCenter);
4472            }
4473            return mHandle;
4474        }
4475
4476        @Override
4477        public void onDetached() {
4478            final ViewTreeObserver observer = mTextView.getViewTreeObserver();
4479            observer.removeOnTouchModeChangeListener(this);
4480
4481            if (mHandle != null) mHandle.onDetached();
4482        }
4483    }
4484
4485    class SelectionModifierCursorController implements CursorController {
4486        // The cursor controller handles, lazily created when shown.
4487        private SelectionHandleView mStartHandle;
4488        private SelectionHandleView mEndHandle;
4489        // The offsets of that last touch down event. Remembered to start selection there.
4490        private int mMinTouchOffset, mMaxTouchOffset;
4491
4492        private float mDownPositionX, mDownPositionY;
4493        private boolean mGestureStayedInTapRegion;
4494
4495        // Where the user first starts the drag motion.
4496        private int mStartOffset = -1;
4497
4498        private boolean mHaventMovedEnoughToStartDrag;
4499        // The line that a selection happened most recently with the drag accelerator.
4500        private int mLineSelectionIsOn = -1;
4501        // Whether the drag accelerator has selected past the initial line.
4502        private boolean mSwitchedLines = false;
4503
4504        // Indicates the drag accelerator mode that the user is currently using.
4505        private int mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
4506        // Drag accelerator is inactive.
4507        private static final int DRAG_ACCELERATOR_MODE_INACTIVE = 0;
4508        // Character based selection by dragging. Only for mouse.
4509        private static final int DRAG_ACCELERATOR_MODE_CHARACTER = 1;
4510        // Word based selection by dragging. Enabled after long pressing or double tapping.
4511        private static final int DRAG_ACCELERATOR_MODE_WORD = 2;
4512
4513        SelectionModifierCursorController() {
4514            resetTouchOffsets();
4515        }
4516
4517        public void show() {
4518            if (mTextView.isInBatchEditMode()) {
4519                return;
4520            }
4521            initDrawables();
4522            initHandles();
4523        }
4524
4525        private void initDrawables() {
4526            if (mSelectHandleLeft == null) {
4527                mSelectHandleLeft = mTextView.getContext().getDrawable(
4528                        mTextView.mTextSelectHandleLeftRes);
4529            }
4530            if (mSelectHandleRight == null) {
4531                mSelectHandleRight = mTextView.getContext().getDrawable(
4532                        mTextView.mTextSelectHandleRightRes);
4533            }
4534        }
4535
4536        private void initHandles() {
4537            // Lazy object creation has to be done before updatePosition() is called.
4538            if (mStartHandle == null) {
4539                mStartHandle = new SelectionHandleView(mSelectHandleLeft, mSelectHandleRight,
4540                        com.android.internal.R.id.selection_start_handle,
4541                        HANDLE_TYPE_SELECTION_START);
4542            }
4543            if (mEndHandle == null) {
4544                mEndHandle = new SelectionHandleView(mSelectHandleRight, mSelectHandleLeft,
4545                        com.android.internal.R.id.selection_end_handle,
4546                        HANDLE_TYPE_SELECTION_END);
4547            }
4548
4549            mStartHandle.show();
4550            mEndHandle.show();
4551
4552            hideInsertionPointCursorController();
4553        }
4554
4555        public void hide() {
4556            if (mStartHandle != null) mStartHandle.hide();
4557            if (mEndHandle != null) mEndHandle.hide();
4558        }
4559
4560        public void enterDrag(int dragAcceleratorMode) {
4561            // Just need to init the handles / hide insertion cursor.
4562            show();
4563            mDragAcceleratorMode = dragAcceleratorMode;
4564            // Start location of selection.
4565            mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX,
4566                    mLastDownPositionY);
4567            mLineSelectionIsOn = mTextView.getLineAtCoordinate(mLastDownPositionY);
4568            // Don't show the handles until user has lifted finger.
4569            hide();
4570
4571            // This stops scrolling parents from intercepting the touch event, allowing
4572            // the user to continue dragging across the screen to select text; TextView will
4573            // scroll as necessary.
4574            mTextView.getParent().requestDisallowInterceptTouchEvent(true);
4575            mTextView.cancelLongPress();
4576        }
4577
4578        public void onTouchEvent(MotionEvent event) {
4579            // This is done even when the View does not have focus, so that long presses can start
4580            // selection and tap can move cursor from this tap position.
4581            final float eventX = event.getX();
4582            final float eventY = event.getY();
4583            final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
4584            switch (event.getActionMasked()) {
4585                case MotionEvent.ACTION_DOWN:
4586                    if (extractedTextModeWillBeStarted()) {
4587                        // Prevent duplicating the selection handles until the mode starts.
4588                        hide();
4589                    } else {
4590                        // Remember finger down position, to be able to start selection from there.
4591                        mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(
4592                                eventX, eventY);
4593
4594                        // Double tap detection
4595                        if (mGestureStayedInTapRegion) {
4596                            if (mDoubleTap) {
4597                                final float deltaX = eventX - mDownPositionX;
4598                                final float deltaY = eventY - mDownPositionY;
4599                                final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
4600
4601                                ViewConfiguration viewConfiguration = ViewConfiguration.get(
4602                                        mTextView.getContext());
4603                                int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
4604                                boolean stayedInArea =
4605                                        distanceSquared < doubleTapSlop * doubleTapSlop;
4606
4607                                if (stayedInArea && (isMouse || isPositionOnText(eventX, eventY))) {
4608                                    selectCurrentWordAndStartDrag();
4609                                    mDiscardNextActionUp = true;
4610                                }
4611                            }
4612                        }
4613
4614                        mDownPositionX = eventX;
4615                        mDownPositionY = eventY;
4616                        mGestureStayedInTapRegion = true;
4617                        mHaventMovedEnoughToStartDrag = true;
4618                    }
4619                    break;
4620
4621                case MotionEvent.ACTION_POINTER_DOWN:
4622                case MotionEvent.ACTION_POINTER_UP:
4623                    // Handle multi-point gestures. Keep min and max offset positions.
4624                    // Only activated for devices that correctly handle multi-touch.
4625                    if (mTextView.getContext().getPackageManager().hasSystemFeature(
4626                            PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
4627                        updateMinAndMaxOffsets(event);
4628                    }
4629                    break;
4630
4631                case MotionEvent.ACTION_MOVE:
4632                    final ViewConfiguration viewConfig = ViewConfiguration.get(
4633                            mTextView.getContext());
4634                    final int touchSlop = viewConfig.getScaledTouchSlop();
4635
4636                    if (mGestureStayedInTapRegion || mHaventMovedEnoughToStartDrag) {
4637                        final float deltaX = eventX - mDownPositionX;
4638                        final float deltaY = eventY - mDownPositionY;
4639                        final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
4640
4641                        if (mGestureStayedInTapRegion) {
4642                            int doubleTapTouchSlop = viewConfig.getScaledDoubleTapTouchSlop();
4643                            mGestureStayedInTapRegion =
4644                                    distanceSquared <= doubleTapTouchSlop * doubleTapTouchSlop;
4645                        }
4646                        if (mHaventMovedEnoughToStartDrag) {
4647                            // We don't start dragging until the user has moved enough.
4648                            mHaventMovedEnoughToStartDrag =
4649                                    distanceSquared <= touchSlop * touchSlop;
4650                        }
4651                    }
4652
4653                    if (isMouse && !isDragAcceleratorActive()) {
4654                        final int offset = mTextView.getOffsetForPosition(eventX, eventY);
4655                        if (mStartOffset != offset) {
4656                            // Start character based drag accelerator.
4657                            if (mTextActionMode != null) {
4658                                mTextActionMode.finish();
4659                            }
4660                            enterDrag(DRAG_ACCELERATOR_MODE_CHARACTER);
4661                            mDiscardNextActionUp = true;
4662                            mHaventMovedEnoughToStartDrag = false;
4663                        }
4664                    }
4665
4666                    if (mStartHandle != null && mStartHandle.isShowing()) {
4667                        // Don't do the drag if the handles are showing already.
4668                        break;
4669                    }
4670
4671                    updateSelection(event);
4672                    break;
4673
4674                case MotionEvent.ACTION_UP:
4675                    if (!isDragAcceleratorActive()) {
4676                        break;
4677                    }
4678                    updateSelection(event);
4679
4680                    // No longer dragging to select text, let the parent intercept events.
4681                    mTextView.getParent().requestDisallowInterceptTouchEvent(false);
4682
4683                    int startOffset = mTextView.getSelectionStart();
4684                    int endOffset = mTextView.getSelectionEnd();
4685
4686                    // Since we don't let drag handles pass once they're visible, we need to
4687                    // make sure the start / end locations are correct because the user *can*
4688                    // switch directions during the initial drag.
4689                    if (endOffset < startOffset) {
4690                        int tmp = endOffset;
4691                        endOffset = startOffset;
4692                        startOffset = tmp;
4693
4694                        // Also update the selection with the right offsets in this case.
4695                        Selection.setSelection((Spannable) mTextView.getText(),
4696                                startOffset, endOffset);
4697                    }
4698                    if (startOffset != endOffset) {
4699                        mStartHandle.showAtLocation(startOffset);
4700                        mEndHandle.showAtLocation(endOffset);
4701                        startSelectionActionMode();
4702                    }
4703
4704                    // No longer the first dragging motion, reset.
4705                    resetDragAcceleratorState();
4706                    break;
4707            }
4708        }
4709
4710        private void updateSelection(MotionEvent event) {
4711            if (mTextView.getLayout() != null) {
4712                switch (mDragAcceleratorMode) {
4713                    case DRAG_ACCELERATOR_MODE_CHARACTER:
4714                        updateCharacterBasedSelection(event);
4715                        break;
4716                    case DRAG_ACCELERATOR_MODE_WORD:
4717                        updateWordBasedSelection(event);
4718                        break;
4719                }
4720            }
4721        }
4722
4723        private void updateCharacterBasedSelection(MotionEvent event) {
4724            final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
4725            Selection.setSelection((Spannable) mTextView.getText(), mStartOffset, offset);
4726        }
4727
4728        private void updateWordBasedSelection(MotionEvent event) {
4729            if (mHaventMovedEnoughToStartDrag) {
4730                return;
4731            }
4732            final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
4733            final ViewConfiguration viewConfig = ViewConfiguration.get(
4734                    mTextView.getContext());
4735            final float eventX = event.getX();
4736            final float eventY = event.getY();
4737            final int currLine;
4738            if (isMouse) {
4739                // No need to offset the y coordinate for mouse input.
4740                currLine = mTextView.getLineAtCoordinate(eventY);
4741            } else {
4742                float y = eventY;
4743                if (mSwitchedLines) {
4744                    // Offset the finger by the same vertical offset as the handles.
4745                    // This improves visibility of the content being selected by
4746                    // shifting the finger below the content, this is applied once
4747                    // the user has switched lines.
4748                    final int touchSlop = viewConfig.getScaledTouchSlop();
4749                    final float fingerOffset = (mStartHandle != null)
4750                            ? mStartHandle.getIdealVerticalOffset()
4751                            : touchSlop;
4752                    y = eventY - fingerOffset;
4753                }
4754
4755                currLine = getCurrentLineAdjustedForSlop(mTextView.getLayout(), mLineSelectionIsOn,
4756                        y);
4757                if (!mSwitchedLines && currLine != mLineSelectionIsOn) {
4758                    // Break early here, we want to offset the finger position from
4759                    // the selection highlight, once the user moved their finger
4760                    // to a different line we should apply the offset and *not* switch
4761                    // lines until recomputing the position with the finger offset.
4762                    mSwitchedLines = true;
4763                    return;
4764                }
4765            }
4766
4767            int startOffset;
4768            int offset = mTextView.getOffsetAtCoordinate(currLine, eventX);
4769            // Snap to word boundaries.
4770            if (mStartOffset < offset) {
4771                // Expanding with end handle.
4772                offset = getWordEnd(offset);
4773                startOffset = getWordStart(mStartOffset);
4774            } else {
4775                // Expanding with start handle.
4776                offset = getWordStart(offset);
4777                startOffset = getWordEnd(mStartOffset);
4778            }
4779            mLineSelectionIsOn = currLine;
4780            Selection.setSelection((Spannable) mTextView.getText(),
4781                    startOffset, offset);
4782        }
4783        /**
4784         * @param event
4785         */
4786        private void updateMinAndMaxOffsets(MotionEvent event) {
4787            int pointerCount = event.getPointerCount();
4788            for (int index = 0; index < pointerCount; index++) {
4789                int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
4790                if (offset < mMinTouchOffset) mMinTouchOffset = offset;
4791                if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
4792            }
4793        }
4794
4795        public int getMinTouchOffset() {
4796            return mMinTouchOffset;
4797        }
4798
4799        public int getMaxTouchOffset() {
4800            return mMaxTouchOffset;
4801        }
4802
4803        public void resetTouchOffsets() {
4804            mMinTouchOffset = mMaxTouchOffset = -1;
4805            resetDragAcceleratorState();
4806        }
4807
4808        private void resetDragAcceleratorState() {
4809            mStartOffset = -1;
4810            mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
4811            mSwitchedLines = false;
4812        }
4813
4814        /**
4815         * @return true iff this controller is currently used to move the selection start.
4816         */
4817        public boolean isSelectionStartDragged() {
4818            return mStartHandle != null && mStartHandle.isDragging();
4819        }
4820
4821        /**
4822         * @return true if the user is selecting text using the drag accelerator.
4823         */
4824        public boolean isDragAcceleratorActive() {
4825            return mDragAcceleratorMode != DRAG_ACCELERATOR_MODE_INACTIVE;
4826        }
4827
4828        public void onTouchModeChanged(boolean isInTouchMode) {
4829            if (!isInTouchMode) {
4830                hide();
4831            }
4832        }
4833
4834        @Override
4835        public void onDetached() {
4836            final ViewTreeObserver observer = mTextView.getViewTreeObserver();
4837            observer.removeOnTouchModeChangeListener(this);
4838
4839            if (mStartHandle != null) mStartHandle.onDetached();
4840            if (mEndHandle != null) mEndHandle.onDetached();
4841        }
4842    }
4843
4844    private class CorrectionHighlighter {
4845        private final Path mPath = new Path();
4846        private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
4847        private int mStart, mEnd;
4848        private long mFadingStartTime;
4849        private RectF mTempRectF;
4850        private final static int FADE_OUT_DURATION = 400;
4851
4852        public CorrectionHighlighter() {
4853            mPaint.setCompatibilityScaling(mTextView.getResources().getCompatibilityInfo().
4854                    applicationScale);
4855            mPaint.setStyle(Paint.Style.FILL);
4856        }
4857
4858        public void highlight(CorrectionInfo info) {
4859            mStart = info.getOffset();
4860            mEnd = mStart + info.getNewText().length();
4861            mFadingStartTime = SystemClock.uptimeMillis();
4862
4863            if (mStart < 0 || mEnd < 0) {
4864                stopAnimation();
4865            }
4866        }
4867
4868        public void draw(Canvas canvas, int cursorOffsetVertical) {
4869            if (updatePath() && updatePaint()) {
4870                if (cursorOffsetVertical != 0) {
4871                    canvas.translate(0, cursorOffsetVertical);
4872                }
4873
4874                canvas.drawPath(mPath, mPaint);
4875
4876                if (cursorOffsetVertical != 0) {
4877                    canvas.translate(0, -cursorOffsetVertical);
4878                }
4879                invalidate(true); // TODO invalidate cursor region only
4880            } else {
4881                stopAnimation();
4882                invalidate(false); // TODO invalidate cursor region only
4883            }
4884        }
4885
4886        private boolean updatePaint() {
4887            final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
4888            if (duration > FADE_OUT_DURATION) return false;
4889
4890            final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
4891            final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
4892            final int color = (mTextView.mHighlightColor & 0x00FFFFFF) +
4893                    ((int) (highlightColorAlpha * coef) << 24);
4894            mPaint.setColor(color);
4895            return true;
4896        }
4897
4898        private boolean updatePath() {
4899            final Layout layout = mTextView.getLayout();
4900            if (layout == null) return false;
4901
4902            // Update in case text is edited while the animation is run
4903            final int length = mTextView.getText().length();
4904            int start = Math.min(length, mStart);
4905            int end = Math.min(length, mEnd);
4906
4907            mPath.reset();
4908            layout.getSelectionPath(start, end, mPath);
4909            return true;
4910        }
4911
4912        private void invalidate(boolean delayed) {
4913            if (mTextView.getLayout() == null) return;
4914
4915            if (mTempRectF == null) mTempRectF = new RectF();
4916            mPath.computeBounds(mTempRectF, false);
4917
4918            int left = mTextView.getCompoundPaddingLeft();
4919            int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
4920
4921            if (delayed) {
4922                mTextView.postInvalidateOnAnimation(
4923                        left + (int) mTempRectF.left, top + (int) mTempRectF.top,
4924                        left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
4925            } else {
4926                mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
4927                        (int) mTempRectF.right, (int) mTempRectF.bottom);
4928            }
4929        }
4930
4931        private void stopAnimation() {
4932            Editor.this.mCorrectionHighlighter = null;
4933        }
4934    }
4935
4936    private static class ErrorPopup extends PopupWindow {
4937        private boolean mAbove = false;
4938        private final TextView mView;
4939        private int mPopupInlineErrorBackgroundId = 0;
4940        private int mPopupInlineErrorAboveBackgroundId = 0;
4941
4942        ErrorPopup(TextView v, int width, int height) {
4943            super(v, width, height);
4944            mView = v;
4945            // Make sure the TextView has a background set as it will be used the first time it is
4946            // shown and positioned. Initialized with below background, which should have
4947            // dimensions identical to the above version for this to work (and is more likely).
4948            mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
4949                    com.android.internal.R.styleable.Theme_errorMessageBackground);
4950            mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
4951        }
4952
4953        void fixDirection(boolean above) {
4954            mAbove = above;
4955
4956            if (above) {
4957                mPopupInlineErrorAboveBackgroundId =
4958                    getResourceId(mPopupInlineErrorAboveBackgroundId,
4959                            com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
4960            } else {
4961                mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
4962                        com.android.internal.R.styleable.Theme_errorMessageBackground);
4963            }
4964
4965            mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId :
4966                mPopupInlineErrorBackgroundId);
4967        }
4968
4969        private int getResourceId(int currentId, int index) {
4970            if (currentId == 0) {
4971                TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
4972                        R.styleable.Theme);
4973                currentId = styledAttributes.getResourceId(index, 0);
4974                styledAttributes.recycle();
4975            }
4976            return currentId;
4977        }
4978
4979        @Override
4980        public void update(int x, int y, int w, int h, boolean force) {
4981            super.update(x, y, w, h, force);
4982
4983            boolean above = isAboveAnchor();
4984            if (above != mAbove) {
4985                fixDirection(above);
4986            }
4987        }
4988    }
4989
4990    static class InputContentType {
4991        int imeOptions = EditorInfo.IME_NULL;
4992        String privateImeOptions;
4993        CharSequence imeActionLabel;
4994        int imeActionId;
4995        Bundle extras;
4996        OnEditorActionListener onEditorActionListener;
4997        boolean enterDown;
4998    }
4999
5000    static class InputMethodState {
5001        ExtractedTextRequest mExtractedTextRequest;
5002        final ExtractedText mExtractedText = new ExtractedText();
5003        int mBatchEditNesting;
5004        boolean mCursorChanged;
5005        boolean mSelectionModeChanged;
5006        boolean mContentChanged;
5007        int mChangedStart, mChangedEnd, mChangedDelta;
5008    }
5009
5010    /**
5011     * @return True iff (start, end) is a valid range within the text.
5012     */
5013    private static boolean isValidRange(CharSequence text, int start, int end) {
5014        return 0 <= start && start <= end && end <= text.length();
5015    }
5016
5017    @VisibleForTesting
5018    public SuggestionsPopupWindow getSuggestionsPopupWindowForTesting() {
5019        return mSuggestionsPopupWindow;
5020    }
5021
5022    /**
5023     * An InputFilter that monitors text input to maintain undo history. It does not modify the
5024     * text being typed (and hence always returns null from the filter() method).
5025     */
5026    public static class UndoInputFilter implements InputFilter {
5027        private final Editor mEditor;
5028
5029        // Whether the current filter pass is directly caused by an end-user text edit.
5030        private boolean mIsUserEdit;
5031
5032        // Whether the text field is handling an IME composition. Must be parceled in case the user
5033        // rotates the screen during composition.
5034        private boolean mHasComposition;
5035
5036        public UndoInputFilter(Editor editor) {
5037            mEditor = editor;
5038        }
5039
5040        public void saveInstanceState(Parcel parcel) {
5041            parcel.writeInt(mIsUserEdit ? 1 : 0);
5042            parcel.writeInt(mHasComposition ? 1 : 0);
5043        }
5044
5045        public void restoreInstanceState(Parcel parcel) {
5046            mIsUserEdit = parcel.readInt() != 0;
5047            mHasComposition = parcel.readInt() != 0;
5048        }
5049
5050        /**
5051         * Signals that a user-triggered edit is starting.
5052         */
5053        public void beginBatchEdit() {
5054            if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit");
5055            mIsUserEdit = true;
5056        }
5057
5058        public void endBatchEdit() {
5059            if (DEBUG_UNDO) Log.d(TAG, "endBatchEdit");
5060            mIsUserEdit = false;
5061        }
5062
5063        @Override
5064        public CharSequence filter(CharSequence source, int start, int end,
5065                Spanned dest, int dstart, int dend) {
5066            if (DEBUG_UNDO) {
5067                Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ") " +
5068                        "dest=" + dest + " (" + dstart + "-" + dend + ")");
5069            }
5070
5071            // Check to see if this edit should be tracked for undo.
5072            if (!canUndoEdit(source, start, end, dest, dstart, dend)) {
5073                return null;
5074            }
5075
5076            // Check for and handle IME composition edits.
5077            if (handleCompositionEdit(source, start, end, dstart)) {
5078                return null;
5079            }
5080
5081            // Handle keyboard edits.
5082            handleKeyboardEdit(source, start, end, dest, dstart, dend);
5083            return null;
5084        }
5085
5086        /**
5087         * Returns true iff the edit was handled, either because it should be ignored or because
5088         * this function created an undo operation for it.
5089         */
5090        private boolean handleCompositionEdit(CharSequence source, int start, int end, int dstart) {
5091            // Ignore edits while the user is composing.
5092            if (isComposition(source)) {
5093                mHasComposition = true;
5094                return true;
5095            }
5096            final boolean hadComposition = mHasComposition;
5097            mHasComposition = false;
5098
5099            // Check for the transition out of the composing state.
5100            if (hadComposition) {
5101                // If there was no text the user canceled composition. Ignore the edit.
5102                if (start == end) {
5103                    return true;
5104                }
5105
5106                // Otherwise the user inserted the composition.
5107                String newText = TextUtils.substring(source, start, end);
5108                EditOperation edit = new EditOperation(mEditor, "", dstart, newText);
5109                recordEdit(edit, false /* forceMerge */);
5110                return true;
5111            }
5112
5113            // This was neither a composition event nor a transition out of composing.
5114            return false;
5115        }
5116
5117        private void handleKeyboardEdit(CharSequence source, int start, int end,
5118                Spanned dest, int dstart, int dend) {
5119            // An application may install a TextWatcher to provide additional modifications after
5120            // the initial input filters run (e.g. a credit card formatter that adds spaces to a
5121            // string). This results in multiple filter() calls for what the user considers to be
5122            // a single operation. Always undo the whole set of changes in one step.
5123            final boolean forceMerge = isInTextWatcher();
5124
5125            // Build a new operation with all the information from this edit.
5126            String newText = TextUtils.substring(source, start, end);
5127            String oldText = TextUtils.substring(dest, dstart, dend);
5128            EditOperation edit = new EditOperation(mEditor, oldText, dstart, newText);
5129            recordEdit(edit, forceMerge);
5130        }
5131
5132        /**
5133         * Fetches the last undo operation and checks to see if a new edit should be merged into it.
5134         * If forceMerge is true then the new edit is always merged.
5135         */
5136        private void recordEdit(EditOperation edit, boolean forceMerge) {
5137            // Fetch the last edit operation and attempt to merge in the new edit.
5138            final UndoManager um = mEditor.mUndoManager;
5139            um.beginUpdate("Edit text");
5140            EditOperation lastEdit = um.getLastOperation(
5141                  EditOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE);
5142            if (lastEdit == null) {
5143                // Add this as the first edit.
5144                if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit);
5145                um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
5146            } else if (forceMerge) {
5147                // Forced merges take priority because they could be the result of a non-user-edit
5148                // change and this case should not create a new undo operation.
5149                if (DEBUG_UNDO) Log.d(TAG, "filter: force merge " + edit);
5150                lastEdit.forceMergeWith(edit);
5151            } else if (!mIsUserEdit) {
5152                // An application directly modified the Editable outside of a text edit. Treat this
5153                // as a new change and don't attempt to merge.
5154                if (DEBUG_UNDO) Log.d(TAG, "non-user edit, new op " + edit);
5155                um.commitState(mEditor.mUndoOwner);
5156                um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
5157            } else if (lastEdit.mergeWith(edit)) {
5158                // Merge succeeded, nothing else to do.
5159                if (DEBUG_UNDO) Log.d(TAG, "filter: merge succeeded, created " + lastEdit);
5160            } else {
5161                // Could not merge with the last edit, so commit the last edit and add this edit.
5162                if (DEBUG_UNDO) Log.d(TAG, "filter: merge failed, adding " + edit);
5163                um.commitState(mEditor.mUndoOwner);
5164                um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
5165            }
5166            um.endUpdate();
5167        }
5168
5169        private boolean canUndoEdit(CharSequence source, int start, int end,
5170                Spanned dest, int dstart, int dend) {
5171            if (!mEditor.mAllowUndo) {
5172                if (DEBUG_UNDO) Log.d(TAG, "filter: undo is disabled");
5173                return false;
5174            }
5175
5176            if (mEditor.mUndoManager.isInUndo()) {
5177                if (DEBUG_UNDO) Log.d(TAG, "filter: skipping, currently performing undo/redo");
5178                return false;
5179            }
5180
5181            // Text filters run before input operations are applied. However, some input operations
5182            // are invalid and will throw exceptions when applied. This is common in tests. Don't
5183            // attempt to undo invalid operations.
5184            if (!isValidRange(source, start, end) || !isValidRange(dest, dstart, dend)) {
5185                if (DEBUG_UNDO) Log.d(TAG, "filter: invalid op");
5186                return false;
5187            }
5188
5189            // Earlier filters can rewrite input to be a no-op, for example due to a length limit
5190            // on an input field. Skip no-op changes.
5191            if (start == end && dstart == dend) {
5192                if (DEBUG_UNDO) Log.d(TAG, "filter: skipping no-op");
5193                return false;
5194            }
5195
5196            return true;
5197        }
5198
5199        private boolean isComposition(CharSequence source) {
5200            if (!(source instanceof Spannable)) {
5201                return false;
5202            }
5203            // This is a composition edit if the source has a non-zero-length composing span.
5204            Spannable text = (Spannable) source;
5205            int composeBegin = EditableInputConnection.getComposingSpanStart(text);
5206            int composeEnd = EditableInputConnection.getComposingSpanEnd(text);
5207            return composeBegin < composeEnd;
5208        }
5209
5210        private boolean isInTextWatcher() {
5211            CharSequence text = mEditor.mTextView.getText();
5212            return (text instanceof SpannableStringBuilder)
5213                    && ((SpannableStringBuilder) text).getTextWatcherDepth() > 0;
5214        }
5215    }
5216
5217    /**
5218     * An operation to undo a single "edit" to a text view.
5219     */
5220    public static class EditOperation extends UndoOperation<Editor> {
5221        private static final int TYPE_INSERT = 0;
5222        private static final int TYPE_DELETE = 1;
5223        private static final int TYPE_REPLACE = 2;
5224
5225        private int mType;
5226        private String mOldText;
5227        private int mOldTextStart;
5228        private String mNewText;
5229        private int mNewTextStart;
5230
5231        private int mOldCursorPos;
5232        private int mNewCursorPos;
5233
5234        /**
5235         * Constructs an edit operation from a text input operation on editor that replaces the
5236         * oldText starting at dstart with newText.
5237         */
5238        public EditOperation(Editor editor, String oldText, int dstart, String newText) {
5239            super(editor.mUndoOwner);
5240            mOldText = oldText;
5241            mNewText = newText;
5242
5243            // Determine the type of the edit and store where it occurred. Avoid storing
5244            // irrevelant data (e.g. mNewTextStart for a delete) because that makes the
5245            // merging logic more complex (e.g. merging deletes could lead to mNewTextStart being
5246            // outside the bounds of the final text).
5247            if (mNewText.length() > 0 && mOldText.length() == 0) {
5248                mType = TYPE_INSERT;
5249                mNewTextStart = dstart;
5250            } else if (mNewText.length() == 0 && mOldText.length() > 0) {
5251                mType = TYPE_DELETE;
5252                mOldTextStart = dstart;
5253            } else {
5254                mType = TYPE_REPLACE;
5255                mOldTextStart = mNewTextStart = dstart;
5256            }
5257
5258            // Store cursor data.
5259            mOldCursorPos = editor.mTextView.getSelectionStart();
5260            mNewCursorPos = dstart + mNewText.length();
5261        }
5262
5263        public EditOperation(Parcel src, ClassLoader loader) {
5264            super(src, loader);
5265            mType = src.readInt();
5266            mOldText = src.readString();
5267            mOldTextStart = src.readInt();
5268            mNewText = src.readString();
5269            mNewTextStart = src.readInt();
5270            mOldCursorPos = src.readInt();
5271            mNewCursorPos = src.readInt();
5272        }
5273
5274        @Override
5275        public void writeToParcel(Parcel dest, int flags) {
5276            dest.writeInt(mType);
5277            dest.writeString(mOldText);
5278            dest.writeInt(mOldTextStart);
5279            dest.writeString(mNewText);
5280            dest.writeInt(mNewTextStart);
5281            dest.writeInt(mOldCursorPos);
5282            dest.writeInt(mNewCursorPos);
5283        }
5284
5285        private int getNewTextEnd() {
5286            return mNewTextStart + mNewText.length();
5287        }
5288
5289        private int getOldTextEnd() {
5290            return mOldTextStart + mOldText.length();
5291        }
5292
5293        @Override
5294        public void commit() {
5295        }
5296
5297        @Override
5298        public void undo() {
5299            if (DEBUG_UNDO) Log.d(TAG, "undo");
5300            // Remove the new text and insert the old.
5301            Editor editor = getOwnerData();
5302            Editable text = (Editable) editor.mTextView.getText();
5303            modifyText(text, mNewTextStart, getNewTextEnd(), mOldText, mOldTextStart,
5304                    mOldCursorPos);
5305        }
5306
5307        @Override
5308        public void redo() {
5309            if (DEBUG_UNDO) Log.d(TAG, "redo");
5310            // Remove the old text and insert the new.
5311            Editor editor = getOwnerData();
5312            Editable text = (Editable) editor.mTextView.getText();
5313            modifyText(text, mOldTextStart, getOldTextEnd(), mNewText, mNewTextStart,
5314                    mNewCursorPos);
5315        }
5316
5317        /**
5318         * Attempts to merge this existing operation with a new edit.
5319         * @param edit The new edit operation.
5320         * @return If the merge succeeded, returns true. Otherwise returns false and leaves this
5321         * object unchanged.
5322         */
5323        private boolean mergeWith(EditOperation edit) {
5324            if (DEBUG_UNDO) {
5325                Log.d(TAG, "mergeWith old " + this);
5326                Log.d(TAG, "mergeWith new " + edit);
5327            }
5328            switch (mType) {
5329                case TYPE_INSERT:
5330                    return mergeInsertWith(edit);
5331                case TYPE_DELETE:
5332                    return mergeDeleteWith(edit);
5333                case TYPE_REPLACE:
5334                    return mergeReplaceWith(edit);
5335                default:
5336                    return false;
5337            }
5338        }
5339
5340        private boolean mergeInsertWith(EditOperation edit) {
5341            // Only merge continuous insertions.
5342            if (edit.mType != TYPE_INSERT) {
5343                return false;
5344            }
5345            // Only merge insertions that are contiguous.
5346            if (getNewTextEnd() != edit.mNewTextStart) {
5347                return false;
5348            }
5349            mNewText += edit.mNewText;
5350            mNewCursorPos = edit.mNewCursorPos;
5351            return true;
5352        }
5353
5354        // TODO: Support forward delete.
5355        private boolean mergeDeleteWith(EditOperation edit) {
5356            // Only merge continuous deletes.
5357            if (edit.mType != TYPE_DELETE) {
5358                return false;
5359            }
5360            // Only merge deletions that are contiguous.
5361            if (mOldTextStart != edit.getOldTextEnd()) {
5362                return false;
5363            }
5364            mOldTextStart = edit.mOldTextStart;
5365            mOldText = edit.mOldText + mOldText;
5366            mNewCursorPos = edit.mNewCursorPos;
5367            return true;
5368        }
5369
5370        private boolean mergeReplaceWith(EditOperation edit) {
5371            // Replacements can merge only with adjacent inserts.
5372            if (edit.mType != TYPE_INSERT || getNewTextEnd() != edit.mNewTextStart) {
5373                return false;
5374            }
5375            mOldText += edit.mOldText;
5376            mNewText += edit.mNewText;
5377            mNewCursorPos = edit.mNewCursorPos;
5378            return true;
5379        }
5380
5381        /**
5382         * Forcibly creates a single merged edit operation by simulating the entire text
5383         * contents being replaced.
5384         */
5385        public void forceMergeWith(EditOperation edit) {
5386            if (DEBUG_UNDO) Log.d(TAG, "forceMerge");
5387            Editor editor = getOwnerData();
5388
5389            // Copy the text of the current field.
5390            // NOTE: Using StringBuilder instead of SpannableStringBuilder would be somewhat faster,
5391            // but would require two parallel implementations of modifyText() because Editable and
5392            // StringBuilder do not share an interface for replace/delete/insert.
5393            Editable editable = (Editable) editor.mTextView.getText();
5394            Editable originalText = new SpannableStringBuilder(editable.toString());
5395
5396            // Roll back the last operation.
5397            modifyText(originalText, mNewTextStart, getNewTextEnd(), mOldText, mOldTextStart,
5398                    mOldCursorPos);
5399
5400            // Clone the text again and apply the new operation.
5401            Editable finalText = new SpannableStringBuilder(editable.toString());
5402            modifyText(finalText, edit.mOldTextStart, edit.getOldTextEnd(), edit.mNewText,
5403                    edit.mNewTextStart, edit.mNewCursorPos);
5404
5405            // Convert this operation into a non-mergeable replacement of the entire string.
5406            mType = TYPE_REPLACE;
5407            mNewText = finalText.toString();
5408            mNewTextStart = 0;
5409            mOldText = originalText.toString();
5410            mOldTextStart = 0;
5411            mNewCursorPos = edit.mNewCursorPos;
5412            // mOldCursorPos is unchanged.
5413        }
5414
5415        private static void modifyText(Editable text, int deleteFrom, int deleteTo,
5416                CharSequence newText, int newTextInsertAt, int newCursorPos) {
5417            // Apply the edit if it is still valid.
5418            if (isValidRange(text, deleteFrom, deleteTo) &&
5419                    newTextInsertAt <= text.length() - (deleteTo - deleteFrom)) {
5420                if (deleteFrom != deleteTo) {
5421                    text.delete(deleteFrom, deleteTo);
5422                }
5423                if (newText.length() != 0) {
5424                    text.insert(newTextInsertAt, newText);
5425                }
5426            }
5427            // Restore the cursor position. If there wasn't an old cursor (newCursorPos == -1) then
5428            // don't explicitly set it and rely on SpannableStringBuilder to position it.
5429            // TODO: Select all the text that was undone.
5430            if (0 <= newCursorPos && newCursorPos <= text.length()) {
5431                Selection.setSelection(text, newCursorPos);
5432            }
5433        }
5434
5435        private String getTypeString() {
5436            switch (mType) {
5437                case TYPE_INSERT:
5438                    return "insert";
5439                case TYPE_DELETE:
5440                    return "delete";
5441                case TYPE_REPLACE:
5442                    return "replace";
5443                default:
5444                    return "";
5445            }
5446        }
5447
5448        @Override
5449        public String toString() {
5450            return "[mType=" + getTypeString() + ", " +
5451                    "mOldText=" + mOldText + ", " +
5452                    "mOldTextStart=" + mOldTextStart + ", " +
5453                    "mNewText=" + mNewText + ", " +
5454                    "mNewTextStart=" + mNewTextStart + ", " +
5455                    "mOldCursorPos=" + mOldCursorPos + ", " +
5456                    "mNewCursorPos=" + mNewCursorPos + "]";
5457        }
5458
5459        public static final Parcelable.ClassLoaderCreator<EditOperation> CREATOR
5460                = new Parcelable.ClassLoaderCreator<EditOperation>() {
5461            @Override
5462            public EditOperation createFromParcel(Parcel in) {
5463                return new EditOperation(in, null);
5464            }
5465
5466            @Override
5467            public EditOperation createFromParcel(Parcel in, ClassLoader loader) {
5468                return new EditOperation(in, loader);
5469            }
5470
5471            @Override
5472            public EditOperation[] newArray(int size) {
5473                return new EditOperation[size];
5474            }
5475        };
5476    }
5477
5478    /**
5479     * A helper for enabling and handling "PROCESS_TEXT" menu actions.
5480     * These allow external applications to plug into currently selected text.
5481     */
5482    static final class ProcessTextIntentActionsHandler {
5483
5484        private final Editor mEditor;
5485        private final TextView mTextView;
5486        private final PackageManager mPackageManager;
5487        private final SparseArray<Intent> mAccessibilityIntents = new SparseArray<Intent>();
5488        private final SparseArray<AccessibilityNodeInfo.AccessibilityAction> mAccessibilityActions
5489                = new SparseArray<AccessibilityNodeInfo.AccessibilityAction>();
5490
5491        private ProcessTextIntentActionsHandler(Editor editor) {
5492            mEditor = Preconditions.checkNotNull(editor);
5493            mTextView = Preconditions.checkNotNull(mEditor.mTextView);
5494            mPackageManager = Preconditions.checkNotNull(
5495                    mTextView.getContext().getPackageManager());
5496        }
5497
5498        /**
5499         * Adds "PROCESS_TEXT" menu items to the specified menu.
5500         */
5501        public void onInitializeMenu(Menu menu) {
5502            int i = 0;
5503            for (ResolveInfo resolveInfo : getSupportedActivities()) {
5504                menu.add(Menu.NONE, Menu.NONE,
5505                        Editor.MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i++,
5506                        getLabel(resolveInfo))
5507                        .setIntent(createProcessTextIntentForResolveInfo(resolveInfo))
5508                        .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
5509            }
5510        }
5511
5512        /**
5513         * Performs a "PROCESS_TEXT" action if there is one associated with the specified
5514         * menu item.
5515         *
5516         * @return True if the action was performed, false otherwise.
5517         */
5518        public boolean performMenuItemAction(MenuItem item) {
5519            return fireIntent(item.getIntent());
5520        }
5521
5522        /**
5523         * Initializes and caches "PROCESS_TEXT" accessibility actions.
5524         */
5525        public void initializeAccessibilityActions() {
5526            mAccessibilityIntents.clear();
5527            mAccessibilityActions.clear();
5528            int i = 0;
5529            for (ResolveInfo resolveInfo : getSupportedActivities()) {
5530                int actionId = TextView.ACCESSIBILITY_ACTION_PROCESS_TEXT_START_ID + i++;
5531                mAccessibilityActions.put(
5532                        actionId,
5533                        new AccessibilityNodeInfo.AccessibilityAction(
5534                                actionId, getLabel(resolveInfo)));
5535                mAccessibilityIntents.put(
5536                        actionId, createProcessTextIntentForResolveInfo(resolveInfo));
5537            }
5538        }
5539
5540        /**
5541         * Adds "PROCESS_TEXT" accessibility actions to the specified accessibility node info.
5542         * NOTE: This needs a prior call to {@link #initializeAccessibilityActions()} to make the
5543         * latest accessibility actions available for this call.
5544         */
5545        public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) {
5546            for (int i = 0; i < mAccessibilityActions.size(); i++) {
5547                nodeInfo.addAction(mAccessibilityActions.valueAt(i));
5548            }
5549        }
5550
5551        /**
5552         * Performs a "PROCESS_TEXT" action if there is one associated with the specified
5553         * accessibility action id.
5554         *
5555         * @return True if the action was performed, false otherwise.
5556         */
5557        public boolean performAccessibilityAction(int actionId) {
5558            return fireIntent(mAccessibilityIntents.get(actionId));
5559        }
5560
5561        private boolean fireIntent(Intent intent) {
5562            if (intent != null && Intent.ACTION_PROCESS_TEXT.equals(intent.getAction())) {
5563                intent.putExtra(Intent.EXTRA_PROCESS_TEXT, mTextView.getSelectedText());
5564                mEditor.mPreserveDetachedSelection = true;
5565                mTextView.startActivityForResult(intent, TextView.PROCESS_TEXT_REQUEST_CODE);
5566                return true;
5567            }
5568            return false;
5569        }
5570
5571        private List<ResolveInfo> getSupportedActivities() {
5572            PackageManager packageManager = mTextView.getContext().getPackageManager();
5573            return packageManager.queryIntentActivities(createProcessTextIntent(), 0);
5574        }
5575
5576        private Intent createProcessTextIntentForResolveInfo(ResolveInfo info) {
5577            return createProcessTextIntent()
5578                    .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !mTextView.isTextEditable())
5579                    .setClassName(info.activityInfo.packageName, info.activityInfo.name);
5580        }
5581
5582        private Intent createProcessTextIntent() {
5583            return new Intent()
5584                    .setAction(Intent.ACTION_PROCESS_TEXT)
5585                    .setType("text/plain");
5586        }
5587
5588        private CharSequence getLabel(ResolveInfo resolveInfo) {
5589            return resolveInfo.loadLabel(mPackageManager);
5590        }
5591    }
5592}
5593