TextView.java revision aa67eef1b64fd52366bcba339150ce571bc1409b
1/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.widget;
18
19import android.R;
20import android.content.ClipData;
21import android.content.ClipData.Item;
22import android.content.ClipboardManager;
23import android.content.Context;
24import android.content.pm.PackageManager;
25import android.content.res.ColorStateList;
26import android.content.res.Resources;
27import android.content.res.TypedArray;
28import android.content.res.XmlResourceParser;
29import android.graphics.Canvas;
30import android.graphics.Color;
31import android.graphics.Paint;
32import android.graphics.Path;
33import android.graphics.Rect;
34import android.graphics.RectF;
35import android.graphics.Typeface;
36import android.graphics.drawable.Drawable;
37import android.inputmethodservice.ExtractEditText;
38import android.net.Uri;
39import android.os.Bundle;
40import android.os.Handler;
41import android.os.Message;
42import android.os.Parcel;
43import android.os.Parcelable;
44import android.os.SystemClock;
45import android.text.BoringLayout;
46import android.text.DynamicLayout;
47import android.text.Editable;
48import android.text.GetChars;
49import android.text.GraphicsOperations;
50import android.text.InputFilter;
51import android.text.InputType;
52import android.text.Layout;
53import android.text.ParcelableSpan;
54import android.text.Selection;
55import android.text.SpanWatcher;
56import android.text.Spannable;
57import android.text.SpannableString;
58import android.text.SpannableStringBuilder;
59import android.text.Spanned;
60import android.text.SpannedString;
61import android.text.StaticLayout;
62import android.text.TextPaint;
63import android.text.TextUtils;
64import android.text.TextWatcher;
65import android.text.method.ArrowKeyMovementMethod;
66import android.text.method.DateKeyListener;
67import android.text.method.DateTimeKeyListener;
68import android.text.method.DialerKeyListener;
69import android.text.method.DigitsKeyListener;
70import android.text.method.KeyListener;
71import android.text.method.LinkMovementMethod;
72import android.text.method.MetaKeyKeyListener;
73import android.text.method.MovementMethod;
74import android.text.method.PasswordTransformationMethod;
75import android.text.method.SingleLineTransformationMethod;
76import android.text.method.TextKeyListener;
77import android.text.method.TimeKeyListener;
78import android.text.method.TransformationMethod;
79import android.text.method.WordIterator;
80import android.text.style.ClickableSpan;
81import android.text.style.ParagraphStyle;
82import android.text.style.SuggestionSpan;
83import android.text.style.TextAppearanceSpan;
84import android.text.style.URLSpan;
85import android.text.style.UnderlineSpan;
86import android.text.style.UpdateAppearance;
87import android.text.util.Linkify;
88import android.util.AttributeSet;
89import android.util.DisplayMetrics;
90import android.util.FloatMath;
91import android.util.Log;
92import android.util.TypedValue;
93import android.view.ActionMode;
94import android.view.ActionMode.Callback;
95import android.view.ContextMenu;
96import android.view.DragEvent;
97import android.view.Gravity;
98import android.view.HapticFeedbackConstants;
99import android.view.KeyCharacterMap;
100import android.view.KeyEvent;
101import android.view.LayoutInflater;
102import android.view.Menu;
103import android.view.MenuItem;
104import android.view.MotionEvent;
105import android.view.View;
106import android.view.ViewAncestor;
107import android.view.ViewConfiguration;
108import android.view.ViewDebug;
109import android.view.ViewGroup;
110import android.view.ViewGroup.LayoutParams;
111import android.view.ViewParent;
112import android.view.ViewTreeObserver;
113import android.view.WindowManager;
114import android.view.accessibility.AccessibilityEvent;
115import android.view.accessibility.AccessibilityManager;
116import android.view.animation.AnimationUtils;
117import android.view.inputmethod.BaseInputConnection;
118import android.view.inputmethod.CompletionInfo;
119import android.view.inputmethod.CorrectionInfo;
120import android.view.inputmethod.EditorInfo;
121import android.view.inputmethod.ExtractedText;
122import android.view.inputmethod.ExtractedTextRequest;
123import android.view.inputmethod.InputConnection;
124import android.view.inputmethod.InputMethodManager;
125import android.widget.RemoteViews.RemoteView;
126
127import com.android.internal.util.FastMath;
128import com.android.internal.widget.EditableInputConnection;
129
130import org.xmlpull.v1.XmlPullParserException;
131
132import java.io.IOException;
133import java.lang.ref.WeakReference;
134import java.text.BreakIterator;
135import java.util.ArrayList;
136
137/**
138 * Displays text to the user and optionally allows them to edit it.  A TextView
139 * is a complete text editor, however the basic class is configured to not
140 * allow editing; see {@link EditText} for a subclass that configures the text
141 * view for editing.
142 *
143 * <p>
144 * <b>XML attributes</b>
145 * <p>
146 * See {@link android.R.styleable#TextView TextView Attributes},
147 * {@link android.R.styleable#View View Attributes}
148 *
149 * @attr ref android.R.styleable#TextView_text
150 * @attr ref android.R.styleable#TextView_bufferType
151 * @attr ref android.R.styleable#TextView_hint
152 * @attr ref android.R.styleable#TextView_textColor
153 * @attr ref android.R.styleable#TextView_textColorHighlight
154 * @attr ref android.R.styleable#TextView_textColorHint
155 * @attr ref android.R.styleable#TextView_textAppearance
156 * @attr ref android.R.styleable#TextView_textColorLink
157 * @attr ref android.R.styleable#TextView_textSize
158 * @attr ref android.R.styleable#TextView_textScaleX
159 * @attr ref android.R.styleable#TextView_typeface
160 * @attr ref android.R.styleable#TextView_textStyle
161 * @attr ref android.R.styleable#TextView_cursorVisible
162 * @attr ref android.R.styleable#TextView_maxLines
163 * @attr ref android.R.styleable#TextView_maxHeight
164 * @attr ref android.R.styleable#TextView_lines
165 * @attr ref android.R.styleable#TextView_height
166 * @attr ref android.R.styleable#TextView_minLines
167 * @attr ref android.R.styleable#TextView_minHeight
168 * @attr ref android.R.styleable#TextView_maxEms
169 * @attr ref android.R.styleable#TextView_maxWidth
170 * @attr ref android.R.styleable#TextView_ems
171 * @attr ref android.R.styleable#TextView_width
172 * @attr ref android.R.styleable#TextView_minEms
173 * @attr ref android.R.styleable#TextView_minWidth
174 * @attr ref android.R.styleable#TextView_gravity
175 * @attr ref android.R.styleable#TextView_scrollHorizontally
176 * @attr ref android.R.styleable#TextView_password
177 * @attr ref android.R.styleable#TextView_singleLine
178 * @attr ref android.R.styleable#TextView_selectAllOnFocus
179 * @attr ref android.R.styleable#TextView_includeFontPadding
180 * @attr ref android.R.styleable#TextView_maxLength
181 * @attr ref android.R.styleable#TextView_shadowColor
182 * @attr ref android.R.styleable#TextView_shadowDx
183 * @attr ref android.R.styleable#TextView_shadowDy
184 * @attr ref android.R.styleable#TextView_shadowRadius
185 * @attr ref android.R.styleable#TextView_autoLink
186 * @attr ref android.R.styleable#TextView_linksClickable
187 * @attr ref android.R.styleable#TextView_numeric
188 * @attr ref android.R.styleable#TextView_digits
189 * @attr ref android.R.styleable#TextView_phoneNumber
190 * @attr ref android.R.styleable#TextView_inputMethod
191 * @attr ref android.R.styleable#TextView_capitalize
192 * @attr ref android.R.styleable#TextView_autoText
193 * @attr ref android.R.styleable#TextView_editable
194 * @attr ref android.R.styleable#TextView_freezesText
195 * @attr ref android.R.styleable#TextView_ellipsize
196 * @attr ref android.R.styleable#TextView_drawableTop
197 * @attr ref android.R.styleable#TextView_drawableBottom
198 * @attr ref android.R.styleable#TextView_drawableRight
199 * @attr ref android.R.styleable#TextView_drawableLeft
200 * @attr ref android.R.styleable#TextView_drawablePadding
201 * @attr ref android.R.styleable#TextView_lineSpacingExtra
202 * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
203 * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
204 * @attr ref android.R.styleable#TextView_inputType
205 * @attr ref android.R.styleable#TextView_imeOptions
206 * @attr ref android.R.styleable#TextView_privateImeOptions
207 * @attr ref android.R.styleable#TextView_imeActionLabel
208 * @attr ref android.R.styleable#TextView_imeActionId
209 * @attr ref android.R.styleable#TextView_editorExtras
210 */
211@RemoteView
212public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
213    static final String LOG_TAG = "TextView";
214    static final boolean DEBUG_EXTRACT = false;
215
216    private static final int PRIORITY = 100;
217    private int mCurrentAlpha = 255;
218
219    final int[] mTempCoords = new int[2];
220    Rect mTempRect;
221
222    private ColorStateList mTextColor;
223    private int mCurTextColor;
224    private ColorStateList mHintTextColor;
225    private ColorStateList mLinkTextColor;
226    private int mCurHintTextColor;
227    private boolean mFreezesText;
228    private boolean mFrozenWithFocus;
229    private boolean mTemporaryDetach;
230    private boolean mDispatchTemporaryDetach;
231
232    private boolean mDiscardNextActionUp = false;
233    private boolean mIgnoreActionUpEvent = false;
234
235    private Editable.Factory mEditableFactory = Editable.Factory.getInstance();
236    private Spannable.Factory mSpannableFactory = Spannable.Factory.getInstance();
237
238    private float mShadowRadius, mShadowDx, mShadowDy;
239
240    private static final int PREDRAW_NOT_REGISTERED = 0;
241    private static final int PREDRAW_PENDING = 1;
242    private static final int PREDRAW_DONE = 2;
243    private int mPreDrawState = PREDRAW_NOT_REGISTERED;
244
245    private TextUtils.TruncateAt mEllipsize = null;
246
247    // Enum for the "typeface" XML parameter.
248    // TODO: How can we get this from the XML instead of hardcoding it here?
249    private static final int SANS = 1;
250    private static final int SERIF = 2;
251    private static final int MONOSPACE = 3;
252
253    // Bitfield for the "numeric" XML parameter.
254    // TODO: How can we get this from the XML instead of hardcoding it here?
255    private static final int SIGNED = 2;
256    private static final int DECIMAL = 4;
257
258    class Drawables {
259        final Rect mCompoundRect = new Rect();
260        Drawable mDrawableTop, mDrawableBottom, mDrawableLeft, mDrawableRight;
261        int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight;
262        int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight;
263        int mDrawablePadding;
264    }
265    private Drawables mDrawables;
266
267    private CharSequence mError;
268    private boolean mErrorWasChanged;
269    private ErrorPopup mPopup;
270    /**
271     * This flag is set if the TextView tries to display an error before it
272     * is attached to the window (so its position is still unknown).
273     * It causes the error to be shown later, when onAttachedToWindow()
274     * is called.
275     */
276    private boolean mShowErrorAfterAttach;
277
278    private CharWrapper mCharWrapper = null;
279
280    private boolean mSelectionMoved = false;
281    private boolean mTouchFocusSelected = false;
282
283    private Marquee mMarquee;
284    private boolean mRestartMarquee;
285
286    private int mMarqueeRepeatLimit = 3;
287
288    class InputContentType {
289        int imeOptions = EditorInfo.IME_NULL;
290        String privateImeOptions;
291        CharSequence imeActionLabel;
292        int imeActionId;
293        Bundle extras;
294        OnEditorActionListener onEditorActionListener;
295        boolean enterDown;
296    }
297    InputContentType mInputContentType;
298
299    class InputMethodState {
300        Rect mCursorRectInWindow = new Rect();
301        RectF mTmpRectF = new RectF();
302        float[] mTmpOffset = new float[2];
303        ExtractedTextRequest mExtracting;
304        final ExtractedText mTmpExtracted = new ExtractedText();
305        int mBatchEditNesting;
306        boolean mCursorChanged;
307        boolean mSelectionModeChanged;
308        boolean mContentChanged;
309        int mChangedStart, mChangedEnd, mChangedDelta;
310    }
311    InputMethodState mInputMethodState;
312
313    private int mTextSelectHandleLeftRes;
314    private int mTextSelectHandleRightRes;
315    private int mTextSelectHandleRes;
316    private int mTextEditPasteWindowLayout, mTextEditSidePasteWindowLayout;
317    private int mTextEditNoPasteWindowLayout, mTextEditSideNoPasteWindowLayout;
318
319    private int mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout;
320    private int mTextEditSuggestionItemLayout;
321    private SuggestionsPopupWindow mSuggestionsPopupWindow;
322    private SuggestionRangeSpan mSuggestionRangeSpan;
323    private boolean mSuggestionsEnabled = true;
324
325    private int mCursorDrawableRes;
326    private final Drawable[] mCursorDrawable = new Drawable[2];
327    private int mCursorCount; // Actual current number of used mCursorDrawable: 0, 1 or 2
328
329    private Drawable mSelectHandleLeft;
330    private Drawable mSelectHandleRight;
331    private Drawable mSelectHandleCenter;
332
333    private float mLastDownPositionX, mLastDownPositionY;
334    private Callback mCustomSelectionActionModeCallback;
335
336    private final int mSquaredTouchSlopDistance;
337    // Set when this TextView gained focus with some text selected. Will start selection mode.
338    private boolean mCreatedWithASelection = false;
339
340    private WordIterator mWordIterator;
341
342    /*
343     * Kick-start the font cache for the zygote process (to pay the cost of
344     * initializing freetype for our default font only once).
345     */
346    static {
347        Paint p = new Paint();
348        p.setAntiAlias(true);
349        // We don't care about the result, just the side-effect of measuring.
350        p.measureText("H");
351    }
352
353    /**
354     * Interface definition for a callback to be invoked when an action is
355     * performed on the editor.
356     */
357    public interface OnEditorActionListener {
358        /**
359         * Called when an action is being performed.
360         *
361         * @param v The view that was clicked.
362         * @param actionId Identifier of the action.  This will be either the
363         * identifier you supplied, or {@link EditorInfo#IME_NULL
364         * EditorInfo.IME_NULL} if being called due to the enter key
365         * being pressed.
366         * @param event If triggered by an enter key, this is the event;
367         * otherwise, this is null.
368         * @return Return true if you have consumed the action, else false.
369         */
370        boolean onEditorAction(TextView v, int actionId, KeyEvent event);
371    }
372
373    public TextView(Context context) {
374        this(context, null);
375    }
376
377    public TextView(Context context,
378                    AttributeSet attrs) {
379        this(context, attrs, com.android.internal.R.attr.textViewStyle);
380    }
381
382    @SuppressWarnings("deprecation")
383    public TextView(Context context,
384                    AttributeSet attrs,
385                    int defStyle) {
386        super(context, attrs, defStyle);
387        mText = "";
388
389        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
390        mTextPaint.density = getResources().getDisplayMetrics().density;
391        mTextPaint.setCompatibilityScaling(
392                getResources().getCompatibilityInfo().applicationScale);
393
394        // If we get the paint from the skin, we should set it to left, since
395        // the layout always wants it to be left.
396        // mTextPaint.setTextAlign(Paint.Align.LEFT);
397
398        mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
399        mHighlightPaint.setCompatibilityScaling(
400                getResources().getCompatibilityInfo().applicationScale);
401
402        mMovement = getDefaultMovementMethod();
403        mTransformation = null;
404
405        TypedArray a =
406            context.obtainStyledAttributes(
407                attrs, com.android.internal.R.styleable.TextView, defStyle, 0);
408
409        int textColorHighlight = 0;
410        ColorStateList textColor = null;
411        ColorStateList textColorHint = null;
412        ColorStateList textColorLink = null;
413        int textSize = 15;
414        int typefaceIndex = -1;
415        int styleIndex = -1;
416
417        /*
418         * Look the appearance up without checking first if it exists because
419         * almost every TextView has one and it greatly simplifies the logic
420         * to be able to parse the appearance first and then let specific tags
421         * for this View override it.
422         */
423        TypedArray appearance = null;
424        int ap = a.getResourceId(com.android.internal.R.styleable.TextView_textAppearance, -1);
425        if (ap != -1) {
426            appearance = context.obtainStyledAttributes(ap,
427                                com.android.internal.R.styleable.
428                                TextAppearance);
429        }
430        if (appearance != null) {
431            int n = appearance.getIndexCount();
432            for (int i = 0; i < n; i++) {
433                int attr = appearance.getIndex(i);
434
435                switch (attr) {
436                case com.android.internal.R.styleable.TextAppearance_textColorHighlight:
437                    textColorHighlight = appearance.getColor(attr, textColorHighlight);
438                    break;
439
440                case com.android.internal.R.styleable.TextAppearance_textColor:
441                    textColor = appearance.getColorStateList(attr);
442                    break;
443
444                case com.android.internal.R.styleable.TextAppearance_textColorHint:
445                    textColorHint = appearance.getColorStateList(attr);
446                    break;
447
448                case com.android.internal.R.styleable.TextAppearance_textColorLink:
449                    textColorLink = appearance.getColorStateList(attr);
450                    break;
451
452                case com.android.internal.R.styleable.TextAppearance_textSize:
453                    textSize = appearance.getDimensionPixelSize(attr, textSize);
454                    break;
455
456                case com.android.internal.R.styleable.TextAppearance_typeface:
457                    typefaceIndex = appearance.getInt(attr, -1);
458                    break;
459
460                case com.android.internal.R.styleable.TextAppearance_textStyle:
461                    styleIndex = appearance.getInt(attr, -1);
462                    break;
463                }
464            }
465
466            appearance.recycle();
467        }
468
469        boolean editable = getDefaultEditable();
470        CharSequence inputMethod = null;
471        int numeric = 0;
472        CharSequence digits = null;
473        boolean phone = false;
474        boolean autotext = false;
475        int autocap = -1;
476        int buffertype = 0;
477        boolean selectallonfocus = false;
478        Drawable drawableLeft = null, drawableTop = null, drawableRight = null,
479            drawableBottom = null;
480        int drawablePadding = 0;
481        int ellipsize = -1;
482        boolean singleLine = false;
483        int maxlength = -1;
484        CharSequence text = "";
485        CharSequence hint = null;
486        int shadowcolor = 0;
487        float dx = 0, dy = 0, r = 0;
488        boolean password = false;
489        int inputType = EditorInfo.TYPE_NULL;
490
491        int n = a.getIndexCount();
492        for (int i = 0; i < n; i++) {
493            int attr = a.getIndex(i);
494
495            switch (attr) {
496            case com.android.internal.R.styleable.TextView_editable:
497                editable = a.getBoolean(attr, editable);
498                break;
499
500            case com.android.internal.R.styleable.TextView_inputMethod:
501                inputMethod = a.getText(attr);
502                break;
503
504            case com.android.internal.R.styleable.TextView_numeric:
505                numeric = a.getInt(attr, numeric);
506                break;
507
508            case com.android.internal.R.styleable.TextView_digits:
509                digits = a.getText(attr);
510                break;
511
512            case com.android.internal.R.styleable.TextView_phoneNumber:
513                phone = a.getBoolean(attr, phone);
514                break;
515
516            case com.android.internal.R.styleable.TextView_autoText:
517                autotext = a.getBoolean(attr, autotext);
518                break;
519
520            case com.android.internal.R.styleable.TextView_capitalize:
521                autocap = a.getInt(attr, autocap);
522                break;
523
524            case com.android.internal.R.styleable.TextView_bufferType:
525                buffertype = a.getInt(attr, buffertype);
526                break;
527
528            case com.android.internal.R.styleable.TextView_selectAllOnFocus:
529                selectallonfocus = a.getBoolean(attr, selectallonfocus);
530                break;
531
532            case com.android.internal.R.styleable.TextView_autoLink:
533                mAutoLinkMask = a.getInt(attr, 0);
534                break;
535
536            case com.android.internal.R.styleable.TextView_linksClickable:
537                mLinksClickable = a.getBoolean(attr, true);
538                break;
539
540            case com.android.internal.R.styleable.TextView_drawableLeft:
541                drawableLeft = a.getDrawable(attr);
542                break;
543
544            case com.android.internal.R.styleable.TextView_drawableTop:
545                drawableTop = a.getDrawable(attr);
546                break;
547
548            case com.android.internal.R.styleable.TextView_drawableRight:
549                drawableRight = a.getDrawable(attr);
550                break;
551
552            case com.android.internal.R.styleable.TextView_drawableBottom:
553                drawableBottom = a.getDrawable(attr);
554                break;
555
556            case com.android.internal.R.styleable.TextView_drawablePadding:
557                drawablePadding = a.getDimensionPixelSize(attr, drawablePadding);
558                break;
559
560            case com.android.internal.R.styleable.TextView_maxLines:
561                setMaxLines(a.getInt(attr, -1));
562                break;
563
564            case com.android.internal.R.styleable.TextView_maxHeight:
565                setMaxHeight(a.getDimensionPixelSize(attr, -1));
566                break;
567
568            case com.android.internal.R.styleable.TextView_lines:
569                setLines(a.getInt(attr, -1));
570                break;
571
572            case com.android.internal.R.styleable.TextView_height:
573                setHeight(a.getDimensionPixelSize(attr, -1));
574                break;
575
576            case com.android.internal.R.styleable.TextView_minLines:
577                setMinLines(a.getInt(attr, -1));
578                break;
579
580            case com.android.internal.R.styleable.TextView_minHeight:
581                setMinHeight(a.getDimensionPixelSize(attr, -1));
582                break;
583
584            case com.android.internal.R.styleable.TextView_maxEms:
585                setMaxEms(a.getInt(attr, -1));
586                break;
587
588            case com.android.internal.R.styleable.TextView_maxWidth:
589                setMaxWidth(a.getDimensionPixelSize(attr, -1));
590                break;
591
592            case com.android.internal.R.styleable.TextView_ems:
593                setEms(a.getInt(attr, -1));
594                break;
595
596            case com.android.internal.R.styleable.TextView_width:
597                setWidth(a.getDimensionPixelSize(attr, -1));
598                break;
599
600            case com.android.internal.R.styleable.TextView_minEms:
601                setMinEms(a.getInt(attr, -1));
602                break;
603
604            case com.android.internal.R.styleable.TextView_minWidth:
605                setMinWidth(a.getDimensionPixelSize(attr, -1));
606                break;
607
608            case com.android.internal.R.styleable.TextView_gravity:
609                setGravity(a.getInt(attr, -1));
610                break;
611
612            case com.android.internal.R.styleable.TextView_hint:
613                hint = a.getText(attr);
614                break;
615
616            case com.android.internal.R.styleable.TextView_text:
617                text = a.getText(attr);
618                break;
619
620            case com.android.internal.R.styleable.TextView_scrollHorizontally:
621                if (a.getBoolean(attr, false)) {
622                    setHorizontallyScrolling(true);
623                }
624                break;
625
626            case com.android.internal.R.styleable.TextView_singleLine:
627                singleLine = a.getBoolean(attr, singleLine);
628                break;
629
630            case com.android.internal.R.styleable.TextView_ellipsize:
631                ellipsize = a.getInt(attr, ellipsize);
632                break;
633
634            case com.android.internal.R.styleable.TextView_marqueeRepeatLimit:
635                setMarqueeRepeatLimit(a.getInt(attr, mMarqueeRepeatLimit));
636                break;
637
638            case com.android.internal.R.styleable.TextView_includeFontPadding:
639                if (!a.getBoolean(attr, true)) {
640                    setIncludeFontPadding(false);
641                }
642                break;
643
644            case com.android.internal.R.styleable.TextView_cursorVisible:
645                if (!a.getBoolean(attr, true)) {
646                    setCursorVisible(false);
647                }
648                break;
649
650            case com.android.internal.R.styleable.TextView_maxLength:
651                maxlength = a.getInt(attr, -1);
652                break;
653
654            case com.android.internal.R.styleable.TextView_textScaleX:
655                setTextScaleX(a.getFloat(attr, 1.0f));
656                break;
657
658            case com.android.internal.R.styleable.TextView_freezesText:
659                mFreezesText = a.getBoolean(attr, false);
660                break;
661
662            case com.android.internal.R.styleable.TextView_shadowColor:
663                shadowcolor = a.getInt(attr, 0);
664                break;
665
666            case com.android.internal.R.styleable.TextView_shadowDx:
667                dx = a.getFloat(attr, 0);
668                break;
669
670            case com.android.internal.R.styleable.TextView_shadowDy:
671                dy = a.getFloat(attr, 0);
672                break;
673
674            case com.android.internal.R.styleable.TextView_shadowRadius:
675                r = a.getFloat(attr, 0);
676                break;
677
678            case com.android.internal.R.styleable.TextView_enabled:
679                setEnabled(a.getBoolean(attr, isEnabled()));
680                break;
681
682            case com.android.internal.R.styleable.TextView_textColorHighlight:
683                textColorHighlight = a.getColor(attr, textColorHighlight);
684                break;
685
686            case com.android.internal.R.styleable.TextView_textColor:
687                textColor = a.getColorStateList(attr);
688                break;
689
690            case com.android.internal.R.styleable.TextView_textColorHint:
691                textColorHint = a.getColorStateList(attr);
692                break;
693
694            case com.android.internal.R.styleable.TextView_textColorLink:
695                textColorLink = a.getColorStateList(attr);
696                break;
697
698            case com.android.internal.R.styleable.TextView_textSize:
699                textSize = a.getDimensionPixelSize(attr, textSize);
700                break;
701
702            case com.android.internal.R.styleable.TextView_typeface:
703                typefaceIndex = a.getInt(attr, typefaceIndex);
704                break;
705
706            case com.android.internal.R.styleable.TextView_textStyle:
707                styleIndex = a.getInt(attr, styleIndex);
708                break;
709
710            case com.android.internal.R.styleable.TextView_password:
711                password = a.getBoolean(attr, password);
712                break;
713
714            case com.android.internal.R.styleable.TextView_lineSpacingExtra:
715                mSpacingAdd = a.getDimensionPixelSize(attr, (int) mSpacingAdd);
716                break;
717
718            case com.android.internal.R.styleable.TextView_lineSpacingMultiplier:
719                mSpacingMult = a.getFloat(attr, mSpacingMult);
720                break;
721
722            case com.android.internal.R.styleable.TextView_inputType:
723                inputType = a.getInt(attr, mInputType);
724                break;
725
726            case com.android.internal.R.styleable.TextView_imeOptions:
727                if (mInputContentType == null) {
728                    mInputContentType = new InputContentType();
729                }
730                mInputContentType.imeOptions = a.getInt(attr,
731                        mInputContentType.imeOptions);
732                break;
733
734            case com.android.internal.R.styleable.TextView_imeActionLabel:
735                if (mInputContentType == null) {
736                    mInputContentType = new InputContentType();
737                }
738                mInputContentType.imeActionLabel = a.getText(attr);
739                break;
740
741            case com.android.internal.R.styleable.TextView_imeActionId:
742                if (mInputContentType == null) {
743                    mInputContentType = new InputContentType();
744                }
745                mInputContentType.imeActionId = a.getInt(attr,
746                        mInputContentType.imeActionId);
747                break;
748
749            case com.android.internal.R.styleable.TextView_privateImeOptions:
750                setPrivateImeOptions(a.getString(attr));
751                break;
752
753            case com.android.internal.R.styleable.TextView_editorExtras:
754                try {
755                    setInputExtras(a.getResourceId(attr, 0));
756                } catch (XmlPullParserException e) {
757                    Log.w(LOG_TAG, "Failure reading input extras", e);
758                } catch (IOException e) {
759                    Log.w(LOG_TAG, "Failure reading input extras", e);
760                }
761                break;
762
763            case com.android.internal.R.styleable.TextView_textCursorDrawable:
764                mCursorDrawableRes = a.getResourceId(attr, 0);
765                break;
766
767            case com.android.internal.R.styleable.TextView_textSelectHandleLeft:
768                mTextSelectHandleLeftRes = a.getResourceId(attr, 0);
769                break;
770
771            case com.android.internal.R.styleable.TextView_textSelectHandleRight:
772                mTextSelectHandleRightRes = a.getResourceId(attr, 0);
773                break;
774
775            case com.android.internal.R.styleable.TextView_textSelectHandle:
776                mTextSelectHandleRes = a.getResourceId(attr, 0);
777                break;
778
779            case com.android.internal.R.styleable.TextView_textEditPasteWindowLayout:
780                mTextEditPasteWindowLayout = a.getResourceId(attr, 0);
781                break;
782
783            case com.android.internal.R.styleable.TextView_textEditNoPasteWindowLayout:
784                mTextEditNoPasteWindowLayout = a.getResourceId(attr, 0);
785                break;
786
787            case com.android.internal.R.styleable.TextView_textEditSidePasteWindowLayout:
788                mTextEditSidePasteWindowLayout = a.getResourceId(attr, 0);
789                break;
790
791            case com.android.internal.R.styleable.TextView_textEditSideNoPasteWindowLayout:
792                mTextEditSideNoPasteWindowLayout = a.getResourceId(attr, 0);
793                break;
794
795            case com.android.internal.R.styleable.TextView_textEditSuggestionsBottomWindowLayout:
796                mTextEditSuggestionsBottomWindowLayout = a.getResourceId(attr, 0);
797                break;
798
799            case com.android.internal.R.styleable.TextView_textEditSuggestionsTopWindowLayout:
800                mTextEditSuggestionsTopWindowLayout = a.getResourceId(attr, 0);
801                break;
802
803            case com.android.internal.R.styleable.TextView_textEditSuggestionItemLayout:
804                mTextEditSuggestionItemLayout = a.getResourceId(attr, 0);
805                break;
806
807            case com.android.internal.R.styleable.TextView_textIsSelectable:
808                mTextIsSelectable = a.getBoolean(attr, false);
809                break;
810
811            case com.android.internal.R.styleable.TextView_suggestionsEnabled:
812                mSuggestionsEnabled = a.getBoolean(attr, true);
813                break;
814            }
815        }
816        a.recycle();
817
818        BufferType bufferType = BufferType.EDITABLE;
819
820        final int variation =
821                inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
822        final boolean passwordInputType = variation
823                == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD);
824        final boolean webPasswordInputType = variation
825                == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD);
826        final boolean numberPasswordInputType = variation
827                == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
828
829        if (inputMethod != null) {
830            Class<?> c;
831
832            try {
833                c = Class.forName(inputMethod.toString());
834            } catch (ClassNotFoundException ex) {
835                throw new RuntimeException(ex);
836            }
837
838            try {
839                mInput = (KeyListener) c.newInstance();
840            } catch (InstantiationException ex) {
841                throw new RuntimeException(ex);
842            } catch (IllegalAccessException ex) {
843                throw new RuntimeException(ex);
844            }
845            try {
846                mInputType = inputType != EditorInfo.TYPE_NULL
847                        ? inputType
848                        : mInput.getInputType();
849            } catch (IncompatibleClassChangeError e) {
850                mInputType = EditorInfo.TYPE_CLASS_TEXT;
851            }
852        } else if (digits != null) {
853            mInput = DigitsKeyListener.getInstance(digits.toString());
854            // If no input type was specified, we will default to generic
855            // text, since we can't tell the IME about the set of digits
856            // that was selected.
857            mInputType = inputType != EditorInfo.TYPE_NULL
858                    ? inputType : EditorInfo.TYPE_CLASS_TEXT;
859        } else if (inputType != EditorInfo.TYPE_NULL) {
860            setInputType(inputType, true);
861            // If set, the input type overrides what was set using the deprecated singleLine flag.
862            singleLine = !isMultilineInputType(inputType);
863        } else if (phone) {
864            mInput = DialerKeyListener.getInstance();
865            mInputType = inputType = EditorInfo.TYPE_CLASS_PHONE;
866        } else if (numeric != 0) {
867            mInput = DigitsKeyListener.getInstance((numeric & SIGNED) != 0,
868                                                   (numeric & DECIMAL) != 0);
869            inputType = EditorInfo.TYPE_CLASS_NUMBER;
870            if ((numeric & SIGNED) != 0) {
871                inputType |= EditorInfo.TYPE_NUMBER_FLAG_SIGNED;
872            }
873            if ((numeric & DECIMAL) != 0) {
874                inputType |= EditorInfo.TYPE_NUMBER_FLAG_DECIMAL;
875            }
876            mInputType = inputType;
877        } else if (autotext || autocap != -1) {
878            TextKeyListener.Capitalize cap;
879
880            inputType = EditorInfo.TYPE_CLASS_TEXT;
881
882            switch (autocap) {
883            case 1:
884                cap = TextKeyListener.Capitalize.SENTENCES;
885                inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES;
886                break;
887
888            case 2:
889                cap = TextKeyListener.Capitalize.WORDS;
890                inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
891                break;
892
893            case 3:
894                cap = TextKeyListener.Capitalize.CHARACTERS;
895                inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS;
896                break;
897
898            default:
899                cap = TextKeyListener.Capitalize.NONE;
900                break;
901            }
902
903            mInput = TextKeyListener.getInstance(autotext, cap);
904            mInputType = inputType;
905        } else if (mTextIsSelectable) {
906            // Prevent text changes from keyboard.
907            mInputType = EditorInfo.TYPE_NULL;
908            mInput = null;
909            bufferType = BufferType.SPANNABLE;
910            // Required to request focus while in touch mode.
911            setFocusableInTouchMode(true);
912            // So that selection can be changed using arrow keys and touch is handled.
913            setMovementMethod(ArrowKeyMovementMethod.getInstance());
914        } else if (editable) {
915            mInput = TextKeyListener.getInstance();
916            mInputType = EditorInfo.TYPE_CLASS_TEXT;
917        } else {
918            mInput = null;
919
920            switch (buffertype) {
921                case 0:
922                    bufferType = BufferType.NORMAL;
923                    break;
924                case 1:
925                    bufferType = BufferType.SPANNABLE;
926                    break;
927                case 2:
928                    bufferType = BufferType.EDITABLE;
929                    break;
930            }
931        }
932
933        // mInputType has been set from inputType, possibly modified by mInputMethod.
934        // Specialize mInputType to [web]password if we have a text class and the original input
935        // type was a password.
936        if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
937            if (password || passwordInputType) {
938                mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
939                        | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
940            }
941            if (webPasswordInputType) {
942                mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
943                        | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
944            }
945        } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
946            if (numberPasswordInputType) {
947                mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
948                        | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
949            }
950        }
951
952        if (selectallonfocus) {
953            mSelectAllOnFocus = true;
954
955            if (bufferType == BufferType.NORMAL)
956                bufferType = BufferType.SPANNABLE;
957        }
958
959        setCompoundDrawablesWithIntrinsicBounds(
960            drawableLeft, drawableTop, drawableRight, drawableBottom);
961        setCompoundDrawablePadding(drawablePadding);
962
963        // Same as setSingleLine(), but make sure the transformation method and the maximum number
964        // of lines of height are unchanged for multi-line TextViews.
965        setInputTypeSingleLine(singleLine);
966        applySingleLine(singleLine, singleLine, singleLine);
967
968        if (singleLine && mInput == null && ellipsize < 0) {
969                ellipsize = 3; // END
970        }
971
972        switch (ellipsize) {
973            case 1:
974                setEllipsize(TextUtils.TruncateAt.START);
975                break;
976            case 2:
977                setEllipsize(TextUtils.TruncateAt.MIDDLE);
978                break;
979            case 3:
980                setEllipsize(TextUtils.TruncateAt.END);
981                break;
982            case 4:
983                setHorizontalFadingEdgeEnabled(true);
984                setEllipsize(TextUtils.TruncateAt.MARQUEE);
985                break;
986        }
987
988        setTextColor(textColor != null ? textColor : ColorStateList.valueOf(0xFF000000));
989        setHintTextColor(textColorHint);
990        setLinkTextColor(textColorLink);
991        if (textColorHighlight != 0) {
992            setHighlightColor(textColorHighlight);
993        }
994        setRawTextSize(textSize);
995
996        if (password || passwordInputType || webPasswordInputType || numberPasswordInputType) {
997            setTransformationMethod(PasswordTransformationMethod.getInstance());
998            typefaceIndex = MONOSPACE;
999        } else if ((mInputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION))
1000                == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)) {
1001            typefaceIndex = MONOSPACE;
1002        }
1003
1004        setTypefaceByIndex(typefaceIndex, styleIndex);
1005
1006        if (shadowcolor != 0) {
1007            setShadowLayer(r, dx, dy, shadowcolor);
1008        }
1009
1010        if (maxlength >= 0) {
1011            setFilters(new InputFilter[] { new InputFilter.LengthFilter(maxlength) });
1012        } else {
1013            setFilters(NO_FILTERS);
1014        }
1015
1016        setText(text, bufferType);
1017        if (hint != null) setHint(hint);
1018
1019        /*
1020         * Views are not normally focusable unless specified to be.
1021         * However, TextViews that have input or movement methods *are*
1022         * focusable by default.
1023         */
1024        a = context.obtainStyledAttributes(attrs,
1025                                           com.android.internal.R.styleable.View,
1026                                           defStyle, 0);
1027
1028        boolean focusable = mMovement != null || mInput != null;
1029        boolean clickable = focusable;
1030        boolean longClickable = focusable;
1031
1032        n = a.getIndexCount();
1033        for (int i = 0; i < n; i++) {
1034            int attr = a.getIndex(i);
1035
1036            switch (attr) {
1037            case com.android.internal.R.styleable.View_focusable:
1038                focusable = a.getBoolean(attr, focusable);
1039                break;
1040
1041            case com.android.internal.R.styleable.View_clickable:
1042                clickable = a.getBoolean(attr, clickable);
1043                break;
1044
1045            case com.android.internal.R.styleable.View_longClickable:
1046                longClickable = a.getBoolean(attr, longClickable);
1047                break;
1048            }
1049        }
1050        a.recycle();
1051
1052        setFocusable(focusable);
1053        setClickable(clickable);
1054        setLongClickable(longClickable);
1055
1056        prepareCursorControllers();
1057
1058        final ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
1059        final int touchSlop = viewConfiguration.getScaledTouchSlop();
1060        mSquaredTouchSlopDistance = touchSlop * touchSlop;
1061    }
1062
1063    private void setTypefaceByIndex(int typefaceIndex, int styleIndex) {
1064        Typeface tf = null;
1065        switch (typefaceIndex) {
1066            case SANS:
1067                tf = Typeface.SANS_SERIF;
1068                break;
1069
1070            case SERIF:
1071                tf = Typeface.SERIF;
1072                break;
1073
1074            case MONOSPACE:
1075                tf = Typeface.MONOSPACE;
1076                break;
1077        }
1078
1079        setTypeface(tf, styleIndex);
1080    }
1081
1082    @Override
1083    public void setEnabled(boolean enabled) {
1084        if (enabled == isEnabled()) {
1085            return;
1086        }
1087
1088        if (!enabled) {
1089            // Hide the soft input if the currently active TextView is disabled
1090            InputMethodManager imm = InputMethodManager.peekInstance();
1091            if (imm != null && imm.isActive(this)) {
1092                imm.hideSoftInputFromWindow(getWindowToken(), 0);
1093            }
1094        }
1095        super.setEnabled(enabled);
1096    }
1097
1098    /**
1099     * Sets the typeface and style in which the text should be displayed,
1100     * and turns on the fake bold and italic bits in the Paint if the
1101     * Typeface that you provided does not have all the bits in the
1102     * style that you specified.
1103     *
1104     * @attr ref android.R.styleable#TextView_typeface
1105     * @attr ref android.R.styleable#TextView_textStyle
1106     */
1107    public void setTypeface(Typeface tf, int style) {
1108        if (style > 0) {
1109            if (tf == null) {
1110                tf = Typeface.defaultFromStyle(style);
1111            } else {
1112                tf = Typeface.create(tf, style);
1113            }
1114
1115            setTypeface(tf);
1116            // now compute what (if any) algorithmic styling is needed
1117            int typefaceStyle = tf != null ? tf.getStyle() : 0;
1118            int need = style & ~typefaceStyle;
1119            mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
1120            mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
1121        } else {
1122            mTextPaint.setFakeBoldText(false);
1123            mTextPaint.setTextSkewX(0);
1124            setTypeface(tf);
1125        }
1126    }
1127
1128    /**
1129     * Subclasses override this to specify that they have a KeyListener
1130     * by default even if not specifically called for in the XML options.
1131     */
1132    protected boolean getDefaultEditable() {
1133        return false;
1134    }
1135
1136    /**
1137     * Subclasses override this to specify a default movement method.
1138     */
1139    protected MovementMethod getDefaultMovementMethod() {
1140        return null;
1141    }
1142
1143    /**
1144     * Return the text the TextView is displaying. If setText() was called with
1145     * an argument of BufferType.SPANNABLE or BufferType.EDITABLE, you can cast
1146     * the return value from this method to Spannable or Editable, respectively.
1147     *
1148     * Note: The content of the return value should not be modified. If you want
1149     * a modifiable one, you should make your own copy first.
1150     */
1151    @ViewDebug.CapturedViewProperty
1152    public CharSequence getText() {
1153        return mText;
1154    }
1155
1156    /**
1157     * Returns the length, in characters, of the text managed by this TextView
1158     */
1159    public int length() {
1160        return mText.length();
1161    }
1162
1163    /**
1164     * Return the text the TextView is displaying as an Editable object.  If
1165     * the text is not editable, null is returned.
1166     *
1167     * @see #getText
1168     */
1169    public Editable getEditableText() {
1170        return (mText instanceof Editable) ? (Editable)mText : null;
1171    }
1172
1173    /**
1174     * @return the height of one standard line in pixels.  Note that markup
1175     * within the text can cause individual lines to be taller or shorter
1176     * than this height, and the layout may contain additional first-
1177     * or last-line padding.
1178     */
1179    public int getLineHeight() {
1180        return FastMath.round(mTextPaint.getFontMetricsInt(null) * mSpacingMult + mSpacingAdd);
1181    }
1182
1183    /**
1184     * @return the Layout that is currently being used to display the text.
1185     * This can be null if the text or width has recently changes.
1186     */
1187    public final Layout getLayout() {
1188        return mLayout;
1189    }
1190
1191    /**
1192     * @return the current key listener for this TextView.
1193     * This will frequently be null for non-EditText TextViews.
1194     */
1195    public final KeyListener getKeyListener() {
1196        return mInput;
1197    }
1198
1199    /**
1200     * Sets the key listener to be used with this TextView.  This can be null
1201     * to disallow user input.  Note that this method has significant and
1202     * subtle interactions with soft keyboards and other input method:
1203     * see {@link KeyListener#getInputType() KeyListener.getContentType()}
1204     * for important details.  Calling this method will replace the current
1205     * content type of the text view with the content type returned by the
1206     * key listener.
1207     * <p>
1208     * Be warned that if you want a TextView with a key listener or movement
1209     * method not to be focusable, or if you want a TextView without a
1210     * key listener or movement method to be focusable, you must call
1211     * {@link #setFocusable} again after calling this to get the focusability
1212     * back the way you want it.
1213     *
1214     * @attr ref android.R.styleable#TextView_numeric
1215     * @attr ref android.R.styleable#TextView_digits
1216     * @attr ref android.R.styleable#TextView_phoneNumber
1217     * @attr ref android.R.styleable#TextView_inputMethod
1218     * @attr ref android.R.styleable#TextView_capitalize
1219     * @attr ref android.R.styleable#TextView_autoText
1220     */
1221    public void setKeyListener(KeyListener input) {
1222        setKeyListenerOnly(input);
1223        fixFocusableAndClickableSettings();
1224
1225        if (input != null) {
1226            try {
1227                mInputType = mInput.getInputType();
1228            } catch (IncompatibleClassChangeError e) {
1229                mInputType = EditorInfo.TYPE_CLASS_TEXT;
1230            }
1231            // Change inputType, without affecting transformation.
1232            // No need to applySingleLine since mSingleLine is unchanged.
1233            setInputTypeSingleLine(mSingleLine);
1234        } else {
1235            mInputType = EditorInfo.TYPE_NULL;
1236        }
1237
1238        InputMethodManager imm = InputMethodManager.peekInstance();
1239        if (imm != null) imm.restartInput(this);
1240    }
1241
1242    private void setKeyListenerOnly(KeyListener input) {
1243        mInput = input;
1244        if (mInput != null && !(mText instanceof Editable))
1245            setText(mText);
1246
1247        setFilters((Editable) mText, mFilters);
1248    }
1249
1250    /**
1251     * @return the movement method being used for this TextView.
1252     * This will frequently be null for non-EditText TextViews.
1253     */
1254    public final MovementMethod getMovementMethod() {
1255        return mMovement;
1256    }
1257
1258    /**
1259     * Sets the movement method (arrow key handler) to be used for
1260     * this TextView.  This can be null to disallow using the arrow keys
1261     * to move the cursor or scroll the view.
1262     * <p>
1263     * Be warned that if you want a TextView with a key listener or movement
1264     * method not to be focusable, or if you want a TextView without a
1265     * key listener or movement method to be focusable, you must call
1266     * {@link #setFocusable} again after calling this to get the focusability
1267     * back the way you want it.
1268     */
1269    public final void setMovementMethod(MovementMethod movement) {
1270        mMovement = movement;
1271
1272        if (mMovement != null && !(mText instanceof Spannable))
1273            setText(mText);
1274
1275        fixFocusableAndClickableSettings();
1276
1277        // SelectionModifierCursorController depends on textCanBeSelected, which depends on mMovement
1278        prepareCursorControllers();
1279    }
1280
1281    private void fixFocusableAndClickableSettings() {
1282        if ((mMovement != null) || mInput != null) {
1283            setFocusable(true);
1284            setClickable(true);
1285            setLongClickable(true);
1286        } else {
1287            setFocusable(false);
1288            setClickable(false);
1289            setLongClickable(false);
1290        }
1291    }
1292
1293    /**
1294     * @return the current transformation method for this TextView.
1295     * This will frequently be null except for single-line and password
1296     * fields.
1297     */
1298    public final TransformationMethod getTransformationMethod() {
1299        return mTransformation;
1300    }
1301
1302    /**
1303     * Sets the transformation that is applied to the text that this
1304     * TextView is displaying.
1305     *
1306     * @attr ref android.R.styleable#TextView_password
1307     * @attr ref android.R.styleable#TextView_singleLine
1308     */
1309    public final void setTransformationMethod(TransformationMethod method) {
1310        if (method == mTransformation) {
1311            // Avoid the setText() below if the transformation is
1312            // the same.
1313            return;
1314        }
1315        if (mTransformation != null) {
1316            if (mText instanceof Spannable) {
1317                ((Spannable) mText).removeSpan(mTransformation);
1318            }
1319        }
1320
1321        mTransformation = method;
1322
1323        setText(mText);
1324    }
1325
1326    /**
1327     * Returns the top padding of the view, plus space for the top
1328     * Drawable if any.
1329     */
1330    public int getCompoundPaddingTop() {
1331        final Drawables dr = mDrawables;
1332        if (dr == null || dr.mDrawableTop == null) {
1333            return mPaddingTop;
1334        } else {
1335            return mPaddingTop + dr.mDrawablePadding + dr.mDrawableSizeTop;
1336        }
1337    }
1338
1339    /**
1340     * Returns the bottom padding of the view, plus space for the bottom
1341     * Drawable if any.
1342     */
1343    public int getCompoundPaddingBottom() {
1344        final Drawables dr = mDrawables;
1345        if (dr == null || dr.mDrawableBottom == null) {
1346            return mPaddingBottom;
1347        } else {
1348            return mPaddingBottom + dr.mDrawablePadding + dr.mDrawableSizeBottom;
1349        }
1350    }
1351
1352    /**
1353     * Returns the left padding of the view, plus space for the left
1354     * Drawable if any.
1355     */
1356    public int getCompoundPaddingLeft() {
1357        final Drawables dr = mDrawables;
1358        if (dr == null || dr.mDrawableLeft == null) {
1359            return mPaddingLeft;
1360        } else {
1361            return mPaddingLeft + dr.mDrawablePadding + dr.mDrawableSizeLeft;
1362        }
1363    }
1364
1365    /**
1366     * Returns the right padding of the view, plus space for the right
1367     * Drawable if any.
1368     */
1369    public int getCompoundPaddingRight() {
1370        final Drawables dr = mDrawables;
1371        if (dr == null || dr.mDrawableRight == null) {
1372            return mPaddingRight;
1373        } else {
1374            return mPaddingRight + dr.mDrawablePadding + dr.mDrawableSizeRight;
1375        }
1376    }
1377
1378    /**
1379     * Returns the extended top padding of the view, including both the
1380     * top Drawable if any and any extra space to keep more than maxLines
1381     * of text from showing.  It is only valid to call this after measuring.
1382     */
1383    public int getExtendedPaddingTop() {
1384        if (mMaxMode != LINES) {
1385            return getCompoundPaddingTop();
1386        }
1387
1388        if (mLayout.getLineCount() <= mMaximum) {
1389            return getCompoundPaddingTop();
1390        }
1391
1392        int top = getCompoundPaddingTop();
1393        int bottom = getCompoundPaddingBottom();
1394        int viewht = getHeight() - top - bottom;
1395        int layoutht = mLayout.getLineTop(mMaximum);
1396
1397        if (layoutht >= viewht) {
1398            return top;
1399        }
1400
1401        final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
1402        if (gravity == Gravity.TOP) {
1403            return top;
1404        } else if (gravity == Gravity.BOTTOM) {
1405            return top + viewht - layoutht;
1406        } else { // (gravity == Gravity.CENTER_VERTICAL)
1407            return top + (viewht - layoutht) / 2;
1408        }
1409    }
1410
1411    /**
1412     * Returns the extended bottom padding of the view, including both the
1413     * bottom Drawable if any and any extra space to keep more than maxLines
1414     * of text from showing.  It is only valid to call this after measuring.
1415     */
1416    public int getExtendedPaddingBottom() {
1417        if (mMaxMode != LINES) {
1418            return getCompoundPaddingBottom();
1419        }
1420
1421        if (mLayout.getLineCount() <= mMaximum) {
1422            return getCompoundPaddingBottom();
1423        }
1424
1425        int top = getCompoundPaddingTop();
1426        int bottom = getCompoundPaddingBottom();
1427        int viewht = getHeight() - top - bottom;
1428        int layoutht = mLayout.getLineTop(mMaximum);
1429
1430        if (layoutht >= viewht) {
1431            return bottom;
1432        }
1433
1434        final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
1435        if (gravity == Gravity.TOP) {
1436            return bottom + viewht - layoutht;
1437        } else if (gravity == Gravity.BOTTOM) {
1438            return bottom;
1439        } else { // (gravity == Gravity.CENTER_VERTICAL)
1440            return bottom + (viewht - layoutht) / 2;
1441        }
1442    }
1443
1444    /**
1445     * Returns the total left padding of the view, including the left
1446     * Drawable if any.
1447     */
1448    public int getTotalPaddingLeft() {
1449        return getCompoundPaddingLeft();
1450    }
1451
1452    /**
1453     * Returns the total right padding of the view, including the right
1454     * Drawable if any.
1455     */
1456    public int getTotalPaddingRight() {
1457        return getCompoundPaddingRight();
1458    }
1459
1460    /**
1461     * Returns the total top padding of the view, including the top
1462     * Drawable if any, the extra space to keep more than maxLines
1463     * from showing, and the vertical offset for gravity, if any.
1464     */
1465    public int getTotalPaddingTop() {
1466        return getExtendedPaddingTop() + getVerticalOffset(true);
1467    }
1468
1469    /**
1470     * Returns the total bottom padding of the view, including the bottom
1471     * Drawable if any, the extra space to keep more than maxLines
1472     * from showing, and the vertical offset for gravity, if any.
1473     */
1474    public int getTotalPaddingBottom() {
1475        return getExtendedPaddingBottom() + getBottomVerticalOffset(true);
1476    }
1477
1478    /**
1479     * Sets the Drawables (if any) to appear to the left of, above,
1480     * to the right of, and below the text.  Use null if you do not
1481     * want a Drawable there.  The Drawables must already have had
1482     * {@link Drawable#setBounds} called.
1483     *
1484     * @attr ref android.R.styleable#TextView_drawableLeft
1485     * @attr ref android.R.styleable#TextView_drawableTop
1486     * @attr ref android.R.styleable#TextView_drawableRight
1487     * @attr ref android.R.styleable#TextView_drawableBottom
1488     */
1489    public void setCompoundDrawables(Drawable left, Drawable top,
1490                                     Drawable right, Drawable bottom) {
1491        Drawables dr = mDrawables;
1492
1493        final boolean drawables = left != null || top != null
1494                || right != null || bottom != null;
1495
1496        if (!drawables) {
1497            // Clearing drawables...  can we free the data structure?
1498            if (dr != null) {
1499                if (dr.mDrawablePadding == 0) {
1500                    mDrawables = null;
1501                } else {
1502                    // We need to retain the last set padding, so just clear
1503                    // out all of the fields in the existing structure.
1504                    if (dr.mDrawableLeft != null) dr.mDrawableLeft.setCallback(null);
1505                    dr.mDrawableLeft = null;
1506                    if (dr.mDrawableTop != null) dr.mDrawableTop.setCallback(null);
1507                    dr.mDrawableTop = null;
1508                    if (dr.mDrawableRight != null) dr.mDrawableRight.setCallback(null);
1509                    dr.mDrawableRight = null;
1510                    if (dr.mDrawableBottom != null) dr.mDrawableBottom.setCallback(null);
1511                    dr.mDrawableBottom = null;
1512                    dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
1513                    dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
1514                    dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
1515                    dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
1516                }
1517            }
1518        } else {
1519            if (dr == null) {
1520                mDrawables = dr = new Drawables();
1521            }
1522
1523            if (dr.mDrawableLeft != left && dr.mDrawableLeft != null) {
1524                dr.mDrawableLeft.setCallback(null);
1525            }
1526            dr.mDrawableLeft = left;
1527
1528            if (dr.mDrawableTop != top && dr.mDrawableTop != null) {
1529                dr.mDrawableTop.setCallback(null);
1530            }
1531            dr.mDrawableTop = top;
1532
1533            if (dr.mDrawableRight != right && dr.mDrawableRight != null) {
1534                dr.mDrawableRight.setCallback(null);
1535            }
1536            dr.mDrawableRight = right;
1537
1538            if (dr.mDrawableBottom != bottom && dr.mDrawableBottom != null) {
1539                dr.mDrawableBottom.setCallback(null);
1540            }
1541            dr.mDrawableBottom = bottom;
1542
1543            final Rect compoundRect = dr.mCompoundRect;
1544            int[] state;
1545
1546            state = getDrawableState();
1547
1548            if (left != null) {
1549                left.setState(state);
1550                left.copyBounds(compoundRect);
1551                left.setCallback(this);
1552                dr.mDrawableSizeLeft = compoundRect.width();
1553                dr.mDrawableHeightLeft = compoundRect.height();
1554            } else {
1555                dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
1556            }
1557
1558            if (right != null) {
1559                right.setState(state);
1560                right.copyBounds(compoundRect);
1561                right.setCallback(this);
1562                dr.mDrawableSizeRight = compoundRect.width();
1563                dr.mDrawableHeightRight = compoundRect.height();
1564            } else {
1565                dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
1566            }
1567
1568            if (top != null) {
1569                top.setState(state);
1570                top.copyBounds(compoundRect);
1571                top.setCallback(this);
1572                dr.mDrawableSizeTop = compoundRect.height();
1573                dr.mDrawableWidthTop = compoundRect.width();
1574            } else {
1575                dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
1576            }
1577
1578            if (bottom != null) {
1579                bottom.setState(state);
1580                bottom.copyBounds(compoundRect);
1581                bottom.setCallback(this);
1582                dr.mDrawableSizeBottom = compoundRect.height();
1583                dr.mDrawableWidthBottom = compoundRect.width();
1584            } else {
1585                dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
1586            }
1587        }
1588
1589        invalidate();
1590        requestLayout();
1591    }
1592
1593    /**
1594     * Sets the Drawables (if any) to appear to the left of, above,
1595     * to the right of, and below the text.  Use 0 if you do not
1596     * want a Drawable there. The Drawables' bounds will be set to
1597     * their intrinsic bounds.
1598     *
1599     * @param left Resource identifier of the left Drawable.
1600     * @param top Resource identifier of the top Drawable.
1601     * @param right Resource identifier of the right Drawable.
1602     * @param bottom Resource identifier of the bottom Drawable.
1603     *
1604     * @attr ref android.R.styleable#TextView_drawableLeft
1605     * @attr ref android.R.styleable#TextView_drawableTop
1606     * @attr ref android.R.styleable#TextView_drawableRight
1607     * @attr ref android.R.styleable#TextView_drawableBottom
1608     */
1609    public void setCompoundDrawablesWithIntrinsicBounds(int left, int top, int right, int bottom) {
1610        final Resources resources = getContext().getResources();
1611        setCompoundDrawablesWithIntrinsicBounds(left != 0 ? resources.getDrawable(left) : null,
1612                top != 0 ? resources.getDrawable(top) : null,
1613                right != 0 ? resources.getDrawable(right) : null,
1614                bottom != 0 ? resources.getDrawable(bottom) : null);
1615    }
1616
1617    /**
1618     * Sets the Drawables (if any) to appear to the left of, above,
1619     * to the right of, and below the text.  Use null if you do not
1620     * want a Drawable there. The Drawables' bounds will be set to
1621     * their intrinsic bounds.
1622     *
1623     * @attr ref android.R.styleable#TextView_drawableLeft
1624     * @attr ref android.R.styleable#TextView_drawableTop
1625     * @attr ref android.R.styleable#TextView_drawableRight
1626     * @attr ref android.R.styleable#TextView_drawableBottom
1627     */
1628    public void setCompoundDrawablesWithIntrinsicBounds(Drawable left, Drawable top,
1629            Drawable right, Drawable bottom) {
1630
1631        if (left != null) {
1632            left.setBounds(0, 0, left.getIntrinsicWidth(), left.getIntrinsicHeight());
1633        }
1634        if (right != null) {
1635            right.setBounds(0, 0, right.getIntrinsicWidth(), right.getIntrinsicHeight());
1636        }
1637        if (top != null) {
1638            top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight());
1639        }
1640        if (bottom != null) {
1641            bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight());
1642        }
1643        setCompoundDrawables(left, top, right, bottom);
1644    }
1645
1646    /**
1647     * Returns drawables for the left, top, right, and bottom borders.
1648     */
1649    public Drawable[] getCompoundDrawables() {
1650        final Drawables dr = mDrawables;
1651        if (dr != null) {
1652            return new Drawable[] {
1653                dr.mDrawableLeft, dr.mDrawableTop, dr.mDrawableRight, dr.mDrawableBottom
1654            };
1655        } else {
1656            return new Drawable[] { null, null, null, null };
1657        }
1658    }
1659
1660    /**
1661     * Sets the size of the padding between the compound drawables and
1662     * the text.
1663     *
1664     * @attr ref android.R.styleable#TextView_drawablePadding
1665     */
1666    public void setCompoundDrawablePadding(int pad) {
1667        Drawables dr = mDrawables;
1668        if (pad == 0) {
1669            if (dr != null) {
1670                dr.mDrawablePadding = pad;
1671            }
1672        } else {
1673            if (dr == null) {
1674                mDrawables = dr = new Drawables();
1675            }
1676            dr.mDrawablePadding = pad;
1677        }
1678
1679        invalidate();
1680        requestLayout();
1681    }
1682
1683    /**
1684     * Returns the padding between the compound drawables and the text.
1685     */
1686    public int getCompoundDrawablePadding() {
1687        final Drawables dr = mDrawables;
1688        return dr != null ? dr.mDrawablePadding : 0;
1689    }
1690
1691    @Override
1692    public void setPadding(int left, int top, int right, int bottom) {
1693        if (left != mPaddingLeft ||
1694            right != mPaddingRight ||
1695            top != mPaddingTop ||
1696            bottom != mPaddingBottom) {
1697            nullLayouts();
1698        }
1699
1700        // the super call will requestLayout()
1701        super.setPadding(left, top, right, bottom);
1702        invalidate();
1703    }
1704
1705    /**
1706     * Gets the autolink mask of the text.  See {@link
1707     * android.text.util.Linkify#ALL Linkify.ALL} and peers for
1708     * possible values.
1709     *
1710     * @attr ref android.R.styleable#TextView_autoLink
1711     */
1712    public final int getAutoLinkMask() {
1713        return mAutoLinkMask;
1714    }
1715
1716    /**
1717     * Sets the text color, size, style, hint color, and highlight color
1718     * from the specified TextAppearance resource.
1719     */
1720    public void setTextAppearance(Context context, int resid) {
1721        TypedArray appearance =
1722            context.obtainStyledAttributes(resid,
1723                                           com.android.internal.R.styleable.TextAppearance);
1724
1725        int color;
1726        ColorStateList colors;
1727        int ts;
1728
1729        color = appearance.getColor(com.android.internal.R.styleable.TextAppearance_textColorHighlight, 0);
1730        if (color != 0) {
1731            setHighlightColor(color);
1732        }
1733
1734        colors = appearance.getColorStateList(com.android.internal.R.styleable.
1735                                              TextAppearance_textColor);
1736        if (colors != null) {
1737            setTextColor(colors);
1738        }
1739
1740        ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
1741                                              TextAppearance_textSize, 0);
1742        if (ts != 0) {
1743            setRawTextSize(ts);
1744        }
1745
1746        colors = appearance.getColorStateList(com.android.internal.R.styleable.
1747                                              TextAppearance_textColorHint);
1748        if (colors != null) {
1749            setHintTextColor(colors);
1750        }
1751
1752        colors = appearance.getColorStateList(com.android.internal.R.styleable.
1753                                              TextAppearance_textColorLink);
1754        if (colors != null) {
1755            setLinkTextColor(colors);
1756        }
1757
1758        int typefaceIndex, styleIndex;
1759
1760        typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
1761                                          TextAppearance_typeface, -1);
1762        styleIndex = appearance.getInt(com.android.internal.R.styleable.
1763                                       TextAppearance_textStyle, -1);
1764
1765        setTypefaceByIndex(typefaceIndex, styleIndex);
1766
1767        appearance.recycle();
1768    }
1769
1770    /**
1771     * @return the size (in pixels) of the default text size in this TextView.
1772     */
1773    public float getTextSize() {
1774        return mTextPaint.getTextSize();
1775    }
1776
1777    /**
1778     * Set the default text size to the given value, interpreted as "scaled
1779     * pixel" units.  This size is adjusted based on the current density and
1780     * user font size preference.
1781     *
1782     * @param size The scaled pixel size.
1783     *
1784     * @attr ref android.R.styleable#TextView_textSize
1785     */
1786    @android.view.RemotableViewMethod
1787    public void setTextSize(float size) {
1788        setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
1789    }
1790
1791    /**
1792     * Set the default text size to a given unit and value.  See {@link
1793     * TypedValue} for the possible dimension units.
1794     *
1795     * @param unit The desired dimension unit.
1796     * @param size The desired size in the given units.
1797     *
1798     * @attr ref android.R.styleable#TextView_textSize
1799     */
1800    public void setTextSize(int unit, float size) {
1801        Context c = getContext();
1802        Resources r;
1803
1804        if (c == null)
1805            r = Resources.getSystem();
1806        else
1807            r = c.getResources();
1808
1809        setRawTextSize(TypedValue.applyDimension(
1810            unit, size, r.getDisplayMetrics()));
1811    }
1812
1813    private void setRawTextSize(float size) {
1814        if (size != mTextPaint.getTextSize()) {
1815            mTextPaint.setTextSize(size);
1816
1817            if (mLayout != null) {
1818                nullLayouts();
1819                requestLayout();
1820                invalidate();
1821            }
1822        }
1823    }
1824
1825    /**
1826     * @return the extent by which text is currently being stretched
1827     * horizontally.  This will usually be 1.
1828     */
1829    public float getTextScaleX() {
1830        return mTextPaint.getTextScaleX();
1831    }
1832
1833    /**
1834     * Sets the extent by which text should be stretched horizontally.
1835     *
1836     * @attr ref android.R.styleable#TextView_textScaleX
1837     */
1838    @android.view.RemotableViewMethod
1839    public void setTextScaleX(float size) {
1840        if (size != mTextPaint.getTextScaleX()) {
1841            mUserSetTextScaleX = true;
1842            mTextPaint.setTextScaleX(size);
1843
1844            if (mLayout != null) {
1845                nullLayouts();
1846                requestLayout();
1847                invalidate();
1848            }
1849        }
1850    }
1851
1852    /**
1853     * Sets the typeface and style in which the text should be displayed.
1854     * Note that not all Typeface families actually have bold and italic
1855     * variants, so you may need to use
1856     * {@link #setTypeface(Typeface, int)} to get the appearance
1857     * that you actually want.
1858     *
1859     * @attr ref android.R.styleable#TextView_typeface
1860     * @attr ref android.R.styleable#TextView_textStyle
1861     */
1862    public void setTypeface(Typeface tf) {
1863        if (mTextPaint.getTypeface() != tf) {
1864            mTextPaint.setTypeface(tf);
1865
1866            if (mLayout != null) {
1867                nullLayouts();
1868                requestLayout();
1869                invalidate();
1870            }
1871        }
1872    }
1873
1874    /**
1875     * @return the current typeface and style in which the text is being
1876     * displayed.
1877     */
1878    public Typeface getTypeface() {
1879        return mTextPaint.getTypeface();
1880    }
1881
1882    /**
1883     * Sets the text color for all the states (normal, selected,
1884     * focused) to be this color.
1885     *
1886     * @attr ref android.R.styleable#TextView_textColor
1887     */
1888    @android.view.RemotableViewMethod
1889    public void setTextColor(int color) {
1890        mTextColor = ColorStateList.valueOf(color);
1891        updateTextColors();
1892    }
1893
1894    /**
1895     * Sets the text color.
1896     *
1897     * @attr ref android.R.styleable#TextView_textColor
1898     */
1899    public void setTextColor(ColorStateList colors) {
1900        if (colors == null) {
1901            throw new NullPointerException();
1902        }
1903
1904        mTextColor = colors;
1905        updateTextColors();
1906    }
1907
1908    /**
1909     * Return the set of text colors.
1910     *
1911     * @return Returns the set of text colors.
1912     */
1913    public final ColorStateList getTextColors() {
1914        return mTextColor;
1915    }
1916
1917    /**
1918     * <p>Return the current color selected for normal text.</p>
1919     *
1920     * @return Returns the current text color.
1921     */
1922    public final int getCurrentTextColor() {
1923        return mCurTextColor;
1924    }
1925
1926    /**
1927     * Sets the color used to display the selection highlight.
1928     *
1929     * @attr ref android.R.styleable#TextView_textColorHighlight
1930     */
1931    @android.view.RemotableViewMethod
1932    public void setHighlightColor(int color) {
1933        if (mHighlightColor != color) {
1934            mHighlightColor = color;
1935            invalidate();
1936        }
1937    }
1938
1939    /**
1940     * Gives the text a shadow of the specified radius and color, the specified
1941     * distance from its normal position.
1942     *
1943     * @attr ref android.R.styleable#TextView_shadowColor
1944     * @attr ref android.R.styleable#TextView_shadowDx
1945     * @attr ref android.R.styleable#TextView_shadowDy
1946     * @attr ref android.R.styleable#TextView_shadowRadius
1947     */
1948    public void setShadowLayer(float radius, float dx, float dy, int color) {
1949        mTextPaint.setShadowLayer(radius, dx, dy, color);
1950
1951        mShadowRadius = radius;
1952        mShadowDx = dx;
1953        mShadowDy = dy;
1954
1955        invalidate();
1956    }
1957
1958    /**
1959     * @return the base paint used for the text.  Please use this only to
1960     * consult the Paint's properties and not to change them.
1961     */
1962    public TextPaint getPaint() {
1963        return mTextPaint;
1964    }
1965
1966    /**
1967     * Sets the autolink mask of the text.  See {@link
1968     * android.text.util.Linkify#ALL Linkify.ALL} and peers for
1969     * possible values.
1970     *
1971     * @attr ref android.R.styleable#TextView_autoLink
1972     */
1973    @android.view.RemotableViewMethod
1974    public final void setAutoLinkMask(int mask) {
1975        mAutoLinkMask = mask;
1976    }
1977
1978    /**
1979     * Sets whether the movement method will automatically be set to
1980     * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been
1981     * set to nonzero and links are detected in {@link #setText}.
1982     * The default is true.
1983     *
1984     * @attr ref android.R.styleable#TextView_linksClickable
1985     */
1986    @android.view.RemotableViewMethod
1987    public final void setLinksClickable(boolean whether) {
1988        mLinksClickable = whether;
1989    }
1990
1991    /**
1992     * Returns whether the movement method will automatically be set to
1993     * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been
1994     * set to nonzero and links are detected in {@link #setText}.
1995     * The default is true.
1996     *
1997     * @attr ref android.R.styleable#TextView_linksClickable
1998     */
1999    public final boolean getLinksClickable() {
2000        return mLinksClickable;
2001    }
2002
2003    /**
2004     * Returns the list of URLSpans attached to the text
2005     * (by {@link Linkify} or otherwise) if any.  You can call
2006     * {@link URLSpan#getURL} on them to find where they link to
2007     * or use {@link Spanned#getSpanStart} and {@link Spanned#getSpanEnd}
2008     * to find the region of the text they are attached to.
2009     */
2010    public URLSpan[] getUrls() {
2011        if (mText instanceof Spanned) {
2012            return ((Spanned) mText).getSpans(0, mText.length(), URLSpan.class);
2013        } else {
2014            return new URLSpan[0];
2015        }
2016    }
2017
2018    /**
2019     * Sets the color of the hint text.
2020     *
2021     * @attr ref android.R.styleable#TextView_textColorHint
2022     */
2023    @android.view.RemotableViewMethod
2024    public final void setHintTextColor(int color) {
2025        mHintTextColor = ColorStateList.valueOf(color);
2026        updateTextColors();
2027    }
2028
2029    /**
2030     * Sets the color of the hint text.
2031     *
2032     * @attr ref android.R.styleable#TextView_textColorHint
2033     */
2034    public final void setHintTextColor(ColorStateList colors) {
2035        mHintTextColor = colors;
2036        updateTextColors();
2037    }
2038
2039    /**
2040     * <p>Return the color used to paint the hint text.</p>
2041     *
2042     * @return Returns the list of hint text colors.
2043     */
2044    public final ColorStateList getHintTextColors() {
2045        return mHintTextColor;
2046    }
2047
2048    /**
2049     * <p>Return the current color selected to paint the hint text.</p>
2050     *
2051     * @return Returns the current hint text color.
2052     */
2053    public final int getCurrentHintTextColor() {
2054        return mHintTextColor != null ? mCurHintTextColor : mCurTextColor;
2055    }
2056
2057    /**
2058     * Sets the color of links in the text.
2059     *
2060     * @attr ref android.R.styleable#TextView_textColorLink
2061     */
2062    @android.view.RemotableViewMethod
2063    public final void setLinkTextColor(int color) {
2064        mLinkTextColor = ColorStateList.valueOf(color);
2065        updateTextColors();
2066    }
2067
2068    /**
2069     * Sets the color of links in the text.
2070     *
2071     * @attr ref android.R.styleable#TextView_textColorLink
2072     */
2073    public final void setLinkTextColor(ColorStateList colors) {
2074        mLinkTextColor = colors;
2075        updateTextColors();
2076    }
2077
2078    /**
2079     * <p>Returns the color used to paint links in the text.</p>
2080     *
2081     * @return Returns the list of link text colors.
2082     */
2083    public final ColorStateList getLinkTextColors() {
2084        return mLinkTextColor;
2085    }
2086
2087    /**
2088     * Sets the horizontal alignment of the text and the
2089     * vertical gravity that will be used when there is extra space
2090     * in the TextView beyond what is required for the text itself.
2091     *
2092     * @see android.view.Gravity
2093     * @attr ref android.R.styleable#TextView_gravity
2094     */
2095    public void setGravity(int gravity) {
2096        if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) {
2097            gravity |= Gravity.BEFORE;
2098        }
2099        if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
2100            gravity |= Gravity.TOP;
2101        }
2102
2103        boolean newLayout = false;
2104
2105        if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) !=
2106            (mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK)) {
2107            newLayout = true;
2108        }
2109
2110        if (gravity != mGravity) {
2111            invalidate();
2112        }
2113
2114        mGravity = gravity;
2115
2116        if (mLayout != null && newLayout) {
2117            // XXX this is heavy-handed because no actual content changes.
2118            int want = mLayout.getWidth();
2119            int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
2120
2121            makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
2122                          mRight - mLeft - getCompoundPaddingLeft() -
2123                          getCompoundPaddingRight(), true);
2124        }
2125    }
2126
2127    /**
2128     * Returns the horizontal and vertical alignment of this TextView.
2129     *
2130     * @see android.view.Gravity
2131     * @attr ref android.R.styleable#TextView_gravity
2132     */
2133    public int getGravity() {
2134        return mGravity;
2135    }
2136
2137    /**
2138     * @return the flags on the Paint being used to display the text.
2139     * @see Paint#getFlags
2140     */
2141    public int getPaintFlags() {
2142        return mTextPaint.getFlags();
2143    }
2144
2145    /**
2146     * Sets flags on the Paint being used to display the text and
2147     * reflows the text if they are different from the old flags.
2148     * @see Paint#setFlags
2149     */
2150    @android.view.RemotableViewMethod
2151    public void setPaintFlags(int flags) {
2152        if (mTextPaint.getFlags() != flags) {
2153            mTextPaint.setFlags(flags);
2154
2155            if (mLayout != null) {
2156                nullLayouts();
2157                requestLayout();
2158                invalidate();
2159            }
2160        }
2161    }
2162
2163    /**
2164     * Sets whether the text should be allowed to be wider than the
2165     * View is.  If false, it will be wrapped to the width of the View.
2166     *
2167     * @attr ref android.R.styleable#TextView_scrollHorizontally
2168     */
2169    public void setHorizontallyScrolling(boolean whether) {
2170        mHorizontallyScrolling = whether;
2171
2172        if (mLayout != null) {
2173            nullLayouts();
2174            requestLayout();
2175            invalidate();
2176        }
2177    }
2178
2179    /**
2180     * Makes the TextView at least this many lines tall.
2181     *
2182     * Setting this value overrides any other (minimum) height setting. A single line TextView will
2183     * set this value to 1.
2184     *
2185     * @attr ref android.R.styleable#TextView_minLines
2186     */
2187    @android.view.RemotableViewMethod
2188    public void setMinLines(int minlines) {
2189        mMinimum = minlines;
2190        mMinMode = LINES;
2191
2192        requestLayout();
2193        invalidate();
2194    }
2195
2196    /**
2197     * Makes the TextView at least this many pixels tall.
2198     *
2199     * Setting this value overrides any other (minimum) number of lines setting.
2200     *
2201     * @attr ref android.R.styleable#TextView_minHeight
2202     */
2203    @android.view.RemotableViewMethod
2204    public void setMinHeight(int minHeight) {
2205        mMinimum = minHeight;
2206        mMinMode = PIXELS;
2207
2208        requestLayout();
2209        invalidate();
2210    }
2211
2212    /**
2213     * Makes the TextView at most this many lines tall.
2214     *
2215     * Setting this value overrides any other (maximum) height setting.
2216     *
2217     * @attr ref android.R.styleable#TextView_maxLines
2218     */
2219    @android.view.RemotableViewMethod
2220    public void setMaxLines(int maxlines) {
2221        mMaximum = maxlines;
2222        mMaxMode = LINES;
2223
2224        requestLayout();
2225        invalidate();
2226    }
2227
2228    /**
2229     * Makes the TextView at most this many pixels tall.  This option is mutually exclusive with the
2230     * {@link #setMaxLines(int)} method.
2231     *
2232     * Setting this value overrides any other (maximum) number of lines setting.
2233     *
2234     * @attr ref android.R.styleable#TextView_maxHeight
2235     */
2236    @android.view.RemotableViewMethod
2237    public void setMaxHeight(int maxHeight) {
2238        mMaximum = maxHeight;
2239        mMaxMode = PIXELS;
2240
2241        requestLayout();
2242        invalidate();
2243    }
2244
2245    /**
2246     * Makes the TextView exactly this many lines tall.
2247     *
2248     * Note that setting this value overrides any other (minimum / maximum) number of lines or
2249     * height setting. A single line TextView will set this value to 1.
2250     *
2251     * @attr ref android.R.styleable#TextView_lines
2252     */
2253    @android.view.RemotableViewMethod
2254    public void setLines(int lines) {
2255        mMaximum = mMinimum = lines;
2256        mMaxMode = mMinMode = LINES;
2257
2258        requestLayout();
2259        invalidate();
2260    }
2261
2262    /**
2263     * Makes the TextView exactly this many pixels tall.
2264     * You could do the same thing by specifying this number in the
2265     * LayoutParams.
2266     *
2267     * Note that setting this value overrides any other (minimum / maximum) number of lines or
2268     * height setting.
2269     *
2270     * @attr ref android.R.styleable#TextView_height
2271     */
2272    @android.view.RemotableViewMethod
2273    public void setHeight(int pixels) {
2274        mMaximum = mMinimum = pixels;
2275        mMaxMode = mMinMode = PIXELS;
2276
2277        requestLayout();
2278        invalidate();
2279    }
2280
2281    /**
2282     * Makes the TextView at least this many ems wide
2283     *
2284     * @attr ref android.R.styleable#TextView_minEms
2285     */
2286    @android.view.RemotableViewMethod
2287    public void setMinEms(int minems) {
2288        mMinWidth = minems;
2289        mMinWidthMode = EMS;
2290
2291        requestLayout();
2292        invalidate();
2293    }
2294
2295    /**
2296     * Makes the TextView at least this many pixels wide
2297     *
2298     * @attr ref android.R.styleable#TextView_minWidth
2299     */
2300    @android.view.RemotableViewMethod
2301    public void setMinWidth(int minpixels) {
2302        mMinWidth = minpixels;
2303        mMinWidthMode = PIXELS;
2304
2305        requestLayout();
2306        invalidate();
2307    }
2308
2309    /**
2310     * Makes the TextView at most this many ems wide
2311     *
2312     * @attr ref android.R.styleable#TextView_maxEms
2313     */
2314    @android.view.RemotableViewMethod
2315    public void setMaxEms(int maxems) {
2316        mMaxWidth = maxems;
2317        mMaxWidthMode = EMS;
2318
2319        requestLayout();
2320        invalidate();
2321    }
2322
2323    /**
2324     * Makes the TextView at most this many pixels wide
2325     *
2326     * @attr ref android.R.styleable#TextView_maxWidth
2327     */
2328    @android.view.RemotableViewMethod
2329    public void setMaxWidth(int maxpixels) {
2330        mMaxWidth = maxpixels;
2331        mMaxWidthMode = PIXELS;
2332
2333        requestLayout();
2334        invalidate();
2335    }
2336
2337    /**
2338     * Makes the TextView exactly this many ems wide
2339     *
2340     * @attr ref android.R.styleable#TextView_ems
2341     */
2342    @android.view.RemotableViewMethod
2343    public void setEms(int ems) {
2344        mMaxWidth = mMinWidth = ems;
2345        mMaxWidthMode = mMinWidthMode = EMS;
2346
2347        requestLayout();
2348        invalidate();
2349    }
2350
2351    /**
2352     * Makes the TextView exactly this many pixels wide.
2353     * You could do the same thing by specifying this number in the
2354     * LayoutParams.
2355     *
2356     * @attr ref android.R.styleable#TextView_width
2357     */
2358    @android.view.RemotableViewMethod
2359    public void setWidth(int pixels) {
2360        mMaxWidth = mMinWidth = pixels;
2361        mMaxWidthMode = mMinWidthMode = PIXELS;
2362
2363        requestLayout();
2364        invalidate();
2365    }
2366
2367
2368    /**
2369     * Sets line spacing for this TextView.  Each line will have its height
2370     * multiplied by <code>mult</code> and have <code>add</code> added to it.
2371     *
2372     * @attr ref android.R.styleable#TextView_lineSpacingExtra
2373     * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
2374     */
2375    public void setLineSpacing(float add, float mult) {
2376        mSpacingMult = mult;
2377        mSpacingAdd = add;
2378
2379        if (mLayout != null) {
2380            nullLayouts();
2381            requestLayout();
2382            invalidate();
2383        }
2384    }
2385
2386    /**
2387     * Convenience method: Append the specified text to the TextView's
2388     * display buffer, upgrading it to BufferType.EDITABLE if it was
2389     * not already editable.
2390     */
2391    public final void append(CharSequence text) {
2392        append(text, 0, text.length());
2393    }
2394
2395    /**
2396     * Convenience method: Append the specified text slice to the TextView's
2397     * display buffer, upgrading it to BufferType.EDITABLE if it was
2398     * not already editable.
2399     */
2400    public void append(CharSequence text, int start, int end) {
2401        if (!(mText instanceof Editable)) {
2402            setText(mText, BufferType.EDITABLE);
2403        }
2404
2405        ((Editable) mText).append(text, start, end);
2406    }
2407
2408    private void updateTextColors() {
2409        boolean inval = false;
2410        int color = mTextColor.getColorForState(getDrawableState(), 0);
2411        if (color != mCurTextColor) {
2412            mCurTextColor = color;
2413            inval = true;
2414        }
2415        if (mLinkTextColor != null) {
2416            color = mLinkTextColor.getColorForState(getDrawableState(), 0);
2417            if (color != mTextPaint.linkColor) {
2418                mTextPaint.linkColor = color;
2419                inval = true;
2420            }
2421        }
2422        if (mHintTextColor != null) {
2423            color = mHintTextColor.getColorForState(getDrawableState(), 0);
2424            if (color != mCurHintTextColor && mText.length() == 0) {
2425                mCurHintTextColor = color;
2426                inval = true;
2427            }
2428        }
2429        if (inval) {
2430            invalidate();
2431        }
2432    }
2433
2434    @Override
2435    protected void drawableStateChanged() {
2436        super.drawableStateChanged();
2437        if (mTextColor != null && mTextColor.isStateful()
2438                || (mHintTextColor != null && mHintTextColor.isStateful())
2439                || (mLinkTextColor != null && mLinkTextColor.isStateful())) {
2440            updateTextColors();
2441        }
2442
2443        final Drawables dr = mDrawables;
2444        if (dr != null) {
2445            int[] state = getDrawableState();
2446            if (dr.mDrawableTop != null && dr.mDrawableTop.isStateful()) {
2447                dr.mDrawableTop.setState(state);
2448            }
2449            if (dr.mDrawableBottom != null && dr.mDrawableBottom.isStateful()) {
2450                dr.mDrawableBottom.setState(state);
2451            }
2452            if (dr.mDrawableLeft != null && dr.mDrawableLeft.isStateful()) {
2453                dr.mDrawableLeft.setState(state);
2454            }
2455            if (dr.mDrawableRight != null && dr.mDrawableRight.isStateful()) {
2456                dr.mDrawableRight.setState(state);
2457            }
2458        }
2459    }
2460
2461    /**
2462     * User interface state that is stored by TextView for implementing
2463     * {@link View#onSaveInstanceState}.
2464     */
2465    public static class SavedState extends BaseSavedState {
2466        int selStart;
2467        int selEnd;
2468        CharSequence text;
2469        boolean frozenWithFocus;
2470        CharSequence error;
2471
2472        SavedState(Parcelable superState) {
2473            super(superState);
2474        }
2475
2476        @Override
2477        public void writeToParcel(Parcel out, int flags) {
2478            super.writeToParcel(out, flags);
2479            out.writeInt(selStart);
2480            out.writeInt(selEnd);
2481            out.writeInt(frozenWithFocus ? 1 : 0);
2482            TextUtils.writeToParcel(text, out, flags);
2483
2484            if (error == null) {
2485                out.writeInt(0);
2486            } else {
2487                out.writeInt(1);
2488                TextUtils.writeToParcel(error, out, flags);
2489            }
2490        }
2491
2492        @Override
2493        public String toString() {
2494            String str = "TextView.SavedState{"
2495                    + Integer.toHexString(System.identityHashCode(this))
2496                    + " start=" + selStart + " end=" + selEnd;
2497            if (text != null) {
2498                str += " text=" + text;
2499            }
2500            return str + "}";
2501        }
2502
2503        @SuppressWarnings("hiding")
2504        public static final Parcelable.Creator<SavedState> CREATOR
2505                = new Parcelable.Creator<SavedState>() {
2506            public SavedState createFromParcel(Parcel in) {
2507                return new SavedState(in);
2508            }
2509
2510            public SavedState[] newArray(int size) {
2511                return new SavedState[size];
2512            }
2513        };
2514
2515        private SavedState(Parcel in) {
2516            super(in);
2517            selStart = in.readInt();
2518            selEnd = in.readInt();
2519            frozenWithFocus = (in.readInt() != 0);
2520            text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
2521
2522            if (in.readInt() != 0) {
2523                error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
2524            }
2525        }
2526    }
2527
2528    @Override
2529    public Parcelable onSaveInstanceState() {
2530        Parcelable superState = super.onSaveInstanceState();
2531
2532        // Save state if we are forced to
2533        boolean save = mFreezesText;
2534        int start = 0;
2535        int end = 0;
2536
2537        if (mText != null) {
2538            start = getSelectionStart();
2539            end = getSelectionEnd();
2540            if (start >= 0 || end >= 0) {
2541                // Or save state if there is a selection
2542                save = true;
2543            }
2544        }
2545
2546        if (save) {
2547            SavedState ss = new SavedState(superState);
2548            // XXX Should also save the current scroll position!
2549            ss.selStart = start;
2550            ss.selEnd = end;
2551
2552            if (mText instanceof Spanned) {
2553                /*
2554                 * Calling setText() strips off any ChangeWatchers;
2555                 * strip them now to avoid leaking references.
2556                 * But do it to a copy so that if there are any
2557                 * further changes to the text of this view, it
2558                 * won't get into an inconsistent state.
2559                 */
2560
2561                Spannable sp = new SpannableString(mText);
2562
2563                for (ChangeWatcher cw :
2564                     sp.getSpans(0, sp.length(), ChangeWatcher.class)) {
2565                    sp.removeSpan(cw);
2566                }
2567
2568                // hideControllers would do it, but it gets called after this method on rotation
2569                sp.removeSpan(mSuggestionRangeSpan);
2570
2571                ss.text = sp;
2572            } else {
2573                ss.text = mText.toString();
2574            }
2575
2576            if (isFocused() && start >= 0 && end >= 0) {
2577                ss.frozenWithFocus = true;
2578            }
2579
2580            ss.error = mError;
2581
2582            return ss;
2583        }
2584
2585        return superState;
2586    }
2587
2588    @Override
2589    public void onRestoreInstanceState(Parcelable state) {
2590        if (!(state instanceof SavedState)) {
2591            super.onRestoreInstanceState(state);
2592            return;
2593        }
2594
2595        SavedState ss = (SavedState)state;
2596        super.onRestoreInstanceState(ss.getSuperState());
2597
2598        // XXX restore buffer type too, as well as lots of other stuff
2599        if (ss.text != null) {
2600            setText(ss.text);
2601        }
2602
2603        if (ss.selStart >= 0 && ss.selEnd >= 0) {
2604            if (mText instanceof Spannable) {
2605                int len = mText.length();
2606
2607                if (ss.selStart > len || ss.selEnd > len) {
2608                    String restored = "";
2609
2610                    if (ss.text != null) {
2611                        restored = "(restored) ";
2612                    }
2613
2614                    Log.e(LOG_TAG, "Saved cursor position " + ss.selStart +
2615                          "/" + ss.selEnd + " out of range for " + restored +
2616                          "text " + mText);
2617                } else {
2618                    Selection.setSelection((Spannable) mText, ss.selStart,
2619                                           ss.selEnd);
2620
2621                    if (ss.frozenWithFocus) {
2622                        mFrozenWithFocus = true;
2623                    }
2624                }
2625            }
2626        }
2627
2628        if (ss.error != null) {
2629            final CharSequence error = ss.error;
2630            // Display the error later, after the first layout pass
2631            post(new Runnable() {
2632                public void run() {
2633                    setError(error);
2634                }
2635            });
2636        }
2637    }
2638
2639    /**
2640     * Control whether this text view saves its entire text contents when
2641     * freezing to an icicle, in addition to dynamic state such as cursor
2642     * position.  By default this is false, not saving the text.  Set to true
2643     * if the text in the text view is not being saved somewhere else in
2644     * persistent storage (such as in a content provider) so that if the
2645     * view is later thawed the user will not lose their data.
2646     *
2647     * @param freezesText Controls whether a frozen icicle should include the
2648     * entire text data: true to include it, false to not.
2649     *
2650     * @attr ref android.R.styleable#TextView_freezesText
2651     */
2652    @android.view.RemotableViewMethod
2653    public void setFreezesText(boolean freezesText) {
2654        mFreezesText = freezesText;
2655    }
2656
2657    /**
2658     * Return whether this text view is including its entire text contents
2659     * in frozen icicles.
2660     *
2661     * @return Returns true if text is included, false if it isn't.
2662     *
2663     * @see #setFreezesText
2664     */
2665    public boolean getFreezesText() {
2666        return mFreezesText;
2667    }
2668
2669    ///////////////////////////////////////////////////////////////////////////
2670
2671    /**
2672     * Sets the Factory used to create new Editables.
2673     */
2674    public final void setEditableFactory(Editable.Factory factory) {
2675        mEditableFactory = factory;
2676        setText(mText);
2677    }
2678
2679    /**
2680     * Sets the Factory used to create new Spannables.
2681     */
2682    public final void setSpannableFactory(Spannable.Factory factory) {
2683        mSpannableFactory = factory;
2684        setText(mText);
2685    }
2686
2687    /**
2688     * Sets the string value of the TextView. TextView <em>does not</em> accept
2689     * HTML-like formatting, which you can do with text strings in XML resource files.
2690     * To style your strings, attach android.text.style.* objects to a
2691     * {@link android.text.SpannableString SpannableString}, or see the
2692     * <a href="{@docRoot}guide/topics/resources/available-resources.html#stringresources">
2693     * Available Resource Types</a> documentation for an example of setting
2694     * formatted text in the XML resource file.
2695     *
2696     * @attr ref android.R.styleable#TextView_text
2697     */
2698    @android.view.RemotableViewMethod
2699    public final void setText(CharSequence text) {
2700        setText(text, mBufferType);
2701    }
2702
2703    /**
2704     * Like {@link #setText(CharSequence)},
2705     * except that the cursor position (if any) is retained in the new text.
2706     *
2707     * @param text The new text to place in the text view.
2708     *
2709     * @see #setText(CharSequence)
2710     */
2711    @android.view.RemotableViewMethod
2712    public final void setTextKeepState(CharSequence text) {
2713        setTextKeepState(text, mBufferType);
2714    }
2715
2716    /**
2717     * Sets the text that this TextView is to display (see
2718     * {@link #setText(CharSequence)}) and also sets whether it is stored
2719     * in a styleable/spannable buffer and whether it is editable.
2720     *
2721     * @attr ref android.R.styleable#TextView_text
2722     * @attr ref android.R.styleable#TextView_bufferType
2723     */
2724    public void setText(CharSequence text, BufferType type) {
2725        setText(text, type, true, 0);
2726
2727        if (mCharWrapper != null) {
2728            mCharWrapper.mChars = null;
2729        }
2730    }
2731
2732    private void setText(CharSequence text, BufferType type,
2733                         boolean notifyBefore, int oldlen) {
2734        if (text == null) {
2735            text = "";
2736        }
2737
2738        if (!mUserSetTextScaleX) mTextPaint.setTextScaleX(1.0f);
2739
2740        if (text instanceof Spanned &&
2741            ((Spanned) text).getSpanStart(TextUtils.TruncateAt.MARQUEE) >= 0) {
2742            setHorizontalFadingEdgeEnabled(true);
2743            setEllipsize(TextUtils.TruncateAt.MARQUEE);
2744        }
2745
2746        int n = mFilters.length;
2747        for (int i = 0; i < n; i++) {
2748            CharSequence out = mFilters[i].filter(text, 0, text.length(),
2749                                                  EMPTY_SPANNED, 0, 0);
2750            if (out != null) {
2751                text = out;
2752            }
2753        }
2754
2755        if (notifyBefore) {
2756            if (mText != null) {
2757                oldlen = mText.length();
2758                sendBeforeTextChanged(mText, 0, oldlen, text.length());
2759            } else {
2760                sendBeforeTextChanged("", 0, 0, text.length());
2761            }
2762        }
2763
2764        boolean needEditableForNotification = false;
2765
2766        if (mListeners != null && mListeners.size() != 0) {
2767            needEditableForNotification = true;
2768        }
2769
2770        if (type == BufferType.EDITABLE || mInput != null ||
2771            needEditableForNotification) {
2772            Editable t = mEditableFactory.newEditable(text);
2773            text = t;
2774            setFilters(t, mFilters);
2775            InputMethodManager imm = InputMethodManager.peekInstance();
2776            if (imm != null) imm.restartInput(this);
2777        } else if (type == BufferType.SPANNABLE || mMovement != null) {
2778            text = mSpannableFactory.newSpannable(text);
2779        } else if (!(text instanceof CharWrapper)) {
2780            text = TextUtils.stringOrSpannedString(text);
2781        }
2782
2783        if (mAutoLinkMask != 0) {
2784            Spannable s2;
2785
2786            if (type == BufferType.EDITABLE || text instanceof Spannable) {
2787                s2 = (Spannable) text;
2788            } else {
2789                s2 = mSpannableFactory.newSpannable(text);
2790            }
2791
2792            if (Linkify.addLinks(s2, mAutoLinkMask)) {
2793                text = s2;
2794                type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE;
2795
2796                /*
2797                 * We must go ahead and set the text before changing the
2798                 * movement method, because setMovementMethod() may call
2799                 * setText() again to try to upgrade the buffer type.
2800                 */
2801                mText = text;
2802
2803                // Do not change the movement method for text that support text selection as it
2804                // would prevent an arbitrary cursor displacement.
2805                final boolean hasTextSelection = this instanceof EditText || mTextIsSelectable;
2806                if (mLinksClickable && !hasTextSelection) {
2807                    setMovementMethod(LinkMovementMethod.getInstance());
2808                }
2809            }
2810        }
2811
2812        mBufferType = type;
2813        mText = text;
2814
2815        if (mTransformation == null)
2816            mTransformed = text;
2817        else
2818            mTransformed = mTransformation.getTransformation(text, this);
2819
2820        final int textLength = text.length();
2821
2822        if (text instanceof Spannable) {
2823            Spannable sp = (Spannable) text;
2824
2825            // Remove any ChangeWatchers that might have come
2826            // from other TextViews.
2827            final ChangeWatcher[] watchers = sp.getSpans(0, sp.length(), ChangeWatcher.class);
2828            final int count = watchers.length;
2829            for (int i = 0; i < count; i++)
2830                sp.removeSpan(watchers[i]);
2831
2832            if (mChangeWatcher == null)
2833                mChangeWatcher = new ChangeWatcher();
2834
2835            sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE |
2836                       (PRIORITY << Spanned.SPAN_PRIORITY_SHIFT));
2837
2838            if (mInput != null) {
2839                sp.setSpan(mInput, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
2840            }
2841
2842            if (mTransformation != null) {
2843                sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
2844
2845            }
2846
2847            if (mMovement != null) {
2848                mMovement.initialize(this, (Spannable) text);
2849
2850                /*
2851                 * Initializing the movement method will have set the
2852                 * selection, so reset mSelectionMoved to keep that from
2853                 * interfering with the normal on-focus selection-setting.
2854                 */
2855                mSelectionMoved = false;
2856            }
2857        }
2858
2859        if (mLayout != null) {
2860            checkForRelayout();
2861        }
2862
2863        sendOnTextChanged(text, 0, oldlen, textLength);
2864        onTextChanged(text, 0, oldlen, textLength);
2865
2866        if (needEditableForNotification) {
2867            sendAfterTextChanged((Editable) text);
2868        }
2869
2870        // SelectionModifierCursorController depends on textCanBeSelected, which depends on text
2871        prepareCursorControllers();
2872    }
2873
2874    /**
2875     * Sets the TextView to display the specified slice of the specified
2876     * char array.  You must promise that you will not change the contents
2877     * of the array except for right before another call to setText(),
2878     * since the TextView has no way to know that the text
2879     * has changed and that it needs to invalidate and re-layout.
2880     */
2881    public final void setText(char[] text, int start, int len) {
2882        int oldlen = 0;
2883
2884        if (start < 0 || len < 0 || start + len > text.length) {
2885            throw new IndexOutOfBoundsException(start + ", " + len);
2886        }
2887
2888        /*
2889         * We must do the before-notification here ourselves because if
2890         * the old text is a CharWrapper we destroy it before calling
2891         * into the normal path.
2892         */
2893        if (mText != null) {
2894            oldlen = mText.length();
2895            sendBeforeTextChanged(mText, 0, oldlen, len);
2896        } else {
2897            sendBeforeTextChanged("", 0, 0, len);
2898        }
2899
2900        if (mCharWrapper == null) {
2901            mCharWrapper = new CharWrapper(text, start, len);
2902        } else {
2903            mCharWrapper.set(text, start, len);
2904        }
2905
2906        setText(mCharWrapper, mBufferType, false, oldlen);
2907    }
2908
2909    private static class CharWrapper implements CharSequence, GetChars, GraphicsOperations {
2910        private char[] mChars;
2911        private int mStart, mLength;
2912
2913        public CharWrapper(char[] chars, int start, int len) {
2914            mChars = chars;
2915            mStart = start;
2916            mLength = len;
2917        }
2918
2919        /* package */ void set(char[] chars, int start, int len) {
2920            mChars = chars;
2921            mStart = start;
2922            mLength = len;
2923        }
2924
2925        public int length() {
2926            return mLength;
2927        }
2928
2929        public char charAt(int off) {
2930            return mChars[off + mStart];
2931        }
2932
2933        @Override
2934        public String toString() {
2935            return new String(mChars, mStart, mLength);
2936        }
2937
2938        public CharSequence subSequence(int start, int end) {
2939            if (start < 0 || end < 0 || start > mLength || end > mLength) {
2940                throw new IndexOutOfBoundsException(start + ", " + end);
2941            }
2942
2943            return new String(mChars, start + mStart, end - start);
2944        }
2945
2946        public void getChars(int start, int end, char[] buf, int off) {
2947            if (start < 0 || end < 0 || start > mLength || end > mLength) {
2948                throw new IndexOutOfBoundsException(start + ", " + end);
2949            }
2950
2951            System.arraycopy(mChars, start + mStart, buf, off, end - start);
2952        }
2953
2954        public void drawText(Canvas c, int start, int end,
2955                             float x, float y, Paint p) {
2956            c.drawText(mChars, start + mStart, end - start, x, y, p);
2957        }
2958
2959        public void drawTextRun(Canvas c, int start, int end,
2960                int contextStart, int contextEnd, float x, float y, int flags, Paint p) {
2961            int count = end - start;
2962            int contextCount = contextEnd - contextStart;
2963            c.drawTextRun(mChars, start + mStart, count, contextStart + mStart,
2964                    contextCount, x, y, flags, p);
2965        }
2966
2967        public float measureText(int start, int end, Paint p) {
2968            return p.measureText(mChars, start + mStart, end - start);
2969        }
2970
2971        public int getTextWidths(int start, int end, float[] widths, Paint p) {
2972            return p.getTextWidths(mChars, start + mStart, end - start, widths);
2973        }
2974
2975        public float getTextRunAdvances(int start, int end, int contextStart,
2976                int contextEnd, int flags, float[] advances, int advancesIndex,
2977                Paint p) {
2978            int count = end - start;
2979            int contextCount = contextEnd - contextStart;
2980            return p.getTextRunAdvances(mChars, start + mStart, count,
2981                    contextStart + mStart, contextCount, flags, advances,
2982                    advancesIndex);
2983        }
2984
2985        public float getTextRunAdvances(int start, int end, int contextStart,
2986                int contextEnd, int flags, float[] advances, int advancesIndex,
2987                Paint p, int reserved) {
2988            int count = end - start;
2989            int contextCount = contextEnd - contextStart;
2990            return p.getTextRunAdvances(mChars, start + mStart, count,
2991                    contextStart + mStart, contextCount, flags, advances,
2992                    advancesIndex, reserved);
2993        }
2994
2995        public int getTextRunCursor(int contextStart, int contextEnd, int flags,
2996                int offset, int cursorOpt, Paint p) {
2997            int contextCount = contextEnd - contextStart;
2998            return p.getTextRunCursor(mChars, contextStart + mStart,
2999                    contextCount, flags, offset + mStart, cursorOpt);
3000        }
3001    }
3002
3003    /**
3004     * Like {@link #setText(CharSequence, android.widget.TextView.BufferType)},
3005     * except that the cursor position (if any) is retained in the new text.
3006     *
3007     * @see #setText(CharSequence, android.widget.TextView.BufferType)
3008     */
3009    public final void setTextKeepState(CharSequence text, BufferType type) {
3010        int start = getSelectionStart();
3011        int end = getSelectionEnd();
3012        int len = text.length();
3013
3014        setText(text, type);
3015
3016        if (start >= 0 || end >= 0) {
3017            if (mText instanceof Spannable) {
3018                Selection.setSelection((Spannable) mText,
3019                                       Math.max(0, Math.min(start, len)),
3020                                       Math.max(0, Math.min(end, len)));
3021            }
3022        }
3023    }
3024
3025    @android.view.RemotableViewMethod
3026    public final void setText(int resid) {
3027        setText(getContext().getResources().getText(resid));
3028    }
3029
3030    public final void setText(int resid, BufferType type) {
3031        setText(getContext().getResources().getText(resid), type);
3032    }
3033
3034    /**
3035     * Sets the text to be displayed when the text of the TextView is empty.
3036     * Null means to use the normal empty text. The hint does not currently
3037     * participate in determining the size of the view.
3038     *
3039     * @attr ref android.R.styleable#TextView_hint
3040     */
3041    @android.view.RemotableViewMethod
3042    public final void setHint(CharSequence hint) {
3043        mHint = TextUtils.stringOrSpannedString(hint);
3044
3045        if (mLayout != null) {
3046            checkForRelayout();
3047        }
3048
3049        if (mText.length() == 0) {
3050            invalidate();
3051        }
3052    }
3053
3054    /**
3055     * Sets the text to be displayed when the text of the TextView is empty,
3056     * from a resource.
3057     *
3058     * @attr ref android.R.styleable#TextView_hint
3059     */
3060    @android.view.RemotableViewMethod
3061    public final void setHint(int resid) {
3062        setHint(getContext().getResources().getText(resid));
3063    }
3064
3065    /**
3066     * Returns the hint that is displayed when the text of the TextView
3067     * is empty.
3068     *
3069     * @attr ref android.R.styleable#TextView_hint
3070     */
3071    @ViewDebug.CapturedViewProperty
3072    public CharSequence getHint() {
3073        return mHint;
3074    }
3075
3076    private boolean isMultilineInputType(int type) {
3077        return (type & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) ==
3078            (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE);
3079    }
3080
3081    /**
3082     * Set the type of the content with a constant as defined for {@link EditorInfo#inputType}. This
3083     * will take care of changing the key listener, by calling {@link #setKeyListener(KeyListener)},
3084     * to match the given content type.  If the given content type is {@link EditorInfo#TYPE_NULL}
3085     * then a soft keyboard will not be displayed for this text view.
3086     *
3087     * Note that the maximum number of displayed lines (see {@link #setMaxLines(int)}) will be
3088     * modified if you change the {@link EditorInfo#TYPE_TEXT_FLAG_MULTI_LINE} flag of the input
3089     * type.
3090     *
3091     * @see #getInputType()
3092     * @see #setRawInputType(int)
3093     * @see android.text.InputType
3094     * @attr ref android.R.styleable#TextView_inputType
3095     */
3096    public void setInputType(int type) {
3097        final boolean wasPassword = isPasswordInputType(mInputType);
3098        final boolean wasVisiblePassword = isVisiblePasswordInputType(mInputType);
3099        setInputType(type, false);
3100        final boolean isPassword = isPasswordInputType(type);
3101        final boolean isVisiblePassword = isVisiblePasswordInputType(type);
3102        boolean forceUpdate = false;
3103        if (isPassword) {
3104            setTransformationMethod(PasswordTransformationMethod.getInstance());
3105            setTypefaceByIndex(MONOSPACE, 0);
3106        } else if (isVisiblePassword) {
3107            if (mTransformation == PasswordTransformationMethod.getInstance()) {
3108                forceUpdate = true;
3109            }
3110            setTypefaceByIndex(MONOSPACE, 0);
3111        } else if (wasPassword || wasVisiblePassword) {
3112            // not in password mode, clean up typeface and transformation
3113            setTypefaceByIndex(-1, -1);
3114            if (mTransformation == PasswordTransformationMethod.getInstance()) {
3115                forceUpdate = true;
3116            }
3117        }
3118
3119        boolean singleLine = !isMultilineInputType(type);
3120
3121        // We need to update the single line mode if it has changed or we
3122        // were previously in password mode.
3123        if (mSingleLine != singleLine || forceUpdate) {
3124            // Change single line mode, but only change the transformation if
3125            // we are not in password mode.
3126            applySingleLine(singleLine, !isPassword, true);
3127        }
3128
3129        InputMethodManager imm = InputMethodManager.peekInstance();
3130        if (imm != null) imm.restartInput(this);
3131    }
3132
3133    /**
3134     * It would be better to rely on the input type for everything. A password inputType should have
3135     * a password transformation. We should hence use isPasswordInputType instead of this method.
3136     *
3137     * We should:
3138     * - Call setInputType in setKeyListener instead of changing the input type directly (which
3139     * would install the correct transformation).
3140     * - Refuse the installation of a non-password transformation in setTransformation if the input
3141     * type is password.
3142     *
3143     * However, this is like this for legacy reasons and we cannot break existing apps. This method
3144     * is useful since it matches what the user can see (obfuscated text or not).
3145     *
3146     * @return true if the current transformation method is of the password type.
3147     */
3148    private boolean hasPasswordTransformationMethod() {
3149        return mTransformation instanceof PasswordTransformationMethod;
3150    }
3151
3152    private boolean isPasswordInputType(int inputType) {
3153        final int variation =
3154                inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
3155        return variation
3156                == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)
3157                || variation
3158                == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD)
3159                || variation
3160                == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
3161    }
3162
3163    private boolean isVisiblePasswordInputType(int inputType) {
3164        final int variation =
3165                inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
3166        return variation
3167                == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
3168    }
3169
3170    /**
3171     * Directly change the content type integer of the text view, without
3172     * modifying any other state.
3173     * @see #setInputType(int)
3174     * @see android.text.InputType
3175     * @attr ref android.R.styleable#TextView_inputType
3176     */
3177    public void setRawInputType(int type) {
3178        mInputType = type;
3179    }
3180
3181    private void setInputType(int type, boolean direct) {
3182        final int cls = type & EditorInfo.TYPE_MASK_CLASS;
3183        KeyListener input;
3184        if (cls == EditorInfo.TYPE_CLASS_TEXT) {
3185            boolean autotext = (type & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) != 0;
3186            TextKeyListener.Capitalize cap;
3187            if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
3188                cap = TextKeyListener.Capitalize.CHARACTERS;
3189            } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS) != 0) {
3190                cap = TextKeyListener.Capitalize.WORDS;
3191            } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) {
3192                cap = TextKeyListener.Capitalize.SENTENCES;
3193            } else {
3194                cap = TextKeyListener.Capitalize.NONE;
3195            }
3196            input = TextKeyListener.getInstance(autotext, cap);
3197        } else if (cls == EditorInfo.TYPE_CLASS_NUMBER) {
3198            input = DigitsKeyListener.getInstance(
3199                    (type & EditorInfo.TYPE_NUMBER_FLAG_SIGNED) != 0,
3200                    (type & EditorInfo.TYPE_NUMBER_FLAG_DECIMAL) != 0);
3201        } else if (cls == EditorInfo.TYPE_CLASS_DATETIME) {
3202            switch (type & EditorInfo.TYPE_MASK_VARIATION) {
3203                case EditorInfo.TYPE_DATETIME_VARIATION_DATE:
3204                    input = DateKeyListener.getInstance();
3205                    break;
3206                case EditorInfo.TYPE_DATETIME_VARIATION_TIME:
3207                    input = TimeKeyListener.getInstance();
3208                    break;
3209                default:
3210                    input = DateTimeKeyListener.getInstance();
3211                    break;
3212            }
3213        } else if (cls == EditorInfo.TYPE_CLASS_PHONE) {
3214            input = DialerKeyListener.getInstance();
3215        } else {
3216            input = TextKeyListener.getInstance();
3217        }
3218        setRawInputType(type);
3219        if (direct) mInput = input;
3220        else {
3221            setKeyListenerOnly(input);
3222        }
3223    }
3224
3225    /**
3226     * Get the type of the content.
3227     *
3228     * @see #setInputType(int)
3229     * @see android.text.InputType
3230     */
3231    public int getInputType() {
3232        return mInputType;
3233    }
3234
3235    /**
3236     * Change the editor type integer associated with the text view, which
3237     * will be reported to an IME with {@link EditorInfo#imeOptions} when it
3238     * has focus.
3239     * @see #getImeOptions
3240     * @see android.view.inputmethod.EditorInfo
3241     * @attr ref android.R.styleable#TextView_imeOptions
3242     */
3243    public void setImeOptions(int imeOptions) {
3244        if (mInputContentType == null) {
3245            mInputContentType = new InputContentType();
3246        }
3247        mInputContentType.imeOptions = imeOptions;
3248    }
3249
3250    /**
3251     * Get the type of the IME editor.
3252     *
3253     * @see #setImeOptions(int)
3254     * @see android.view.inputmethod.EditorInfo
3255     */
3256    public int getImeOptions() {
3257        return mInputContentType != null
3258                ? mInputContentType.imeOptions : EditorInfo.IME_NULL;
3259    }
3260
3261    /**
3262     * Change the custom IME action associated with the text view, which
3263     * will be reported to an IME with {@link EditorInfo#actionLabel}
3264     * and {@link EditorInfo#actionId} when it has focus.
3265     * @see #getImeActionLabel
3266     * @see #getImeActionId
3267     * @see android.view.inputmethod.EditorInfo
3268     * @attr ref android.R.styleable#TextView_imeActionLabel
3269     * @attr ref android.R.styleable#TextView_imeActionId
3270     */
3271    public void setImeActionLabel(CharSequence label, int actionId) {
3272        if (mInputContentType == null) {
3273            mInputContentType = new InputContentType();
3274        }
3275        mInputContentType.imeActionLabel = label;
3276        mInputContentType.imeActionId = actionId;
3277    }
3278
3279    /**
3280     * Get the IME action label previous set with {@link #setImeActionLabel}.
3281     *
3282     * @see #setImeActionLabel
3283     * @see android.view.inputmethod.EditorInfo
3284     */
3285    public CharSequence getImeActionLabel() {
3286        return mInputContentType != null
3287                ? mInputContentType.imeActionLabel : null;
3288    }
3289
3290    /**
3291     * Get the IME action ID previous set with {@link #setImeActionLabel}.
3292     *
3293     * @see #setImeActionLabel
3294     * @see android.view.inputmethod.EditorInfo
3295     */
3296    public int getImeActionId() {
3297        return mInputContentType != null
3298                ? mInputContentType.imeActionId : 0;
3299    }
3300
3301    /**
3302     * Set a special listener to be called when an action is performed
3303     * on the text view.  This will be called when the enter key is pressed,
3304     * or when an action supplied to the IME is selected by the user.  Setting
3305     * this means that the normal hard key event will not insert a newline
3306     * into the text view, even if it is multi-line; holding down the ALT
3307     * modifier will, however, allow the user to insert a newline character.
3308     */
3309    public void setOnEditorActionListener(OnEditorActionListener l) {
3310        if (mInputContentType == null) {
3311            mInputContentType = new InputContentType();
3312        }
3313        mInputContentType.onEditorActionListener = l;
3314    }
3315
3316    /**
3317     * Called when an attached input method calls
3318     * {@link InputConnection#performEditorAction(int)
3319     * InputConnection.performEditorAction()}
3320     * for this text view.  The default implementation will call your action
3321     * listener supplied to {@link #setOnEditorActionListener}, or perform
3322     * a standard operation for {@link EditorInfo#IME_ACTION_NEXT
3323     * EditorInfo.IME_ACTION_NEXT}, {@link EditorInfo#IME_ACTION_PREVIOUS
3324     * EditorInfo.IME_ACTION_PREVIOUS}, or {@link EditorInfo#IME_ACTION_DONE
3325     * EditorInfo.IME_ACTION_DONE}.
3326     *
3327     * <p>For backwards compatibility, if no IME options have been set and the
3328     * text view would not normally advance focus on enter, then
3329     * the NEXT and DONE actions received here will be turned into an enter
3330     * key down/up pair to go through the normal key handling.
3331     *
3332     * @param actionCode The code of the action being performed.
3333     *
3334     * @see #setOnEditorActionListener
3335     */
3336    public void onEditorAction(int actionCode) {
3337        final InputContentType ict = mInputContentType;
3338        if (ict != null) {
3339            if (ict.onEditorActionListener != null) {
3340                if (ict.onEditorActionListener.onEditorAction(this,
3341                        actionCode, null)) {
3342                    return;
3343                }
3344            }
3345
3346            // This is the handling for some default action.
3347            // Note that for backwards compatibility we don't do this
3348            // default handling if explicit ime options have not been given,
3349            // instead turning this into the normal enter key codes that an
3350            // app may be expecting.
3351            if (actionCode == EditorInfo.IME_ACTION_NEXT) {
3352                View v = focusSearch(FOCUS_DOWN);
3353                if (v != null) {
3354                    if (!v.requestFocus(FOCUS_DOWN)) {
3355                        throw new IllegalStateException("focus search returned a view " +
3356                                "that wasn't able to take focus!");
3357                    }
3358                }
3359                return;
3360
3361            } else if (actionCode == EditorInfo.IME_ACTION_PREVIOUS) {
3362                View v = focusSearch(FOCUS_UP);
3363                if (v != null) {
3364                    if (!v.requestFocus(FOCUS_UP)) {
3365                        throw new IllegalStateException("focus search returned a view " +
3366                                "that wasn't able to take focus!");
3367                    }
3368                }
3369                return;
3370
3371            } else if (actionCode == EditorInfo.IME_ACTION_DONE) {
3372                InputMethodManager imm = InputMethodManager.peekInstance();
3373                if (imm != null && imm.isActive(this)) {
3374                    imm.hideSoftInputFromWindow(getWindowToken(), 0);
3375                }
3376                return;
3377            }
3378        }
3379
3380        Handler h = getHandler();
3381        if (h != null) {
3382            long eventTime = SystemClock.uptimeMillis();
3383            h.sendMessage(h.obtainMessage(ViewAncestor.DISPATCH_KEY_FROM_IME,
3384                    new KeyEvent(eventTime, eventTime,
3385                    KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0,
3386                    KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
3387                    KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
3388                    | KeyEvent.FLAG_EDITOR_ACTION)));
3389            h.sendMessage(h.obtainMessage(ViewAncestor.DISPATCH_KEY_FROM_IME,
3390                    new KeyEvent(SystemClock.uptimeMillis(), eventTime,
3391                    KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0,
3392                    KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
3393                    KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
3394                    | KeyEvent.FLAG_EDITOR_ACTION)));
3395        }
3396    }
3397
3398    /**
3399     * Set the private content type of the text, which is the
3400     * {@link EditorInfo#privateImeOptions EditorInfo.privateImeOptions}
3401     * field that will be filled in when creating an input connection.
3402     *
3403     * @see #getPrivateImeOptions()
3404     * @see EditorInfo#privateImeOptions
3405     * @attr ref android.R.styleable#TextView_privateImeOptions
3406     */
3407    public void setPrivateImeOptions(String type) {
3408        if (mInputContentType == null) mInputContentType = new InputContentType();
3409        mInputContentType.privateImeOptions = type;
3410    }
3411
3412    /**
3413     * Get the private type of the content.
3414     *
3415     * @see #setPrivateImeOptions(String)
3416     * @see EditorInfo#privateImeOptions
3417     */
3418    public String getPrivateImeOptions() {
3419        return mInputContentType != null
3420                ? mInputContentType.privateImeOptions : null;
3421    }
3422
3423    /**
3424     * Set the extra input data of the text, which is the
3425     * {@link EditorInfo#extras TextBoxAttribute.extras}
3426     * Bundle that will be filled in when creating an input connection.  The
3427     * given integer is the resource ID of an XML resource holding an
3428     * {@link android.R.styleable#InputExtras &lt;input-extras&gt;} XML tree.
3429     *
3430     * @see #getInputExtras(boolean)
3431     * @see EditorInfo#extras
3432     * @attr ref android.R.styleable#TextView_editorExtras
3433     */
3434    public void setInputExtras(int xmlResId)
3435            throws XmlPullParserException, IOException {
3436        XmlResourceParser parser = getResources().getXml(xmlResId);
3437        if (mInputContentType == null) mInputContentType = new InputContentType();
3438        mInputContentType.extras = new Bundle();
3439        getResources().parseBundleExtras(parser, mInputContentType.extras);
3440    }
3441
3442    /**
3443     * Retrieve the input extras currently associated with the text view, which
3444     * can be viewed as well as modified.
3445     *
3446     * @param create If true, the extras will be created if they don't already
3447     * exist.  Otherwise, null will be returned if none have been created.
3448     * @see #setInputExtras(int)
3449     * @see EditorInfo#extras
3450     * @attr ref android.R.styleable#TextView_editorExtras
3451     */
3452    public Bundle getInputExtras(boolean create) {
3453        if (mInputContentType == null) {
3454            if (!create) return null;
3455            mInputContentType = new InputContentType();
3456        }
3457        if (mInputContentType.extras == null) {
3458            if (!create) return null;
3459            mInputContentType.extras = new Bundle();
3460        }
3461        return mInputContentType.extras;
3462    }
3463
3464    /**
3465     * Returns the error message that was set to be displayed with
3466     * {@link #setError}, or <code>null</code> if no error was set
3467     * or if it the error was cleared by the widget after user input.
3468     */
3469    public CharSequence getError() {
3470        return mError;
3471    }
3472
3473    /**
3474     * Sets the right-hand compound drawable of the TextView to the "error"
3475     * icon and sets an error message that will be displayed in a popup when
3476     * the TextView has focus.  The icon and error message will be reset to
3477     * null when any key events cause changes to the TextView's text.  If the
3478     * <code>error</code> is <code>null</code>, the error message and icon
3479     * will be cleared.
3480     */
3481    @android.view.RemotableViewMethod
3482    public void setError(CharSequence error) {
3483        if (error == null) {
3484            setError(null, null);
3485        } else {
3486            Drawable dr = getContext().getResources().
3487                getDrawable(com.android.internal.R.drawable.indicator_input_error);
3488
3489            dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight());
3490            setError(error, dr);
3491        }
3492    }
3493
3494    /**
3495     * Sets the right-hand compound drawable of the TextView to the specified
3496     * icon and sets an error message that will be displayed in a popup when
3497     * the TextView has focus.  The icon and error message will be reset to
3498     * null when any key events cause changes to the TextView's text.  The
3499     * drawable must already have had {@link Drawable#setBounds} set on it.
3500     * If the <code>error</code> is <code>null</code>, the error message will
3501     * be cleared (and you should provide a <code>null</code> icon as well).
3502     */
3503    public void setError(CharSequence error, Drawable icon) {
3504        error = TextUtils.stringOrSpannedString(error);
3505
3506        mError = error;
3507        mErrorWasChanged = true;
3508        final Drawables dr = mDrawables;
3509        if (dr != null) {
3510            setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, icon, dr.mDrawableBottom);
3511        } else {
3512            setCompoundDrawables(null, null, icon, null);
3513        }
3514
3515        if (error == null) {
3516            if (mPopup != null) {
3517                if (mPopup.isShowing()) {
3518                    mPopup.dismiss();
3519                }
3520
3521                mPopup = null;
3522            }
3523        } else {
3524            if (isFocused()) {
3525                showError();
3526            }
3527        }
3528    }
3529
3530    private void showError() {
3531        if (getWindowToken() == null) {
3532            mShowErrorAfterAttach = true;
3533            return;
3534        }
3535
3536        if (mPopup == null) {
3537            LayoutInflater inflater = LayoutInflater.from(getContext());
3538            final TextView err = (TextView) inflater.inflate(
3539                    com.android.internal.R.layout.textview_hint, null);
3540
3541            final float scale = getResources().getDisplayMetrics().density;
3542            mPopup = new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f));
3543            mPopup.setFocusable(false);
3544            // The user is entering text, so the input method is needed.  We
3545            // don't want the popup to be displayed on top of it.
3546            mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
3547        }
3548
3549        TextView tv = (TextView) mPopup.getContentView();
3550        chooseSize(mPopup, mError, tv);
3551        tv.setText(mError);
3552
3553        mPopup.showAsDropDown(this, getErrorX(), getErrorY());
3554        mPopup.fixDirection(mPopup.isAboveAnchor());
3555    }
3556
3557    private static class ErrorPopup extends PopupWindow {
3558        private boolean mAbove = false;
3559        private final TextView mView;
3560        private int mPopupInlineErrorBackgroundId = 0;
3561        private int mPopupInlineErrorAboveBackgroundId = 0;
3562
3563        ErrorPopup(TextView v, int width, int height) {
3564            super(v, width, height);
3565            mView = v;
3566            // Make sure the TextView has a background set as it will be used the first time it is
3567            // shown and positionned. Initialized with below background, which should have
3568            // dimensions identical to the above version for this to work (and is more likely).
3569            mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
3570                    com.android.internal.R.styleable.Theme_errorMessageBackground);
3571            mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
3572        }
3573
3574        void fixDirection(boolean above) {
3575            mAbove = above;
3576
3577            if (above) {
3578                mPopupInlineErrorAboveBackgroundId =
3579                    getResourceId(mPopupInlineErrorAboveBackgroundId,
3580                            com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
3581            } else {
3582                mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
3583                        com.android.internal.R.styleable.Theme_errorMessageBackground);
3584            }
3585
3586            mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId :
3587                mPopupInlineErrorBackgroundId);
3588        }
3589
3590        private int getResourceId(int currentId, int index) {
3591            if (currentId == 0) {
3592                TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
3593                        R.styleable.Theme);
3594                currentId = styledAttributes.getResourceId(index, 0);
3595                styledAttributes.recycle();
3596            }
3597            return currentId;
3598        }
3599
3600        @Override
3601        public void update(int x, int y, int w, int h, boolean force) {
3602            super.update(x, y, w, h, force);
3603
3604            boolean above = isAboveAnchor();
3605            if (above != mAbove) {
3606                fixDirection(above);
3607            }
3608        }
3609    }
3610
3611    /**
3612     * Returns the Y offset to make the pointy top of the error point
3613     * at the middle of the error icon.
3614     */
3615    private int getErrorX() {
3616        /*
3617         * The "25" is the distance between the point and the right edge
3618         * of the background
3619         */
3620        final float scale = getResources().getDisplayMetrics().density;
3621
3622        final Drawables dr = mDrawables;
3623        return getWidth() - mPopup.getWidth() - getPaddingRight() -
3624                (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
3625    }
3626
3627    /**
3628     * Returns the Y offset to make the pointy top of the error point
3629     * at the bottom of the error icon.
3630     */
3631    private int getErrorY() {
3632        /*
3633         * Compound, not extended, because the icon is not clipped
3634         * if the text height is smaller.
3635         */
3636        final int compoundPaddingTop = getCompoundPaddingTop();
3637        int vspace = mBottom - mTop - getCompoundPaddingBottom() - compoundPaddingTop;
3638
3639        final Drawables dr = mDrawables;
3640        int icontop = compoundPaddingTop +
3641                (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2;
3642
3643        /*
3644         * The "2" is the distance between the point and the top edge
3645         * of the background.
3646         */
3647        final float scale = getResources().getDisplayMetrics().density;
3648        return icontop + (dr != null ? dr.mDrawableHeightRight : 0) - getHeight() -
3649                (int) (2 * scale + 0.5f);
3650    }
3651
3652    private void hideError() {
3653        if (mPopup != null) {
3654            if (mPopup.isShowing()) {
3655                mPopup.dismiss();
3656            }
3657        }
3658
3659        mShowErrorAfterAttach = false;
3660    }
3661
3662    private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
3663        int wid = tv.getPaddingLeft() + tv.getPaddingRight();
3664        int ht = tv.getPaddingTop() + tv.getPaddingBottom();
3665
3666        /*
3667         * Figure out how big the text would be if we laid it out to the
3668         * full width of this view minus the border.
3669         */
3670        int cap = getWidth() - wid;
3671        if (cap < 0) {
3672            cap = 200; // We must not be measured yet -- setFrame() will fix it.
3673        }
3674
3675        Layout l = new StaticLayout(text, tv.getPaint(), cap,
3676                                    Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
3677        float max = 0;
3678        for (int i = 0; i < l.getLineCount(); i++) {
3679            max = Math.max(max, l.getLineWidth(i));
3680        }
3681
3682        /*
3683         * Now set the popup size to be big enough for the text plus the border.
3684         */
3685        pop.setWidth(wid + (int) Math.ceil(max));
3686        pop.setHeight(ht + l.getHeight());
3687    }
3688
3689
3690    @Override
3691    protected boolean setFrame(int l, int t, int r, int b) {
3692        boolean result = super.setFrame(l, t, r, b);
3693
3694        if (mPopup != null) {
3695            TextView tv = (TextView) mPopup.getContentView();
3696            chooseSize(mPopup, mError, tv);
3697            mPopup.update(this, getErrorX(), getErrorY(),
3698                          mPopup.getWidth(), mPopup.getHeight());
3699        }
3700
3701        restartMarqueeIfNeeded();
3702
3703        return result;
3704    }
3705
3706    private void restartMarqueeIfNeeded() {
3707        if (mRestartMarquee && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
3708            mRestartMarquee = false;
3709            startMarquee();
3710        }
3711    }
3712
3713    /**
3714     * Sets the list of input filters that will be used if the buffer is
3715     * Editable.  Has no effect otherwise.
3716     *
3717     * @attr ref android.R.styleable#TextView_maxLength
3718     */
3719    public void setFilters(InputFilter[] filters) {
3720        if (filters == null) {
3721            throw new IllegalArgumentException();
3722        }
3723
3724        mFilters = filters;
3725
3726        if (mText instanceof Editable) {
3727            setFilters((Editable) mText, filters);
3728        }
3729    }
3730
3731    /**
3732     * Sets the list of input filters on the specified Editable,
3733     * and includes mInput in the list if it is an InputFilter.
3734     */
3735    private void setFilters(Editable e, InputFilter[] filters) {
3736        if (mInput instanceof InputFilter) {
3737            InputFilter[] nf = new InputFilter[filters.length + 1];
3738
3739            System.arraycopy(filters, 0, nf, 0, filters.length);
3740            nf[filters.length] = (InputFilter) mInput;
3741
3742            e.setFilters(nf);
3743        } else {
3744            e.setFilters(filters);
3745        }
3746    }
3747
3748    /**
3749     * Returns the current list of input filters.
3750     */
3751    public InputFilter[] getFilters() {
3752        return mFilters;
3753    }
3754
3755    /////////////////////////////////////////////////////////////////////////
3756
3757    private int getVerticalOffset(boolean forceNormal) {
3758        int voffset = 0;
3759        final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
3760
3761        Layout l = mLayout;
3762        if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
3763            l = mHintLayout;
3764        }
3765
3766        if (gravity != Gravity.TOP) {
3767            int boxht;
3768
3769            if (l == mHintLayout) {
3770                boxht = getMeasuredHeight() - getCompoundPaddingTop() -
3771                        getCompoundPaddingBottom();
3772            } else {
3773                boxht = getMeasuredHeight() - getExtendedPaddingTop() -
3774                        getExtendedPaddingBottom();
3775            }
3776            int textht = l.getHeight();
3777
3778            if (textht < boxht) {
3779                if (gravity == Gravity.BOTTOM)
3780                    voffset = boxht - textht;
3781                else // (gravity == Gravity.CENTER_VERTICAL)
3782                    voffset = (boxht - textht) >> 1;
3783            }
3784        }
3785        return voffset;
3786    }
3787
3788    private int getBottomVerticalOffset(boolean forceNormal) {
3789        int voffset = 0;
3790        final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
3791
3792        Layout l = mLayout;
3793        if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
3794            l = mHintLayout;
3795        }
3796
3797        if (gravity != Gravity.BOTTOM) {
3798            int boxht;
3799
3800            if (l == mHintLayout) {
3801                boxht = getMeasuredHeight() - getCompoundPaddingTop() -
3802                        getCompoundPaddingBottom();
3803            } else {
3804                boxht = getMeasuredHeight() - getExtendedPaddingTop() -
3805                        getExtendedPaddingBottom();
3806            }
3807            int textht = l.getHeight();
3808
3809            if (textht < boxht) {
3810                if (gravity == Gravity.TOP)
3811                    voffset = boxht - textht;
3812                else // (gravity == Gravity.CENTER_VERTICAL)
3813                    voffset = (boxht - textht) >> 1;
3814            }
3815        }
3816        return voffset;
3817    }
3818
3819    private void invalidateCursorPath() {
3820        if (mHighlightPathBogus) {
3821            invalidateCursor();
3822        } else {
3823            final int horizontalPadding = getCompoundPaddingLeft();
3824            final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
3825
3826            if (mCursorCount == 0) {
3827                synchronized (sTempRect) {
3828                    /*
3829                     * The reason for this concern about the thickness of the
3830                     * cursor and doing the floor/ceil on the coordinates is that
3831                     * some EditTexts (notably textfields in the Browser) have
3832                     * anti-aliased text where not all the characters are
3833                     * necessarily at integer-multiple locations.  This should
3834                     * make sure the entire cursor gets invalidated instead of
3835                     * sometimes missing half a pixel.
3836                     */
3837                    float thick = FloatMath.ceil(mTextPaint.getStrokeWidth());
3838                    if (thick < 1.0f) {
3839                        thick = 1.0f;
3840                    }
3841
3842                    thick /= 2.0f;
3843
3844                    mHighlightPath.computeBounds(sTempRect, false);
3845
3846                    invalidate((int) FloatMath.floor(horizontalPadding + sTempRect.left - thick),
3847                            (int) FloatMath.floor(verticalPadding + sTempRect.top - thick),
3848                            (int) FloatMath.ceil(horizontalPadding + sTempRect.right + thick),
3849                            (int) FloatMath.ceil(verticalPadding + sTempRect.bottom + thick));
3850                }
3851            } else {
3852                for (int i = 0; i < mCursorCount; i++) {
3853                    Rect bounds = mCursorDrawable[i].getBounds();
3854                    invalidate(bounds.left + horizontalPadding, bounds.top + verticalPadding,
3855                            bounds.right + horizontalPadding, bounds.bottom + verticalPadding);
3856                }
3857            }
3858        }
3859    }
3860
3861    private void invalidateCursor() {
3862        int where = getSelectionEnd();
3863
3864        invalidateCursor(where, where, where);
3865    }
3866
3867    private void invalidateCursor(int a, int b, int c) {
3868        if (mLayout == null) {
3869            invalidate();
3870        } else {
3871            if (a >= 0 || b >= 0 || c >= 0) {
3872                int first = Math.min(Math.min(a, b), c);
3873                int last = Math.max(Math.max(a, b), c);
3874
3875                int line = mLayout.getLineForOffset(first);
3876                int top = mLayout.getLineTop(line);
3877
3878                // This is ridiculous, but the descent from the line above
3879                // can hang down into the line we really want to redraw,
3880                // so we have to invalidate part of the line above to make
3881                // sure everything that needs to be redrawn really is.
3882                // (But not the whole line above, because that would cause
3883                // the same problem with the descenders on the line above it!)
3884                if (line > 0) {
3885                    top -= mLayout.getLineDescent(line - 1);
3886                }
3887
3888                int line2;
3889
3890                if (first == last)
3891                    line2 = line;
3892                else
3893                    line2 = mLayout.getLineForOffset(last);
3894
3895                int bottom = mLayout.getLineTop(line2 + 1);
3896
3897                final int horizontalPadding = getCompoundPaddingLeft();
3898                final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
3899
3900                // If used, the cursor drawables can have an arbitrary dimension that can go beyond
3901                // the invalidated lines specified above.
3902                for (int i = 0; i < mCursorCount; i++) {
3903                    Rect bounds = mCursorDrawable[i].getBounds();
3904                    top = Math.min(top, bounds.top);
3905                    bottom = Math.max(bottom, bounds.bottom);
3906                    // Horizontal bounds are already full width, no need to update
3907                }
3908
3909                invalidate(horizontalPadding + mScrollX, top + verticalPadding,
3910                        horizontalPadding + mScrollX + getWidth() -
3911                        getCompoundPaddingLeft() - getCompoundPaddingRight(),
3912                        bottom + verticalPadding);
3913            }
3914        }
3915    }
3916
3917    private void registerForPreDraw() {
3918        final ViewTreeObserver observer = getViewTreeObserver();
3919
3920        if (mPreDrawState == PREDRAW_NOT_REGISTERED) {
3921            observer.addOnPreDrawListener(this);
3922            mPreDrawState = PREDRAW_PENDING;
3923        } else if (mPreDrawState == PREDRAW_DONE) {
3924            mPreDrawState = PREDRAW_PENDING;
3925        }
3926
3927        // else state is PREDRAW_PENDING, so keep waiting.
3928    }
3929
3930    /**
3931     * {@inheritDoc}
3932     */
3933    public boolean onPreDraw() {
3934        if (mPreDrawState != PREDRAW_PENDING) {
3935            return true;
3936        }
3937
3938        if (mLayout == null) {
3939            assumeLayout();
3940        }
3941
3942        boolean changed = false;
3943
3944        if (mMovement != null) {
3945            /* This code also provides auto-scrolling when a cursor is moved using a
3946             * CursorController (insertion point or selection limits).
3947             * For selection, ensure start or end is visible depending on controller's state.
3948             */
3949            int curs = getSelectionEnd();
3950            // Do not create the controller if it is not already created.
3951            if (mSelectionModifierCursorController != null &&
3952                    mSelectionModifierCursorController.isSelectionStartDragged()) {
3953                curs = getSelectionStart();
3954            }
3955
3956            /*
3957             * TODO: This should really only keep the end in view if
3958             * it already was before the text changed.  I'm not sure
3959             * of a good way to tell from here if it was.
3960             */
3961            if (curs < 0 &&
3962                  (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
3963                curs = mText.length();
3964            }
3965
3966            if (curs >= 0) {
3967                changed = bringPointIntoView(curs);
3968            }
3969        } else {
3970            changed = bringTextIntoView();
3971        }
3972
3973        // This has to be checked here since:
3974        // - onFocusChanged cannot start it when focus is given to a view with selected text (after
3975        //   a screen rotation) since layout is not yet initialized at that point.
3976        if (mCreatedWithASelection) {
3977            startSelectionActionMode();
3978            mCreatedWithASelection = false;
3979        }
3980
3981        // Phone specific code (there is no ExtractEditText on tablets).
3982        // ExtractEditText does not call onFocus when it is displayed, and mHasSelectionOnFocus can
3983        // not be set. Do the test here instead.
3984        if (this instanceof ExtractEditText && hasSelection()) {
3985            startSelectionActionMode();
3986        }
3987
3988        mPreDrawState = PREDRAW_DONE;
3989        return !changed;
3990    }
3991
3992    @Override
3993    protected void onAttachedToWindow() {
3994        super.onAttachedToWindow();
3995
3996        mTemporaryDetach = false;
3997
3998        if (mShowErrorAfterAttach) {
3999            showError();
4000            mShowErrorAfterAttach = false;
4001        }
4002
4003        final ViewTreeObserver observer = getViewTreeObserver();
4004        // No need to create the controller.
4005        // The get method will add the listener on controller creation.
4006        if (mInsertionPointCursorController != null) {
4007            observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
4008        }
4009        if (mSelectionModifierCursorController != null) {
4010            observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
4011        }
4012    }
4013
4014    @Override
4015    protected void onDetachedFromWindow() {
4016        super.onDetachedFromWindow();
4017
4018        final ViewTreeObserver observer = getViewTreeObserver();
4019        if (mPreDrawState != PREDRAW_NOT_REGISTERED) {
4020            observer.removeOnPreDrawListener(this);
4021            mPreDrawState = PREDRAW_NOT_REGISTERED;
4022        }
4023
4024        if (mError != null) {
4025            hideError();
4026        }
4027
4028        if (mBlink != null) {
4029            mBlink.removeCallbacks(mBlink);
4030        }
4031
4032        if (mInsertionPointCursorController != null) {
4033            mInsertionPointCursorController.onDetached();
4034        }
4035
4036        if (mSelectionModifierCursorController != null) {
4037            mSelectionModifierCursorController.onDetached();
4038        }
4039
4040        hideControllers();
4041    }
4042
4043    @Override
4044    protected boolean isPaddingOffsetRequired() {
4045        return mShadowRadius != 0 || mDrawables != null;
4046    }
4047
4048    @Override
4049    protected int getLeftPaddingOffset() {
4050        return getCompoundPaddingLeft() - mPaddingLeft +
4051                (int) Math.min(0, mShadowDx - mShadowRadius);
4052    }
4053
4054    @Override
4055    protected int getTopPaddingOffset() {
4056        return (int) Math.min(0, mShadowDy - mShadowRadius);
4057    }
4058
4059    @Override
4060    protected int getBottomPaddingOffset() {
4061        return (int) Math.max(0, mShadowDy + mShadowRadius);
4062    }
4063
4064    @Override
4065    protected int getRightPaddingOffset() {
4066        return -(getCompoundPaddingRight() - mPaddingRight) +
4067                (int) Math.max(0, mShadowDx + mShadowRadius);
4068    }
4069
4070    @Override
4071    protected boolean verifyDrawable(Drawable who) {
4072        final boolean verified = super.verifyDrawable(who);
4073        if (!verified && mDrawables != null) {
4074            return who == mDrawables.mDrawableLeft || who == mDrawables.mDrawableTop ||
4075                    who == mDrawables.mDrawableRight || who == mDrawables.mDrawableBottom;
4076        }
4077        return verified;
4078    }
4079
4080    @Override
4081    public void jumpDrawablesToCurrentState() {
4082        super.jumpDrawablesToCurrentState();
4083        if (mDrawables != null) {
4084            if (mDrawables.mDrawableLeft != null) {
4085                mDrawables.mDrawableLeft.jumpToCurrentState();
4086            }
4087            if (mDrawables.mDrawableTop != null) {
4088                mDrawables.mDrawableTop.jumpToCurrentState();
4089            }
4090            if (mDrawables.mDrawableRight != null) {
4091                mDrawables.mDrawableRight.jumpToCurrentState();
4092            }
4093            if (mDrawables.mDrawableBottom != null) {
4094                mDrawables.mDrawableBottom.jumpToCurrentState();
4095            }
4096        }
4097    }
4098
4099    @Override
4100    public void invalidateDrawable(Drawable drawable) {
4101        if (verifyDrawable(drawable)) {
4102            final Rect dirty = drawable.getBounds();
4103            int scrollX = mScrollX;
4104            int scrollY = mScrollY;
4105
4106            // IMPORTANT: The coordinates below are based on the coordinates computed
4107            // for each compound drawable in onDraw(). Make sure to update each section
4108            // accordingly.
4109            final TextView.Drawables drawables = mDrawables;
4110            if (drawables != null) {
4111                if (drawable == drawables.mDrawableLeft) {
4112                    final int compoundPaddingTop = getCompoundPaddingTop();
4113                    final int compoundPaddingBottom = getCompoundPaddingBottom();
4114                    final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
4115
4116                    scrollX += mPaddingLeft;
4117                    scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightLeft) / 2;
4118                } else if (drawable == drawables.mDrawableRight) {
4119                    final int compoundPaddingTop = getCompoundPaddingTop();
4120                    final int compoundPaddingBottom = getCompoundPaddingBottom();
4121                    final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
4122
4123                    scrollX += (mRight - mLeft - mPaddingRight - drawables.mDrawableSizeRight);
4124                    scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightRight) / 2;
4125                } else if (drawable == drawables.mDrawableTop) {
4126                    final int compoundPaddingLeft = getCompoundPaddingLeft();
4127                    final int compoundPaddingRight = getCompoundPaddingRight();
4128                    final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
4129
4130                    scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthTop) / 2;
4131                    scrollY += mPaddingTop;
4132                } else if (drawable == drawables.mDrawableBottom) {
4133                    final int compoundPaddingLeft = getCompoundPaddingLeft();
4134                    final int compoundPaddingRight = getCompoundPaddingRight();
4135                    final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
4136
4137                    scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthBottom) / 2;
4138                    scrollY += (mBottom - mTop - mPaddingBottom - drawables.mDrawableSizeBottom);
4139                }
4140            }
4141
4142            invalidate(dirty.left + scrollX, dirty.top + scrollY,
4143                    dirty.right + scrollX, dirty.bottom + scrollY);
4144        }
4145    }
4146
4147    @Override
4148    public boolean isLayoutRtl(Drawable who) {
4149        if (who == null) return false;
4150        if (mDrawables != null) {
4151            final Drawables drawables = mDrawables;
4152            if (who == drawables.mDrawableLeft || who == drawables.mDrawableRight ||
4153                who == drawables.mDrawableTop || who == drawables.mDrawableBottom) {
4154                return isLayoutRtl();
4155            }
4156        }
4157        return super.isLayoutRtl(who);
4158    }
4159
4160    @Override
4161    protected boolean onSetAlpha(int alpha) {
4162        // Alpha is supported if and only if the drawing can be done in one pass.
4163        // TODO text with spans with a background color currently do not respect this alpha.
4164        if (getBackground() == null) {
4165            mCurrentAlpha = alpha;
4166            final Drawables dr = mDrawables;
4167            if (dr != null) {
4168                if (dr.mDrawableLeft != null) dr.mDrawableLeft.mutate().setAlpha(alpha);
4169                if (dr.mDrawableTop != null) dr.mDrawableTop.mutate().setAlpha(alpha);
4170                if (dr.mDrawableRight != null) dr.mDrawableRight.mutate().setAlpha(alpha);
4171                if (dr.mDrawableBottom != null) dr.mDrawableBottom.mutate().setAlpha(alpha);
4172            }
4173            return true;
4174        }
4175
4176        mCurrentAlpha = 255;
4177        return false;
4178    }
4179
4180    /**
4181     * When a TextView is used to display a useful piece of information to the user (such as a
4182     * contact's address), it should be made selectable, so that the user can select and copy this
4183     * content.
4184     *
4185     * Use {@link #setTextIsSelectable(boolean)} or the
4186     * {@link android.R.styleable#TextView_textIsSelectable} XML attribute to make this TextView
4187     * selectable (text is not selectable by default).
4188     *
4189     * Note that the content of an EditText is always selectable.
4190     *
4191     * @return True if the text displayed in this TextView can be selected by the user.
4192     *
4193     * @attr ref android.R.styleable#TextView_textIsSelectable
4194     */
4195    public boolean isTextSelectable() {
4196        return mTextIsSelectable;
4197    }
4198
4199    /**
4200     * Sets whether or not (default) the content of this view is selectable by the user.
4201     *
4202     * Note that this methods affect the {@link #setFocusable(boolean)},
4203     * {@link #setFocusableInTouchMode(boolean)} {@link #setClickable(boolean)} and
4204     * {@link #setLongClickable(boolean)} states and you may want to restore these if they were
4205     * customized.
4206     *
4207     * See {@link #isTextSelectable} for details.
4208     *
4209     * @param selectable Whether or not the content of this TextView should be selectable.
4210     */
4211    public void setTextIsSelectable(boolean selectable) {
4212        if (mTextIsSelectable == selectable) return;
4213
4214        mTextIsSelectable = selectable;
4215
4216        setFocusableInTouchMode(selectable);
4217        setFocusable(selectable);
4218        setClickable(selectable);
4219        setLongClickable(selectable);
4220
4221        // mInputType is already EditorInfo.TYPE_NULL and mInput is null;
4222
4223        setMovementMethod(selectable ? ArrowKeyMovementMethod.getInstance() : null);
4224        setText(getText(), selectable ? BufferType.SPANNABLE : BufferType.NORMAL);
4225
4226        // Called by setText above, but safer in case of future code changes
4227        prepareCursorControllers();
4228    }
4229
4230    @Override
4231    protected int[] onCreateDrawableState(int extraSpace) {
4232        final int[] drawableState;
4233
4234        if (mSingleLine) {
4235            drawableState = super.onCreateDrawableState(extraSpace);
4236        } else {
4237            drawableState = super.onCreateDrawableState(extraSpace + 1);
4238            mergeDrawableStates(drawableState, MULTILINE_STATE_SET);
4239        }
4240
4241        if (mTextIsSelectable) {
4242            // Disable pressed state, which was introduced when TextView was made clickable.
4243            // Prevents text color change.
4244            // setClickable(false) would have a similar effect, but it also disables focus changes
4245            // and long press actions, which are both needed by text selection.
4246            final int length = drawableState.length;
4247            for (int i = 0; i < length; i++) {
4248                if (drawableState[i] == R.attr.state_pressed) {
4249                    final int[] nonPressedState = new int[length - 1];
4250                    System.arraycopy(drawableState, 0, nonPressedState, 0, i);
4251                    System.arraycopy(drawableState, i + 1, nonPressedState, i, length - i - 1);
4252                    return nonPressedState;
4253                }
4254            }
4255        }
4256
4257        return drawableState;
4258    }
4259
4260    @Override
4261    protected void onDraw(Canvas canvas) {
4262        if (mPreDrawState == PREDRAW_DONE) {
4263            final ViewTreeObserver observer = getViewTreeObserver();
4264            observer.removeOnPreDrawListener(this);
4265            mPreDrawState = PREDRAW_NOT_REGISTERED;
4266        }
4267
4268        if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return;
4269
4270        restartMarqueeIfNeeded();
4271
4272        // Draw the background for this view
4273        super.onDraw(canvas);
4274
4275        final int compoundPaddingLeft = getCompoundPaddingLeft();
4276        final int compoundPaddingTop = getCompoundPaddingTop();
4277        final int compoundPaddingRight = getCompoundPaddingRight();
4278        final int compoundPaddingBottom = getCompoundPaddingBottom();
4279        final int scrollX = mScrollX;
4280        final int scrollY = mScrollY;
4281        final int right = mRight;
4282        final int left = mLeft;
4283        final int bottom = mBottom;
4284        final int top = mTop;
4285
4286        final Drawables dr = mDrawables;
4287        if (dr != null) {
4288            /*
4289             * Compound, not extended, because the icon is not clipped
4290             * if the text height is smaller.
4291             */
4292
4293            int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
4294            int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;
4295
4296            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4297            // Make sure to update invalidateDrawable() when changing this code.
4298            if (dr.mDrawableLeft != null) {
4299                canvas.save();
4300                canvas.translate(scrollX + mPaddingLeft,
4301                                 scrollY + compoundPaddingTop +
4302                                 (vspace - dr.mDrawableHeightLeft) / 2);
4303                dr.mDrawableLeft.draw(canvas);
4304                canvas.restore();
4305            }
4306
4307            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4308            // Make sure to update invalidateDrawable() when changing this code.
4309            if (dr.mDrawableRight != null) {
4310                canvas.save();
4311                canvas.translate(scrollX + right - left - mPaddingRight - dr.mDrawableSizeRight,
4312                         scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2);
4313                dr.mDrawableRight.draw(canvas);
4314                canvas.restore();
4315            }
4316
4317            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4318            // Make sure to update invalidateDrawable() when changing this code.
4319            if (dr.mDrawableTop != null) {
4320                canvas.save();
4321                canvas.translate(scrollX + compoundPaddingLeft + (hspace - dr.mDrawableWidthTop) / 2,
4322                        scrollY + mPaddingTop);
4323                dr.mDrawableTop.draw(canvas);
4324                canvas.restore();
4325            }
4326
4327            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4328            // Make sure to update invalidateDrawable() when changing this code.
4329            if (dr.mDrawableBottom != null) {
4330                canvas.save();
4331                canvas.translate(scrollX + compoundPaddingLeft +
4332                        (hspace - dr.mDrawableWidthBottom) / 2,
4333                         scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom);
4334                dr.mDrawableBottom.draw(canvas);
4335                canvas.restore();
4336            }
4337        }
4338
4339        int color = mCurTextColor;
4340
4341        if (mLayout == null) {
4342            assumeLayout();
4343        }
4344
4345        Layout layout = mLayout;
4346        int cursorcolor = color;
4347
4348        if (mHint != null && mText.length() == 0) {
4349            if (mHintTextColor != null) {
4350                color = mCurHintTextColor;
4351            }
4352
4353            layout = mHintLayout;
4354        }
4355
4356        mTextPaint.setColor(color);
4357        if (mCurrentAlpha != 255) {
4358            // If set, the alpha will override the color's alpha. Multiply the alphas.
4359            mTextPaint.setAlpha((mCurrentAlpha * Color.alpha(color)) / 255);
4360        }
4361        mTextPaint.drawableState = getDrawableState();
4362
4363        canvas.save();
4364        /*  Would be faster if we didn't have to do this. Can we chop the
4365            (displayable) text so that we don't need to do this ever?
4366        */
4367
4368        int extendedPaddingTop = getExtendedPaddingTop();
4369        int extendedPaddingBottom = getExtendedPaddingBottom();
4370
4371        float clipLeft = compoundPaddingLeft + scrollX;
4372        float clipTop = extendedPaddingTop + scrollY;
4373        float clipRight = right - left - compoundPaddingRight + scrollX;
4374        float clipBottom = bottom - top - extendedPaddingBottom + scrollY;
4375
4376        if (mShadowRadius != 0) {
4377            clipLeft += Math.min(0, mShadowDx - mShadowRadius);
4378            clipRight += Math.max(0, mShadowDx + mShadowRadius);
4379
4380            clipTop += Math.min(0, mShadowDy - mShadowRadius);
4381            clipBottom += Math.max(0, mShadowDy + mShadowRadius);
4382        }
4383
4384        canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom);
4385
4386        int voffsetText = 0;
4387        int voffsetCursor = 0;
4388
4389        // translate in by our padding
4390        {
4391            /* shortcircuit calling getVerticaOffset() */
4392            if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
4393                voffsetText = getVerticalOffset(false);
4394                voffsetCursor = getVerticalOffset(true);
4395            }
4396            canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText);
4397        }
4398
4399        final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, isLayoutRtl());
4400        if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
4401            if (!mSingleLine && getLineCount() == 1 && canMarquee() &&
4402                    (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
4403                canvas.translate(mLayout.getLineRight(0) - (mRight - mLeft -
4404                        getCompoundPaddingLeft() - getCompoundPaddingRight()), 0.0f);
4405            }
4406
4407            if (mMarquee != null && mMarquee.isRunning()) {
4408                canvas.translate(-mMarquee.mScroll, 0.0f);
4409            }
4410        }
4411
4412        Path highlight = null;
4413        int selStart = -1, selEnd = -1;
4414        boolean drawCursor = false;
4415
4416        //  If there is no movement method, then there can be no selection.
4417        //  Check that first and attempt to skip everything having to do with
4418        //  the cursor.
4419        //  XXX This is not strictly true -- a program could set the
4420        //  selection manually if it really wanted to.
4421        if (mMovement != null && (isFocused() || isPressed())) {
4422            selStart = getSelectionStart();
4423            selEnd = getSelectionEnd();
4424
4425            if ((isCursorVisible() || mTextIsSelectable) && selStart >= 0 && isEnabled()) {
4426                if (mHighlightPath == null)
4427                    mHighlightPath = new Path();
4428
4429                if (selStart == selEnd) {
4430                    if (!mTextIsSelectable &&
4431                            (SystemClock.uptimeMillis() - mShowCursor) % (2 * BLINK) < BLINK) {
4432                        if (mHighlightPathBogus) {
4433                            mHighlightPath.reset();
4434                            mLayout.getCursorPath(selStart, mHighlightPath, mText);
4435                            updateCursorsPositions();
4436                            mHighlightPathBogus = false;
4437                        }
4438
4439                        // XXX should pass to skin instead of drawing directly
4440                        mHighlightPaint.setColor(cursorcolor);
4441                        if (mCurrentAlpha != 255) {
4442                            mHighlightPaint.setAlpha(
4443                                    (mCurrentAlpha * Color.alpha(cursorcolor)) / 255);
4444                        }
4445                        mHighlightPaint.setStyle(Paint.Style.STROKE);
4446                        highlight = mHighlightPath;
4447                        drawCursor = mCursorCount > 0;
4448                    }
4449                } else {
4450                    if (mHighlightPathBogus) {
4451                        mHighlightPath.reset();
4452                        mLayout.getSelectionPath(selStart, selEnd, mHighlightPath);
4453                        mHighlightPathBogus = false;
4454                    }
4455
4456                    // XXX should pass to skin instead of drawing directly
4457                    mHighlightPaint.setColor(mHighlightColor);
4458                    if (mCurrentAlpha != 255) {
4459                        mHighlightPaint.setAlpha(
4460                                (mCurrentAlpha * Color.alpha(mHighlightColor)) / 255);
4461                    }
4462                    mHighlightPaint.setStyle(Paint.Style.FILL);
4463
4464                    highlight = mHighlightPath;
4465                }
4466            }
4467        }
4468
4469        /*  Comment out until we decide what to do about animations
4470        boolean isLinearTextOn = false;
4471        if (currentTransformation != null) {
4472            isLinearTextOn = mTextPaint.isLinearTextOn();
4473            Matrix m = currentTransformation.getMatrix();
4474            if (!m.isIdentity()) {
4475                // mTextPaint.setLinearTextOn(true);
4476            }
4477        }
4478        */
4479
4480        final InputMethodState ims = mInputMethodState;
4481        final int cursorOffsetVertical = voffsetCursor - voffsetText;
4482        if (ims != null && ims.mBatchEditNesting == 0) {
4483            InputMethodManager imm = InputMethodManager.peekInstance();
4484            if (imm != null) {
4485                if (imm.isActive(this)) {
4486                    boolean reported = false;
4487                    if (ims.mContentChanged || ims.mSelectionModeChanged) {
4488                        // We are in extract mode and the content has changed
4489                        // in some way... just report complete new text to the
4490                        // input method.
4491                        reported = reportExtractedText();
4492                    }
4493                    if (!reported && highlight != null) {
4494                        int candStart = -1;
4495                        int candEnd = -1;
4496                        if (mText instanceof Spannable) {
4497                            Spannable sp = (Spannable)mText;
4498                            candStart = EditableInputConnection.getComposingSpanStart(sp);
4499                            candEnd = EditableInputConnection.getComposingSpanEnd(sp);
4500                        }
4501                        imm.updateSelection(this, selStart, selEnd, candStart, candEnd);
4502                    }
4503                }
4504
4505                if (imm.isWatchingCursor(this) && highlight != null) {
4506                    highlight.computeBounds(ims.mTmpRectF, true);
4507                    ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0;
4508
4509                    canvas.getMatrix().mapPoints(ims.mTmpOffset);
4510                    ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]);
4511
4512                    ims.mTmpRectF.offset(0, cursorOffsetVertical);
4513
4514                    ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5),
4515                            (int)(ims.mTmpRectF.top + 0.5),
4516                            (int)(ims.mTmpRectF.right + 0.5),
4517                            (int)(ims.mTmpRectF.bottom + 0.5));
4518
4519                    imm.updateCursor(this,
4520                            ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top,
4521                            ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom);
4522                }
4523            }
4524        }
4525
4526        if (mCorrectionHighlighter != null) {
4527            mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
4528        }
4529
4530        if (drawCursor) {
4531            drawCursor(canvas, cursorOffsetVertical);
4532            // Rely on the drawable entirely, do not draw the cursor line.
4533            // Has to be done after the IMM related code above which relies on the highlight.
4534            highlight = null;
4535        }
4536
4537        layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
4538
4539        if (mMarquee != null && mMarquee.shouldDrawGhost()) {
4540            canvas.translate((int) mMarquee.getGhostOffset(), 0.0f);
4541            layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
4542        }
4543
4544        /*  Comment out until we decide what to do about animations
4545        if (currentTransformation != null) {
4546            mTextPaint.setLinearTextOn(isLinearTextOn);
4547        }
4548        */
4549
4550        canvas.restore();
4551    }
4552
4553    private void updateCursorsPositions() {
4554        if (mCursorDrawableRes == 0) {
4555            mCursorCount = 0;
4556            return;
4557        }
4558
4559        final int offset = getSelectionStart();
4560        final int line = mLayout.getLineForOffset(offset);
4561        final int top = mLayout.getLineTop(line);
4562        final int bottom = mLayout.getLineTop(line + 1);
4563
4564        mCursorCount = mLayout.isLevelBoundary(offset) ? 2 : 1;
4565
4566        int middle = bottom;
4567        if (mCursorCount == 2) {
4568            // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)}
4569            middle = (top + bottom) >> 1;
4570        }
4571
4572        updateCursorPosition(0, top, middle, mLayout.getPrimaryHorizontal(offset));
4573
4574        if (mCursorCount == 2) {
4575            updateCursorPosition(1, middle, bottom, mLayout.getSecondaryHorizontal(offset));
4576        }
4577    }
4578
4579    private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
4580        if (mCursorDrawable[cursorIndex] == null)
4581            mCursorDrawable[cursorIndex] = mContext.getResources().getDrawable(mCursorDrawableRes);
4582
4583        if (mTempRect == null) mTempRect = new Rect();
4584
4585        mCursorDrawable[cursorIndex].getPadding(mTempRect);
4586        final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth();
4587        horizontal = Math.max(0.5f, horizontal - 0.5f);
4588        final int left = (int) (horizontal) - mTempRect.left;
4589        mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width,
4590                bottom + mTempRect.bottom);
4591    }
4592
4593    private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
4594        final boolean translate = cursorOffsetVertical != 0;
4595        if (translate) canvas.translate(0, cursorOffsetVertical);
4596        for (int i = 0; i < mCursorCount; i++) {
4597            mCursorDrawable[i].draw(canvas);
4598        }
4599        if (translate) canvas.translate(0, -cursorOffsetVertical);
4600    }
4601
4602    @Override
4603    public void getFocusedRect(Rect r) {
4604        if (mLayout == null) {
4605            super.getFocusedRect(r);
4606            return;
4607        }
4608
4609        int sel = getSelectionEnd();
4610        if (sel < 0) {
4611            super.getFocusedRect(r);
4612            return;
4613        }
4614
4615        int line = mLayout.getLineForOffset(sel);
4616        r.top = mLayout.getLineTop(line);
4617        r.bottom = mLayout.getLineBottom(line);
4618
4619        r.left = (int) mLayout.getPrimaryHorizontal(sel);
4620        r.right = r.left + 1;
4621
4622        // Adjust for padding and gravity.
4623        int paddingLeft = getCompoundPaddingLeft();
4624        int paddingTop = getExtendedPaddingTop();
4625        if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
4626            paddingTop += getVerticalOffset(false);
4627        }
4628        r.offset(paddingLeft, paddingTop);
4629    }
4630
4631    /**
4632     * Return the number of lines of text, or 0 if the internal Layout has not
4633     * been built.
4634     */
4635    public int getLineCount() {
4636        return mLayout != null ? mLayout.getLineCount() : 0;
4637    }
4638
4639    /**
4640     * Return the baseline for the specified line (0...getLineCount() - 1)
4641     * If bounds is not null, return the top, left, right, bottom extents
4642     * of the specified line in it. If the internal Layout has not been built,
4643     * return 0 and set bounds to (0, 0, 0, 0)
4644     * @param line which line to examine (0..getLineCount() - 1)
4645     * @param bounds Optional. If not null, it returns the extent of the line
4646     * @return the Y-coordinate of the baseline
4647     */
4648    public int getLineBounds(int line, Rect bounds) {
4649        if (mLayout == null) {
4650            if (bounds != null) {
4651                bounds.set(0, 0, 0, 0);
4652            }
4653            return 0;
4654        }
4655        else {
4656            int baseline = mLayout.getLineBounds(line, bounds);
4657
4658            int voffset = getExtendedPaddingTop();
4659            if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
4660                voffset += getVerticalOffset(true);
4661            }
4662            if (bounds != null) {
4663                bounds.offset(getCompoundPaddingLeft(), voffset);
4664            }
4665            return baseline + voffset;
4666        }
4667    }
4668
4669    @Override
4670    public int getBaseline() {
4671        if (mLayout == null) {
4672            return super.getBaseline();
4673        }
4674
4675        int voffset = 0;
4676        if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
4677            voffset = getVerticalOffset(true);
4678        }
4679
4680        return getExtendedPaddingTop() + voffset + mLayout.getLineBaseline(0);
4681    }
4682
4683    @Override
4684    public boolean onKeyDown(int keyCode, KeyEvent event) {
4685        int which = doKeyDown(keyCode, event, null);
4686        if (which == 0) {
4687            // Go through default dispatching.
4688            return super.onKeyDown(keyCode, event);
4689        }
4690
4691        return true;
4692    }
4693
4694    @Override
4695    public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
4696        KeyEvent down = KeyEvent.changeAction(event, KeyEvent.ACTION_DOWN);
4697
4698        int which = doKeyDown(keyCode, down, event);
4699        if (which == 0) {
4700            // Go through default dispatching.
4701            return super.onKeyMultiple(keyCode, repeatCount, event);
4702        }
4703        if (which == -1) {
4704            // Consumed the whole thing.
4705            return true;
4706        }
4707
4708        repeatCount--;
4709
4710        // We are going to dispatch the remaining events to either the input
4711        // or movement method.  To do this, we will just send a repeated stream
4712        // of down and up events until we have done the complete repeatCount.
4713        // It would be nice if those interfaces had an onKeyMultiple() method,
4714        // but adding that is a more complicated change.
4715        KeyEvent up = KeyEvent.changeAction(event, KeyEvent.ACTION_UP);
4716        if (which == 1) {
4717            mInput.onKeyUp(this, (Editable)mText, keyCode, up);
4718            while (--repeatCount > 0) {
4719                mInput.onKeyDown(this, (Editable)mText, keyCode, down);
4720                mInput.onKeyUp(this, (Editable)mText, keyCode, up);
4721            }
4722            hideErrorIfUnchanged();
4723
4724        } else if (which == 2) {
4725            mMovement.onKeyUp(this, (Spannable)mText, keyCode, up);
4726            while (--repeatCount > 0) {
4727                mMovement.onKeyDown(this, (Spannable)mText, keyCode, down);
4728                mMovement.onKeyUp(this, (Spannable)mText, keyCode, up);
4729            }
4730        }
4731
4732        return true;
4733    }
4734
4735    /**
4736     * Returns true if pressing ENTER in this field advances focus instead
4737     * of inserting the character.  This is true mostly in single-line fields,
4738     * but also in mail addresses and subjects which will display on multiple
4739     * lines but where it doesn't make sense to insert newlines.
4740     */
4741    private boolean shouldAdvanceFocusOnEnter() {
4742        if (mInput == null) {
4743            return false;
4744        }
4745
4746        if (mSingleLine) {
4747            return true;
4748        }
4749
4750        if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
4751            int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
4752            if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
4753                    || variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT) {
4754                return true;
4755            }
4756        }
4757
4758        return false;
4759    }
4760
4761    /**
4762     * Returns true if pressing TAB in this field advances focus instead
4763     * of inserting the character.  Insert tabs only in multi-line editors.
4764     */
4765    private boolean shouldAdvanceFocusOnTab() {
4766        if (mInput != null && !mSingleLine) {
4767            if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
4768                int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
4769                if (variation == EditorInfo.TYPE_TEXT_FLAG_IME_MULTI_LINE
4770                        || variation == EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) {
4771                    return false;
4772                }
4773            }
4774        }
4775        return true;
4776    }
4777
4778    private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) {
4779        if (!isEnabled()) {
4780            return 0;
4781        }
4782
4783        switch (keyCode) {
4784            case KeyEvent.KEYCODE_ENTER:
4785                mEnterKeyIsDown = true;
4786                if (event.hasNoModifiers()) {
4787                    // When mInputContentType is set, we know that we are
4788                    // running in a "modern" cupcake environment, so don't need
4789                    // to worry about the application trying to capture
4790                    // enter key events.
4791                    if (mInputContentType != null) {
4792                        // If there is an action listener, given them a
4793                        // chance to consume the event.
4794                        if (mInputContentType.onEditorActionListener != null &&
4795                                mInputContentType.onEditorActionListener.onEditorAction(
4796                                this, EditorInfo.IME_NULL, event)) {
4797                            mInputContentType.enterDown = true;
4798                            // We are consuming the enter key for them.
4799                            return -1;
4800                        }
4801                    }
4802
4803                    // If our editor should move focus when enter is pressed, or
4804                    // this is a generated event from an IME action button, then
4805                    // don't let it be inserted into the text.
4806                    if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
4807                            || shouldAdvanceFocusOnEnter()) {
4808                        if (mOnClickListener != null) {
4809                            return 0;
4810                        }
4811                        return -1;
4812                    }
4813                }
4814                break;
4815
4816            case KeyEvent.KEYCODE_DPAD_CENTER:
4817                mDPadCenterIsDown = true;
4818                if (event.hasNoModifiers()) {
4819                    if (shouldAdvanceFocusOnEnter()) {
4820                        return 0;
4821                    }
4822                }
4823                break;
4824
4825            case KeyEvent.KEYCODE_TAB:
4826                if (event.hasNoModifiers() || event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
4827                    if (shouldAdvanceFocusOnTab()) {
4828                        return 0;
4829                    }
4830                }
4831                break;
4832
4833                // Has to be done on key down (and not on key up) to correctly be intercepted.
4834            case KeyEvent.KEYCODE_BACK:
4835                if (mSelectionActionMode != null) {
4836                    stopSelectionActionMode();
4837                    return -1;
4838                }
4839                break;
4840        }
4841
4842        if (mInput != null) {
4843            resetErrorChangedFlag();
4844
4845            boolean doDown = true;
4846            if (otherEvent != null) {
4847                try {
4848                    beginBatchEdit();
4849                    final boolean handled = mInput.onKeyOther(this, (Editable) mText, otherEvent);
4850                    hideErrorIfUnchanged();
4851                    doDown = false;
4852                    if (handled) {
4853                        return -1;
4854                    }
4855                } catch (AbstractMethodError e) {
4856                    // onKeyOther was added after 1.0, so if it isn't
4857                    // implemented we need to try to dispatch as a regular down.
4858                } finally {
4859                    endBatchEdit();
4860                }
4861            }
4862
4863            if (doDown) {
4864                beginBatchEdit();
4865                final boolean handled = mInput.onKeyDown(this, (Editable) mText, keyCode, event);
4866                endBatchEdit();
4867                hideErrorIfUnchanged();
4868                if (handled) return 1;
4869            }
4870        }
4871
4872        // bug 650865: sometimes we get a key event before a layout.
4873        // don't try to move around if we don't know the layout.
4874
4875        if (mMovement != null && mLayout != null) {
4876            boolean doDown = true;
4877            if (otherEvent != null) {
4878                try {
4879                    boolean handled = mMovement.onKeyOther(this, (Spannable) mText,
4880                            otherEvent);
4881                    doDown = false;
4882                    if (handled) {
4883                        return -1;
4884                    }
4885                } catch (AbstractMethodError e) {
4886                    // onKeyOther was added after 1.0, so if it isn't
4887                    // implemented we need to try to dispatch as a regular down.
4888                }
4889            }
4890            if (doDown) {
4891                if (mMovement.onKeyDown(this, (Spannable)mText, keyCode, event))
4892                    return 2;
4893            }
4894        }
4895
4896        return 0;
4897    }
4898
4899    /**
4900     * Resets the mErrorWasChanged flag, so that future calls to {@link #setError(CharSequence)}
4901     * can be recorded.
4902     * @hide
4903     */
4904    public void resetErrorChangedFlag() {
4905        /*
4906         * Keep track of what the error was before doing the input
4907         * so that if an input filter changed the error, we leave
4908         * that error showing.  Otherwise, we take down whatever
4909         * error was showing when the user types something.
4910         */
4911        mErrorWasChanged = false;
4912    }
4913
4914    /**
4915     * @hide
4916     */
4917    public void hideErrorIfUnchanged() {
4918        if (mError != null && !mErrorWasChanged) {
4919            setError(null, null);
4920        }
4921    }
4922
4923    @Override
4924    public boolean onKeyUp(int keyCode, KeyEvent event) {
4925        if (!isEnabled()) {
4926            return super.onKeyUp(keyCode, event);
4927        }
4928
4929        switch (keyCode) {
4930            case KeyEvent.KEYCODE_DPAD_CENTER:
4931                mDPadCenterIsDown = false;
4932                if (event.hasNoModifiers()) {
4933                    /*
4934                     * If there is a click listener, just call through to
4935                     * super, which will invoke it.
4936                     *
4937                     * If there isn't a click listener, try to show the soft
4938                     * input method.  (It will also
4939                     * call performClick(), but that won't do anything in
4940                     * this case.)
4941                     */
4942                    if (mOnClickListener == null) {
4943                        if (mMovement != null && mText instanceof Editable
4944                                && mLayout != null && onCheckIsTextEditor()) {
4945                            InputMethodManager imm = InputMethodManager.peekInstance();
4946                            if (imm != null) imm.showSoftInput(this, 0);
4947                        }
4948                    }
4949                }
4950                return super.onKeyUp(keyCode, event);
4951
4952            case KeyEvent.KEYCODE_ENTER:
4953                mEnterKeyIsDown = false;
4954                if (event.hasNoModifiers()) {
4955                    if (mInputContentType != null
4956                            && mInputContentType.onEditorActionListener != null
4957                            && mInputContentType.enterDown) {
4958                        mInputContentType.enterDown = false;
4959                        if (mInputContentType.onEditorActionListener.onEditorAction(
4960                                this, EditorInfo.IME_NULL, event)) {
4961                            return true;
4962                        }
4963                    }
4964
4965                    if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
4966                            || shouldAdvanceFocusOnEnter()) {
4967                        /*
4968                         * If there is a click listener, just call through to
4969                         * super, which will invoke it.
4970                         *
4971                         * If there isn't a click listener, try to advance focus,
4972                         * but still call through to super, which will reset the
4973                         * pressed state and longpress state.  (It will also
4974                         * call performClick(), but that won't do anything in
4975                         * this case.)
4976                         */
4977                        if (mOnClickListener == null) {
4978                            View v = focusSearch(FOCUS_DOWN);
4979
4980                            if (v != null) {
4981                                if (!v.requestFocus(FOCUS_DOWN)) {
4982                                    throw new IllegalStateException(
4983                                            "focus search returned a view " +
4984                                            "that wasn't able to take focus!");
4985                                }
4986
4987                                /*
4988                                 * Return true because we handled the key; super
4989                                 * will return false because there was no click
4990                                 * listener.
4991                                 */
4992                                super.onKeyUp(keyCode, event);
4993                                return true;
4994                            } else if ((event.getFlags()
4995                                    & KeyEvent.FLAG_EDITOR_ACTION) != 0) {
4996                                // No target for next focus, but make sure the IME
4997                                // if this came from it.
4998                                InputMethodManager imm = InputMethodManager.peekInstance();
4999                                if (imm != null && imm.isActive(this)) {
5000                                    imm.hideSoftInputFromWindow(getWindowToken(), 0);
5001                                }
5002                            }
5003                        }
5004                    }
5005                    return super.onKeyUp(keyCode, event);
5006                }
5007                break;
5008        }
5009
5010        if (mInput != null)
5011            if (mInput.onKeyUp(this, (Editable) mText, keyCode, event))
5012                return true;
5013
5014        if (mMovement != null && mLayout != null)
5015            if (mMovement.onKeyUp(this, (Spannable) mText, keyCode, event))
5016                return true;
5017
5018        return super.onKeyUp(keyCode, event);
5019    }
5020
5021    @Override public boolean onCheckIsTextEditor() {
5022        return mInputType != EditorInfo.TYPE_NULL;
5023    }
5024
5025    @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
5026        if (onCheckIsTextEditor() && isEnabled()) {
5027            if (mInputMethodState == null) {
5028                mInputMethodState = new InputMethodState();
5029            }
5030            outAttrs.inputType = mInputType;
5031            if (mInputContentType != null) {
5032                outAttrs.imeOptions = mInputContentType.imeOptions;
5033                outAttrs.privateImeOptions = mInputContentType.privateImeOptions;
5034                outAttrs.actionLabel = mInputContentType.imeActionLabel;
5035                outAttrs.actionId = mInputContentType.imeActionId;
5036                outAttrs.extras = mInputContentType.extras;
5037            } else {
5038                outAttrs.imeOptions = EditorInfo.IME_NULL;
5039            }
5040            if (focusSearch(FOCUS_DOWN) != null) {
5041                outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT;
5042            }
5043            if (focusSearch(FOCUS_UP) != null) {
5044                outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS;
5045            }
5046            if ((outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION)
5047                    == EditorInfo.IME_ACTION_UNSPECIFIED) {
5048                if ((outAttrs.imeOptions&EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0) {
5049                    // An action has not been set, but the enter key will move to
5050                    // the next focus, so set the action to that.
5051                    outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
5052                } else {
5053                    // An action has not been set, and there is no focus to move
5054                    // to, so let's just supply a "done" action.
5055                    outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
5056                }
5057                if (!shouldAdvanceFocusOnEnter()) {
5058                    outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
5059                }
5060            }
5061            if (isMultilineInputType(outAttrs.inputType)) {
5062                // Multi-line text editors should always show an enter key.
5063                outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
5064            }
5065            outAttrs.hintText = mHint;
5066            if (mText instanceof Editable) {
5067                InputConnection ic = new EditableInputConnection(this);
5068                outAttrs.initialSelStart = getSelectionStart();
5069                outAttrs.initialSelEnd = getSelectionEnd();
5070                outAttrs.initialCapsMode = ic.getCursorCapsMode(mInputType);
5071                return ic;
5072            }
5073        }
5074        return null;
5075    }
5076
5077    /**
5078     * If this TextView contains editable content, extract a portion of it
5079     * based on the information in <var>request</var> in to <var>outText</var>.
5080     * @return Returns true if the text was successfully extracted, else false.
5081     */
5082    public boolean extractText(ExtractedTextRequest request,
5083            ExtractedText outText) {
5084        return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
5085                EXTRACT_UNKNOWN, outText);
5086    }
5087
5088    static final int EXTRACT_NOTHING = -2;
5089    static final int EXTRACT_UNKNOWN = -1;
5090
5091    boolean extractTextInternal(ExtractedTextRequest request,
5092            int partialStartOffset, int partialEndOffset, int delta,
5093            ExtractedText outText) {
5094        final CharSequence content = mText;
5095        if (content != null) {
5096            if (partialStartOffset != EXTRACT_NOTHING) {
5097                final int N = content.length();
5098                if (partialStartOffset < 0) {
5099                    outText.partialStartOffset = outText.partialEndOffset = -1;
5100                    partialStartOffset = 0;
5101                    partialEndOffset = N;
5102                } else {
5103                    // Now use the delta to determine the actual amount of text
5104                    // we need.
5105                    partialEndOffset += delta;
5106                    // Adjust offsets to ensure we contain full spans.
5107                    if (content instanceof Spanned) {
5108                        Spanned spanned = (Spanned)content;
5109                        Object[] spans = spanned.getSpans(partialStartOffset,
5110                                partialEndOffset, ParcelableSpan.class);
5111                        int i = spans.length;
5112                        while (i > 0) {
5113                            i--;
5114                            int j = spanned.getSpanStart(spans[i]);
5115                            if (j < partialStartOffset) partialStartOffset = j;
5116                            j = spanned.getSpanEnd(spans[i]);
5117                            if (j > partialEndOffset) partialEndOffset = j;
5118                        }
5119                    }
5120                    outText.partialStartOffset = partialStartOffset;
5121                    outText.partialEndOffset = partialEndOffset - delta;
5122
5123                    if (partialStartOffset > N) {
5124                        partialStartOffset = N;
5125                    } else if (partialStartOffset < 0) {
5126                        partialStartOffset = 0;
5127                    }
5128                    if (partialEndOffset > N) {
5129                        partialEndOffset = N;
5130                    } else if (partialEndOffset < 0) {
5131                        partialEndOffset = 0;
5132                    }
5133                }
5134                if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) {
5135                    outText.text = content.subSequence(partialStartOffset,
5136                            partialEndOffset);
5137                } else {
5138                    outText.text = TextUtils.substring(content, partialStartOffset,
5139                            partialEndOffset);
5140                }
5141            } else {
5142                outText.partialStartOffset = 0;
5143                outText.partialEndOffset = 0;
5144                outText.text = "";
5145            }
5146            outText.flags = 0;
5147            if (MetaKeyKeyListener.getMetaState(mText, MetaKeyKeyListener.META_SELECTING) != 0) {
5148                outText.flags |= ExtractedText.FLAG_SELECTING;
5149            }
5150            if (mSingleLine) {
5151                outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
5152            }
5153            outText.startOffset = 0;
5154            outText.selectionStart = getSelectionStart();
5155            outText.selectionEnd = getSelectionEnd();
5156            return true;
5157        }
5158        return false;
5159    }
5160
5161    boolean reportExtractedText() {
5162        final InputMethodState ims = mInputMethodState;
5163        if (ims != null) {
5164            final boolean contentChanged = ims.mContentChanged;
5165            if (contentChanged || ims.mSelectionModeChanged) {
5166                ims.mContentChanged = false;
5167                ims.mSelectionModeChanged = false;
5168                final ExtractedTextRequest req = mInputMethodState.mExtracting;
5169                if (req != null) {
5170                    InputMethodManager imm = InputMethodManager.peekInstance();
5171                    if (imm != null) {
5172                        if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Retrieving extracted start="
5173                                + ims.mChangedStart + " end=" + ims.mChangedEnd
5174                                + " delta=" + ims.mChangedDelta);
5175                        if (ims.mChangedStart < 0 && !contentChanged) {
5176                            ims.mChangedStart = EXTRACT_NOTHING;
5177                        }
5178                        if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
5179                                ims.mChangedDelta, ims.mTmpExtracted)) {
5180                            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Reporting extracted start="
5181                                    + ims.mTmpExtracted.partialStartOffset
5182                                    + " end=" + ims.mTmpExtracted.partialEndOffset
5183                                    + ": " + ims.mTmpExtracted.text);
5184                            imm.updateExtractedText(this, req.token,
5185                                    mInputMethodState.mTmpExtracted);
5186                            ims.mChangedStart = EXTRACT_UNKNOWN;
5187                            ims.mChangedEnd = EXTRACT_UNKNOWN;
5188                            ims.mChangedDelta = 0;
5189                            ims.mContentChanged = false;
5190                            return true;
5191                        }
5192                    }
5193                }
5194            }
5195        }
5196        return false;
5197    }
5198
5199    /**
5200     * This is used to remove all style-impacting spans from text before new
5201     * extracted text is being replaced into it, so that we don't have any
5202     * lingering spans applied during the replace.
5203     */
5204    static void removeParcelableSpans(Spannable spannable, int start, int end) {
5205        Object[] spans = spannable.getSpans(start, end, ParcelableSpan.class);
5206        int i = spans.length;
5207        while (i > 0) {
5208            i--;
5209            spannable.removeSpan(spans[i]);
5210        }
5211    }
5212
5213    /**
5214     * Apply to this text view the given extracted text, as previously
5215     * returned by {@link #extractText(ExtractedTextRequest, ExtractedText)}.
5216     */
5217    public void setExtractedText(ExtractedText text) {
5218        Editable content = getEditableText();
5219        if (text.text != null) {
5220            if (content == null) {
5221                setText(text.text, TextView.BufferType.EDITABLE);
5222            } else if (text.partialStartOffset < 0) {
5223                removeParcelableSpans(content, 0, content.length());
5224                content.replace(0, content.length(), text.text);
5225            } else {
5226                final int N = content.length();
5227                int start = text.partialStartOffset;
5228                if (start > N) start = N;
5229                int end = text.partialEndOffset;
5230                if (end > N) end = N;
5231                removeParcelableSpans(content, start, end);
5232                content.replace(start, end, text.text);
5233            }
5234        }
5235
5236        // Now set the selection position...  make sure it is in range, to
5237        // avoid crashes.  If this is a partial update, it is possible that
5238        // the underlying text may have changed, causing us problems here.
5239        // Also we just don't want to trust clients to do the right thing.
5240        Spannable sp = (Spannable)getText();
5241        final int N = sp.length();
5242        int start = text.selectionStart;
5243        if (start < 0) start = 0;
5244        else if (start > N) start = N;
5245        int end = text.selectionEnd;
5246        if (end < 0) end = 0;
5247        else if (end > N) end = N;
5248        Selection.setSelection(sp, start, end);
5249
5250        // Finally, update the selection mode.
5251        if ((text.flags&ExtractedText.FLAG_SELECTING) != 0) {
5252            MetaKeyKeyListener.startSelecting(this, sp);
5253        } else {
5254            MetaKeyKeyListener.stopSelecting(this, sp);
5255        }
5256    }
5257
5258    /**
5259     * @hide
5260     */
5261    public void setExtracting(ExtractedTextRequest req) {
5262        if (mInputMethodState != null) {
5263            mInputMethodState.mExtracting = req;
5264        }
5265        // This stops a possible text selection mode. Maybe not intended.
5266        hideControllers();
5267    }
5268
5269    /**
5270     * Called by the framework in response to a text completion from
5271     * the current input method, provided by it calling
5272     * {@link InputConnection#commitCompletion
5273     * InputConnection.commitCompletion()}.  The default implementation does
5274     * nothing; text views that are supporting auto-completion should override
5275     * this to do their desired behavior.
5276     *
5277     * @param text The auto complete text the user has selected.
5278     */
5279    public void onCommitCompletion(CompletionInfo text) {
5280        // intentionally empty
5281    }
5282
5283    /**
5284     * Called by the framework in response to a text auto-correction (such as fixing a typo using a
5285     * a dictionnary) from the current input method, provided by it calling
5286     * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
5287     * implementation flashes the background of the corrected word to provide feedback to the user.
5288     *
5289     * @param info The auto correct info about the text that was corrected.
5290     */
5291    public void onCommitCorrection(CorrectionInfo info) {
5292        if (mCorrectionHighlighter == null) {
5293            mCorrectionHighlighter = new CorrectionHighlighter();
5294        } else {
5295            mCorrectionHighlighter.invalidate(false);
5296        }
5297
5298        mCorrectionHighlighter.highlight(info);
5299    }
5300
5301    private class CorrectionHighlighter {
5302        private final Path mPath = new Path();
5303        private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
5304        private int mStart, mEnd;
5305        private long mFadingStartTime;
5306        private final static int FADE_OUT_DURATION = 400;
5307
5308        public CorrectionHighlighter() {
5309            mPaint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale);
5310            mPaint.setStyle(Paint.Style.FILL);
5311        }
5312
5313        public void highlight(CorrectionInfo info) {
5314            mStart = info.getOffset();
5315            mEnd = mStart + info.getNewText().length();
5316            mFadingStartTime = SystemClock.uptimeMillis();
5317
5318            if (mStart < 0 || mEnd < 0) {
5319                stopAnimation();
5320            }
5321        }
5322
5323        public void draw(Canvas canvas, int cursorOffsetVertical) {
5324            if (updatePath() && updatePaint()) {
5325                if (cursorOffsetVertical != 0) {
5326                    canvas.translate(0, cursorOffsetVertical);
5327                }
5328
5329                canvas.drawPath(mPath, mPaint);
5330
5331                if (cursorOffsetVertical != 0) {
5332                    canvas.translate(0, -cursorOffsetVertical);
5333                }
5334                invalidate(true);
5335            } else {
5336                stopAnimation();
5337                invalidate(false);
5338            }
5339        }
5340
5341        private boolean updatePaint() {
5342            final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
5343            if (duration > FADE_OUT_DURATION) return false;
5344
5345            final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
5346            final int highlightColorAlpha = Color.alpha(mHighlightColor);
5347            final int color = (mHighlightColor & 0x00FFFFFF) +
5348                    ((int) (highlightColorAlpha * coef) << 24);
5349            mPaint.setColor(color);
5350            return true;
5351        }
5352
5353        private boolean updatePath() {
5354            final Layout layout = TextView.this.mLayout;
5355            if (layout == null) return false;
5356
5357            // Update in case text is edited while the animation is run
5358            final int length = mText.length();
5359            int start = Math.min(length, mStart);
5360            int end = Math.min(length, mEnd);
5361
5362            mPath.reset();
5363            TextView.this.mLayout.getSelectionPath(start, end, mPath);
5364            return true;
5365        }
5366
5367        private void invalidate(boolean delayed) {
5368            if (TextView.this.mLayout == null) return;
5369
5370            synchronized (sTempRect) {
5371                mPath.computeBounds(sTempRect, false);
5372
5373                int left = getCompoundPaddingLeft();
5374                int top = getExtendedPaddingTop() + getVerticalOffset(true);
5375
5376                if (delayed) {
5377                    TextView.this.postInvalidateDelayed(16, // 60 Hz update
5378                            left + (int) sTempRect.left, top + (int) sTempRect.top,
5379                            left + (int) sTempRect.right, top + (int) sTempRect.bottom);
5380                } else {
5381                    TextView.this.postInvalidate((int) sTempRect.left, (int) sTempRect.top,
5382                            (int) sTempRect.right, (int) sTempRect.bottom);
5383                }
5384            }
5385        }
5386
5387        private void stopAnimation() {
5388            TextView.this.mCorrectionHighlighter = null;
5389        }
5390    }
5391
5392    public void beginBatchEdit() {
5393        mInBatchEditControllers = true;
5394        final InputMethodState ims = mInputMethodState;
5395        if (ims != null) {
5396            int nesting = ++ims.mBatchEditNesting;
5397            if (nesting == 1) {
5398                ims.mCursorChanged = false;
5399                ims.mChangedDelta = 0;
5400                if (ims.mContentChanged) {
5401                    // We already have a pending change from somewhere else,
5402                    // so turn this into a full update.
5403                    ims.mChangedStart = 0;
5404                    ims.mChangedEnd = mText.length();
5405                } else {
5406                    ims.mChangedStart = EXTRACT_UNKNOWN;
5407                    ims.mChangedEnd = EXTRACT_UNKNOWN;
5408                    ims.mContentChanged = false;
5409                }
5410                onBeginBatchEdit();
5411            }
5412        }
5413    }
5414
5415    public void endBatchEdit() {
5416        mInBatchEditControllers = false;
5417        final InputMethodState ims = mInputMethodState;
5418        if (ims != null) {
5419            int nesting = --ims.mBatchEditNesting;
5420            if (nesting == 0) {
5421                finishBatchEdit(ims);
5422            }
5423        }
5424    }
5425
5426    void ensureEndedBatchEdit() {
5427        final InputMethodState ims = mInputMethodState;
5428        if (ims != null && ims.mBatchEditNesting != 0) {
5429            ims.mBatchEditNesting = 0;
5430            finishBatchEdit(ims);
5431        }
5432    }
5433
5434    void finishBatchEdit(final InputMethodState ims) {
5435        onEndBatchEdit();
5436
5437        if (ims.mContentChanged || ims.mSelectionModeChanged) {
5438            updateAfterEdit();
5439            reportExtractedText();
5440        } else if (ims.mCursorChanged) {
5441            // Cheezy way to get us to report the current cursor location.
5442            invalidateCursor();
5443        }
5444    }
5445
5446    void updateAfterEdit() {
5447        invalidate();
5448        int curs = getSelectionStart();
5449
5450        if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
5451            registerForPreDraw();
5452        }
5453
5454        if (curs >= 0) {
5455            mHighlightPathBogus = true;
5456            makeBlink();
5457        }
5458
5459        checkForResize();
5460    }
5461
5462    /**
5463     * Called by the framework in response to a request to begin a batch
5464     * of edit operations through a call to link {@link #beginBatchEdit()}.
5465     */
5466    public void onBeginBatchEdit() {
5467        // intentionally empty
5468    }
5469
5470    /**
5471     * Called by the framework in response to a request to end a batch
5472     * of edit operations through a call to link {@link #endBatchEdit}.
5473     */
5474    public void onEndBatchEdit() {
5475        // intentionally empty
5476    }
5477
5478    /**
5479     * Called by the framework in response to a private command from the
5480     * current method, provided by it calling
5481     * {@link InputConnection#performPrivateCommand
5482     * InputConnection.performPrivateCommand()}.
5483     *
5484     * @param action The action name of the command.
5485     * @param data Any additional data for the command.  This may be null.
5486     * @return Return true if you handled the command, else false.
5487     */
5488    public boolean onPrivateIMECommand(String action, Bundle data) {
5489        return false;
5490    }
5491
5492    private void nullLayouts() {
5493        if (mLayout instanceof BoringLayout && mSavedLayout == null) {
5494            mSavedLayout = (BoringLayout) mLayout;
5495        }
5496        if (mHintLayout instanceof BoringLayout && mSavedHintLayout == null) {
5497            mSavedHintLayout = (BoringLayout) mHintLayout;
5498        }
5499
5500        mLayout = mHintLayout = null;
5501
5502        // Since it depends on the value of mLayout
5503        prepareCursorControllers();
5504    }
5505
5506    /**
5507     * Make a new Layout based on the already-measured size of the view,
5508     * on the assumption that it was measured correctly at some point.
5509     */
5510    private void assumeLayout() {
5511        int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
5512
5513        if (width < 1) {
5514            width = 0;
5515        }
5516
5517        int physicalWidth = width;
5518
5519        if (mHorizontallyScrolling) {
5520            width = VERY_WIDE;
5521        }
5522
5523        makeNewLayout(width, physicalWidth, UNKNOWN_BORING, UNKNOWN_BORING,
5524                      physicalWidth, false);
5525    }
5526
5527    /**
5528     * The width passed in is now the desired layout width,
5529     * not the full view width with padding.
5530     * {@hide}
5531     */
5532    protected void makeNewLayout(int w, int hintWidth,
5533                                 BoringLayout.Metrics boring,
5534                                 BoringLayout.Metrics hintBoring,
5535                                 int ellipsisWidth, boolean bringIntoView) {
5536        stopMarquee();
5537
5538        mHighlightPathBogus = true;
5539
5540        if (w < 0) {
5541            w = 0;
5542        }
5543        if (hintWidth < 0) {
5544            hintWidth = 0;
5545        }
5546
5547        Layout.Alignment alignment;
5548        final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, isLayoutRtl());
5549        switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
5550            case Gravity.CENTER_HORIZONTAL:
5551                alignment = Layout.Alignment.ALIGN_CENTER;
5552                break;
5553
5554            case Gravity.RIGHT:
5555                // Note, Layout resolves ALIGN_OPPOSITE to left or
5556                // right based on the paragraph direction.
5557                alignment = Layout.Alignment.ALIGN_OPPOSITE;
5558                break;
5559
5560            default:
5561                alignment = Layout.Alignment.ALIGN_NORMAL;
5562        }
5563
5564        boolean shouldEllipsize = mEllipsize != null && mInput == null;
5565
5566        if (mText instanceof Spannable) {
5567            mLayout = new DynamicLayout(mText, mTransformed, mTextPaint, w,
5568                    alignment, mSpacingMult,
5569                    mSpacingAdd, mIncludePad, mInput == null ? mEllipsize : null,
5570                    ellipsisWidth);
5571        } else {
5572            if (boring == UNKNOWN_BORING) {
5573                boring = BoringLayout.isBoring(mTransformed, mTextPaint, mBoring);
5574                if (boring != null) {
5575                    mBoring = boring;
5576                }
5577            }
5578
5579            if (boring != null) {
5580                if (boring.width <= w &&
5581                    (mEllipsize == null || boring.width <= ellipsisWidth)) {
5582                    if (mSavedLayout != null) {
5583                        mLayout = mSavedLayout.
5584                                replaceOrMake(mTransformed, mTextPaint,
5585                                w, alignment, mSpacingMult, mSpacingAdd,
5586                                boring, mIncludePad);
5587                    } else {
5588                        mLayout = BoringLayout.make(mTransformed, mTextPaint,
5589                                w, alignment, mSpacingMult, mSpacingAdd,
5590                                boring, mIncludePad);
5591                    }
5592
5593                    mSavedLayout = (BoringLayout) mLayout;
5594                } else if (shouldEllipsize && boring.width <= w) {
5595                    if (mSavedLayout != null) {
5596                        mLayout = mSavedLayout.
5597                                replaceOrMake(mTransformed, mTextPaint,
5598                                w, alignment, mSpacingMult, mSpacingAdd,
5599                                boring, mIncludePad, mEllipsize,
5600                                ellipsisWidth);
5601                    } else {
5602                        mLayout = BoringLayout.make(mTransformed, mTextPaint,
5603                                w, alignment, mSpacingMult, mSpacingAdd,
5604                                boring, mIncludePad, mEllipsize,
5605                                ellipsisWidth);
5606                    }
5607                } else if (shouldEllipsize) {
5608                    mLayout = new StaticLayout(mTransformed,
5609                                0, mTransformed.length(),
5610                                mTextPaint, w, alignment, mSpacingMult,
5611                                mSpacingAdd, mIncludePad, mEllipsize,
5612                                ellipsisWidth);
5613                } else {
5614                    mLayout = new StaticLayout(mTransformed, mTextPaint,
5615                            w, alignment, mSpacingMult, mSpacingAdd,
5616                            mIncludePad);
5617                }
5618            } else if (shouldEllipsize) {
5619                mLayout = new StaticLayout(mTransformed,
5620                            0, mTransformed.length(),
5621                            mTextPaint, w, alignment, mSpacingMult,
5622                            mSpacingAdd, mIncludePad, mEllipsize,
5623                            ellipsisWidth);
5624            } else {
5625                mLayout = new StaticLayout(mTransformed, mTextPaint,
5626                        w, alignment, mSpacingMult, mSpacingAdd,
5627                        mIncludePad);
5628            }
5629        }
5630
5631        shouldEllipsize = mEllipsize != null;
5632        mHintLayout = null;
5633
5634        if (mHint != null) {
5635            if (shouldEllipsize) hintWidth = w;
5636
5637            if (hintBoring == UNKNOWN_BORING) {
5638                hintBoring = BoringLayout.isBoring(mHint, mTextPaint,
5639                                                   mHintBoring);
5640                if (hintBoring != null) {
5641                    mHintBoring = hintBoring;
5642                }
5643            }
5644
5645            if (hintBoring != null) {
5646                if (hintBoring.width <= hintWidth &&
5647                    (!shouldEllipsize || hintBoring.width <= ellipsisWidth)) {
5648                    if (mSavedHintLayout != null) {
5649                        mHintLayout = mSavedHintLayout.
5650                                replaceOrMake(mHint, mTextPaint,
5651                                hintWidth, alignment, mSpacingMult, mSpacingAdd,
5652                                hintBoring, mIncludePad);
5653                    } else {
5654                        mHintLayout = BoringLayout.make(mHint, mTextPaint,
5655                                hintWidth, alignment, mSpacingMult, mSpacingAdd,
5656                                hintBoring, mIncludePad);
5657                    }
5658
5659                    mSavedHintLayout = (BoringLayout) mHintLayout;
5660                } else if (shouldEllipsize && hintBoring.width <= hintWidth) {
5661                    if (mSavedHintLayout != null) {
5662                        mHintLayout = mSavedHintLayout.
5663                                replaceOrMake(mHint, mTextPaint,
5664                                hintWidth, alignment, mSpacingMult, mSpacingAdd,
5665                                hintBoring, mIncludePad, mEllipsize,
5666                                ellipsisWidth);
5667                    } else {
5668                        mHintLayout = BoringLayout.make(mHint, mTextPaint,
5669                                hintWidth, alignment, mSpacingMult, mSpacingAdd,
5670                                hintBoring, mIncludePad, mEllipsize,
5671                                ellipsisWidth);
5672                    }
5673                } else if (shouldEllipsize) {
5674                    mHintLayout = new StaticLayout(mHint,
5675                                0, mHint.length(),
5676                                mTextPaint, hintWidth, alignment, mSpacingMult,
5677                                mSpacingAdd, mIncludePad, mEllipsize,
5678                                ellipsisWidth);
5679                } else {
5680                    mHintLayout = new StaticLayout(mHint, mTextPaint,
5681                            hintWidth, alignment, mSpacingMult, mSpacingAdd,
5682                            mIncludePad);
5683                }
5684            } else if (shouldEllipsize) {
5685                mHintLayout = new StaticLayout(mHint,
5686                            0, mHint.length(),
5687                            mTextPaint, hintWidth, alignment, mSpacingMult,
5688                            mSpacingAdd, mIncludePad, mEllipsize,
5689                            ellipsisWidth);
5690            } else {
5691                mHintLayout = new StaticLayout(mHint, mTextPaint,
5692                        hintWidth, alignment, mSpacingMult, mSpacingAdd,
5693                        mIncludePad);
5694            }
5695        }
5696
5697        if (bringIntoView) {
5698            registerForPreDraw();
5699        }
5700
5701        if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
5702            if (!compressText(ellipsisWidth)) {
5703                final int height = mLayoutParams.height;
5704                // If the size of the view does not depend on the size of the text, try to
5705                // start the marquee immediately
5706                if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT) {
5707                    startMarquee();
5708                } else {
5709                    // Defer the start of the marquee until we know our width (see setFrame())
5710                    mRestartMarquee = true;
5711                }
5712            }
5713        }
5714
5715        // CursorControllers need a non-null mLayout
5716        prepareCursorControllers();
5717    }
5718
5719    private boolean compressText(float width) {
5720        if (isHardwareAccelerated()) return false;
5721
5722        // Only compress the text if it hasn't been compressed by the previous pass
5723        if (width > 0.0f && mLayout != null && getLineCount() == 1 && !mUserSetTextScaleX &&
5724                mTextPaint.getTextScaleX() == 1.0f) {
5725            final float textWidth = mLayout.getLineWidth(0);
5726            final float overflow = (textWidth + 1.0f - width) / width;
5727            if (overflow > 0.0f && overflow <= Marquee.MARQUEE_DELTA_MAX) {
5728                mTextPaint.setTextScaleX(1.0f - overflow - 0.005f);
5729                post(new Runnable() {
5730                    public void run() {
5731                        requestLayout();
5732                    }
5733                });
5734                return true;
5735            }
5736        }
5737
5738        return false;
5739    }
5740
5741    private static int desired(Layout layout) {
5742        int n = layout.getLineCount();
5743        CharSequence text = layout.getText();
5744        float max = 0;
5745
5746        // if any line was wrapped, we can't use it.
5747        // but it's ok for the last line not to have a newline
5748
5749        for (int i = 0; i < n - 1; i++) {
5750            if (text.charAt(layout.getLineEnd(i) - 1) != '\n')
5751                return -1;
5752        }
5753
5754        for (int i = 0; i < n; i++) {
5755            max = Math.max(max, layout.getLineWidth(i));
5756        }
5757
5758        return (int) FloatMath.ceil(max);
5759    }
5760
5761    /**
5762     * Set whether the TextView includes extra top and bottom padding to make
5763     * room for accents that go above the normal ascent and descent.
5764     * The default is true.
5765     *
5766     * @attr ref android.R.styleable#TextView_includeFontPadding
5767     */
5768    public void setIncludeFontPadding(boolean includepad) {
5769        mIncludePad = includepad;
5770
5771        if (mLayout != null) {
5772            nullLayouts();
5773            requestLayout();
5774            invalidate();
5775        }
5776    }
5777
5778    private static final BoringLayout.Metrics UNKNOWN_BORING = new BoringLayout.Metrics();
5779
5780    @Override
5781    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
5782        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
5783        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
5784        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
5785        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
5786
5787        int width;
5788        int height;
5789
5790        BoringLayout.Metrics boring = UNKNOWN_BORING;
5791        BoringLayout.Metrics hintBoring = UNKNOWN_BORING;
5792
5793        int des = -1;
5794        boolean fromexisting = false;
5795
5796        if (widthMode == MeasureSpec.EXACTLY) {
5797            // Parent has told us how big to be. So be it.
5798            width = widthSize;
5799        } else {
5800            if (mLayout != null && mEllipsize == null) {
5801                des = desired(mLayout);
5802            }
5803
5804            if (des < 0) {
5805                boring = BoringLayout.isBoring(mTransformed, mTextPaint, mBoring);
5806                if (boring != null) {
5807                    mBoring = boring;
5808                }
5809            } else {
5810                fromexisting = true;
5811            }
5812
5813            if (boring == null || boring == UNKNOWN_BORING) {
5814                if (des < 0) {
5815                    des = (int) FloatMath.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint));
5816                }
5817
5818                width = des;
5819            } else {
5820                width = boring.width;
5821            }
5822
5823            final Drawables dr = mDrawables;
5824            if (dr != null) {
5825                width = Math.max(width, dr.mDrawableWidthTop);
5826                width = Math.max(width, dr.mDrawableWidthBottom);
5827            }
5828
5829            if (mHint != null) {
5830                int hintDes = -1;
5831                int hintWidth;
5832
5833                if (mHintLayout != null && mEllipsize == null) {
5834                    hintDes = desired(mHintLayout);
5835                }
5836
5837                if (hintDes < 0) {
5838                    hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mHintBoring);
5839                    if (hintBoring != null) {
5840                        mHintBoring = hintBoring;
5841                    }
5842                }
5843
5844                if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
5845                    if (hintDes < 0) {
5846                        hintDes = (int) FloatMath.ceil(
5847                                Layout.getDesiredWidth(mHint, mTextPaint));
5848                    }
5849
5850                    hintWidth = hintDes;
5851                } else {
5852                    hintWidth = hintBoring.width;
5853                }
5854
5855                if (hintWidth > width) {
5856                    width = hintWidth;
5857                }
5858            }
5859
5860            width += getCompoundPaddingLeft() + getCompoundPaddingRight();
5861
5862            if (mMaxWidthMode == EMS) {
5863                width = Math.min(width, mMaxWidth * getLineHeight());
5864            } else {
5865                width = Math.min(width, mMaxWidth);
5866            }
5867
5868            if (mMinWidthMode == EMS) {
5869                width = Math.max(width, mMinWidth * getLineHeight());
5870            } else {
5871                width = Math.max(width, mMinWidth);
5872            }
5873
5874            // Check against our minimum width
5875            width = Math.max(width, getSuggestedMinimumWidth());
5876
5877            if (widthMode == MeasureSpec.AT_MOST) {
5878                width = Math.min(widthSize, width);
5879            }
5880        }
5881
5882        int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
5883        int unpaddedWidth = want;
5884
5885        if (mHorizontallyScrolling) want = VERY_WIDE;
5886
5887        int hintWant = want;
5888        int hintWidth = mHintLayout == null ? hintWant : mHintLayout.getWidth();
5889
5890        if (mLayout == null) {
5891            makeNewLayout(want, hintWant, boring, hintBoring,
5892                          width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
5893        } else if ((mLayout.getWidth() != want) || (hintWidth != hintWant) ||
5894                   (mLayout.getEllipsizedWidth() !=
5895                        width - getCompoundPaddingLeft() - getCompoundPaddingRight())) {
5896            if (mHint == null && mEllipsize == null &&
5897                    want > mLayout.getWidth() &&
5898                    (mLayout instanceof BoringLayout ||
5899                            (fromexisting && des >= 0 && des <= want))) {
5900                mLayout.increaseWidthTo(want);
5901            } else {
5902                makeNewLayout(want, hintWant, boring, hintBoring,
5903                              width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
5904            }
5905        } else {
5906            // Width has not changed.
5907        }
5908
5909        if (heightMode == MeasureSpec.EXACTLY) {
5910            // Parent has told us how big to be. So be it.
5911            height = heightSize;
5912            mDesiredHeightAtMeasure = -1;
5913        } else {
5914            int desired = getDesiredHeight();
5915
5916            height = desired;
5917            mDesiredHeightAtMeasure = desired;
5918
5919            if (heightMode == MeasureSpec.AT_MOST) {
5920                height = Math.min(desired, heightSize);
5921            }
5922        }
5923
5924        int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
5925        if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
5926            unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
5927        }
5928
5929        /*
5930         * We didn't let makeNewLayout() register to bring the cursor into view,
5931         * so do it here if there is any possibility that it is needed.
5932         */
5933        if (mMovement != null ||
5934            mLayout.getWidth() > unpaddedWidth ||
5935            mLayout.getHeight() > unpaddedHeight) {
5936            registerForPreDraw();
5937        } else {
5938            scrollTo(0, 0);
5939        }
5940
5941        setMeasuredDimension(width, height);
5942    }
5943
5944    private int getDesiredHeight() {
5945        return Math.max(
5946                getDesiredHeight(mLayout, true),
5947                getDesiredHeight(mHintLayout, mEllipsize != null));
5948    }
5949
5950    private int getDesiredHeight(Layout layout, boolean cap) {
5951        if (layout == null) {
5952            return 0;
5953        }
5954
5955        int linecount = layout.getLineCount();
5956        int pad = getCompoundPaddingTop() + getCompoundPaddingBottom();
5957        int desired = layout.getLineTop(linecount);
5958
5959        final Drawables dr = mDrawables;
5960        if (dr != null) {
5961            desired = Math.max(desired, dr.mDrawableHeightLeft);
5962            desired = Math.max(desired, dr.mDrawableHeightRight);
5963        }
5964
5965        desired += pad;
5966        layout.setMaximumVisibleLineCount(0);
5967
5968        if (mMaxMode == LINES) {
5969            /*
5970             * Don't cap the hint to a certain number of lines.
5971             * (Do cap it, though, if we have a maximum pixel height.)
5972             */
5973            if (cap) {
5974                if (linecount > mMaximum) {
5975                    layout.setMaximumVisibleLineCount(mMaximum);
5976                    desired = layout.getLineTop(mMaximum);
5977
5978                    if (dr != null) {
5979                        desired = Math.max(desired, dr.mDrawableHeightLeft);
5980                        desired = Math.max(desired, dr.mDrawableHeightRight);
5981                    }
5982
5983                    desired += pad;
5984                    linecount = mMaximum;
5985                }
5986            }
5987        } else {
5988            desired = Math.min(desired, mMaximum);
5989        }
5990
5991        if (mMinMode == LINES) {
5992            if (linecount < mMinimum) {
5993                desired += getLineHeight() * (mMinimum - linecount);
5994            }
5995        } else {
5996            desired = Math.max(desired, mMinimum);
5997        }
5998
5999        // Check against our minimum height
6000        desired = Math.max(desired, getSuggestedMinimumHeight());
6001
6002        return desired;
6003    }
6004
6005    /**
6006     * Check whether a change to the existing text layout requires a
6007     * new view layout.
6008     */
6009    private void checkForResize() {
6010        boolean sizeChanged = false;
6011
6012        if (mLayout != null) {
6013            // Check if our width changed
6014            if (mLayoutParams.width == LayoutParams.WRAP_CONTENT) {
6015                sizeChanged = true;
6016                invalidate();
6017            }
6018
6019            // Check if our height changed
6020            if (mLayoutParams.height == LayoutParams.WRAP_CONTENT) {
6021                int desiredHeight = getDesiredHeight();
6022
6023                if (desiredHeight != this.getHeight()) {
6024                    sizeChanged = true;
6025                }
6026            } else if (mLayoutParams.height == LayoutParams.MATCH_PARENT) {
6027                if (mDesiredHeightAtMeasure >= 0) {
6028                    int desiredHeight = getDesiredHeight();
6029
6030                    if (desiredHeight != mDesiredHeightAtMeasure) {
6031                        sizeChanged = true;
6032                    }
6033                }
6034            }
6035        }
6036
6037        if (sizeChanged) {
6038            requestLayout();
6039            // caller will have already invalidated
6040        }
6041    }
6042
6043    /**
6044     * Check whether entirely new text requires a new view layout
6045     * or merely a new text layout.
6046     */
6047    private void checkForRelayout() {
6048        // If we have a fixed width, we can just swap in a new text layout
6049        // if the text height stays the same or if the view height is fixed.
6050
6051        if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT ||
6052                (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth)) &&
6053                (mHint == null || mHintLayout != null) &&
6054                (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
6055            // Static width, so try making a new text layout.
6056
6057            int oldht = mLayout.getHeight();
6058            int want = mLayout.getWidth();
6059            int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
6060
6061            /*
6062             * No need to bring the text into view, since the size is not
6063             * changing (unless we do the requestLayout(), in which case it
6064             * will happen at measure).
6065             */
6066            makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
6067                          mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
6068                          false);
6069
6070            if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
6071                // In a fixed-height view, so use our new text layout.
6072                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT &&
6073                    mLayoutParams.height != LayoutParams.MATCH_PARENT) {
6074                    invalidate();
6075                    return;
6076                }
6077
6078                // Dynamic height, but height has stayed the same,
6079                // so use our new text layout.
6080                if (mLayout.getHeight() == oldht &&
6081                    (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
6082                    invalidate();
6083                    return;
6084                }
6085            }
6086
6087            // We lose: the height has changed and we have a dynamic height.
6088            // Request a new view layout using our new text layout.
6089            requestLayout();
6090            invalidate();
6091        } else {
6092            // Dynamic width, so we have no choice but to request a new
6093            // view layout with a new text layout.
6094
6095            nullLayouts();
6096            requestLayout();
6097            invalidate();
6098        }
6099    }
6100
6101    /**
6102     * Returns true if anything changed.
6103     */
6104    private boolean bringTextIntoView() {
6105        int line = 0;
6106        if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
6107            line = mLayout.getLineCount() - 1;
6108        }
6109
6110        Layout.Alignment a = mLayout.getParagraphAlignment(line);
6111        int dir = mLayout.getParagraphDirection(line);
6112        int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
6113        int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
6114        int ht = mLayout.getHeight();
6115
6116        int scrollx, scrolly;
6117
6118        if (a == Layout.Alignment.ALIGN_CENTER) {
6119            /*
6120             * Keep centered if possible, or, if it is too wide to fit,
6121             * keep leading edge in view.
6122             */
6123
6124            int left = (int) FloatMath.floor(mLayout.getLineLeft(line));
6125            int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6126
6127            if (right - left < hspace) {
6128                scrollx = (right + left) / 2 - hspace / 2;
6129            } else {
6130                if (dir < 0) {
6131                    scrollx = right - hspace;
6132                } else {
6133                    scrollx = left;
6134                }
6135            }
6136        } else if (a == Layout.Alignment.ALIGN_NORMAL) {
6137            /*
6138             * Keep leading edge in view.
6139             */
6140
6141            if (dir < 0) {
6142                int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6143                scrollx = right - hspace;
6144            } else {
6145                scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line));
6146            }
6147        } else /* a == Layout.Alignment.ALIGN_OPPOSITE */ {
6148            /*
6149             * Keep trailing edge in view.
6150             */
6151
6152            if (dir < 0) {
6153                scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line));
6154            } else {
6155                int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6156                scrollx = right - hspace;
6157            }
6158        }
6159
6160        if (ht < vspace) {
6161            scrolly = 0;
6162        } else {
6163            if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
6164                scrolly = ht - vspace;
6165            } else {
6166                scrolly = 0;
6167            }
6168        }
6169
6170        if (scrollx != mScrollX || scrolly != mScrollY) {
6171            scrollTo(scrollx, scrolly);
6172            return true;
6173        } else {
6174            return false;
6175        }
6176    }
6177
6178    /**
6179     * Move the point, specified by the offset, into the view if it is needed.
6180     * This has to be called after layout. Returns true if anything changed.
6181     */
6182    public boolean bringPointIntoView(int offset) {
6183        boolean changed = false;
6184
6185        int line = mLayout.getLineForOffset(offset);
6186
6187        // FIXME: Is it okay to truncate this, or should we round?
6188        final int x = (int)mLayout.getPrimaryHorizontal(offset);
6189        final int top = mLayout.getLineTop(line);
6190        final int bottom = mLayout.getLineTop(line + 1);
6191
6192        int left = (int) FloatMath.floor(mLayout.getLineLeft(line));
6193        int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6194        int ht = mLayout.getHeight();
6195
6196        int grav;
6197
6198        switch (mLayout.getParagraphAlignment(line)) {
6199            case ALIGN_NORMAL:
6200                grav = 1;
6201                break;
6202
6203            case ALIGN_OPPOSITE:
6204                grav = -1;
6205                break;
6206
6207            default:
6208                grav = 0;
6209        }
6210
6211        grav *= mLayout.getParagraphDirection(line);
6212
6213        int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
6214        int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
6215
6216        int hslack = (bottom - top) / 2;
6217        int vslack = hslack;
6218
6219        if (vslack > vspace / 4)
6220            vslack = vspace / 4;
6221        if (hslack > hspace / 4)
6222            hslack = hspace / 4;
6223
6224        int hs = mScrollX;
6225        int vs = mScrollY;
6226
6227        if (top - vs < vslack)
6228            vs = top - vslack;
6229        if (bottom - vs > vspace - vslack)
6230            vs = bottom - (vspace - vslack);
6231        if (ht - vs < vspace)
6232            vs = ht - vspace;
6233        if (0 - vs > 0)
6234            vs = 0;
6235
6236        if (grav != 0) {
6237            if (x - hs < hslack) {
6238                hs = x - hslack;
6239            }
6240            if (x - hs > hspace - hslack) {
6241                hs = x - (hspace - hslack);
6242            }
6243        }
6244
6245        if (grav < 0) {
6246            if (left - hs > 0)
6247                hs = left;
6248            if (right - hs < hspace)
6249                hs = right - hspace;
6250        } else if (grav > 0) {
6251            if (right - hs < hspace)
6252                hs = right - hspace;
6253            if (left - hs > 0)
6254                hs = left;
6255        } else /* grav == 0 */ {
6256            if (right - left <= hspace) {
6257                /*
6258                 * If the entire text fits, center it exactly.
6259                 */
6260                hs = left - (hspace - (right - left)) / 2;
6261            } else if (x > right - hslack) {
6262                /*
6263                 * If we are near the right edge, keep the right edge
6264                 * at the edge of the view.
6265                 */
6266                hs = right - hspace;
6267            } else if (x < left + hslack) {
6268                /*
6269                 * If we are near the left edge, keep the left edge
6270                 * at the edge of the view.
6271                 */
6272                hs = left;
6273            } else if (left > hs) {
6274                /*
6275                 * Is there whitespace visible at the left?  Fix it if so.
6276                 */
6277                hs = left;
6278            } else if (right < hs + hspace) {
6279                /*
6280                 * Is there whitespace visible at the right?  Fix it if so.
6281                 */
6282                hs = right - hspace;
6283            } else {
6284                /*
6285                 * Otherwise, float as needed.
6286                 */
6287                if (x - hs < hslack) {
6288                    hs = x - hslack;
6289                }
6290                if (x - hs > hspace - hslack) {
6291                    hs = x - (hspace - hslack);
6292                }
6293            }
6294        }
6295
6296        if (hs != mScrollX || vs != mScrollY) {
6297            if (mScroller == null) {
6298                scrollTo(hs, vs);
6299            } else {
6300                long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
6301                int dx = hs - mScrollX;
6302                int dy = vs - mScrollY;
6303
6304                if (duration > ANIMATED_SCROLL_GAP) {
6305                    mScroller.startScroll(mScrollX, mScrollY, dx, dy);
6306                    awakenScrollBars(mScroller.getDuration());
6307                    invalidate();
6308                } else {
6309                    if (!mScroller.isFinished()) {
6310                        mScroller.abortAnimation();
6311                    }
6312
6313                    scrollBy(dx, dy);
6314                }
6315
6316                mLastScroll = AnimationUtils.currentAnimationTimeMillis();
6317            }
6318
6319            changed = true;
6320        }
6321
6322        if (isFocused()) {
6323            // This offsets because getInterestingRect() is in terms of viewport coordinates, but
6324            // requestRectangleOnScreen() is in terms of content coordinates.
6325
6326            if (mTempRect == null) mTempRect = new Rect();
6327            mTempRect.set(x, top, x + 1, bottom);
6328            getInterestingRect(mTempRect, line);
6329            mTempRect.offset(mScrollX, mScrollY);
6330
6331            if (requestRectangleOnScreen(mTempRect)) {
6332                changed = true;
6333            }
6334        }
6335
6336        return changed;
6337    }
6338
6339    /**
6340     * Move the cursor, if needed, so that it is at an offset that is visible
6341     * to the user.  This will not move the cursor if it represents more than
6342     * one character (a selection range).  This will only work if the
6343     * TextView contains spannable text; otherwise it will do nothing.
6344     *
6345     * @return True if the cursor was actually moved, false otherwise.
6346     */
6347    public boolean moveCursorToVisibleOffset() {
6348        if (!(mText instanceof Spannable)) {
6349            return false;
6350        }
6351        int start = getSelectionStart();
6352        int end = getSelectionEnd();
6353        if (start != end) {
6354            return false;
6355        }
6356
6357        // First: make sure the line is visible on screen:
6358
6359        int line = mLayout.getLineForOffset(start);
6360
6361        final int top = mLayout.getLineTop(line);
6362        final int bottom = mLayout.getLineTop(line + 1);
6363        final int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
6364        int vslack = (bottom - top) / 2;
6365        if (vslack > vspace / 4)
6366            vslack = vspace / 4;
6367        final int vs = mScrollY;
6368
6369        if (top < (vs+vslack)) {
6370            line = mLayout.getLineForVertical(vs+vslack+(bottom-top));
6371        } else if (bottom > (vspace+vs-vslack)) {
6372            line = mLayout.getLineForVertical(vspace+vs-vslack-(bottom-top));
6373        }
6374
6375        // Next: make sure the character is visible on screen:
6376
6377        final int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
6378        final int hs = mScrollX;
6379        final int leftChar = mLayout.getOffsetForHorizontal(line, hs);
6380        final int rightChar = mLayout.getOffsetForHorizontal(line, hspace+hs);
6381
6382        // line might contain bidirectional text
6383        final int lowChar = leftChar < rightChar ? leftChar : rightChar;
6384        final int highChar = leftChar > rightChar ? leftChar : rightChar;
6385
6386        int newStart = start;
6387        if (newStart < lowChar) {
6388            newStart = lowChar;
6389        } else if (newStart > highChar) {
6390            newStart = highChar;
6391        }
6392
6393        if (newStart != start) {
6394            Selection.setSelection((Spannable)mText, newStart);
6395            return true;
6396        }
6397
6398        return false;
6399    }
6400
6401    @Override
6402    public void computeScroll() {
6403        if (mScroller != null) {
6404            if (mScroller.computeScrollOffset()) {
6405                mScrollX = mScroller.getCurrX();
6406                mScrollY = mScroller.getCurrY();
6407                invalidateParentCaches();
6408                postInvalidate();  // So we draw again
6409            }
6410        }
6411    }
6412
6413    private void getInterestingRect(Rect r, int line) {
6414        convertFromViewportToContentCoordinates(r);
6415
6416        // Rectangle can can be expanded on first and last line to take
6417        // padding into account.
6418        // TODO Take left/right padding into account too?
6419        if (line == 0) r.top -= getExtendedPaddingTop();
6420        if (line == mLayout.getLineCount() - 1) r.bottom += getExtendedPaddingBottom();
6421    }
6422
6423    private void convertFromViewportToContentCoordinates(Rect r) {
6424        final int horizontalOffset = viewportToContentHorizontalOffset();
6425        r.left += horizontalOffset;
6426        r.right += horizontalOffset;
6427
6428        final int verticalOffset = viewportToContentVerticalOffset();
6429        r.top += verticalOffset;
6430        r.bottom += verticalOffset;
6431    }
6432
6433    private int viewportToContentHorizontalOffset() {
6434        return getCompoundPaddingLeft() - mScrollX;
6435    }
6436
6437    private int viewportToContentVerticalOffset() {
6438        int offset = getExtendedPaddingTop() - mScrollY;
6439        if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
6440            offset += getVerticalOffset(false);
6441        }
6442        return offset;
6443    }
6444
6445    @Override
6446    public void debug(int depth) {
6447        super.debug(depth);
6448
6449        String output = debugIndent(depth);
6450        output += "frame={" + mLeft + ", " + mTop + ", " + mRight
6451                + ", " + mBottom + "} scroll={" + mScrollX + ", " + mScrollY
6452                + "} ";
6453
6454        if (mText != null) {
6455
6456            output += "mText=\"" + mText + "\" ";
6457            if (mLayout != null) {
6458                output += "mLayout width=" + mLayout.getWidth()
6459                        + " height=" + mLayout.getHeight();
6460            }
6461        } else {
6462            output += "mText=NULL";
6463        }
6464        Log.d(VIEW_LOG_TAG, output);
6465    }
6466
6467    /**
6468     * Convenience for {@link Selection#getSelectionStart}.
6469     */
6470    @ViewDebug.ExportedProperty(category = "text")
6471    public int getSelectionStart() {
6472        return Selection.getSelectionStart(getText());
6473    }
6474
6475    /**
6476     * Convenience for {@link Selection#getSelectionEnd}.
6477     */
6478    @ViewDebug.ExportedProperty(category = "text")
6479    public int getSelectionEnd() {
6480        return Selection.getSelectionEnd(getText());
6481    }
6482
6483    /**
6484     * Return true iff there is a selection inside this text view.
6485     */
6486    public boolean hasSelection() {
6487        final int selectionStart = getSelectionStart();
6488        final int selectionEnd = getSelectionEnd();
6489
6490        return selectionStart >= 0 && selectionStart != selectionEnd;
6491    }
6492
6493    /**
6494     * Sets the properties of this field (lines, horizontally scrolling,
6495     * transformation method) to be for a single-line input.
6496     *
6497     * @attr ref android.R.styleable#TextView_singleLine
6498     */
6499    public void setSingleLine() {
6500        setSingleLine(true);
6501    }
6502
6503    /**
6504     * If true, sets the properties of this field (number of lines, horizontally scrolling,
6505     * transformation method) to be for a single-line input; if false, restores these to the default
6506     * conditions.
6507     *
6508     * Note that the default conditions are not necessarily those that were in effect prior this
6509     * method, and you may want to reset these properties to your custom values.
6510     *
6511     * @attr ref android.R.styleable#TextView_singleLine
6512     */
6513    @android.view.RemotableViewMethod
6514    public void setSingleLine(boolean singleLine) {
6515        // Could be used, but may break backward compatibility.
6516        // if (mSingleLine == singleLine) return;
6517        setInputTypeSingleLine(singleLine);
6518        applySingleLine(singleLine, true, true);
6519    }
6520
6521    /**
6522     * Adds or remove the EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE on the mInputType.
6523     * @param singleLine
6524     */
6525    private void setInputTypeSingleLine(boolean singleLine) {
6526        if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
6527            if (singleLine) {
6528                mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
6529            } else {
6530                mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
6531            }
6532        }
6533    }
6534
6535    private void applySingleLine(boolean singleLine, boolean applyTransformation,
6536            boolean changeMaxLines) {
6537        mSingleLine = singleLine;
6538        if (singleLine) {
6539            setLines(1);
6540            setHorizontallyScrolling(true);
6541            if (applyTransformation) {
6542                setTransformationMethod(SingleLineTransformationMethod.getInstance());
6543            }
6544        } else {
6545            if (changeMaxLines) {
6546                setMaxLines(Integer.MAX_VALUE);
6547            }
6548            setHorizontallyScrolling(false);
6549            if (applyTransformation) {
6550                setTransformationMethod(null);
6551            }
6552        }
6553    }
6554
6555    /**
6556     * Causes words in the text that are longer than the view is wide
6557     * to be ellipsized instead of broken in the middle.  You may also
6558     * want to {@link #setSingleLine} or {@link #setHorizontallyScrolling}
6559     * to constrain the text to a single line.  Use <code>null</code>
6560     * to turn off ellipsizing.
6561     *
6562     * @attr ref android.R.styleable#TextView_ellipsize
6563     */
6564    public void setEllipsize(TextUtils.TruncateAt where) {
6565        mEllipsize = where;
6566
6567        if (mLayout != null) {
6568            nullLayouts();
6569            requestLayout();
6570            invalidate();
6571        }
6572    }
6573
6574    /**
6575     * Sets how many times to repeat the marquee animation. Only applied if the
6576     * TextView has marquee enabled. Set to -1 to repeat indefinitely.
6577     *
6578     * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
6579     */
6580    public void setMarqueeRepeatLimit(int marqueeLimit) {
6581        mMarqueeRepeatLimit = marqueeLimit;
6582    }
6583
6584    /**
6585     * Returns where, if anywhere, words that are longer than the view
6586     * is wide should be ellipsized.
6587     */
6588    @ViewDebug.ExportedProperty
6589    public TextUtils.TruncateAt getEllipsize() {
6590        return mEllipsize;
6591    }
6592
6593    /**
6594     * Set the TextView so that when it takes focus, all the text is
6595     * selected.
6596     *
6597     * @attr ref android.R.styleable#TextView_selectAllOnFocus
6598     */
6599    @android.view.RemotableViewMethod
6600    public void setSelectAllOnFocus(boolean selectAllOnFocus) {
6601        mSelectAllOnFocus = selectAllOnFocus;
6602
6603        if (selectAllOnFocus && !(mText instanceof Spannable)) {
6604            setText(mText, BufferType.SPANNABLE);
6605        }
6606    }
6607
6608    /**
6609     * Set whether the cursor is visible.  The default is true.
6610     *
6611     * @attr ref android.R.styleable#TextView_cursorVisible
6612     */
6613    @android.view.RemotableViewMethod
6614    public void setCursorVisible(boolean visible) {
6615        if (mCursorVisible != visible) {
6616            mCursorVisible = visible;
6617            invalidate();
6618
6619            makeBlink();
6620
6621            // InsertionPointCursorController depends on mCursorVisible
6622            prepareCursorControllers();
6623        }
6624    }
6625
6626    private boolean isCursorVisible() {
6627        return mCursorVisible && isTextEditable();
6628    }
6629
6630    private boolean canMarquee() {
6631        int width = (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight());
6632        return width > 0 && mLayout.getLineWidth(0) > width;
6633    }
6634
6635    private void startMarquee() {
6636        // Do not ellipsize EditText
6637        if (mInput != null) return;
6638
6639        if (compressText(getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight())) {
6640            return;
6641        }
6642
6643        if ((mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected()) &&
6644                getLineCount() == 1 && canMarquee()) {
6645
6646            if (mMarquee == null) mMarquee = new Marquee(this);
6647            mMarquee.start(mMarqueeRepeatLimit);
6648        }
6649    }
6650
6651    private void stopMarquee() {
6652        if (mMarquee != null && !mMarquee.isStopped()) {
6653            mMarquee.stop();
6654        }
6655    }
6656
6657    private void startStopMarquee(boolean start) {
6658        if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
6659            if (start) {
6660                startMarquee();
6661            } else {
6662                stopMarquee();
6663            }
6664        }
6665    }
6666
6667    private static final class Marquee extends Handler {
6668        // TODO: Add an option to configure this
6669        private static final float MARQUEE_DELTA_MAX = 0.07f;
6670        private static final int MARQUEE_DELAY = 1200;
6671        private static final int MARQUEE_RESTART_DELAY = 1200;
6672        private static final int MARQUEE_RESOLUTION = 1000 / 30;
6673        private static final int MARQUEE_PIXELS_PER_SECOND = 30;
6674
6675        private static final byte MARQUEE_STOPPED = 0x0;
6676        private static final byte MARQUEE_STARTING = 0x1;
6677        private static final byte MARQUEE_RUNNING = 0x2;
6678
6679        private static final int MESSAGE_START = 0x1;
6680        private static final int MESSAGE_TICK = 0x2;
6681        private static final int MESSAGE_RESTART = 0x3;
6682
6683        private final WeakReference<TextView> mView;
6684
6685        private byte mStatus = MARQUEE_STOPPED;
6686        private final float mScrollUnit;
6687        private float mMaxScroll;
6688        float mMaxFadeScroll;
6689        private float mGhostStart;
6690        private float mGhostOffset;
6691        private float mFadeStop;
6692        private int mRepeatLimit;
6693
6694        float mScroll;
6695
6696        Marquee(TextView v) {
6697            final float density = v.getContext().getResources().getDisplayMetrics().density;
6698            mScrollUnit = (MARQUEE_PIXELS_PER_SECOND * density) / MARQUEE_RESOLUTION;
6699            mView = new WeakReference<TextView>(v);
6700        }
6701
6702        @Override
6703        public void handleMessage(Message msg) {
6704            switch (msg.what) {
6705                case MESSAGE_START:
6706                    mStatus = MARQUEE_RUNNING;
6707                    tick();
6708                    break;
6709                case MESSAGE_TICK:
6710                    tick();
6711                    break;
6712                case MESSAGE_RESTART:
6713                    if (mStatus == MARQUEE_RUNNING) {
6714                        if (mRepeatLimit >= 0) {
6715                            mRepeatLimit--;
6716                        }
6717                        start(mRepeatLimit);
6718                    }
6719                    break;
6720            }
6721        }
6722
6723        void tick() {
6724            if (mStatus != MARQUEE_RUNNING) {
6725                return;
6726            }
6727
6728            removeMessages(MESSAGE_TICK);
6729
6730            final TextView textView = mView.get();
6731            if (textView != null && (textView.isFocused() || textView.isSelected())) {
6732                mScroll += mScrollUnit;
6733                if (mScroll > mMaxScroll) {
6734                    mScroll = mMaxScroll;
6735                    sendEmptyMessageDelayed(MESSAGE_RESTART, MARQUEE_RESTART_DELAY);
6736                } else {
6737                    sendEmptyMessageDelayed(MESSAGE_TICK, MARQUEE_RESOLUTION);
6738                }
6739                textView.invalidate();
6740            }
6741        }
6742
6743        void stop() {
6744            mStatus = MARQUEE_STOPPED;
6745            removeMessages(MESSAGE_START);
6746            removeMessages(MESSAGE_RESTART);
6747            removeMessages(MESSAGE_TICK);
6748            resetScroll();
6749        }
6750
6751        private void resetScroll() {
6752            mScroll = 0.0f;
6753            final TextView textView = mView.get();
6754            if (textView != null) textView.invalidate();
6755        }
6756
6757        void start(int repeatLimit) {
6758            if (repeatLimit == 0) {
6759                stop();
6760                return;
6761            }
6762            mRepeatLimit = repeatLimit;
6763            final TextView textView = mView.get();
6764            if (textView != null && textView.mLayout != null) {
6765                mStatus = MARQUEE_STARTING;
6766                mScroll = 0.0f;
6767                final int textWidth = textView.getWidth() - textView.getCompoundPaddingLeft() -
6768                        textView.getCompoundPaddingRight();
6769                final float lineWidth = textView.mLayout.getLineWidth(0);
6770                final float gap = textWidth / 3.0f;
6771                mGhostStart = lineWidth - textWidth + gap;
6772                mMaxScroll = mGhostStart + textWidth;
6773                mGhostOffset = lineWidth + gap;
6774                mFadeStop = lineWidth + textWidth / 6.0f;
6775                mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;
6776
6777                textView.invalidate();
6778                sendEmptyMessageDelayed(MESSAGE_START, MARQUEE_DELAY);
6779            }
6780        }
6781
6782        float getGhostOffset() {
6783            return mGhostOffset;
6784        }
6785
6786        boolean shouldDrawLeftFade() {
6787            return mScroll <= mFadeStop;
6788        }
6789
6790        boolean shouldDrawGhost() {
6791            return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart;
6792        }
6793
6794        boolean isRunning() {
6795            return mStatus == MARQUEE_RUNNING;
6796        }
6797
6798        boolean isStopped() {
6799            return mStatus == MARQUEE_STOPPED;
6800        }
6801    }
6802
6803    /**
6804     * This method is called when the text is changed, in case any subclasses
6805     * would like to know.
6806     *
6807     * Within <code>text</code>, the <code>lengthAfter</code> characters
6808     * beginning at <code>start</code> have just replaced old text that had
6809     * length <code>lengthBefore</code>. It is an error to attempt to make
6810     * changes to <code>text</code> from this callback.
6811     *
6812     * @param text The text the TextView is displaying
6813     * @param start The offset of the start of the range of the text that was
6814     * modified
6815     * @param lengthBefore The length of the former text that has been replaced
6816     * @param lengthAfter The length of the replacement modified text
6817     */
6818    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
6819        // intentionally empty
6820    }
6821
6822    /**
6823     * This method is called when the selection has changed, in case any
6824     * subclasses would like to know.
6825     *
6826     * @param selStart The new selection start location.
6827     * @param selEnd The new selection end location.
6828     */
6829    protected void onSelectionChanged(int selStart, int selEnd) {
6830        // intentionally empty
6831    }
6832
6833    /**
6834     * Adds a TextWatcher to the list of those whose methods are called
6835     * whenever this TextView's text changes.
6836     * <p>
6837     * In 1.0, the {@link TextWatcher#afterTextChanged} method was erroneously
6838     * not called after {@link #setText} calls.  Now, doing {@link #setText}
6839     * if there are any text changed listeners forces the buffer type to
6840     * Editable if it would not otherwise be and does call this method.
6841     */
6842    public void addTextChangedListener(TextWatcher watcher) {
6843        if (mListeners == null) {
6844            mListeners = new ArrayList<TextWatcher>();
6845        }
6846
6847        mListeners.add(watcher);
6848    }
6849
6850    /**
6851     * Removes the specified TextWatcher from the list of those whose
6852     * methods are called
6853     * whenever this TextView's text changes.
6854     */
6855    public void removeTextChangedListener(TextWatcher watcher) {
6856        if (mListeners != null) {
6857            int i = mListeners.indexOf(watcher);
6858
6859            if (i >= 0) {
6860                mListeners.remove(i);
6861            }
6862        }
6863    }
6864
6865    private void sendBeforeTextChanged(CharSequence text, int start, int before,
6866                                   int after) {
6867        if (mListeners != null) {
6868            final ArrayList<TextWatcher> list = mListeners;
6869            final int count = list.size();
6870            for (int i = 0; i < count; i++) {
6871                list.get(i).beforeTextChanged(text, start, before, after);
6872            }
6873        }
6874    }
6875
6876    /**
6877     * Not private so it can be called from an inner class without going
6878     * through a thunk.
6879     */
6880    void sendOnTextChanged(CharSequence text, int start, int before,
6881                                   int after) {
6882        if (mListeners != null) {
6883            final ArrayList<TextWatcher> list = mListeners;
6884            final int count = list.size();
6885            for (int i = 0; i < count; i++) {
6886                list.get(i).onTextChanged(text, start, before, after);
6887            }
6888        }
6889    }
6890
6891    /**
6892     * Not private so it can be called from an inner class without going
6893     * through a thunk.
6894     */
6895    void sendAfterTextChanged(Editable text) {
6896        if (mListeners != null) {
6897            final ArrayList<TextWatcher> list = mListeners;
6898            final int count = list.size();
6899            for (int i = 0; i < count; i++) {
6900                list.get(i).afterTextChanged(text);
6901            }
6902        }
6903    }
6904
6905    /**
6906     * Not private so it can be called from an inner class without going
6907     * through a thunk.
6908     */
6909    void handleTextChanged(CharSequence buffer, int start, int before, int after) {
6910        final InputMethodState ims = mInputMethodState;
6911        if (ims == null || ims.mBatchEditNesting == 0) {
6912            updateAfterEdit();
6913        }
6914        if (ims != null) {
6915            ims.mContentChanged = true;
6916            if (ims.mChangedStart < 0) {
6917                ims.mChangedStart = start;
6918                ims.mChangedEnd = start+before;
6919            } else {
6920                ims.mChangedStart = Math.min(ims.mChangedStart, start);
6921                ims.mChangedEnd = Math.max(ims.mChangedEnd, start + before - ims.mChangedDelta);
6922            }
6923            ims.mChangedDelta += after-before;
6924        }
6925
6926        sendOnTextChanged(buffer, start, before, after);
6927        onTextChanged(buffer, start, before, after);
6928
6929        // Hide the controllers if the amount of content changed
6930        if (before != after) {
6931            hideControllers();
6932        }
6933    }
6934
6935    /**
6936     * Not private so it can be called from an inner class without going
6937     * through a thunk.
6938     */
6939    void spanChange(Spanned buf, Object what, int oldStart, int newStart, int oldEnd, int newEnd) {
6940        // XXX Make the start and end move together if this ends up
6941        // spending too much time invalidating.
6942
6943        boolean selChanged = false;
6944        int newSelStart=-1, newSelEnd=-1;
6945
6946        final InputMethodState ims = mInputMethodState;
6947
6948        if (what == Selection.SELECTION_END) {
6949            mHighlightPathBogus = true;
6950            selChanged = true;
6951            newSelEnd = newStart;
6952
6953            if (!isFocused()) {
6954                mSelectionMoved = true;
6955            }
6956
6957            if (oldStart >= 0 || newStart >= 0) {
6958                invalidateCursor(Selection.getSelectionStart(buf), oldStart, newStart);
6959                registerForPreDraw();
6960                makeBlink();
6961            }
6962        }
6963
6964        if (what == Selection.SELECTION_START) {
6965            mHighlightPathBogus = true;
6966            selChanged = true;
6967            newSelStart = newStart;
6968
6969            if (!isFocused()) {
6970                mSelectionMoved = true;
6971            }
6972
6973            if (oldStart >= 0 || newStart >= 0) {
6974                int end = Selection.getSelectionEnd(buf);
6975                invalidateCursor(end, oldStart, newStart);
6976            }
6977        }
6978
6979        if (selChanged) {
6980            if ((buf.getSpanFlags(what)&Spanned.SPAN_INTERMEDIATE) == 0) {
6981                if (newSelStart < 0) {
6982                    newSelStart = Selection.getSelectionStart(buf);
6983                }
6984                if (newSelEnd < 0) {
6985                    newSelEnd = Selection.getSelectionEnd(buf);
6986                }
6987                onSelectionChanged(newSelStart, newSelEnd);
6988            }
6989        }
6990
6991        if (what instanceof UpdateAppearance ||
6992            what instanceof ParagraphStyle) {
6993            if (ims == null || ims.mBatchEditNesting == 0) {
6994                invalidate();
6995                mHighlightPathBogus = true;
6996                checkForResize();
6997            } else {
6998                ims.mContentChanged = true;
6999            }
7000        }
7001
7002        if (MetaKeyKeyListener.isMetaTracker(buf, what)) {
7003            mHighlightPathBogus = true;
7004            if (ims != null && MetaKeyKeyListener.isSelectingMetaTracker(buf, what)) {
7005                ims.mSelectionModeChanged = true;
7006            }
7007
7008            if (Selection.getSelectionStart(buf) >= 0) {
7009                if (ims == null || ims.mBatchEditNesting == 0) {
7010                    invalidateCursor();
7011                } else {
7012                    ims.mCursorChanged = true;
7013                }
7014            }
7015        }
7016
7017        if (what instanceof ParcelableSpan) {
7018            // If this is a span that can be sent to a remote process,
7019            // the current extract editor would be interested in it.
7020            if (ims != null && ims.mExtracting != null) {
7021                if (ims.mBatchEditNesting != 0) {
7022                    if (oldStart >= 0) {
7023                        if (ims.mChangedStart > oldStart) {
7024                            ims.mChangedStart = oldStart;
7025                        }
7026                        if (ims.mChangedStart > oldEnd) {
7027                            ims.mChangedStart = oldEnd;
7028                        }
7029                    }
7030                    if (newStart >= 0) {
7031                        if (ims.mChangedStart > newStart) {
7032                            ims.mChangedStart = newStart;
7033                        }
7034                        if (ims.mChangedStart > newEnd) {
7035                            ims.mChangedStart = newEnd;
7036                        }
7037                    }
7038                } else {
7039                    if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Span change outside of batch: "
7040                            + oldStart + "-" + oldEnd + ","
7041                            + newStart + "-" + newEnd + what);
7042                    ims.mContentChanged = true;
7043                }
7044            }
7045        }
7046    }
7047
7048    private class ChangeWatcher
7049    implements TextWatcher, SpanWatcher {
7050
7051        private CharSequence mBeforeText;
7052
7053        public void beforeTextChanged(CharSequence buffer, int start,
7054                                      int before, int after) {
7055            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "beforeTextChanged start=" + start
7056                    + " before=" + before + " after=" + after + ": " + buffer);
7057
7058            if (AccessibilityManager.getInstance(mContext).isEnabled()
7059                    && !isPasswordInputType(mInputType)
7060                    && !hasPasswordTransformationMethod()) {
7061                mBeforeText = buffer.toString();
7062            }
7063
7064            TextView.this.sendBeforeTextChanged(buffer, start, before, after);
7065        }
7066
7067        public void onTextChanged(CharSequence buffer, int start,
7068                                  int before, int after) {
7069            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onTextChanged start=" + start
7070                    + " before=" + before + " after=" + after + ": " + buffer);
7071            TextView.this.handleTextChanged(buffer, start, before, after);
7072
7073            if (AccessibilityManager.getInstance(mContext).isEnabled() &&
7074                    (isFocused() || isSelected() &&
7075                    isShown())) {
7076                sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after);
7077                mBeforeText = null;
7078            }
7079        }
7080
7081        public void afterTextChanged(Editable buffer) {
7082            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "afterTextChanged: " + buffer);
7083            TextView.this.sendAfterTextChanged(buffer);
7084
7085            if (MetaKeyKeyListener.getMetaState(buffer,
7086                                 MetaKeyKeyListener.META_SELECTING) != 0) {
7087                MetaKeyKeyListener.stopSelecting(TextView.this, buffer);
7088            }
7089        }
7090
7091        public void onSpanChanged(Spannable buf,
7092                                  Object what, int s, int e, int st, int en) {
7093            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanChanged s=" + s + " e=" + e
7094                    + " st=" + st + " en=" + en + " what=" + what + ": " + buf);
7095            TextView.this.spanChange(buf, what, s, st, e, en);
7096        }
7097
7098        public void onSpanAdded(Spannable buf, Object what, int s, int e) {
7099            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanAdded s=" + s + " e=" + e
7100                    + " what=" + what + ": " + buf);
7101            TextView.this.spanChange(buf, what, -1, s, -1, e);
7102        }
7103
7104        public void onSpanRemoved(Spannable buf, Object what, int s, int e) {
7105            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanRemoved s=" + s + " e=" + e
7106                    + " what=" + what + ": " + buf);
7107            TextView.this.spanChange(buf, what, s, -1, e, -1);
7108        }
7109    }
7110
7111    /**
7112     * @hide
7113     */
7114    @Override
7115    public void dispatchFinishTemporaryDetach() {
7116        mDispatchTemporaryDetach = true;
7117        super.dispatchFinishTemporaryDetach();
7118        mDispatchTemporaryDetach = false;
7119    }
7120
7121    @Override
7122    public void onStartTemporaryDetach() {
7123        super.onStartTemporaryDetach();
7124        // Only track when onStartTemporaryDetach() is called directly,
7125        // usually because this instance is an editable field in a list
7126        if (!mDispatchTemporaryDetach) mTemporaryDetach = true;
7127
7128        // Because of View recycling in ListView, there is no easy way to know when a TextView with
7129        // selection becomes visible again. Until a better solution is found, stop text selection
7130        // mode (if any) as soon as this TextView is recycled.
7131        stopSelectionActionMode();
7132    }
7133
7134    @Override
7135    public void onFinishTemporaryDetach() {
7136        super.onFinishTemporaryDetach();
7137        // Only track when onStartTemporaryDetach() is called directly,
7138        // usually because this instance is an editable field in a list
7139        if (!mDispatchTemporaryDetach) mTemporaryDetach = false;
7140    }
7141
7142    @Override
7143    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
7144        if (mTemporaryDetach) {
7145            // If we are temporarily in the detach state, then do nothing.
7146            super.onFocusChanged(focused, direction, previouslyFocusedRect);
7147            return;
7148        }
7149
7150        mShowCursor = SystemClock.uptimeMillis();
7151
7152        ensureEndedBatchEdit();
7153
7154        if (focused) {
7155            int selStart = getSelectionStart();
7156            int selEnd = getSelectionEnd();
7157
7158            // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
7159            // mode for these, unless there was a specific selection already started.
7160            final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0 &&
7161                    selEnd == mText.length();
7162            mCreatedWithASelection = mFrozenWithFocus && hasSelection() && !isFocusHighlighted;
7163
7164            if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
7165                // If a tap was used to give focus to that view, move cursor at tap position.
7166                // Has to be done before onTakeFocus, which can be overloaded.
7167                final int lastTapPosition = getLastTapPosition();
7168                if (lastTapPosition >= 0) {
7169                    Selection.setSelection((Spannable) mText, lastTapPosition);
7170                }
7171
7172                if (mMovement != null) {
7173                    mMovement.onTakeFocus(this, (Spannable) mText, direction);
7174                }
7175
7176                // The DecorView does not have focus when the 'Done' ExtractEditText button is
7177                // pressed. Since it is the ViewAncestor's mView, it requests focus before
7178                // ExtractEditText clears focus, which gives focus to the ExtractEditText.
7179                // This special case ensure that we keep current selection in that case.
7180                // It would be better to know why the DecorView does not have focus at that time.
7181                if (((this instanceof ExtractEditText) || mSelectionMoved) &&
7182                        selStart >= 0 && selEnd >= 0) {
7183                    /*
7184                     * Someone intentionally set the selection, so let them
7185                     * do whatever it is that they wanted to do instead of
7186                     * the default on-focus behavior.  We reset the selection
7187                     * here instead of just skipping the onTakeFocus() call
7188                     * because some movement methods do something other than
7189                     * just setting the selection in theirs and we still
7190                     * need to go through that path.
7191                     */
7192                    Selection.setSelection((Spannable) mText, selStart, selEnd);
7193                }
7194
7195                if (mSelectAllOnFocus) {
7196                    selectAll();
7197                }
7198
7199                mTouchFocusSelected = true;
7200            }
7201
7202            mFrozenWithFocus = false;
7203            mSelectionMoved = false;
7204
7205            if (mText instanceof Spannable) {
7206                Spannable sp = (Spannable) mText;
7207                MetaKeyKeyListener.resetMetaState(sp);
7208            }
7209
7210            makeBlink();
7211
7212            if (mError != null) {
7213                showError();
7214            }
7215        } else {
7216            if (mError != null) {
7217                hideError();
7218            }
7219            // Don't leave us in the middle of a batch edit.
7220            onEndBatchEdit();
7221
7222            if (this instanceof ExtractEditText) {
7223                // terminateTextSelectionMode removes selection, which we want to keep when
7224                // ExtractEditText goes out of focus.
7225                final int selStart = getSelectionStart();
7226                final int selEnd = getSelectionEnd();
7227                hideControllers();
7228                Selection.setSelection((Spannable) mText, selStart, selEnd);
7229            } else {
7230                hideControllers();
7231            }
7232
7233            // No need to create the controller
7234            if (mSelectionModifierCursorController != null) {
7235                mSelectionModifierCursorController.resetTouchOffsets();
7236            }
7237        }
7238
7239        startStopMarquee(focused);
7240
7241        if (mTransformation != null) {
7242            mTransformation.onFocusChanged(this, mText, focused, direction, previouslyFocusedRect);
7243        }
7244
7245        super.onFocusChanged(focused, direction, previouslyFocusedRect);
7246    }
7247
7248    private int getLastTapPosition() {
7249        // No need to create the controller at that point, no last tap position saved
7250        if (mSelectionModifierCursorController != null) {
7251            int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
7252            if (lastTapPosition >= 0) {
7253                // Safety check, should not be possible.
7254                if (lastTapPosition > mText.length()) {
7255                    Log.e(LOG_TAG, "Invalid tap focus position (" + lastTapPosition + " vs "
7256                            + mText.length() + ")");
7257                    lastTapPosition = mText.length();
7258                }
7259                return lastTapPosition;
7260            }
7261        }
7262
7263        return -1;
7264    }
7265
7266    @Override
7267    public void onWindowFocusChanged(boolean hasWindowFocus) {
7268        super.onWindowFocusChanged(hasWindowFocus);
7269
7270        if (hasWindowFocus) {
7271            if (mBlink != null) {
7272                mBlink.uncancel();
7273                makeBlink();
7274            }
7275        } else {
7276            if (mBlink != null) {
7277                mBlink.cancel();
7278            }
7279            // Don't leave us in the middle of a batch edit.
7280            onEndBatchEdit();
7281            if (mInputContentType != null) {
7282                mInputContentType.enterDown = false;
7283            }
7284            hideControllers();
7285            removeAllSuggestionSpans();
7286        }
7287
7288        startStopMarquee(hasWindowFocus);
7289    }
7290
7291    private void removeAllSuggestionSpans() {
7292        if (mText instanceof Editable) {
7293            Editable editable = ((Editable) mText);
7294            SuggestionSpan[] spans = editable.getSpans(0, mText.length(), SuggestionSpan.class);
7295            final int length = spans.length;
7296            for (int i = 0; i < length; i++) {
7297                editable.removeSpan(spans[i]);
7298            }
7299        }
7300    }
7301
7302    @Override
7303    protected void onVisibilityChanged(View changedView, int visibility) {
7304        super.onVisibilityChanged(changedView, visibility);
7305        if (visibility != VISIBLE) {
7306            hideControllers();
7307        }
7308    }
7309
7310    /**
7311     * Use {@link BaseInputConnection#removeComposingSpans
7312     * BaseInputConnection.removeComposingSpans()} to remove any IME composing
7313     * state from this text view.
7314     */
7315    public void clearComposingText() {
7316        if (mText instanceof Spannable) {
7317            BaseInputConnection.removeComposingSpans((Spannable)mText);
7318        }
7319    }
7320
7321    @Override
7322    public void setSelected(boolean selected) {
7323        boolean wasSelected = isSelected();
7324
7325        super.setSelected(selected);
7326
7327        if (selected != wasSelected && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
7328            if (selected) {
7329                startMarquee();
7330            } else {
7331                stopMarquee();
7332            }
7333        }
7334    }
7335
7336    @Override
7337    public boolean onTouchEvent(MotionEvent event) {
7338        final int action = event.getActionMasked();
7339
7340        if (hasInsertionController()) {
7341            getInsertionController().onTouchEvent(event);
7342        }
7343        if (hasSelectionController()) {
7344            getSelectionController().onTouchEvent(event);
7345        }
7346
7347        if (action == MotionEvent.ACTION_DOWN) {
7348            mLastDownPositionX = event.getX();
7349            mLastDownPositionY = event.getY();
7350
7351            // Reset this state; it will be re-set if super.onTouchEvent
7352            // causes focus to move to the view.
7353            mTouchFocusSelected = false;
7354            mIgnoreActionUpEvent = false;
7355        }
7356
7357        final boolean superResult = super.onTouchEvent(event);
7358
7359        /*
7360         * Don't handle the release after a long press, because it will
7361         * move the selection away from whatever the menu action was
7362         * trying to affect.
7363         */
7364        if (mDiscardNextActionUp && action == MotionEvent.ACTION_UP) {
7365            mDiscardNextActionUp = false;
7366            return superResult;
7367        }
7368
7369        final boolean touchIsFinished = action == MotionEvent.ACTION_UP && !mIgnoreActionUpEvent &&
7370                isFocused();
7371
7372        if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
7373                && mText instanceof Spannable && mLayout != null) {
7374            boolean handled = false;
7375
7376            if (mMovement != null) {
7377                handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
7378            }
7379
7380            if (mLinksClickable && mAutoLinkMask != 0 && mTextIsSelectable && touchIsFinished) {
7381                // The LinkMovementMethod which should handle taps on links has not been installed
7382                // to support text selection. We reproduce its behavior here to open links.
7383                ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(),
7384                        getSelectionEnd(), ClickableSpan.class);
7385
7386                if (links.length != 0) {
7387                    links[0].onClick(this);
7388                    handled = true;
7389                }
7390            }
7391
7392            if ((isTextEditable() || mTextIsSelectable) && touchIsFinished) {
7393                // Show the IME, except when selecting in read-only text.
7394                if (!mTextIsSelectable) {
7395                    final InputMethodManager imm = InputMethodManager.peekInstance();
7396                    handled |= imm != null && imm.showSoftInput(this, 0);
7397                }
7398
7399                boolean selectAllGotFocus = mSelectAllOnFocus && didTouchFocusSelect();
7400                if (!selectAllGotFocus && hasSelection()) {
7401                    startSelectionActionMode();
7402                } else {
7403                    stopSelectionActionMode();
7404                    hideSuggestions();
7405                    if (hasInsertionController() && !selectAllGotFocus && mText.length() > 0) {
7406                        getInsertionController().show();
7407                    }
7408                }
7409            }
7410
7411            if (handled) {
7412                return true;
7413            }
7414        }
7415
7416        return superResult;
7417    }
7418
7419    @Override
7420    public boolean onGenericMotionEvent(MotionEvent event) {
7421        if (mMovement != null && mText instanceof Spannable && mLayout != null) {
7422            try {
7423                if (mMovement.onGenericMotionEvent(this, (Spannable) mText, event)) {
7424                    return true;
7425                }
7426            } catch (AbstractMethodError ex) {
7427                // onGenericMotionEvent was added to the MovementMethod interface in API 12.
7428                // Ignore its absence in case third party applications implemented the
7429                // interface directly.
7430            }
7431        }
7432        return super.onGenericMotionEvent(event);
7433    }
7434
7435    private void prepareCursorControllers() {
7436        boolean windowSupportsHandles = false;
7437
7438        ViewGroup.LayoutParams params = getRootView().getLayoutParams();
7439        if (params instanceof WindowManager.LayoutParams) {
7440            WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
7441            windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
7442                    || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
7443        }
7444
7445        mInsertionControllerEnabled = windowSupportsHandles && isCursorVisible() && mLayout != null;
7446        mSelectionControllerEnabled = windowSupportsHandles && textCanBeSelected() &&
7447                mLayout != null;
7448
7449        if (!mInsertionControllerEnabled) {
7450            hideInsertionPointCursorController();
7451            if (mInsertionPointCursorController != null) {
7452                mInsertionPointCursorController.onDetached();
7453                mInsertionPointCursorController = null;
7454            }
7455        }
7456
7457        if (!mSelectionControllerEnabled) {
7458            stopSelectionActionMode();
7459            if (mSelectionModifierCursorController != null) {
7460                mSelectionModifierCursorController.onDetached();
7461                mSelectionModifierCursorController = null;
7462            }
7463        }
7464    }
7465
7466    /**
7467     * @return True iff this TextView contains a text that can be edited, or if this is
7468     * a selectable TextView.
7469     */
7470    private boolean isTextEditable() {
7471        return mText instanceof Editable && onCheckIsTextEditor() && isEnabled();
7472    }
7473
7474    /**
7475     * Returns true, only while processing a touch gesture, if the initial
7476     * touch down event caused focus to move to the text view and as a result
7477     * its selection changed.  Only valid while processing the touch gesture
7478     * of interest.
7479     */
7480    public boolean didTouchFocusSelect() {
7481        return mTouchFocusSelected;
7482    }
7483
7484    @Override
7485    public void cancelLongPress() {
7486        super.cancelLongPress();
7487        mIgnoreActionUpEvent = true;
7488    }
7489
7490    @Override
7491    public boolean onTrackballEvent(MotionEvent event) {
7492        if (mMovement != null && mText instanceof Spannable &&
7493            mLayout != null) {
7494            if (mMovement.onTrackballEvent(this, (Spannable) mText, event)) {
7495                return true;
7496            }
7497        }
7498
7499        return super.onTrackballEvent(event);
7500    }
7501
7502    public void setScroller(Scroller s) {
7503        mScroller = s;
7504    }
7505
7506    private static class Blink extends Handler implements Runnable {
7507        private final WeakReference<TextView> mView;
7508        private boolean mCancelled;
7509
7510        public Blink(TextView v) {
7511            mView = new WeakReference<TextView>(v);
7512        }
7513
7514        public void run() {
7515            if (mCancelled) {
7516                return;
7517            }
7518
7519            removeCallbacks(Blink.this);
7520
7521            TextView tv = mView.get();
7522
7523            if (tv != null && tv.shouldBlink()) {
7524                if (tv.mLayout != null) {
7525                    tv.invalidateCursorPath();
7526                }
7527
7528                postAtTime(this, SystemClock.uptimeMillis() + BLINK);
7529            }
7530        }
7531
7532        void cancel() {
7533            if (!mCancelled) {
7534                removeCallbacks(Blink.this);
7535                mCancelled = true;
7536            }
7537        }
7538
7539        void uncancel() {
7540            mCancelled = false;
7541        }
7542    }
7543
7544    /**
7545     * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
7546     */
7547    private boolean shouldBlink() {
7548        if (!isFocused()) return false;
7549
7550        final int start = getSelectionStart();
7551        if (start < 0) return false;
7552
7553        final int end = getSelectionEnd();
7554        if (end < 0) return false;
7555
7556        return start == end;
7557    }
7558
7559    private void makeBlink() {
7560        if (isCursorVisible()) {
7561            if (shouldBlink()) {
7562                mShowCursor = SystemClock.uptimeMillis();
7563                if (mBlink == null) mBlink = new Blink(this);
7564                mBlink.removeCallbacks(mBlink);
7565                mBlink.postAtTime(mBlink, mShowCursor + BLINK);
7566            }
7567        } else {
7568            if (mBlink != null) mBlink.removeCallbacks(mBlink);
7569        }
7570    }
7571
7572    @Override
7573    protected float getLeftFadingEdgeStrength() {
7574        if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return 0.0f;
7575        if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
7576            if (mMarquee != null && !mMarquee.isStopped()) {
7577                final Marquee marquee = mMarquee;
7578                if (marquee.shouldDrawLeftFade()) {
7579                    return marquee.mScroll / getHorizontalFadingEdgeLength();
7580                } else {
7581                    return 0.0f;
7582                }
7583            } else if (getLineCount() == 1) {
7584                final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, isLayoutRtl());
7585                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
7586                    case Gravity.LEFT:
7587                        return 0.0f;
7588                    case Gravity.RIGHT:
7589                        return (mLayout.getLineRight(0) - (mRight - mLeft) -
7590                                getCompoundPaddingLeft() - getCompoundPaddingRight() -
7591                                mLayout.getLineLeft(0)) / getHorizontalFadingEdgeLength();
7592                    case Gravity.CENTER_HORIZONTAL:
7593                        return 0.0f;
7594                }
7595            }
7596        }
7597        return super.getLeftFadingEdgeStrength();
7598    }
7599
7600    @Override
7601    protected float getRightFadingEdgeStrength() {
7602        if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return 0.0f;
7603        if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
7604            if (mMarquee != null && !mMarquee.isStopped()) {
7605                final Marquee marquee = mMarquee;
7606                return (marquee.mMaxFadeScroll - marquee.mScroll) / getHorizontalFadingEdgeLength();
7607            } else if (getLineCount() == 1) {
7608                final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, isLayoutRtl());
7609                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
7610                    case Gravity.LEFT:
7611                        final int textWidth = (mRight - mLeft) - getCompoundPaddingLeft() -
7612                                getCompoundPaddingRight();
7613                        final float lineWidth = mLayout.getLineWidth(0);
7614                        return (lineWidth - textWidth) / getHorizontalFadingEdgeLength();
7615                    case Gravity.RIGHT:
7616                        return 0.0f;
7617                    case Gravity.CENTER_HORIZONTAL:
7618                    case Gravity.FILL_HORIZONTAL:
7619                        return (mLayout.getLineWidth(0) - ((mRight - mLeft) -
7620                                getCompoundPaddingLeft() - getCompoundPaddingRight())) /
7621                                getHorizontalFadingEdgeLength();
7622                }
7623            }
7624        }
7625        return super.getRightFadingEdgeStrength();
7626    }
7627
7628    @Override
7629    protected int computeHorizontalScrollRange() {
7630        if (mLayout != null) {
7631            return mSingleLine && (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT ?
7632                    (int) mLayout.getLineWidth(0) : mLayout.getWidth();
7633        }
7634
7635        return super.computeHorizontalScrollRange();
7636    }
7637
7638    @Override
7639    protected int computeVerticalScrollRange() {
7640        if (mLayout != null)
7641            return mLayout.getHeight();
7642
7643        return super.computeVerticalScrollRange();
7644    }
7645
7646    @Override
7647    protected int computeVerticalScrollExtent() {
7648        return getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom();
7649    }
7650
7651    public enum BufferType {
7652        NORMAL, SPANNABLE, EDITABLE,
7653    }
7654
7655    /**
7656     * Returns the TextView_textColor attribute from the
7657     * Resources.StyledAttributes, if set, or the TextAppearance_textColor
7658     * from the TextView_textAppearance attribute, if TextView_textColor
7659     * was not set directly.
7660     */
7661    public static ColorStateList getTextColors(Context context, TypedArray attrs) {
7662        ColorStateList colors;
7663        colors = attrs.getColorStateList(com.android.internal.R.styleable.
7664                                         TextView_textColor);
7665
7666        if (colors == null) {
7667            int ap = attrs.getResourceId(com.android.internal.R.styleable.
7668                                         TextView_textAppearance, -1);
7669            if (ap != -1) {
7670                TypedArray appearance;
7671                appearance = context.obtainStyledAttributes(ap,
7672                                            com.android.internal.R.styleable.TextAppearance);
7673                colors = appearance.getColorStateList(com.android.internal.R.styleable.
7674                                                  TextAppearance_textColor);
7675                appearance.recycle();
7676            }
7677        }
7678
7679        return colors;
7680    }
7681
7682    /**
7683     * Returns the default color from the TextView_textColor attribute
7684     * from the AttributeSet, if set, or the default color from the
7685     * TextAppearance_textColor from the TextView_textAppearance attribute,
7686     * if TextView_textColor was not set directly.
7687     */
7688    public static int getTextColor(Context context,
7689                                   TypedArray attrs,
7690                                   int def) {
7691        ColorStateList colors = getTextColors(context, attrs);
7692
7693        if (colors == null) {
7694            return def;
7695        } else {
7696            return colors.getDefaultColor();
7697        }
7698    }
7699
7700    @Override
7701    public boolean onKeyShortcut(int keyCode, KeyEvent event) {
7702        final int filteredMetaState = event.getMetaState() & ~KeyEvent.META_CTRL_MASK;
7703        if (KeyEvent.metaStateHasNoModifiers(filteredMetaState)) {
7704            switch (keyCode) {
7705            case KeyEvent.KEYCODE_A:
7706                if (canSelectText()) {
7707                    return onTextContextMenuItem(ID_SELECT_ALL);
7708                }
7709                break;
7710            case KeyEvent.KEYCODE_X:
7711                if (canCut()) {
7712                    return onTextContextMenuItem(ID_CUT);
7713                }
7714                break;
7715            case KeyEvent.KEYCODE_C:
7716                if (canCopy()) {
7717                    return onTextContextMenuItem(ID_COPY);
7718                }
7719                break;
7720            case KeyEvent.KEYCODE_V:
7721                if (canPaste()) {
7722                    return onTextContextMenuItem(ID_PASTE);
7723                }
7724                break;
7725            }
7726        }
7727        return super.onKeyShortcut(keyCode, event);
7728    }
7729
7730    /**
7731     * Unlike {@link #textCanBeSelected()}, this method is based on the <i>current</i> state of the
7732     * TextView. {@link #textCanBeSelected()} has to be true (this is one of the conditions to have
7733     * a selection controller (see {@link #prepareCursorControllers()}), but this is not sufficient.
7734     */
7735    private boolean canSelectText() {
7736        return hasSelectionController() && mText.length() != 0;
7737    }
7738
7739    /**
7740     * Test based on the <i>intrinsic</i> charateristics of the TextView.
7741     * The text must be spannable and the movement method must allow for arbitary selection.
7742     *
7743     * See also {@link #canSelectText()}.
7744     */
7745    private boolean textCanBeSelected() {
7746        // prepareCursorController() relies on this method.
7747        // If you change this condition, make sure prepareCursorController is called anywhere
7748        // the value of this condition might be changed.
7749        return mText instanceof Spannable && mMovement != null && mMovement.canSelectArbitrarily();
7750    }
7751
7752    private boolean canCut() {
7753        if (hasPasswordTransformationMethod()) {
7754            return false;
7755        }
7756
7757        if (mText.length() > 0 && hasSelection() && mText instanceof Editable && mInput != null) {
7758            return true;
7759        }
7760
7761        return false;
7762    }
7763
7764    private boolean canCopy() {
7765        if (hasPasswordTransformationMethod()) {
7766            return false;
7767        }
7768
7769        if (mText.length() > 0 && hasSelection()) {
7770            return true;
7771        }
7772
7773        return false;
7774    }
7775
7776    private boolean canPaste() {
7777        return (mText instanceof Editable &&
7778                mInput != null &&
7779                getSelectionStart() >= 0 &&
7780                getSelectionEnd() >= 0 &&
7781                ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)).
7782                hasPrimaryClip());
7783    }
7784
7785    private static long packRangeInLong(int start, int end) {
7786        return (((long) start) << 32) | end;
7787    }
7788
7789    private static int extractRangeStartFromLong(long range) {
7790        return (int) (range >>> 32);
7791    }
7792
7793    private static int extractRangeEndFromLong(long range) {
7794        return (int) (range & 0x00000000FFFFFFFFL);
7795    }
7796
7797    private boolean selectAll() {
7798        final int length = mText.length();
7799        Selection.setSelection((Spannable) mText, 0, length);
7800        return length > 0;
7801    }
7802
7803    /**
7804     * Adjusts selection to the word under last touch offset.
7805     * Return true if the operation was successfully performed.
7806     */
7807    private boolean selectCurrentWord() {
7808        if (!canSelectText()) {
7809            return false;
7810        }
7811
7812        if (hasPasswordTransformationMethod()) {
7813            // Always select all on a password field.
7814            // Cut/copy menu entries are not available for passwords, but being able to select all
7815            // is however useful to delete or paste to replace the entire content.
7816            return selectAll();
7817        }
7818
7819        int klass = mInputType & InputType.TYPE_MASK_CLASS;
7820        int variation = mInputType & InputType.TYPE_MASK_VARIATION;
7821
7822        // Specific text field types: select the entire text for these
7823        if (klass == InputType.TYPE_CLASS_NUMBER ||
7824                klass == InputType.TYPE_CLASS_PHONE ||
7825                klass == InputType.TYPE_CLASS_DATETIME ||
7826                variation == InputType.TYPE_TEXT_VARIATION_URI ||
7827                variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
7828                variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ||
7829                variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
7830            return selectAll();
7831        }
7832
7833        long lastTouchOffsets = getLastTouchOffsets();
7834        final int minOffset = extractRangeStartFromLong(lastTouchOffsets);
7835        final int maxOffset = extractRangeEndFromLong(lastTouchOffsets);
7836
7837        int selectionStart, selectionEnd;
7838
7839        // If a URLSpan (web address, email, phone...) is found at that position, select it.
7840        URLSpan[] urlSpans = ((Spanned) mText).getSpans(minOffset, maxOffset, URLSpan.class);
7841        if (urlSpans.length == 1) {
7842            URLSpan url = urlSpans[0];
7843            selectionStart = ((Spanned) mText).getSpanStart(url);
7844            selectionEnd = ((Spanned) mText).getSpanEnd(url);
7845        } else {
7846            if (mWordIterator == null) {
7847                mWordIterator = new WordIterator();
7848            }
7849            // WordIerator handles text changes, this is a no-op if text in unchanged.
7850            mWordIterator.setCharSequence(mText);
7851
7852            selectionStart = mWordIterator.getBeginning(minOffset);
7853            if (selectionStart == BreakIterator.DONE) return false;
7854
7855            selectionEnd = mWordIterator.getEnd(maxOffset);
7856            if (selectionEnd == BreakIterator.DONE) return false;
7857        }
7858
7859        Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
7860        return true;
7861    }
7862
7863    private long getLastTouchOffsets() {
7864        int minOffset, maxOffset;
7865
7866        if (mContextMenuTriggeredByKey) {
7867            minOffset = getSelectionStart();
7868            maxOffset = getSelectionEnd();
7869        } else {
7870            SelectionModifierCursorController selectionController = getSelectionController();
7871            minOffset = selectionController.getMinTouchOffset();
7872            maxOffset = selectionController.getMaxTouchOffset();
7873        }
7874
7875        return packRangeInLong(minOffset, maxOffset);
7876    }
7877
7878    @Override
7879    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
7880        super.onPopulateAccessibilityEvent(event);
7881
7882        final boolean isPassword = hasPasswordTransformationMethod();
7883        if (!isPassword) {
7884            CharSequence text = getText();
7885            if (TextUtils.isEmpty(text)) {
7886                text = getHint();
7887            }
7888            if (!TextUtils.isEmpty(text)) {
7889                event.getText().add(text);
7890            }
7891        }
7892    }
7893
7894    @Override
7895    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
7896        super.onInitializeAccessibilityEvent(event);
7897
7898        final boolean isPassword = hasPasswordTransformationMethod();
7899        event.setPassword(isPassword);
7900    }
7901
7902    void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText,
7903            int fromIndex, int removedCount, int addedCount) {
7904        AccessibilityEvent event =
7905            AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
7906        event.setFromIndex(fromIndex);
7907        event.setRemovedCount(removedCount);
7908        event.setAddedCount(addedCount);
7909        event.setBeforeText(beforeText);
7910        sendAccessibilityEventUnchecked(event);
7911    }
7912
7913    @Override
7914    protected void onCreateContextMenu(ContextMenu menu) {
7915        super.onCreateContextMenu(menu);
7916        boolean added = false;
7917        mContextMenuTriggeredByKey = mDPadCenterIsDown || mEnterKeyIsDown;
7918        // Problem with context menu on long press: the menu appears while the key in down and when
7919        // the key is released, the view does not receive the key_up event.
7920        // We need two layers of flags: mDPadCenterIsDown and mEnterKeyIsDown are set in key down/up
7921        // events. We cannot simply clear these flags in onTextContextMenuItem since
7922        // it may not be called (if the user/ discards the context menu with the back key).
7923        // We clear these flags here and mContextMenuTriggeredByKey saves that state so that it is
7924        // available in onTextContextMenuItem.
7925        mDPadCenterIsDown = mEnterKeyIsDown = false;
7926
7927        MenuHandler handler = new MenuHandler();
7928
7929        if (mText instanceof Spanned && hasSelectionController()) {
7930            long lastTouchOffset = getLastTouchOffsets();
7931            final int selStart = extractRangeStartFromLong(lastTouchOffset);
7932            final int selEnd = extractRangeEndFromLong(lastTouchOffset);
7933
7934            URLSpan[] urls = ((Spanned) mText).getSpans(selStart, selEnd, URLSpan.class);
7935            if (urls.length > 0) {
7936                menu.add(0, ID_COPY_URL, 0, com.android.internal.R.string.copyUrl).
7937                        setOnMenuItemClickListener(handler);
7938
7939                added = true;
7940            }
7941        }
7942
7943        // The context menu is not empty, which will prevent the selection mode from starting.
7944        // Add a entry to start it in the context menu.
7945        // TODO Does not handle the case where a subclass does not call super.thisMethod or
7946        // populates the menu AFTER this call.
7947        if (menu.size() > 0) {
7948            menu.add(0, ID_SELECTION_MODE, 0, com.android.internal.R.string.selectTextMode).
7949                    setOnMenuItemClickListener(handler);
7950            added = true;
7951        }
7952
7953        if (added) {
7954            menu.setHeaderTitle(com.android.internal.R.string.editTextMenuTitle);
7955        }
7956    }
7957
7958    /**
7959     * Returns whether this text view is a current input method target.  The
7960     * default implementation just checks with {@link InputMethodManager}.
7961     */
7962    public boolean isInputMethodTarget() {
7963        InputMethodManager imm = InputMethodManager.peekInstance();
7964        return imm != null && imm.isActive(this);
7965    }
7966
7967    // Selection context mode
7968    private static final int ID_SELECT_ALL = android.R.id.selectAll;
7969    private static final int ID_CUT = android.R.id.cut;
7970    private static final int ID_COPY = android.R.id.copy;
7971    private static final int ID_PASTE = android.R.id.paste;
7972    // Context menu entries
7973    private static final int ID_COPY_URL = android.R.id.copyUrl;
7974    private static final int ID_SELECTION_MODE = android.R.id.selectTextMode;
7975
7976    private class MenuHandler implements MenuItem.OnMenuItemClickListener {
7977        public boolean onMenuItemClick(MenuItem item) {
7978            return onTextContextMenuItem(item.getItemId());
7979        }
7980    }
7981
7982    /**
7983     * Called when a context menu option for the text view is selected.  Currently
7984     * this will be {@link android.R.id#copyUrl}, {@link android.R.id#selectTextMode},
7985     * {@link android.R.id#selectAll}, {@link android.R.id#paste}, {@link android.R.id#cut}
7986     * or {@link android.R.id#copy}.
7987     *
7988     * @return true if the context menu item action was performed.
7989     */
7990    public boolean onTextContextMenuItem(int id) {
7991        int min = 0;
7992        int max = mText.length();
7993
7994        if (isFocused()) {
7995            final int selStart = getSelectionStart();
7996            final int selEnd = getSelectionEnd();
7997
7998            min = Math.max(0, Math.min(selStart, selEnd));
7999            max = Math.max(0, Math.max(selStart, selEnd));
8000        }
8001
8002        switch (id) {
8003            case ID_COPY_URL:
8004                URLSpan[] urls = ((Spanned) mText).getSpans(min, max, URLSpan.class);
8005                if (urls.length >= 1) {
8006                    ClipData clip = null;
8007                    for (int i=0; i<urls.length; i++) {
8008                        Uri uri = Uri.parse(urls[0].getURL());
8009                        if (clip == null) {
8010                            clip = ClipData.newRawUri(null, uri);
8011                        } else {
8012                            clip.addItem(new ClipData.Item(uri));
8013                        }
8014                    }
8015                    if (clip != null) {
8016                        setPrimaryClip(clip);
8017                    }
8018                }
8019                stopSelectionActionMode();
8020                return true;
8021
8022            case ID_SELECTION_MODE:
8023                if (mSelectionActionMode != null) {
8024                    // Selection mode is already started, simply change selected part.
8025                    selectCurrentWord();
8026                } else {
8027                    startSelectionActionMode();
8028                }
8029                return true;
8030
8031            case ID_SELECT_ALL:
8032                // This does not enter text selection mode. Text is highlighted, so that it can be
8033                // bulk edited, like selectAllOnFocus does. Returns true even if text is empty.
8034                selectAll();
8035                return true;
8036
8037            case ID_PASTE:
8038                paste(min, max);
8039                return true;
8040
8041            case ID_CUT:
8042                setPrimaryClip(ClipData.newPlainText(null, mTransformed.subSequence(min, max)));
8043                ((Editable) mText).delete(min, max);
8044                stopSelectionActionMode();
8045                return true;
8046
8047            case ID_COPY:
8048                setPrimaryClip(ClipData.newPlainText(null, mTransformed.subSequence(min, max)));
8049                stopSelectionActionMode();
8050                return true;
8051        }
8052        return false;
8053    }
8054
8055    /**
8056     * Prepare text so that there are not zero or two spaces at beginning and end of region defined
8057     * by [min, max] when replacing this region by paste.
8058     * Note that if there were two spaces (or more) at that position before, they are kept. We just
8059     * make sure we do not add an extra one from the paste content.
8060     */
8061    private long prepareSpacesAroundPaste(int min, int max, CharSequence paste) {
8062        if (paste.length() > 0) {
8063            if (min > 0) {
8064                final char charBefore = mTransformed.charAt(min - 1);
8065                final char charAfter = paste.charAt(0);
8066
8067                if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
8068                    // Two spaces at beginning of paste: remove one
8069                    final int originalLength = mText.length();
8070                    ((Editable) mText).delete(min - 1, min);
8071                    // Due to filters, there is no guarantee that exactly one character was
8072                    // removed: count instead.
8073                    final int delta = mText.length() - originalLength;
8074                    min += delta;
8075                    max += delta;
8076                } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
8077                        !Character.isSpaceChar(charAfter) && charAfter != '\n') {
8078                    // No space at beginning of paste: add one
8079                    final int originalLength = mText.length();
8080                    ((Editable) mText).replace(min, min, " ");
8081                    // Taking possible filters into account as above.
8082                    final int delta = mText.length() - originalLength;
8083                    min += delta;
8084                    max += delta;
8085                }
8086            }
8087
8088            if (max < mText.length()) {
8089                final char charBefore = paste.charAt(paste.length() - 1);
8090                final char charAfter = mTransformed.charAt(max);
8091
8092                if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
8093                    // Two spaces at end of paste: remove one
8094                    ((Editable) mText).delete(max, max + 1);
8095                } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
8096                        !Character.isSpaceChar(charAfter) && charAfter != '\n') {
8097                    // No space at end of paste: add one
8098                    ((Editable) mText).replace(max, max, " ");
8099                }
8100            }
8101        }
8102
8103        return packRangeInLong(min, max);
8104    }
8105
8106    private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) {
8107        TextView shadowView = (TextView) inflate(mContext,
8108                com.android.internal.R.layout.text_drag_thumbnail, null);
8109
8110        if (shadowView == null) {
8111            throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
8112        }
8113
8114        if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) {
8115            text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH);
8116        }
8117        shadowView.setText(text);
8118        shadowView.setTextColor(getTextColors());
8119
8120        shadowView.setTextAppearance(mContext, R.styleable.Theme_textAppearanceLarge);
8121        shadowView.setGravity(Gravity.CENTER);
8122
8123        shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
8124                ViewGroup.LayoutParams.WRAP_CONTENT));
8125
8126        final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
8127        shadowView.measure(size, size);
8128
8129        shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
8130        shadowView.invalidate();
8131        return new DragShadowBuilder(shadowView);
8132    }
8133
8134    private static class DragLocalState {
8135        public TextView sourceTextView;
8136        public int start, end;
8137
8138        public DragLocalState(TextView sourceTextView, int start, int end) {
8139            this.sourceTextView = sourceTextView;
8140            this.start = start;
8141            this.end = end;
8142        }
8143    }
8144
8145    @Override
8146    public boolean performLongClick() {
8147        if (super.performLongClick()) {
8148            mDiscardNextActionUp = true;
8149            return true;
8150        }
8151
8152        boolean handled = false;
8153
8154        // Long press in empty space moves cursor and shows the Paste affordance if available.
8155        if (!isPositionOnText(mLastDownPositionX, mLastDownPositionY) &&
8156                mInsertionControllerEnabled) {
8157            final int offset = getOffsetForPosition(mLastDownPositionX, mLastDownPositionY);
8158            stopSelectionActionMode();
8159            Selection.setSelection((Spannable) mText, offset);
8160            getInsertionController().showWithPaste();
8161            handled = true;
8162        }
8163
8164        if (!handled && mSelectionActionMode != null) {
8165            if (touchPositionIsInSelection()) {
8166                // Start a drag
8167                final int start = getSelectionStart();
8168                final int end = getSelectionEnd();
8169                CharSequence selectedText = mTransformed.subSequence(start, end);
8170                ClipData data = ClipData.newPlainText(null, selectedText);
8171                DragLocalState localState = new DragLocalState(this, start, end);
8172                startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0);
8173                stopSelectionActionMode();
8174            } else {
8175                selectCurrentWord();
8176            }
8177            handled = true;
8178        }
8179
8180        // Start a new selection
8181        handled |= !handled && startSelectionActionMode();
8182
8183        if (handled) {
8184            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
8185            mDiscardNextActionUp = true;
8186        }
8187
8188        return handled;
8189    }
8190
8191    private boolean touchPositionIsInSelection() {
8192        int selectionStart = getSelectionStart();
8193        int selectionEnd = getSelectionEnd();
8194
8195        if (selectionStart == selectionEnd) {
8196            return false;
8197        }
8198
8199        if (selectionStart > selectionEnd) {
8200            int tmp = selectionStart;
8201            selectionStart = selectionEnd;
8202            selectionEnd = tmp;
8203            Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
8204        }
8205
8206        SelectionModifierCursorController selectionController = getSelectionController();
8207        int minOffset = selectionController.getMinTouchOffset();
8208        int maxOffset = selectionController.getMaxTouchOffset();
8209
8210        return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
8211    }
8212
8213    private static class SuggestionRangeSpan extends UnderlineSpan {
8214        // TODO themable, would be nice to make it a child class of TextAppearanceSpan, but
8215        // there is no way to have underline and TextAppearanceSpan.
8216    }
8217
8218    private class SuggestionsPopupWindow implements OnClickListener {
8219        private static final int MAX_NUMBER_SUGGESTIONS = 5;
8220        private static final int NO_SUGGESTIONS = -1;
8221        private final PopupWindow mContainer;
8222        private final ViewGroup[] mSuggestionViews = new ViewGroup[2];
8223        private final int[] mSuggestionViewLayouts = new int[] {
8224                mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout};
8225        private WordIterator mSuggestionWordIterator;
8226        private TextAppearanceSpan[] mHighlightSpans = new TextAppearanceSpan[0];
8227
8228        public SuggestionsPopupWindow() {
8229            mContainer = new PopupWindow(TextView.this.mContext, null,
8230                    com.android.internal.R.attr.textSuggestionsWindowStyle);
8231            mContainer.setSplitTouchEnabled(true);
8232            mContainer.setClippingEnabled(false);
8233            mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
8234
8235            mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
8236            mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
8237        }
8238
8239        private class SuggestionInfo {
8240            int suggestionStart, suggestionEnd; // range of suggestion item with replacement text
8241            int spanStart, spanEnd; // range in TextView where text should be inserted
8242            SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents
8243            int suggestionIndex; // the index of the suggestion inside suggestionSpan
8244        }
8245
8246        private ViewGroup getViewGroup(boolean under) {
8247            final int viewIndex = under ? 0 : 1;
8248            ViewGroup viewGroup = mSuggestionViews[viewIndex];
8249
8250            if (viewGroup == null) {
8251                final int layout = mSuggestionViewLayouts[viewIndex];
8252                LayoutInflater inflater = (LayoutInflater) TextView.this.mContext.
8253                        getSystemService(Context.LAYOUT_INFLATER_SERVICE);
8254
8255                if (inflater == null) {
8256                    throw new IllegalArgumentException(
8257                            "Unable to create TextEdit suggestion window inflater");
8258                }
8259
8260                View view = inflater.inflate(layout, null);
8261
8262                if (! (view instanceof ViewGroup)) {
8263                    throw new IllegalArgumentException(
8264                            "Inflated TextEdit suggestion window is not a ViewGroup: " + view);
8265                }
8266
8267                viewGroup = (ViewGroup) view;
8268
8269                // Inflate the suggestion items once and for all.
8270                for (int i = 0; i < MAX_NUMBER_SUGGESTIONS; i++) {
8271                    View childView = inflater.inflate(mTextEditSuggestionItemLayout, viewGroup,
8272                            false);
8273
8274                    if (! (childView instanceof TextView)) {
8275                        throw new IllegalArgumentException(
8276                               "Inflated TextEdit suggestion item is not a TextView: " + childView);
8277                    }
8278
8279                    childView.setTag(new SuggestionInfo());
8280                    viewGroup.addView(childView);
8281                    childView.setOnClickListener(this);
8282                }
8283
8284                mSuggestionViews[viewIndex] = viewGroup;
8285            }
8286
8287            return viewGroup;
8288        }
8289
8290        public void show() {
8291            if (!(mText instanceof Editable)) return;
8292
8293            final int pos = TextView.this.getSelectionStart();
8294            Spannable spannable = (Spannable)TextView.this.mText;
8295            SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
8296            final int nbSpans = suggestionSpans.length;
8297
8298            ViewGroup viewGroup = getViewGroup(true);
8299            mContainer.setContentView(viewGroup);
8300
8301            int totalNbSuggestions = 0;
8302            int spanUnionStart = mText.length();
8303            int spanUnionEnd = 0;
8304
8305            for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) {
8306                SuggestionSpan suggestionSpan = suggestionSpans[spanIndex];
8307                final int spanStart = spannable.getSpanStart(suggestionSpan);
8308                final int spanEnd = spannable.getSpanEnd(suggestionSpan);
8309                spanUnionStart = Math.min(spanStart, spanUnionStart);
8310                spanUnionEnd = Math.max(spanEnd, spanUnionEnd);
8311
8312                String[] suggestions = suggestionSpan.getSuggestions();
8313                int nbSuggestions = suggestions.length;
8314                for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
8315                    TextView textView = (TextView) viewGroup.getChildAt(totalNbSuggestions);
8316                    textView.setText(suggestions[suggestionIndex]);
8317                    SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag();
8318                    suggestionInfo.spanStart = spanStart;
8319                    suggestionInfo.spanEnd = spanEnd;
8320                    suggestionInfo.suggestionSpan = suggestionSpan;
8321                    suggestionInfo.suggestionIndex = suggestionIndex;
8322
8323                    totalNbSuggestions++;
8324                    if (totalNbSuggestions == MAX_NUMBER_SUGGESTIONS) {
8325                        // Also end outer for loop
8326                        spanIndex = nbSpans;
8327                        break;
8328                    }
8329                }
8330            }
8331
8332            if (totalNbSuggestions == 0) {
8333                // TODO Replace by final text, use a dedicated layout, add a fade out timer...
8334                TextView textView = (TextView) viewGroup.getChildAt(0);
8335                textView.setText("No suggestions available");
8336                SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag();
8337                suggestionInfo.spanStart = NO_SUGGESTIONS;
8338                totalNbSuggestions++;
8339            } else {
8340                if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
8341                ((Editable) mText).setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
8342                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
8343
8344                for (int i = 0; i < totalNbSuggestions; i++) {
8345                    final TextView textView = (TextView) viewGroup.getChildAt(i);
8346                    highlightTextDifferences(textView, spanUnionStart, spanUnionEnd);
8347                }
8348            }
8349
8350            for (int i = 0; i < MAX_NUMBER_SUGGESTIONS; i++) {
8351                viewGroup.getChildAt(i).setVisibility(i < totalNbSuggestions ? VISIBLE : GONE);
8352            }
8353
8354            final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
8355            viewGroup.measure(size, size);
8356
8357            positionAtCursor();
8358        }
8359
8360        private long[] getWordLimits(CharSequence text) {
8361            // TODO locale for mSuggestionWordIterator
8362            if (mSuggestionWordIterator == null) mSuggestionWordIterator = new WordIterator();
8363            mSuggestionWordIterator.setCharSequence(text);
8364
8365            // First pass will simply count the number of words to be able to create an array
8366            // Not too expensive since previous break positions are cached by the BreakIterator
8367            int nbWords = 0;
8368            int position = mSuggestionWordIterator.following(0);
8369            while (position != BreakIterator.DONE) {
8370                nbWords++;
8371                position = mSuggestionWordIterator.following(position);
8372            }
8373
8374            int index = 0;
8375            long[] result = new long[nbWords];
8376
8377            position = mSuggestionWordIterator.following(0);
8378            while (position != BreakIterator.DONE) {
8379                int wordStart = mSuggestionWordIterator.getBeginning(position);
8380                result[index++] = packRangeInLong(wordStart, position);
8381                position = mSuggestionWordIterator.following(position);
8382            }
8383
8384            return result;
8385        }
8386
8387        private TextAppearanceSpan highlightSpan(int index) {
8388            final int length = mHighlightSpans.length;
8389            if (index < length) {
8390                return mHighlightSpans[index];
8391            }
8392
8393            // Assumes indexes are requested in sequence: simply append one more item
8394            TextAppearanceSpan[] newArray = new TextAppearanceSpan[length + 1];
8395            System.arraycopy(mHighlightSpans, 0, newArray, 0, length);
8396            TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mContext,
8397                    android.R.style.TextAppearance_SuggestionHighlight);
8398            newArray[length] = highlightSpan;
8399            mHighlightSpans = newArray;
8400            return highlightSpan;
8401        }
8402
8403        private void highlightTextDifferences(TextView textView, int unionStart, int unionEnd) {
8404            SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag();
8405            final int spanStart = suggestionInfo.spanStart;
8406            final int spanEnd = suggestionInfo.spanEnd;
8407
8408            // Remove all text formating by converting to Strings
8409            final String text = textView.getText().toString();
8410            final String sourceText = mText.subSequence(spanStart, spanEnd).toString();
8411
8412            long[] sourceWordLimits = getWordLimits(sourceText);
8413            long[] wordLimits = getWordLimits(text);
8414
8415            SpannableStringBuilder ssb = new SpannableStringBuilder();
8416            // span [spanStart, spanEnd] is included in union [spanUnionStart, int spanUnionEnd]
8417            // The final result is made of 3 parts: the text before, between and after the span
8418            // This is the text before, provided for context
8419            ssb.append(mText.subSequence(unionStart, spanStart).toString());
8420
8421            // shift is used to offset spans positions wrt span's beginning
8422            final int shift = spanStart - unionStart;
8423            suggestionInfo.suggestionStart = shift;
8424            suggestionInfo.suggestionEnd = shift + text.length();
8425
8426            // This is the actual suggestion text, which will be highlighted by the following code
8427            ssb.append(text);
8428
8429            String[] words = new String[wordLimits.length];
8430            for (int i = 0; i < wordLimits.length; i++) {
8431                int wordStart = extractRangeStartFromLong(wordLimits[i]);
8432                int wordEnd = extractRangeEndFromLong(wordLimits[i]);
8433                words[i] = text.substring(wordStart, wordEnd);
8434            }
8435
8436            // Highlighted word algorithm is based on word matching between source and text
8437            // Matching words are found from left to right. TODO: change for RTL languages
8438            // Characters between matching words are highlighted
8439            int previousCommonWordIndex = -1;
8440            int nbHighlightSpans = 0;
8441            for (int i = 0; i < sourceWordLimits.length; i++) {
8442                int wordStart = extractRangeStartFromLong(sourceWordLimits[i]);
8443                int wordEnd = extractRangeEndFromLong(sourceWordLimits[i]);
8444                String sourceWord = sourceText.substring(wordStart, wordEnd);
8445
8446                for (int j = previousCommonWordIndex + 1; j < words.length; j++) {
8447                    if (sourceWord.equals(words[j])) {
8448                        if (j != previousCommonWordIndex + 1) {
8449                            int firstDifferentPosition = previousCommonWordIndex < 0 ? 0 :
8450                                extractRangeEndFromLong(wordLimits[previousCommonWordIndex]);
8451                            int lastDifferentPosition = extractRangeStartFromLong(wordLimits[j]);
8452                            ssb.setSpan(highlightSpan(nbHighlightSpans++),
8453                                    shift + firstDifferentPosition, shift + lastDifferentPosition,
8454                                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
8455                        } else {
8456                            // Compare characters between words
8457                            int previousSourceWordEnd = i == 0 ? 0 :
8458                                extractRangeEndFromLong(sourceWordLimits[i - 1]);
8459                            int sourceWordStart = extractRangeStartFromLong(sourceWordLimits[i]);
8460                            String sourceSpaces = sourceText.substring(previousSourceWordEnd,
8461                                    sourceWordStart);
8462
8463                            int previousWordEnd = j == 0 ? 0 :
8464                                extractRangeEndFromLong(wordLimits[j - 1]);
8465                            int currentWordStart = extractRangeStartFromLong(wordLimits[j]);
8466                            String textSpaces = text.substring(previousWordEnd, currentWordStart);
8467
8468                            if (!sourceSpaces.equals(textSpaces)) {
8469                                ssb.setSpan(highlightSpan(nbHighlightSpans++),
8470                                        shift + previousWordEnd, shift + currentWordStart,
8471                                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
8472                            }
8473                        }
8474                        previousCommonWordIndex = j;
8475                        break;
8476                    }
8477                }
8478            }
8479
8480            // Finally, compare ends of Strings
8481            if (previousCommonWordIndex < words.length - 1) {
8482                int firstDifferentPosition = previousCommonWordIndex < 0 ? 0 :
8483                    extractRangeEndFromLong(wordLimits[previousCommonWordIndex]);
8484                int lastDifferentPosition = textView.length();
8485                ssb.setSpan(highlightSpan(nbHighlightSpans++),
8486                        shift + firstDifferentPosition, shift + lastDifferentPosition,
8487                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
8488            } else {
8489                int lastSourceWordEnd = sourceWordLimits.length == 0 ? 0 :
8490                    extractRangeEndFromLong(sourceWordLimits[sourceWordLimits.length - 1]);
8491                String sourceSpaces = sourceText.substring(lastSourceWordEnd, sourceText.length());
8492
8493                int lastCommonTextWordEnd = previousCommonWordIndex < 0 ? 0 :
8494                    extractRangeEndFromLong(wordLimits[previousCommonWordIndex]);
8495                String textSpaces = text.substring(lastCommonTextWordEnd, textView.length());
8496
8497                if (!sourceSpaces.equals(textSpaces) && textSpaces.length() > 0) {
8498                    ssb.setSpan(highlightSpan(nbHighlightSpans++),
8499                            shift + lastCommonTextWordEnd, shift + textView.length(),
8500                            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
8501                }
8502            }
8503
8504            // Final part, text after the current suggestion range.
8505            ssb.append(mText.subSequence(spanEnd, unionEnd).toString());
8506            textView.setText(ssb);
8507        }
8508
8509        public void hide() {
8510            if ((mText instanceof Editable) && mSuggestionRangeSpan != null) {
8511                ((Editable) mText).removeSpan(mSuggestionRangeSpan);
8512            }
8513            mContainer.dismiss();
8514        }
8515
8516        @Override
8517        public void onClick(View view) {
8518            if (view instanceof TextView) {
8519                TextView textView = (TextView) view;
8520                SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag();
8521                final int spanStart = suggestionInfo.spanStart;
8522                final int spanEnd = suggestionInfo.spanEnd;
8523                if (spanStart != NO_SUGGESTIONS) {
8524                    // SuggestionSpans are removed by replace: save them before
8525                    Editable editable = ((Editable) mText);
8526                    SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
8527                            SuggestionSpan.class);
8528                    final int length = suggestionSpans.length;
8529                    int[] suggestionSpansStarts = new int[length];
8530                    int[] suggestionSpansEnds = new int[length];
8531                    int[] suggestionSpansFlags = new int[length];
8532                    for (int i = 0; i < length; i++) {
8533                        final SuggestionSpan suggestionSpan = suggestionSpans[i];
8534                        suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
8535                        suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
8536                        suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
8537                    }
8538
8539                    final int suggestionStart = suggestionInfo.suggestionStart;
8540                    final int suggestionEnd = suggestionInfo.suggestionEnd;
8541                    final String suggestion = textView.getText().subSequence(
8542                            suggestionStart, suggestionEnd).toString();
8543                    final String originalText = mText.subSequence(spanStart, spanEnd).toString();
8544                    ((Editable) mText).replace(spanStart, spanEnd, suggestion);
8545
8546                    // Swap text content between actual text and Suggestion span
8547                    String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions();
8548                    suggestions[suggestionInfo.suggestionIndex] = originalText;
8549
8550                    // Notify source IME of the suggestion pick
8551                    if (!TextUtils.isEmpty(
8552                            suggestionInfo.suggestionSpan.getNotificationTargetClassName())) {
8553                        InputMethodManager imm = InputMethodManager.peekInstance();
8554                        imm.notifySuggestionPicked(suggestionInfo.suggestionSpan, originalText,
8555                                suggestionInfo.suggestionIndex);
8556                    }
8557
8558                    // Restore previous SuggestionSpans
8559                    final int lengthDifference = suggestion.length() - (spanEnd - spanStart);
8560                    for (int i = 0; i < length; i++) {
8561                        // Only spans that include the modified region make sense after replacement
8562                        // Spans partially included in the replaced region are removed, there is no
8563                        // way to assign them a valid range after replacement
8564                        if (suggestionSpansStarts[i] <= spanStart &&
8565                                suggestionSpansEnds[i] >= spanEnd) {
8566                            editable.setSpan(suggestionSpans[i], suggestionSpansStarts[i],
8567                                    suggestionSpansEnds[i] + lengthDifference,
8568                                    suggestionSpansFlags[i]);
8569                        }
8570                    }
8571                }
8572            }
8573            hide();
8574        }
8575
8576        void positionAtCursor() {
8577            View contentView = mContainer.getContentView();
8578            int width = contentView.getMeasuredWidth();
8579            int height = contentView.getMeasuredHeight();
8580            final int offset = TextView.this.getSelectionStart();
8581            final int line = mLayout.getLineForOffset(offset);
8582            final int lineBottom = mLayout.getLineBottom(line);
8583            float primaryHorizontal = mLayout.getPrimaryHorizontal(offset);
8584
8585            final Rect bounds = sCursorControllerTempRect;
8586            bounds.left = (int) (primaryHorizontal - width / 2.0f);
8587            bounds.top = lineBottom;
8588
8589            bounds.right = bounds.left + width;
8590            bounds.bottom = bounds.top + height;
8591
8592            convertFromViewportToContentCoordinates(bounds);
8593
8594            final int[] coords = mTempCoords;
8595            TextView.this.getLocationInWindow(coords);
8596            coords[0] += bounds.left;
8597            coords[1] += bounds.top;
8598
8599            final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
8600            final int screenHeight = displayMetrics.heightPixels;
8601
8602            // Vertical clipping
8603            if (coords[1] + height > screenHeight) {
8604                // Try to position above current line instead
8605                // TODO use top layout instead, reverse suggestion order,
8606                // try full screen vertical down if it still does not fit. TBD with designers.
8607
8608                // Update dimensions from new view
8609                contentView = mContainer.getContentView();
8610                width = contentView.getMeasuredWidth();
8611                height = contentView.getMeasuredHeight();
8612
8613                final int lineTop = mLayout.getLineTop(line);
8614                final int lineHeight = lineBottom - lineTop;
8615                coords[1] -= height + lineHeight;
8616            }
8617
8618            // Horizontal clipping
8619            coords[0] = Math.max(0, coords[0]);
8620            coords[0] = Math.min(displayMetrics.widthPixels - width, coords[0]);
8621
8622            mContainer.showAtLocation(TextView.this, Gravity.NO_GRAVITY, coords[0], coords[1]);
8623        }
8624    }
8625
8626    void showSuggestions() {
8627        if (!mSuggestionsEnabled || !isTextEditable()) return;
8628
8629        if (mSuggestionsPopupWindow == null) {
8630            mSuggestionsPopupWindow = new SuggestionsPopupWindow();
8631        }
8632        hideControllers();
8633        mSuggestionsPopupWindow.show();
8634    }
8635
8636    void hideSuggestions() {
8637        if (mSuggestionsPopupWindow != null) {
8638            mSuggestionsPopupWindow.hide();
8639        }
8640    }
8641
8642    /**
8643     * Some parts of the text can have alternate suggestion text attached. This is typically done by
8644     * the IME by adding {@link SuggestionSpan}s to the text.
8645     *
8646     * When suggestions are enabled (default), this list of suggestions will be displayed when the
8647     * user double taps on these parts of the text. No suggestions are displayed when this value is
8648     * false. Use {@link #setSuggestionsEnabled(boolean)} to change this value.
8649     *
8650     * @return true if the suggestions popup window is enabled.
8651     *
8652     * @attr ref android.R.styleable#TextView_suggestionsEnabled
8653     */
8654    public boolean isSuggestionsEnabled() {
8655        return mSuggestionsEnabled;
8656    }
8657
8658    /**
8659     * Enables or disables the suggestion popup. See {@link #isSuggestionsEnabled()}.
8660     *
8661     * @param enabled Whether or not suggestions are enabled.
8662     */
8663    public void setSuggestionsEnabled(boolean enabled) {
8664        mSuggestionsEnabled = enabled;
8665    }
8666
8667    /**
8668     * If provided, this ActionMode.Callback will be used to create the ActionMode when text
8669     * selection is initiated in this View.
8670     *
8671     * The standard implementation populates the menu with a subset of Select All, Cut, Copy and
8672     * Paste actions, depending on what this View supports.
8673     *
8674     * A custom implementation can add new entries in the default menu in its
8675     * {@link ActionMode.Callback#onPrepareActionMode(ActionMode, Menu)} method. The default actions
8676     * can also be removed from the menu using {@link Menu#removeItem(int)} and passing
8677     * {@link android.R.id#selectAll}, {@link android.R.id#cut}, {@link android.R.id#copy} or
8678     * {@link android.R.id#paste} ids as parameters.
8679     *
8680     * Returning false from {@link ActionMode.Callback#onCreateActionMode(ActionMode, Menu)} will
8681     * prevent the action mode from being started.
8682     *
8683     * Action click events should be handled by the custom implementation of
8684     * {@link ActionMode.Callback#onActionItemClicked(ActionMode, MenuItem)}.
8685     *
8686     * Note that text selection mode is not started when a TextView receives focus and the
8687     * {@link android.R.attr#selectAllOnFocus} flag has been set. The content is highlighted in
8688     * that case, to allow for quick replacement.
8689     */
8690    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
8691        mCustomSelectionActionModeCallback = actionModeCallback;
8692    }
8693
8694    /**
8695     * Retrieves the value set in {@link #setCustomSelectionActionModeCallback}. Default is null.
8696     *
8697     * @return The current custom selection callback.
8698     */
8699    public ActionMode.Callback getCustomSelectionActionModeCallback() {
8700        return mCustomSelectionActionModeCallback;
8701    }
8702
8703    /**
8704     *
8705     * @return true if the selection mode was actually started.
8706     */
8707    private boolean startSelectionActionMode() {
8708        if (mSelectionActionMode != null) {
8709            // Selection action mode is already started
8710            return false;
8711        }
8712
8713        if (!canSelectText() || !requestFocus()) {
8714            Log.w(LOG_TAG, "TextView does not support text selection. Action mode cancelled.");
8715            return false;
8716        }
8717
8718        boolean currentWordSelected = selectCurrentWord();
8719        if (!currentWordSelected) {
8720            // No word found under cursor or text selection not permitted.
8721            return false;
8722        }
8723
8724        ActionMode.Callback actionModeCallback = new SelectionActionModeCallback();
8725        mSelectionActionMode = startActionMode(actionModeCallback);
8726        final boolean selectionStarted = mSelectionActionMode != null;
8727
8728        if (selectionStarted && !mTextIsSelectable) {
8729            // Show the IME to be able to replace text, except when selecting non editable text.
8730            final InputMethodManager imm = InputMethodManager.peekInstance();
8731            if (imm != null) imm.showSoftInput(this, 0, null);
8732        }
8733
8734        return selectionStarted;
8735    }
8736
8737    private void stopSelectionActionMode() {
8738        if (mSelectionActionMode != null) {
8739            // This will hide the mSelectionModifierCursorController
8740            mSelectionActionMode.finish();
8741        }
8742    }
8743
8744    /**
8745     * Paste clipboard content between min and max positions.
8746     */
8747    private void paste(int min, int max) {
8748        ClipboardManager clipboard =
8749            (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
8750        ClipData clip = clipboard.getPrimaryClip();
8751        if (clip != null) {
8752            boolean didFirst = false;
8753            for (int i=0; i<clip.getItemCount(); i++) {
8754                CharSequence paste = clip.getItemAt(i).coerceToText(getContext());
8755                if (paste != null) {
8756                    if (!didFirst) {
8757                        long minMax = prepareSpacesAroundPaste(min, max, paste);
8758                        min = extractRangeStartFromLong(minMax);
8759                        max = extractRangeEndFromLong(minMax);
8760                        Selection.setSelection((Spannable) mText, max);
8761                        ((Editable) mText).replace(min, max, paste);
8762                        didFirst = true;
8763                    } else {
8764                        ((Editable) mText).insert(getSelectionEnd(), "\n");
8765                        ((Editable) mText).insert(getSelectionEnd(), paste);
8766                    }
8767                }
8768            }
8769            stopSelectionActionMode();
8770            sLastCutOrCopyTime = 0;
8771        }
8772    }
8773
8774    private void setPrimaryClip(ClipData clip) {
8775        ClipboardManager clipboard = (ClipboardManager) getContext().
8776                getSystemService(Context.CLIPBOARD_SERVICE);
8777        clipboard.setPrimaryClip(clip);
8778        sLastCutOrCopyTime = SystemClock.uptimeMillis();
8779    }
8780
8781    /**
8782     * An ActionMode Callback class that is used to provide actions while in text selection mode.
8783     *
8784     * The default callback provides a subset of Select All, Cut, Copy and Paste actions, depending
8785     * on which of these this TextView supports.
8786     */
8787    private class SelectionActionModeCallback implements ActionMode.Callback {
8788
8789        @Override
8790        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
8791            TypedArray styledAttributes = mContext.obtainStyledAttributes(R.styleable.Theme);
8792
8793            boolean allowText = getContext().getResources().getBoolean(
8794                    com.android.internal.R.bool.allow_action_menu_item_text_with_icon);
8795
8796            mode.setTitle(allowText ?
8797                    mContext.getString(com.android.internal.R.string.textSelectionCABTitle) : null);
8798            mode.setSubtitle(null);
8799
8800            int selectAllIconId = 0; // No icon by default
8801            if (!allowText) {
8802                // Provide an icon, text will not be displayed on smaller screens.
8803                selectAllIconId = styledAttributes.getResourceId(
8804                        R.styleable.Theme_actionModeSelectAllDrawable, 0);
8805            }
8806
8807            menu.add(0, ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll).
8808                    setIcon(selectAllIconId).
8809                    setAlphabeticShortcut('a').
8810                    setShowAsAction(
8811                            MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
8812
8813            if (canCut()) {
8814                menu.add(0, ID_CUT, 0, com.android.internal.R.string.cut).
8815                    setIcon(styledAttributes.getResourceId(
8816                            R.styleable.Theme_actionModeCutDrawable, 0)).
8817                    setAlphabeticShortcut('x').
8818                    setShowAsAction(
8819                            MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
8820            }
8821
8822            if (canCopy()) {
8823                menu.add(0, ID_COPY, 0, com.android.internal.R.string.copy).
8824                    setIcon(styledAttributes.getResourceId(
8825                            R.styleable.Theme_actionModeCopyDrawable, 0)).
8826                    setAlphabeticShortcut('c').
8827                    setShowAsAction(
8828                            MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
8829            }
8830
8831            if (canPaste()) {
8832                menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste).
8833                        setIcon(styledAttributes.getResourceId(
8834                                R.styleable.Theme_actionModePasteDrawable, 0)).
8835                        setAlphabeticShortcut('v').
8836                        setShowAsAction(
8837                                MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
8838            }
8839
8840            styledAttributes.recycle();
8841
8842            if (mCustomSelectionActionModeCallback != null) {
8843                if (!mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) {
8844                    // The custom mode can choose to cancel the action mode
8845                    return false;
8846                }
8847            }
8848
8849            if (menu.hasVisibleItems() || mode.getCustomView() != null) {
8850                getSelectionController().show();
8851                return true;
8852            } else {
8853                return false;
8854            }
8855        }
8856
8857        @Override
8858        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
8859            if (mCustomSelectionActionModeCallback != null) {
8860                return mCustomSelectionActionModeCallback.onPrepareActionMode(mode, menu);
8861            }
8862            return true;
8863        }
8864
8865        @Override
8866        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
8867            if (mCustomSelectionActionModeCallback != null &&
8868                 mCustomSelectionActionModeCallback.onActionItemClicked(mode, item)) {
8869                return true;
8870            }
8871            return onTextContextMenuItem(item.getItemId());
8872        }
8873
8874        @Override
8875        public void onDestroyActionMode(ActionMode mode) {
8876            if (mCustomSelectionActionModeCallback != null) {
8877                mCustomSelectionActionModeCallback.onDestroyActionMode(mode);
8878            }
8879            Selection.setSelection((Spannable) mText, getSelectionEnd());
8880
8881            if (mSelectionModifierCursorController != null) {
8882                mSelectionModifierCursorController.hide();
8883            }
8884
8885            mSelectionActionMode = null;
8886        }
8887    }
8888
8889    private class PastePopupWindow implements OnClickListener {
8890        private final PopupWindow mContainer;
8891        private final View[] mPasteViews = new View[4];
8892        private final int[] mPasteViewLayouts = new int[] {
8893                mTextEditPasteWindowLayout,  mTextEditNoPasteWindowLayout,
8894                mTextEditSidePasteWindowLayout, mTextEditSideNoPasteWindowLayout };
8895
8896        public PastePopupWindow() {
8897            mContainer = new PopupWindow(TextView.this.mContext, null,
8898                    com.android.internal.R.attr.textSelectHandleWindowStyle);
8899            mContainer.setSplitTouchEnabled(true);
8900            mContainer.setClippingEnabled(false);
8901            mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
8902
8903            mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
8904            mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
8905        }
8906
8907        private int viewIndex(boolean onTop) {
8908            return (onTop ? 0 : 1<<1) + (canPaste() ? 0 : 1<<0);
8909        }
8910
8911        private void updateContent(boolean onTop) {
8912            final int viewIndex = viewIndex(onTop);
8913            View view = mPasteViews[viewIndex];
8914
8915            if (view == null) {
8916                final int layout = mPasteViewLayouts[viewIndex];
8917                LayoutInflater inflater = (LayoutInflater)TextView.this.mContext.
8918                    getSystemService(Context.LAYOUT_INFLATER_SERVICE);
8919                if (inflater != null) {
8920                    view = inflater.inflate(layout, null);
8921                }
8922
8923                if (view == null) {
8924                    throw new IllegalArgumentException("Unable to inflate TextEdit paste window");
8925                }
8926
8927                final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
8928                view.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
8929                        ViewGroup.LayoutParams.WRAP_CONTENT));
8930                view.measure(size, size);
8931
8932                view.setOnClickListener(this);
8933
8934                mPasteViews[viewIndex] = view;
8935            }
8936
8937            mContainer.setContentView(view);
8938        }
8939
8940        public void show() {
8941            updateContent(true);
8942            positionAtCursor();
8943        }
8944
8945        public void hide() {
8946            mContainer.dismiss();
8947        }
8948
8949        public boolean isShowing() {
8950            return mContainer.isShowing();
8951        }
8952
8953        @Override
8954        public void onClick(View v) {
8955            if (canPaste()) {
8956                paste(getSelectionStart(), getSelectionEnd());
8957            }
8958            hide();
8959        }
8960
8961        void positionAtCursor() {
8962            View contentView = mContainer.getContentView();
8963            int width = contentView.getMeasuredWidth();
8964            int height = contentView.getMeasuredHeight();
8965            final int offset = TextView.this.getSelectionStart();
8966            final int line = mLayout.getLineForOffset(offset);
8967            final int lineTop = mLayout.getLineTop(line);
8968            float primaryHorizontal = mLayout.getPrimaryHorizontal(offset);
8969
8970            final Rect bounds = sCursorControllerTempRect;
8971            bounds.left = (int) (primaryHorizontal - width / 2.0f);
8972            bounds.top = lineTop - height;
8973
8974            bounds.right = bounds.left + width;
8975            bounds.bottom = bounds.top + height;
8976
8977            convertFromViewportToContentCoordinates(bounds);
8978
8979            final int[] coords = mTempCoords;
8980            TextView.this.getLocationInWindow(coords);
8981            coords[0] += bounds.left;
8982            coords[1] += bounds.top;
8983
8984            final int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels;
8985            if (coords[1] < 0) {
8986                updateContent(false);
8987                // Update dimensions from new view
8988                contentView = mContainer.getContentView();
8989                width = contentView.getMeasuredWidth();
8990                height = contentView.getMeasuredHeight();
8991
8992                // Vertical clipping, move under edited line and to the side of insertion cursor
8993                // TODO bottom clipping in case there is no system bar
8994                coords[1] += height;
8995                final int lineBottom = mLayout.getLineBottom(line);
8996                final int lineHeight = lineBottom - lineTop;
8997                coords[1] += lineHeight;
8998
8999                // Move to right hand side of insertion cursor by default. TODO RTL text.
9000                final Drawable handle = mContext.getResources().getDrawable(mTextSelectHandleRes);
9001                final int handleHalfWidth = handle.getIntrinsicWidth() / 2;
9002
9003                if (primaryHorizontal + handleHalfWidth + width < screenWidth) {
9004                    coords[0] += handleHalfWidth + width / 2;
9005                } else {
9006                    coords[0] -= handleHalfWidth + width / 2;
9007                }
9008            } else {
9009                // Horizontal clipping
9010                coords[0] = Math.max(0, coords[0]);
9011                coords[0] = Math.min(screenWidth - width, coords[0]);
9012            }
9013
9014            mContainer.showAtLocation(TextView.this, Gravity.NO_GRAVITY, coords[0], coords[1]);
9015        }
9016    }
9017
9018    private abstract class HandleView extends View implements ViewTreeObserver.OnPreDrawListener {
9019        protected Drawable mDrawable;
9020        private final PopupWindow mContainer;
9021        // Position with respect to the parent TextView
9022        private int mPositionX, mPositionY;
9023        private boolean mIsDragging;
9024        // Offset from touch position to mPosition
9025        private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
9026        protected float mHotspotX;
9027        // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
9028        private float mTouchOffsetY;
9029        // Where the touch position should be on the handle to ensure a maximum cursor visibility
9030        private float mIdealVerticalOffset;
9031        // Parent's (TextView) previous position in window
9032        private int mLastParentX, mLastParentY;
9033        // PopupWindow container absolute position with respect to the enclosing window
9034        private int mContainerPositionX, mContainerPositionY;
9035        // Visible or not (scrolled off screen), whether or not this handle should be visible
9036        private boolean mIsActive = false;
9037
9038        public HandleView() {
9039            super(TextView.this.mContext);
9040            mContainer = new PopupWindow(TextView.this.mContext, null,
9041                    com.android.internal.R.attr.textSelectHandleWindowStyle);
9042            mContainer.setSplitTouchEnabled(true);
9043            mContainer.setClippingEnabled(false);
9044            mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
9045            mContainer.setContentView(this);
9046
9047            initDrawable();
9048
9049            final int handleHeight = mDrawable.getIntrinsicHeight();
9050            mTouchOffsetY = -0.3f * handleHeight;
9051            mIdealVerticalOffset = 0.7f * handleHeight;
9052        }
9053
9054        protected abstract void initDrawable();
9055
9056        // Touch-up filter: number of previous positions remembered
9057        private static final int HISTORY_SIZE = 5;
9058        private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
9059        private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
9060        private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
9061        private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
9062        private int mPreviousOffsetIndex = 0;
9063        private int mNumberPreviousOffsets = 0;
9064
9065        private void startTouchUpFilter(int offset) {
9066            mNumberPreviousOffsets = 0;
9067            addPositionToTouchUpFilter(offset);
9068        }
9069
9070        private void addPositionToTouchUpFilter(int offset) {
9071            if (mNumberPreviousOffsets > 0 &&
9072                    mPreviousOffsets[mPreviousOffsetIndex] == offset) {
9073                // Make sure only actual changes of position are recorded.
9074                return;
9075            }
9076
9077            mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
9078            mPreviousOffsets[mPreviousOffsetIndex] = offset;
9079            mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
9080            mNumberPreviousOffsets++;
9081        }
9082
9083        private void filterOnTouchUp() {
9084            final long now = SystemClock.uptimeMillis();
9085            int i = 0;
9086            int index = mPreviousOffsetIndex;
9087            final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
9088            while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
9089                i++;
9090                index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
9091            }
9092
9093            if (i > 0 && i < iMax &&
9094                    (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
9095                updateOffset(mPreviousOffsets[index]);
9096            }
9097        }
9098
9099        @Override
9100        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
9101            setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
9102        }
9103
9104        public void show() {
9105            if (isShowing()) {
9106                mContainer.update(mContainerPositionX, mContainerPositionY,
9107                        mRight - mLeft, mBottom - mTop);
9108            } else {
9109                mContainer.showAtLocation(TextView.this, 0,
9110                        mContainerPositionX, mContainerPositionY);
9111
9112                mIsActive = true;
9113
9114                ViewTreeObserver vto = TextView.this.getViewTreeObserver();
9115                vto.addOnPreDrawListener(this);
9116            }
9117        }
9118
9119        protected void dismiss() {
9120            mIsDragging = false;
9121            mContainer.dismiss();
9122        }
9123
9124        public void hide() {
9125            dismiss();
9126
9127            mIsActive = false;
9128
9129            ViewTreeObserver vto = TextView.this.getViewTreeObserver();
9130            vto.removeOnPreDrawListener(this);
9131        }
9132
9133        public boolean isShowing() {
9134            return mContainer.isShowing();
9135        }
9136
9137        private boolean isPositionVisible() {
9138            // Always show a dragging handle.
9139            if (mIsDragging) {
9140                return true;
9141            }
9142
9143            if (isInBatchEditMode()) {
9144                return false;
9145            }
9146
9147            final int extendedPaddingTop = getExtendedPaddingTop();
9148            final int extendedPaddingBottom = getExtendedPaddingBottom();
9149            final int compoundPaddingLeft = getCompoundPaddingLeft();
9150            final int compoundPaddingRight = getCompoundPaddingRight();
9151
9152            final TextView textView = TextView.this;
9153
9154            if (mTempRect == null) mTempRect = new Rect();
9155            final Rect clip = mTempRect;
9156            clip.left = compoundPaddingLeft;
9157            clip.top = extendedPaddingTop;
9158            clip.right = textView.getWidth() - compoundPaddingRight;
9159            clip.bottom = textView.getHeight() - extendedPaddingBottom;
9160
9161            final ViewParent parent = textView.getParent();
9162            if (parent == null || !parent.getChildVisibleRect(textView, clip, null)) {
9163                return false;
9164            }
9165
9166            final int[] coords = mTempCoords;
9167            textView.getLocationInWindow(coords);
9168            final int posX = coords[0] + mPositionX + (int) mHotspotX;
9169            final int posY = coords[1] + mPositionY;
9170
9171            // Offset by 1 to take into account 0.5 and int rounding around getPrimaryHorizontal.
9172            return posX >= clip.left - 1 && posX <= clip.right + 1 &&
9173                    posY >= clip.top && posY <= clip.bottom;
9174        }
9175
9176        public abstract int getCurrentCursorOffset();
9177
9178        public abstract void updateOffset(int offset);
9179
9180        public abstract void updatePosition(float x, float y);
9181
9182        protected void positionAtCursorOffset(int offset) {
9183            // A HandleView relies on the layout, which may be nulled by external methods.
9184            if (mLayout == null) {
9185                // Will update controllers' state, hiding them and stopping selection mode if needed
9186                prepareCursorControllers();
9187                return;
9188            }
9189
9190            addPositionToTouchUpFilter(offset);
9191            final int line = mLayout.getLineForOffset(offset);
9192            final int lineBottom = mLayout.getLineBottom(line);
9193
9194            mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX);
9195            mPositionY = lineBottom;
9196
9197            // Take TextView's padding into account.
9198            mPositionX += viewportToContentHorizontalOffset();
9199            mPositionY += viewportToContentVerticalOffset();
9200        }
9201
9202        protected boolean updateContainerPosition() {
9203            positionAtCursorOffset(getCurrentCursorOffset());
9204
9205            final int previousContainerPositionX = mContainerPositionX;
9206            final int previousContainerPositionY = mContainerPositionY;
9207
9208            TextView.this.getLocationInWindow(mTempCoords);
9209            mContainerPositionX = mTempCoords[0] + mPositionX;
9210            mContainerPositionY = mTempCoords[1] + mPositionY;
9211
9212            return (previousContainerPositionX != mContainerPositionX ||
9213                    previousContainerPositionY != mContainerPositionY);
9214        }
9215
9216        public boolean onPreDraw() {
9217            if (updateContainerPosition()) {
9218                if (mIsDragging) {
9219                    if (mTempCoords[0] != mLastParentX || mTempCoords[1] != mLastParentY) {
9220                        mTouchToWindowOffsetX += mTempCoords[0] - mLastParentX;
9221                        mTouchToWindowOffsetY += mTempCoords[1] - mLastParentY;
9222                        mLastParentX = mTempCoords[0];
9223                        mLastParentY = mTempCoords[1];
9224                    }
9225                }
9226
9227                onHandleMoved();
9228
9229                if (isPositionVisible()) {
9230                    mContainer.update(mContainerPositionX, mContainerPositionY,
9231                            mRight - mLeft, mBottom - mTop);
9232
9233                    if (mIsActive && !isShowing()) {
9234                        show();
9235                    }
9236                } else {
9237                    if (isShowing()) {
9238                        dismiss();
9239                    }
9240                }
9241            }
9242            return true;
9243        }
9244
9245        @Override
9246        protected void onDraw(Canvas c) {
9247            mDrawable.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
9248            mDrawable.draw(c);
9249        }
9250
9251        @Override
9252        public boolean onTouchEvent(MotionEvent ev) {
9253            switch (ev.getActionMasked()) {
9254                case MotionEvent.ACTION_DOWN: {
9255                    startTouchUpFilter(getCurrentCursorOffset());
9256                    mTouchToWindowOffsetX = ev.getRawX() - mPositionX;
9257                    mTouchToWindowOffsetY = ev.getRawY() - mPositionY;
9258
9259                    final int[] coords = mTempCoords;
9260                    TextView.this.getLocationInWindow(coords);
9261                    mLastParentX = coords[0];
9262                    mLastParentY = coords[1];
9263                    mIsDragging = true;
9264                    break;
9265                }
9266
9267                case MotionEvent.ACTION_MOVE: {
9268                    final float rawX = ev.getRawX();
9269                    final float rawY = ev.getRawY();
9270
9271                    // Vertical hysteresis: vertical down movement tends to snap to ideal offset
9272                    final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
9273                    final float currentVerticalOffset = rawY - mPositionY - mLastParentY;
9274                    float newVerticalOffset;
9275                    if (previousVerticalOffset < mIdealVerticalOffset) {
9276                        newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
9277                        newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
9278                    } else {
9279                        newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
9280                        newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
9281                    }
9282                    mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
9283
9284                    final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX;
9285                    final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY;
9286
9287                    updatePosition(newPosX, newPosY);
9288                    break;
9289                }
9290
9291                case MotionEvent.ACTION_UP:
9292                    filterOnTouchUp();
9293                    mIsDragging = false;
9294                    break;
9295
9296                case MotionEvent.ACTION_CANCEL:
9297                    mIsDragging = false;
9298                    break;
9299            }
9300            return true;
9301        }
9302
9303        public boolean isDragging() {
9304            return mIsDragging;
9305        }
9306
9307        void onHandleMoved() {
9308            // Does nothing by default
9309        }
9310
9311        public void onDetached() {
9312            // Should be overriden to clean possible Runnable
9313        }
9314    }
9315
9316    private class InsertionHandleView extends HandleView {
9317        private static final int DELAY_BEFORE_FADE_OUT = 4000;
9318        private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
9319
9320        // Used to detect taps on the insertion handle, which will affect the PastePopupWindow
9321        private long mTouchTimer;
9322        private float mDownPositionX, mDownPositionY;
9323        private PastePopupWindow mPastePopupWindow;
9324        private Runnable mHider;
9325        private Runnable mPastePopupShower;
9326
9327        @Override
9328        public void show() {
9329            super.show();
9330            hideDelayed();
9331            hidePastePopupWindow();
9332        }
9333
9334        public void show(int delayBeforePaste) {
9335            show();
9336
9337            final long durationSinceCutOrCopy = SystemClock.uptimeMillis() - sLastCutOrCopyTime;
9338            if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) {
9339                delayBeforePaste = 0;
9340            }
9341            if (delayBeforePaste == 0 || canPaste()) {
9342                if (mPastePopupShower == null) {
9343                    mPastePopupShower = new Runnable() {
9344                        public void run() {
9345                            showPastePopupWindow();
9346                        }
9347                    };
9348                }
9349                TextView.this.postDelayed(mPastePopupShower, delayBeforePaste);
9350            }
9351        }
9352
9353        @Override
9354        protected void dismiss() {
9355            super.dismiss();
9356            onDetached();
9357        }
9358
9359        private void hideDelayed() {
9360            removeHiderCallback();
9361            if (mHider == null) {
9362                mHider = new Runnable() {
9363                    public void run() {
9364                        hide();
9365                    }
9366                };
9367            }
9368            TextView.this.postDelayed(mHider, DELAY_BEFORE_FADE_OUT);
9369        }
9370
9371        private void removeHiderCallback() {
9372            if (mHider != null) {
9373                TextView.this.removeCallbacks(mHider);
9374            }
9375        }
9376
9377        @Override
9378        protected void initDrawable() {
9379            if (mSelectHandleCenter == null) {
9380                mSelectHandleCenter = mContext.getResources().getDrawable(
9381                        mTextSelectHandleRes);
9382            }
9383            mDrawable = mSelectHandleCenter;
9384            mHotspotX = mDrawable.getIntrinsicWidth() / 2.0f;
9385        }
9386
9387        @Override
9388        public boolean onTouchEvent(MotionEvent ev) {
9389            final boolean result = super.onTouchEvent(ev);
9390
9391            switch (ev.getActionMasked()) {
9392                case MotionEvent.ACTION_DOWN:
9393                    mDownPositionX = ev.getRawX();
9394                    mDownPositionY = ev.getRawY();
9395                    mTouchTimer = SystemClock.uptimeMillis();
9396                    break;
9397
9398                case MotionEvent.ACTION_UP:
9399                    long delay = SystemClock.uptimeMillis() - mTouchTimer;
9400                    if (delay < ViewConfiguration.getTapTimeout()) {
9401                        final float deltaX = mDownPositionX - ev.getRawX();
9402                        final float deltaY = mDownPositionY - ev.getRawY();
9403                        final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
9404                        if (distanceSquared < mSquaredTouchSlopDistance) {
9405                            if (mPastePopupWindow != null && mPastePopupWindow.isShowing()) {
9406                                // Tapping on the handle dismisses the displayed paste view,
9407                                mPastePopupWindow.hide();
9408                            } else {
9409                                show(0);
9410                            }
9411                        }
9412                    }
9413                    hideDelayed();
9414                    break;
9415
9416                case MotionEvent.ACTION_CANCEL:
9417                    hideDelayed();
9418                    break;
9419
9420                default:
9421                    break;
9422            }
9423
9424            return result;
9425        }
9426
9427        @Override
9428        public int getCurrentCursorOffset() {
9429            return TextView.this.getSelectionStart();
9430        }
9431
9432        @Override
9433        public void updateOffset(int offset) {
9434            Selection.setSelection((Spannable) mText, offset);
9435        }
9436
9437        @Override
9438        public void updatePosition(float x, float y) {
9439            updateOffset(getOffsetForPosition(x, y));
9440        }
9441
9442        void showPastePopupWindow() {
9443            if (mPastePopupWindow == null) {
9444                mPastePopupWindow = new PastePopupWindow();
9445            }
9446            mPastePopupWindow.show();
9447        }
9448
9449        @Override
9450        void onHandleMoved() {
9451            removeHiderCallback();
9452            hidePastePopupWindow();
9453        }
9454
9455        void hidePastePopupWindow() {
9456            if (mPastePopupShower != null) {
9457                TextView.this.removeCallbacks(mPastePopupShower);
9458            }
9459            if (mPastePopupWindow != null) {
9460                mPastePopupWindow.hide();
9461            }
9462        }
9463
9464        @Override
9465        public void onDetached() {
9466            removeHiderCallback();
9467            hidePastePopupWindow();
9468        }
9469    }
9470
9471    private class SelectionStartHandleView extends HandleView {
9472        @Override
9473        protected void initDrawable() {
9474            if (mSelectHandleLeft == null) {
9475                mSelectHandleLeft = mContext.getResources().getDrawable(
9476                        mTextSelectHandleLeftRes);
9477            }
9478            mDrawable = mSelectHandleLeft;
9479            mHotspotX = mDrawable.getIntrinsicWidth() * 3.0f / 4.0f;
9480        }
9481
9482        @Override
9483        public int getCurrentCursorOffset() {
9484            return TextView.this.getSelectionStart();
9485        }
9486
9487        @Override
9488        public void updateOffset(int offset) {
9489            Selection.setSelection((Spannable) mText, offset, getSelectionEnd());
9490        }
9491
9492        @Override
9493        public void updatePosition(float x, float y) {
9494            final int selectionStart = getSelectionStart();
9495            final int selectionEnd = getSelectionEnd();
9496
9497            int offset = getOffsetForPosition(x, y);
9498
9499            // No need to redraw when the offset is unchanged
9500            if (offset == selectionStart) return;
9501            // Handles can not cross and selection is at least one character
9502            if (offset >= selectionEnd) offset = selectionEnd - 1;
9503
9504            Selection.setSelection((Spannable) mText, offset, selectionEnd);
9505        }
9506    }
9507
9508    private class SelectionEndHandleView extends HandleView {
9509        @Override
9510        protected void initDrawable() {
9511            if (mSelectHandleRight == null) {
9512                mSelectHandleRight = mContext.getResources().getDrawable(
9513                        mTextSelectHandleRightRes);
9514            }
9515            mDrawable = mSelectHandleRight;
9516            mHotspotX = mDrawable.getIntrinsicWidth() / 4.0f;
9517        }
9518
9519        @Override
9520        public int getCurrentCursorOffset() {
9521            return TextView.this.getSelectionEnd();
9522        }
9523
9524        @Override
9525        public void updateOffset(int offset) {
9526            Selection.setSelection((Spannable) mText, getSelectionStart(), offset);
9527        }
9528
9529        @Override
9530        public void updatePosition(float x, float y) {
9531            final int selectionStart = getSelectionStart();
9532            final int selectionEnd = getSelectionEnd();
9533
9534            int offset = getOffsetForPosition(x, y);
9535
9536            // No need to redraw when the offset is unchanged
9537            if (offset == selectionEnd) return;
9538            // Handles can not cross and selection is at least one character
9539            if (offset <= selectionStart) offset = selectionStart + 1;
9540
9541            Selection.setSelection((Spannable) mText, selectionStart, offset);
9542        }
9543    }
9544
9545    /**
9546     * A CursorController instance can be used to control a cursor in the text.
9547     * It is not used outside of {@link TextView}.
9548     * @hide
9549     */
9550    private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
9551        /**
9552         * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}.
9553         * See also {@link #hide()}.
9554         */
9555        public void show();
9556
9557        /**
9558         * Hide the cursor controller from screen.
9559         * See also {@link #show()}.
9560         */
9561        public void hide();
9562
9563        /**
9564         * This method is called by {@link #onTouchEvent(MotionEvent)} and gives the controller
9565         * a chance to become active and/or visible.
9566         * @param event The touch event
9567         */
9568        public boolean onTouchEvent(MotionEvent event);
9569
9570        /**
9571         * Called when the view is detached from window. Perform house keeping task, such as
9572         * stopping Runnable thread that would otherwise keep a reference on the context, thus
9573         * preventing the activity from being recycled.
9574         */
9575        public void onDetached();
9576    }
9577
9578    private class InsertionPointCursorController implements CursorController {
9579        private static final int DELAY_BEFORE_PASTE = 2000;
9580
9581        private InsertionHandleView mHandle;
9582
9583        public void show() {
9584            ((InsertionHandleView) getHandle()).show(DELAY_BEFORE_PASTE);
9585        }
9586
9587        public void showWithPaste() {
9588            ((InsertionHandleView) getHandle()).show(0);
9589        }
9590
9591        public void hide() {
9592            if (mHandle != null) {
9593                mHandle.hide();
9594            }
9595        }
9596
9597        public boolean onTouchEvent(MotionEvent ev) {
9598            return false;
9599        }
9600
9601        public void onTouchModeChanged(boolean isInTouchMode) {
9602            if (!isInTouchMode) {
9603                hide();
9604            }
9605        }
9606
9607        private HandleView getHandle() {
9608            if (mHandle == null) {
9609                mHandle = new InsertionHandleView();
9610            }
9611            return mHandle;
9612        }
9613
9614        @Override
9615        public void onDetached() {
9616            final ViewTreeObserver observer = getViewTreeObserver();
9617            observer.removeOnTouchModeChangeListener(this);
9618
9619            if (mHandle != null) mHandle.onDetached();
9620        }
9621    }
9622
9623    private class SelectionModifierCursorController implements CursorController {
9624        // The cursor controller handles, lazily created when shown.
9625        private SelectionStartHandleView mStartHandle;
9626        private SelectionEndHandleView mEndHandle;
9627        // The offsets of that last touch down event. Remembered to start selection there.
9628        private int mMinTouchOffset, mMaxTouchOffset;
9629
9630        // Double tap detection
9631        private long mPreviousTapUpTime = 0;
9632        private float mPreviousTapPositionX, mPreviousTapPositionY;
9633
9634        SelectionModifierCursorController() {
9635            resetTouchOffsets();
9636        }
9637
9638        public void show() {
9639            if (isInBatchEditMode()) {
9640                return;
9641            }
9642
9643            // Lazy object creation has to be done before updatePosition() is called.
9644            if (mStartHandle == null) mStartHandle = new SelectionStartHandleView();
9645            if (mEndHandle == null) mEndHandle = new SelectionEndHandleView();
9646
9647            mStartHandle.show();
9648            mEndHandle.show();
9649
9650            hideInsertionPointCursorController();
9651            hideSuggestions();
9652        }
9653
9654        public void hide() {
9655            if (mStartHandle != null) mStartHandle.hide();
9656            if (mEndHandle != null) mEndHandle.hide();
9657        }
9658
9659        public boolean onTouchEvent(MotionEvent event) {
9660            // This is done even when the View does not have focus, so that long presses can start
9661            // selection and tap can move cursor from this tap position.
9662            if (isTextEditable() || mTextIsSelectable) {
9663                switch (event.getActionMasked()) {
9664                    case MotionEvent.ACTION_DOWN:
9665                        final float x = event.getX();
9666                        final float y = event.getY();
9667
9668                        // Remember finger down position, to be able to start selection from there
9669                        mMinTouchOffset = mMaxTouchOffset = getOffsetForPosition(x, y);
9670
9671                        // Double tap detection
9672                        long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime;
9673                        if (duration <= ViewConfiguration.getDoubleTapTimeout() &&
9674                                isPositionOnText(x, y)) {
9675                            final float deltaX = x - mPreviousTapPositionX;
9676                            final float deltaY = y - mPreviousTapPositionY;
9677                            final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
9678                            if (distanceSquared < mSquaredTouchSlopDistance) {
9679                                showSuggestions();
9680                                mDiscardNextActionUp = true;
9681                            }
9682                        }
9683
9684                        mPreviousTapPositionX = x;
9685                        mPreviousTapPositionY = y;
9686
9687                        break;
9688
9689                    case MotionEvent.ACTION_POINTER_DOWN:
9690                    case MotionEvent.ACTION_POINTER_UP:
9691                        // Handle multi-point gestures. Keep min and max offset positions.
9692                        // Only activated for devices that correctly handle multi-touch.
9693                        if (mContext.getPackageManager().hasSystemFeature(
9694                                PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
9695                            updateMinAndMaxOffsets(event);
9696                        }
9697                        break;
9698
9699                    case MotionEvent.ACTION_UP:
9700                        mPreviousTapUpTime = SystemClock.uptimeMillis();
9701                        break;
9702                }
9703            }
9704            return false;
9705        }
9706
9707        /**
9708         * @param event
9709         */
9710        private void updateMinAndMaxOffsets(MotionEvent event) {
9711            int pointerCount = event.getPointerCount();
9712            for (int index = 0; index < pointerCount; index++) {
9713                int offset = getOffsetForPosition(event.getX(index), event.getY(index));
9714                if (offset < mMinTouchOffset) mMinTouchOffset = offset;
9715                if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
9716            }
9717        }
9718
9719        public int getMinTouchOffset() {
9720            return mMinTouchOffset;
9721        }
9722
9723        public int getMaxTouchOffset() {
9724            return mMaxTouchOffset;
9725        }
9726
9727        public void resetTouchOffsets() {
9728            mMinTouchOffset = mMaxTouchOffset = -1;
9729        }
9730
9731        /**
9732         * @return true iff this controller is currently used to move the selection start.
9733         */
9734        public boolean isSelectionStartDragged() {
9735            return mStartHandle != null && mStartHandle.isDragging();
9736        }
9737
9738        public void onTouchModeChanged(boolean isInTouchMode) {
9739            if (!isInTouchMode) {
9740                hide();
9741            }
9742        }
9743
9744        @Override
9745        public void onDetached() {
9746            final ViewTreeObserver observer = getViewTreeObserver();
9747            observer.removeOnTouchModeChangeListener(this);
9748
9749            if (mStartHandle != null) mStartHandle.onDetached();
9750            if (mEndHandle != null) mEndHandle.onDetached();
9751        }
9752    }
9753
9754    private void hideInsertionPointCursorController() {
9755        // No need to create the controller to hide it.
9756        if (mInsertionPointCursorController != null) {
9757            mInsertionPointCursorController.hide();
9758        }
9759    }
9760
9761    /**
9762     * Hides the insertion controller and stops text selection mode, hiding the selection controller
9763     */
9764    private void hideControllers() {
9765        hideInsertionPointCursorController();
9766        stopSelectionActionMode();
9767        hideSuggestions();
9768    }
9769
9770    /**
9771     * Get the character offset closest to the specified absolute position. A typical use case is to
9772     * pass the result of {@link MotionEvent#getX()} and {@link MotionEvent#getY()} to this method.
9773     *
9774     * @param x The horizontal absolute position of a point on screen
9775     * @param y The vertical absolute position of a point on screen
9776     * @return the character offset for the character whose position is closest to the specified
9777     *  position. Returns -1 if there is no layout.
9778     */
9779    public int getOffsetForPosition(float x, float y) {
9780        if (getLayout() == null) return -1;
9781        final int line = getLineAtCoordinate(y);
9782        final int offset = getOffsetAtCoordinate(line, x);
9783        return offset;
9784    }
9785
9786    private float convertToLocalHorizontalCoordinate(float x) {
9787        x -= getTotalPaddingLeft();
9788        // Clamp the position to inside of the view.
9789        x = Math.max(0.0f, x);
9790        x = Math.min(getWidth() - getTotalPaddingRight() - 1, x);
9791        x += getScrollX();
9792        return x;
9793    }
9794
9795    private int getLineAtCoordinate(float y) {
9796        y -= getTotalPaddingTop();
9797        // Clamp the position to inside of the view.
9798        y = Math.max(0.0f, y);
9799        y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y);
9800        y += getScrollY();
9801        return getLayout().getLineForVertical((int) y);
9802    }
9803
9804    private int getOffsetAtCoordinate(int line, float x) {
9805        x = convertToLocalHorizontalCoordinate(x);
9806        return getLayout().getOffsetForHorizontal(line, x);
9807    }
9808
9809    /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
9810     * in the view. Returns false when the position is in the empty space of left/right of text.
9811     */
9812    private boolean isPositionOnText(float x, float y) {
9813        if (getLayout() == null) return false;
9814
9815        final int line = getLineAtCoordinate(y);
9816        x = convertToLocalHorizontalCoordinate(x);
9817
9818        if (x < getLayout().getLineLeft(line)) return false;
9819        if (x > getLayout().getLineRight(line)) return false;
9820        return true;
9821    }
9822
9823    @Override
9824    public boolean onDragEvent(DragEvent event) {
9825        switch (event.getAction()) {
9826            case DragEvent.ACTION_DRAG_STARTED:
9827                return hasInsertionController();
9828
9829            case DragEvent.ACTION_DRAG_ENTERED:
9830                TextView.this.requestFocus();
9831                return true;
9832
9833            case DragEvent.ACTION_DRAG_LOCATION:
9834                final int offset = getOffsetForPosition(event.getX(), event.getY());
9835                Selection.setSelection((Spannable)mText, offset);
9836                return true;
9837
9838            case DragEvent.ACTION_DROP:
9839                onDrop(event);
9840                return true;
9841
9842            case DragEvent.ACTION_DRAG_ENDED:
9843            case DragEvent.ACTION_DRAG_EXITED:
9844            default:
9845                return true;
9846        }
9847    }
9848
9849    private void onDrop(DragEvent event) {
9850        StringBuilder content = new StringBuilder("");
9851        ClipData clipData = event.getClipData();
9852        final int itemCount = clipData.getItemCount();
9853        for (int i=0; i < itemCount; i++) {
9854            Item item = clipData.getItemAt(i);
9855            content.append(item.coerceToText(TextView.this.mContext));
9856        }
9857
9858        final int offset = getOffsetForPosition(event.getX(), event.getY());
9859
9860        Object localState = event.getLocalState();
9861        DragLocalState dragLocalState = null;
9862        if (localState instanceof DragLocalState) {
9863            dragLocalState = (DragLocalState) localState;
9864        }
9865        boolean dragDropIntoItself = dragLocalState != null &&
9866                dragLocalState.sourceTextView == this;
9867
9868        if (dragDropIntoItself) {
9869            if (offset >= dragLocalState.start && offset < dragLocalState.end) {
9870                // A drop inside the original selection discards the drop.
9871                return;
9872            }
9873        }
9874
9875        final int originalLength = mText.length();
9876        long minMax = prepareSpacesAroundPaste(offset, offset, content);
9877        int min = extractRangeStartFromLong(minMax);
9878        int max = extractRangeEndFromLong(minMax);
9879
9880        Selection.setSelection((Spannable) mText, max);
9881        ((Editable) mText).replace(min, max, content);
9882
9883        if (dragDropIntoItself) {
9884            int dragSourceStart = dragLocalState.start;
9885            int dragSourceEnd = dragLocalState.end;
9886            if (max <= dragSourceStart) {
9887                // Inserting text before selection has shifted positions
9888                final int shift = mText.length() - originalLength;
9889                dragSourceStart += shift;
9890                dragSourceEnd += shift;
9891            }
9892
9893            // Delete original selection
9894            ((Editable) mText).delete(dragSourceStart, dragSourceEnd);
9895
9896            // Make sure we do not leave two adjacent spaces.
9897            if ((dragSourceStart == 0 ||
9898                    Character.isSpaceChar(mTransformed.charAt(dragSourceStart - 1))) &&
9899                    (dragSourceStart == mText.length() ||
9900                    Character.isSpaceChar(mTransformed.charAt(dragSourceStart)))) {
9901                final int pos = dragSourceStart == mText.length() ?
9902                        dragSourceStart - 1 : dragSourceStart;
9903                ((Editable) mText).delete(pos, pos + 1);
9904            }
9905        }
9906    }
9907
9908    /**
9909     * @return True if this view supports insertion handles.
9910     */
9911    boolean hasInsertionController() {
9912        return mInsertionControllerEnabled;
9913    }
9914
9915    /**
9916     * @return True if this view supports selection handles.
9917     */
9918    boolean hasSelectionController() {
9919        return mSelectionControllerEnabled;
9920    }
9921
9922    InsertionPointCursorController getInsertionController() {
9923        if (!mInsertionControllerEnabled) {
9924            return null;
9925        }
9926
9927        if (mInsertionPointCursorController == null) {
9928            mInsertionPointCursorController = new InsertionPointCursorController();
9929
9930            final ViewTreeObserver observer = getViewTreeObserver();
9931            observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
9932        }
9933
9934        return mInsertionPointCursorController;
9935    }
9936
9937    SelectionModifierCursorController getSelectionController() {
9938        if (!mSelectionControllerEnabled) {
9939            return null;
9940        }
9941
9942        if (mSelectionModifierCursorController == null) {
9943            mSelectionModifierCursorController = new SelectionModifierCursorController();
9944
9945            final ViewTreeObserver observer = getViewTreeObserver();
9946            observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
9947        }
9948
9949        return mSelectionModifierCursorController;
9950    }
9951
9952    boolean isInBatchEditMode() {
9953        final InputMethodState ims = mInputMethodState;
9954        if (ims != null) {
9955            return ims.mBatchEditNesting > 0;
9956        }
9957        return mInBatchEditControllers;
9958    }
9959
9960    @ViewDebug.ExportedProperty(category = "text")
9961    private CharSequence            mText;
9962    private CharSequence            mTransformed;
9963    private BufferType              mBufferType = BufferType.NORMAL;
9964
9965    private int                     mInputType = EditorInfo.TYPE_NULL;
9966    private CharSequence            mHint;
9967    private Layout                  mHintLayout;
9968
9969    private KeyListener             mInput;
9970
9971    private MovementMethod          mMovement;
9972    private TransformationMethod    mTransformation;
9973    private ChangeWatcher           mChangeWatcher;
9974
9975    private ArrayList<TextWatcher>  mListeners = null;
9976
9977    // display attributes
9978    private final TextPaint         mTextPaint;
9979    private boolean                 mUserSetTextScaleX;
9980    private final Paint             mHighlightPaint;
9981    private int                     mHighlightColor = 0xCC475925;
9982    /**
9983     * This is temporarily visible to fix bug 3085564 in webView. Do not rely on
9984     * this field being protected. Will be restored as private when lineHeight
9985     * feature request 3215097 is implemented
9986     * @hide
9987     */
9988    protected Layout                mLayout;
9989
9990    private long                    mShowCursor;
9991    private Blink                   mBlink;
9992    private boolean                 mCursorVisible = true;
9993
9994    // Cursor Controllers.
9995    private InsertionPointCursorController mInsertionPointCursorController;
9996    private SelectionModifierCursorController mSelectionModifierCursorController;
9997    private ActionMode              mSelectionActionMode;
9998    private boolean                 mInsertionControllerEnabled;
9999    private boolean                 mSelectionControllerEnabled;
10000    private boolean                 mInBatchEditControllers;
10001
10002    // These are needed to desambiguate a long click. If the long click comes from ones of these, we
10003    // select from the current cursor position. Otherwise, select from long pressed position.
10004    private boolean                 mDPadCenterIsDown = false;
10005    private boolean                 mEnterKeyIsDown = false;
10006    private boolean                 mContextMenuTriggeredByKey = false;
10007    // Created once and shared by different CursorController helper methods.
10008    // Only one cursor controller is active at any time which prevent race conditions.
10009    private static Rect             sCursorControllerTempRect = new Rect();
10010
10011    private boolean                 mSelectAllOnFocus = false;
10012
10013    private int                     mGravity = Gravity.TOP | Gravity.LEFT;
10014    private boolean                 mHorizontallyScrolling;
10015
10016    private int                     mAutoLinkMask;
10017    private boolean                 mLinksClickable = true;
10018
10019    private float                   mSpacingMult = 1;
10020    private float                   mSpacingAdd = 0;
10021    private boolean                 mTextIsSelectable = false;
10022
10023    private static final int        LINES = 1;
10024    private static final int        EMS = LINES;
10025    private static final int        PIXELS = 2;
10026
10027    private int                     mMaximum = Integer.MAX_VALUE;
10028    private int                     mMaxMode = LINES;
10029    private int                     mMinimum = 0;
10030    private int                     mMinMode = LINES;
10031
10032    private int                     mMaxWidth = Integer.MAX_VALUE;
10033    private int                     mMaxWidthMode = PIXELS;
10034    private int                     mMinWidth = 0;
10035    private int                     mMinWidthMode = PIXELS;
10036
10037    private boolean                 mSingleLine;
10038    private int                     mDesiredHeightAtMeasure = -1;
10039    private boolean                 mIncludePad = true;
10040
10041    // tmp primitives, so we don't alloc them on each draw
10042    private Path                    mHighlightPath;
10043    private boolean                 mHighlightPathBogus = true;
10044    private static final RectF      sTempRect = new RectF();
10045
10046    // XXX should be much larger
10047    private static final int        VERY_WIDE = 16384;
10048
10049    private static final int        BLINK = 500;
10050
10051    private static final int ANIMATED_SCROLL_GAP = 250;
10052    private long mLastScroll;
10053    private Scroller mScroller = null;
10054
10055    private BoringLayout.Metrics mBoring;
10056    private BoringLayout.Metrics mHintBoring;
10057
10058    private BoringLayout mSavedLayout, mSavedHintLayout;
10059
10060    private static final InputFilter[] NO_FILTERS = new InputFilter[0];
10061    private InputFilter[] mFilters = NO_FILTERS;
10062    private static final Spanned EMPTY_SPANNED = new SpannedString("");
10063    private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
10064    // System wide time for last cut or copy action.
10065    private static long sLastCutOrCopyTime;
10066    // Used to highlight a word when it is corrected by the IME
10067    private CorrectionHighlighter mCorrectionHighlighter;
10068    // New state used to change background based on whether this TextView is multiline.
10069    private static final int[] MULTILINE_STATE_SET = { R.attr.state_multiline };
10070}
10071