TextView.java revision b1619e5615a295981b6467442783b3e57edf36b3
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.HORIZONTAL_GRAVITY_MASK) == 0) {
2097            gravity |= Gravity.LEFT;
2098        }
2099        if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
2100            gravity |= Gravity.TOP;
2101        }
2102
2103        boolean newLayout = false;
2104
2105        if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) !=
2106            (mGravity & Gravity.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                ss.text = sp;
2569            } else {
2570                ss.text = mText.toString();
2571            }
2572
2573            if (isFocused() && start >= 0 && end >= 0) {
2574                ss.frozenWithFocus = true;
2575            }
2576
2577            ss.error = mError;
2578
2579            return ss;
2580        }
2581
2582        return superState;
2583    }
2584
2585    @Override
2586    public void onRestoreInstanceState(Parcelable state) {
2587        if (!(state instanceof SavedState)) {
2588            super.onRestoreInstanceState(state);
2589            return;
2590        }
2591
2592        SavedState ss = (SavedState)state;
2593        super.onRestoreInstanceState(ss.getSuperState());
2594
2595        // XXX restore buffer type too, as well as lots of other stuff
2596        if (ss.text != null) {
2597            setText(ss.text);
2598        }
2599
2600        if (ss.selStart >= 0 && ss.selEnd >= 0) {
2601            if (mText instanceof Spannable) {
2602                int len = mText.length();
2603
2604                if (ss.selStart > len || ss.selEnd > len) {
2605                    String restored = "";
2606
2607                    if (ss.text != null) {
2608                        restored = "(restored) ";
2609                    }
2610
2611                    Log.e(LOG_TAG, "Saved cursor position " + ss.selStart +
2612                          "/" + ss.selEnd + " out of range for " + restored +
2613                          "text " + mText);
2614                } else {
2615                    Selection.setSelection((Spannable) mText, ss.selStart,
2616                                           ss.selEnd);
2617
2618                    if (ss.frozenWithFocus) {
2619                        mFrozenWithFocus = true;
2620                    }
2621                }
2622            }
2623        }
2624
2625        if (ss.error != null) {
2626            final CharSequence error = ss.error;
2627            // Display the error later, after the first layout pass
2628            post(new Runnable() {
2629                public void run() {
2630                    setError(error);
2631                }
2632            });
2633        }
2634    }
2635
2636    /**
2637     * Control whether this text view saves its entire text contents when
2638     * freezing to an icicle, in addition to dynamic state such as cursor
2639     * position.  By default this is false, not saving the text.  Set to true
2640     * if the text in the text view is not being saved somewhere else in
2641     * persistent storage (such as in a content provider) so that if the
2642     * view is later thawed the user will not lose their data.
2643     *
2644     * @param freezesText Controls whether a frozen icicle should include the
2645     * entire text data: true to include it, false to not.
2646     *
2647     * @attr ref android.R.styleable#TextView_freezesText
2648     */
2649    @android.view.RemotableViewMethod
2650    public void setFreezesText(boolean freezesText) {
2651        mFreezesText = freezesText;
2652    }
2653
2654    /**
2655     * Return whether this text view is including its entire text contents
2656     * in frozen icicles.
2657     *
2658     * @return Returns true if text is included, false if it isn't.
2659     *
2660     * @see #setFreezesText
2661     */
2662    public boolean getFreezesText() {
2663        return mFreezesText;
2664    }
2665
2666    ///////////////////////////////////////////////////////////////////////////
2667
2668    /**
2669     * Sets the Factory used to create new Editables.
2670     */
2671    public final void setEditableFactory(Editable.Factory factory) {
2672        mEditableFactory = factory;
2673        setText(mText);
2674    }
2675
2676    /**
2677     * Sets the Factory used to create new Spannables.
2678     */
2679    public final void setSpannableFactory(Spannable.Factory factory) {
2680        mSpannableFactory = factory;
2681        setText(mText);
2682    }
2683
2684    /**
2685     * Sets the string value of the TextView. TextView <em>does not</em> accept
2686     * HTML-like formatting, which you can do with text strings in XML resource files.
2687     * To style your strings, attach android.text.style.* objects to a
2688     * {@link android.text.SpannableString SpannableString}, or see the
2689     * <a href="{@docRoot}guide/topics/resources/available-resources.html#stringresources">
2690     * Available Resource Types</a> documentation for an example of setting
2691     * formatted text in the XML resource file.
2692     *
2693     * @attr ref android.R.styleable#TextView_text
2694     */
2695    @android.view.RemotableViewMethod
2696    public final void setText(CharSequence text) {
2697        setText(text, mBufferType);
2698    }
2699
2700    /**
2701     * Like {@link #setText(CharSequence)},
2702     * except that the cursor position (if any) is retained in the new text.
2703     *
2704     * @param text The new text to place in the text view.
2705     *
2706     * @see #setText(CharSequence)
2707     */
2708    @android.view.RemotableViewMethod
2709    public final void setTextKeepState(CharSequence text) {
2710        setTextKeepState(text, mBufferType);
2711    }
2712
2713    /**
2714     * Sets the text that this TextView is to display (see
2715     * {@link #setText(CharSequence)}) and also sets whether it is stored
2716     * in a styleable/spannable buffer and whether it is editable.
2717     *
2718     * @attr ref android.R.styleable#TextView_text
2719     * @attr ref android.R.styleable#TextView_bufferType
2720     */
2721    public void setText(CharSequence text, BufferType type) {
2722        setText(text, type, true, 0);
2723
2724        if (mCharWrapper != null) {
2725            mCharWrapper.mChars = null;
2726        }
2727    }
2728
2729    private void setText(CharSequence text, BufferType type,
2730                         boolean notifyBefore, int oldlen) {
2731        if (text == null) {
2732            text = "";
2733        }
2734
2735        if (!mUserSetTextScaleX) mTextPaint.setTextScaleX(1.0f);
2736
2737        if (text instanceof Spanned &&
2738            ((Spanned) text).getSpanStart(TextUtils.TruncateAt.MARQUEE) >= 0) {
2739            setHorizontalFadingEdgeEnabled(true);
2740            setEllipsize(TextUtils.TruncateAt.MARQUEE);
2741        }
2742
2743        int n = mFilters.length;
2744        for (int i = 0; i < n; i++) {
2745            CharSequence out = mFilters[i].filter(text, 0, text.length(),
2746                                                  EMPTY_SPANNED, 0, 0);
2747            if (out != null) {
2748                text = out;
2749            }
2750        }
2751
2752        if (notifyBefore) {
2753            if (mText != null) {
2754                oldlen = mText.length();
2755                sendBeforeTextChanged(mText, 0, oldlen, text.length());
2756            } else {
2757                sendBeforeTextChanged("", 0, 0, text.length());
2758            }
2759        }
2760
2761        boolean needEditableForNotification = false;
2762
2763        if (mListeners != null && mListeners.size() != 0) {
2764            needEditableForNotification = true;
2765        }
2766
2767        if (type == BufferType.EDITABLE || mInput != null ||
2768            needEditableForNotification) {
2769            Editable t = mEditableFactory.newEditable(text);
2770            text = t;
2771            setFilters(t, mFilters);
2772            InputMethodManager imm = InputMethodManager.peekInstance();
2773            if (imm != null) imm.restartInput(this);
2774        } else if (type == BufferType.SPANNABLE || mMovement != null) {
2775            text = mSpannableFactory.newSpannable(text);
2776        } else if (!(text instanceof CharWrapper)) {
2777            text = TextUtils.stringOrSpannedString(text);
2778        }
2779
2780        if (mAutoLinkMask != 0) {
2781            Spannable s2;
2782
2783            if (type == BufferType.EDITABLE || text instanceof Spannable) {
2784                s2 = (Spannable) text;
2785            } else {
2786                s2 = mSpannableFactory.newSpannable(text);
2787            }
2788
2789            if (Linkify.addLinks(s2, mAutoLinkMask)) {
2790                text = s2;
2791                type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE;
2792
2793                /*
2794                 * We must go ahead and set the text before changing the
2795                 * movement method, because setMovementMethod() may call
2796                 * setText() again to try to upgrade the buffer type.
2797                 */
2798                mText = text;
2799
2800                // Do not change the movement method for text that support text selection as it
2801                // would prevent an arbitrary cursor displacement.
2802                final boolean hasTextSelection = this instanceof EditText || mTextIsSelectable;
2803                if (mLinksClickable && !hasTextSelection) {
2804                    setMovementMethod(LinkMovementMethod.getInstance());
2805                }
2806            }
2807        }
2808
2809        mBufferType = type;
2810        mText = text;
2811
2812        if (mTransformation == null)
2813            mTransformed = text;
2814        else
2815            mTransformed = mTransformation.getTransformation(text, this);
2816
2817        final int textLength = text.length();
2818
2819        if (text instanceof Spannable) {
2820            Spannable sp = (Spannable) text;
2821
2822            // Remove any ChangeWatchers that might have come
2823            // from other TextViews.
2824            final ChangeWatcher[] watchers = sp.getSpans(0, sp.length(), ChangeWatcher.class);
2825            final int count = watchers.length;
2826            for (int i = 0; i < count; i++)
2827                sp.removeSpan(watchers[i]);
2828
2829            if (mChangeWatcher == null)
2830                mChangeWatcher = new ChangeWatcher();
2831
2832            sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE |
2833                       (PRIORITY << Spanned.SPAN_PRIORITY_SHIFT));
2834
2835            if (mInput != null) {
2836                sp.setSpan(mInput, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
2837            }
2838
2839            if (mTransformation != null) {
2840                sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
2841
2842            }
2843
2844            if (mMovement != null) {
2845                mMovement.initialize(this, (Spannable) text);
2846
2847                /*
2848                 * Initializing the movement method will have set the
2849                 * selection, so reset mSelectionMoved to keep that from
2850                 * interfering with the normal on-focus selection-setting.
2851                 */
2852                mSelectionMoved = false;
2853            }
2854        }
2855
2856        if (mLayout != null) {
2857            checkForRelayout();
2858        }
2859
2860        sendOnTextChanged(text, 0, oldlen, textLength);
2861        onTextChanged(text, 0, oldlen, textLength);
2862
2863        if (needEditableForNotification) {
2864            sendAfterTextChanged((Editable) text);
2865        }
2866
2867        // SelectionModifierCursorController depends on textCanBeSelected, which depends on text
2868        prepareCursorControllers();
2869    }
2870
2871    /**
2872     * Sets the TextView to display the specified slice of the specified
2873     * char array.  You must promise that you will not change the contents
2874     * of the array except for right before another call to setText(),
2875     * since the TextView has no way to know that the text
2876     * has changed and that it needs to invalidate and re-layout.
2877     */
2878    public final void setText(char[] text, int start, int len) {
2879        int oldlen = 0;
2880
2881        if (start < 0 || len < 0 || start + len > text.length) {
2882            throw new IndexOutOfBoundsException(start + ", " + len);
2883        }
2884
2885        /*
2886         * We must do the before-notification here ourselves because if
2887         * the old text is a CharWrapper we destroy it before calling
2888         * into the normal path.
2889         */
2890        if (mText != null) {
2891            oldlen = mText.length();
2892            sendBeforeTextChanged(mText, 0, oldlen, len);
2893        } else {
2894            sendBeforeTextChanged("", 0, 0, len);
2895        }
2896
2897        if (mCharWrapper == null) {
2898            mCharWrapper = new CharWrapper(text, start, len);
2899        } else {
2900            mCharWrapper.set(text, start, len);
2901        }
2902
2903        setText(mCharWrapper, mBufferType, false, oldlen);
2904    }
2905
2906    private static class CharWrapper implements CharSequence, GetChars, GraphicsOperations {
2907        private char[] mChars;
2908        private int mStart, mLength;
2909
2910        public CharWrapper(char[] chars, int start, int len) {
2911            mChars = chars;
2912            mStart = start;
2913            mLength = len;
2914        }
2915
2916        /* package */ void set(char[] chars, int start, int len) {
2917            mChars = chars;
2918            mStart = start;
2919            mLength = len;
2920        }
2921
2922        public int length() {
2923            return mLength;
2924        }
2925
2926        public char charAt(int off) {
2927            return mChars[off + mStart];
2928        }
2929
2930        @Override
2931        public String toString() {
2932            return new String(mChars, mStart, mLength);
2933        }
2934
2935        public CharSequence subSequence(int start, int end) {
2936            if (start < 0 || end < 0 || start > mLength || end > mLength) {
2937                throw new IndexOutOfBoundsException(start + ", " + end);
2938            }
2939
2940            return new String(mChars, start + mStart, end - start);
2941        }
2942
2943        public void getChars(int start, int end, char[] buf, int off) {
2944            if (start < 0 || end < 0 || start > mLength || end > mLength) {
2945                throw new IndexOutOfBoundsException(start + ", " + end);
2946            }
2947
2948            System.arraycopy(mChars, start + mStart, buf, off, end - start);
2949        }
2950
2951        public void drawText(Canvas c, int start, int end,
2952                             float x, float y, Paint p) {
2953            c.drawText(mChars, start + mStart, end - start, x, y, p);
2954        }
2955
2956        public void drawTextRun(Canvas c, int start, int end,
2957                int contextStart, int contextEnd, float x, float y, int flags, Paint p) {
2958            int count = end - start;
2959            int contextCount = contextEnd - contextStart;
2960            c.drawTextRun(mChars, start + mStart, count, contextStart + mStart,
2961                    contextCount, x, y, flags, p);
2962        }
2963
2964        public float measureText(int start, int end, Paint p) {
2965            return p.measureText(mChars, start + mStart, end - start);
2966        }
2967
2968        public int getTextWidths(int start, int end, float[] widths, Paint p) {
2969            return p.getTextWidths(mChars, start + mStart, end - start, widths);
2970        }
2971
2972        public float getTextRunAdvances(int start, int end, int contextStart,
2973                int contextEnd, int flags, float[] advances, int advancesIndex,
2974                Paint p) {
2975            int count = end - start;
2976            int contextCount = contextEnd - contextStart;
2977            return p.getTextRunAdvances(mChars, start + mStart, count,
2978                    contextStart + mStart, contextCount, flags, advances,
2979                    advancesIndex);
2980        }
2981
2982        public float getTextRunAdvances(int start, int end, int contextStart,
2983                int contextEnd, int flags, float[] advances, int advancesIndex,
2984                Paint p, int reserved) {
2985            int count = end - start;
2986            int contextCount = contextEnd - contextStart;
2987            return p.getTextRunAdvances(mChars, start + mStart, count,
2988                    contextStart + mStart, contextCount, flags, advances,
2989                    advancesIndex, reserved);
2990        }
2991
2992        public int getTextRunCursor(int contextStart, int contextEnd, int flags,
2993                int offset, int cursorOpt, Paint p) {
2994            int contextCount = contextEnd - contextStart;
2995            return p.getTextRunCursor(mChars, contextStart + mStart,
2996                    contextCount, flags, offset + mStart, cursorOpt);
2997        }
2998    }
2999
3000    /**
3001     * Like {@link #setText(CharSequence, android.widget.TextView.BufferType)},
3002     * except that the cursor position (if any) is retained in the new text.
3003     *
3004     * @see #setText(CharSequence, android.widget.TextView.BufferType)
3005     */
3006    public final void setTextKeepState(CharSequence text, BufferType type) {
3007        int start = getSelectionStart();
3008        int end = getSelectionEnd();
3009        int len = text.length();
3010
3011        setText(text, type);
3012
3013        if (start >= 0 || end >= 0) {
3014            if (mText instanceof Spannable) {
3015                Selection.setSelection((Spannable) mText,
3016                                       Math.max(0, Math.min(start, len)),
3017                                       Math.max(0, Math.min(end, len)));
3018            }
3019        }
3020    }
3021
3022    @android.view.RemotableViewMethod
3023    public final void setText(int resid) {
3024        setText(getContext().getResources().getText(resid));
3025    }
3026
3027    public final void setText(int resid, BufferType type) {
3028        setText(getContext().getResources().getText(resid), type);
3029    }
3030
3031    /**
3032     * Sets the text to be displayed when the text of the TextView is empty.
3033     * Null means to use the normal empty text. The hint does not currently
3034     * participate in determining the size of the view.
3035     *
3036     * @attr ref android.R.styleable#TextView_hint
3037     */
3038    @android.view.RemotableViewMethod
3039    public final void setHint(CharSequence hint) {
3040        mHint = TextUtils.stringOrSpannedString(hint);
3041
3042        if (mLayout != null) {
3043            checkForRelayout();
3044        }
3045
3046        if (mText.length() == 0) {
3047            invalidate();
3048        }
3049    }
3050
3051    /**
3052     * Sets the text to be displayed when the text of the TextView is empty,
3053     * from a resource.
3054     *
3055     * @attr ref android.R.styleable#TextView_hint
3056     */
3057    @android.view.RemotableViewMethod
3058    public final void setHint(int resid) {
3059        setHint(getContext().getResources().getText(resid));
3060    }
3061
3062    /**
3063     * Returns the hint that is displayed when the text of the TextView
3064     * is empty.
3065     *
3066     * @attr ref android.R.styleable#TextView_hint
3067     */
3068    @ViewDebug.CapturedViewProperty
3069    public CharSequence getHint() {
3070        return mHint;
3071    }
3072
3073    private boolean isMultilineInputType(int type) {
3074        return (type & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) ==
3075            (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE);
3076    }
3077
3078    /**
3079     * Set the type of the content with a constant as defined for {@link EditorInfo#inputType}. This
3080     * will take care of changing the key listener, by calling {@link #setKeyListener(KeyListener)},
3081     * to match the given content type.  If the given content type is {@link EditorInfo#TYPE_NULL}
3082     * then a soft keyboard will not be displayed for this text view.
3083     *
3084     * Note that the maximum number of displayed lines (see {@link #setMaxLines(int)}) will be
3085     * modified if you change the {@link EditorInfo#TYPE_TEXT_FLAG_MULTI_LINE} flag of the input
3086     * type.
3087     *
3088     * @see #getInputType()
3089     * @see #setRawInputType(int)
3090     * @see android.text.InputType
3091     * @attr ref android.R.styleable#TextView_inputType
3092     */
3093    public void setInputType(int type) {
3094        final boolean wasPassword = isPasswordInputType(mInputType);
3095        final boolean wasVisiblePassword = isVisiblePasswordInputType(mInputType);
3096        setInputType(type, false);
3097        final boolean isPassword = isPasswordInputType(type);
3098        final boolean isVisiblePassword = isVisiblePasswordInputType(type);
3099        boolean forceUpdate = false;
3100        if (isPassword) {
3101            setTransformationMethod(PasswordTransformationMethod.getInstance());
3102            setTypefaceByIndex(MONOSPACE, 0);
3103        } else if (isVisiblePassword) {
3104            if (mTransformation == PasswordTransformationMethod.getInstance()) {
3105                forceUpdate = true;
3106            }
3107            setTypefaceByIndex(MONOSPACE, 0);
3108        } else if (wasPassword || wasVisiblePassword) {
3109            // not in password mode, clean up typeface and transformation
3110            setTypefaceByIndex(-1, -1);
3111            if (mTransformation == PasswordTransformationMethod.getInstance()) {
3112                forceUpdate = true;
3113            }
3114        }
3115
3116        boolean singleLine = !isMultilineInputType(type);
3117
3118        // We need to update the single line mode if it has changed or we
3119        // were previously in password mode.
3120        if (mSingleLine != singleLine || forceUpdate) {
3121            // Change single line mode, but only change the transformation if
3122            // we are not in password mode.
3123            applySingleLine(singleLine, !isPassword, true);
3124        }
3125
3126        InputMethodManager imm = InputMethodManager.peekInstance();
3127        if (imm != null) imm.restartInput(this);
3128    }
3129
3130    /**
3131     * It would be better to rely on the input type for everything. A password inputType should have
3132     * a password transformation. We should hence use isPasswordInputType instead of this method.
3133     *
3134     * We should:
3135     * - Call setInputType in setKeyListener instead of changing the input type directly (which
3136     * would install the correct transformation).
3137     * - Refuse the installation of a non-password transformation in setTransformation if the input
3138     * type is password.
3139     *
3140     * However, this is like this for legacy reasons and we cannot break existing apps. This method
3141     * is useful since it matches what the user can see (obfuscated text or not).
3142     *
3143     * @return true if the current transformation method is of the password type.
3144     */
3145    private boolean hasPasswordTransformationMethod() {
3146        return mTransformation instanceof PasswordTransformationMethod;
3147    }
3148
3149    private boolean isPasswordInputType(int inputType) {
3150        final int variation =
3151                inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
3152        return variation
3153                == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)
3154                || variation
3155                == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD)
3156                || variation
3157                == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
3158    }
3159
3160    private boolean isVisiblePasswordInputType(int inputType) {
3161        final int variation =
3162                inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
3163        return variation
3164                == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
3165    }
3166
3167    /**
3168     * Directly change the content type integer of the text view, without
3169     * modifying any other state.
3170     * @see #setInputType(int)
3171     * @see android.text.InputType
3172     * @attr ref android.R.styleable#TextView_inputType
3173     */
3174    public void setRawInputType(int type) {
3175        mInputType = type;
3176    }
3177
3178    private void setInputType(int type, boolean direct) {
3179        final int cls = type & EditorInfo.TYPE_MASK_CLASS;
3180        KeyListener input;
3181        if (cls == EditorInfo.TYPE_CLASS_TEXT) {
3182            boolean autotext = (type & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) != 0;
3183            TextKeyListener.Capitalize cap;
3184            if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
3185                cap = TextKeyListener.Capitalize.CHARACTERS;
3186            } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS) != 0) {
3187                cap = TextKeyListener.Capitalize.WORDS;
3188            } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) {
3189                cap = TextKeyListener.Capitalize.SENTENCES;
3190            } else {
3191                cap = TextKeyListener.Capitalize.NONE;
3192            }
3193            input = TextKeyListener.getInstance(autotext, cap);
3194        } else if (cls == EditorInfo.TYPE_CLASS_NUMBER) {
3195            input = DigitsKeyListener.getInstance(
3196                    (type & EditorInfo.TYPE_NUMBER_FLAG_SIGNED) != 0,
3197                    (type & EditorInfo.TYPE_NUMBER_FLAG_DECIMAL) != 0);
3198        } else if (cls == EditorInfo.TYPE_CLASS_DATETIME) {
3199            switch (type & EditorInfo.TYPE_MASK_VARIATION) {
3200                case EditorInfo.TYPE_DATETIME_VARIATION_DATE:
3201                    input = DateKeyListener.getInstance();
3202                    break;
3203                case EditorInfo.TYPE_DATETIME_VARIATION_TIME:
3204                    input = TimeKeyListener.getInstance();
3205                    break;
3206                default:
3207                    input = DateTimeKeyListener.getInstance();
3208                    break;
3209            }
3210        } else if (cls == EditorInfo.TYPE_CLASS_PHONE) {
3211            input = DialerKeyListener.getInstance();
3212        } else {
3213            input = TextKeyListener.getInstance();
3214        }
3215        setRawInputType(type);
3216        if (direct) mInput = input;
3217        else {
3218            setKeyListenerOnly(input);
3219        }
3220    }
3221
3222    /**
3223     * Get the type of the content.
3224     *
3225     * @see #setInputType(int)
3226     * @see android.text.InputType
3227     */
3228    public int getInputType() {
3229        return mInputType;
3230    }
3231
3232    /**
3233     * Change the editor type integer associated with the text view, which
3234     * will be reported to an IME with {@link EditorInfo#imeOptions} when it
3235     * has focus.
3236     * @see #getImeOptions
3237     * @see android.view.inputmethod.EditorInfo
3238     * @attr ref android.R.styleable#TextView_imeOptions
3239     */
3240    public void setImeOptions(int imeOptions) {
3241        if (mInputContentType == null) {
3242            mInputContentType = new InputContentType();
3243        }
3244        mInputContentType.imeOptions = imeOptions;
3245    }
3246
3247    /**
3248     * Get the type of the IME editor.
3249     *
3250     * @see #setImeOptions(int)
3251     * @see android.view.inputmethod.EditorInfo
3252     */
3253    public int getImeOptions() {
3254        return mInputContentType != null
3255                ? mInputContentType.imeOptions : EditorInfo.IME_NULL;
3256    }
3257
3258    /**
3259     * Change the custom IME action associated with the text view, which
3260     * will be reported to an IME with {@link EditorInfo#actionLabel}
3261     * and {@link EditorInfo#actionId} when it has focus.
3262     * @see #getImeActionLabel
3263     * @see #getImeActionId
3264     * @see android.view.inputmethod.EditorInfo
3265     * @attr ref android.R.styleable#TextView_imeActionLabel
3266     * @attr ref android.R.styleable#TextView_imeActionId
3267     */
3268    public void setImeActionLabel(CharSequence label, int actionId) {
3269        if (mInputContentType == null) {
3270            mInputContentType = new InputContentType();
3271        }
3272        mInputContentType.imeActionLabel = label;
3273        mInputContentType.imeActionId = actionId;
3274    }
3275
3276    /**
3277     * Get the IME action label previous set with {@link #setImeActionLabel}.
3278     *
3279     * @see #setImeActionLabel
3280     * @see android.view.inputmethod.EditorInfo
3281     */
3282    public CharSequence getImeActionLabel() {
3283        return mInputContentType != null
3284                ? mInputContentType.imeActionLabel : null;
3285    }
3286
3287    /**
3288     * Get the IME action ID previous set with {@link #setImeActionLabel}.
3289     *
3290     * @see #setImeActionLabel
3291     * @see android.view.inputmethod.EditorInfo
3292     */
3293    public int getImeActionId() {
3294        return mInputContentType != null
3295                ? mInputContentType.imeActionId : 0;
3296    }
3297
3298    /**
3299     * Set a special listener to be called when an action is performed
3300     * on the text view.  This will be called when the enter key is pressed,
3301     * or when an action supplied to the IME is selected by the user.  Setting
3302     * this means that the normal hard key event will not insert a newline
3303     * into the text view, even if it is multi-line; holding down the ALT
3304     * modifier will, however, allow the user to insert a newline character.
3305     */
3306    public void setOnEditorActionListener(OnEditorActionListener l) {
3307        if (mInputContentType == null) {
3308            mInputContentType = new InputContentType();
3309        }
3310        mInputContentType.onEditorActionListener = l;
3311    }
3312
3313    /**
3314     * Called when an attached input method calls
3315     * {@link InputConnection#performEditorAction(int)
3316     * InputConnection.performEditorAction()}
3317     * for this text view.  The default implementation will call your action
3318     * listener supplied to {@link #setOnEditorActionListener}, or perform
3319     * a standard operation for {@link EditorInfo#IME_ACTION_NEXT
3320     * EditorInfo.IME_ACTION_NEXT}, {@link EditorInfo#IME_ACTION_PREVIOUS
3321     * EditorInfo.IME_ACTION_PREVIOUS}, or {@link EditorInfo#IME_ACTION_DONE
3322     * EditorInfo.IME_ACTION_DONE}.
3323     *
3324     * <p>For backwards compatibility, if no IME options have been set and the
3325     * text view would not normally advance focus on enter, then
3326     * the NEXT and DONE actions received here will be turned into an enter
3327     * key down/up pair to go through the normal key handling.
3328     *
3329     * @param actionCode The code of the action being performed.
3330     *
3331     * @see #setOnEditorActionListener
3332     */
3333    public void onEditorAction(int actionCode) {
3334        final InputContentType ict = mInputContentType;
3335        if (ict != null) {
3336            if (ict.onEditorActionListener != null) {
3337                if (ict.onEditorActionListener.onEditorAction(this,
3338                        actionCode, null)) {
3339                    return;
3340                }
3341            }
3342
3343            // This is the handling for some default action.
3344            // Note that for backwards compatibility we don't do this
3345            // default handling if explicit ime options have not been given,
3346            // instead turning this into the normal enter key codes that an
3347            // app may be expecting.
3348            if (actionCode == EditorInfo.IME_ACTION_NEXT) {
3349                View v = focusSearch(FOCUS_DOWN);
3350                if (v != null) {
3351                    if (!v.requestFocus(FOCUS_DOWN)) {
3352                        throw new IllegalStateException("focus search returned a view " +
3353                                "that wasn't able to take focus!");
3354                    }
3355                }
3356                return;
3357
3358            } else if (actionCode == EditorInfo.IME_ACTION_PREVIOUS) {
3359                View v = focusSearch(FOCUS_UP);
3360                if (v != null) {
3361                    if (!v.requestFocus(FOCUS_UP)) {
3362                        throw new IllegalStateException("focus search returned a view " +
3363                                "that wasn't able to take focus!");
3364                    }
3365                }
3366                return;
3367
3368            } else if (actionCode == EditorInfo.IME_ACTION_DONE) {
3369                InputMethodManager imm = InputMethodManager.peekInstance();
3370                if (imm != null && imm.isActive(this)) {
3371                    imm.hideSoftInputFromWindow(getWindowToken(), 0);
3372                }
3373                return;
3374            }
3375        }
3376
3377        Handler h = getHandler();
3378        if (h != null) {
3379            long eventTime = SystemClock.uptimeMillis();
3380            h.sendMessage(h.obtainMessage(ViewAncestor.DISPATCH_KEY_FROM_IME,
3381                    new KeyEvent(eventTime, eventTime,
3382                    KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0,
3383                    KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
3384                    KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
3385                    | KeyEvent.FLAG_EDITOR_ACTION)));
3386            h.sendMessage(h.obtainMessage(ViewAncestor.DISPATCH_KEY_FROM_IME,
3387                    new KeyEvent(SystemClock.uptimeMillis(), eventTime,
3388                    KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0,
3389                    KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
3390                    KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
3391                    | KeyEvent.FLAG_EDITOR_ACTION)));
3392        }
3393    }
3394
3395    /**
3396     * Set the private content type of the text, which is the
3397     * {@link EditorInfo#privateImeOptions EditorInfo.privateImeOptions}
3398     * field that will be filled in when creating an input connection.
3399     *
3400     * @see #getPrivateImeOptions()
3401     * @see EditorInfo#privateImeOptions
3402     * @attr ref android.R.styleable#TextView_privateImeOptions
3403     */
3404    public void setPrivateImeOptions(String type) {
3405        if (mInputContentType == null) mInputContentType = new InputContentType();
3406        mInputContentType.privateImeOptions = type;
3407    }
3408
3409    /**
3410     * Get the private type of the content.
3411     *
3412     * @see #setPrivateImeOptions(String)
3413     * @see EditorInfo#privateImeOptions
3414     */
3415    public String getPrivateImeOptions() {
3416        return mInputContentType != null
3417                ? mInputContentType.privateImeOptions : null;
3418    }
3419
3420    /**
3421     * Set the extra input data of the text, which is the
3422     * {@link EditorInfo#extras TextBoxAttribute.extras}
3423     * Bundle that will be filled in when creating an input connection.  The
3424     * given integer is the resource ID of an XML resource holding an
3425     * {@link android.R.styleable#InputExtras &lt;input-extras&gt;} XML tree.
3426     *
3427     * @see #getInputExtras(boolean)
3428     * @see EditorInfo#extras
3429     * @attr ref android.R.styleable#TextView_editorExtras
3430     */
3431    public void setInputExtras(int xmlResId)
3432            throws XmlPullParserException, IOException {
3433        XmlResourceParser parser = getResources().getXml(xmlResId);
3434        if (mInputContentType == null) mInputContentType = new InputContentType();
3435        mInputContentType.extras = new Bundle();
3436        getResources().parseBundleExtras(parser, mInputContentType.extras);
3437    }
3438
3439    /**
3440     * Retrieve the input extras currently associated with the text view, which
3441     * can be viewed as well as modified.
3442     *
3443     * @param create If true, the extras will be created if they don't already
3444     * exist.  Otherwise, null will be returned if none have been created.
3445     * @see #setInputExtras(int)
3446     * @see EditorInfo#extras
3447     * @attr ref android.R.styleable#TextView_editorExtras
3448     */
3449    public Bundle getInputExtras(boolean create) {
3450        if (mInputContentType == null) {
3451            if (!create) return null;
3452            mInputContentType = new InputContentType();
3453        }
3454        if (mInputContentType.extras == null) {
3455            if (!create) return null;
3456            mInputContentType.extras = new Bundle();
3457        }
3458        return mInputContentType.extras;
3459    }
3460
3461    /**
3462     * Returns the error message that was set to be displayed with
3463     * {@link #setError}, or <code>null</code> if no error was set
3464     * or if it the error was cleared by the widget after user input.
3465     */
3466    public CharSequence getError() {
3467        return mError;
3468    }
3469
3470    /**
3471     * Sets the right-hand compound drawable of the TextView to the "error"
3472     * icon and sets an error message that will be displayed in a popup when
3473     * the TextView has focus.  The icon and error message will be reset to
3474     * null when any key events cause changes to the TextView's text.  If the
3475     * <code>error</code> is <code>null</code>, the error message and icon
3476     * will be cleared.
3477     */
3478    @android.view.RemotableViewMethod
3479    public void setError(CharSequence error) {
3480        if (error == null) {
3481            setError(null, null);
3482        } else {
3483            Drawable dr = getContext().getResources().
3484                getDrawable(com.android.internal.R.drawable.indicator_input_error);
3485
3486            dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight());
3487            setError(error, dr);
3488        }
3489    }
3490
3491    /**
3492     * Sets the right-hand compound drawable of the TextView to the specified
3493     * icon and sets an error message that will be displayed in a popup when
3494     * the TextView has focus.  The icon and error message will be reset to
3495     * null when any key events cause changes to the TextView's text.  The
3496     * drawable must already have had {@link Drawable#setBounds} set on it.
3497     * If the <code>error</code> is <code>null</code>, the error message will
3498     * be cleared (and you should provide a <code>null</code> icon as well).
3499     */
3500    public void setError(CharSequence error, Drawable icon) {
3501        error = TextUtils.stringOrSpannedString(error);
3502
3503        mError = error;
3504        mErrorWasChanged = true;
3505        final Drawables dr = mDrawables;
3506        if (dr != null) {
3507            setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, icon, dr.mDrawableBottom);
3508        } else {
3509            setCompoundDrawables(null, null, icon, null);
3510        }
3511
3512        if (error == null) {
3513            if (mPopup != null) {
3514                if (mPopup.isShowing()) {
3515                    mPopup.dismiss();
3516                }
3517
3518                mPopup = null;
3519            }
3520        } else {
3521            if (isFocused()) {
3522                showError();
3523            }
3524        }
3525    }
3526
3527    private void showError() {
3528        if (getWindowToken() == null) {
3529            mShowErrorAfterAttach = true;
3530            return;
3531        }
3532
3533        if (mPopup == null) {
3534            LayoutInflater inflater = LayoutInflater.from(getContext());
3535            final TextView err = (TextView) inflater.inflate(
3536                    com.android.internal.R.layout.textview_hint, null);
3537
3538            final float scale = getResources().getDisplayMetrics().density;
3539            mPopup = new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f));
3540            mPopup.setFocusable(false);
3541            // The user is entering text, so the input method is needed.  We
3542            // don't want the popup to be displayed on top of it.
3543            mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
3544        }
3545
3546        TextView tv = (TextView) mPopup.getContentView();
3547        chooseSize(mPopup, mError, tv);
3548        tv.setText(mError);
3549
3550        mPopup.showAsDropDown(this, getErrorX(), getErrorY());
3551        mPopup.fixDirection(mPopup.isAboveAnchor());
3552    }
3553
3554    private static class ErrorPopup extends PopupWindow {
3555        private boolean mAbove = false;
3556        private final TextView mView;
3557        private int mPopupInlineErrorBackgroundId = 0;
3558        private int mPopupInlineErrorAboveBackgroundId = 0;
3559
3560        ErrorPopup(TextView v, int width, int height) {
3561            super(v, width, height);
3562            mView = v;
3563            // Make sure the TextView has a background set as it will be used the first time it is
3564            // shown and positionned. Initialized with below background, which should have
3565            // dimensions identical to the above version for this to work (and is more likely).
3566            mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
3567                    com.android.internal.R.styleable.Theme_errorMessageBackground);
3568            mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
3569        }
3570
3571        void fixDirection(boolean above) {
3572            mAbove = above;
3573
3574            if (above) {
3575                mPopupInlineErrorAboveBackgroundId =
3576                    getResourceId(mPopupInlineErrorAboveBackgroundId,
3577                            com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
3578            } else {
3579                mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
3580                        com.android.internal.R.styleable.Theme_errorMessageBackground);
3581            }
3582
3583            mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId :
3584                mPopupInlineErrorBackgroundId);
3585        }
3586
3587        private int getResourceId(int currentId, int index) {
3588            if (currentId == 0) {
3589                TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
3590                        R.styleable.Theme);
3591                currentId = styledAttributes.getResourceId(index, 0);
3592                styledAttributes.recycle();
3593            }
3594            return currentId;
3595        }
3596
3597        @Override
3598        public void update(int x, int y, int w, int h, boolean force) {
3599            super.update(x, y, w, h, force);
3600
3601            boolean above = isAboveAnchor();
3602            if (above != mAbove) {
3603                fixDirection(above);
3604            }
3605        }
3606    }
3607
3608    /**
3609     * Returns the Y offset to make the pointy top of the error point
3610     * at the middle of the error icon.
3611     */
3612    private int getErrorX() {
3613        /*
3614         * The "25" is the distance between the point and the right edge
3615         * of the background
3616         */
3617        final float scale = getResources().getDisplayMetrics().density;
3618
3619        final Drawables dr = mDrawables;
3620        return getWidth() - mPopup.getWidth() - getPaddingRight() -
3621                (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
3622    }
3623
3624    /**
3625     * Returns the Y offset to make the pointy top of the error point
3626     * at the bottom of the error icon.
3627     */
3628    private int getErrorY() {
3629        /*
3630         * Compound, not extended, because the icon is not clipped
3631         * if the text height is smaller.
3632         */
3633        final int compoundPaddingTop = getCompoundPaddingTop();
3634        int vspace = mBottom - mTop - getCompoundPaddingBottom() - compoundPaddingTop;
3635
3636        final Drawables dr = mDrawables;
3637        int icontop = compoundPaddingTop +
3638                (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2;
3639
3640        /*
3641         * The "2" is the distance between the point and the top edge
3642         * of the background.
3643         */
3644        final float scale = getResources().getDisplayMetrics().density;
3645        return icontop + (dr != null ? dr.mDrawableHeightRight : 0) - getHeight() -
3646                (int) (2 * scale + 0.5f);
3647    }
3648
3649    private void hideError() {
3650        if (mPopup != null) {
3651            if (mPopup.isShowing()) {
3652                mPopup.dismiss();
3653            }
3654        }
3655
3656        mShowErrorAfterAttach = false;
3657    }
3658
3659    private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
3660        int wid = tv.getPaddingLeft() + tv.getPaddingRight();
3661        int ht = tv.getPaddingTop() + tv.getPaddingBottom();
3662
3663        /*
3664         * Figure out how big the text would be if we laid it out to the
3665         * full width of this view minus the border.
3666         */
3667        int cap = getWidth() - wid;
3668        if (cap < 0) {
3669            cap = 200; // We must not be measured yet -- setFrame() will fix it.
3670        }
3671
3672        Layout l = new StaticLayout(text, tv.getPaint(), cap,
3673                                    Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
3674        float max = 0;
3675        for (int i = 0; i < l.getLineCount(); i++) {
3676            max = Math.max(max, l.getLineWidth(i));
3677        }
3678
3679        /*
3680         * Now set the popup size to be big enough for the text plus the border.
3681         */
3682        pop.setWidth(wid + (int) Math.ceil(max));
3683        pop.setHeight(ht + l.getHeight());
3684    }
3685
3686
3687    @Override
3688    protected boolean setFrame(int l, int t, int r, int b) {
3689        boolean result = super.setFrame(l, t, r, b);
3690
3691        if (mPopup != null) {
3692            TextView tv = (TextView) mPopup.getContentView();
3693            chooseSize(mPopup, mError, tv);
3694            mPopup.update(this, getErrorX(), getErrorY(),
3695                          mPopup.getWidth(), mPopup.getHeight());
3696        }
3697
3698        restartMarqueeIfNeeded();
3699
3700        return result;
3701    }
3702
3703    private void restartMarqueeIfNeeded() {
3704        if (mRestartMarquee && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
3705            mRestartMarquee = false;
3706            startMarquee();
3707        }
3708    }
3709
3710    /**
3711     * Sets the list of input filters that will be used if the buffer is
3712     * Editable.  Has no effect otherwise.
3713     *
3714     * @attr ref android.R.styleable#TextView_maxLength
3715     */
3716    public void setFilters(InputFilter[] filters) {
3717        if (filters == null) {
3718            throw new IllegalArgumentException();
3719        }
3720
3721        mFilters = filters;
3722
3723        if (mText instanceof Editable) {
3724            setFilters((Editable) mText, filters);
3725        }
3726    }
3727
3728    /**
3729     * Sets the list of input filters on the specified Editable,
3730     * and includes mInput in the list if it is an InputFilter.
3731     */
3732    private void setFilters(Editable e, InputFilter[] filters) {
3733        if (mInput instanceof InputFilter) {
3734            InputFilter[] nf = new InputFilter[filters.length + 1];
3735
3736            System.arraycopy(filters, 0, nf, 0, filters.length);
3737            nf[filters.length] = (InputFilter) mInput;
3738
3739            e.setFilters(nf);
3740        } else {
3741            e.setFilters(filters);
3742        }
3743    }
3744
3745    /**
3746     * Returns the current list of input filters.
3747     */
3748    public InputFilter[] getFilters() {
3749        return mFilters;
3750    }
3751
3752    /////////////////////////////////////////////////////////////////////////
3753
3754    private int getVerticalOffset(boolean forceNormal) {
3755        int voffset = 0;
3756        final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
3757
3758        Layout l = mLayout;
3759        if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
3760            l = mHintLayout;
3761        }
3762
3763        if (gravity != Gravity.TOP) {
3764            int boxht;
3765
3766            if (l == mHintLayout) {
3767                boxht = getMeasuredHeight() - getCompoundPaddingTop() -
3768                        getCompoundPaddingBottom();
3769            } else {
3770                boxht = getMeasuredHeight() - getExtendedPaddingTop() -
3771                        getExtendedPaddingBottom();
3772            }
3773            int textht = l.getHeight();
3774
3775            if (textht < boxht) {
3776                if (gravity == Gravity.BOTTOM)
3777                    voffset = boxht - textht;
3778                else // (gravity == Gravity.CENTER_VERTICAL)
3779                    voffset = (boxht - textht) >> 1;
3780            }
3781        }
3782        return voffset;
3783    }
3784
3785    private int getBottomVerticalOffset(boolean forceNormal) {
3786        int voffset = 0;
3787        final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
3788
3789        Layout l = mLayout;
3790        if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
3791            l = mHintLayout;
3792        }
3793
3794        if (gravity != Gravity.BOTTOM) {
3795            int boxht;
3796
3797            if (l == mHintLayout) {
3798                boxht = getMeasuredHeight() - getCompoundPaddingTop() -
3799                        getCompoundPaddingBottom();
3800            } else {
3801                boxht = getMeasuredHeight() - getExtendedPaddingTop() -
3802                        getExtendedPaddingBottom();
3803            }
3804            int textht = l.getHeight();
3805
3806            if (textht < boxht) {
3807                if (gravity == Gravity.TOP)
3808                    voffset = boxht - textht;
3809                else // (gravity == Gravity.CENTER_VERTICAL)
3810                    voffset = (boxht - textht) >> 1;
3811            }
3812        }
3813        return voffset;
3814    }
3815
3816    private void invalidateCursorPath() {
3817        if (mHighlightPathBogus) {
3818            invalidateCursor();
3819        } else {
3820            final int horizontalPadding = getCompoundPaddingLeft();
3821            final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
3822
3823            if (mCursorCount == 0) {
3824                synchronized (sTempRect) {
3825                    /*
3826                     * The reason for this concern about the thickness of the
3827                     * cursor and doing the floor/ceil on the coordinates is that
3828                     * some EditTexts (notably textfields in the Browser) have
3829                     * anti-aliased text where not all the characters are
3830                     * necessarily at integer-multiple locations.  This should
3831                     * make sure the entire cursor gets invalidated instead of
3832                     * sometimes missing half a pixel.
3833                     */
3834                    float thick = FloatMath.ceil(mTextPaint.getStrokeWidth());
3835                    if (thick < 1.0f) {
3836                        thick = 1.0f;
3837                    }
3838
3839                    thick /= 2.0f;
3840
3841                    mHighlightPath.computeBounds(sTempRect, false);
3842
3843                    invalidate((int) FloatMath.floor(horizontalPadding + sTempRect.left - thick),
3844                            (int) FloatMath.floor(verticalPadding + sTempRect.top - thick),
3845                            (int) FloatMath.ceil(horizontalPadding + sTempRect.right + thick),
3846                            (int) FloatMath.ceil(verticalPadding + sTempRect.bottom + thick));
3847                }
3848            } else {
3849                for (int i = 0; i < mCursorCount; i++) {
3850                    Rect bounds = mCursorDrawable[i].getBounds();
3851                    invalidate(bounds.left + horizontalPadding, bounds.top + verticalPadding,
3852                            bounds.right + horizontalPadding, bounds.bottom + verticalPadding);
3853                }
3854            }
3855        }
3856    }
3857
3858    private void invalidateCursor() {
3859        int where = getSelectionEnd();
3860
3861        invalidateCursor(where, where, where);
3862    }
3863
3864    private void invalidateCursor(int a, int b, int c) {
3865        if (mLayout == null) {
3866            invalidate();
3867        } else {
3868            if (a >= 0 || b >= 0 || c >= 0) {
3869                int first = Math.min(Math.min(a, b), c);
3870                int last = Math.max(Math.max(a, b), c);
3871
3872                int line = mLayout.getLineForOffset(first);
3873                int top = mLayout.getLineTop(line);
3874
3875                // This is ridiculous, but the descent from the line above
3876                // can hang down into the line we really want to redraw,
3877                // so we have to invalidate part of the line above to make
3878                // sure everything that needs to be redrawn really is.
3879                // (But not the whole line above, because that would cause
3880                // the same problem with the descenders on the line above it!)
3881                if (line > 0) {
3882                    top -= mLayout.getLineDescent(line - 1);
3883                }
3884
3885                int line2;
3886
3887                if (first == last)
3888                    line2 = line;
3889                else
3890                    line2 = mLayout.getLineForOffset(last);
3891
3892                int bottom = mLayout.getLineTop(line2 + 1);
3893
3894                final int horizontalPadding = getCompoundPaddingLeft();
3895                final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
3896
3897                // If used, the cursor drawables can have an arbitrary dimension that can go beyond
3898                // the invalidated lines specified above.
3899                for (int i = 0; i < mCursorCount; i++) {
3900                    Rect bounds = mCursorDrawable[i].getBounds();
3901                    top = Math.min(top, bounds.top);
3902                    bottom = Math.max(bottom, bounds.bottom);
3903                    // Horizontal bounds are already full width, no need to update
3904                }
3905
3906                invalidate(horizontalPadding + mScrollX, top + verticalPadding,
3907                        horizontalPadding + mScrollX + getWidth() -
3908                        getCompoundPaddingLeft() - getCompoundPaddingRight(),
3909                        bottom + verticalPadding);
3910            }
3911        }
3912    }
3913
3914    private void registerForPreDraw() {
3915        final ViewTreeObserver observer = getViewTreeObserver();
3916
3917        if (mPreDrawState == PREDRAW_NOT_REGISTERED) {
3918            observer.addOnPreDrawListener(this);
3919            mPreDrawState = PREDRAW_PENDING;
3920        } else if (mPreDrawState == PREDRAW_DONE) {
3921            mPreDrawState = PREDRAW_PENDING;
3922        }
3923
3924        // else state is PREDRAW_PENDING, so keep waiting.
3925    }
3926
3927    /**
3928     * {@inheritDoc}
3929     */
3930    public boolean onPreDraw() {
3931        if (mPreDrawState != PREDRAW_PENDING) {
3932            return true;
3933        }
3934
3935        if (mLayout == null) {
3936            assumeLayout();
3937        }
3938
3939        boolean changed = false;
3940
3941        if (mMovement != null) {
3942            /* This code also provides auto-scrolling when a cursor is moved using a
3943             * CursorController (insertion point or selection limits).
3944             * For selection, ensure start or end is visible depending on controller's state.
3945             */
3946            int curs = getSelectionEnd();
3947            // Do not create the controller if it is not already created.
3948            if (mSelectionModifierCursorController != null &&
3949                    mSelectionModifierCursorController.isSelectionStartDragged()) {
3950                curs = getSelectionStart();
3951            }
3952
3953            /*
3954             * TODO: This should really only keep the end in view if
3955             * it already was before the text changed.  I'm not sure
3956             * of a good way to tell from here if it was.
3957             */
3958            if (curs < 0 &&
3959                  (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
3960                curs = mText.length();
3961            }
3962
3963            if (curs >= 0) {
3964                changed = bringPointIntoView(curs);
3965            }
3966        } else {
3967            changed = bringTextIntoView();
3968        }
3969
3970        // This has to be checked here since:
3971        // - onFocusChanged cannot start it when focus is given to a view with selected text (after
3972        //   a screen rotation) since layout is not yet initialized at that point.
3973        if (mCreatedWithASelection) {
3974            startSelectionActionMode();
3975            mCreatedWithASelection = false;
3976        }
3977
3978        // Phone specific code (there is no ExtractEditText on tablets).
3979        // ExtractEditText does not call onFocus when it is displayed, and mHasSelectionOnFocus can
3980        // not be set. Do the test here instead.
3981        if (this instanceof ExtractEditText && hasSelection()) {
3982            startSelectionActionMode();
3983        }
3984
3985        mPreDrawState = PREDRAW_DONE;
3986        return !changed;
3987    }
3988
3989    @Override
3990    protected void onAttachedToWindow() {
3991        super.onAttachedToWindow();
3992
3993        mTemporaryDetach = false;
3994
3995        if (mShowErrorAfterAttach) {
3996            showError();
3997            mShowErrorAfterAttach = false;
3998        }
3999
4000        final ViewTreeObserver observer = getViewTreeObserver();
4001        // No need to create the controller.
4002        // The get method will add the listener on controller creation.
4003        if (mInsertionPointCursorController != null) {
4004            observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
4005        }
4006        if (mSelectionModifierCursorController != null) {
4007            observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
4008        }
4009    }
4010
4011    @Override
4012    protected void onDetachedFromWindow() {
4013        super.onDetachedFromWindow();
4014
4015        final ViewTreeObserver observer = getViewTreeObserver();
4016        if (mPreDrawState != PREDRAW_NOT_REGISTERED) {
4017            observer.removeOnPreDrawListener(this);
4018            mPreDrawState = PREDRAW_NOT_REGISTERED;
4019        }
4020
4021        if (mError != null) {
4022            hideError();
4023        }
4024
4025        if (mBlink != null) {
4026            mBlink.removeCallbacks(mBlink);
4027        }
4028
4029        if (mInsertionPointCursorController != null) {
4030            mInsertionPointCursorController.onDetached();
4031        }
4032
4033        if (mSelectionModifierCursorController != null) {
4034            mSelectionModifierCursorController.onDetached();
4035        }
4036
4037        hideControllers();
4038    }
4039
4040    @Override
4041    protected boolean isPaddingOffsetRequired() {
4042        return mShadowRadius != 0 || mDrawables != null;
4043    }
4044
4045    @Override
4046    protected int getLeftPaddingOffset() {
4047        return getCompoundPaddingLeft() - mPaddingLeft +
4048                (int) Math.min(0, mShadowDx - mShadowRadius);
4049    }
4050
4051    @Override
4052    protected int getTopPaddingOffset() {
4053        return (int) Math.min(0, mShadowDy - mShadowRadius);
4054    }
4055
4056    @Override
4057    protected int getBottomPaddingOffset() {
4058        return (int) Math.max(0, mShadowDy + mShadowRadius);
4059    }
4060
4061    @Override
4062    protected int getRightPaddingOffset() {
4063        return -(getCompoundPaddingRight() - mPaddingRight) +
4064                (int) Math.max(0, mShadowDx + mShadowRadius);
4065    }
4066
4067    @Override
4068    protected boolean verifyDrawable(Drawable who) {
4069        final boolean verified = super.verifyDrawable(who);
4070        if (!verified && mDrawables != null) {
4071            return who == mDrawables.mDrawableLeft || who == mDrawables.mDrawableTop ||
4072                    who == mDrawables.mDrawableRight || who == mDrawables.mDrawableBottom;
4073        }
4074        return verified;
4075    }
4076
4077    @Override
4078    public void jumpDrawablesToCurrentState() {
4079        super.jumpDrawablesToCurrentState();
4080        if (mDrawables != null) {
4081            if (mDrawables.mDrawableLeft != null) {
4082                mDrawables.mDrawableLeft.jumpToCurrentState();
4083            }
4084            if (mDrawables.mDrawableTop != null) {
4085                mDrawables.mDrawableTop.jumpToCurrentState();
4086            }
4087            if (mDrawables.mDrawableRight != null) {
4088                mDrawables.mDrawableRight.jumpToCurrentState();
4089            }
4090            if (mDrawables.mDrawableBottom != null) {
4091                mDrawables.mDrawableBottom.jumpToCurrentState();
4092            }
4093        }
4094    }
4095
4096    @Override
4097    public void invalidateDrawable(Drawable drawable) {
4098        if (verifyDrawable(drawable)) {
4099            final Rect dirty = drawable.getBounds();
4100            int scrollX = mScrollX;
4101            int scrollY = mScrollY;
4102
4103            // IMPORTANT: The coordinates below are based on the coordinates computed
4104            // for each compound drawable in onDraw(). Make sure to update each section
4105            // accordingly.
4106            final TextView.Drawables drawables = mDrawables;
4107            if (drawables != null) {
4108                if (drawable == drawables.mDrawableLeft) {
4109                    final int compoundPaddingTop = getCompoundPaddingTop();
4110                    final int compoundPaddingBottom = getCompoundPaddingBottom();
4111                    final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
4112
4113                    scrollX += mPaddingLeft;
4114                    scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightLeft) / 2;
4115                } else if (drawable == drawables.mDrawableRight) {
4116                    final int compoundPaddingTop = getCompoundPaddingTop();
4117                    final int compoundPaddingBottom = getCompoundPaddingBottom();
4118                    final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
4119
4120                    scrollX += (mRight - mLeft - mPaddingRight - drawables.mDrawableSizeRight);
4121                    scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightRight) / 2;
4122                } else if (drawable == drawables.mDrawableTop) {
4123                    final int compoundPaddingLeft = getCompoundPaddingLeft();
4124                    final int compoundPaddingRight = getCompoundPaddingRight();
4125                    final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
4126
4127                    scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthTop) / 2;
4128                    scrollY += mPaddingTop;
4129                } else if (drawable == drawables.mDrawableBottom) {
4130                    final int compoundPaddingLeft = getCompoundPaddingLeft();
4131                    final int compoundPaddingRight = getCompoundPaddingRight();
4132                    final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
4133
4134                    scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthBottom) / 2;
4135                    scrollY += (mBottom - mTop - mPaddingBottom - drawables.mDrawableSizeBottom);
4136                }
4137            }
4138
4139            invalidate(dirty.left + scrollX, dirty.top + scrollY,
4140                    dirty.right + scrollX, dirty.bottom + scrollY);
4141        }
4142    }
4143
4144    @Override
4145    protected boolean onSetAlpha(int alpha) {
4146        // Alpha is supported if and only if the drawing can be done in one pass.
4147        // TODO text with spans with a background color currently do not respect this alpha.
4148        if (getBackground() == null) {
4149            mCurrentAlpha = alpha;
4150            final Drawables dr = mDrawables;
4151            if (dr != null) {
4152                if (dr.mDrawableLeft != null) dr.mDrawableLeft.mutate().setAlpha(alpha);
4153                if (dr.mDrawableTop != null) dr.mDrawableTop.mutate().setAlpha(alpha);
4154                if (dr.mDrawableRight != null) dr.mDrawableRight.mutate().setAlpha(alpha);
4155                if (dr.mDrawableBottom != null) dr.mDrawableBottom.mutate().setAlpha(alpha);
4156            }
4157            return true;
4158        }
4159
4160        mCurrentAlpha = 255;
4161        return false;
4162    }
4163
4164    /**
4165     * When a TextView is used to display a useful piece of information to the user (such as a
4166     * contact's address), it should be made selectable, so that the user can select and copy this
4167     * content.
4168     *
4169     * Use {@link #setTextIsSelectable(boolean)} or the
4170     * {@link android.R.styleable#TextView_textIsSelectable} XML attribute to make this TextView
4171     * selectable (text is not selectable by default).
4172     *
4173     * Note that the content of an EditText is always selectable.
4174     *
4175     * @return True if the text displayed in this TextView can be selected by the user.
4176     *
4177     * @attr ref android.R.styleable#TextView_textIsSelectable
4178     */
4179    public boolean isTextSelectable() {
4180        return mTextIsSelectable;
4181    }
4182
4183    /**
4184     * Sets whether or not (default) the content of this view is selectable by the user.
4185     *
4186     * Note that this methods affect the {@link #setFocusable(boolean)},
4187     * {@link #setFocusableInTouchMode(boolean)} {@link #setClickable(boolean)} and
4188     * {@link #setLongClickable(boolean)} states and you may want to restore these if they were
4189     * customized.
4190     *
4191     * See {@link #isTextSelectable} for details.
4192     *
4193     * @param selectable Whether or not the content of this TextView should be selectable.
4194     */
4195    public void setTextIsSelectable(boolean selectable) {
4196        if (mTextIsSelectable == selectable) return;
4197
4198        mTextIsSelectable = selectable;
4199
4200        setFocusableInTouchMode(selectable);
4201        setFocusable(selectable);
4202        setClickable(selectable);
4203        setLongClickable(selectable);
4204
4205        // mInputType is already EditorInfo.TYPE_NULL and mInput is null;
4206
4207        setMovementMethod(selectable ? ArrowKeyMovementMethod.getInstance() : null);
4208        setText(getText(), selectable ? BufferType.SPANNABLE : BufferType.NORMAL);
4209
4210        // Called by setText above, but safer in case of future code changes
4211        prepareCursorControllers();
4212    }
4213
4214    @Override
4215    protected int[] onCreateDrawableState(int extraSpace) {
4216        final int[] drawableState;
4217
4218        if (mSingleLine) {
4219            drawableState = super.onCreateDrawableState(extraSpace);
4220        } else {
4221            drawableState = super.onCreateDrawableState(extraSpace + 1);
4222            mergeDrawableStates(drawableState, MULTILINE_STATE_SET);
4223        }
4224
4225        if (mTextIsSelectable) {
4226            // Disable pressed state, which was introduced when TextView was made clickable.
4227            // Prevents text color change.
4228            // setClickable(false) would have a similar effect, but it also disables focus changes
4229            // and long press actions, which are both needed by text selection.
4230            final int length = drawableState.length;
4231            for (int i = 0; i < length; i++) {
4232                if (drawableState[i] == R.attr.state_pressed) {
4233                    final int[] nonPressedState = new int[length - 1];
4234                    System.arraycopy(drawableState, 0, nonPressedState, 0, i);
4235                    System.arraycopy(drawableState, i + 1, nonPressedState, i, length - i - 1);
4236                    return nonPressedState;
4237                }
4238            }
4239        }
4240
4241        return drawableState;
4242    }
4243
4244    @Override
4245    protected void onDraw(Canvas canvas) {
4246        if (mPreDrawState == PREDRAW_DONE) {
4247            final ViewTreeObserver observer = getViewTreeObserver();
4248            observer.removeOnPreDrawListener(this);
4249            mPreDrawState = PREDRAW_NOT_REGISTERED;
4250        }
4251
4252        if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return;
4253
4254        restartMarqueeIfNeeded();
4255
4256        // Draw the background for this view
4257        super.onDraw(canvas);
4258
4259        final int compoundPaddingLeft = getCompoundPaddingLeft();
4260        final int compoundPaddingTop = getCompoundPaddingTop();
4261        final int compoundPaddingRight = getCompoundPaddingRight();
4262        final int compoundPaddingBottom = getCompoundPaddingBottom();
4263        final int scrollX = mScrollX;
4264        final int scrollY = mScrollY;
4265        final int right = mRight;
4266        final int left = mLeft;
4267        final int bottom = mBottom;
4268        final int top = mTop;
4269
4270        final Drawables dr = mDrawables;
4271        if (dr != null) {
4272            /*
4273             * Compound, not extended, because the icon is not clipped
4274             * if the text height is smaller.
4275             */
4276
4277            int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
4278            int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;
4279
4280            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4281            // Make sure to update invalidateDrawable() when changing this code.
4282            if (dr.mDrawableLeft != null) {
4283                canvas.save();
4284                canvas.translate(scrollX + mPaddingLeft,
4285                                 scrollY + compoundPaddingTop +
4286                                 (vspace - dr.mDrawableHeightLeft) / 2);
4287                dr.mDrawableLeft.draw(canvas);
4288                canvas.restore();
4289            }
4290
4291            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4292            // Make sure to update invalidateDrawable() when changing this code.
4293            if (dr.mDrawableRight != null) {
4294                canvas.save();
4295                canvas.translate(scrollX + right - left - mPaddingRight - dr.mDrawableSizeRight,
4296                         scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2);
4297                dr.mDrawableRight.draw(canvas);
4298                canvas.restore();
4299            }
4300
4301            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4302            // Make sure to update invalidateDrawable() when changing this code.
4303            if (dr.mDrawableTop != null) {
4304                canvas.save();
4305                canvas.translate(scrollX + compoundPaddingLeft + (hspace - dr.mDrawableWidthTop) / 2,
4306                        scrollY + mPaddingTop);
4307                dr.mDrawableTop.draw(canvas);
4308                canvas.restore();
4309            }
4310
4311            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
4312            // Make sure to update invalidateDrawable() when changing this code.
4313            if (dr.mDrawableBottom != null) {
4314                canvas.save();
4315                canvas.translate(scrollX + compoundPaddingLeft +
4316                        (hspace - dr.mDrawableWidthBottom) / 2,
4317                         scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom);
4318                dr.mDrawableBottom.draw(canvas);
4319                canvas.restore();
4320            }
4321        }
4322
4323        int color = mCurTextColor;
4324
4325        if (mLayout == null) {
4326            assumeLayout();
4327        }
4328
4329        Layout layout = mLayout;
4330        int cursorcolor = color;
4331
4332        if (mHint != null && mText.length() == 0) {
4333            if (mHintTextColor != null) {
4334                color = mCurHintTextColor;
4335            }
4336
4337            layout = mHintLayout;
4338        }
4339
4340        mTextPaint.setColor(color);
4341        if (mCurrentAlpha != 255) {
4342            // If set, the alpha will override the color's alpha. Multiply the alphas.
4343            mTextPaint.setAlpha((mCurrentAlpha * Color.alpha(color)) / 255);
4344        }
4345        mTextPaint.drawableState = getDrawableState();
4346
4347        canvas.save();
4348        /*  Would be faster if we didn't have to do this. Can we chop the
4349            (displayable) text so that we don't need to do this ever?
4350        */
4351
4352        int extendedPaddingTop = getExtendedPaddingTop();
4353        int extendedPaddingBottom = getExtendedPaddingBottom();
4354
4355        float clipLeft = compoundPaddingLeft + scrollX;
4356        float clipTop = extendedPaddingTop + scrollY;
4357        float clipRight = right - left - compoundPaddingRight + scrollX;
4358        float clipBottom = bottom - top - extendedPaddingBottom + scrollY;
4359
4360        if (mShadowRadius != 0) {
4361            clipLeft += Math.min(0, mShadowDx - mShadowRadius);
4362            clipRight += Math.max(0, mShadowDx + mShadowRadius);
4363
4364            clipTop += Math.min(0, mShadowDy - mShadowRadius);
4365            clipBottom += Math.max(0, mShadowDy + mShadowRadius);
4366        }
4367
4368        canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom);
4369
4370        int voffsetText = 0;
4371        int voffsetCursor = 0;
4372
4373        // translate in by our padding
4374        {
4375            /* shortcircuit calling getVerticaOffset() */
4376            if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
4377                voffsetText = getVerticalOffset(false);
4378                voffsetCursor = getVerticalOffset(true);
4379            }
4380            canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText);
4381        }
4382
4383        if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
4384            if (!mSingleLine && getLineCount() == 1 && canMarquee() &&
4385                    (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
4386                canvas.translate(mLayout.getLineRight(0) - (mRight - mLeft -
4387                        getCompoundPaddingLeft() - getCompoundPaddingRight()), 0.0f);
4388            }
4389
4390            if (mMarquee != null && mMarquee.isRunning()) {
4391                canvas.translate(-mMarquee.mScroll, 0.0f);
4392            }
4393        }
4394
4395        Path highlight = null;
4396        int selStart = -1, selEnd = -1;
4397        boolean drawCursor = false;
4398
4399        //  If there is no movement method, then there can be no selection.
4400        //  Check that first and attempt to skip everything having to do with
4401        //  the cursor.
4402        //  XXX This is not strictly true -- a program could set the
4403        //  selection manually if it really wanted to.
4404        if (mMovement != null && (isFocused() || isPressed())) {
4405            selStart = getSelectionStart();
4406            selEnd = getSelectionEnd();
4407
4408            if ((isCursorVisible() || mTextIsSelectable) && selStart >= 0 && isEnabled()) {
4409                if (mHighlightPath == null)
4410                    mHighlightPath = new Path();
4411
4412                if (selStart == selEnd) {
4413                    if (!mTextIsSelectable &&
4414                            (SystemClock.uptimeMillis() - mShowCursor) % (2 * BLINK) < BLINK) {
4415                        if (mHighlightPathBogus) {
4416                            mHighlightPath.reset();
4417                            mLayout.getCursorPath(selStart, mHighlightPath, mText);
4418                            updateCursorsPositions();
4419                            mHighlightPathBogus = false;
4420                        }
4421
4422                        // XXX should pass to skin instead of drawing directly
4423                        mHighlightPaint.setColor(cursorcolor);
4424                        if (mCurrentAlpha != 255) {
4425                            mHighlightPaint.setAlpha(
4426                                    (mCurrentAlpha * Color.alpha(cursorcolor)) / 255);
4427                        }
4428                        mHighlightPaint.setStyle(Paint.Style.STROKE);
4429                        highlight = mHighlightPath;
4430                        drawCursor = mCursorCount > 0;
4431                    }
4432                } else {
4433                    if (mHighlightPathBogus) {
4434                        mHighlightPath.reset();
4435                        mLayout.getSelectionPath(selStart, selEnd, mHighlightPath);
4436                        mHighlightPathBogus = false;
4437                    }
4438
4439                    // XXX should pass to skin instead of drawing directly
4440                    mHighlightPaint.setColor(mHighlightColor);
4441                    if (mCurrentAlpha != 255) {
4442                        mHighlightPaint.setAlpha(
4443                                (mCurrentAlpha * Color.alpha(mHighlightColor)) / 255);
4444                    }
4445                    mHighlightPaint.setStyle(Paint.Style.FILL);
4446
4447                    highlight = mHighlightPath;
4448                }
4449            }
4450        }
4451
4452        /*  Comment out until we decide what to do about animations
4453        boolean isLinearTextOn = false;
4454        if (currentTransformation != null) {
4455            isLinearTextOn = mTextPaint.isLinearTextOn();
4456            Matrix m = currentTransformation.getMatrix();
4457            if (!m.isIdentity()) {
4458                // mTextPaint.setLinearTextOn(true);
4459            }
4460        }
4461        */
4462
4463        final InputMethodState ims = mInputMethodState;
4464        final int cursorOffsetVertical = voffsetCursor - voffsetText;
4465        if (ims != null && ims.mBatchEditNesting == 0) {
4466            InputMethodManager imm = InputMethodManager.peekInstance();
4467            if (imm != null) {
4468                if (imm.isActive(this)) {
4469                    boolean reported = false;
4470                    if (ims.mContentChanged || ims.mSelectionModeChanged) {
4471                        // We are in extract mode and the content has changed
4472                        // in some way... just report complete new text to the
4473                        // input method.
4474                        reported = reportExtractedText();
4475                    }
4476                    if (!reported && highlight != null) {
4477                        int candStart = -1;
4478                        int candEnd = -1;
4479                        if (mText instanceof Spannable) {
4480                            Spannable sp = (Spannable)mText;
4481                            candStart = EditableInputConnection.getComposingSpanStart(sp);
4482                            candEnd = EditableInputConnection.getComposingSpanEnd(sp);
4483                        }
4484                        imm.updateSelection(this, selStart, selEnd, candStart, candEnd);
4485                    }
4486                }
4487
4488                if (imm.isWatchingCursor(this) && highlight != null) {
4489                    highlight.computeBounds(ims.mTmpRectF, true);
4490                    ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0;
4491
4492                    canvas.getMatrix().mapPoints(ims.mTmpOffset);
4493                    ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]);
4494
4495                    ims.mTmpRectF.offset(0, cursorOffsetVertical);
4496
4497                    ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5),
4498                            (int)(ims.mTmpRectF.top + 0.5),
4499                            (int)(ims.mTmpRectF.right + 0.5),
4500                            (int)(ims.mTmpRectF.bottom + 0.5));
4501
4502                    imm.updateCursor(this,
4503                            ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top,
4504                            ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom);
4505                }
4506            }
4507        }
4508
4509        if (mCorrectionHighlighter != null) {
4510            mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
4511        }
4512
4513        if (drawCursor) {
4514            drawCursor(canvas, cursorOffsetVertical);
4515            // Rely on the drawable entirely, do not draw the cursor line.
4516            // Has to be done after the IMM related code above which relies on the highlight.
4517            highlight = null;
4518        }
4519
4520        layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
4521
4522        if (mMarquee != null && mMarquee.shouldDrawGhost()) {
4523            canvas.translate((int) mMarquee.getGhostOffset(), 0.0f);
4524            layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
4525        }
4526
4527        /*  Comment out until we decide what to do about animations
4528        if (currentTransformation != null) {
4529            mTextPaint.setLinearTextOn(isLinearTextOn);
4530        }
4531        */
4532
4533        canvas.restore();
4534    }
4535
4536    private void updateCursorsPositions() {
4537        if (mCursorDrawableRes == 0) {
4538            mCursorCount = 0;
4539            return;
4540        }
4541
4542        final int offset = getSelectionStart();
4543        final int line = mLayout.getLineForOffset(offset);
4544        final int top = mLayout.getLineTop(line);
4545        final int bottom = mLayout.getLineTop(line + 1);
4546
4547        mCursorCount = mLayout.isLevelBoundary(offset) ? 2 : 1;
4548
4549        int middle = bottom;
4550        if (mCursorCount == 2) {
4551            // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)}
4552            middle = (top + bottom) >> 1;
4553        }
4554
4555        updateCursorPosition(0, top, middle, mLayout.getPrimaryHorizontal(offset));
4556
4557        if (mCursorCount == 2) {
4558            updateCursorPosition(1, middle, bottom, mLayout.getSecondaryHorizontal(offset));
4559        }
4560    }
4561
4562    private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
4563        if (mCursorDrawable[cursorIndex] == null)
4564            mCursorDrawable[cursorIndex] = mContext.getResources().getDrawable(mCursorDrawableRes);
4565
4566        if (mTempRect == null) mTempRect = new Rect();
4567
4568        mCursorDrawable[cursorIndex].getPadding(mTempRect);
4569        final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth();
4570        horizontal = Math.max(0.5f, horizontal - 0.5f);
4571        final int left = (int) (horizontal) - mTempRect.left;
4572        mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width,
4573                bottom + mTempRect.bottom);
4574    }
4575
4576    private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
4577        final boolean translate = cursorOffsetVertical != 0;
4578        if (translate) canvas.translate(0, cursorOffsetVertical);
4579        for (int i = 0; i < mCursorCount; i++) {
4580            mCursorDrawable[i].draw(canvas);
4581        }
4582        if (translate) canvas.translate(0, -cursorOffsetVertical);
4583    }
4584
4585    @Override
4586    public void getFocusedRect(Rect r) {
4587        if (mLayout == null) {
4588            super.getFocusedRect(r);
4589            return;
4590        }
4591
4592        int sel = getSelectionEnd();
4593        if (sel < 0) {
4594            super.getFocusedRect(r);
4595            return;
4596        }
4597
4598        int line = mLayout.getLineForOffset(sel);
4599        r.top = mLayout.getLineTop(line);
4600        r.bottom = mLayout.getLineBottom(line);
4601
4602        r.left = (int) mLayout.getPrimaryHorizontal(sel);
4603        r.right = r.left + 1;
4604
4605        // Adjust for padding and gravity.
4606        int paddingLeft = getCompoundPaddingLeft();
4607        int paddingTop = getExtendedPaddingTop();
4608        if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
4609            paddingTop += getVerticalOffset(false);
4610        }
4611        r.offset(paddingLeft, paddingTop);
4612    }
4613
4614    /**
4615     * Return the number of lines of text, or 0 if the internal Layout has not
4616     * been built.
4617     */
4618    public int getLineCount() {
4619        return mLayout != null ? mLayout.getLineCount() : 0;
4620    }
4621
4622    /**
4623     * Return the baseline for the specified line (0...getLineCount() - 1)
4624     * If bounds is not null, return the top, left, right, bottom extents
4625     * of the specified line in it. If the internal Layout has not been built,
4626     * return 0 and set bounds to (0, 0, 0, 0)
4627     * @param line which line to examine (0..getLineCount() - 1)
4628     * @param bounds Optional. If not null, it returns the extent of the line
4629     * @return the Y-coordinate of the baseline
4630     */
4631    public int getLineBounds(int line, Rect bounds) {
4632        if (mLayout == null) {
4633            if (bounds != null) {
4634                bounds.set(0, 0, 0, 0);
4635            }
4636            return 0;
4637        }
4638        else {
4639            int baseline = mLayout.getLineBounds(line, bounds);
4640
4641            int voffset = getExtendedPaddingTop();
4642            if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
4643                voffset += getVerticalOffset(true);
4644            }
4645            if (bounds != null) {
4646                bounds.offset(getCompoundPaddingLeft(), voffset);
4647            }
4648            return baseline + voffset;
4649        }
4650    }
4651
4652    @Override
4653    public int getBaseline() {
4654        if (mLayout == null) {
4655            return super.getBaseline();
4656        }
4657
4658        int voffset = 0;
4659        if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
4660            voffset = getVerticalOffset(true);
4661        }
4662
4663        return getExtendedPaddingTop() + voffset + mLayout.getLineBaseline(0);
4664    }
4665
4666    @Override
4667    public boolean onKeyDown(int keyCode, KeyEvent event) {
4668        int which = doKeyDown(keyCode, event, null);
4669        if (which == 0) {
4670            // Go through default dispatching.
4671            return super.onKeyDown(keyCode, event);
4672        }
4673
4674        return true;
4675    }
4676
4677    @Override
4678    public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
4679        KeyEvent down = KeyEvent.changeAction(event, KeyEvent.ACTION_DOWN);
4680
4681        int which = doKeyDown(keyCode, down, event);
4682        if (which == 0) {
4683            // Go through default dispatching.
4684            return super.onKeyMultiple(keyCode, repeatCount, event);
4685        }
4686        if (which == -1) {
4687            // Consumed the whole thing.
4688            return true;
4689        }
4690
4691        repeatCount--;
4692
4693        // We are going to dispatch the remaining events to either the input
4694        // or movement method.  To do this, we will just send a repeated stream
4695        // of down and up events until we have done the complete repeatCount.
4696        // It would be nice if those interfaces had an onKeyMultiple() method,
4697        // but adding that is a more complicated change.
4698        KeyEvent up = KeyEvent.changeAction(event, KeyEvent.ACTION_UP);
4699        if (which == 1) {
4700            mInput.onKeyUp(this, (Editable)mText, keyCode, up);
4701            while (--repeatCount > 0) {
4702                mInput.onKeyDown(this, (Editable)mText, keyCode, down);
4703                mInput.onKeyUp(this, (Editable)mText, keyCode, up);
4704            }
4705            hideErrorIfUnchanged();
4706
4707        } else if (which == 2) {
4708            mMovement.onKeyUp(this, (Spannable)mText, keyCode, up);
4709            while (--repeatCount > 0) {
4710                mMovement.onKeyDown(this, (Spannable)mText, keyCode, down);
4711                mMovement.onKeyUp(this, (Spannable)mText, keyCode, up);
4712            }
4713        }
4714
4715        return true;
4716    }
4717
4718    /**
4719     * Returns true if pressing ENTER in this field advances focus instead
4720     * of inserting the character.  This is true mostly in single-line fields,
4721     * but also in mail addresses and subjects which will display on multiple
4722     * lines but where it doesn't make sense to insert newlines.
4723     */
4724    private boolean shouldAdvanceFocusOnEnter() {
4725        if (mInput == null) {
4726            return false;
4727        }
4728
4729        if (mSingleLine) {
4730            return true;
4731        }
4732
4733        if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
4734            int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
4735            if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
4736                    || variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT) {
4737                return true;
4738            }
4739        }
4740
4741        return false;
4742    }
4743
4744    /**
4745     * Returns true if pressing TAB in this field advances focus instead
4746     * of inserting the character.  Insert tabs only in multi-line editors.
4747     */
4748    private boolean shouldAdvanceFocusOnTab() {
4749        if (mInput != null && !mSingleLine) {
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_FLAG_IME_MULTI_LINE
4753                        || variation == EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) {
4754                    return false;
4755                }
4756            }
4757        }
4758        return true;
4759    }
4760
4761    private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) {
4762        if (!isEnabled()) {
4763            return 0;
4764        }
4765
4766        switch (keyCode) {
4767            case KeyEvent.KEYCODE_ENTER:
4768                mEnterKeyIsDown = true;
4769                if (event.hasNoModifiers()) {
4770                    // When mInputContentType is set, we know that we are
4771                    // running in a "modern" cupcake environment, so don't need
4772                    // to worry about the application trying to capture
4773                    // enter key events.
4774                    if (mInputContentType != null) {
4775                        // If there is an action listener, given them a
4776                        // chance to consume the event.
4777                        if (mInputContentType.onEditorActionListener != null &&
4778                                mInputContentType.onEditorActionListener.onEditorAction(
4779                                this, EditorInfo.IME_NULL, event)) {
4780                            mInputContentType.enterDown = true;
4781                            // We are consuming the enter key for them.
4782                            return -1;
4783                        }
4784                    }
4785
4786                    // If our editor should move focus when enter is pressed, or
4787                    // this is a generated event from an IME action button, then
4788                    // don't let it be inserted into the text.
4789                    if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
4790                            || shouldAdvanceFocusOnEnter()) {
4791                        if (mOnClickListener != null) {
4792                            return 0;
4793                        }
4794                        return -1;
4795                    }
4796                }
4797                break;
4798
4799            case KeyEvent.KEYCODE_DPAD_CENTER:
4800                mDPadCenterIsDown = true;
4801                if (event.hasNoModifiers()) {
4802                    if (shouldAdvanceFocusOnEnter()) {
4803                        return 0;
4804                    }
4805                }
4806                break;
4807
4808            case KeyEvent.KEYCODE_TAB:
4809                if (event.hasNoModifiers() || event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
4810                    if (shouldAdvanceFocusOnTab()) {
4811                        return 0;
4812                    }
4813                }
4814                break;
4815
4816                // Has to be done on key down (and not on key up) to correctly be intercepted.
4817            case KeyEvent.KEYCODE_BACK:
4818                if (mSelectionActionMode != null) {
4819                    stopSelectionActionMode();
4820                    return -1;
4821                }
4822                break;
4823        }
4824
4825        if (mInput != null) {
4826            resetErrorChangedFlag();
4827
4828            boolean doDown = true;
4829            if (otherEvent != null) {
4830                try {
4831                    beginBatchEdit();
4832                    final boolean handled = mInput.onKeyOther(this, (Editable) mText, otherEvent);
4833                    hideErrorIfUnchanged();
4834                    doDown = false;
4835                    if (handled) {
4836                        return -1;
4837                    }
4838                } catch (AbstractMethodError e) {
4839                    // onKeyOther was added after 1.0, so if it isn't
4840                    // implemented we need to try to dispatch as a regular down.
4841                } finally {
4842                    endBatchEdit();
4843                }
4844            }
4845
4846            if (doDown) {
4847                beginBatchEdit();
4848                final boolean handled = mInput.onKeyDown(this, (Editable) mText, keyCode, event);
4849                endBatchEdit();
4850                hideErrorIfUnchanged();
4851                if (handled) return 1;
4852            }
4853        }
4854
4855        // bug 650865: sometimes we get a key event before a layout.
4856        // don't try to move around if we don't know the layout.
4857
4858        if (mMovement != null && mLayout != null) {
4859            boolean doDown = true;
4860            if (otherEvent != null) {
4861                try {
4862                    boolean handled = mMovement.onKeyOther(this, (Spannable) mText,
4863                            otherEvent);
4864                    doDown = false;
4865                    if (handled) {
4866                        return -1;
4867                    }
4868                } catch (AbstractMethodError e) {
4869                    // onKeyOther was added after 1.0, so if it isn't
4870                    // implemented we need to try to dispatch as a regular down.
4871                }
4872            }
4873            if (doDown) {
4874                if (mMovement.onKeyDown(this, (Spannable)mText, keyCode, event))
4875                    return 2;
4876            }
4877        }
4878
4879        return 0;
4880    }
4881
4882    /**
4883     * Resets the mErrorWasChanged flag, so that future calls to {@link #setError(CharSequence)}
4884     * can be recorded.
4885     * @hide
4886     */
4887    public void resetErrorChangedFlag() {
4888        /*
4889         * Keep track of what the error was before doing the input
4890         * so that if an input filter changed the error, we leave
4891         * that error showing.  Otherwise, we take down whatever
4892         * error was showing when the user types something.
4893         */
4894        mErrorWasChanged = false;
4895    }
4896
4897    /**
4898     * @hide
4899     */
4900    public void hideErrorIfUnchanged() {
4901        if (mError != null && !mErrorWasChanged) {
4902            setError(null, null);
4903        }
4904    }
4905
4906    @Override
4907    public boolean onKeyUp(int keyCode, KeyEvent event) {
4908        if (!isEnabled()) {
4909            return super.onKeyUp(keyCode, event);
4910        }
4911
4912        switch (keyCode) {
4913            case KeyEvent.KEYCODE_DPAD_CENTER:
4914                mDPadCenterIsDown = false;
4915                if (event.hasNoModifiers()) {
4916                    /*
4917                     * If there is a click listener, just call through to
4918                     * super, which will invoke it.
4919                     *
4920                     * If there isn't a click listener, try to show the soft
4921                     * input method.  (It will also
4922                     * call performClick(), but that won't do anything in
4923                     * this case.)
4924                     */
4925                    if (mOnClickListener == null) {
4926                        if (mMovement != null && mText instanceof Editable
4927                                && mLayout != null && onCheckIsTextEditor()) {
4928                            InputMethodManager imm = InputMethodManager.peekInstance();
4929                            if (imm != null) imm.showSoftInput(this, 0);
4930                        }
4931                    }
4932                }
4933                return super.onKeyUp(keyCode, event);
4934
4935            case KeyEvent.KEYCODE_ENTER:
4936                mEnterKeyIsDown = false;
4937                if (event.hasNoModifiers()) {
4938                    if (mInputContentType != null
4939                            && mInputContentType.onEditorActionListener != null
4940                            && mInputContentType.enterDown) {
4941                        mInputContentType.enterDown = false;
4942                        if (mInputContentType.onEditorActionListener.onEditorAction(
4943                                this, EditorInfo.IME_NULL, event)) {
4944                            return true;
4945                        }
4946                    }
4947
4948                    if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
4949                            || shouldAdvanceFocusOnEnter()) {
4950                        /*
4951                         * If there is a click listener, just call through to
4952                         * super, which will invoke it.
4953                         *
4954                         * If there isn't a click listener, try to advance focus,
4955                         * but still call through to super, which will reset the
4956                         * pressed state and longpress state.  (It will also
4957                         * call performClick(), but that won't do anything in
4958                         * this case.)
4959                         */
4960                        if (mOnClickListener == null) {
4961                            View v = focusSearch(FOCUS_DOWN);
4962
4963                            if (v != null) {
4964                                if (!v.requestFocus(FOCUS_DOWN)) {
4965                                    throw new IllegalStateException(
4966                                            "focus search returned a view " +
4967                                            "that wasn't able to take focus!");
4968                                }
4969
4970                                /*
4971                                 * Return true because we handled the key; super
4972                                 * will return false because there was no click
4973                                 * listener.
4974                                 */
4975                                super.onKeyUp(keyCode, event);
4976                                return true;
4977                            } else if ((event.getFlags()
4978                                    & KeyEvent.FLAG_EDITOR_ACTION) != 0) {
4979                                // No target for next focus, but make sure the IME
4980                                // if this came from it.
4981                                InputMethodManager imm = InputMethodManager.peekInstance();
4982                                if (imm != null && imm.isActive(this)) {
4983                                    imm.hideSoftInputFromWindow(getWindowToken(), 0);
4984                                }
4985                            }
4986                        }
4987                    }
4988                    return super.onKeyUp(keyCode, event);
4989                }
4990                break;
4991        }
4992
4993        if (mInput != null)
4994            if (mInput.onKeyUp(this, (Editable) mText, keyCode, event))
4995                return true;
4996
4997        if (mMovement != null && mLayout != null)
4998            if (mMovement.onKeyUp(this, (Spannable) mText, keyCode, event))
4999                return true;
5000
5001        return super.onKeyUp(keyCode, event);
5002    }
5003
5004    @Override public boolean onCheckIsTextEditor() {
5005        return mInputType != EditorInfo.TYPE_NULL;
5006    }
5007
5008    @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
5009        if (onCheckIsTextEditor() && isEnabled()) {
5010            if (mInputMethodState == null) {
5011                mInputMethodState = new InputMethodState();
5012            }
5013            outAttrs.inputType = mInputType;
5014            if (mInputContentType != null) {
5015                outAttrs.imeOptions = mInputContentType.imeOptions;
5016                outAttrs.privateImeOptions = mInputContentType.privateImeOptions;
5017                outAttrs.actionLabel = mInputContentType.imeActionLabel;
5018                outAttrs.actionId = mInputContentType.imeActionId;
5019                outAttrs.extras = mInputContentType.extras;
5020            } else {
5021                outAttrs.imeOptions = EditorInfo.IME_NULL;
5022            }
5023            if (focusSearch(FOCUS_DOWN) != null) {
5024                outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT;
5025            }
5026            if (focusSearch(FOCUS_UP) != null) {
5027                outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS;
5028            }
5029            if ((outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION)
5030                    == EditorInfo.IME_ACTION_UNSPECIFIED) {
5031                if ((outAttrs.imeOptions&EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0) {
5032                    // An action has not been set, but the enter key will move to
5033                    // the next focus, so set the action to that.
5034                    outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
5035                } else {
5036                    // An action has not been set, and there is no focus to move
5037                    // to, so let's just supply a "done" action.
5038                    outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
5039                }
5040                if (!shouldAdvanceFocusOnEnter()) {
5041                    outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
5042                }
5043            }
5044            if (isMultilineInputType(outAttrs.inputType)) {
5045                // Multi-line text editors should always show an enter key.
5046                outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
5047            }
5048            outAttrs.hintText = mHint;
5049            if (mText instanceof Editable) {
5050                InputConnection ic = new EditableInputConnection(this);
5051                outAttrs.initialSelStart = getSelectionStart();
5052                outAttrs.initialSelEnd = getSelectionEnd();
5053                outAttrs.initialCapsMode = ic.getCursorCapsMode(mInputType);
5054                return ic;
5055            }
5056        }
5057        return null;
5058    }
5059
5060    /**
5061     * If this TextView contains editable content, extract a portion of it
5062     * based on the information in <var>request</var> in to <var>outText</var>.
5063     * @return Returns true if the text was successfully extracted, else false.
5064     */
5065    public boolean extractText(ExtractedTextRequest request,
5066            ExtractedText outText) {
5067        return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
5068                EXTRACT_UNKNOWN, outText);
5069    }
5070
5071    static final int EXTRACT_NOTHING = -2;
5072    static final int EXTRACT_UNKNOWN = -1;
5073
5074    boolean extractTextInternal(ExtractedTextRequest request,
5075            int partialStartOffset, int partialEndOffset, int delta,
5076            ExtractedText outText) {
5077        final CharSequence content = mText;
5078        if (content != null) {
5079            if (partialStartOffset != EXTRACT_NOTHING) {
5080                final int N = content.length();
5081                if (partialStartOffset < 0) {
5082                    outText.partialStartOffset = outText.partialEndOffset = -1;
5083                    partialStartOffset = 0;
5084                    partialEndOffset = N;
5085                } else {
5086                    // Now use the delta to determine the actual amount of text
5087                    // we need.
5088                    partialEndOffset += delta;
5089                    // Adjust offsets to ensure we contain full spans.
5090                    if (content instanceof Spanned) {
5091                        Spanned spanned = (Spanned)content;
5092                        Object[] spans = spanned.getSpans(partialStartOffset,
5093                                partialEndOffset, ParcelableSpan.class);
5094                        int i = spans.length;
5095                        while (i > 0) {
5096                            i--;
5097                            int j = spanned.getSpanStart(spans[i]);
5098                            if (j < partialStartOffset) partialStartOffset = j;
5099                            j = spanned.getSpanEnd(spans[i]);
5100                            if (j > partialEndOffset) partialEndOffset = j;
5101                        }
5102                    }
5103                    outText.partialStartOffset = partialStartOffset;
5104                    outText.partialEndOffset = partialEndOffset - delta;
5105
5106                    if (partialStartOffset > N) {
5107                        partialStartOffset = N;
5108                    } else if (partialStartOffset < 0) {
5109                        partialStartOffset = 0;
5110                    }
5111                    if (partialEndOffset > N) {
5112                        partialEndOffset = N;
5113                    } else if (partialEndOffset < 0) {
5114                        partialEndOffset = 0;
5115                    }
5116                }
5117                if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) {
5118                    outText.text = content.subSequence(partialStartOffset,
5119                            partialEndOffset);
5120                } else {
5121                    outText.text = TextUtils.substring(content, partialStartOffset,
5122                            partialEndOffset);
5123                }
5124            } else {
5125                outText.partialStartOffset = 0;
5126                outText.partialEndOffset = 0;
5127                outText.text = "";
5128            }
5129            outText.flags = 0;
5130            if (MetaKeyKeyListener.getMetaState(mText, MetaKeyKeyListener.META_SELECTING) != 0) {
5131                outText.flags |= ExtractedText.FLAG_SELECTING;
5132            }
5133            if (mSingleLine) {
5134                outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
5135            }
5136            outText.startOffset = 0;
5137            outText.selectionStart = getSelectionStart();
5138            outText.selectionEnd = getSelectionEnd();
5139            return true;
5140        }
5141        return false;
5142    }
5143
5144    boolean reportExtractedText() {
5145        final InputMethodState ims = mInputMethodState;
5146        if (ims != null) {
5147            final boolean contentChanged = ims.mContentChanged;
5148            if (contentChanged || ims.mSelectionModeChanged) {
5149                ims.mContentChanged = false;
5150                ims.mSelectionModeChanged = false;
5151                final ExtractedTextRequest req = mInputMethodState.mExtracting;
5152                if (req != null) {
5153                    InputMethodManager imm = InputMethodManager.peekInstance();
5154                    if (imm != null) {
5155                        if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Retrieving extracted start="
5156                                + ims.mChangedStart + " end=" + ims.mChangedEnd
5157                                + " delta=" + ims.mChangedDelta);
5158                        if (ims.mChangedStart < 0 && !contentChanged) {
5159                            ims.mChangedStart = EXTRACT_NOTHING;
5160                        }
5161                        if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
5162                                ims.mChangedDelta, ims.mTmpExtracted)) {
5163                            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Reporting extracted start="
5164                                    + ims.mTmpExtracted.partialStartOffset
5165                                    + " end=" + ims.mTmpExtracted.partialEndOffset
5166                                    + ": " + ims.mTmpExtracted.text);
5167                            imm.updateExtractedText(this, req.token,
5168                                    mInputMethodState.mTmpExtracted);
5169                            ims.mChangedStart = EXTRACT_UNKNOWN;
5170                            ims.mChangedEnd = EXTRACT_UNKNOWN;
5171                            ims.mChangedDelta = 0;
5172                            ims.mContentChanged = false;
5173                            return true;
5174                        }
5175                    }
5176                }
5177            }
5178        }
5179        return false;
5180    }
5181
5182    /**
5183     * This is used to remove all style-impacting spans from text before new
5184     * extracted text is being replaced into it, so that we don't have any
5185     * lingering spans applied during the replace.
5186     */
5187    static void removeParcelableSpans(Spannable spannable, int start, int end) {
5188        Object[] spans = spannable.getSpans(start, end, ParcelableSpan.class);
5189        int i = spans.length;
5190        while (i > 0) {
5191            i--;
5192            spannable.removeSpan(spans[i]);
5193        }
5194    }
5195
5196    /**
5197     * Apply to this text view the given extracted text, as previously
5198     * returned by {@link #extractText(ExtractedTextRequest, ExtractedText)}.
5199     */
5200    public void setExtractedText(ExtractedText text) {
5201        Editable content = getEditableText();
5202        if (text.text != null) {
5203            if (content == null) {
5204                setText(text.text, TextView.BufferType.EDITABLE);
5205            } else if (text.partialStartOffset < 0) {
5206                removeParcelableSpans(content, 0, content.length());
5207                content.replace(0, content.length(), text.text);
5208            } else {
5209                final int N = content.length();
5210                int start = text.partialStartOffset;
5211                if (start > N) start = N;
5212                int end = text.partialEndOffset;
5213                if (end > N) end = N;
5214                removeParcelableSpans(content, start, end);
5215                content.replace(start, end, text.text);
5216            }
5217        }
5218
5219        // Now set the selection position...  make sure it is in range, to
5220        // avoid crashes.  If this is a partial update, it is possible that
5221        // the underlying text may have changed, causing us problems here.
5222        // Also we just don't want to trust clients to do the right thing.
5223        Spannable sp = (Spannable)getText();
5224        final int N = sp.length();
5225        int start = text.selectionStart;
5226        if (start < 0) start = 0;
5227        else if (start > N) start = N;
5228        int end = text.selectionEnd;
5229        if (end < 0) end = 0;
5230        else if (end > N) end = N;
5231        Selection.setSelection(sp, start, end);
5232
5233        // Finally, update the selection mode.
5234        if ((text.flags&ExtractedText.FLAG_SELECTING) != 0) {
5235            MetaKeyKeyListener.startSelecting(this, sp);
5236        } else {
5237            MetaKeyKeyListener.stopSelecting(this, sp);
5238        }
5239    }
5240
5241    /**
5242     * @hide
5243     */
5244    public void setExtracting(ExtractedTextRequest req) {
5245        if (mInputMethodState != null) {
5246            mInputMethodState.mExtracting = req;
5247        }
5248        // This stops a possible text selection mode. Maybe not intended.
5249        hideControllers();
5250    }
5251
5252    /**
5253     * Called by the framework in response to a text completion from
5254     * the current input method, provided by it calling
5255     * {@link InputConnection#commitCompletion
5256     * InputConnection.commitCompletion()}.  The default implementation does
5257     * nothing; text views that are supporting auto-completion should override
5258     * this to do their desired behavior.
5259     *
5260     * @param text The auto complete text the user has selected.
5261     */
5262    public void onCommitCompletion(CompletionInfo text) {
5263        // intentionally empty
5264    }
5265
5266    /**
5267     * Called by the framework in response to a text auto-correction (such as fixing a typo using a
5268     * a dictionnary) from the current input method, provided by it calling
5269     * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
5270     * implementation flashes the background of the corrected word to provide feedback to the user.
5271     *
5272     * @param info The auto correct info about the text that was corrected.
5273     */
5274    public void onCommitCorrection(CorrectionInfo info) {
5275        if (mCorrectionHighlighter == null) {
5276            mCorrectionHighlighter = new CorrectionHighlighter();
5277        } else {
5278            mCorrectionHighlighter.invalidate(false);
5279        }
5280
5281        mCorrectionHighlighter.highlight(info);
5282    }
5283
5284    private class CorrectionHighlighter {
5285        private final Path mPath = new Path();
5286        private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
5287        private int mStart, mEnd;
5288        private long mFadingStartTime;
5289        private final static int FADE_OUT_DURATION = 400;
5290
5291        public CorrectionHighlighter() {
5292            mPaint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale);
5293            mPaint.setStyle(Paint.Style.FILL);
5294        }
5295
5296        public void highlight(CorrectionInfo info) {
5297            mStart = info.getOffset();
5298            mEnd = mStart + info.getNewText().length();
5299            mFadingStartTime = SystemClock.uptimeMillis();
5300
5301            if (mStart < 0 || mEnd < 0) {
5302                stopAnimation();
5303            }
5304        }
5305
5306        public void draw(Canvas canvas, int cursorOffsetVertical) {
5307            if (updatePath() && updatePaint()) {
5308                if (cursorOffsetVertical != 0) {
5309                    canvas.translate(0, cursorOffsetVertical);
5310                }
5311
5312                canvas.drawPath(mPath, mPaint);
5313
5314                if (cursorOffsetVertical != 0) {
5315                    canvas.translate(0, -cursorOffsetVertical);
5316                }
5317                invalidate(true);
5318            } else {
5319                stopAnimation();
5320                invalidate(false);
5321            }
5322        }
5323
5324        private boolean updatePaint() {
5325            final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
5326            if (duration > FADE_OUT_DURATION) return false;
5327
5328            final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
5329            final int highlightColorAlpha = Color.alpha(mHighlightColor);
5330            final int color = (mHighlightColor & 0x00FFFFFF) +
5331                    ((int) (highlightColorAlpha * coef) << 24);
5332            mPaint.setColor(color);
5333            return true;
5334        }
5335
5336        private boolean updatePath() {
5337            final Layout layout = TextView.this.mLayout;
5338            if (layout == null) return false;
5339
5340            // Update in case text is edited while the animation is run
5341            final int length = mText.length();
5342            int start = Math.min(length, mStart);
5343            int end = Math.min(length, mEnd);
5344
5345            mPath.reset();
5346            TextView.this.mLayout.getSelectionPath(start, end, mPath);
5347            return true;
5348        }
5349
5350        private void invalidate(boolean delayed) {
5351            if (TextView.this.mLayout == null) return;
5352
5353            synchronized (sTempRect) {
5354                mPath.computeBounds(sTempRect, false);
5355
5356                int left = getCompoundPaddingLeft();
5357                int top = getExtendedPaddingTop() + getVerticalOffset(true);
5358
5359                if (delayed) {
5360                    TextView.this.postInvalidateDelayed(16, // 60 Hz update
5361                            left + (int) sTempRect.left, top + (int) sTempRect.top,
5362                            left + (int) sTempRect.right, top + (int) sTempRect.bottom);
5363                } else {
5364                    TextView.this.postInvalidate((int) sTempRect.left, (int) sTempRect.top,
5365                            (int) sTempRect.right, (int) sTempRect.bottom);
5366                }
5367            }
5368        }
5369
5370        private void stopAnimation() {
5371            TextView.this.mCorrectionHighlighter = null;
5372        }
5373    }
5374
5375    public void beginBatchEdit() {
5376        mInBatchEditControllers = true;
5377        final InputMethodState ims = mInputMethodState;
5378        if (ims != null) {
5379            int nesting = ++ims.mBatchEditNesting;
5380            if (nesting == 1) {
5381                ims.mCursorChanged = false;
5382                ims.mChangedDelta = 0;
5383                if (ims.mContentChanged) {
5384                    // We already have a pending change from somewhere else,
5385                    // so turn this into a full update.
5386                    ims.mChangedStart = 0;
5387                    ims.mChangedEnd = mText.length();
5388                } else {
5389                    ims.mChangedStart = EXTRACT_UNKNOWN;
5390                    ims.mChangedEnd = EXTRACT_UNKNOWN;
5391                    ims.mContentChanged = false;
5392                }
5393                onBeginBatchEdit();
5394            }
5395        }
5396    }
5397
5398    public void endBatchEdit() {
5399        mInBatchEditControllers = false;
5400        final InputMethodState ims = mInputMethodState;
5401        if (ims != null) {
5402            int nesting = --ims.mBatchEditNesting;
5403            if (nesting == 0) {
5404                finishBatchEdit(ims);
5405            }
5406        }
5407    }
5408
5409    void ensureEndedBatchEdit() {
5410        final InputMethodState ims = mInputMethodState;
5411        if (ims != null && ims.mBatchEditNesting != 0) {
5412            ims.mBatchEditNesting = 0;
5413            finishBatchEdit(ims);
5414        }
5415    }
5416
5417    void finishBatchEdit(final InputMethodState ims) {
5418        onEndBatchEdit();
5419
5420        if (ims.mContentChanged || ims.mSelectionModeChanged) {
5421            updateAfterEdit();
5422            reportExtractedText();
5423        } else if (ims.mCursorChanged) {
5424            // Cheezy way to get us to report the current cursor location.
5425            invalidateCursor();
5426        }
5427    }
5428
5429    void updateAfterEdit() {
5430        invalidate();
5431        int curs = getSelectionStart();
5432
5433        if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
5434            registerForPreDraw();
5435        }
5436
5437        if (curs >= 0) {
5438            mHighlightPathBogus = true;
5439            makeBlink();
5440        }
5441
5442        checkForResize();
5443    }
5444
5445    /**
5446     * Called by the framework in response to a request to begin a batch
5447     * of edit operations through a call to link {@link #beginBatchEdit()}.
5448     */
5449    public void onBeginBatchEdit() {
5450        // intentionally empty
5451    }
5452
5453    /**
5454     * Called by the framework in response to a request to end a batch
5455     * of edit operations through a call to link {@link #endBatchEdit}.
5456     */
5457    public void onEndBatchEdit() {
5458        // intentionally empty
5459    }
5460
5461    /**
5462     * Called by the framework in response to a private command from the
5463     * current method, provided by it calling
5464     * {@link InputConnection#performPrivateCommand
5465     * InputConnection.performPrivateCommand()}.
5466     *
5467     * @param action The action name of the command.
5468     * @param data Any additional data for the command.  This may be null.
5469     * @return Return true if you handled the command, else false.
5470     */
5471    public boolean onPrivateIMECommand(String action, Bundle data) {
5472        return false;
5473    }
5474
5475    private void nullLayouts() {
5476        if (mLayout instanceof BoringLayout && mSavedLayout == null) {
5477            mSavedLayout = (BoringLayout) mLayout;
5478        }
5479        if (mHintLayout instanceof BoringLayout && mSavedHintLayout == null) {
5480            mSavedHintLayout = (BoringLayout) mHintLayout;
5481        }
5482
5483        mLayout = mHintLayout = null;
5484
5485        // Since it depends on the value of mLayout
5486        prepareCursorControllers();
5487    }
5488
5489    /**
5490     * Make a new Layout based on the already-measured size of the view,
5491     * on the assumption that it was measured correctly at some point.
5492     */
5493    private void assumeLayout() {
5494        int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
5495
5496        if (width < 1) {
5497            width = 0;
5498        }
5499
5500        int physicalWidth = width;
5501
5502        if (mHorizontallyScrolling) {
5503            width = VERY_WIDE;
5504        }
5505
5506        makeNewLayout(width, physicalWidth, UNKNOWN_BORING, UNKNOWN_BORING,
5507                      physicalWidth, false);
5508    }
5509
5510    /**
5511     * The width passed in is now the desired layout width,
5512     * not the full view width with padding.
5513     * {@hide}
5514     */
5515    protected void makeNewLayout(int w, int hintWidth,
5516                                 BoringLayout.Metrics boring,
5517                                 BoringLayout.Metrics hintBoring,
5518                                 int ellipsisWidth, boolean bringIntoView) {
5519        stopMarquee();
5520
5521        mHighlightPathBogus = true;
5522
5523        if (w < 0) {
5524            w = 0;
5525        }
5526        if (hintWidth < 0) {
5527            hintWidth = 0;
5528        }
5529
5530        Layout.Alignment alignment;
5531        switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
5532            case Gravity.CENTER_HORIZONTAL:
5533                alignment = Layout.Alignment.ALIGN_CENTER;
5534                break;
5535
5536            case Gravity.RIGHT:
5537                // Note, Layout resolves ALIGN_OPPOSITE to left or
5538                // right based on the paragraph direction.
5539                alignment = Layout.Alignment.ALIGN_OPPOSITE;
5540                break;
5541
5542            default:
5543                alignment = Layout.Alignment.ALIGN_NORMAL;
5544        }
5545
5546        boolean shouldEllipsize = mEllipsize != null && mInput == null;
5547
5548        if (mText instanceof Spannable) {
5549            mLayout = new DynamicLayout(mText, mTransformed, mTextPaint, w,
5550                    alignment, mSpacingMult,
5551                    mSpacingAdd, mIncludePad, mInput == null ? mEllipsize : null,
5552                    ellipsisWidth);
5553        } else {
5554            if (boring == UNKNOWN_BORING) {
5555                boring = BoringLayout.isBoring(mTransformed, mTextPaint, mBoring);
5556                if (boring != null) {
5557                    mBoring = boring;
5558                }
5559            }
5560
5561            if (boring != null) {
5562                if (boring.width <= w &&
5563                    (mEllipsize == null || boring.width <= ellipsisWidth)) {
5564                    if (mSavedLayout != null) {
5565                        mLayout = mSavedLayout.
5566                                replaceOrMake(mTransformed, mTextPaint,
5567                                w, alignment, mSpacingMult, mSpacingAdd,
5568                                boring, mIncludePad);
5569                    } else {
5570                        mLayout = BoringLayout.make(mTransformed, mTextPaint,
5571                                w, alignment, mSpacingMult, mSpacingAdd,
5572                                boring, mIncludePad);
5573                    }
5574
5575                    mSavedLayout = (BoringLayout) mLayout;
5576                } else if (shouldEllipsize && boring.width <= w) {
5577                    if (mSavedLayout != null) {
5578                        mLayout = mSavedLayout.
5579                                replaceOrMake(mTransformed, mTextPaint,
5580                                w, alignment, mSpacingMult, mSpacingAdd,
5581                                boring, mIncludePad, mEllipsize,
5582                                ellipsisWidth);
5583                    } else {
5584                        mLayout = BoringLayout.make(mTransformed, mTextPaint,
5585                                w, alignment, mSpacingMult, mSpacingAdd,
5586                                boring, mIncludePad, mEllipsize,
5587                                ellipsisWidth);
5588                    }
5589                } else if (shouldEllipsize) {
5590                    mLayout = new StaticLayout(mTransformed,
5591                                0, mTransformed.length(),
5592                                mTextPaint, w, alignment, mSpacingMult,
5593                                mSpacingAdd, mIncludePad, mEllipsize,
5594                                ellipsisWidth);
5595                } else {
5596                    mLayout = new StaticLayout(mTransformed, mTextPaint,
5597                            w, alignment, mSpacingMult, mSpacingAdd,
5598                            mIncludePad);
5599                }
5600            } else if (shouldEllipsize) {
5601                mLayout = new StaticLayout(mTransformed,
5602                            0, mTransformed.length(),
5603                            mTextPaint, w, alignment, mSpacingMult,
5604                            mSpacingAdd, mIncludePad, mEllipsize,
5605                            ellipsisWidth);
5606            } else {
5607                mLayout = new StaticLayout(mTransformed, mTextPaint,
5608                        w, alignment, mSpacingMult, mSpacingAdd,
5609                        mIncludePad);
5610            }
5611        }
5612
5613        shouldEllipsize = mEllipsize != null;
5614        mHintLayout = null;
5615
5616        if (mHint != null) {
5617            if (shouldEllipsize) hintWidth = w;
5618
5619            if (hintBoring == UNKNOWN_BORING) {
5620                hintBoring = BoringLayout.isBoring(mHint, mTextPaint,
5621                                                   mHintBoring);
5622                if (hintBoring != null) {
5623                    mHintBoring = hintBoring;
5624                }
5625            }
5626
5627            if (hintBoring != null) {
5628                if (hintBoring.width <= hintWidth &&
5629                    (!shouldEllipsize || hintBoring.width <= ellipsisWidth)) {
5630                    if (mSavedHintLayout != null) {
5631                        mHintLayout = mSavedHintLayout.
5632                                replaceOrMake(mHint, mTextPaint,
5633                                hintWidth, alignment, mSpacingMult, mSpacingAdd,
5634                                hintBoring, mIncludePad);
5635                    } else {
5636                        mHintLayout = BoringLayout.make(mHint, mTextPaint,
5637                                hintWidth, alignment, mSpacingMult, mSpacingAdd,
5638                                hintBoring, mIncludePad);
5639                    }
5640
5641                    mSavedHintLayout = (BoringLayout) mHintLayout;
5642                } else if (shouldEllipsize && hintBoring.width <= hintWidth) {
5643                    if (mSavedHintLayout != null) {
5644                        mHintLayout = mSavedHintLayout.
5645                                replaceOrMake(mHint, mTextPaint,
5646                                hintWidth, alignment, mSpacingMult, mSpacingAdd,
5647                                hintBoring, mIncludePad, mEllipsize,
5648                                ellipsisWidth);
5649                    } else {
5650                        mHintLayout = BoringLayout.make(mHint, mTextPaint,
5651                                hintWidth, alignment, mSpacingMult, mSpacingAdd,
5652                                hintBoring, mIncludePad, mEllipsize,
5653                                ellipsisWidth);
5654                    }
5655                } else if (shouldEllipsize) {
5656                    mHintLayout = new StaticLayout(mHint,
5657                                0, mHint.length(),
5658                                mTextPaint, hintWidth, alignment, mSpacingMult,
5659                                mSpacingAdd, mIncludePad, mEllipsize,
5660                                ellipsisWidth);
5661                } else {
5662                    mHintLayout = new StaticLayout(mHint, mTextPaint,
5663                            hintWidth, alignment, mSpacingMult, mSpacingAdd,
5664                            mIncludePad);
5665                }
5666            } else if (shouldEllipsize) {
5667                mHintLayout = new StaticLayout(mHint,
5668                            0, mHint.length(),
5669                            mTextPaint, hintWidth, alignment, mSpacingMult,
5670                            mSpacingAdd, mIncludePad, mEllipsize,
5671                            ellipsisWidth);
5672            } else {
5673                mHintLayout = new StaticLayout(mHint, mTextPaint,
5674                        hintWidth, alignment, mSpacingMult, mSpacingAdd,
5675                        mIncludePad);
5676            }
5677        }
5678
5679        if (bringIntoView) {
5680            registerForPreDraw();
5681        }
5682
5683        if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
5684            if (!compressText(ellipsisWidth)) {
5685                final int height = mLayoutParams.height;
5686                // If the size of the view does not depend on the size of the text, try to
5687                // start the marquee immediately
5688                if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT) {
5689                    startMarquee();
5690                } else {
5691                    // Defer the start of the marquee until we know our width (see setFrame())
5692                    mRestartMarquee = true;
5693                }
5694            }
5695        }
5696
5697        // CursorControllers need a non-null mLayout
5698        prepareCursorControllers();
5699    }
5700
5701    private boolean compressText(float width) {
5702        if (isHardwareAccelerated()) return false;
5703
5704        // Only compress the text if it hasn't been compressed by the previous pass
5705        if (width > 0.0f && mLayout != null && getLineCount() == 1 && !mUserSetTextScaleX &&
5706                mTextPaint.getTextScaleX() == 1.0f) {
5707            final float textWidth = mLayout.getLineWidth(0);
5708            final float overflow = (textWidth + 1.0f - width) / width;
5709            if (overflow > 0.0f && overflow <= Marquee.MARQUEE_DELTA_MAX) {
5710                mTextPaint.setTextScaleX(1.0f - overflow - 0.005f);
5711                post(new Runnable() {
5712                    public void run() {
5713                        requestLayout();
5714                    }
5715                });
5716                return true;
5717            }
5718        }
5719
5720        return false;
5721    }
5722
5723    private static int desired(Layout layout) {
5724        int n = layout.getLineCount();
5725        CharSequence text = layout.getText();
5726        float max = 0;
5727
5728        // if any line was wrapped, we can't use it.
5729        // but it's ok for the last line not to have a newline
5730
5731        for (int i = 0; i < n - 1; i++) {
5732            if (text.charAt(layout.getLineEnd(i) - 1) != '\n')
5733                return -1;
5734        }
5735
5736        for (int i = 0; i < n; i++) {
5737            max = Math.max(max, layout.getLineWidth(i));
5738        }
5739
5740        return (int) FloatMath.ceil(max);
5741    }
5742
5743    /**
5744     * Set whether the TextView includes extra top and bottom padding to make
5745     * room for accents that go above the normal ascent and descent.
5746     * The default is true.
5747     *
5748     * @attr ref android.R.styleable#TextView_includeFontPadding
5749     */
5750    public void setIncludeFontPadding(boolean includepad) {
5751        mIncludePad = includepad;
5752
5753        if (mLayout != null) {
5754            nullLayouts();
5755            requestLayout();
5756            invalidate();
5757        }
5758    }
5759
5760    private static final BoringLayout.Metrics UNKNOWN_BORING = new BoringLayout.Metrics();
5761
5762    @Override
5763    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
5764        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
5765        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
5766        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
5767        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
5768
5769        int width;
5770        int height;
5771
5772        BoringLayout.Metrics boring = UNKNOWN_BORING;
5773        BoringLayout.Metrics hintBoring = UNKNOWN_BORING;
5774
5775        int des = -1;
5776        boolean fromexisting = false;
5777
5778        if (widthMode == MeasureSpec.EXACTLY) {
5779            // Parent has told us how big to be. So be it.
5780            width = widthSize;
5781        } else {
5782            if (mLayout != null && mEllipsize == null) {
5783                des = desired(mLayout);
5784            }
5785
5786            if (des < 0) {
5787                boring = BoringLayout.isBoring(mTransformed, mTextPaint, mBoring);
5788                if (boring != null) {
5789                    mBoring = boring;
5790                }
5791            } else {
5792                fromexisting = true;
5793            }
5794
5795            if (boring == null || boring == UNKNOWN_BORING) {
5796                if (des < 0) {
5797                    des = (int) FloatMath.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint));
5798                }
5799
5800                width = des;
5801            } else {
5802                width = boring.width;
5803            }
5804
5805            final Drawables dr = mDrawables;
5806            if (dr != null) {
5807                width = Math.max(width, dr.mDrawableWidthTop);
5808                width = Math.max(width, dr.mDrawableWidthBottom);
5809            }
5810
5811            if (mHint != null) {
5812                int hintDes = -1;
5813                int hintWidth;
5814
5815                if (mHintLayout != null && mEllipsize == null) {
5816                    hintDes = desired(mHintLayout);
5817                }
5818
5819                if (hintDes < 0) {
5820                    hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mHintBoring);
5821                    if (hintBoring != null) {
5822                        mHintBoring = hintBoring;
5823                    }
5824                }
5825
5826                if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
5827                    if (hintDes < 0) {
5828                        hintDes = (int) FloatMath.ceil(
5829                                Layout.getDesiredWidth(mHint, mTextPaint));
5830                    }
5831
5832                    hintWidth = hintDes;
5833                } else {
5834                    hintWidth = hintBoring.width;
5835                }
5836
5837                if (hintWidth > width) {
5838                    width = hintWidth;
5839                }
5840            }
5841
5842            width += getCompoundPaddingLeft() + getCompoundPaddingRight();
5843
5844            if (mMaxWidthMode == EMS) {
5845                width = Math.min(width, mMaxWidth * getLineHeight());
5846            } else {
5847                width = Math.min(width, mMaxWidth);
5848            }
5849
5850            if (mMinWidthMode == EMS) {
5851                width = Math.max(width, mMinWidth * getLineHeight());
5852            } else {
5853                width = Math.max(width, mMinWidth);
5854            }
5855
5856            // Check against our minimum width
5857            width = Math.max(width, getSuggestedMinimumWidth());
5858
5859            if (widthMode == MeasureSpec.AT_MOST) {
5860                width = Math.min(widthSize, width);
5861            }
5862        }
5863
5864        int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
5865        int unpaddedWidth = want;
5866
5867        if (mHorizontallyScrolling) want = VERY_WIDE;
5868
5869        int hintWant = want;
5870        int hintWidth = mHintLayout == null ? hintWant : mHintLayout.getWidth();
5871
5872        if (mLayout == null) {
5873            makeNewLayout(want, hintWant, boring, hintBoring,
5874                          width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
5875        } else if ((mLayout.getWidth() != want) || (hintWidth != hintWant) ||
5876                   (mLayout.getEllipsizedWidth() !=
5877                        width - getCompoundPaddingLeft() - getCompoundPaddingRight())) {
5878            if (mHint == null && mEllipsize == null &&
5879                    want > mLayout.getWidth() &&
5880                    (mLayout instanceof BoringLayout ||
5881                            (fromexisting && des >= 0 && des <= want))) {
5882                mLayout.increaseWidthTo(want);
5883            } else {
5884                makeNewLayout(want, hintWant, boring, hintBoring,
5885                              width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
5886            }
5887        } else {
5888            // Width has not changed.
5889        }
5890
5891        if (heightMode == MeasureSpec.EXACTLY) {
5892            // Parent has told us how big to be. So be it.
5893            height = heightSize;
5894            mDesiredHeightAtMeasure = -1;
5895        } else {
5896            int desired = getDesiredHeight();
5897
5898            height = desired;
5899            mDesiredHeightAtMeasure = desired;
5900
5901            if (heightMode == MeasureSpec.AT_MOST) {
5902                height = Math.min(desired, heightSize);
5903            }
5904        }
5905
5906        int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
5907        if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
5908            unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
5909        }
5910
5911        /*
5912         * We didn't let makeNewLayout() register to bring the cursor into view,
5913         * so do it here if there is any possibility that it is needed.
5914         */
5915        if (mMovement != null ||
5916            mLayout.getWidth() > unpaddedWidth ||
5917            mLayout.getHeight() > unpaddedHeight) {
5918            registerForPreDraw();
5919        } else {
5920            scrollTo(0, 0);
5921        }
5922
5923        setMeasuredDimension(width, height);
5924    }
5925
5926    private int getDesiredHeight() {
5927        return Math.max(
5928                getDesiredHeight(mLayout, true),
5929                getDesiredHeight(mHintLayout, mEllipsize != null));
5930    }
5931
5932    private int getDesiredHeight(Layout layout, boolean cap) {
5933        if (layout == null) {
5934            return 0;
5935        }
5936
5937        int linecount = layout.getLineCount();
5938        int pad = getCompoundPaddingTop() + getCompoundPaddingBottom();
5939        int desired = layout.getLineTop(linecount);
5940
5941        final Drawables dr = mDrawables;
5942        if (dr != null) {
5943            desired = Math.max(desired, dr.mDrawableHeightLeft);
5944            desired = Math.max(desired, dr.mDrawableHeightRight);
5945        }
5946
5947        desired += pad;
5948        layout.setMaximumVisibleLineCount(0);
5949
5950        if (mMaxMode == LINES) {
5951            /*
5952             * Don't cap the hint to a certain number of lines.
5953             * (Do cap it, though, if we have a maximum pixel height.)
5954             */
5955            if (cap) {
5956                if (linecount > mMaximum) {
5957                    layout.setMaximumVisibleLineCount(mMaximum);
5958                    desired = layout.getLineTop(mMaximum);
5959
5960                    if (dr != null) {
5961                        desired = Math.max(desired, dr.mDrawableHeightLeft);
5962                        desired = Math.max(desired, dr.mDrawableHeightRight);
5963                    }
5964
5965                    desired += pad;
5966                    linecount = mMaximum;
5967                }
5968            }
5969        } else {
5970            desired = Math.min(desired, mMaximum);
5971        }
5972
5973        if (mMinMode == LINES) {
5974            if (linecount < mMinimum) {
5975                desired += getLineHeight() * (mMinimum - linecount);
5976            }
5977        } else {
5978            desired = Math.max(desired, mMinimum);
5979        }
5980
5981        // Check against our minimum height
5982        desired = Math.max(desired, getSuggestedMinimumHeight());
5983
5984        return desired;
5985    }
5986
5987    /**
5988     * Check whether a change to the existing text layout requires a
5989     * new view layout.
5990     */
5991    private void checkForResize() {
5992        boolean sizeChanged = false;
5993
5994        if (mLayout != null) {
5995            // Check if our width changed
5996            if (mLayoutParams.width == LayoutParams.WRAP_CONTENT) {
5997                sizeChanged = true;
5998                invalidate();
5999            }
6000
6001            // Check if our height changed
6002            if (mLayoutParams.height == LayoutParams.WRAP_CONTENT) {
6003                int desiredHeight = getDesiredHeight();
6004
6005                if (desiredHeight != this.getHeight()) {
6006                    sizeChanged = true;
6007                }
6008            } else if (mLayoutParams.height == LayoutParams.MATCH_PARENT) {
6009                if (mDesiredHeightAtMeasure >= 0) {
6010                    int desiredHeight = getDesiredHeight();
6011
6012                    if (desiredHeight != mDesiredHeightAtMeasure) {
6013                        sizeChanged = true;
6014                    }
6015                }
6016            }
6017        }
6018
6019        if (sizeChanged) {
6020            requestLayout();
6021            // caller will have already invalidated
6022        }
6023    }
6024
6025    /**
6026     * Check whether entirely new text requires a new view layout
6027     * or merely a new text layout.
6028     */
6029    private void checkForRelayout() {
6030        // If we have a fixed width, we can just swap in a new text layout
6031        // if the text height stays the same or if the view height is fixed.
6032
6033        if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT ||
6034                (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth)) &&
6035                (mHint == null || mHintLayout != null) &&
6036                (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
6037            // Static width, so try making a new text layout.
6038
6039            int oldht = mLayout.getHeight();
6040            int want = mLayout.getWidth();
6041            int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
6042
6043            /*
6044             * No need to bring the text into view, since the size is not
6045             * changing (unless we do the requestLayout(), in which case it
6046             * will happen at measure).
6047             */
6048            makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
6049                          mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
6050                          false);
6051
6052            if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
6053                // In a fixed-height view, so use our new text layout.
6054                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT &&
6055                    mLayoutParams.height != LayoutParams.MATCH_PARENT) {
6056                    invalidate();
6057                    return;
6058                }
6059
6060                // Dynamic height, but height has stayed the same,
6061                // so use our new text layout.
6062                if (mLayout.getHeight() == oldht &&
6063                    (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
6064                    invalidate();
6065                    return;
6066                }
6067            }
6068
6069            // We lose: the height has changed and we have a dynamic height.
6070            // Request a new view layout using our new text layout.
6071            requestLayout();
6072            invalidate();
6073        } else {
6074            // Dynamic width, so we have no choice but to request a new
6075            // view layout with a new text layout.
6076
6077            nullLayouts();
6078            requestLayout();
6079            invalidate();
6080        }
6081    }
6082
6083    /**
6084     * Returns true if anything changed.
6085     */
6086    private boolean bringTextIntoView() {
6087        int line = 0;
6088        if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
6089            line = mLayout.getLineCount() - 1;
6090        }
6091
6092        Layout.Alignment a = mLayout.getParagraphAlignment(line);
6093        int dir = mLayout.getParagraphDirection(line);
6094        int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
6095        int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
6096        int ht = mLayout.getHeight();
6097
6098        int scrollx, scrolly;
6099
6100        if (a == Layout.Alignment.ALIGN_CENTER) {
6101            /*
6102             * Keep centered if possible, or, if it is too wide to fit,
6103             * keep leading edge in view.
6104             */
6105
6106            int left = (int) FloatMath.floor(mLayout.getLineLeft(line));
6107            int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6108
6109            if (right - left < hspace) {
6110                scrollx = (right + left) / 2 - hspace / 2;
6111            } else {
6112                if (dir < 0) {
6113                    scrollx = right - hspace;
6114                } else {
6115                    scrollx = left;
6116                }
6117            }
6118        } else if (a == Layout.Alignment.ALIGN_NORMAL) {
6119            /*
6120             * Keep leading edge in view.
6121             */
6122
6123            if (dir < 0) {
6124                int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6125                scrollx = right - hspace;
6126            } else {
6127                scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line));
6128            }
6129        } else /* a == Layout.Alignment.ALIGN_OPPOSITE */ {
6130            /*
6131             * Keep trailing edge in view.
6132             */
6133
6134            if (dir < 0) {
6135                scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line));
6136            } else {
6137                int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6138                scrollx = right - hspace;
6139            }
6140        }
6141
6142        if (ht < vspace) {
6143            scrolly = 0;
6144        } else {
6145            if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
6146                scrolly = ht - vspace;
6147            } else {
6148                scrolly = 0;
6149            }
6150        }
6151
6152        if (scrollx != mScrollX || scrolly != mScrollY) {
6153            scrollTo(scrollx, scrolly);
6154            return true;
6155        } else {
6156            return false;
6157        }
6158    }
6159
6160    /**
6161     * Move the point, specified by the offset, into the view if it is needed.
6162     * This has to be called after layout. Returns true if anything changed.
6163     */
6164    public boolean bringPointIntoView(int offset) {
6165        boolean changed = false;
6166
6167        int line = mLayout.getLineForOffset(offset);
6168
6169        // FIXME: Is it okay to truncate this, or should we round?
6170        final int x = (int)mLayout.getPrimaryHorizontal(offset);
6171        final int top = mLayout.getLineTop(line);
6172        final int bottom = mLayout.getLineTop(line + 1);
6173
6174        int left = (int) FloatMath.floor(mLayout.getLineLeft(line));
6175        int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
6176        int ht = mLayout.getHeight();
6177
6178        int grav;
6179
6180        switch (mLayout.getParagraphAlignment(line)) {
6181            case ALIGN_NORMAL:
6182                grav = 1;
6183                break;
6184
6185            case ALIGN_OPPOSITE:
6186                grav = -1;
6187                break;
6188
6189            default:
6190                grav = 0;
6191        }
6192
6193        grav *= mLayout.getParagraphDirection(line);
6194
6195        int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
6196        int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
6197
6198        int hslack = (bottom - top) / 2;
6199        int vslack = hslack;
6200
6201        if (vslack > vspace / 4)
6202            vslack = vspace / 4;
6203        if (hslack > hspace / 4)
6204            hslack = hspace / 4;
6205
6206        int hs = mScrollX;
6207        int vs = mScrollY;
6208
6209        if (top - vs < vslack)
6210            vs = top - vslack;
6211        if (bottom - vs > vspace - vslack)
6212            vs = bottom - (vspace - vslack);
6213        if (ht - vs < vspace)
6214            vs = ht - vspace;
6215        if (0 - vs > 0)
6216            vs = 0;
6217
6218        if (grav != 0) {
6219            if (x - hs < hslack) {
6220                hs = x - hslack;
6221            }
6222            if (x - hs > hspace - hslack) {
6223                hs = x - (hspace - hslack);
6224            }
6225        }
6226
6227        if (grav < 0) {
6228            if (left - hs > 0)
6229                hs = left;
6230            if (right - hs < hspace)
6231                hs = right - hspace;
6232        } else if (grav > 0) {
6233            if (right - hs < hspace)
6234                hs = right - hspace;
6235            if (left - hs > 0)
6236                hs = left;
6237        } else /* grav == 0 */ {
6238            if (right - left <= hspace) {
6239                /*
6240                 * If the entire text fits, center it exactly.
6241                 */
6242                hs = left - (hspace - (right - left)) / 2;
6243            } else if (x > right - hslack) {
6244                /*
6245                 * If we are near the right edge, keep the right edge
6246                 * at the edge of the view.
6247                 */
6248                hs = right - hspace;
6249            } else if (x < left + hslack) {
6250                /*
6251                 * If we are near the left edge, keep the left edge
6252                 * at the edge of the view.
6253                 */
6254                hs = left;
6255            } else if (left > hs) {
6256                /*
6257                 * Is there whitespace visible at the left?  Fix it if so.
6258                 */
6259                hs = left;
6260            } else if (right < hs + hspace) {
6261                /*
6262                 * Is there whitespace visible at the right?  Fix it if so.
6263                 */
6264                hs = right - hspace;
6265            } else {
6266                /*
6267                 * Otherwise, float as needed.
6268                 */
6269                if (x - hs < hslack) {
6270                    hs = x - hslack;
6271                }
6272                if (x - hs > hspace - hslack) {
6273                    hs = x - (hspace - hslack);
6274                }
6275            }
6276        }
6277
6278        if (hs != mScrollX || vs != mScrollY) {
6279            if (mScroller == null) {
6280                scrollTo(hs, vs);
6281            } else {
6282                long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
6283                int dx = hs - mScrollX;
6284                int dy = vs - mScrollY;
6285
6286                if (duration > ANIMATED_SCROLL_GAP) {
6287                    mScroller.startScroll(mScrollX, mScrollY, dx, dy);
6288                    awakenScrollBars(mScroller.getDuration());
6289                    invalidate();
6290                } else {
6291                    if (!mScroller.isFinished()) {
6292                        mScroller.abortAnimation();
6293                    }
6294
6295                    scrollBy(dx, dy);
6296                }
6297
6298                mLastScroll = AnimationUtils.currentAnimationTimeMillis();
6299            }
6300
6301            changed = true;
6302        }
6303
6304        if (isFocused()) {
6305            // This offsets because getInterestingRect() is in terms of viewport coordinates, but
6306            // requestRectangleOnScreen() is in terms of content coordinates.
6307
6308            if (mTempRect == null) mTempRect = new Rect();
6309            mTempRect.set(x, top, x + 1, bottom);
6310            getInterestingRect(mTempRect, line);
6311            mTempRect.offset(mScrollX, mScrollY);
6312
6313            if (requestRectangleOnScreen(mTempRect)) {
6314                changed = true;
6315            }
6316        }
6317
6318        return changed;
6319    }
6320
6321    /**
6322     * Move the cursor, if needed, so that it is at an offset that is visible
6323     * to the user.  This will not move the cursor if it represents more than
6324     * one character (a selection range).  This will only work if the
6325     * TextView contains spannable text; otherwise it will do nothing.
6326     *
6327     * @return True if the cursor was actually moved, false otherwise.
6328     */
6329    public boolean moveCursorToVisibleOffset() {
6330        if (!(mText instanceof Spannable)) {
6331            return false;
6332        }
6333        int start = getSelectionStart();
6334        int end = getSelectionEnd();
6335        if (start != end) {
6336            return false;
6337        }
6338
6339        // First: make sure the line is visible on screen:
6340
6341        int line = mLayout.getLineForOffset(start);
6342
6343        final int top = mLayout.getLineTop(line);
6344        final int bottom = mLayout.getLineTop(line + 1);
6345        final int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
6346        int vslack = (bottom - top) / 2;
6347        if (vslack > vspace / 4)
6348            vslack = vspace / 4;
6349        final int vs = mScrollY;
6350
6351        if (top < (vs+vslack)) {
6352            line = mLayout.getLineForVertical(vs+vslack+(bottom-top));
6353        } else if (bottom > (vspace+vs-vslack)) {
6354            line = mLayout.getLineForVertical(vspace+vs-vslack-(bottom-top));
6355        }
6356
6357        // Next: make sure the character is visible on screen:
6358
6359        final int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
6360        final int hs = mScrollX;
6361        final int leftChar = mLayout.getOffsetForHorizontal(line, hs);
6362        final int rightChar = mLayout.getOffsetForHorizontal(line, hspace+hs);
6363
6364        // line might contain bidirectional text
6365        final int lowChar = leftChar < rightChar ? leftChar : rightChar;
6366        final int highChar = leftChar > rightChar ? leftChar : rightChar;
6367
6368        int newStart = start;
6369        if (newStart < lowChar) {
6370            newStart = lowChar;
6371        } else if (newStart > highChar) {
6372            newStart = highChar;
6373        }
6374
6375        if (newStart != start) {
6376            Selection.setSelection((Spannable)mText, newStart);
6377            return true;
6378        }
6379
6380        return false;
6381    }
6382
6383    @Override
6384    public void computeScroll() {
6385        if (mScroller != null) {
6386            if (mScroller.computeScrollOffset()) {
6387                mScrollX = mScroller.getCurrX();
6388                mScrollY = mScroller.getCurrY();
6389                invalidateParentCaches();
6390                postInvalidate();  // So we draw again
6391            }
6392        }
6393    }
6394
6395    private void getInterestingRect(Rect r, int line) {
6396        convertFromViewportToContentCoordinates(r);
6397
6398        // Rectangle can can be expanded on first and last line to take
6399        // padding into account.
6400        // TODO Take left/right padding into account too?
6401        if (line == 0) r.top -= getExtendedPaddingTop();
6402        if (line == mLayout.getLineCount() - 1) r.bottom += getExtendedPaddingBottom();
6403    }
6404
6405    private void convertFromViewportToContentCoordinates(Rect r) {
6406        final int horizontalOffset = viewportToContentHorizontalOffset();
6407        r.left += horizontalOffset;
6408        r.right += horizontalOffset;
6409
6410        final int verticalOffset = viewportToContentVerticalOffset();
6411        r.top += verticalOffset;
6412        r.bottom += verticalOffset;
6413    }
6414
6415    private int viewportToContentHorizontalOffset() {
6416        return getCompoundPaddingLeft() - mScrollX;
6417    }
6418
6419    private int viewportToContentVerticalOffset() {
6420        int offset = getExtendedPaddingTop() - mScrollY;
6421        if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
6422            offset += getVerticalOffset(false);
6423        }
6424        return offset;
6425    }
6426
6427    @Override
6428    public void debug(int depth) {
6429        super.debug(depth);
6430
6431        String output = debugIndent(depth);
6432        output += "frame={" + mLeft + ", " + mTop + ", " + mRight
6433                + ", " + mBottom + "} scroll={" + mScrollX + ", " + mScrollY
6434                + "} ";
6435
6436        if (mText != null) {
6437
6438            output += "mText=\"" + mText + "\" ";
6439            if (mLayout != null) {
6440                output += "mLayout width=" + mLayout.getWidth()
6441                        + " height=" + mLayout.getHeight();
6442            }
6443        } else {
6444            output += "mText=NULL";
6445        }
6446        Log.d(VIEW_LOG_TAG, output);
6447    }
6448
6449    /**
6450     * Convenience for {@link Selection#getSelectionStart}.
6451     */
6452    @ViewDebug.ExportedProperty(category = "text")
6453    public int getSelectionStart() {
6454        return Selection.getSelectionStart(getText());
6455    }
6456
6457    /**
6458     * Convenience for {@link Selection#getSelectionEnd}.
6459     */
6460    @ViewDebug.ExportedProperty(category = "text")
6461    public int getSelectionEnd() {
6462        return Selection.getSelectionEnd(getText());
6463    }
6464
6465    /**
6466     * Return true iff there is a selection inside this text view.
6467     */
6468    public boolean hasSelection() {
6469        final int selectionStart = getSelectionStart();
6470        final int selectionEnd = getSelectionEnd();
6471
6472        return selectionStart >= 0 && selectionStart != selectionEnd;
6473    }
6474
6475    /**
6476     * Sets the properties of this field (lines, horizontally scrolling,
6477     * transformation method) to be for a single-line input.
6478     *
6479     * @attr ref android.R.styleable#TextView_singleLine
6480     */
6481    public void setSingleLine() {
6482        setSingleLine(true);
6483    }
6484
6485    /**
6486     * If true, sets the properties of this field (number of lines, horizontally scrolling,
6487     * transformation method) to be for a single-line input; if false, restores these to the default
6488     * conditions.
6489     *
6490     * Note that the default conditions are not necessarily those that were in effect prior this
6491     * method, and you may want to reset these properties to your custom values.
6492     *
6493     * @attr ref android.R.styleable#TextView_singleLine
6494     */
6495    @android.view.RemotableViewMethod
6496    public void setSingleLine(boolean singleLine) {
6497        // Could be used, but may break backward compatibility.
6498        // if (mSingleLine == singleLine) return;
6499        setInputTypeSingleLine(singleLine);
6500        applySingleLine(singleLine, true, true);
6501    }
6502
6503    /**
6504     * Adds or remove the EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE on the mInputType.
6505     * @param singleLine
6506     */
6507    private void setInputTypeSingleLine(boolean singleLine) {
6508        if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
6509            if (singleLine) {
6510                mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
6511            } else {
6512                mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
6513            }
6514        }
6515    }
6516
6517    private void applySingleLine(boolean singleLine, boolean applyTransformation,
6518            boolean changeMaxLines) {
6519        mSingleLine = singleLine;
6520        if (singleLine) {
6521            setLines(1);
6522            setHorizontallyScrolling(true);
6523            if (applyTransformation) {
6524                setTransformationMethod(SingleLineTransformationMethod.getInstance());
6525            }
6526        } else {
6527            if (changeMaxLines) {
6528                setMaxLines(Integer.MAX_VALUE);
6529            }
6530            setHorizontallyScrolling(false);
6531            if (applyTransformation) {
6532                setTransformationMethod(null);
6533            }
6534        }
6535    }
6536
6537    /**
6538     * Causes words in the text that are longer than the view is wide
6539     * to be ellipsized instead of broken in the middle.  You may also
6540     * want to {@link #setSingleLine} or {@link #setHorizontallyScrolling}
6541     * to constrain the text to a single line.  Use <code>null</code>
6542     * to turn off ellipsizing.
6543     *
6544     * @attr ref android.R.styleable#TextView_ellipsize
6545     */
6546    public void setEllipsize(TextUtils.TruncateAt where) {
6547        mEllipsize = where;
6548
6549        if (mLayout != null) {
6550            nullLayouts();
6551            requestLayout();
6552            invalidate();
6553        }
6554    }
6555
6556    /**
6557     * Sets how many times to repeat the marquee animation. Only applied if the
6558     * TextView has marquee enabled. Set to -1 to repeat indefinitely.
6559     *
6560     * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
6561     */
6562    public void setMarqueeRepeatLimit(int marqueeLimit) {
6563        mMarqueeRepeatLimit = marqueeLimit;
6564    }
6565
6566    /**
6567     * Returns where, if anywhere, words that are longer than the view
6568     * is wide should be ellipsized.
6569     */
6570    @ViewDebug.ExportedProperty
6571    public TextUtils.TruncateAt getEllipsize() {
6572        return mEllipsize;
6573    }
6574
6575    /**
6576     * Set the TextView so that when it takes focus, all the text is
6577     * selected.
6578     *
6579     * @attr ref android.R.styleable#TextView_selectAllOnFocus
6580     */
6581    @android.view.RemotableViewMethod
6582    public void setSelectAllOnFocus(boolean selectAllOnFocus) {
6583        mSelectAllOnFocus = selectAllOnFocus;
6584
6585        if (selectAllOnFocus && !(mText instanceof Spannable)) {
6586            setText(mText, BufferType.SPANNABLE);
6587        }
6588    }
6589
6590    /**
6591     * Set whether the cursor is visible.  The default is true.
6592     *
6593     * @attr ref android.R.styleable#TextView_cursorVisible
6594     */
6595    @android.view.RemotableViewMethod
6596    public void setCursorVisible(boolean visible) {
6597        if (mCursorVisible != visible) {
6598            mCursorVisible = visible;
6599            invalidate();
6600
6601            makeBlink();
6602
6603            // InsertionPointCursorController depends on mCursorVisible
6604            prepareCursorControllers();
6605        }
6606    }
6607
6608    private boolean isCursorVisible() {
6609        return mCursorVisible && isTextEditable();
6610    }
6611
6612    private boolean canMarquee() {
6613        int width = (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight());
6614        return width > 0 && mLayout.getLineWidth(0) > width;
6615    }
6616
6617    private void startMarquee() {
6618        // Do not ellipsize EditText
6619        if (mInput != null) return;
6620
6621        if (compressText(getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight())) {
6622            return;
6623        }
6624
6625        if ((mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected()) &&
6626                getLineCount() == 1 && canMarquee()) {
6627
6628            if (mMarquee == null) mMarquee = new Marquee(this);
6629            mMarquee.start(mMarqueeRepeatLimit);
6630        }
6631    }
6632
6633    private void stopMarquee() {
6634        if (mMarquee != null && !mMarquee.isStopped()) {
6635            mMarquee.stop();
6636        }
6637    }
6638
6639    private void startStopMarquee(boolean start) {
6640        if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
6641            if (start) {
6642                startMarquee();
6643            } else {
6644                stopMarquee();
6645            }
6646        }
6647    }
6648
6649    private static final class Marquee extends Handler {
6650        // TODO: Add an option to configure this
6651        private static final float MARQUEE_DELTA_MAX = 0.07f;
6652        private static final int MARQUEE_DELAY = 1200;
6653        private static final int MARQUEE_RESTART_DELAY = 1200;
6654        private static final int MARQUEE_RESOLUTION = 1000 / 30;
6655        private static final int MARQUEE_PIXELS_PER_SECOND = 30;
6656
6657        private static final byte MARQUEE_STOPPED = 0x0;
6658        private static final byte MARQUEE_STARTING = 0x1;
6659        private static final byte MARQUEE_RUNNING = 0x2;
6660
6661        private static final int MESSAGE_START = 0x1;
6662        private static final int MESSAGE_TICK = 0x2;
6663        private static final int MESSAGE_RESTART = 0x3;
6664
6665        private final WeakReference<TextView> mView;
6666
6667        private byte mStatus = MARQUEE_STOPPED;
6668        private final float mScrollUnit;
6669        private float mMaxScroll;
6670        float mMaxFadeScroll;
6671        private float mGhostStart;
6672        private float mGhostOffset;
6673        private float mFadeStop;
6674        private int mRepeatLimit;
6675
6676        float mScroll;
6677
6678        Marquee(TextView v) {
6679            final float density = v.getContext().getResources().getDisplayMetrics().density;
6680            mScrollUnit = (MARQUEE_PIXELS_PER_SECOND * density) / MARQUEE_RESOLUTION;
6681            mView = new WeakReference<TextView>(v);
6682        }
6683
6684        @Override
6685        public void handleMessage(Message msg) {
6686            switch (msg.what) {
6687                case MESSAGE_START:
6688                    mStatus = MARQUEE_RUNNING;
6689                    tick();
6690                    break;
6691                case MESSAGE_TICK:
6692                    tick();
6693                    break;
6694                case MESSAGE_RESTART:
6695                    if (mStatus == MARQUEE_RUNNING) {
6696                        if (mRepeatLimit >= 0) {
6697                            mRepeatLimit--;
6698                        }
6699                        start(mRepeatLimit);
6700                    }
6701                    break;
6702            }
6703        }
6704
6705        void tick() {
6706            if (mStatus != MARQUEE_RUNNING) {
6707                return;
6708            }
6709
6710            removeMessages(MESSAGE_TICK);
6711
6712            final TextView textView = mView.get();
6713            if (textView != null && (textView.isFocused() || textView.isSelected())) {
6714                mScroll += mScrollUnit;
6715                if (mScroll > mMaxScroll) {
6716                    mScroll = mMaxScroll;
6717                    sendEmptyMessageDelayed(MESSAGE_RESTART, MARQUEE_RESTART_DELAY);
6718                } else {
6719                    sendEmptyMessageDelayed(MESSAGE_TICK, MARQUEE_RESOLUTION);
6720                }
6721                textView.invalidate();
6722            }
6723        }
6724
6725        void stop() {
6726            mStatus = MARQUEE_STOPPED;
6727            removeMessages(MESSAGE_START);
6728            removeMessages(MESSAGE_RESTART);
6729            removeMessages(MESSAGE_TICK);
6730            resetScroll();
6731        }
6732
6733        private void resetScroll() {
6734            mScroll = 0.0f;
6735            final TextView textView = mView.get();
6736            if (textView != null) textView.invalidate();
6737        }
6738
6739        void start(int repeatLimit) {
6740            if (repeatLimit == 0) {
6741                stop();
6742                return;
6743            }
6744            mRepeatLimit = repeatLimit;
6745            final TextView textView = mView.get();
6746            if (textView != null && textView.mLayout != null) {
6747                mStatus = MARQUEE_STARTING;
6748                mScroll = 0.0f;
6749                final int textWidth = textView.getWidth() - textView.getCompoundPaddingLeft() -
6750                        textView.getCompoundPaddingRight();
6751                final float lineWidth = textView.mLayout.getLineWidth(0);
6752                final float gap = textWidth / 3.0f;
6753                mGhostStart = lineWidth - textWidth + gap;
6754                mMaxScroll = mGhostStart + textWidth;
6755                mGhostOffset = lineWidth + gap;
6756                mFadeStop = lineWidth + textWidth / 6.0f;
6757                mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;
6758
6759                textView.invalidate();
6760                sendEmptyMessageDelayed(MESSAGE_START, MARQUEE_DELAY);
6761            }
6762        }
6763
6764        float getGhostOffset() {
6765            return mGhostOffset;
6766        }
6767
6768        boolean shouldDrawLeftFade() {
6769            return mScroll <= mFadeStop;
6770        }
6771
6772        boolean shouldDrawGhost() {
6773            return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart;
6774        }
6775
6776        boolean isRunning() {
6777            return mStatus == MARQUEE_RUNNING;
6778        }
6779
6780        boolean isStopped() {
6781            return mStatus == MARQUEE_STOPPED;
6782        }
6783    }
6784
6785    /**
6786     * This method is called when the text is changed, in case any subclasses
6787     * would like to know.
6788     *
6789     * Within <code>text</code>, the <code>lengthAfter</code> characters
6790     * beginning at <code>start</code> have just replaced old text that had
6791     * length <code>lengthBefore</code>. It is an error to attempt to make
6792     * changes to <code>text</code> from this callback.
6793     *
6794     * @param text The text the TextView is displaying
6795     * @param start The offset of the start of the range of the text that was
6796     * modified
6797     * @param lengthBefore The length of the former text that has been replaced
6798     * @param lengthAfter The length of the replacement modified text
6799     */
6800    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
6801        // intentionally empty
6802    }
6803
6804    /**
6805     * This method is called when the selection has changed, in case any
6806     * subclasses would like to know.
6807     *
6808     * @param selStart The new selection start location.
6809     * @param selEnd The new selection end location.
6810     */
6811    protected void onSelectionChanged(int selStart, int selEnd) {
6812        // intentionally empty
6813    }
6814
6815    /**
6816     * Adds a TextWatcher to the list of those whose methods are called
6817     * whenever this TextView's text changes.
6818     * <p>
6819     * In 1.0, the {@link TextWatcher#afterTextChanged} method was erroneously
6820     * not called after {@link #setText} calls.  Now, doing {@link #setText}
6821     * if there are any text changed listeners forces the buffer type to
6822     * Editable if it would not otherwise be and does call this method.
6823     */
6824    public void addTextChangedListener(TextWatcher watcher) {
6825        if (mListeners == null) {
6826            mListeners = new ArrayList<TextWatcher>();
6827        }
6828
6829        mListeners.add(watcher);
6830    }
6831
6832    /**
6833     * Removes the specified TextWatcher from the list of those whose
6834     * methods are called
6835     * whenever this TextView's text changes.
6836     */
6837    public void removeTextChangedListener(TextWatcher watcher) {
6838        if (mListeners != null) {
6839            int i = mListeners.indexOf(watcher);
6840
6841            if (i >= 0) {
6842                mListeners.remove(i);
6843            }
6844        }
6845    }
6846
6847    private void sendBeforeTextChanged(CharSequence text, int start, int before,
6848                                   int after) {
6849        if (mListeners != null) {
6850            final ArrayList<TextWatcher> list = mListeners;
6851            final int count = list.size();
6852            for (int i = 0; i < count; i++) {
6853                list.get(i).beforeTextChanged(text, start, before, after);
6854            }
6855        }
6856    }
6857
6858    /**
6859     * Not private so it can be called from an inner class without going
6860     * through a thunk.
6861     */
6862    void sendOnTextChanged(CharSequence text, int start, int before,
6863                                   int after) {
6864        if (mListeners != null) {
6865            final ArrayList<TextWatcher> list = mListeners;
6866            final int count = list.size();
6867            for (int i = 0; i < count; i++) {
6868                list.get(i).onTextChanged(text, start, before, after);
6869            }
6870        }
6871    }
6872
6873    /**
6874     * Not private so it can be called from an inner class without going
6875     * through a thunk.
6876     */
6877    void sendAfterTextChanged(Editable text) {
6878        if (mListeners != null) {
6879            final ArrayList<TextWatcher> list = mListeners;
6880            final int count = list.size();
6881            for (int i = 0; i < count; i++) {
6882                list.get(i).afterTextChanged(text);
6883            }
6884        }
6885    }
6886
6887    /**
6888     * Not private so it can be called from an inner class without going
6889     * through a thunk.
6890     */
6891    void handleTextChanged(CharSequence buffer, int start, int before, int after) {
6892        final InputMethodState ims = mInputMethodState;
6893        if (ims == null || ims.mBatchEditNesting == 0) {
6894            updateAfterEdit();
6895        }
6896        if (ims != null) {
6897            ims.mContentChanged = true;
6898            if (ims.mChangedStart < 0) {
6899                ims.mChangedStart = start;
6900                ims.mChangedEnd = start+before;
6901            } else {
6902                ims.mChangedStart = Math.min(ims.mChangedStart, start);
6903                ims.mChangedEnd = Math.max(ims.mChangedEnd, start + before - ims.mChangedDelta);
6904            }
6905            ims.mChangedDelta += after-before;
6906        }
6907
6908        sendOnTextChanged(buffer, start, before, after);
6909        onTextChanged(buffer, start, before, after);
6910
6911        // Hide the controllers if the amount of content changed
6912        if (before != after) {
6913            hideControllers();
6914        }
6915    }
6916
6917    /**
6918     * Not private so it can be called from an inner class without going
6919     * through a thunk.
6920     */
6921    void spanChange(Spanned buf, Object what, int oldStart, int newStart, int oldEnd, int newEnd) {
6922        // XXX Make the start and end move together if this ends up
6923        // spending too much time invalidating.
6924
6925        boolean selChanged = false;
6926        int newSelStart=-1, newSelEnd=-1;
6927
6928        final InputMethodState ims = mInputMethodState;
6929
6930        if (what == Selection.SELECTION_END) {
6931            mHighlightPathBogus = true;
6932            selChanged = true;
6933            newSelEnd = newStart;
6934
6935            if (!isFocused()) {
6936                mSelectionMoved = true;
6937            }
6938
6939            if (oldStart >= 0 || newStart >= 0) {
6940                invalidateCursor(Selection.getSelectionStart(buf), oldStart, newStart);
6941                registerForPreDraw();
6942                makeBlink();
6943            }
6944        }
6945
6946        if (what == Selection.SELECTION_START) {
6947            mHighlightPathBogus = true;
6948            selChanged = true;
6949            newSelStart = newStart;
6950
6951            if (!isFocused()) {
6952                mSelectionMoved = true;
6953            }
6954
6955            if (oldStart >= 0 || newStart >= 0) {
6956                int end = Selection.getSelectionEnd(buf);
6957                invalidateCursor(end, oldStart, newStart);
6958            }
6959        }
6960
6961        if (selChanged) {
6962            if ((buf.getSpanFlags(what)&Spanned.SPAN_INTERMEDIATE) == 0) {
6963                if (newSelStart < 0) {
6964                    newSelStart = Selection.getSelectionStart(buf);
6965                }
6966                if (newSelEnd < 0) {
6967                    newSelEnd = Selection.getSelectionEnd(buf);
6968                }
6969                onSelectionChanged(newSelStart, newSelEnd);
6970            }
6971        }
6972
6973        if (what instanceof UpdateAppearance ||
6974            what instanceof ParagraphStyle) {
6975            if (ims == null || ims.mBatchEditNesting == 0) {
6976                invalidate();
6977                mHighlightPathBogus = true;
6978                checkForResize();
6979            } else {
6980                ims.mContentChanged = true;
6981            }
6982        }
6983
6984        if (MetaKeyKeyListener.isMetaTracker(buf, what)) {
6985            mHighlightPathBogus = true;
6986            if (ims != null && MetaKeyKeyListener.isSelectingMetaTracker(buf, what)) {
6987                ims.mSelectionModeChanged = true;
6988            }
6989
6990            if (Selection.getSelectionStart(buf) >= 0) {
6991                if (ims == null || ims.mBatchEditNesting == 0) {
6992                    invalidateCursor();
6993                } else {
6994                    ims.mCursorChanged = true;
6995                }
6996            }
6997        }
6998
6999        if (what instanceof ParcelableSpan) {
7000            // If this is a span that can be sent to a remote process,
7001            // the current extract editor would be interested in it.
7002            if (ims != null && ims.mExtracting != null) {
7003                if (ims.mBatchEditNesting != 0) {
7004                    if (oldStart >= 0) {
7005                        if (ims.mChangedStart > oldStart) {
7006                            ims.mChangedStart = oldStart;
7007                        }
7008                        if (ims.mChangedStart > oldEnd) {
7009                            ims.mChangedStart = oldEnd;
7010                        }
7011                    }
7012                    if (newStart >= 0) {
7013                        if (ims.mChangedStart > newStart) {
7014                            ims.mChangedStart = newStart;
7015                        }
7016                        if (ims.mChangedStart > newEnd) {
7017                            ims.mChangedStart = newEnd;
7018                        }
7019                    }
7020                } else {
7021                    if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Span change outside of batch: "
7022                            + oldStart + "-" + oldEnd + ","
7023                            + newStart + "-" + newEnd + what);
7024                    ims.mContentChanged = true;
7025                }
7026            }
7027        }
7028    }
7029
7030    private class ChangeWatcher
7031    implements TextWatcher, SpanWatcher {
7032
7033        private CharSequence mBeforeText;
7034
7035        public void beforeTextChanged(CharSequence buffer, int start,
7036                                      int before, int after) {
7037            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "beforeTextChanged start=" + start
7038                    + " before=" + before + " after=" + after + ": " + buffer);
7039
7040            if (AccessibilityManager.getInstance(mContext).isEnabled()
7041                    && !isPasswordInputType(mInputType)
7042                    && !hasPasswordTransformationMethod()) {
7043                mBeforeText = buffer.toString();
7044            }
7045
7046            TextView.this.sendBeforeTextChanged(buffer, start, before, after);
7047        }
7048
7049        public void onTextChanged(CharSequence buffer, int start,
7050                                  int before, int after) {
7051            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onTextChanged start=" + start
7052                    + " before=" + before + " after=" + after + ": " + buffer);
7053            TextView.this.handleTextChanged(buffer, start, before, after);
7054
7055            if (AccessibilityManager.getInstance(mContext).isEnabled() &&
7056                    (isFocused() || isSelected() &&
7057                    isShown())) {
7058                sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after);
7059                mBeforeText = null;
7060            }
7061        }
7062
7063        public void afterTextChanged(Editable buffer) {
7064            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "afterTextChanged: " + buffer);
7065            TextView.this.sendAfterTextChanged(buffer);
7066
7067            if (MetaKeyKeyListener.getMetaState(buffer,
7068                                 MetaKeyKeyListener.META_SELECTING) != 0) {
7069                MetaKeyKeyListener.stopSelecting(TextView.this, buffer);
7070            }
7071        }
7072
7073        public void onSpanChanged(Spannable buf,
7074                                  Object what, int s, int e, int st, int en) {
7075            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanChanged s=" + s + " e=" + e
7076                    + " st=" + st + " en=" + en + " what=" + what + ": " + buf);
7077            TextView.this.spanChange(buf, what, s, st, e, en);
7078        }
7079
7080        public void onSpanAdded(Spannable buf, Object what, int s, int e) {
7081            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanAdded s=" + s + " e=" + e
7082                    + " what=" + what + ": " + buf);
7083            TextView.this.spanChange(buf, what, -1, s, -1, e);
7084        }
7085
7086        public void onSpanRemoved(Spannable buf, Object what, int s, int e) {
7087            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanRemoved s=" + s + " e=" + e
7088                    + " what=" + what + ": " + buf);
7089            TextView.this.spanChange(buf, what, s, -1, e, -1);
7090        }
7091    }
7092
7093    /**
7094     * @hide
7095     */
7096    @Override
7097    public void dispatchFinishTemporaryDetach() {
7098        mDispatchTemporaryDetach = true;
7099        super.dispatchFinishTemporaryDetach();
7100        mDispatchTemporaryDetach = false;
7101    }
7102
7103    @Override
7104    public void onStartTemporaryDetach() {
7105        super.onStartTemporaryDetach();
7106        // Only track when onStartTemporaryDetach() is called directly,
7107        // usually because this instance is an editable field in a list
7108        if (!mDispatchTemporaryDetach) mTemporaryDetach = true;
7109
7110        // Because of View recycling in ListView, there is no easy way to know when a TextView with
7111        // selection becomes visible again. Until a better solution is found, stop text selection
7112        // mode (if any) as soon as this TextView is recycled.
7113        stopSelectionActionMode();
7114    }
7115
7116    @Override
7117    public void onFinishTemporaryDetach() {
7118        super.onFinishTemporaryDetach();
7119        // Only track when onStartTemporaryDetach() is called directly,
7120        // usually because this instance is an editable field in a list
7121        if (!mDispatchTemporaryDetach) mTemporaryDetach = false;
7122    }
7123
7124    @Override
7125    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
7126        if (mTemporaryDetach) {
7127            // If we are temporarily in the detach state, then do nothing.
7128            super.onFocusChanged(focused, direction, previouslyFocusedRect);
7129            return;
7130        }
7131
7132        mShowCursor = SystemClock.uptimeMillis();
7133
7134        ensureEndedBatchEdit();
7135
7136        if (focused) {
7137            int selStart = getSelectionStart();
7138            int selEnd = getSelectionEnd();
7139
7140            // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
7141            // mode for these, unless there was a specific selection already started.
7142            final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0 &&
7143                    selEnd == mText.length();
7144            mCreatedWithASelection = mFrozenWithFocus && hasSelection() && !isFocusHighlighted;
7145
7146            if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
7147                // If a tap was used to give focus to that view, move cursor at tap position.
7148                // Has to be done before onTakeFocus, which can be overloaded.
7149                final int lastTapPosition = getLastTapPosition();
7150                if (lastTapPosition >= 0) {
7151                    Selection.setSelection((Spannable) mText, lastTapPosition);
7152                }
7153
7154                if (mMovement != null) {
7155                    mMovement.onTakeFocus(this, (Spannable) mText, direction);
7156                }
7157
7158                // The DecorView does not have focus when the 'Done' ExtractEditText button is
7159                // pressed. Since it is the ViewAncestor's mView, it requests focus before
7160                // ExtractEditText clears focus, which gives focus to the ExtractEditText.
7161                // This special case ensure that we keep current selection in that case.
7162                // It would be better to know why the DecorView does not have focus at that time.
7163                if (((this instanceof ExtractEditText) || mSelectionMoved) &&
7164                        selStart >= 0 && selEnd >= 0) {
7165                    /*
7166                     * Someone intentionally set the selection, so let them
7167                     * do whatever it is that they wanted to do instead of
7168                     * the default on-focus behavior.  We reset the selection
7169                     * here instead of just skipping the onTakeFocus() call
7170                     * because some movement methods do something other than
7171                     * just setting the selection in theirs and we still
7172                     * need to go through that path.
7173                     */
7174                    Selection.setSelection((Spannable) mText, selStart, selEnd);
7175                }
7176
7177                if (mSelectAllOnFocus) {
7178                    selectAll();
7179                }
7180
7181                mTouchFocusSelected = true;
7182            }
7183
7184            mFrozenWithFocus = false;
7185            mSelectionMoved = false;
7186
7187            if (mText instanceof Spannable) {
7188                Spannable sp = (Spannable) mText;
7189                MetaKeyKeyListener.resetMetaState(sp);
7190            }
7191
7192            makeBlink();
7193
7194            if (mError != null) {
7195                showError();
7196            }
7197        } else {
7198            if (mError != null) {
7199                hideError();
7200            }
7201            // Don't leave us in the middle of a batch edit.
7202            onEndBatchEdit();
7203
7204            if (this instanceof ExtractEditText) {
7205                // terminateTextSelectionMode removes selection, which we want to keep when
7206                // ExtractEditText goes out of focus.
7207                final int selStart = getSelectionStart();
7208                final int selEnd = getSelectionEnd();
7209                hideControllers();
7210                Selection.setSelection((Spannable) mText, selStart, selEnd);
7211            } else {
7212                hideControllers();
7213            }
7214
7215            // No need to create the controller
7216            if (mSelectionModifierCursorController != null) {
7217                mSelectionModifierCursorController.resetTouchOffsets();
7218            }
7219        }
7220
7221        startStopMarquee(focused);
7222
7223        if (mTransformation != null) {
7224            mTransformation.onFocusChanged(this, mText, focused, direction, previouslyFocusedRect);
7225        }
7226
7227        super.onFocusChanged(focused, direction, previouslyFocusedRect);
7228    }
7229
7230    private int getLastTapPosition() {
7231        // No need to create the controller at that point, no last tap position saved
7232        if (mSelectionModifierCursorController != null) {
7233            int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
7234            if (lastTapPosition >= 0) {
7235                // Safety check, should not be possible.
7236                if (lastTapPosition > mText.length()) {
7237                    Log.e(LOG_TAG, "Invalid tap focus position (" + lastTapPosition + " vs "
7238                            + mText.length() + ")");
7239                    lastTapPosition = mText.length();
7240                }
7241                return lastTapPosition;
7242            }
7243        }
7244
7245        return -1;
7246    }
7247
7248    @Override
7249    public void onWindowFocusChanged(boolean hasWindowFocus) {
7250        super.onWindowFocusChanged(hasWindowFocus);
7251
7252        if (hasWindowFocus) {
7253            if (mBlink != null) {
7254                mBlink.uncancel();
7255                makeBlink();
7256            }
7257        } else {
7258            if (mBlink != null) {
7259                mBlink.cancel();
7260            }
7261            // Don't leave us in the middle of a batch edit.
7262            onEndBatchEdit();
7263            if (mInputContentType != null) {
7264                mInputContentType.enterDown = false;
7265            }
7266            hideControllers();
7267            removeAllSuggestionSpans();
7268        }
7269
7270        startStopMarquee(hasWindowFocus);
7271    }
7272
7273    private void removeAllSuggestionSpans() {
7274        if (mText instanceof Editable) {
7275            Editable editable = ((Editable) mText);
7276            SuggestionSpan[] spans = editable.getSpans(0, mText.length(), SuggestionSpan.class);
7277            final int length = spans.length;
7278            for (int i = 0; i < length; i++) {
7279                editable.removeSpan(spans[i]);
7280            }
7281        }
7282    }
7283
7284    @Override
7285    protected void onVisibilityChanged(View changedView, int visibility) {
7286        super.onVisibilityChanged(changedView, visibility);
7287        if (visibility != VISIBLE) {
7288            hideControllers();
7289        }
7290    }
7291
7292    /**
7293     * Use {@link BaseInputConnection#removeComposingSpans
7294     * BaseInputConnection.removeComposingSpans()} to remove any IME composing
7295     * state from this text view.
7296     */
7297    public void clearComposingText() {
7298        if (mText instanceof Spannable) {
7299            BaseInputConnection.removeComposingSpans((Spannable)mText);
7300        }
7301    }
7302
7303    @Override
7304    public void setSelected(boolean selected) {
7305        boolean wasSelected = isSelected();
7306
7307        super.setSelected(selected);
7308
7309        if (selected != wasSelected && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
7310            if (selected) {
7311                startMarquee();
7312            } else {
7313                stopMarquee();
7314            }
7315        }
7316    }
7317
7318    @Override
7319    public boolean onTouchEvent(MotionEvent event) {
7320        final int action = event.getActionMasked();
7321
7322        if (hasInsertionController()) {
7323            getInsertionController().onTouchEvent(event);
7324        }
7325        if (hasSelectionController()) {
7326            getSelectionController().onTouchEvent(event);
7327        }
7328
7329        if (action == MotionEvent.ACTION_DOWN) {
7330            mLastDownPositionX = event.getX();
7331            mLastDownPositionY = event.getY();
7332
7333            // Reset this state; it will be re-set if super.onTouchEvent
7334            // causes focus to move to the view.
7335            mTouchFocusSelected = false;
7336            mIgnoreActionUpEvent = false;
7337        }
7338
7339        final boolean superResult = super.onTouchEvent(event);
7340
7341        /*
7342         * Don't handle the release after a long press, because it will
7343         * move the selection away from whatever the menu action was
7344         * trying to affect.
7345         */
7346        if (mDiscardNextActionUp && action == MotionEvent.ACTION_UP) {
7347            mDiscardNextActionUp = false;
7348            return superResult;
7349        }
7350
7351        final boolean touchIsFinished = action == MotionEvent.ACTION_UP && !mIgnoreActionUpEvent &&
7352                isFocused();
7353
7354        if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
7355                && mText instanceof Spannable && mLayout != null) {
7356            boolean handled = false;
7357
7358            if (mMovement != null) {
7359                handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
7360            }
7361
7362            if (mLinksClickable && mAutoLinkMask != 0 && mTextIsSelectable && touchIsFinished) {
7363                // The LinkMovementMethod which should handle taps on links has not been installed
7364                // to support text selection. We reproduce its behavior here to open links.
7365                ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(),
7366                        getSelectionEnd(), ClickableSpan.class);
7367
7368                if (links.length != 0) {
7369                    links[0].onClick(this);
7370                    handled = true;
7371                }
7372            }
7373
7374            if ((isTextEditable() || mTextIsSelectable) && touchIsFinished) {
7375                // Show the IME, except when selecting in read-only text.
7376                if (!mTextIsSelectable) {
7377                    final InputMethodManager imm = InputMethodManager.peekInstance();
7378                    handled |= imm != null && imm.showSoftInput(this, 0);
7379                }
7380
7381                boolean selectAllGotFocus = mSelectAllOnFocus && didTouchFocusSelect();
7382                if (!selectAllGotFocus && hasSelection()) {
7383                    startSelectionActionMode();
7384                } else {
7385                    stopSelectionActionMode();
7386                    hideSuggestions();
7387                    if (hasInsertionController() && !selectAllGotFocus && mText.length() > 0) {
7388                        getInsertionController().show();
7389                    }
7390                }
7391            }
7392
7393            if (handled) {
7394                return true;
7395            }
7396        }
7397
7398        return superResult;
7399    }
7400
7401    @Override
7402    public boolean onGenericMotionEvent(MotionEvent event) {
7403        if (mMovement != null && mText instanceof Spannable && mLayout != null) {
7404            try {
7405                if (mMovement.onGenericMotionEvent(this, (Spannable) mText, event)) {
7406                    return true;
7407                }
7408            } catch (AbstractMethodError ex) {
7409                // onGenericMotionEvent was added to the MovementMethod interface in API 12.
7410                // Ignore its absence in case third party applications implemented the
7411                // interface directly.
7412            }
7413        }
7414        return super.onGenericMotionEvent(event);
7415    }
7416
7417    private void prepareCursorControllers() {
7418        boolean windowSupportsHandles = false;
7419
7420        ViewGroup.LayoutParams params = getRootView().getLayoutParams();
7421        if (params instanceof WindowManager.LayoutParams) {
7422            WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
7423            windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
7424                    || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
7425        }
7426
7427        mInsertionControllerEnabled = windowSupportsHandles && isCursorVisible() && mLayout != null;
7428        mSelectionControllerEnabled = windowSupportsHandles && textCanBeSelected() &&
7429                mLayout != null;
7430
7431        if (!mInsertionControllerEnabled) {
7432            hideInsertionPointCursorController();
7433            if (mInsertionPointCursorController != null) {
7434                mInsertionPointCursorController.onDetached();
7435                mInsertionPointCursorController = null;
7436            }
7437        }
7438
7439        if (!mSelectionControllerEnabled) {
7440            stopSelectionActionMode();
7441            if (mSelectionModifierCursorController != null) {
7442                mSelectionModifierCursorController.onDetached();
7443                mSelectionModifierCursorController = null;
7444            }
7445        }
7446    }
7447
7448    /**
7449     * @return True iff this TextView contains a text that can be edited, or if this is
7450     * a selectable TextView.
7451     */
7452    private boolean isTextEditable() {
7453        return mText instanceof Editable && onCheckIsTextEditor() && isEnabled();
7454    }
7455
7456    /**
7457     * Returns true, only while processing a touch gesture, if the initial
7458     * touch down event caused focus to move to the text view and as a result
7459     * its selection changed.  Only valid while processing the touch gesture
7460     * of interest.
7461     */
7462    public boolean didTouchFocusSelect() {
7463        return mTouchFocusSelected;
7464    }
7465
7466    @Override
7467    public void cancelLongPress() {
7468        super.cancelLongPress();
7469        mIgnoreActionUpEvent = true;
7470    }
7471
7472    @Override
7473    public boolean onTrackballEvent(MotionEvent event) {
7474        if (mMovement != null && mText instanceof Spannable &&
7475            mLayout != null) {
7476            if (mMovement.onTrackballEvent(this, (Spannable) mText, event)) {
7477                return true;
7478            }
7479        }
7480
7481        return super.onTrackballEvent(event);
7482    }
7483
7484    public void setScroller(Scroller s) {
7485        mScroller = s;
7486    }
7487
7488    private static class Blink extends Handler implements Runnable {
7489        private final WeakReference<TextView> mView;
7490        private boolean mCancelled;
7491
7492        public Blink(TextView v) {
7493            mView = new WeakReference<TextView>(v);
7494        }
7495
7496        public void run() {
7497            if (mCancelled) {
7498                return;
7499            }
7500
7501            removeCallbacks(Blink.this);
7502
7503            TextView tv = mView.get();
7504
7505            if (tv != null && tv.shouldBlink()) {
7506                if (tv.mLayout != null) {
7507                    tv.invalidateCursorPath();
7508                }
7509
7510                postAtTime(this, SystemClock.uptimeMillis() + BLINK);
7511            }
7512        }
7513
7514        void cancel() {
7515            if (!mCancelled) {
7516                removeCallbacks(Blink.this);
7517                mCancelled = true;
7518            }
7519        }
7520
7521        void uncancel() {
7522            mCancelled = false;
7523        }
7524    }
7525
7526    /**
7527     * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
7528     */
7529    private boolean shouldBlink() {
7530        if (!isFocused()) return false;
7531
7532        final int start = getSelectionStart();
7533        if (start < 0) return false;
7534
7535        final int end = getSelectionEnd();
7536        if (end < 0) return false;
7537
7538        return start == end;
7539    }
7540
7541    private void makeBlink() {
7542        if (isCursorVisible()) {
7543            if (shouldBlink()) {
7544                mShowCursor = SystemClock.uptimeMillis();
7545                if (mBlink == null) mBlink = new Blink(this);
7546                mBlink.removeCallbacks(mBlink);
7547                mBlink.postAtTime(mBlink, mShowCursor + BLINK);
7548            }
7549        } else {
7550            if (mBlink != null) mBlink.removeCallbacks(mBlink);
7551        }
7552    }
7553
7554    @Override
7555    protected float getLeftFadingEdgeStrength() {
7556        if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return 0.0f;
7557        if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
7558            if (mMarquee != null && !mMarquee.isStopped()) {
7559                final Marquee marquee = mMarquee;
7560                if (marquee.shouldDrawLeftFade()) {
7561                    return marquee.mScroll / getHorizontalFadingEdgeLength();
7562                } else {
7563                    return 0.0f;
7564                }
7565            } else if (getLineCount() == 1) {
7566                switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
7567                    case Gravity.LEFT:
7568                        return 0.0f;
7569                    case Gravity.RIGHT:
7570                        return (mLayout.getLineRight(0) - (mRight - mLeft) -
7571                                getCompoundPaddingLeft() - getCompoundPaddingRight() -
7572                                mLayout.getLineLeft(0)) / getHorizontalFadingEdgeLength();
7573                    case Gravity.CENTER_HORIZONTAL:
7574                        return 0.0f;
7575                }
7576            }
7577        }
7578        return super.getLeftFadingEdgeStrength();
7579    }
7580
7581    @Override
7582    protected float getRightFadingEdgeStrength() {
7583        if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return 0.0f;
7584        if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
7585            if (mMarquee != null && !mMarquee.isStopped()) {
7586                final Marquee marquee = mMarquee;
7587                return (marquee.mMaxFadeScroll - marquee.mScroll) / getHorizontalFadingEdgeLength();
7588            } else if (getLineCount() == 1) {
7589                switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
7590                    case Gravity.LEFT:
7591                        final int textWidth = (mRight - mLeft) - getCompoundPaddingLeft() -
7592                                getCompoundPaddingRight();
7593                        final float lineWidth = mLayout.getLineWidth(0);
7594                        return (lineWidth - textWidth) / getHorizontalFadingEdgeLength();
7595                    case Gravity.RIGHT:
7596                        return 0.0f;
7597                    case Gravity.CENTER_HORIZONTAL:
7598                    case Gravity.FILL_HORIZONTAL:
7599                        return (mLayout.getLineWidth(0) - ((mRight - mLeft) -
7600                                getCompoundPaddingLeft() - getCompoundPaddingRight())) /
7601                                getHorizontalFadingEdgeLength();
7602                }
7603            }
7604        }
7605        return super.getRightFadingEdgeStrength();
7606    }
7607
7608    @Override
7609    protected int computeHorizontalScrollRange() {
7610        if (mLayout != null) {
7611            return mSingleLine && (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT ?
7612                    (int) mLayout.getLineWidth(0) : mLayout.getWidth();
7613        }
7614
7615        return super.computeHorizontalScrollRange();
7616    }
7617
7618    @Override
7619    protected int computeVerticalScrollRange() {
7620        if (mLayout != null)
7621            return mLayout.getHeight();
7622
7623        return super.computeVerticalScrollRange();
7624    }
7625
7626    @Override
7627    protected int computeVerticalScrollExtent() {
7628        return getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom();
7629    }
7630
7631    public enum BufferType {
7632        NORMAL, SPANNABLE, EDITABLE,
7633    }
7634
7635    /**
7636     * Returns the TextView_textColor attribute from the
7637     * Resources.StyledAttributes, if set, or the TextAppearance_textColor
7638     * from the TextView_textAppearance attribute, if TextView_textColor
7639     * was not set directly.
7640     */
7641    public static ColorStateList getTextColors(Context context, TypedArray attrs) {
7642        ColorStateList colors;
7643        colors = attrs.getColorStateList(com.android.internal.R.styleable.
7644                                         TextView_textColor);
7645
7646        if (colors == null) {
7647            int ap = attrs.getResourceId(com.android.internal.R.styleable.
7648                                         TextView_textAppearance, -1);
7649            if (ap != -1) {
7650                TypedArray appearance;
7651                appearance = context.obtainStyledAttributes(ap,
7652                                            com.android.internal.R.styleable.TextAppearance);
7653                colors = appearance.getColorStateList(com.android.internal.R.styleable.
7654                                                  TextAppearance_textColor);
7655                appearance.recycle();
7656            }
7657        }
7658
7659        return colors;
7660    }
7661
7662    /**
7663     * Returns the default color from the TextView_textColor attribute
7664     * from the AttributeSet, if set, or the default color from the
7665     * TextAppearance_textColor from the TextView_textAppearance attribute,
7666     * if TextView_textColor was not set directly.
7667     */
7668    public static int getTextColor(Context context,
7669                                   TypedArray attrs,
7670                                   int def) {
7671        ColorStateList colors = getTextColors(context, attrs);
7672
7673        if (colors == null) {
7674            return def;
7675        } else {
7676            return colors.getDefaultColor();
7677        }
7678    }
7679
7680    @Override
7681    public boolean onKeyShortcut(int keyCode, KeyEvent event) {
7682        final int filteredMetaState = event.getMetaState() & ~KeyEvent.META_CTRL_MASK;
7683        if (KeyEvent.metaStateHasNoModifiers(filteredMetaState)) {
7684            switch (keyCode) {
7685            case KeyEvent.KEYCODE_A:
7686                if (canSelectText()) {
7687                    return onTextContextMenuItem(ID_SELECT_ALL);
7688                }
7689                break;
7690            case KeyEvent.KEYCODE_X:
7691                if (canCut()) {
7692                    return onTextContextMenuItem(ID_CUT);
7693                }
7694                break;
7695            case KeyEvent.KEYCODE_C:
7696                if (canCopy()) {
7697                    return onTextContextMenuItem(ID_COPY);
7698                }
7699                break;
7700            case KeyEvent.KEYCODE_V:
7701                if (canPaste()) {
7702                    return onTextContextMenuItem(ID_PASTE);
7703                }
7704                break;
7705            }
7706        }
7707        return super.onKeyShortcut(keyCode, event);
7708    }
7709
7710    /**
7711     * Unlike {@link #textCanBeSelected()}, this method is based on the <i>current</i> state of the
7712     * TextView. {@link #textCanBeSelected()} has to be true (this is one of the conditions to have
7713     * a selection controller (see {@link #prepareCursorControllers()}), but this is not sufficient.
7714     */
7715    private boolean canSelectText() {
7716        return hasSelectionController() && mText.length() != 0;
7717    }
7718
7719    /**
7720     * Test based on the <i>intrinsic</i> charateristics of the TextView.
7721     * The text must be spannable and the movement method must allow for arbitary selection.
7722     *
7723     * See also {@link #canSelectText()}.
7724     */
7725    private boolean textCanBeSelected() {
7726        // prepareCursorController() relies on this method.
7727        // If you change this condition, make sure prepareCursorController is called anywhere
7728        // the value of this condition might be changed.
7729        return mText instanceof Spannable && mMovement != null && mMovement.canSelectArbitrarily();
7730    }
7731
7732    private boolean canCut() {
7733        if (hasPasswordTransformationMethod()) {
7734            return false;
7735        }
7736
7737        if (mText.length() > 0 && hasSelection() && mText instanceof Editable && mInput != null) {
7738            return true;
7739        }
7740
7741        return false;
7742    }
7743
7744    private boolean canCopy() {
7745        if (hasPasswordTransformationMethod()) {
7746            return false;
7747        }
7748
7749        if (mText.length() > 0 && hasSelection()) {
7750            return true;
7751        }
7752
7753        return false;
7754    }
7755
7756    private boolean canPaste() {
7757        return (mText instanceof Editable &&
7758                mInput != null &&
7759                getSelectionStart() >= 0 &&
7760                getSelectionEnd() >= 0 &&
7761                ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)).
7762                hasPrimaryClip());
7763    }
7764
7765    private static long packRangeInLong(int start, int end) {
7766        return (((long) start) << 32) | end;
7767    }
7768
7769    private static int extractRangeStartFromLong(long range) {
7770        return (int) (range >>> 32);
7771    }
7772
7773    private static int extractRangeEndFromLong(long range) {
7774        return (int) (range & 0x00000000FFFFFFFFL);
7775    }
7776
7777    private boolean selectAll() {
7778        final int length = mText.length();
7779        Selection.setSelection((Spannable) mText, 0, length);
7780        return length > 0;
7781    }
7782
7783    /**
7784     * Adjusts selection to the word under last touch offset.
7785     * Return true if the operation was successfully performed.
7786     */
7787    private boolean selectCurrentWord() {
7788        if (!canSelectText()) {
7789            return false;
7790        }
7791
7792        if (hasPasswordTransformationMethod()) {
7793            // Always select all on a password field.
7794            // Cut/copy menu entries are not available for passwords, but being able to select all
7795            // is however useful to delete or paste to replace the entire content.
7796            return selectAll();
7797        }
7798
7799        int klass = mInputType & InputType.TYPE_MASK_CLASS;
7800        int variation = mInputType & InputType.TYPE_MASK_VARIATION;
7801
7802        // Specific text field types: select the entire text for these
7803        if (klass == InputType.TYPE_CLASS_NUMBER ||
7804                klass == InputType.TYPE_CLASS_PHONE ||
7805                klass == InputType.TYPE_CLASS_DATETIME ||
7806                variation == InputType.TYPE_TEXT_VARIATION_URI ||
7807                variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
7808                variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ||
7809                variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
7810            return selectAll();
7811        }
7812
7813        long lastTouchOffsets = getLastTouchOffsets();
7814        final int minOffset = extractRangeStartFromLong(lastTouchOffsets);
7815        final int maxOffset = extractRangeEndFromLong(lastTouchOffsets);
7816
7817        int selectionStart, selectionEnd;
7818
7819        // If a URLSpan (web address, email, phone...) is found at that position, select it.
7820        URLSpan[] urlSpans = ((Spanned) mText).getSpans(minOffset, maxOffset, URLSpan.class);
7821        if (urlSpans.length == 1) {
7822            URLSpan url = urlSpans[0];
7823            selectionStart = ((Spanned) mText).getSpanStart(url);
7824            selectionEnd = ((Spanned) mText).getSpanEnd(url);
7825        } else {
7826            if (mWordIterator == null) {
7827                mWordIterator = new WordIterator();
7828            }
7829            // WordIerator handles text changes, this is a no-op if text in unchanged.
7830            mWordIterator.setCharSequence(mText);
7831
7832            selectionStart = mWordIterator.getBeginning(minOffset);
7833            if (selectionStart == BreakIterator.DONE) return false;
7834
7835            selectionEnd = mWordIterator.getEnd(maxOffset);
7836            if (selectionEnd == BreakIterator.DONE) return false;
7837        }
7838
7839        Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
7840        return true;
7841    }
7842
7843    private long getLastTouchOffsets() {
7844        int minOffset, maxOffset;
7845
7846        if (mContextMenuTriggeredByKey) {
7847            minOffset = getSelectionStart();
7848            maxOffset = getSelectionEnd();
7849        } else {
7850            SelectionModifierCursorController selectionController = getSelectionController();
7851            minOffset = selectionController.getMinTouchOffset();
7852            maxOffset = selectionController.getMaxTouchOffset();
7853        }
7854
7855        return packRangeInLong(minOffset, maxOffset);
7856    }
7857
7858    @Override
7859    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
7860        super.onPopulateAccessibilityEvent(event);
7861
7862        final boolean isPassword = hasPasswordTransformationMethod();
7863        if (!isPassword) {
7864            CharSequence text = getText();
7865            if (TextUtils.isEmpty(text)) {
7866                text = getHint();
7867            }
7868            if (!TextUtils.isEmpty(text)) {
7869                event.getText().add(text);
7870            }
7871        }
7872    }
7873
7874    @Override
7875    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
7876        super.onInitializeAccessibilityEvent(event);
7877
7878        final boolean isPassword = hasPasswordTransformationMethod();
7879        event.setPassword(isPassword);
7880    }
7881
7882    void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText,
7883            int fromIndex, int removedCount, int addedCount) {
7884        AccessibilityEvent event =
7885            AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
7886        event.setFromIndex(fromIndex);
7887        event.setRemovedCount(removedCount);
7888        event.setAddedCount(addedCount);
7889        event.setBeforeText(beforeText);
7890        sendAccessibilityEventUnchecked(event);
7891    }
7892
7893    @Override
7894    protected void onCreateContextMenu(ContextMenu menu) {
7895        super.onCreateContextMenu(menu);
7896        boolean added = false;
7897        mContextMenuTriggeredByKey = mDPadCenterIsDown || mEnterKeyIsDown;
7898        // Problem with context menu on long press: the menu appears while the key in down and when
7899        // the key is released, the view does not receive the key_up event.
7900        // We need two layers of flags: mDPadCenterIsDown and mEnterKeyIsDown are set in key down/up
7901        // events. We cannot simply clear these flags in onTextContextMenuItem since
7902        // it may not be called (if the user/ discards the context menu with the back key).
7903        // We clear these flags here and mContextMenuTriggeredByKey saves that state so that it is
7904        // available in onTextContextMenuItem.
7905        mDPadCenterIsDown = mEnterKeyIsDown = false;
7906
7907        MenuHandler handler = new MenuHandler();
7908
7909        if (mText instanceof Spanned && hasSelectionController()) {
7910            long lastTouchOffset = getLastTouchOffsets();
7911            final int selStart = extractRangeStartFromLong(lastTouchOffset);
7912            final int selEnd = extractRangeEndFromLong(lastTouchOffset);
7913
7914            URLSpan[] urls = ((Spanned) mText).getSpans(selStart, selEnd, URLSpan.class);
7915            if (urls.length > 0) {
7916                menu.add(0, ID_COPY_URL, 0, com.android.internal.R.string.copyUrl).
7917                        setOnMenuItemClickListener(handler);
7918
7919                added = true;
7920            }
7921        }
7922
7923        // The context menu is not empty, which will prevent the selection mode from starting.
7924        // Add a entry to start it in the context menu.
7925        // TODO Does not handle the case where a subclass does not call super.thisMethod or
7926        // populates the menu AFTER this call.
7927        if (menu.size() > 0) {
7928            menu.add(0, ID_SELECTION_MODE, 0, com.android.internal.R.string.selectTextMode).
7929                    setOnMenuItemClickListener(handler);
7930            added = true;
7931        }
7932
7933        if (added) {
7934            menu.setHeaderTitle(com.android.internal.R.string.editTextMenuTitle);
7935        }
7936    }
7937
7938    /**
7939     * Returns whether this text view is a current input method target.  The
7940     * default implementation just checks with {@link InputMethodManager}.
7941     */
7942    public boolean isInputMethodTarget() {
7943        InputMethodManager imm = InputMethodManager.peekInstance();
7944        return imm != null && imm.isActive(this);
7945    }
7946
7947    // Selection context mode
7948    private static final int ID_SELECT_ALL = android.R.id.selectAll;
7949    private static final int ID_CUT = android.R.id.cut;
7950    private static final int ID_COPY = android.R.id.copy;
7951    private static final int ID_PASTE = android.R.id.paste;
7952    // Context menu entries
7953    private static final int ID_COPY_URL = android.R.id.copyUrl;
7954    private static final int ID_SELECTION_MODE = android.R.id.selectTextMode;
7955
7956    private class MenuHandler implements MenuItem.OnMenuItemClickListener {
7957        public boolean onMenuItemClick(MenuItem item) {
7958            return onTextContextMenuItem(item.getItemId());
7959        }
7960    }
7961
7962    /**
7963     * Called when a context menu option for the text view is selected.  Currently
7964     * this will be {@link android.R.id#copyUrl}, {@link android.R.id#selectTextMode},
7965     * {@link android.R.id#selectAll}, {@link android.R.id#paste}, {@link android.R.id#cut}
7966     * or {@link android.R.id#copy}.
7967     *
7968     * @return true if the context menu item action was performed.
7969     */
7970    public boolean onTextContextMenuItem(int id) {
7971        int min = 0;
7972        int max = mText.length();
7973
7974        if (isFocused()) {
7975            final int selStart = getSelectionStart();
7976            final int selEnd = getSelectionEnd();
7977
7978            min = Math.max(0, Math.min(selStart, selEnd));
7979            max = Math.max(0, Math.max(selStart, selEnd));
7980        }
7981
7982        switch (id) {
7983            case ID_COPY_URL:
7984                URLSpan[] urls = ((Spanned) mText).getSpans(min, max, URLSpan.class);
7985                if (urls.length >= 1) {
7986                    ClipData clip = null;
7987                    for (int i=0; i<urls.length; i++) {
7988                        Uri uri = Uri.parse(urls[0].getURL());
7989                        if (clip == null) {
7990                            clip = ClipData.newRawUri(null, uri);
7991                        } else {
7992                            clip.addItem(new ClipData.Item(uri));
7993                        }
7994                    }
7995                    if (clip != null) {
7996                        setPrimaryClip(clip);
7997                    }
7998                }
7999                stopSelectionActionMode();
8000                return true;
8001
8002            case ID_SELECTION_MODE:
8003                if (mSelectionActionMode != null) {
8004                    // Selection mode is already started, simply change selected part.
8005                    selectCurrentWord();
8006                } else {
8007                    startSelectionActionMode();
8008                }
8009                return true;
8010
8011            case ID_SELECT_ALL:
8012                // This does not enter text selection mode. Text is highlighted, so that it can be
8013                // bulk edited, like selectAllOnFocus does. Returns true even if text is empty.
8014                selectAll();
8015                return true;
8016
8017            case ID_PASTE:
8018                paste(min, max);
8019                return true;
8020
8021            case ID_CUT:
8022                setPrimaryClip(ClipData.newPlainText(null, mTransformed.subSequence(min, max)));
8023                ((Editable) mText).delete(min, max);
8024                stopSelectionActionMode();
8025                return true;
8026
8027            case ID_COPY:
8028                setPrimaryClip(ClipData.newPlainText(null, mTransformed.subSequence(min, max)));
8029                stopSelectionActionMode();
8030                return true;
8031        }
8032        return false;
8033    }
8034
8035    /**
8036     * Prepare text so that there are not zero or two spaces at beginning and end of region defined
8037     * by [min, max] when replacing this region by paste.
8038     * Note that if there were two spaces (or more) at that position before, they are kept. We just
8039     * make sure we do not add an extra one from the paste content.
8040     */
8041    private long prepareSpacesAroundPaste(int min, int max, CharSequence paste) {
8042        if (paste.length() > 0) {
8043            if (min > 0) {
8044                final char charBefore = mTransformed.charAt(min - 1);
8045                final char charAfter = paste.charAt(0);
8046
8047                if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
8048                    // Two spaces at beginning of paste: remove one
8049                    final int originalLength = mText.length();
8050                    ((Editable) mText).delete(min - 1, min);
8051                    // Due to filters, there is no guarantee that exactly one character was
8052                    // removed: count instead.
8053                    final int delta = mText.length() - originalLength;
8054                    min += delta;
8055                    max += delta;
8056                } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
8057                        !Character.isSpaceChar(charAfter) && charAfter != '\n') {
8058                    // No space at beginning of paste: add one
8059                    final int originalLength = mText.length();
8060                    ((Editable) mText).replace(min, min, " ");
8061                    // Taking possible filters into account as above.
8062                    final int delta = mText.length() - originalLength;
8063                    min += delta;
8064                    max += delta;
8065                }
8066            }
8067
8068            if (max < mText.length()) {
8069                final char charBefore = paste.charAt(paste.length() - 1);
8070                final char charAfter = mTransformed.charAt(max);
8071
8072                if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
8073                    // Two spaces at end of paste: remove one
8074                    ((Editable) mText).delete(max, max + 1);
8075                } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
8076                        !Character.isSpaceChar(charAfter) && charAfter != '\n') {
8077                    // No space at end of paste: add one
8078                    ((Editable) mText).replace(max, max, " ");
8079                }
8080            }
8081        }
8082
8083        return packRangeInLong(min, max);
8084    }
8085
8086    private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) {
8087        TextView shadowView = (TextView) inflate(mContext,
8088                com.android.internal.R.layout.text_drag_thumbnail, null);
8089
8090        if (shadowView == null) {
8091            throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
8092        }
8093
8094        if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) {
8095            text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH);
8096        }
8097        shadowView.setText(text);
8098        shadowView.setTextColor(getTextColors());
8099
8100        shadowView.setTextAppearance(mContext, R.styleable.Theme_textAppearanceLarge);
8101        shadowView.setGravity(Gravity.CENTER);
8102
8103        shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
8104                ViewGroup.LayoutParams.WRAP_CONTENT));
8105
8106        final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
8107        shadowView.measure(size, size);
8108
8109        shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
8110        shadowView.invalidate();
8111        return new DragShadowBuilder(shadowView);
8112    }
8113
8114    private static class DragLocalState {
8115        public TextView sourceTextView;
8116        public int start, end;
8117
8118        public DragLocalState(TextView sourceTextView, int start, int end) {
8119            this.sourceTextView = sourceTextView;
8120            this.start = start;
8121            this.end = end;
8122        }
8123    }
8124
8125    @Override
8126    public boolean performLongClick() {
8127        if (super.performLongClick()) {
8128            mDiscardNextActionUp = true;
8129            return true;
8130        }
8131
8132        boolean handled = false;
8133
8134        // Long press in empty space moves cursor and shows the Paste affordance if available.
8135        if (!isPositionOnText(mLastDownPositionX, mLastDownPositionY) &&
8136                mInsertionControllerEnabled) {
8137            final int offset = getOffsetForPosition(mLastDownPositionX, mLastDownPositionY);
8138            stopSelectionActionMode();
8139            Selection.setSelection((Spannable) mText, offset);
8140            getInsertionController().showWithPaste();
8141            handled = true;
8142        }
8143
8144        if (!handled && mSelectionActionMode != null) {
8145            if (touchPositionIsInSelection()) {
8146                // Start a drag
8147                final int start = getSelectionStart();
8148                final int end = getSelectionEnd();
8149                CharSequence selectedText = mTransformed.subSequence(start, end);
8150                ClipData data = ClipData.newPlainText(null, selectedText);
8151                DragLocalState localState = new DragLocalState(this, start, end);
8152                startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0);
8153                stopSelectionActionMode();
8154            } else {
8155                selectCurrentWord();
8156            }
8157            handled = true;
8158        }
8159
8160        // Start a new selection
8161        handled |= !handled && startSelectionActionMode();
8162
8163        if (handled) {
8164            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
8165            mDiscardNextActionUp = true;
8166        }
8167
8168        return handled;
8169    }
8170
8171    private boolean touchPositionIsInSelection() {
8172        int selectionStart = getSelectionStart();
8173        int selectionEnd = getSelectionEnd();
8174
8175        if (selectionStart == selectionEnd) {
8176            return false;
8177        }
8178
8179        if (selectionStart > selectionEnd) {
8180            int tmp = selectionStart;
8181            selectionStart = selectionEnd;
8182            selectionEnd = tmp;
8183            Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
8184        }
8185
8186        SelectionModifierCursorController selectionController = getSelectionController();
8187        int minOffset = selectionController.getMinTouchOffset();
8188        int maxOffset = selectionController.getMaxTouchOffset();
8189
8190        return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
8191    }
8192
8193    private static class SuggestionRangeSpan extends UnderlineSpan {
8194        // TODO themable, would be nice to make it a child class of TextAppearanceSpan, but
8195        // there is no way to have underline and TextAppearanceSpan.
8196    }
8197
8198    private class SuggestionsPopupWindow implements OnClickListener {
8199        private static final int MAX_NUMBER_SUGGESTIONS = 5;
8200        private static final int NO_SUGGESTIONS = -1;
8201        private final PopupWindow mContainer;
8202        private final ViewGroup[] mSuggestionViews = new ViewGroup[2];
8203        private final int[] mSuggestionViewLayouts = new int[] {
8204                mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout};
8205        private WordIterator mSuggestionWordIterator;
8206        private TextAppearanceSpan[] mHighlightSpans = new TextAppearanceSpan[0];
8207
8208        public SuggestionsPopupWindow() {
8209            mContainer = new PopupWindow(TextView.this.mContext, null,
8210                    com.android.internal.R.attr.textSuggestionsWindowStyle);
8211            mContainer.setSplitTouchEnabled(true);
8212            mContainer.setClippingEnabled(false);
8213            mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
8214
8215            mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
8216            mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
8217        }
8218
8219        private class SuggestionInfo {
8220            int suggestionStart, suggestionEnd; // range of suggestion item with replacement text
8221            int spanStart, spanEnd; // range in TextView where text should be inserted
8222            SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents
8223            int suggestionIndex; // the index of the suggestion inside suggestionSpan
8224        }
8225
8226        private ViewGroup getViewGroup(boolean under) {
8227            final int viewIndex = under ? 0 : 1;
8228            ViewGroup viewGroup = mSuggestionViews[viewIndex];
8229
8230            if (viewGroup == null) {
8231                final int layout = mSuggestionViewLayouts[viewIndex];
8232                LayoutInflater inflater = (LayoutInflater) TextView.this.mContext.
8233                        getSystemService(Context.LAYOUT_INFLATER_SERVICE);
8234
8235                if (inflater == null) {
8236                    throw new IllegalArgumentException(
8237                            "Unable to create TextEdit suggestion window inflater");
8238                }
8239
8240                View view = inflater.inflate(layout, null);
8241
8242                if (! (view instanceof ViewGroup)) {
8243                    throw new IllegalArgumentException(
8244                            "Inflated TextEdit suggestion window is not a ViewGroup: " + view);
8245                }
8246
8247                viewGroup = (ViewGroup) view;
8248
8249                // Inflate the suggestion items once and for all.
8250                for (int i = 0; i < MAX_NUMBER_SUGGESTIONS; i++) {
8251                    View childView = inflater.inflate(mTextEditSuggestionItemLayout, viewGroup,
8252                            false);
8253
8254                    if (! (childView instanceof TextView)) {
8255                        throw new IllegalArgumentException(
8256                               "Inflated TextEdit suggestion item is not a TextView: " + childView);
8257                    }
8258
8259                    childView.setTag(new SuggestionInfo());
8260                    viewGroup.addView(childView);
8261                    childView.setOnClickListener(this);
8262                }
8263
8264                mSuggestionViews[viewIndex] = viewGroup;
8265            }
8266
8267            return viewGroup;
8268        }
8269
8270        public void show() {
8271            if (!(mText instanceof Editable)) return;
8272
8273            final int pos = TextView.this.getSelectionStart();
8274            Spannable spannable = (Spannable)TextView.this.mText;
8275            SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
8276            final int nbSpans = suggestionSpans.length;
8277
8278            ViewGroup viewGroup = getViewGroup(true);
8279            mContainer.setContentView(viewGroup);
8280
8281            int totalNbSuggestions = 0;
8282            int spanUnionStart = mText.length();
8283            int spanUnionEnd = 0;
8284
8285            for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) {
8286                SuggestionSpan suggestionSpan = suggestionSpans[spanIndex];
8287                final int spanStart = spannable.getSpanStart(suggestionSpan);
8288                final int spanEnd = spannable.getSpanEnd(suggestionSpan);
8289                spanUnionStart = Math.min(spanStart, spanUnionStart);
8290                spanUnionEnd = Math.max(spanEnd, spanUnionEnd);
8291
8292                String[] suggestions = suggestionSpan.getSuggestions();
8293                int nbSuggestions = suggestions.length;
8294                for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
8295                    TextView textView = (TextView) viewGroup.getChildAt(totalNbSuggestions);
8296                    textView.setText(suggestions[suggestionIndex]);
8297                    SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag();
8298                    suggestionInfo.spanStart = spanStart;
8299                    suggestionInfo.spanEnd = spanEnd;
8300                    suggestionInfo.suggestionSpan = suggestionSpan;
8301                    suggestionInfo.suggestionIndex = suggestionIndex;
8302
8303                    totalNbSuggestions++;
8304                    if (totalNbSuggestions == MAX_NUMBER_SUGGESTIONS) {
8305                        // Also end outer for loop
8306                        spanIndex = nbSpans;
8307                        break;
8308                    }
8309                }
8310            }
8311
8312            if (totalNbSuggestions == 0) {
8313                // TODO Replace by final text, use a dedicated layout, add a fade out timer...
8314                TextView textView = (TextView) viewGroup.getChildAt(0);
8315                textView.setText("No suggestions available");
8316                SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag();
8317                suggestionInfo.spanStart = NO_SUGGESTIONS;
8318                totalNbSuggestions++;
8319            } else {
8320                if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
8321                ((Editable) mText).setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
8322                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
8323
8324                for (int i = 0; i < totalNbSuggestions; i++) {
8325                    final TextView textView = (TextView) viewGroup.getChildAt(i);
8326                    highlightTextDifferences(textView, spanUnionStart, spanUnionEnd);
8327                }
8328            }
8329
8330            for (int i = 0; i < MAX_NUMBER_SUGGESTIONS; i++) {
8331                viewGroup.getChildAt(i).setVisibility(i < totalNbSuggestions ? VISIBLE : GONE);
8332            }
8333
8334            final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
8335            viewGroup.measure(size, size);
8336
8337            positionAtCursor();
8338        }
8339
8340        private long[] getWordLimits(CharSequence text) {
8341            // TODO locale for mSuggestionWordIterator
8342            if (mSuggestionWordIterator == null) mSuggestionWordIterator = new WordIterator();
8343            mSuggestionWordIterator.setCharSequence(text);
8344
8345            // First pass will simply count the number of words to be able to create an array
8346            // Not too expensive since previous break positions are cached by the BreakIterator
8347            int nbWords = 0;
8348            int position = mSuggestionWordIterator.following(0);
8349            while (position != BreakIterator.DONE) {
8350                nbWords++;
8351                position = mSuggestionWordIterator.following(position);
8352            }
8353
8354            int index = 0;
8355            long[] result = new long[nbWords];
8356
8357            position = mSuggestionWordIterator.following(0);
8358            while (position != BreakIterator.DONE) {
8359                int wordStart = mSuggestionWordIterator.getBeginning(position);
8360                result[index++] = packRangeInLong(wordStart, position);
8361                position = mSuggestionWordIterator.following(position);
8362            }
8363
8364            return result;
8365        }
8366
8367        private TextAppearanceSpan highlightSpan(int index) {
8368            final int length = mHighlightSpans.length;
8369            if (index < length) {
8370                return mHighlightSpans[index];
8371            }
8372
8373            // Assumes indexes are requested in sequence: simply append one more item
8374            TextAppearanceSpan[] newArray = new TextAppearanceSpan[length + 1];
8375            System.arraycopy(mHighlightSpans, 0, newArray, 0, length);
8376            TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mContext,
8377                    android.R.style.TextAppearance_SuggestionHighlight);
8378            newArray[length] = highlightSpan;
8379            mHighlightSpans = newArray;
8380            return highlightSpan;
8381        }
8382
8383        private void highlightTextDifferences(TextView textView, int unionStart, int unionEnd) {
8384            SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag();
8385            final int spanStart = suggestionInfo.spanStart;
8386            final int spanEnd = suggestionInfo.spanEnd;
8387
8388            // Remove all text formating by converting to Strings
8389            final String text = textView.getText().toString();
8390            final String sourceText = mText.subSequence(spanStart, spanEnd).toString();
8391
8392            long[] sourceWordLimits = getWordLimits(sourceText);
8393            long[] wordLimits = getWordLimits(text);
8394
8395            SpannableStringBuilder ssb = new SpannableStringBuilder();
8396            // span [spanStart, spanEnd] is included in union [spanUnionStart, int spanUnionEnd]
8397            // The final result is made of 3 parts: the text before, between and after the span
8398            // This is the text before, provided for context
8399            ssb.append(mText.subSequence(unionStart, spanStart).toString());
8400
8401            // shift is used to offset spans positions wrt span's beginning
8402            final int shift = spanStart - unionStart;
8403            suggestionInfo.suggestionStart = shift;
8404            suggestionInfo.suggestionEnd = shift + text.length();
8405
8406            // This is the actual suggestion text, which will be highlighted by the following code
8407            ssb.append(text);
8408
8409            String[] words = new String[wordLimits.length];
8410            for (int i = 0; i < wordLimits.length; i++) {
8411                int wordStart = extractRangeStartFromLong(wordLimits[i]);
8412                int wordEnd = extractRangeEndFromLong(wordLimits[i]);
8413                words[i] = text.substring(wordStart, wordEnd);
8414            }
8415
8416            // Highlighted word algorithm is based on word matching between source and text
8417            // Matching words are found from left to right. TODO: change for RTL languages
8418            // Characters between matching words are highlighted
8419            int previousCommonWordIndex = -1;
8420            int nbHighlightSpans = 0;
8421            for (int i = 0; i < sourceWordLimits.length; i++) {
8422                int wordStart = extractRangeStartFromLong(sourceWordLimits[i]);
8423                int wordEnd = extractRangeEndFromLong(sourceWordLimits[i]);
8424                String sourceWord = sourceText.substring(wordStart, wordEnd);
8425
8426                for (int j = previousCommonWordIndex + 1; j < words.length; j++) {
8427                    if (sourceWord.equals(words[j])) {
8428                        if (j != previousCommonWordIndex + 1) {
8429                            int firstDifferentPosition = previousCommonWordIndex < 0 ? 0 :
8430                                extractRangeEndFromLong(wordLimits[previousCommonWordIndex]);
8431                            int lastDifferentPosition = extractRangeStartFromLong(wordLimits[j]);
8432                            ssb.setSpan(highlightSpan(nbHighlightSpans++),
8433                                    shift + firstDifferentPosition, shift + lastDifferentPosition,
8434                                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
8435                        } else {
8436                            // Compare characters between words
8437                            int previousSourceWordEnd = i == 0 ? 0 :
8438                                extractRangeEndFromLong(sourceWordLimits[i - 1]);
8439                            int sourceWordStart = extractRangeStartFromLong(sourceWordLimits[i]);
8440                            String sourceSpaces = sourceText.substring(previousSourceWordEnd,
8441                                    sourceWordStart);
8442
8443                            int previousWordEnd = j == 0 ? 0 :
8444                                extractRangeEndFromLong(wordLimits[j - 1]);
8445                            int currentWordStart = extractRangeStartFromLong(wordLimits[j]);
8446                            String textSpaces = text.substring(previousWordEnd, currentWordStart);
8447
8448                            if (!sourceSpaces.equals(textSpaces)) {
8449                                ssb.setSpan(highlightSpan(nbHighlightSpans++),
8450                                        shift + previousWordEnd, shift + currentWordStart,
8451                                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
8452                            }
8453                        }
8454                        previousCommonWordIndex = j;
8455                        break;
8456                    }
8457                }
8458            }
8459
8460            // Finally, compare ends of Strings
8461            if (previousCommonWordIndex < words.length - 1) {
8462                int firstDifferentPosition = previousCommonWordIndex < 0 ? 0 :
8463                    extractRangeEndFromLong(wordLimits[previousCommonWordIndex]);
8464                int lastDifferentPosition = textView.length();
8465                ssb.setSpan(highlightSpan(nbHighlightSpans++),
8466                        shift + firstDifferentPosition, shift + lastDifferentPosition,
8467                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
8468            } else {
8469                int lastSourceWordEnd = sourceWordLimits.length == 0 ? 0 :
8470                    extractRangeEndFromLong(sourceWordLimits[sourceWordLimits.length - 1]);
8471                String sourceSpaces = sourceText.substring(lastSourceWordEnd, sourceText.length());
8472
8473                int lastCommonTextWordEnd = previousCommonWordIndex < 0 ? 0 :
8474                    extractRangeEndFromLong(wordLimits[previousCommonWordIndex]);
8475                String textSpaces = text.substring(lastCommonTextWordEnd, textView.length());
8476
8477                if (!sourceSpaces.equals(textSpaces) && textSpaces.length() > 0) {
8478                    ssb.setSpan(highlightSpan(nbHighlightSpans++),
8479                            shift + lastCommonTextWordEnd, shift + textView.length(),
8480                            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
8481                }
8482            }
8483
8484            // Final part, text after the current suggestion range.
8485            ssb.append(mText.subSequence(spanEnd, unionEnd).toString());
8486            textView.setText(ssb);
8487        }
8488
8489        public void hide() {
8490            if ((mText instanceof Editable) && mSuggestionRangeSpan != null) {
8491                ((Editable) mText).removeSpan(mSuggestionRangeSpan);
8492            }
8493            mContainer.dismiss();
8494        }
8495
8496        @Override
8497        public void onClick(View view) {
8498            if (view instanceof TextView) {
8499                TextView textView = (TextView) view;
8500                SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag();
8501                final int spanStart = suggestionInfo.spanStart;
8502                final int spanEnd = suggestionInfo.spanEnd;
8503                if (spanStart != NO_SUGGESTIONS) {
8504                    // SuggestionSpans are removed by replace: save them before
8505                    Editable editable = ((Editable) mText);
8506                    SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
8507                            SuggestionSpan.class);
8508                    final int length = suggestionSpans.length;
8509                    int[] suggestionSpansStarts = new int[length];
8510                    int[] suggestionSpansEnds = new int[length];
8511                    int[] suggestionSpansFlags = new int[length];
8512                    for (int i = 0; i < length; i++) {
8513                        final SuggestionSpan suggestionSpan = suggestionSpans[i];
8514                        suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
8515                        suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
8516                        suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
8517                    }
8518
8519                    final int suggestionStart = suggestionInfo.suggestionStart;
8520                    final int suggestionEnd = suggestionInfo.suggestionEnd;
8521                    final String suggestion = textView.getText().subSequence(
8522                            suggestionStart, suggestionEnd).toString();
8523                    final String originalText = mText.subSequence(spanStart, spanEnd).toString();
8524                    ((Editable) mText).replace(spanStart, spanEnd, suggestion);
8525
8526                    // Swap text content between actual text and Suggestion span
8527                    String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions();
8528                    suggestions[suggestionInfo.suggestionIndex] = originalText;
8529
8530                    // Notify source IME of the suggestion pick
8531                    if (!TextUtils.isEmpty(
8532                            suggestionInfo.suggestionSpan.getNotificationTargetClassName())) {
8533                        InputMethodManager imm = InputMethodManager.peekInstance();
8534                        imm.notifySuggestionPicked(suggestionInfo.suggestionSpan, originalText,
8535                                suggestionInfo.suggestionIndex);
8536                    }
8537
8538                    // Restore previous SuggestionSpans
8539                    final int lengthDifference = suggestion.length() - (spanEnd - spanStart);
8540                    for (int i = 0; i < length; i++) {
8541                        // Only spans that include the modified region make sense after replacement
8542                        // Spans partially included in the replaced region are removed, there is no
8543                        // way to assign them a valid range after replacement
8544                        if (suggestionSpansStarts[i] <= spanStart &&
8545                                suggestionSpansEnds[i] >= spanEnd) {
8546                            editable.setSpan(suggestionSpans[i], suggestionSpansStarts[i],
8547                                    suggestionSpansEnds[i] + lengthDifference,
8548                                    suggestionSpansFlags[i]);
8549                        }
8550                    }
8551                }
8552            }
8553            hide();
8554        }
8555
8556        void positionAtCursor() {
8557            View contentView = mContainer.getContentView();
8558            int width = contentView.getMeasuredWidth();
8559            int height = contentView.getMeasuredHeight();
8560            final int offset = TextView.this.getSelectionStart();
8561            final int line = mLayout.getLineForOffset(offset);
8562            final int lineBottom = mLayout.getLineBottom(line);
8563            float primaryHorizontal = mLayout.getPrimaryHorizontal(offset);
8564
8565            final Rect bounds = sCursorControllerTempRect;
8566            bounds.left = (int) (primaryHorizontal - width / 2.0f);
8567            bounds.top = lineBottom;
8568
8569            bounds.right = bounds.left + width;
8570            bounds.bottom = bounds.top + height;
8571
8572            convertFromViewportToContentCoordinates(bounds);
8573
8574            final int[] coords = mTempCoords;
8575            TextView.this.getLocationInWindow(coords);
8576            coords[0] += bounds.left;
8577            coords[1] += bounds.top;
8578
8579            final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
8580            final int screenHeight = displayMetrics.heightPixels;
8581
8582            // Vertical clipping
8583            if (coords[1] + height > screenHeight) {
8584                // Try to position above current line instead
8585                // TODO use top layout instead, reverse suggestion order,
8586                // try full screen vertical down if it still does not fit. TBD with designers.
8587
8588                // Update dimensions from new view
8589                contentView = mContainer.getContentView();
8590                width = contentView.getMeasuredWidth();
8591                height = contentView.getMeasuredHeight();
8592
8593                final int lineTop = mLayout.getLineTop(line);
8594                final int lineHeight = lineBottom - lineTop;
8595                coords[1] -= height + lineHeight;
8596            }
8597
8598            // Horizontal clipping
8599            coords[0] = Math.max(0, coords[0]);
8600            coords[0] = Math.min(displayMetrics.widthPixels - width, coords[0]);
8601
8602            mContainer.showAtLocation(TextView.this, Gravity.NO_GRAVITY, coords[0], coords[1]);
8603        }
8604    }
8605
8606    void showSuggestions() {
8607        if (!mSuggestionsEnabled || !isTextEditable()) return;
8608
8609        if (mSuggestionsPopupWindow == null) {
8610            mSuggestionsPopupWindow = new SuggestionsPopupWindow();
8611        }
8612        hideControllers();
8613        mSuggestionsPopupWindow.show();
8614    }
8615
8616    void hideSuggestions() {
8617        if (mSuggestionsPopupWindow != null) {
8618            mSuggestionsPopupWindow.hide();
8619        }
8620    }
8621
8622    /**
8623     * Some parts of the text can have alternate suggestion text attached. This is typically done by
8624     * the IME by adding {@link SuggestionSpan}s to the text.
8625     *
8626     * When suggestions are enabled (default), this list of suggestions will be displayed when the
8627     * user double taps on these parts of the text. No suggestions are displayed when this value is
8628     * false. Use {@link #setSuggestionsEnabled(boolean)} to change this value.
8629     *
8630     * @return true if the suggestions popup window is enabled.
8631     *
8632     * @attr ref android.R.styleable#TextView_suggestionsEnabled
8633     */
8634    public boolean isSuggestionsEnabled() {
8635        return mSuggestionsEnabled;
8636    }
8637
8638    /**
8639     * Enables or disables the suggestion popup. See {@link #isSuggestionsEnabled()}.
8640     *
8641     * @param enabled Whether or not suggestions are enabled.
8642     */
8643    public void setSuggestionsEnabled(boolean enabled) {
8644        mSuggestionsEnabled = enabled;
8645    }
8646
8647    /**
8648     * If provided, this ActionMode.Callback will be used to create the ActionMode when text
8649     * selection is initiated in this View.
8650     *
8651     * The standard implementation populates the menu with a subset of Select All, Cut, Copy and
8652     * Paste actions, depending on what this View supports.
8653     *
8654     * A custom implementation can add new entries in the default menu in its
8655     * {@link ActionMode.Callback#onPrepareActionMode(ActionMode, Menu)} method. The default actions
8656     * can also be removed from the menu using {@link Menu#removeItem(int)} and passing
8657     * {@link android.R.id#selectAll}, {@link android.R.id#cut}, {@link android.R.id#copy} or
8658     * {@link android.R.id#paste} ids as parameters.
8659     *
8660     * Returning false from {@link ActionMode.Callback#onCreateActionMode(ActionMode, Menu)} will
8661     * prevent the action mode from being started.
8662     *
8663     * Action click events should be handled by the custom implementation of
8664     * {@link ActionMode.Callback#onActionItemClicked(ActionMode, MenuItem)}.
8665     *
8666     * Note that text selection mode is not started when a TextView receives focus and the
8667     * {@link android.R.attr#selectAllOnFocus} flag has been set. The content is highlighted in
8668     * that case, to allow for quick replacement.
8669     */
8670    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
8671        mCustomSelectionActionModeCallback = actionModeCallback;
8672    }
8673
8674    /**
8675     * Retrieves the value set in {@link #setCustomSelectionActionModeCallback}. Default is null.
8676     *
8677     * @return The current custom selection callback.
8678     */
8679    public ActionMode.Callback getCustomSelectionActionModeCallback() {
8680        return mCustomSelectionActionModeCallback;
8681    }
8682
8683    /**
8684     *
8685     * @return true if the selection mode was actually started.
8686     */
8687    private boolean startSelectionActionMode() {
8688        if (mSelectionActionMode != null) {
8689            // Selection action mode is already started
8690            return false;
8691        }
8692
8693        if (!canSelectText() || !requestFocus()) {
8694            Log.w(LOG_TAG, "TextView does not support text selection. Action mode cancelled.");
8695            return false;
8696        }
8697
8698        boolean currentWordSelected = selectCurrentWord();
8699        if (!currentWordSelected) {
8700            // No word found under cursor or text selection not permitted.
8701            return false;
8702        }
8703
8704        ActionMode.Callback actionModeCallback = new SelectionActionModeCallback();
8705        mSelectionActionMode = startActionMode(actionModeCallback);
8706        final boolean selectionStarted = mSelectionActionMode != null;
8707
8708        if (selectionStarted && !mTextIsSelectable) {
8709            // Show the IME to be able to replace text, except when selecting non editable text.
8710            final InputMethodManager imm = InputMethodManager.peekInstance();
8711            if (imm != null) imm.showSoftInput(this, 0, null);
8712        }
8713
8714        return selectionStarted;
8715    }
8716
8717    private void stopSelectionActionMode() {
8718        if (mSelectionActionMode != null) {
8719            // This will hide the mSelectionModifierCursorController
8720            mSelectionActionMode.finish();
8721        }
8722    }
8723
8724    /**
8725     * Paste clipboard content between min and max positions.
8726     */
8727    private void paste(int min, int max) {
8728        ClipboardManager clipboard =
8729            (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
8730        ClipData clip = clipboard.getPrimaryClip();
8731        if (clip != null) {
8732            boolean didFirst = false;
8733            for (int i=0; i<clip.getItemCount(); i++) {
8734                CharSequence paste = clip.getItemAt(i).coerceToText(getContext());
8735                if (paste != null) {
8736                    if (!didFirst) {
8737                        long minMax = prepareSpacesAroundPaste(min, max, paste);
8738                        min = extractRangeStartFromLong(minMax);
8739                        max = extractRangeEndFromLong(minMax);
8740                        Selection.setSelection((Spannable) mText, max);
8741                        ((Editable) mText).replace(min, max, paste);
8742                        didFirst = true;
8743                    } else {
8744                        ((Editable) mText).insert(getSelectionEnd(), "\n");
8745                        ((Editable) mText).insert(getSelectionEnd(), paste);
8746                    }
8747                }
8748            }
8749            stopSelectionActionMode();
8750            sLastCutOrCopyTime = 0;
8751        }
8752    }
8753
8754    private void setPrimaryClip(ClipData clip) {
8755        ClipboardManager clipboard = (ClipboardManager) getContext().
8756                getSystemService(Context.CLIPBOARD_SERVICE);
8757        clipboard.setPrimaryClip(clip);
8758        sLastCutOrCopyTime = SystemClock.uptimeMillis();
8759    }
8760
8761    /**
8762     * An ActionMode Callback class that is used to provide actions while in text selection mode.
8763     *
8764     * The default callback provides a subset of Select All, Cut, Copy and Paste actions, depending
8765     * on which of these this TextView supports.
8766     */
8767    private class SelectionActionModeCallback implements ActionMode.Callback {
8768
8769        @Override
8770        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
8771            TypedArray styledAttributes = mContext.obtainStyledAttributes(R.styleable.Theme);
8772
8773            mode.setTitle(mContext.getString(com.android.internal.R.string.textSelectionCABTitle));
8774            mode.setSubtitle(null);
8775
8776            menu.add(0, ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll).
8777                    setAlphabeticShortcut('a').
8778                    setShowAsAction(
8779                            MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
8780
8781            if (canCut()) {
8782                menu.add(0, ID_CUT, 0, com.android.internal.R.string.cut).
8783                    setIcon(styledAttributes.getResourceId(
8784                            R.styleable.Theme_actionModeCutDrawable, 0)).
8785                    setAlphabeticShortcut('x').
8786                    setShowAsAction(
8787                            MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
8788            }
8789
8790            if (canCopy()) {
8791                menu.add(0, ID_COPY, 0, com.android.internal.R.string.copy).
8792                    setIcon(styledAttributes.getResourceId(
8793                            R.styleable.Theme_actionModeCopyDrawable, 0)).
8794                    setAlphabeticShortcut('c').
8795                    setShowAsAction(
8796                            MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
8797            }
8798
8799            if (canPaste()) {
8800                menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste).
8801                        setIcon(styledAttributes.getResourceId(
8802                                R.styleable.Theme_actionModePasteDrawable, 0)).
8803                        setAlphabeticShortcut('v').
8804                        setShowAsAction(
8805                                MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
8806            }
8807
8808            styledAttributes.recycle();
8809
8810            if (mCustomSelectionActionModeCallback != null) {
8811                if (!mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) {
8812                    // The custom mode can choose to cancel the action mode
8813                    return false;
8814                }
8815            }
8816
8817            if (menu.hasVisibleItems() || mode.getCustomView() != null) {
8818                getSelectionController().show();
8819                return true;
8820            } else {
8821                return false;
8822            }
8823        }
8824
8825        @Override
8826        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
8827            if (mCustomSelectionActionModeCallback != null) {
8828                return mCustomSelectionActionModeCallback.onPrepareActionMode(mode, menu);
8829            }
8830            return true;
8831        }
8832
8833        @Override
8834        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
8835            if (mCustomSelectionActionModeCallback != null &&
8836                 mCustomSelectionActionModeCallback.onActionItemClicked(mode, item)) {
8837                return true;
8838            }
8839            return onTextContextMenuItem(item.getItemId());
8840        }
8841
8842        @Override
8843        public void onDestroyActionMode(ActionMode mode) {
8844            if (mCustomSelectionActionModeCallback != null) {
8845                mCustomSelectionActionModeCallback.onDestroyActionMode(mode);
8846            }
8847            Selection.setSelection((Spannable) mText, getSelectionEnd());
8848
8849            if (mSelectionModifierCursorController != null) {
8850                mSelectionModifierCursorController.hide();
8851            }
8852
8853            mSelectionActionMode = null;
8854        }
8855    }
8856
8857    private class PastePopupWindow implements OnClickListener {
8858        private final PopupWindow mContainer;
8859        private final View[] mPasteViews = new View[4];
8860        private final int[] mPasteViewLayouts = new int[] {
8861                mTextEditPasteWindowLayout,  mTextEditNoPasteWindowLayout,
8862                mTextEditSidePasteWindowLayout, mTextEditSideNoPasteWindowLayout };
8863
8864        public PastePopupWindow() {
8865            mContainer = new PopupWindow(TextView.this.mContext, null,
8866                    com.android.internal.R.attr.textSelectHandleWindowStyle);
8867            mContainer.setSplitTouchEnabled(true);
8868            mContainer.setClippingEnabled(false);
8869            mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
8870
8871            mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
8872            mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
8873        }
8874
8875        private int viewIndex(boolean onTop) {
8876            return (onTop ? 0 : 1<<1) + (canPaste() ? 0 : 1<<0);
8877        }
8878
8879        private void updateContent(boolean onTop) {
8880            final int viewIndex = viewIndex(onTop);
8881            View view = mPasteViews[viewIndex];
8882
8883            if (view == null) {
8884                final int layout = mPasteViewLayouts[viewIndex];
8885                LayoutInflater inflater = (LayoutInflater)TextView.this.mContext.
8886                    getSystemService(Context.LAYOUT_INFLATER_SERVICE);
8887                if (inflater != null) {
8888                    view = inflater.inflate(layout, null);
8889                }
8890
8891                if (view == null) {
8892                    throw new IllegalArgumentException("Unable to inflate TextEdit paste window");
8893                }
8894
8895                final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
8896                view.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
8897                        ViewGroup.LayoutParams.WRAP_CONTENT));
8898                view.measure(size, size);
8899
8900                view.setOnClickListener(this);
8901
8902                mPasteViews[viewIndex] = view;
8903            }
8904
8905            mContainer.setContentView(view);
8906        }
8907
8908        public void show() {
8909            updateContent(true);
8910            positionAtCursor();
8911        }
8912
8913        public void hide() {
8914            mContainer.dismiss();
8915        }
8916
8917        public boolean isShowing() {
8918            return mContainer.isShowing();
8919        }
8920
8921        @Override
8922        public void onClick(View v) {
8923            if (canPaste()) {
8924                paste(getSelectionStart(), getSelectionEnd());
8925            }
8926            hide();
8927        }
8928
8929        void positionAtCursor() {
8930            View contentView = mContainer.getContentView();
8931            int width = contentView.getMeasuredWidth();
8932            int height = contentView.getMeasuredHeight();
8933            final int offset = TextView.this.getSelectionStart();
8934            final int line = mLayout.getLineForOffset(offset);
8935            final int lineTop = mLayout.getLineTop(line);
8936            float primaryHorizontal = mLayout.getPrimaryHorizontal(offset);
8937
8938            final Rect bounds = sCursorControllerTempRect;
8939            bounds.left = (int) (primaryHorizontal - width / 2.0f);
8940            bounds.top = lineTop - height;
8941
8942            bounds.right = bounds.left + width;
8943            bounds.bottom = bounds.top + height;
8944
8945            convertFromViewportToContentCoordinates(bounds);
8946
8947            final int[] coords = mTempCoords;
8948            TextView.this.getLocationInWindow(coords);
8949            coords[0] += bounds.left;
8950            coords[1] += bounds.top;
8951
8952            final int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels;
8953            if (coords[1] < 0) {
8954                updateContent(false);
8955                // Update dimensions from new view
8956                contentView = mContainer.getContentView();
8957                width = contentView.getMeasuredWidth();
8958                height = contentView.getMeasuredHeight();
8959
8960                // Vertical clipping, move under edited line and to the side of insertion cursor
8961                // TODO bottom clipping in case there is no system bar
8962                coords[1] += height;
8963                final int lineBottom = mLayout.getLineBottom(line);
8964                final int lineHeight = lineBottom - lineTop;
8965                coords[1] += lineHeight;
8966
8967                // Move to right hand side of insertion cursor by default. TODO RTL text.
8968                final Drawable handle = mContext.getResources().getDrawable(mTextSelectHandleRes);
8969                final int handleHalfWidth = handle.getIntrinsicWidth() / 2;
8970
8971                if (primaryHorizontal + handleHalfWidth + width < screenWidth) {
8972                    coords[0] += handleHalfWidth + width / 2;
8973                } else {
8974                    coords[0] -= handleHalfWidth + width / 2;
8975                }
8976            } else {
8977                // Horizontal clipping
8978                coords[0] = Math.max(0, coords[0]);
8979                coords[0] = Math.min(screenWidth - width, coords[0]);
8980            }
8981
8982            mContainer.showAtLocation(TextView.this, Gravity.NO_GRAVITY, coords[0], coords[1]);
8983        }
8984    }
8985
8986    private abstract class HandleView extends View implements ViewTreeObserver.OnPreDrawListener {
8987        protected Drawable mDrawable;
8988        private final PopupWindow mContainer;
8989        // Position with respect to the parent TextView
8990        private int mPositionX, mPositionY;
8991        private boolean mIsDragging;
8992        // Offset from touch position to mPosition
8993        private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
8994        protected float mHotspotX;
8995        // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
8996        private float mTouchOffsetY;
8997        // Where the touch position should be on the handle to ensure a maximum cursor visibility
8998        private float mIdealVerticalOffset;
8999        // Parent's (TextView) previous position in window
9000        private int mLastParentX, mLastParentY;
9001        // PopupWindow container absolute position with respect to the enclosing window
9002        private int mContainerPositionX, mContainerPositionY;
9003        // Visible or not (scrolled off screen), whether or not this handle should be visible
9004        private boolean mIsActive = false;
9005
9006        public HandleView() {
9007            super(TextView.this.mContext);
9008            mContainer = new PopupWindow(TextView.this.mContext, null,
9009                    com.android.internal.R.attr.textSelectHandleWindowStyle);
9010            mContainer.setSplitTouchEnabled(true);
9011            mContainer.setClippingEnabled(false);
9012            mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
9013            mContainer.setContentView(this);
9014
9015            initDrawable();
9016
9017            final int handleHeight = mDrawable.getIntrinsicHeight();
9018            mTouchOffsetY = -0.3f * handleHeight;
9019            mIdealVerticalOffset = 0.7f * handleHeight;
9020        }
9021
9022        protected abstract void initDrawable();
9023
9024        // Touch-up filter: number of previous positions remembered
9025        private static final int HISTORY_SIZE = 5;
9026        private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
9027        private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
9028        private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
9029        private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
9030        private int mPreviousOffsetIndex = 0;
9031        private int mNumberPreviousOffsets = 0;
9032
9033        private void startTouchUpFilter(int offset) {
9034            mNumberPreviousOffsets = 0;
9035            addPositionToTouchUpFilter(offset);
9036        }
9037
9038        private void addPositionToTouchUpFilter(int offset) {
9039            if (mNumberPreviousOffsets > 0 &&
9040                    mPreviousOffsets[mPreviousOffsetIndex] == offset) {
9041                // Make sure only actual changes of position are recorded.
9042                return;
9043            }
9044
9045            mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
9046            mPreviousOffsets[mPreviousOffsetIndex] = offset;
9047            mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
9048            mNumberPreviousOffsets++;
9049        }
9050
9051        private void filterOnTouchUp() {
9052            final long now = SystemClock.uptimeMillis();
9053            int i = 0;
9054            int index = mPreviousOffsetIndex;
9055            final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
9056            while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
9057                i++;
9058                index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
9059            }
9060
9061            if (i > 0 && i < iMax &&
9062                    (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
9063                updateOffset(mPreviousOffsets[index]);
9064            }
9065        }
9066
9067        @Override
9068        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
9069            setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
9070        }
9071
9072        public void show() {
9073            if (isShowing()) {
9074                mContainer.update(mContainerPositionX, mContainerPositionY,
9075                        mRight - mLeft, mBottom - mTop);
9076            } else {
9077                mContainer.showAtLocation(TextView.this, 0,
9078                        mContainerPositionX, mContainerPositionY);
9079
9080                mIsActive = true;
9081
9082                ViewTreeObserver vto = TextView.this.getViewTreeObserver();
9083                vto.addOnPreDrawListener(this);
9084            }
9085        }
9086
9087        protected void dismiss() {
9088            mIsDragging = false;
9089            mContainer.dismiss();
9090        }
9091
9092        public void hide() {
9093            dismiss();
9094
9095            mIsActive = false;
9096
9097            ViewTreeObserver vto = TextView.this.getViewTreeObserver();
9098            vto.removeOnPreDrawListener(this);
9099        }
9100
9101        public boolean isShowing() {
9102            return mContainer.isShowing();
9103        }
9104
9105        private boolean isPositionVisible() {
9106            // Always show a dragging handle.
9107            if (mIsDragging) {
9108                return true;
9109            }
9110
9111            if (isInBatchEditMode()) {
9112                return false;
9113            }
9114
9115            final int extendedPaddingTop = getExtendedPaddingTop();
9116            final int extendedPaddingBottom = getExtendedPaddingBottom();
9117            final int compoundPaddingLeft = getCompoundPaddingLeft();
9118            final int compoundPaddingRight = getCompoundPaddingRight();
9119
9120            final TextView textView = TextView.this;
9121
9122            if (mTempRect == null) mTempRect = new Rect();
9123            final Rect clip = mTempRect;
9124            clip.left = compoundPaddingLeft;
9125            clip.top = extendedPaddingTop;
9126            clip.right = textView.getWidth() - compoundPaddingRight;
9127            clip.bottom = textView.getHeight() - extendedPaddingBottom;
9128
9129            final ViewParent parent = textView.getParent();
9130            if (parent == null || !parent.getChildVisibleRect(textView, clip, null)) {
9131                return false;
9132            }
9133
9134            final int[] coords = mTempCoords;
9135            textView.getLocationInWindow(coords);
9136            final int posX = coords[0] + mPositionX + (int) mHotspotX;
9137            final int posY = coords[1] + mPositionY;
9138
9139            // Offset by 1 to take into account 0.5 and int rounding around getPrimaryHorizontal.
9140            return posX >= clip.left - 1 && posX <= clip.right + 1 &&
9141                    posY >= clip.top && posY <= clip.bottom;
9142        }
9143
9144        public abstract int getCurrentCursorOffset();
9145
9146        public abstract void updateOffset(int offset);
9147
9148        public abstract void updatePosition(float x, float y);
9149
9150        protected void positionAtCursorOffset(int offset) {
9151            addPositionToTouchUpFilter(offset);
9152            final int line = mLayout.getLineForOffset(offset);
9153            final int lineBottom = mLayout.getLineBottom(line);
9154
9155            mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX);
9156            mPositionY = lineBottom;
9157
9158            // Take TextView's padding into account.
9159            mPositionX += viewportToContentHorizontalOffset();
9160            mPositionY += viewportToContentVerticalOffset();
9161        }
9162
9163        protected boolean updateContainerPosition() {
9164            positionAtCursorOffset(getCurrentCursorOffset());
9165
9166            final int previousContainerPositionX = mContainerPositionX;
9167            final int previousContainerPositionY = mContainerPositionY;
9168
9169            TextView.this.getLocationInWindow(mTempCoords);
9170            mContainerPositionX = mTempCoords[0] + mPositionX;
9171            mContainerPositionY = mTempCoords[1] + mPositionY;
9172
9173            return (previousContainerPositionX != mContainerPositionX ||
9174                    previousContainerPositionY != mContainerPositionY);
9175        }
9176
9177        public boolean onPreDraw() {
9178            if (updateContainerPosition()) {
9179                if (mIsDragging) {
9180                    if (mTempCoords[0] != mLastParentX || mTempCoords[1] != mLastParentY) {
9181                        mTouchToWindowOffsetX += mTempCoords[0] - mLastParentX;
9182                        mTouchToWindowOffsetY += mTempCoords[1] - mLastParentY;
9183                        mLastParentX = mTempCoords[0];
9184                        mLastParentY = mTempCoords[1];
9185                    }
9186                }
9187
9188                onHandleMoved();
9189
9190                if (isPositionVisible()) {
9191                    mContainer.update(mContainerPositionX, mContainerPositionY,
9192                            mRight - mLeft, mBottom - mTop);
9193
9194                    if (mIsActive && !isShowing()) {
9195                        show();
9196                    }
9197                } else {
9198                    if (isShowing()) {
9199                        dismiss();
9200                    }
9201                }
9202            }
9203            return true;
9204        }
9205
9206        @Override
9207        protected void onDraw(Canvas c) {
9208            mDrawable.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
9209            mDrawable.draw(c);
9210        }
9211
9212        @Override
9213        public boolean onTouchEvent(MotionEvent ev) {
9214            switch (ev.getActionMasked()) {
9215                case MotionEvent.ACTION_DOWN: {
9216                    startTouchUpFilter(getCurrentCursorOffset());
9217                    mTouchToWindowOffsetX = ev.getRawX() - mPositionX;
9218                    mTouchToWindowOffsetY = ev.getRawY() - mPositionY;
9219
9220                    final int[] coords = mTempCoords;
9221                    TextView.this.getLocationInWindow(coords);
9222                    mLastParentX = coords[0];
9223                    mLastParentY = coords[1];
9224                    mIsDragging = true;
9225                    break;
9226                }
9227
9228                case MotionEvent.ACTION_MOVE: {
9229                    final float rawX = ev.getRawX();
9230                    final float rawY = ev.getRawY();
9231
9232                    // Vertical hysteresis: vertical down movement tends to snap to ideal offset
9233                    final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
9234                    final float currentVerticalOffset = rawY - mPositionY - mLastParentY;
9235                    float newVerticalOffset;
9236                    if (previousVerticalOffset < mIdealVerticalOffset) {
9237                        newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
9238                        newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
9239                    } else {
9240                        newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
9241                        newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
9242                    }
9243                    mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
9244
9245                    final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX;
9246                    final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY;
9247
9248                    updatePosition(newPosX, newPosY);
9249                    break;
9250                }
9251
9252                case MotionEvent.ACTION_UP:
9253                    filterOnTouchUp();
9254                    mIsDragging = false;
9255                    break;
9256
9257                case MotionEvent.ACTION_CANCEL:
9258                    mIsDragging = false;
9259                    break;
9260            }
9261            return true;
9262        }
9263
9264        public boolean isDragging() {
9265            return mIsDragging;
9266        }
9267
9268        void onHandleMoved() {
9269            // Does nothing by default
9270        }
9271
9272        public void onDetached() {
9273            // Should be overriden to clean possible Runnable
9274        }
9275    }
9276
9277    private class InsertionHandleView extends HandleView {
9278        private static final int DELAY_BEFORE_FADE_OUT = 4000;
9279        private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
9280
9281        // Used to detect taps on the insertion handle, which will affect the PastePopupWindow
9282        private long mTouchTimer;
9283        private float mDownPositionX, mDownPositionY;
9284        private PastePopupWindow mPastePopupWindow;
9285        private Runnable mHider;
9286        private Runnable mPastePopupShower;
9287
9288        @Override
9289        public void show() {
9290            super.show();
9291            hideDelayed();
9292            hidePastePopupWindow();
9293        }
9294
9295        public void show(int delayBeforePaste) {
9296            show();
9297
9298            final long durationSinceCutOrCopy = SystemClock.uptimeMillis() - sLastCutOrCopyTime;
9299            if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) {
9300                delayBeforePaste = 0;
9301            }
9302            if (delayBeforePaste == 0 || canPaste()) {
9303                if (mPastePopupShower == null) {
9304                    mPastePopupShower = new Runnable() {
9305                        public void run() {
9306                            showPastePopupWindow();
9307                        }
9308                    };
9309                }
9310                TextView.this.postDelayed(mPastePopupShower, delayBeforePaste);
9311            }
9312        }
9313
9314        @Override
9315        protected void dismiss() {
9316            super.dismiss();
9317            onDetached();
9318        }
9319
9320        private void hideDelayed() {
9321            removeHiderCallback();
9322            if (mHider == null) {
9323                mHider = new Runnable() {
9324                    public void run() {
9325                        hide();
9326                    }
9327                };
9328            }
9329            TextView.this.postDelayed(mHider, DELAY_BEFORE_FADE_OUT);
9330        }
9331
9332        private void removeHiderCallback() {
9333            if (mHider != null) {
9334                TextView.this.removeCallbacks(mHider);
9335            }
9336        }
9337
9338        @Override
9339        protected void initDrawable() {
9340            if (mSelectHandleCenter == null) {
9341                mSelectHandleCenter = mContext.getResources().getDrawable(
9342                        mTextSelectHandleRes);
9343            }
9344            mDrawable = mSelectHandleCenter;
9345            mHotspotX = mDrawable.getIntrinsicWidth() / 2.0f;
9346        }
9347
9348        @Override
9349        public boolean onTouchEvent(MotionEvent ev) {
9350            final boolean result = super.onTouchEvent(ev);
9351
9352            switch (ev.getActionMasked()) {
9353                case MotionEvent.ACTION_DOWN:
9354                    mDownPositionX = ev.getRawX();
9355                    mDownPositionY = ev.getRawY();
9356                    mTouchTimer = SystemClock.uptimeMillis();
9357                    break;
9358
9359                case MotionEvent.ACTION_UP:
9360                    long delay = SystemClock.uptimeMillis() - mTouchTimer;
9361                    if (delay < ViewConfiguration.getTapTimeout()) {
9362                        final float deltaX = mDownPositionX - ev.getRawX();
9363                        final float deltaY = mDownPositionY - ev.getRawY();
9364                        final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
9365                        if (distanceSquared < mSquaredTouchSlopDistance) {
9366                            if (mPastePopupWindow != null && mPastePopupWindow.isShowing()) {
9367                                // Tapping on the handle dismisses the displayed paste view,
9368                                mPastePopupWindow.hide();
9369                            } else {
9370                                show(0);
9371                            }
9372                        }
9373                    }
9374                    hideDelayed();
9375                    break;
9376
9377                case MotionEvent.ACTION_CANCEL:
9378                    hideDelayed();
9379                    break;
9380
9381                default:
9382                    break;
9383            }
9384
9385            return result;
9386        }
9387
9388        @Override
9389        public int getCurrentCursorOffset() {
9390            return TextView.this.getSelectionStart();
9391        }
9392
9393        @Override
9394        public void updateOffset(int offset) {
9395            Selection.setSelection((Spannable) mText, offset);
9396        }
9397
9398        @Override
9399        public void updatePosition(float x, float y) {
9400            updateOffset(getOffsetForPosition(x, y));
9401        }
9402
9403        void showPastePopupWindow() {
9404            if (mPastePopupWindow == null) {
9405                mPastePopupWindow = new PastePopupWindow();
9406            }
9407            mPastePopupWindow.show();
9408        }
9409
9410        @Override
9411        void onHandleMoved() {
9412            removeHiderCallback();
9413            hidePastePopupWindow();
9414        }
9415
9416        void hidePastePopupWindow() {
9417            if (mPastePopupShower != null) {
9418                TextView.this.removeCallbacks(mPastePopupShower);
9419            }
9420            if (mPastePopupWindow != null) {
9421                mPastePopupWindow.hide();
9422            }
9423        }
9424
9425        @Override
9426        public void onDetached() {
9427            removeHiderCallback();
9428            hidePastePopupWindow();
9429        }
9430    }
9431
9432    private class SelectionStartHandleView extends HandleView {
9433        @Override
9434        protected void initDrawable() {
9435            if (mSelectHandleLeft == null) {
9436                mSelectHandleLeft = mContext.getResources().getDrawable(
9437                        mTextSelectHandleLeftRes);
9438            }
9439            mDrawable = mSelectHandleLeft;
9440            mHotspotX = mDrawable.getIntrinsicWidth() * 3.0f / 4.0f;
9441        }
9442
9443        @Override
9444        public int getCurrentCursorOffset() {
9445            return TextView.this.getSelectionStart();
9446        }
9447
9448        @Override
9449        public void updateOffset(int offset) {
9450            Selection.setSelection((Spannable) mText, offset, getSelectionEnd());
9451        }
9452
9453        @Override
9454        public void updatePosition(float x, float y) {
9455            final int selectionStart = getSelectionStart();
9456            final int selectionEnd = getSelectionEnd();
9457
9458            int offset = getOffsetForPosition(x, y);
9459
9460            // No need to redraw when the offset is unchanged
9461            if (offset == selectionStart) return;
9462            // Handles can not cross and selection is at least one character
9463            if (offset >= selectionEnd) offset = selectionEnd - 1;
9464
9465            Selection.setSelection((Spannable) mText, offset, selectionEnd);
9466        }
9467    }
9468
9469    private class SelectionEndHandleView extends HandleView {
9470        @Override
9471        protected void initDrawable() {
9472            if (mSelectHandleRight == null) {
9473                mSelectHandleRight = mContext.getResources().getDrawable(
9474                        mTextSelectHandleRightRes);
9475            }
9476            mDrawable = mSelectHandleRight;
9477            mHotspotX = mDrawable.getIntrinsicWidth() / 4.0f;
9478        }
9479
9480        @Override
9481        public int getCurrentCursorOffset() {
9482            return TextView.this.getSelectionEnd();
9483        }
9484
9485        @Override
9486        public void updateOffset(int offset) {
9487            Selection.setSelection((Spannable) mText, getSelectionStart(), offset);
9488        }
9489
9490        @Override
9491        public void updatePosition(float x, float y) {
9492            final int selectionStart = getSelectionStart();
9493            final int selectionEnd = getSelectionEnd();
9494
9495            int offset = getOffsetForPosition(x, y);
9496
9497            // No need to redraw when the offset is unchanged
9498            if (offset == selectionEnd) return;
9499            // Handles can not cross and selection is at least one character
9500            if (offset <= selectionStart) offset = selectionStart + 1;
9501
9502            Selection.setSelection((Spannable) mText, selectionStart, offset);
9503        }
9504    }
9505
9506    /**
9507     * A CursorController instance can be used to control a cursor in the text.
9508     * It is not used outside of {@link TextView}.
9509     * @hide
9510     */
9511    private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
9512        /**
9513         * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}.
9514         * See also {@link #hide()}.
9515         */
9516        public void show();
9517
9518        /**
9519         * Hide the cursor controller from screen.
9520         * See also {@link #show()}.
9521         */
9522        public void hide();
9523
9524        /**
9525         * This method is called by {@link #onTouchEvent(MotionEvent)} and gives the controller
9526         * a chance to become active and/or visible.
9527         * @param event The touch event
9528         */
9529        public boolean onTouchEvent(MotionEvent event);
9530
9531        /**
9532         * Called when the view is detached from window. Perform house keeping task, such as
9533         * stopping Runnable thread that would otherwise keep a reference on the context, thus
9534         * preventing the activity from being recycled.
9535         */
9536        public void onDetached();
9537    }
9538
9539    private class InsertionPointCursorController implements CursorController {
9540        private static final int DELAY_BEFORE_PASTE = 2000;
9541
9542        private InsertionHandleView mHandle;
9543
9544        public void show() {
9545            ((InsertionHandleView) getHandle()).show(DELAY_BEFORE_PASTE);
9546        }
9547
9548        public void showWithPaste() {
9549            ((InsertionHandleView) getHandle()).show(0);
9550        }
9551
9552        public void hide() {
9553            if (mHandle != null) {
9554                mHandle.hide();
9555            }
9556        }
9557
9558        public boolean onTouchEvent(MotionEvent ev) {
9559            return false;
9560        }
9561
9562        public void onTouchModeChanged(boolean isInTouchMode) {
9563            if (!isInTouchMode) {
9564                hide();
9565            }
9566        }
9567
9568        private HandleView getHandle() {
9569            if (mHandle == null) {
9570                mHandle = new InsertionHandleView();
9571            }
9572            return mHandle;
9573        }
9574
9575        @Override
9576        public void onDetached() {
9577            final ViewTreeObserver observer = getViewTreeObserver();
9578            observer.removeOnTouchModeChangeListener(this);
9579
9580            if (mHandle != null) mHandle.onDetached();
9581        }
9582    }
9583
9584    private class SelectionModifierCursorController implements CursorController {
9585        // The cursor controller handles, lazily created when shown.
9586        private SelectionStartHandleView mStartHandle;
9587        private SelectionEndHandleView mEndHandle;
9588        // The offsets of that last touch down event. Remembered to start selection there.
9589        private int mMinTouchOffset, mMaxTouchOffset;
9590
9591        // Double tap detection
9592        private long mPreviousTapUpTime = 0;
9593        private float mPreviousTapPositionX, mPreviousTapPositionY;
9594
9595        SelectionModifierCursorController() {
9596            resetTouchOffsets();
9597        }
9598
9599        public void show() {
9600            if (isInBatchEditMode()) {
9601                return;
9602            }
9603
9604            // Lazy object creation has to be done before updatePosition() is called.
9605            if (mStartHandle == null) mStartHandle = new SelectionStartHandleView();
9606            if (mEndHandle == null) mEndHandle = new SelectionEndHandleView();
9607
9608            mStartHandle.show();
9609            mEndHandle.show();
9610
9611            hideInsertionPointCursorController();
9612            hideSuggestions();
9613        }
9614
9615        public void hide() {
9616            if (mStartHandle != null) mStartHandle.hide();
9617            if (mEndHandle != null) mEndHandle.hide();
9618        }
9619
9620        public boolean onTouchEvent(MotionEvent event) {
9621            // This is done even when the View does not have focus, so that long presses can start
9622            // selection and tap can move cursor from this tap position.
9623            if (isTextEditable() || mTextIsSelectable) {
9624                switch (event.getActionMasked()) {
9625                    case MotionEvent.ACTION_DOWN:
9626                        final float x = event.getX();
9627                        final float y = event.getY();
9628
9629                        // Remember finger down position, to be able to start selection from there
9630                        mMinTouchOffset = mMaxTouchOffset = getOffsetForPosition(x, y);
9631
9632                        // Double tap detection
9633                        long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime;
9634                        if (duration <= ViewConfiguration.getDoubleTapTimeout() &&
9635                                isPositionOnText(x, y)) {
9636                            final float deltaX = x - mPreviousTapPositionX;
9637                            final float deltaY = y - mPreviousTapPositionY;
9638                            final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
9639                            if (distanceSquared < mSquaredTouchSlopDistance) {
9640                                showSuggestions();
9641                                mDiscardNextActionUp = true;
9642                            }
9643                        }
9644
9645                        mPreviousTapPositionX = x;
9646                        mPreviousTapPositionY = y;
9647
9648                        break;
9649
9650                    case MotionEvent.ACTION_POINTER_DOWN:
9651                    case MotionEvent.ACTION_POINTER_UP:
9652                        // Handle multi-point gestures. Keep min and max offset positions.
9653                        // Only activated for devices that correctly handle multi-touch.
9654                        if (mContext.getPackageManager().hasSystemFeature(
9655                                PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
9656                            updateMinAndMaxOffsets(event);
9657                        }
9658                        break;
9659
9660                    case MotionEvent.ACTION_UP:
9661                        mPreviousTapUpTime = SystemClock.uptimeMillis();
9662                        break;
9663                }
9664            }
9665            return false;
9666        }
9667
9668        /**
9669         * @param event
9670         */
9671        private void updateMinAndMaxOffsets(MotionEvent event) {
9672            int pointerCount = event.getPointerCount();
9673            for (int index = 0; index < pointerCount; index++) {
9674                int offset = getOffsetForPosition(event.getX(index), event.getY(index));
9675                if (offset < mMinTouchOffset) mMinTouchOffset = offset;
9676                if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
9677            }
9678        }
9679
9680        public int getMinTouchOffset() {
9681            return mMinTouchOffset;
9682        }
9683
9684        public int getMaxTouchOffset() {
9685            return mMaxTouchOffset;
9686        }
9687
9688        public void resetTouchOffsets() {
9689            mMinTouchOffset = mMaxTouchOffset = -1;
9690        }
9691
9692        /**
9693         * @return true iff this controller is currently used to move the selection start.
9694         */
9695        public boolean isSelectionStartDragged() {
9696            return mStartHandle != null && mStartHandle.isDragging();
9697        }
9698
9699        public void onTouchModeChanged(boolean isInTouchMode) {
9700            if (!isInTouchMode) {
9701                hide();
9702            }
9703        }
9704
9705        @Override
9706        public void onDetached() {
9707            final ViewTreeObserver observer = getViewTreeObserver();
9708            observer.removeOnTouchModeChangeListener(this);
9709
9710            if (mStartHandle != null) mStartHandle.onDetached();
9711            if (mEndHandle != null) mEndHandle.onDetached();
9712        }
9713    }
9714
9715    private void hideInsertionPointCursorController() {
9716        // No need to create the controller to hide it.
9717        if (mInsertionPointCursorController != null) {
9718            mInsertionPointCursorController.hide();
9719        }
9720    }
9721
9722    /**
9723     * Hides the insertion controller and stops text selection mode, hiding the selection controller
9724     */
9725    private void hideControllers() {
9726        hideInsertionPointCursorController();
9727        stopSelectionActionMode();
9728        hideSuggestions();
9729    }
9730
9731    /**
9732     * Get the character offset closest to the specified absolute position. A typical use case is to
9733     * pass the result of {@link MotionEvent#getX()} and {@link MotionEvent#getY()} to this method.
9734     *
9735     * @param x The horizontal absolute position of a point on screen
9736     * @param y The vertical absolute position of a point on screen
9737     * @return the character offset for the character whose position is closest to the specified
9738     *  position. Returns -1 if there is no layout.
9739     */
9740    public int getOffsetForPosition(float x, float y) {
9741        if (getLayout() == null) return -1;
9742        final int line = getLineAtCoordinate(y);
9743        final int offset = getOffsetAtCoordinate(line, x);
9744        return offset;
9745    }
9746
9747    private float convertToLocalHorizontalCoordinate(float x) {
9748        x -= getTotalPaddingLeft();
9749        // Clamp the position to inside of the view.
9750        x = Math.max(0.0f, x);
9751        x = Math.min(getWidth() - getTotalPaddingRight() - 1, x);
9752        x += getScrollX();
9753        return x;
9754    }
9755
9756    private int getLineAtCoordinate(float y) {
9757        y -= getTotalPaddingTop();
9758        // Clamp the position to inside of the view.
9759        y = Math.max(0.0f, y);
9760        y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y);
9761        y += getScrollY();
9762        return getLayout().getLineForVertical((int) y);
9763    }
9764
9765    private int getOffsetAtCoordinate(int line, float x) {
9766        x = convertToLocalHorizontalCoordinate(x);
9767        return getLayout().getOffsetForHorizontal(line, x);
9768    }
9769
9770    /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
9771     * in the view. Returns false when the position is in the empty space of left/right of text.
9772     */
9773    private boolean isPositionOnText(float x, float y) {
9774        if (getLayout() == null) return false;
9775
9776        final int line = getLineAtCoordinate(y);
9777        x = convertToLocalHorizontalCoordinate(x);
9778
9779        if (x < getLayout().getLineLeft(line)) return false;
9780        if (x > getLayout().getLineRight(line)) return false;
9781        return true;
9782    }
9783
9784    @Override
9785    public boolean onDragEvent(DragEvent event) {
9786        switch (event.getAction()) {
9787            case DragEvent.ACTION_DRAG_STARTED:
9788                return hasInsertionController();
9789
9790            case DragEvent.ACTION_DRAG_ENTERED:
9791                TextView.this.requestFocus();
9792                return true;
9793
9794            case DragEvent.ACTION_DRAG_LOCATION:
9795                final int offset = getOffsetForPosition(event.getX(), event.getY());
9796                Selection.setSelection((Spannable)mText, offset);
9797                return true;
9798
9799            case DragEvent.ACTION_DROP:
9800                onDrop(event);
9801                return true;
9802
9803            case DragEvent.ACTION_DRAG_ENDED:
9804            case DragEvent.ACTION_DRAG_EXITED:
9805            default:
9806                return true;
9807        }
9808    }
9809
9810    private void onDrop(DragEvent event) {
9811        StringBuilder content = new StringBuilder("");
9812        ClipData clipData = event.getClipData();
9813        final int itemCount = clipData.getItemCount();
9814        for (int i=0; i < itemCount; i++) {
9815            Item item = clipData.getItemAt(i);
9816            content.append(item.coerceToText(TextView.this.mContext));
9817        }
9818
9819        final int offset = getOffsetForPosition(event.getX(), event.getY());
9820
9821        Object localState = event.getLocalState();
9822        DragLocalState dragLocalState = null;
9823        if (localState instanceof DragLocalState) {
9824            dragLocalState = (DragLocalState) localState;
9825        }
9826        boolean dragDropIntoItself = dragLocalState != null &&
9827                dragLocalState.sourceTextView == this;
9828
9829        if (dragDropIntoItself) {
9830            if (offset >= dragLocalState.start && offset < dragLocalState.end) {
9831                // A drop inside the original selection discards the drop.
9832                return;
9833            }
9834        }
9835
9836        final int originalLength = mText.length();
9837        long minMax = prepareSpacesAroundPaste(offset, offset, content);
9838        int min = extractRangeStartFromLong(minMax);
9839        int max = extractRangeEndFromLong(minMax);
9840
9841        Selection.setSelection((Spannable) mText, max);
9842        ((Editable) mText).replace(min, max, content);
9843
9844        if (dragDropIntoItself) {
9845            int dragSourceStart = dragLocalState.start;
9846            int dragSourceEnd = dragLocalState.end;
9847            if (max <= dragSourceStart) {
9848                // Inserting text before selection has shifted positions
9849                final int shift = mText.length() - originalLength;
9850                dragSourceStart += shift;
9851                dragSourceEnd += shift;
9852            }
9853
9854            // Delete original selection
9855            ((Editable) mText).delete(dragSourceStart, dragSourceEnd);
9856
9857            // Make sure we do not leave two adjacent spaces.
9858            if ((dragSourceStart == 0 ||
9859                    Character.isSpaceChar(mTransformed.charAt(dragSourceStart - 1))) &&
9860                    (dragSourceStart == mText.length() ||
9861                    Character.isSpaceChar(mTransformed.charAt(dragSourceStart)))) {
9862                final int pos = dragSourceStart == mText.length() ?
9863                        dragSourceStart - 1 : dragSourceStart;
9864                ((Editable) mText).delete(pos, pos + 1);
9865            }
9866        }
9867    }
9868
9869    /**
9870     * @return True if this view supports insertion handles.
9871     */
9872    boolean hasInsertionController() {
9873        return mInsertionControllerEnabled;
9874    }
9875
9876    /**
9877     * @return True if this view supports selection handles.
9878     */
9879    boolean hasSelectionController() {
9880        return mSelectionControllerEnabled;
9881    }
9882
9883    InsertionPointCursorController getInsertionController() {
9884        if (!mInsertionControllerEnabled) {
9885            return null;
9886        }
9887
9888        if (mInsertionPointCursorController == null) {
9889            mInsertionPointCursorController = new InsertionPointCursorController();
9890
9891            final ViewTreeObserver observer = getViewTreeObserver();
9892            observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
9893        }
9894
9895        return mInsertionPointCursorController;
9896    }
9897
9898    SelectionModifierCursorController getSelectionController() {
9899        if (!mSelectionControllerEnabled) {
9900            return null;
9901        }
9902
9903        if (mSelectionModifierCursorController == null) {
9904            mSelectionModifierCursorController = new SelectionModifierCursorController();
9905
9906            final ViewTreeObserver observer = getViewTreeObserver();
9907            observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
9908        }
9909
9910        return mSelectionModifierCursorController;
9911    }
9912
9913    boolean isInBatchEditMode() {
9914        final InputMethodState ims = mInputMethodState;
9915        if (ims != null) {
9916            return ims.mBatchEditNesting > 0;
9917        }
9918        return mInBatchEditControllers;
9919    }
9920
9921    @ViewDebug.ExportedProperty(category = "text")
9922    private CharSequence            mText;
9923    private CharSequence            mTransformed;
9924    private BufferType              mBufferType = BufferType.NORMAL;
9925
9926    private int                     mInputType = EditorInfo.TYPE_NULL;
9927    private CharSequence            mHint;
9928    private Layout                  mHintLayout;
9929
9930    private KeyListener             mInput;
9931
9932    private MovementMethod          mMovement;
9933    private TransformationMethod    mTransformation;
9934    private ChangeWatcher           mChangeWatcher;
9935
9936    private ArrayList<TextWatcher>  mListeners = null;
9937
9938    // display attributes
9939    private final TextPaint         mTextPaint;
9940    private boolean                 mUserSetTextScaleX;
9941    private final Paint             mHighlightPaint;
9942    private int                     mHighlightColor = 0xCC475925;
9943    /**
9944     * This is temporarily visible to fix bug 3085564 in webView. Do not rely on
9945     * this field being protected. Will be restored as private when lineHeight
9946     * feature request 3215097 is implemented
9947     * @hide
9948     */
9949    protected Layout                mLayout;
9950
9951    private long                    mShowCursor;
9952    private Blink                   mBlink;
9953    private boolean                 mCursorVisible = true;
9954
9955    // Cursor Controllers.
9956    private InsertionPointCursorController mInsertionPointCursorController;
9957    private SelectionModifierCursorController mSelectionModifierCursorController;
9958    private ActionMode              mSelectionActionMode;
9959    private boolean                 mInsertionControllerEnabled;
9960    private boolean                 mSelectionControllerEnabled;
9961    private boolean                 mInBatchEditControllers;
9962
9963    // These are needed to desambiguate a long click. If the long click comes from ones of these, we
9964    // select from the current cursor position. Otherwise, select from long pressed position.
9965    private boolean                 mDPadCenterIsDown = false;
9966    private boolean                 mEnterKeyIsDown = false;
9967    private boolean                 mContextMenuTriggeredByKey = false;
9968    // Created once and shared by different CursorController helper methods.
9969    // Only one cursor controller is active at any time which prevent race conditions.
9970    private static Rect             sCursorControllerTempRect = new Rect();
9971
9972    private boolean                 mSelectAllOnFocus = false;
9973
9974    private int                     mGravity = Gravity.TOP | Gravity.LEFT;
9975    private boolean                 mHorizontallyScrolling;
9976
9977    private int                     mAutoLinkMask;
9978    private boolean                 mLinksClickable = true;
9979
9980    private float                   mSpacingMult = 1;
9981    private float                   mSpacingAdd = 0;
9982    private boolean                 mTextIsSelectable = false;
9983
9984    private static final int        LINES = 1;
9985    private static final int        EMS = LINES;
9986    private static final int        PIXELS = 2;
9987
9988    private int                     mMaximum = Integer.MAX_VALUE;
9989    private int                     mMaxMode = LINES;
9990    private int                     mMinimum = 0;
9991    private int                     mMinMode = LINES;
9992
9993    private int                     mMaxWidth = Integer.MAX_VALUE;
9994    private int                     mMaxWidthMode = PIXELS;
9995    private int                     mMinWidth = 0;
9996    private int                     mMinWidthMode = PIXELS;
9997
9998    private boolean                 mSingleLine;
9999    private int                     mDesiredHeightAtMeasure = -1;
10000    private boolean                 mIncludePad = true;
10001
10002    // tmp primitives, so we don't alloc them on each draw
10003    private Path                    mHighlightPath;
10004    private boolean                 mHighlightPathBogus = true;
10005    private static final RectF      sTempRect = new RectF();
10006
10007    // XXX should be much larger
10008    private static final int        VERY_WIDE = 16384;
10009
10010    private static final int        BLINK = 500;
10011
10012    private static final int ANIMATED_SCROLL_GAP = 250;
10013    private long mLastScroll;
10014    private Scroller mScroller = null;
10015
10016    private BoringLayout.Metrics mBoring;
10017    private BoringLayout.Metrics mHintBoring;
10018
10019    private BoringLayout mSavedLayout, mSavedHintLayout;
10020
10021    private static final InputFilter[] NO_FILTERS = new InputFilter[0];
10022    private InputFilter[] mFilters = NO_FILTERS;
10023    private static final Spanned EMPTY_SPANNED = new SpannedString("");
10024    private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
10025    // System wide time for last cut or copy action.
10026    private static long sLastCutOrCopyTime;
10027    // Used to highlight a word when it is corrected by the IME
10028    private CorrectionHighlighter mCorrectionHighlighter;
10029    // New state used to change background based on whether this TextView is multiline.
10030    private static final int[] MULTILINE_STATE_SET = { R.attr.state_multiline };
10031}
10032