TextView.java revision e67b58a347109b444070a34e86e81ce119f266b3
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 com.android.internal.util.FastMath;
20import com.android.internal.widget.EditableInputConnection;
21
22import org.xmlpull.v1.XmlPullParserException;
23
24import android.content.Context;
25import android.content.Intent;
26import android.content.pm.PackageManager;
27import android.content.res.ColorStateList;
28import android.content.res.Resources;
29import android.content.res.TypedArray;
30import android.content.res.XmlResourceParser;
31import android.graphics.Canvas;
32import android.graphics.Paint;
33import android.graphics.Path;
34import android.graphics.Rect;
35import android.graphics.RectF;
36import android.graphics.Typeface;
37import android.graphics.drawable.Drawable;
38import android.os.Bundle;
39import android.os.Handler;
40import android.os.Message;
41import android.os.Parcel;
42import android.os.Parcelable;
43import android.os.ResultReceiver;
44import android.os.SystemClock;
45import android.text.BoringLayout;
46import android.text.ClipboardManager;
47import android.text.DynamicLayout;
48import android.text.Editable;
49import android.text.GetChars;
50import android.text.GraphicsOperations;
51import android.text.InputFilter;
52import android.text.InputType;
53import android.text.Layout;
54import android.text.ParcelableSpan;
55import android.text.Selection;
56import android.text.SpanWatcher;
57import android.text.Spannable;
58import android.text.SpannableString;
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.style.ParagraphStyle;
80import android.text.style.URLSpan;
81import android.text.style.UpdateAppearance;
82import android.text.util.Linkify;
83import android.util.AttributeSet;
84import android.util.FloatMath;
85import android.util.Log;
86import android.util.TypedValue;
87import android.view.ContextMenu;
88import android.view.Gravity;
89import android.view.KeyEvent;
90import android.view.LayoutInflater;
91import android.view.MenuItem;
92import android.view.MotionEvent;
93import android.view.View;
94import android.view.ViewConfiguration;
95import android.view.ViewDebug;
96import android.view.ViewGroup.LayoutParams;
97import android.view.ViewRoot;
98import android.view.ViewTreeObserver;
99import android.view.accessibility.AccessibilityEvent;
100import android.view.accessibility.AccessibilityManager;
101import android.view.animation.AnimationUtils;
102import android.view.inputmethod.BaseInputConnection;
103import android.view.inputmethod.CompletionInfo;
104import android.view.inputmethod.EditorInfo;
105import android.view.inputmethod.ExtractedText;
106import android.view.inputmethod.ExtractedTextRequest;
107import android.view.inputmethod.InputConnection;
108import android.view.inputmethod.InputMethodManager;
109import android.widget.RemoteViews.RemoteView;
110
111import java.io.IOException;
112import java.lang.ref.WeakReference;
113import java.util.ArrayList;
114
115/**
116 * Displays text to the user and optionally allows them to edit it.  A TextView
117 * is a complete text editor, however the basic class is configured to not
118 * allow editing; see {@link EditText} for a subclass that configures the text
119 * view for editing.
120 *
121 * <p>
122 * <b>XML attributes</b>
123 * <p>
124 * See {@link android.R.styleable#TextView TextView Attributes},
125 * {@link android.R.styleable#View View Attributes}
126 *
127 * @attr ref android.R.styleable#TextView_text
128 * @attr ref android.R.styleable#TextView_bufferType
129 * @attr ref android.R.styleable#TextView_hint
130 * @attr ref android.R.styleable#TextView_textColor
131 * @attr ref android.R.styleable#TextView_textColorHighlight
132 * @attr ref android.R.styleable#TextView_textColorHint
133 * @attr ref android.R.styleable#TextView_textAppearance
134 * @attr ref android.R.styleable#TextView_textColorLink
135 * @attr ref android.R.styleable#TextView_textSize
136 * @attr ref android.R.styleable#TextView_textScaleX
137 * @attr ref android.R.styleable#TextView_typeface
138 * @attr ref android.R.styleable#TextView_textStyle
139 * @attr ref android.R.styleable#TextView_cursorVisible
140 * @attr ref android.R.styleable#TextView_maxLines
141 * @attr ref android.R.styleable#TextView_maxHeight
142 * @attr ref android.R.styleable#TextView_lines
143 * @attr ref android.R.styleable#TextView_height
144 * @attr ref android.R.styleable#TextView_minLines
145 * @attr ref android.R.styleable#TextView_minHeight
146 * @attr ref android.R.styleable#TextView_maxEms
147 * @attr ref android.R.styleable#TextView_maxWidth
148 * @attr ref android.R.styleable#TextView_ems
149 * @attr ref android.R.styleable#TextView_width
150 * @attr ref android.R.styleable#TextView_minEms
151 * @attr ref android.R.styleable#TextView_minWidth
152 * @attr ref android.R.styleable#TextView_gravity
153 * @attr ref android.R.styleable#TextView_scrollHorizontally
154 * @attr ref android.R.styleable#TextView_password
155 * @attr ref android.R.styleable#TextView_singleLine
156 * @attr ref android.R.styleable#TextView_selectAllOnFocus
157 * @attr ref android.R.styleable#TextView_includeFontPadding
158 * @attr ref android.R.styleable#TextView_maxLength
159 * @attr ref android.R.styleable#TextView_shadowColor
160 * @attr ref android.R.styleable#TextView_shadowDx
161 * @attr ref android.R.styleable#TextView_shadowDy
162 * @attr ref android.R.styleable#TextView_shadowRadius
163 * @attr ref android.R.styleable#TextView_autoLink
164 * @attr ref android.R.styleable#TextView_linksClickable
165 * @attr ref android.R.styleable#TextView_numeric
166 * @attr ref android.R.styleable#TextView_digits
167 * @attr ref android.R.styleable#TextView_phoneNumber
168 * @attr ref android.R.styleable#TextView_inputMethod
169 * @attr ref android.R.styleable#TextView_capitalize
170 * @attr ref android.R.styleable#TextView_autoText
171 * @attr ref android.R.styleable#TextView_editable
172 * @attr ref android.R.styleable#TextView_freezesText
173 * @attr ref android.R.styleable#TextView_ellipsize
174 * @attr ref android.R.styleable#TextView_drawableTop
175 * @attr ref android.R.styleable#TextView_drawableBottom
176 * @attr ref android.R.styleable#TextView_drawableRight
177 * @attr ref android.R.styleable#TextView_drawableLeft
178 * @attr ref android.R.styleable#TextView_drawablePadding
179 * @attr ref android.R.styleable#TextView_lineSpacingExtra
180 * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
181 * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
182 * @attr ref android.R.styleable#TextView_inputType
183 * @attr ref android.R.styleable#TextView_imeOptions
184 * @attr ref android.R.styleable#TextView_privateImeOptions
185 * @attr ref android.R.styleable#TextView_imeActionLabel
186 * @attr ref android.R.styleable#TextView_imeActionId
187 * @attr ref android.R.styleable#TextView_editorExtras
188 */
189@RemoteView
190public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
191    static final String LOG_TAG = "TextView";
192    static final boolean DEBUG_EXTRACT = false;
193
194    private static int PRIORITY = 100;
195
196    private ColorStateList mTextColor;
197    private int mCurTextColor;
198    private ColorStateList mHintTextColor;
199    private ColorStateList mLinkTextColor;
200    private int mCurHintTextColor;
201    private boolean mFreezesText;
202    private boolean mFrozenWithFocus;
203    private boolean mTemporaryDetach;
204    private boolean mDispatchTemporaryDetach;
205
206    private boolean mEatTouchRelease = false;
207    private boolean mScrolled = false;
208
209    private Editable.Factory mEditableFactory = Editable.Factory.getInstance();
210    private Spannable.Factory mSpannableFactory = Spannable.Factory.getInstance();
211
212    private float mShadowRadius, mShadowDx, mShadowDy;
213
214    private static final int PREDRAW_NOT_REGISTERED = 0;
215    private static final int PREDRAW_PENDING = 1;
216    private static final int PREDRAW_DONE = 2;
217    private int mPreDrawState = PREDRAW_NOT_REGISTERED;
218
219    private TextUtils.TruncateAt mEllipsize = null;
220
221    // Enum for the "typeface" XML parameter.
222    // TODO: How can we get this from the XML instead of hardcoding it here?
223    private static final int SANS = 1;
224    private static final int SERIF = 2;
225    private static final int MONOSPACE = 3;
226
227    // Bitfield for the "numeric" XML parameter.
228    // TODO: How can we get this from the XML instead of hardcoding it here?
229    private static final int SIGNED = 2;
230    private static final int DECIMAL = 4;
231
232    class Drawables {
233        final Rect mCompoundRect = new Rect();
234        Drawable mDrawableTop, mDrawableBottom, mDrawableLeft, mDrawableRight;
235        int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight;
236        int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight;
237        int mDrawablePadding;
238    }
239    private Drawables mDrawables;
240
241    private CharSequence mError;
242    private boolean mErrorWasChanged;
243    private ErrorPopup mPopup;
244    /**
245     * This flag is set if the TextView tries to display an error before it
246     * is attached to the window (so its position is still unknown).
247     * It causes the error to be shown later, when onAttachedToWindow()
248     * is called.
249     */
250    private boolean mShowErrorAfterAttach;
251
252    private CharWrapper mCharWrapper = null;
253
254    private boolean mSelectionMoved = false;
255    private boolean mTouchFocusSelected = false;
256
257    private Marquee mMarquee;
258    private boolean mRestartMarquee;
259
260    private int mMarqueeRepeatLimit = 3;
261
262    class InputContentType {
263        int imeOptions = EditorInfo.IME_NULL;
264        String privateImeOptions;
265        CharSequence imeActionLabel;
266        int imeActionId;
267        Bundle extras;
268        OnEditorActionListener onEditorActionListener;
269        boolean enterDown;
270    }
271    InputContentType mInputContentType;
272
273    class InputMethodState {
274        Rect mCursorRectInWindow = new Rect();
275        RectF mTmpRectF = new RectF();
276        float[] mTmpOffset = new float[2];
277        ExtractedTextRequest mExtracting;
278        final ExtractedText mTmpExtracted = new ExtractedText();
279        int mBatchEditNesting;
280        boolean mCursorChanged;
281        boolean mSelectionModeChanged;
282        boolean mContentChanged;
283        int mChangedStart, mChangedEnd, mChangedDelta;
284    }
285    InputMethodState mInputMethodState;
286
287    /*
288     * Kick-start the font cache for the zygote process (to pay the cost of
289     * initializing freetype for our default font only once).
290     */
291    static {
292        Paint p = new Paint();
293        p.setAntiAlias(true);
294        // We don't care about the result, just the side-effect of measuring.
295        p.measureText("H");
296    }
297
298    /**
299     * Interface definition for a callback to be invoked when an action is
300     * performed on the editor.
301     */
302    public interface OnEditorActionListener {
303        /**
304         * Called when an action is being performed.
305         *
306         * @param v The view that was clicked.
307         * @param actionId Identifier of the action.  This will be either the
308         * identifier you supplied, or {@link EditorInfo#IME_NULL
309         * EditorInfo.IME_NULL} if being called due to the enter key
310         * being pressed.
311         * @param event If triggered by an enter key, this is the event;
312         * otherwise, this is null.
313         * @return Return true if you have consumed the action, else false.
314         */
315        boolean onEditorAction(TextView v, int actionId, KeyEvent event);
316    }
317
318    public TextView(Context context) {
319        this(context, null);
320    }
321
322    public TextView(Context context,
323                    AttributeSet attrs) {
324        this(context, attrs, com.android.internal.R.attr.textViewStyle);
325    }
326
327    @SuppressWarnings("deprecation")
328    public TextView(Context context,
329                    AttributeSet attrs,
330                    int defStyle) {
331        super(context, attrs, defStyle);
332        mText = "";
333
334        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
335        mTextPaint.density = getResources().getDisplayMetrics().density;
336        mTextPaint.setCompatibilityScaling(
337                getResources().getCompatibilityInfo().applicationScale);
338
339        // If we get the paint from the skin, we should set it to left, since
340        // the layout always wants it to be left.
341        // mTextPaint.setTextAlign(Paint.Align.LEFT);
342
343        mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
344        mHighlightPaint.setCompatibilityScaling(
345                getResources().getCompatibilityInfo().applicationScale);
346
347        mMovement = getDefaultMovementMethod();
348        mTransformation = null;
349
350        TypedArray a =
351            context.obtainStyledAttributes(
352                attrs, com.android.internal.R.styleable.TextView, defStyle, 0);
353
354        int textColorHighlight = 0;
355        ColorStateList textColor = null;
356        ColorStateList textColorHint = null;
357        ColorStateList textColorLink = null;
358        int textSize = 15;
359        int typefaceIndex = -1;
360        int styleIndex = -1;
361
362        /*
363         * Look the appearance up without checking first if it exists because
364         * almost every TextView has one and it greatly simplifies the logic
365         * to be able to parse the appearance first and then let specific tags
366         * for this View override it.
367         */
368        TypedArray appearance = null;
369        int ap = a.getResourceId(com.android.internal.R.styleable.TextView_textAppearance, -1);
370        if (ap != -1) {
371            appearance = context.obtainStyledAttributes(ap,
372                                com.android.internal.R.styleable.
373                                TextAppearance);
374        }
375        if (appearance != null) {
376            int n = appearance.getIndexCount();
377            for (int i = 0; i < n; i++) {
378                int attr = appearance.getIndex(i);
379
380                switch (attr) {
381                case com.android.internal.R.styleable.TextAppearance_textColorHighlight:
382                    textColorHighlight = appearance.getColor(attr, textColorHighlight);
383                    break;
384
385                case com.android.internal.R.styleable.TextAppearance_textColor:
386                    textColor = appearance.getColorStateList(attr);
387                    break;
388
389                case com.android.internal.R.styleable.TextAppearance_textColorHint:
390                    textColorHint = appearance.getColorStateList(attr);
391                    break;
392
393                case com.android.internal.R.styleable.TextAppearance_textColorLink:
394                    textColorLink = appearance.getColorStateList(attr);
395                    break;
396
397                case com.android.internal.R.styleable.TextAppearance_textSize:
398                    textSize = appearance.getDimensionPixelSize(attr, textSize);
399                    break;
400
401                case com.android.internal.R.styleable.TextAppearance_typeface:
402                    typefaceIndex = appearance.getInt(attr, -1);
403                    break;
404
405                case com.android.internal.R.styleable.TextAppearance_textStyle:
406                    styleIndex = appearance.getInt(attr, -1);
407                    break;
408                }
409            }
410
411            appearance.recycle();
412        }
413
414        boolean editable = getDefaultEditable();
415        CharSequence inputMethod = null;
416        int numeric = 0;
417        CharSequence digits = null;
418        boolean phone = false;
419        boolean autotext = false;
420        int autocap = -1;
421        int buffertype = 0;
422        boolean selectallonfocus = false;
423        Drawable drawableLeft = null, drawableTop = null, drawableRight = null,
424            drawableBottom = null;
425        int drawablePadding = 0;
426        int ellipsize = -1;
427        boolean singleLine = false;
428        int maxlength = -1;
429        CharSequence text = "";
430        CharSequence hint = null;
431        int shadowcolor = 0;
432        float dx = 0, dy = 0, r = 0;
433        boolean password = false;
434        int inputType = EditorInfo.TYPE_NULL;
435
436        int n = a.getIndexCount();
437        for (int i = 0; i < n; i++) {
438            int attr = a.getIndex(i);
439
440            switch (attr) {
441            case com.android.internal.R.styleable.TextView_editable:
442                editable = a.getBoolean(attr, editable);
443                break;
444
445            case com.android.internal.R.styleable.TextView_inputMethod:
446                inputMethod = a.getText(attr);
447                break;
448
449            case com.android.internal.R.styleable.TextView_numeric:
450                numeric = a.getInt(attr, numeric);
451                break;
452
453            case com.android.internal.R.styleable.TextView_digits:
454                digits = a.getText(attr);
455                break;
456
457            case com.android.internal.R.styleable.TextView_phoneNumber:
458                phone = a.getBoolean(attr, phone);
459                break;
460
461            case com.android.internal.R.styleable.TextView_autoText:
462                autotext = a.getBoolean(attr, autotext);
463                break;
464
465            case com.android.internal.R.styleable.TextView_capitalize:
466                autocap = a.getInt(attr, autocap);
467                break;
468
469            case com.android.internal.R.styleable.TextView_bufferType:
470                buffertype = a.getInt(attr, buffertype);
471                break;
472
473            case com.android.internal.R.styleable.TextView_selectAllOnFocus:
474                selectallonfocus = a.getBoolean(attr, selectallonfocus);
475                break;
476
477            case com.android.internal.R.styleable.TextView_autoLink:
478                mAutoLinkMask = a.getInt(attr, 0);
479                break;
480
481            case com.android.internal.R.styleable.TextView_linksClickable:
482                mLinksClickable = a.getBoolean(attr, true);
483                break;
484
485            case com.android.internal.R.styleable.TextView_drawableLeft:
486                drawableLeft = a.getDrawable(attr);
487                break;
488
489            case com.android.internal.R.styleable.TextView_drawableTop:
490                drawableTop = a.getDrawable(attr);
491                break;
492
493            case com.android.internal.R.styleable.TextView_drawableRight:
494                drawableRight = a.getDrawable(attr);
495                break;
496
497            case com.android.internal.R.styleable.TextView_drawableBottom:
498                drawableBottom = a.getDrawable(attr);
499                break;
500
501            case com.android.internal.R.styleable.TextView_drawablePadding:
502                drawablePadding = a.getDimensionPixelSize(attr, drawablePadding);
503                break;
504
505            case com.android.internal.R.styleable.TextView_maxLines:
506                setMaxLines(a.getInt(attr, -1));
507                break;
508
509            case com.android.internal.R.styleable.TextView_maxHeight:
510                setMaxHeight(a.getDimensionPixelSize(attr, -1));
511                break;
512
513            case com.android.internal.R.styleable.TextView_lines:
514                setLines(a.getInt(attr, -1));
515                break;
516
517            case com.android.internal.R.styleable.TextView_height:
518                setHeight(a.getDimensionPixelSize(attr, -1));
519                break;
520
521            case com.android.internal.R.styleable.TextView_minLines:
522                setMinLines(a.getInt(attr, -1));
523                break;
524
525            case com.android.internal.R.styleable.TextView_minHeight:
526                setMinHeight(a.getDimensionPixelSize(attr, -1));
527                break;
528
529            case com.android.internal.R.styleable.TextView_maxEms:
530                setMaxEms(a.getInt(attr, -1));
531                break;
532
533            case com.android.internal.R.styleable.TextView_maxWidth:
534                setMaxWidth(a.getDimensionPixelSize(attr, -1));
535                break;
536
537            case com.android.internal.R.styleable.TextView_ems:
538                setEms(a.getInt(attr, -1));
539                break;
540
541            case com.android.internal.R.styleable.TextView_width:
542                setWidth(a.getDimensionPixelSize(attr, -1));
543                break;
544
545            case com.android.internal.R.styleable.TextView_minEms:
546                setMinEms(a.getInt(attr, -1));
547                break;
548
549            case com.android.internal.R.styleable.TextView_minWidth:
550                setMinWidth(a.getDimensionPixelSize(attr, -1));
551                break;
552
553            case com.android.internal.R.styleable.TextView_gravity:
554                setGravity(a.getInt(attr, -1));
555                break;
556
557            case com.android.internal.R.styleable.TextView_hint:
558                hint = a.getText(attr);
559                break;
560
561            case com.android.internal.R.styleable.TextView_text:
562                text = a.getText(attr);
563                break;
564
565            case com.android.internal.R.styleable.TextView_scrollHorizontally:
566                if (a.getBoolean(attr, false)) {
567                    setHorizontallyScrolling(true);
568                }
569                break;
570
571            case com.android.internal.R.styleable.TextView_singleLine:
572                singleLine = a.getBoolean(attr, singleLine);
573                break;
574
575            case com.android.internal.R.styleable.TextView_ellipsize:
576                ellipsize = a.getInt(attr, ellipsize);
577                break;
578
579            case com.android.internal.R.styleable.TextView_marqueeRepeatLimit:
580                setMarqueeRepeatLimit(a.getInt(attr, mMarqueeRepeatLimit));
581                break;
582
583            case com.android.internal.R.styleable.TextView_includeFontPadding:
584                if (!a.getBoolean(attr, true)) {
585                    setIncludeFontPadding(false);
586                }
587                break;
588
589            case com.android.internal.R.styleable.TextView_cursorVisible:
590                if (!a.getBoolean(attr, true)) {
591                    setCursorVisible(false);
592                }
593                break;
594
595            case com.android.internal.R.styleable.TextView_maxLength:
596                maxlength = a.getInt(attr, -1);
597                break;
598
599            case com.android.internal.R.styleable.TextView_textScaleX:
600                setTextScaleX(a.getFloat(attr, 1.0f));
601                break;
602
603            case com.android.internal.R.styleable.TextView_freezesText:
604                mFreezesText = a.getBoolean(attr, false);
605                break;
606
607            case com.android.internal.R.styleable.TextView_shadowColor:
608                shadowcolor = a.getInt(attr, 0);
609                break;
610
611            case com.android.internal.R.styleable.TextView_shadowDx:
612                dx = a.getFloat(attr, 0);
613                break;
614
615            case com.android.internal.R.styleable.TextView_shadowDy:
616                dy = a.getFloat(attr, 0);
617                break;
618
619            case com.android.internal.R.styleable.TextView_shadowRadius:
620                r = a.getFloat(attr, 0);
621                break;
622
623            case com.android.internal.R.styleable.TextView_enabled:
624                setEnabled(a.getBoolean(attr, isEnabled()));
625                break;
626
627            case com.android.internal.R.styleable.TextView_textColorHighlight:
628                textColorHighlight = a.getColor(attr, textColorHighlight);
629                break;
630
631            case com.android.internal.R.styleable.TextView_textColor:
632                textColor = a.getColorStateList(attr);
633                break;
634
635            case com.android.internal.R.styleable.TextView_textColorHint:
636                textColorHint = a.getColorStateList(attr);
637                break;
638
639            case com.android.internal.R.styleable.TextView_textColorLink:
640                textColorLink = a.getColorStateList(attr);
641                break;
642
643            case com.android.internal.R.styleable.TextView_textSize:
644                textSize = a.getDimensionPixelSize(attr, textSize);
645                break;
646
647            case com.android.internal.R.styleable.TextView_typeface:
648                typefaceIndex = a.getInt(attr, typefaceIndex);
649                break;
650
651            case com.android.internal.R.styleable.TextView_textStyle:
652                styleIndex = a.getInt(attr, styleIndex);
653                break;
654
655            case com.android.internal.R.styleable.TextView_password:
656                password = a.getBoolean(attr, password);
657                break;
658
659            case com.android.internal.R.styleable.TextView_lineSpacingExtra:
660                mSpacingAdd = a.getDimensionPixelSize(attr, (int) mSpacingAdd);
661                break;
662
663            case com.android.internal.R.styleable.TextView_lineSpacingMultiplier:
664                mSpacingMult = a.getFloat(attr, mSpacingMult);
665                break;
666
667            case com.android.internal.R.styleable.TextView_inputType:
668                inputType = a.getInt(attr, mInputType);
669                break;
670
671            case com.android.internal.R.styleable.TextView_imeOptions:
672                if (mInputContentType == null) {
673                    mInputContentType = new InputContentType();
674                }
675                mInputContentType.imeOptions = a.getInt(attr,
676                        mInputContentType.imeOptions);
677                break;
678
679            case com.android.internal.R.styleable.TextView_imeActionLabel:
680                if (mInputContentType == null) {
681                    mInputContentType = new InputContentType();
682                }
683                mInputContentType.imeActionLabel = a.getText(attr);
684                break;
685
686            case com.android.internal.R.styleable.TextView_imeActionId:
687                if (mInputContentType == null) {
688                    mInputContentType = new InputContentType();
689                }
690                mInputContentType.imeActionId = a.getInt(attr,
691                        mInputContentType.imeActionId);
692                break;
693
694            case com.android.internal.R.styleable.TextView_privateImeOptions:
695                setPrivateImeOptions(a.getString(attr));
696                break;
697
698            case com.android.internal.R.styleable.TextView_editorExtras:
699                try {
700                    setInputExtras(a.getResourceId(attr, 0));
701                } catch (XmlPullParserException e) {
702                    Log.w(LOG_TAG, "Failure reading input extras", e);
703                } catch (IOException e) {
704                    Log.w(LOG_TAG, "Failure reading input extras", e);
705                }
706                break;
707            }
708        }
709        a.recycle();
710
711        BufferType bufferType = BufferType.EDITABLE;
712
713        if ((inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION))
714                == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)) {
715            password = true;
716        }
717
718        if (inputMethod != null) {
719            Class<?> c;
720
721            try {
722                c = Class.forName(inputMethod.toString());
723            } catch (ClassNotFoundException ex) {
724                throw new RuntimeException(ex);
725            }
726
727            try {
728                mInput = (KeyListener) c.newInstance();
729            } catch (InstantiationException ex) {
730                throw new RuntimeException(ex);
731            } catch (IllegalAccessException ex) {
732                throw new RuntimeException(ex);
733            }
734            try {
735                mInputType = inputType != EditorInfo.TYPE_NULL
736                        ? inputType
737                        : mInput.getInputType();
738            } catch (IncompatibleClassChangeError e) {
739                mInputType = EditorInfo.TYPE_CLASS_TEXT;
740            }
741        } else if (digits != null) {
742            mInput = DigitsKeyListener.getInstance(digits.toString());
743            // If no input type was specified, we will default to generic
744            // text, since we can't tell the IME about the set of digits
745            // that was selected.
746            mInputType = inputType != EditorInfo.TYPE_NULL
747                    ? inputType : EditorInfo.TYPE_CLASS_TEXT;
748        } else if (inputType != EditorInfo.TYPE_NULL) {
749            setInputType(inputType, true);
750            singleLine = (inputType&(EditorInfo.TYPE_MASK_CLASS
751                            | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) !=
752                    (EditorInfo.TYPE_CLASS_TEXT
753                            | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE);
754        } else if (phone) {
755            mInput = DialerKeyListener.getInstance();
756            mInputType = inputType = EditorInfo.TYPE_CLASS_PHONE;
757        } else if (numeric != 0) {
758            mInput = DigitsKeyListener.getInstance((numeric & SIGNED) != 0,
759                                                   (numeric & DECIMAL) != 0);
760            inputType = EditorInfo.TYPE_CLASS_NUMBER;
761            if ((numeric & SIGNED) != 0) {
762                inputType |= EditorInfo.TYPE_NUMBER_FLAG_SIGNED;
763            }
764            if ((numeric & DECIMAL) != 0) {
765                inputType |= EditorInfo.TYPE_NUMBER_FLAG_DECIMAL;
766            }
767            mInputType = inputType;
768        } else if (autotext || autocap != -1) {
769            TextKeyListener.Capitalize cap;
770
771            inputType = EditorInfo.TYPE_CLASS_TEXT;
772            if (!singleLine) {
773                inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
774            }
775
776            switch (autocap) {
777            case 1:
778                cap = TextKeyListener.Capitalize.SENTENCES;
779                inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES;
780                break;
781
782            case 2:
783                cap = TextKeyListener.Capitalize.WORDS;
784                inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
785                break;
786
787            case 3:
788                cap = TextKeyListener.Capitalize.CHARACTERS;
789                inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS;
790                break;
791
792            default:
793                cap = TextKeyListener.Capitalize.NONE;
794                break;
795            }
796
797            mInput = TextKeyListener.getInstance(autotext, cap);
798            mInputType = inputType;
799        } else if (editable) {
800            mInput = TextKeyListener.getInstance();
801            mInputType = EditorInfo.TYPE_CLASS_TEXT;
802            if (!singleLine) {
803                mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
804            }
805        } else {
806            mInput = null;
807
808            switch (buffertype) {
809                case 0:
810                    bufferType = BufferType.NORMAL;
811                    break;
812                case 1:
813                    bufferType = BufferType.SPANNABLE;
814                    break;
815                case 2:
816                    bufferType = BufferType.EDITABLE;
817                    break;
818            }
819        }
820
821        if (password && (mInputType&EditorInfo.TYPE_MASK_CLASS)
822                == EditorInfo.TYPE_CLASS_TEXT) {
823            mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
824                | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
825        }
826
827        if (selectallonfocus) {
828            mSelectAllOnFocus = true;
829
830            if (bufferType == BufferType.NORMAL)
831                bufferType = BufferType.SPANNABLE;
832        }
833
834        setCompoundDrawablesWithIntrinsicBounds(
835            drawableLeft, drawableTop, drawableRight, drawableBottom);
836        setCompoundDrawablePadding(drawablePadding);
837
838        if (singleLine) {
839            setSingleLine();
840
841            if (mInput == null && ellipsize < 0) {
842                ellipsize = 3; // END
843            }
844        }
845
846        switch (ellipsize) {
847            case 1:
848                setEllipsize(TextUtils.TruncateAt.START);
849                break;
850            case 2:
851                setEllipsize(TextUtils.TruncateAt.MIDDLE);
852                break;
853            case 3:
854                setEllipsize(TextUtils.TruncateAt.END);
855                break;
856            case 4:
857                setHorizontalFadingEdgeEnabled(true);
858                setEllipsize(TextUtils.TruncateAt.MARQUEE);
859                break;
860        }
861
862        setTextColor(textColor != null ? textColor : ColorStateList.valueOf(0xFF000000));
863        setHintTextColor(textColorHint);
864        setLinkTextColor(textColorLink);
865        if (textColorHighlight != 0) {
866            setHighlightColor(textColorHighlight);
867        }
868        setRawTextSize(textSize);
869
870        if (password) {
871            setTransformationMethod(PasswordTransformationMethod.getInstance());
872            typefaceIndex = MONOSPACE;
873        } else if ((mInputType&(EditorInfo.TYPE_MASK_CLASS
874                |EditorInfo.TYPE_MASK_VARIATION))
875                == (EditorInfo.TYPE_CLASS_TEXT
876                        |EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)) {
877            typefaceIndex = MONOSPACE;
878        }
879
880        setTypefaceByIndex(typefaceIndex, styleIndex);
881
882        if (shadowcolor != 0) {
883            setShadowLayer(r, dx, dy, shadowcolor);
884        }
885
886        if (maxlength >= 0) {
887            setFilters(new InputFilter[] { new InputFilter.LengthFilter(maxlength) });
888        } else {
889            setFilters(NO_FILTERS);
890        }
891
892        setText(text, bufferType);
893        if (hint != null) setHint(hint);
894
895        /*
896         * Views are not normally focusable unless specified to be.
897         * However, TextViews that have input or movement methods *are*
898         * focusable by default.
899         */
900        a = context.obtainStyledAttributes(attrs,
901                                           com.android.internal.R.styleable.View,
902                                           defStyle, 0);
903
904        boolean focusable = mMovement != null || mInput != null;
905        boolean clickable = focusable;
906        boolean longClickable = focusable;
907
908        n = a.getIndexCount();
909        for (int i = 0; i < n; i++) {
910            int attr = a.getIndex(i);
911
912            switch (attr) {
913            case com.android.internal.R.styleable.View_focusable:
914                focusable = a.getBoolean(attr, focusable);
915                break;
916
917            case com.android.internal.R.styleable.View_clickable:
918                clickable = a.getBoolean(attr, clickable);
919                break;
920
921            case com.android.internal.R.styleable.View_longClickable:
922                longClickable = a.getBoolean(attr, longClickable);
923                break;
924            }
925        }
926        a.recycle();
927
928        setFocusable(focusable);
929        setClickable(clickable);
930        setLongClickable(longClickable);
931
932        prepareCursorControllers();
933    }
934
935    private void setTypefaceByIndex(int typefaceIndex, int styleIndex) {
936        Typeface tf = null;
937        switch (typefaceIndex) {
938            case SANS:
939                tf = Typeface.SANS_SERIF;
940                break;
941
942            case SERIF:
943                tf = Typeface.SERIF;
944                break;
945
946            case MONOSPACE:
947                tf = Typeface.MONOSPACE;
948                break;
949        }
950
951        setTypeface(tf, styleIndex);
952    }
953
954    /**
955     * Sets the typeface and style in which the text should be displayed,
956     * and turns on the fake bold and italic bits in the Paint if the
957     * Typeface that you provided does not have all the bits in the
958     * style that you specified.
959     *
960     * @attr ref android.R.styleable#TextView_typeface
961     * @attr ref android.R.styleable#TextView_textStyle
962     */
963    public void setTypeface(Typeface tf, int style) {
964        if (style > 0) {
965            if (tf == null) {
966                tf = Typeface.defaultFromStyle(style);
967            } else {
968                tf = Typeface.create(tf, style);
969            }
970
971            setTypeface(tf);
972            // now compute what (if any) algorithmic styling is needed
973            int typefaceStyle = tf != null ? tf.getStyle() : 0;
974            int need = style & ~typefaceStyle;
975            mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
976            mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
977        } else {
978            mTextPaint.setFakeBoldText(false);
979            mTextPaint.setTextSkewX(0);
980            setTypeface(tf);
981        }
982    }
983
984    /**
985     * Subclasses override this to specify that they have a KeyListener
986     * by default even if not specifically called for in the XML options.
987     */
988    protected boolean getDefaultEditable() {
989        return false;
990    }
991
992    /**
993     * Subclasses override this to specify a default movement method.
994     */
995    protected MovementMethod getDefaultMovementMethod() {
996        return null;
997    }
998
999    /**
1000     * Return the text the TextView is displaying. If setText() was called with
1001     * an argument of BufferType.SPANNABLE or BufferType.EDITABLE, you can cast
1002     * the return value from this method to Spannable or Editable, respectively.
1003     *
1004     * Note: The content of the return value should not be modified. If you want
1005     * a modifiable one, you should make your own copy first.
1006     */
1007    @ViewDebug.CapturedViewProperty
1008    public CharSequence getText() {
1009        return mText;
1010    }
1011
1012    /**
1013     * Returns the length, in characters, of the text managed by this TextView
1014     */
1015    public int length() {
1016        return mText.length();
1017    }
1018
1019    /**
1020     * Return the text the TextView is displaying as an Editable object.  If
1021     * the text is not editable, null is returned.
1022     *
1023     * @see #getText
1024     */
1025    public Editable getEditableText() {
1026        return (mText instanceof Editable) ? (Editable)mText : null;
1027    }
1028
1029    /**
1030     * @return the height of one standard line in pixels.  Note that markup
1031     * within the text can cause individual lines to be taller or shorter
1032     * than this height, and the layout may contain additional first-
1033     * or last-line padding.
1034     */
1035    public int getLineHeight() {
1036        return FastMath.round(mTextPaint.getFontMetricsInt(null) * mSpacingMult
1037                          + mSpacingAdd);
1038    }
1039
1040    /**
1041     * @return the Layout that is currently being used to display the text.
1042     * This can be null if the text or width has recently changes.
1043     */
1044    public final Layout getLayout() {
1045        return mLayout;
1046    }
1047
1048    /**
1049     * @return the current key listener for this TextView.
1050     * This will frequently be null for non-EditText TextViews.
1051     */
1052    public final KeyListener getKeyListener() {
1053        return mInput;
1054    }
1055
1056    /**
1057     * Sets the key listener to be used with this TextView.  This can be null
1058     * to disallow user input.  Note that this method has significant and
1059     * subtle interactions with soft keyboards and other input method:
1060     * see {@link KeyListener#getInputType() KeyListener.getContentType()}
1061     * for important details.  Calling this method will replace the current
1062     * content type of the text view with the content type returned by the
1063     * key listener.
1064     * <p>
1065     * Be warned that if you want a TextView with a key listener or movement
1066     * method not to be focusable, or if you want a TextView without a
1067     * key listener or movement method to be focusable, you must call
1068     * {@link #setFocusable} again after calling this to get the focusability
1069     * back the way you want it.
1070     *
1071     * @attr ref android.R.styleable#TextView_numeric
1072     * @attr ref android.R.styleable#TextView_digits
1073     * @attr ref android.R.styleable#TextView_phoneNumber
1074     * @attr ref android.R.styleable#TextView_inputMethod
1075     * @attr ref android.R.styleable#TextView_capitalize
1076     * @attr ref android.R.styleable#TextView_autoText
1077     */
1078    public void setKeyListener(KeyListener input) {
1079        setKeyListenerOnly(input);
1080        fixFocusableAndClickableSettings();
1081
1082        if (input != null) {
1083            try {
1084                mInputType = mInput.getInputType();
1085            } catch (IncompatibleClassChangeError e) {
1086                mInputType = EditorInfo.TYPE_CLASS_TEXT;
1087            }
1088            if ((mInputType&EditorInfo.TYPE_MASK_CLASS)
1089                    == EditorInfo.TYPE_CLASS_TEXT) {
1090                if (mSingleLine) {
1091                    mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
1092                } else {
1093                    mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
1094                }
1095            }
1096        } else {
1097            mInputType = EditorInfo.TYPE_NULL;
1098        }
1099
1100        InputMethodManager imm = InputMethodManager.peekInstance();
1101        if (imm != null) imm.restartInput(this);
1102    }
1103
1104    private void setKeyListenerOnly(KeyListener input) {
1105        mInput = input;
1106        if (mInput != null && !(mText instanceof Editable))
1107            setText(mText);
1108
1109        setFilters((Editable) mText, mFilters);
1110    }
1111
1112    /**
1113     * @return the movement method being used for this TextView.
1114     * This will frequently be null for non-EditText TextViews.
1115     */
1116    public final MovementMethod getMovementMethod() {
1117        return mMovement;
1118    }
1119
1120    /**
1121     * Sets the movement method (arrow key handler) to be used for
1122     * this TextView.  This can be null to disallow using the arrow keys
1123     * to move the cursor or scroll the view.
1124     * <p>
1125     * Be warned that if you want a TextView with a key listener or movement
1126     * method not to be focusable, or if you want a TextView without a
1127     * key listener or movement method to be focusable, you must call
1128     * {@link #setFocusable} again after calling this to get the focusability
1129     * back the way you want it.
1130     */
1131    public final void setMovementMethod(MovementMethod movement) {
1132        mMovement = movement;
1133
1134        if (mMovement != null && !(mText instanceof Spannable))
1135            setText(mText);
1136
1137        fixFocusableAndClickableSettings();
1138
1139        // SelectionModifierCursorController depends on canSelectText, which depends on mMovement
1140        prepareCursorControllers();
1141    }
1142
1143    private void fixFocusableAndClickableSettings() {
1144        if ((mMovement != null) || mInput != null) {
1145            setFocusable(true);
1146            setClickable(true);
1147            setLongClickable(true);
1148        } else {
1149            setFocusable(false);
1150            setClickable(false);
1151            setLongClickable(false);
1152        }
1153    }
1154
1155    /**
1156     * @return the current transformation method for this TextView.
1157     * This will frequently be null except for single-line and password
1158     * fields.
1159     */
1160    public final TransformationMethod getTransformationMethod() {
1161        return mTransformation;
1162    }
1163
1164    /**
1165     * Sets the transformation that is applied to the text that this
1166     * TextView is displaying.
1167     *
1168     * @attr ref android.R.styleable#TextView_password
1169     * @attr ref android.R.styleable#TextView_singleLine
1170     */
1171    public final void setTransformationMethod(TransformationMethod method) {
1172        if (method == mTransformation) {
1173            // Avoid the setText() below if the transformation is
1174            // the same.
1175            return;
1176        }
1177        if (mTransformation != null) {
1178            if (mText instanceof Spannable) {
1179                ((Spannable) mText).removeSpan(mTransformation);
1180            }
1181        }
1182
1183        mTransformation = method;
1184
1185        setText(mText);
1186    }
1187
1188    /**
1189     * Returns the top padding of the view, plus space for the top
1190     * Drawable if any.
1191     */
1192    public int getCompoundPaddingTop() {
1193        final Drawables dr = mDrawables;
1194        if (dr == null || dr.mDrawableTop == null) {
1195            return mPaddingTop;
1196        } else {
1197            return mPaddingTop + dr.mDrawablePadding + dr.mDrawableSizeTop;
1198        }
1199    }
1200
1201    /**
1202     * Returns the bottom padding of the view, plus space for the bottom
1203     * Drawable if any.
1204     */
1205    public int getCompoundPaddingBottom() {
1206        final Drawables dr = mDrawables;
1207        if (dr == null || dr.mDrawableBottom == null) {
1208            return mPaddingBottom;
1209        } else {
1210            return mPaddingBottom + dr.mDrawablePadding + dr.mDrawableSizeBottom;
1211        }
1212    }
1213
1214    /**
1215     * Returns the left padding of the view, plus space for the left
1216     * Drawable if any.
1217     */
1218    public int getCompoundPaddingLeft() {
1219        final Drawables dr = mDrawables;
1220        if (dr == null || dr.mDrawableLeft == null) {
1221            return mPaddingLeft;
1222        } else {
1223            return mPaddingLeft + dr.mDrawablePadding + dr.mDrawableSizeLeft;
1224        }
1225    }
1226
1227    /**
1228     * Returns the right padding of the view, plus space for the right
1229     * Drawable if any.
1230     */
1231    public int getCompoundPaddingRight() {
1232        final Drawables dr = mDrawables;
1233        if (dr == null || dr.mDrawableRight == null) {
1234            return mPaddingRight;
1235        } else {
1236            return mPaddingRight + dr.mDrawablePadding + dr.mDrawableSizeRight;
1237        }
1238    }
1239
1240    /**
1241     * Returns the extended top padding of the view, including both the
1242     * top Drawable if any and any extra space to keep more than maxLines
1243     * of text from showing.  It is only valid to call this after measuring.
1244     */
1245    public int getExtendedPaddingTop() {
1246        if (mMaxMode != LINES) {
1247            return getCompoundPaddingTop();
1248        }
1249
1250        if (mLayout.getLineCount() <= mMaximum) {
1251            return getCompoundPaddingTop();
1252        }
1253
1254        int top = getCompoundPaddingTop();
1255        int bottom = getCompoundPaddingBottom();
1256        int viewht = getHeight() - top - bottom;
1257        int layoutht = mLayout.getLineTop(mMaximum);
1258
1259        if (layoutht >= viewht) {
1260            return top;
1261        }
1262
1263        final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
1264        if (gravity == Gravity.TOP) {
1265            return top;
1266        } else if (gravity == Gravity.BOTTOM) {
1267            return top + viewht - layoutht;
1268        } else { // (gravity == Gravity.CENTER_VERTICAL)
1269            return top + (viewht - layoutht) / 2;
1270        }
1271    }
1272
1273    /**
1274     * Returns the extended bottom padding of the view, including both the
1275     * bottom Drawable if any and any extra space to keep more than maxLines
1276     * of text from showing.  It is only valid to call this after measuring.
1277     */
1278    public int getExtendedPaddingBottom() {
1279        if (mMaxMode != LINES) {
1280            return getCompoundPaddingBottom();
1281        }
1282
1283        if (mLayout.getLineCount() <= mMaximum) {
1284            return getCompoundPaddingBottom();
1285        }
1286
1287        int top = getCompoundPaddingTop();
1288        int bottom = getCompoundPaddingBottom();
1289        int viewht = getHeight() - top - bottom;
1290        int layoutht = mLayout.getLineTop(mMaximum);
1291
1292        if (layoutht >= viewht) {
1293            return bottom;
1294        }
1295
1296        final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
1297        if (gravity == Gravity.TOP) {
1298            return bottom + viewht - layoutht;
1299        } else if (gravity == Gravity.BOTTOM) {
1300            return bottom;
1301        } else { // (gravity == Gravity.CENTER_VERTICAL)
1302            return bottom + (viewht - layoutht) / 2;
1303        }
1304    }
1305
1306    /**
1307     * Returns the total left padding of the view, including the left
1308     * Drawable if any.
1309     */
1310    public int getTotalPaddingLeft() {
1311        return getCompoundPaddingLeft();
1312    }
1313
1314    /**
1315     * Returns the total right padding of the view, including the right
1316     * Drawable if any.
1317     */
1318    public int getTotalPaddingRight() {
1319        return getCompoundPaddingRight();
1320    }
1321
1322    /**
1323     * Returns the total top padding of the view, including the top
1324     * Drawable if any, the extra space to keep more than maxLines
1325     * from showing, and the vertical offset for gravity, if any.
1326     */
1327    public int getTotalPaddingTop() {
1328        return getExtendedPaddingTop() + getVerticalOffset(true);
1329    }
1330
1331    /**
1332     * Returns the total bottom padding of the view, including the bottom
1333     * Drawable if any, the extra space to keep more than maxLines
1334     * from showing, and the vertical offset for gravity, if any.
1335     */
1336    public int getTotalPaddingBottom() {
1337        return getExtendedPaddingBottom() + getBottomVerticalOffset(true);
1338    }
1339
1340    /**
1341     * Sets the Drawables (if any) to appear to the left of, above,
1342     * to the right of, and below the text.  Use null if you do not
1343     * want a Drawable there.  The Drawables must already have had
1344     * {@link Drawable#setBounds} called.
1345     *
1346     * @attr ref android.R.styleable#TextView_drawableLeft
1347     * @attr ref android.R.styleable#TextView_drawableTop
1348     * @attr ref android.R.styleable#TextView_drawableRight
1349     * @attr ref android.R.styleable#TextView_drawableBottom
1350     */
1351    public void setCompoundDrawables(Drawable left, Drawable top,
1352                                     Drawable right, Drawable bottom) {
1353        Drawables dr = mDrawables;
1354
1355        final boolean drawables = left != null || top != null
1356                || right != null || bottom != null;
1357
1358        if (!drawables) {
1359            // Clearing drawables...  can we free the data structure?
1360            if (dr != null) {
1361                if (dr.mDrawablePadding == 0) {
1362                    mDrawables = null;
1363                } else {
1364                    // We need to retain the last set padding, so just clear
1365                    // out all of the fields in the existing structure.
1366                    if (dr.mDrawableLeft != null) dr.mDrawableLeft.setCallback(null);
1367                    dr.mDrawableLeft = null;
1368                    if (dr.mDrawableTop != null) dr.mDrawableTop.setCallback(null);
1369                    dr.mDrawableTop = null;
1370                    if (dr.mDrawableRight != null) dr.mDrawableRight.setCallback(null);
1371                    dr.mDrawableRight = null;
1372                    if (dr.mDrawableBottom != null) dr.mDrawableBottom.setCallback(null);
1373                    dr.mDrawableBottom = null;
1374                    dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
1375                    dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
1376                    dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
1377                    dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
1378                }
1379            }
1380        } else {
1381            if (dr == null) {
1382                mDrawables = dr = new Drawables();
1383            }
1384
1385            if (dr.mDrawableLeft != left && dr.mDrawableLeft != null) {
1386                dr.mDrawableLeft.setCallback(null);
1387            }
1388            dr.mDrawableLeft = left;
1389
1390            if (dr.mDrawableTop != top && dr.mDrawableTop != null) {
1391                dr.mDrawableTop.setCallback(null);
1392            }
1393            dr.mDrawableTop = top;
1394
1395            if (dr.mDrawableRight != right && dr.mDrawableRight != null) {
1396                dr.mDrawableRight.setCallback(null);
1397            }
1398            dr.mDrawableRight = right;
1399
1400            if (dr.mDrawableBottom != bottom && dr.mDrawableBottom != null) {
1401                dr.mDrawableBottom.setCallback(null);
1402            }
1403            dr.mDrawableBottom = bottom;
1404
1405            final Rect compoundRect = dr.mCompoundRect;
1406            int[] state;
1407
1408            state = getDrawableState();
1409
1410            if (left != null) {
1411                left.setState(state);
1412                left.copyBounds(compoundRect);
1413                left.setCallback(this);
1414                dr.mDrawableSizeLeft = compoundRect.width();
1415                dr.mDrawableHeightLeft = compoundRect.height();
1416            } else {
1417                dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
1418            }
1419
1420            if (right != null) {
1421                right.setState(state);
1422                right.copyBounds(compoundRect);
1423                right.setCallback(this);
1424                dr.mDrawableSizeRight = compoundRect.width();
1425                dr.mDrawableHeightRight = compoundRect.height();
1426            } else {
1427                dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
1428            }
1429
1430            if (top != null) {
1431                top.setState(state);
1432                top.copyBounds(compoundRect);
1433                top.setCallback(this);
1434                dr.mDrawableSizeTop = compoundRect.height();
1435                dr.mDrawableWidthTop = compoundRect.width();
1436            } else {
1437                dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
1438            }
1439
1440            if (bottom != null) {
1441                bottom.setState(state);
1442                bottom.copyBounds(compoundRect);
1443                bottom.setCallback(this);
1444                dr.mDrawableSizeBottom = compoundRect.height();
1445                dr.mDrawableWidthBottom = compoundRect.width();
1446            } else {
1447                dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
1448            }
1449        }
1450
1451        invalidate();
1452        requestLayout();
1453    }
1454
1455    /**
1456     * Sets the Drawables (if any) to appear to the left of, above,
1457     * to the right of, and below the text.  Use 0 if you do not
1458     * want a Drawable there. The Drawables' bounds will be set to
1459     * their intrinsic bounds.
1460     *
1461     * @param left Resource identifier of the left Drawable.
1462     * @param top Resource identifier of the top Drawable.
1463     * @param right Resource identifier of the right Drawable.
1464     * @param bottom Resource identifier of the bottom Drawable.
1465     *
1466     * @attr ref android.R.styleable#TextView_drawableLeft
1467     * @attr ref android.R.styleable#TextView_drawableTop
1468     * @attr ref android.R.styleable#TextView_drawableRight
1469     * @attr ref android.R.styleable#TextView_drawableBottom
1470     */
1471    public void setCompoundDrawablesWithIntrinsicBounds(int left, int top, int right, int bottom) {
1472        final Resources resources = getContext().getResources();
1473        setCompoundDrawablesWithIntrinsicBounds(left != 0 ? resources.getDrawable(left) : null,
1474                top != 0 ? resources.getDrawable(top) : null,
1475                right != 0 ? resources.getDrawable(right) : null,
1476                bottom != 0 ? resources.getDrawable(bottom) : null);
1477    }
1478
1479    /**
1480     * Sets the Drawables (if any) to appear to the left of, above,
1481     * to the right of, and below the text.  Use null if you do not
1482     * want a Drawable there. The Drawables' bounds will be set to
1483     * their intrinsic bounds.
1484     *
1485     * @attr ref android.R.styleable#TextView_drawableLeft
1486     * @attr ref android.R.styleable#TextView_drawableTop
1487     * @attr ref android.R.styleable#TextView_drawableRight
1488     * @attr ref android.R.styleable#TextView_drawableBottom
1489     */
1490    public void setCompoundDrawablesWithIntrinsicBounds(Drawable left, Drawable top,
1491            Drawable right, Drawable bottom) {
1492
1493        if (left != null) {
1494            left.setBounds(0, 0, left.getIntrinsicWidth(), left.getIntrinsicHeight());
1495        }
1496        if (right != null) {
1497            right.setBounds(0, 0, right.getIntrinsicWidth(), right.getIntrinsicHeight());
1498        }
1499        if (top != null) {
1500            top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight());
1501        }
1502        if (bottom != null) {
1503            bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight());
1504        }
1505        setCompoundDrawables(left, top, right, bottom);
1506    }
1507
1508    /**
1509     * Returns drawables for the left, top, right, and bottom borders.
1510     */
1511    public Drawable[] getCompoundDrawables() {
1512        final Drawables dr = mDrawables;
1513        if (dr != null) {
1514            return new Drawable[] {
1515                dr.mDrawableLeft, dr.mDrawableTop, dr.mDrawableRight, dr.mDrawableBottom
1516            };
1517        } else {
1518            return new Drawable[] { null, null, null, null };
1519        }
1520    }
1521
1522    /**
1523     * Sets the size of the padding between the compound drawables and
1524     * the text.
1525     *
1526     * @attr ref android.R.styleable#TextView_drawablePadding
1527     */
1528    public void setCompoundDrawablePadding(int pad) {
1529        Drawables dr = mDrawables;
1530        if (pad == 0) {
1531            if (dr != null) {
1532                dr.mDrawablePadding = pad;
1533            }
1534        } else {
1535            if (dr == null) {
1536                mDrawables = dr = new Drawables();
1537            }
1538            dr.mDrawablePadding = pad;
1539        }
1540
1541        invalidate();
1542        requestLayout();
1543    }
1544
1545    /**
1546     * Returns the padding between the compound drawables and the text.
1547     */
1548    public int getCompoundDrawablePadding() {
1549        final Drawables dr = mDrawables;
1550        return dr != null ? dr.mDrawablePadding : 0;
1551    }
1552
1553    @Override
1554    public void setPadding(int left, int top, int right, int bottom) {
1555        if (left != mPaddingLeft ||
1556            right != mPaddingRight ||
1557            top != mPaddingTop ||
1558            bottom != mPaddingBottom) {
1559            nullLayouts();
1560        }
1561
1562        // the super call will requestLayout()
1563        super.setPadding(left, top, right, bottom);
1564        invalidate();
1565    }
1566
1567    /**
1568     * Gets the autolink mask of the text.  See {@link
1569     * android.text.util.Linkify#ALL Linkify.ALL} and peers for
1570     * possible values.
1571     *
1572     * @attr ref android.R.styleable#TextView_autoLink
1573     */
1574    public final int getAutoLinkMask() {
1575        return mAutoLinkMask;
1576    }
1577
1578    /**
1579     * Sets the text color, size, style, hint color, and highlight color
1580     * from the specified TextAppearance resource.
1581     */
1582    public void setTextAppearance(Context context, int resid) {
1583        TypedArray appearance =
1584            context.obtainStyledAttributes(resid,
1585                                           com.android.internal.R.styleable.TextAppearance);
1586
1587        int color;
1588        ColorStateList colors;
1589        int ts;
1590
1591        color = appearance.getColor(com.android.internal.R.styleable.TextAppearance_textColorHighlight, 0);
1592        if (color != 0) {
1593            setHighlightColor(color);
1594        }
1595
1596        colors = appearance.getColorStateList(com.android.internal.R.styleable.
1597                                              TextAppearance_textColor);
1598        if (colors != null) {
1599            setTextColor(colors);
1600        }
1601
1602        ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
1603                                              TextAppearance_textSize, 0);
1604        if (ts != 0) {
1605            setRawTextSize(ts);
1606        }
1607
1608        colors = appearance.getColorStateList(com.android.internal.R.styleable.
1609                                              TextAppearance_textColorHint);
1610        if (colors != null) {
1611            setHintTextColor(colors);
1612        }
1613
1614        colors = appearance.getColorStateList(com.android.internal.R.styleable.
1615                                              TextAppearance_textColorLink);
1616        if (colors != null) {
1617            setLinkTextColor(colors);
1618        }
1619
1620        int typefaceIndex, styleIndex;
1621
1622        typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
1623                                          TextAppearance_typeface, -1);
1624        styleIndex = appearance.getInt(com.android.internal.R.styleable.
1625                                       TextAppearance_textStyle, -1);
1626
1627        setTypefaceByIndex(typefaceIndex, styleIndex);
1628        appearance.recycle();
1629    }
1630
1631    /**
1632     * @return the size (in pixels) of the default text size in this TextView.
1633     */
1634    public float getTextSize() {
1635        return mTextPaint.getTextSize();
1636    }
1637
1638    /**
1639     * Set the default text size to the given value, interpreted as "scaled
1640     * pixel" units.  This size is adjusted based on the current density and
1641     * user font size preference.
1642     *
1643     * @param size The scaled pixel size.
1644     *
1645     * @attr ref android.R.styleable#TextView_textSize
1646     */
1647    @android.view.RemotableViewMethod
1648    public void setTextSize(float size) {
1649        setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
1650    }
1651
1652    /**
1653     * Set the default text size to a given unit and value.  See {@link
1654     * TypedValue} for the possible dimension units.
1655     *
1656     * @param unit The desired dimension unit.
1657     * @param size The desired size in the given units.
1658     *
1659     * @attr ref android.R.styleable#TextView_textSize
1660     */
1661    public void setTextSize(int unit, float size) {
1662        Context c = getContext();
1663        Resources r;
1664
1665        if (c == null)
1666            r = Resources.getSystem();
1667        else
1668            r = c.getResources();
1669
1670        setRawTextSize(TypedValue.applyDimension(
1671            unit, size, r.getDisplayMetrics()));
1672    }
1673
1674    private void setRawTextSize(float size) {
1675        if (size != mTextPaint.getTextSize()) {
1676            mTextPaint.setTextSize(size);
1677
1678            if (mLayout != null) {
1679                nullLayouts();
1680                requestLayout();
1681                invalidate();
1682            }
1683        }
1684    }
1685
1686    /**
1687     * @return the extent by which text is currently being stretched
1688     * horizontally.  This will usually be 1.
1689     */
1690    public float getTextScaleX() {
1691        return mTextPaint.getTextScaleX();
1692    }
1693
1694    /**
1695     * Sets the extent by which text should be stretched horizontally.
1696     *
1697     * @attr ref android.R.styleable#TextView_textScaleX
1698     */
1699    @android.view.RemotableViewMethod
1700    public void setTextScaleX(float size) {
1701        if (size != mTextPaint.getTextScaleX()) {
1702            mUserSetTextScaleX = true;
1703            mTextPaint.setTextScaleX(size);
1704
1705            if (mLayout != null) {
1706                nullLayouts();
1707                requestLayout();
1708                invalidate();
1709            }
1710        }
1711    }
1712
1713    /**
1714     * Sets the typeface and style in which the text should be displayed.
1715     * Note that not all Typeface families actually have bold and italic
1716     * variants, so you may need to use
1717     * {@link #setTypeface(Typeface, int)} to get the appearance
1718     * that you actually want.
1719     *
1720     * @attr ref android.R.styleable#TextView_typeface
1721     * @attr ref android.R.styleable#TextView_textStyle
1722     */
1723    public void setTypeface(Typeface tf) {
1724        if (mTextPaint.getTypeface() != tf) {
1725            mTextPaint.setTypeface(tf);
1726
1727            if (mLayout != null) {
1728                nullLayouts();
1729                requestLayout();
1730                invalidate();
1731            }
1732        }
1733    }
1734
1735    /**
1736     * @return the current typeface and style in which the text is being
1737     * displayed.
1738     */
1739    public Typeface getTypeface() {
1740        return mTextPaint.getTypeface();
1741    }
1742
1743    /**
1744     * Sets the text color for all the states (normal, selected,
1745     * focused) to be this color.
1746     *
1747     * @attr ref android.R.styleable#TextView_textColor
1748     */
1749    @android.view.RemotableViewMethod
1750    public void setTextColor(int color) {
1751        mTextColor = ColorStateList.valueOf(color);
1752        updateTextColors();
1753    }
1754
1755    /**
1756     * Sets the text color.
1757     *
1758     * @attr ref android.R.styleable#TextView_textColor
1759     */
1760    public void setTextColor(ColorStateList colors) {
1761        if (colors == null) {
1762            throw new NullPointerException();
1763        }
1764
1765        mTextColor = colors;
1766        updateTextColors();
1767    }
1768
1769    /**
1770     * Return the set of text colors.
1771     *
1772     * @return Returns the set of text colors.
1773     */
1774    public final ColorStateList getTextColors() {
1775        return mTextColor;
1776    }
1777
1778    /**
1779     * <p>Return the current color selected for normal text.</p>
1780     *
1781     * @return Returns the current text color.
1782     */
1783    public final int getCurrentTextColor() {
1784        return mCurTextColor;
1785    }
1786
1787    /**
1788     * Sets the color used to display the selection highlight.
1789     *
1790     * @attr ref android.R.styleable#TextView_textColorHighlight
1791     */
1792    @android.view.RemotableViewMethod
1793    public void setHighlightColor(int color) {
1794        if (mHighlightColor != color) {
1795            mHighlightColor = color;
1796            invalidate();
1797        }
1798    }
1799
1800    /**
1801     * Gives the text a shadow of the specified radius and color, the specified
1802     * distance from its normal position.
1803     *
1804     * @attr ref android.R.styleable#TextView_shadowColor
1805     * @attr ref android.R.styleable#TextView_shadowDx
1806     * @attr ref android.R.styleable#TextView_shadowDy
1807     * @attr ref android.R.styleable#TextView_shadowRadius
1808     */
1809    public void setShadowLayer(float radius, float dx, float dy, int color) {
1810        mTextPaint.setShadowLayer(radius, dx, dy, color);
1811
1812        mShadowRadius = radius;
1813        mShadowDx = dx;
1814        mShadowDy = dy;
1815
1816        invalidate();
1817    }
1818
1819    /**
1820     * @return the base paint used for the text.  Please use this only to
1821     * consult the Paint's properties and not to change them.
1822     */
1823    public TextPaint getPaint() {
1824        return mTextPaint;
1825    }
1826
1827    /**
1828     * Sets the autolink mask of the text.  See {@link
1829     * android.text.util.Linkify#ALL Linkify.ALL} and peers for
1830     * possible values.
1831     *
1832     * @attr ref android.R.styleable#TextView_autoLink
1833     */
1834    @android.view.RemotableViewMethod
1835    public final void setAutoLinkMask(int mask) {
1836        mAutoLinkMask = mask;
1837    }
1838
1839    /**
1840     * Sets whether the movement method will automatically be set to
1841     * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been
1842     * set to nonzero and links are detected in {@link #setText}.
1843     * The default is true.
1844     *
1845     * @attr ref android.R.styleable#TextView_linksClickable
1846     */
1847    @android.view.RemotableViewMethod
1848    public final void setLinksClickable(boolean whether) {
1849        mLinksClickable = whether;
1850    }
1851
1852    /**
1853     * Returns whether the movement method will automatically be set to
1854     * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been
1855     * set to nonzero and links are detected in {@link #setText}.
1856     * The default is true.
1857     *
1858     * @attr ref android.R.styleable#TextView_linksClickable
1859     */
1860    public final boolean getLinksClickable() {
1861        return mLinksClickable;
1862    }
1863
1864    /**
1865     * Returns the list of URLSpans attached to the text
1866     * (by {@link Linkify} or otherwise) if any.  You can call
1867     * {@link URLSpan#getURL} on them to find where they link to
1868     * or use {@link Spanned#getSpanStart} and {@link Spanned#getSpanEnd}
1869     * to find the region of the text they are attached to.
1870     */
1871    public URLSpan[] getUrls() {
1872        if (mText instanceof Spanned) {
1873            return ((Spanned) mText).getSpans(0, mText.length(), URLSpan.class);
1874        } else {
1875            return new URLSpan[0];
1876        }
1877    }
1878
1879    /**
1880     * Sets the color of the hint text.
1881     *
1882     * @attr ref android.R.styleable#TextView_textColorHint
1883     */
1884    @android.view.RemotableViewMethod
1885    public final void setHintTextColor(int color) {
1886        mHintTextColor = ColorStateList.valueOf(color);
1887        updateTextColors();
1888    }
1889
1890    /**
1891     * Sets the color of the hint text.
1892     *
1893     * @attr ref android.R.styleable#TextView_textColorHint
1894     */
1895    public final void setHintTextColor(ColorStateList colors) {
1896        mHintTextColor = colors;
1897        updateTextColors();
1898    }
1899
1900    /**
1901     * <p>Return the color used to paint the hint text.</p>
1902     *
1903     * @return Returns the list of hint text colors.
1904     */
1905    public final ColorStateList getHintTextColors() {
1906        return mHintTextColor;
1907    }
1908
1909    /**
1910     * <p>Return the current color selected to paint the hint text.</p>
1911     *
1912     * @return Returns the current hint text color.
1913     */
1914    public final int getCurrentHintTextColor() {
1915        return mHintTextColor != null ? mCurHintTextColor : mCurTextColor;
1916    }
1917
1918    /**
1919     * Sets the color of links in the text.
1920     *
1921     * @attr ref android.R.styleable#TextView_textColorLink
1922     */
1923    @android.view.RemotableViewMethod
1924    public final void setLinkTextColor(int color) {
1925        mLinkTextColor = ColorStateList.valueOf(color);
1926        updateTextColors();
1927    }
1928
1929    /**
1930     * Sets the color of links in the text.
1931     *
1932     * @attr ref android.R.styleable#TextView_textColorLink
1933     */
1934    public final void setLinkTextColor(ColorStateList colors) {
1935        mLinkTextColor = colors;
1936        updateTextColors();
1937    }
1938
1939    /**
1940     * <p>Returns the color used to paint links in the text.</p>
1941     *
1942     * @return Returns the list of link text colors.
1943     */
1944    public final ColorStateList getLinkTextColors() {
1945        return mLinkTextColor;
1946    }
1947
1948    /**
1949     * Sets the horizontal alignment of the text and the
1950     * vertical gravity that will be used when there is extra space
1951     * in the TextView beyond what is required for the text itself.
1952     *
1953     * @see android.view.Gravity
1954     * @attr ref android.R.styleable#TextView_gravity
1955     */
1956    public void setGravity(int gravity) {
1957        if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) {
1958            gravity |= Gravity.LEFT;
1959        }
1960        if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
1961            gravity |= Gravity.TOP;
1962        }
1963
1964        boolean newLayout = false;
1965
1966        if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) !=
1967            (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK)) {
1968            newLayout = true;
1969        }
1970
1971        if (gravity != mGravity) {
1972            invalidate();
1973        }
1974
1975        mGravity = gravity;
1976
1977        if (mLayout != null && newLayout) {
1978            // XXX this is heavy-handed because no actual content changes.
1979            int want = mLayout.getWidth();
1980            int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
1981
1982            makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
1983                          mRight - mLeft - getCompoundPaddingLeft() -
1984                          getCompoundPaddingRight(), true);
1985        }
1986    }
1987
1988    /**
1989     * Returns the horizontal and vertical alignment of this TextView.
1990     *
1991     * @see android.view.Gravity
1992     * @attr ref android.R.styleable#TextView_gravity
1993     */
1994    public int getGravity() {
1995        return mGravity;
1996    }
1997
1998    /**
1999     * @return the flags on the Paint being used to display the text.
2000     * @see Paint#getFlags
2001     */
2002    public int getPaintFlags() {
2003        return mTextPaint.getFlags();
2004    }
2005
2006    /**
2007     * Sets flags on the Paint being used to display the text and
2008     * reflows the text if they are different from the old flags.
2009     * @see Paint#setFlags
2010     */
2011    @android.view.RemotableViewMethod
2012    public void setPaintFlags(int flags) {
2013        if (mTextPaint.getFlags() != flags) {
2014            mTextPaint.setFlags(flags);
2015
2016            if (mLayout != null) {
2017                nullLayouts();
2018                requestLayout();
2019                invalidate();
2020            }
2021        }
2022    }
2023
2024    /**
2025     * Sets whether the text should be allowed to be wider than the
2026     * View is.  If false, it will be wrapped to the width of the View.
2027     *
2028     * @attr ref android.R.styleable#TextView_scrollHorizontally
2029     */
2030    public void setHorizontallyScrolling(boolean whether) {
2031        mHorizontallyScrolling = whether;
2032
2033        if (mLayout != null) {
2034            nullLayouts();
2035            requestLayout();
2036            invalidate();
2037        }
2038    }
2039
2040    /**
2041     * Makes the TextView at least this many lines tall
2042     *
2043     * @attr ref android.R.styleable#TextView_minLines
2044     */
2045    @android.view.RemotableViewMethod
2046    public void setMinLines(int minlines) {
2047        mMinimum = minlines;
2048        mMinMode = LINES;
2049
2050        requestLayout();
2051        invalidate();
2052    }
2053
2054    /**
2055     * Makes the TextView at least this many pixels tall
2056     *
2057     * @attr ref android.R.styleable#TextView_minHeight
2058     */
2059    @android.view.RemotableViewMethod
2060    public void setMinHeight(int minHeight) {
2061        mMinimum = minHeight;
2062        mMinMode = PIXELS;
2063
2064        requestLayout();
2065        invalidate();
2066    }
2067
2068    /**
2069     * Makes the TextView at most this many lines tall
2070     *
2071     * @attr ref android.R.styleable#TextView_maxLines
2072     */
2073    @android.view.RemotableViewMethod
2074    public void setMaxLines(int maxlines) {
2075        mMaximum = maxlines;
2076        mMaxMode = LINES;
2077
2078        requestLayout();
2079        invalidate();
2080    }
2081
2082    /**
2083     * Makes the TextView at most this many pixels tall
2084     *
2085     * @attr ref android.R.styleable#TextView_maxHeight
2086     */
2087    @android.view.RemotableViewMethod
2088    public void setMaxHeight(int maxHeight) {
2089        mMaximum = maxHeight;
2090        mMaxMode = PIXELS;
2091
2092        requestLayout();
2093        invalidate();
2094    }
2095
2096    /**
2097     * Makes the TextView exactly this many lines tall
2098     *
2099     * @attr ref android.R.styleable#TextView_lines
2100     */
2101    @android.view.RemotableViewMethod
2102    public void setLines(int lines) {
2103        mMaximum = mMinimum = lines;
2104        mMaxMode = mMinMode = LINES;
2105
2106        requestLayout();
2107        invalidate();
2108    }
2109
2110    /**
2111     * Makes the TextView exactly this many pixels tall.
2112     * You could do the same thing by specifying this number in the
2113     * LayoutParams.
2114     *
2115     * @attr ref android.R.styleable#TextView_height
2116     */
2117    @android.view.RemotableViewMethod
2118    public void setHeight(int pixels) {
2119        mMaximum = mMinimum = pixels;
2120        mMaxMode = mMinMode = PIXELS;
2121
2122        requestLayout();
2123        invalidate();
2124    }
2125
2126    /**
2127     * Makes the TextView at least this many ems wide
2128     *
2129     * @attr ref android.R.styleable#TextView_minEms
2130     */
2131    @android.view.RemotableViewMethod
2132    public void setMinEms(int minems) {
2133        mMinWidth = minems;
2134        mMinWidthMode = EMS;
2135
2136        requestLayout();
2137        invalidate();
2138    }
2139
2140    /**
2141     * Makes the TextView at least this many pixels wide
2142     *
2143     * @attr ref android.R.styleable#TextView_minWidth
2144     */
2145    @android.view.RemotableViewMethod
2146    public void setMinWidth(int minpixels) {
2147        mMinWidth = minpixels;
2148        mMinWidthMode = PIXELS;
2149
2150        requestLayout();
2151        invalidate();
2152    }
2153
2154    /**
2155     * Makes the TextView at most this many ems wide
2156     *
2157     * @attr ref android.R.styleable#TextView_maxEms
2158     */
2159    @android.view.RemotableViewMethod
2160    public void setMaxEms(int maxems) {
2161        mMaxWidth = maxems;
2162        mMaxWidthMode = EMS;
2163
2164        requestLayout();
2165        invalidate();
2166    }
2167
2168    /**
2169     * Makes the TextView at most this many pixels wide
2170     *
2171     * @attr ref android.R.styleable#TextView_maxWidth
2172     */
2173    @android.view.RemotableViewMethod
2174    public void setMaxWidth(int maxpixels) {
2175        mMaxWidth = maxpixels;
2176        mMaxWidthMode = PIXELS;
2177
2178        requestLayout();
2179        invalidate();
2180    }
2181
2182    /**
2183     * Makes the TextView exactly this many ems wide
2184     *
2185     * @attr ref android.R.styleable#TextView_ems
2186     */
2187    @android.view.RemotableViewMethod
2188    public void setEms(int ems) {
2189        mMaxWidth = mMinWidth = ems;
2190        mMaxWidthMode = mMinWidthMode = EMS;
2191
2192        requestLayout();
2193        invalidate();
2194    }
2195
2196    /**
2197     * Makes the TextView exactly this many pixels wide.
2198     * You could do the same thing by specifying this number in the
2199     * LayoutParams.
2200     *
2201     * @attr ref android.R.styleable#TextView_width
2202     */
2203    @android.view.RemotableViewMethod
2204    public void setWidth(int pixels) {
2205        mMaxWidth = mMinWidth = pixels;
2206        mMaxWidthMode = mMinWidthMode = PIXELS;
2207
2208        requestLayout();
2209        invalidate();
2210    }
2211
2212
2213    /**
2214     * Sets line spacing for this TextView.  Each line will have its height
2215     * multiplied by <code>mult</code> and have <code>add</code> added to it.
2216     *
2217     * @attr ref android.R.styleable#TextView_lineSpacingExtra
2218     * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
2219     */
2220    public void setLineSpacing(float add, float mult) {
2221        mSpacingMult = mult;
2222        mSpacingAdd = add;
2223
2224        if (mLayout != null) {
2225            nullLayouts();
2226            requestLayout();
2227            invalidate();
2228        }
2229    }
2230
2231    /**
2232     * Convenience method: Append the specified text to the TextView's
2233     * display buffer, upgrading it to BufferType.EDITABLE if it was
2234     * not already editable.
2235     */
2236    public final void append(CharSequence text) {
2237        append(text, 0, text.length());
2238    }
2239
2240    /**
2241     * Convenience method: Append the specified text slice to the TextView's
2242     * display buffer, upgrading it to BufferType.EDITABLE if it was
2243     * not already editable.
2244     */
2245    public void append(CharSequence text, int start, int end) {
2246        if (!(mText instanceof Editable)) {
2247            setText(mText, BufferType.EDITABLE);
2248        }
2249
2250        ((Editable) mText).append(text, start, end);
2251    }
2252
2253    private void updateTextColors() {
2254        boolean inval = false;
2255        int color = mTextColor.getColorForState(getDrawableState(), 0);
2256        if (color != mCurTextColor) {
2257            mCurTextColor = color;
2258            inval = true;
2259        }
2260        if (mLinkTextColor != null) {
2261            color = mLinkTextColor.getColorForState(getDrawableState(), 0);
2262            if (color != mTextPaint.linkColor) {
2263                mTextPaint.linkColor = color;
2264                inval = true;
2265            }
2266        }
2267        if (mHintTextColor != null) {
2268            color = mHintTextColor.getColorForState(getDrawableState(), 0);
2269            if (color != mCurHintTextColor && mText.length() == 0) {
2270                mCurHintTextColor = color;
2271                inval = true;
2272            }
2273        }
2274        if (inval) {
2275            invalidate();
2276        }
2277    }
2278
2279    @Override
2280    protected void drawableStateChanged() {
2281        super.drawableStateChanged();
2282        if (mTextColor != null && mTextColor.isStateful()
2283                || (mHintTextColor != null && mHintTextColor.isStateful())
2284                || (mLinkTextColor != null && mLinkTextColor.isStateful())) {
2285            updateTextColors();
2286        }
2287
2288        final Drawables dr = mDrawables;
2289        if (dr != null) {
2290            int[] state = getDrawableState();
2291            if (dr.mDrawableTop != null && dr.mDrawableTop.isStateful()) {
2292                dr.mDrawableTop.setState(state);
2293            }
2294            if (dr.mDrawableBottom != null && dr.mDrawableBottom.isStateful()) {
2295                dr.mDrawableBottom.setState(state);
2296            }
2297            if (dr.mDrawableLeft != null && dr.mDrawableLeft.isStateful()) {
2298                dr.mDrawableLeft.setState(state);
2299            }
2300            if (dr.mDrawableRight != null && dr.mDrawableRight.isStateful()) {
2301                dr.mDrawableRight.setState(state);
2302            }
2303        }
2304    }
2305
2306    /**
2307     * User interface state that is stored by TextView for implementing
2308     * {@link View#onSaveInstanceState}.
2309     */
2310    public static class SavedState extends BaseSavedState {
2311        int selStart;
2312        int selEnd;
2313        CharSequence text;
2314        boolean frozenWithFocus;
2315        CharSequence error;
2316
2317        SavedState(Parcelable superState) {
2318            super(superState);
2319        }
2320
2321        @Override
2322        public void writeToParcel(Parcel out, int flags) {
2323            super.writeToParcel(out, flags);
2324            out.writeInt(selStart);
2325            out.writeInt(selEnd);
2326            out.writeInt(frozenWithFocus ? 1 : 0);
2327            TextUtils.writeToParcel(text, out, flags);
2328
2329            if (error == null) {
2330                out.writeInt(0);
2331            } else {
2332                out.writeInt(1);
2333                TextUtils.writeToParcel(error, out, flags);
2334            }
2335        }
2336
2337        @Override
2338        public String toString() {
2339            String str = "TextView.SavedState{"
2340                    + Integer.toHexString(System.identityHashCode(this))
2341                    + " start=" + selStart + " end=" + selEnd;
2342            if (text != null) {
2343                str += " text=" + text;
2344            }
2345            return str + "}";
2346        }
2347
2348        @SuppressWarnings("hiding")
2349        public static final Parcelable.Creator<SavedState> CREATOR
2350                = new Parcelable.Creator<SavedState>() {
2351            public SavedState createFromParcel(Parcel in) {
2352                return new SavedState(in);
2353            }
2354
2355            public SavedState[] newArray(int size) {
2356                return new SavedState[size];
2357            }
2358        };
2359
2360        private SavedState(Parcel in) {
2361            super(in);
2362            selStart = in.readInt();
2363            selEnd = in.readInt();
2364            frozenWithFocus = (in.readInt() != 0);
2365            text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
2366
2367            if (in.readInt() != 0) {
2368                error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
2369            }
2370        }
2371    }
2372
2373    @Override
2374    public Parcelable onSaveInstanceState() {
2375        Parcelable superState = super.onSaveInstanceState();
2376
2377        // Save state if we are forced to
2378        boolean save = mFreezesText;
2379        int start = 0;
2380        int end = 0;
2381
2382        if (mText != null) {
2383            start = getSelectionStart();
2384            end = getSelectionEnd();
2385            if (start >= 0 || end >= 0) {
2386                // Or save state if there is a selection
2387                save = true;
2388            }
2389        }
2390
2391        if (save) {
2392            SavedState ss = new SavedState(superState);
2393            // XXX Should also save the current scroll position!
2394            ss.selStart = start;
2395            ss.selEnd = end;
2396
2397            if (mText instanceof Spanned) {
2398                /*
2399                 * Calling setText() strips off any ChangeWatchers;
2400                 * strip them now to avoid leaking references.
2401                 * But do it to a copy so that if there are any
2402                 * further changes to the text of this view, it
2403                 * won't get into an inconsistent state.
2404                 */
2405
2406                Spannable sp = new SpannableString(mText);
2407
2408                for (ChangeWatcher cw :
2409                     sp.getSpans(0, sp.length(), ChangeWatcher.class)) {
2410                    sp.removeSpan(cw);
2411                }
2412
2413                ss.text = sp;
2414            } else {
2415                ss.text = mText.toString();
2416            }
2417
2418            if (isFocused() && start >= 0 && end >= 0) {
2419                ss.frozenWithFocus = true;
2420            }
2421
2422            ss.error = mError;
2423
2424            return ss;
2425        }
2426
2427        return superState;
2428    }
2429
2430    @Override
2431    public void onRestoreInstanceState(Parcelable state) {
2432        if (!(state instanceof SavedState)) {
2433            super.onRestoreInstanceState(state);
2434            return;
2435        }
2436
2437        SavedState ss = (SavedState)state;
2438        super.onRestoreInstanceState(ss.getSuperState());
2439
2440        // XXX restore buffer type too, as well as lots of other stuff
2441        if (ss.text != null) {
2442            setText(ss.text);
2443        }
2444
2445        if (ss.selStart >= 0 && ss.selEnd >= 0) {
2446            if (mText instanceof Spannable) {
2447                int len = mText.length();
2448
2449                if (ss.selStart > len || ss.selEnd > len) {
2450                    String restored = "";
2451
2452                    if (ss.text != null) {
2453                        restored = "(restored) ";
2454                    }
2455
2456                    Log.e(LOG_TAG, "Saved cursor position " + ss.selStart +
2457                          "/" + ss.selEnd + " out of range for " + restored +
2458                          "text " + mText);
2459                } else {
2460                    Selection.setSelection((Spannable) mText, ss.selStart,
2461                                           ss.selEnd);
2462
2463                    if (ss.frozenWithFocus) {
2464                        mFrozenWithFocus = true;
2465                    }
2466                }
2467            }
2468        }
2469
2470        if (ss.error != null) {
2471            final CharSequence error = ss.error;
2472            // Display the error later, after the first layout pass
2473            post(new Runnable() {
2474                public void run() {
2475                    setError(error);
2476                }
2477            });
2478        }
2479    }
2480
2481    /**
2482     * Control whether this text view saves its entire text contents when
2483     * freezing to an icicle, in addition to dynamic state such as cursor
2484     * position.  By default this is false, not saving the text.  Set to true
2485     * if the text in the text view is not being saved somewhere else in
2486     * persistent storage (such as in a content provider) so that if the
2487     * view is later thawed the user will not lose their data.
2488     *
2489     * @param freezesText Controls whether a frozen icicle should include the
2490     * entire text data: true to include it, false to not.
2491     *
2492     * @attr ref android.R.styleable#TextView_freezesText
2493     */
2494    @android.view.RemotableViewMethod
2495    public void setFreezesText(boolean freezesText) {
2496        mFreezesText = freezesText;
2497    }
2498
2499    /**
2500     * Return whether this text view is including its entire text contents
2501     * in frozen icicles.
2502     *
2503     * @return Returns true if text is included, false if it isn't.
2504     *
2505     * @see #setFreezesText
2506     */
2507    public boolean getFreezesText() {
2508        return mFreezesText;
2509    }
2510
2511    ///////////////////////////////////////////////////////////////////////////
2512
2513    /**
2514     * Sets the Factory used to create new Editables.
2515     */
2516    public final void setEditableFactory(Editable.Factory factory) {
2517        mEditableFactory = factory;
2518        setText(mText);
2519    }
2520
2521    /**
2522     * Sets the Factory used to create new Spannables.
2523     */
2524    public final void setSpannableFactory(Spannable.Factory factory) {
2525        mSpannableFactory = factory;
2526        setText(mText);
2527    }
2528
2529    /**
2530     * Sets the string value of the TextView. TextView <em>does not</em> accept
2531     * HTML-like formatting, which you can do with text strings in XML resource files.
2532     * To style your strings, attach android.text.style.* objects to a
2533     * {@link android.text.SpannableString SpannableString}, or see the
2534     * <a href="{@docRoot}guide/topics/resources/available-resources.html#stringresources">
2535     * Available Resource Types</a> documentation for an example of setting
2536     * formatted text in the XML resource file.
2537     *
2538     * @attr ref android.R.styleable#TextView_text
2539     */
2540    @android.view.RemotableViewMethod
2541    public final void setText(CharSequence text) {
2542        setText(text, mBufferType);
2543    }
2544
2545    /**
2546     * Like {@link #setText(CharSequence)},
2547     * except that the cursor position (if any) is retained in the new text.
2548     *
2549     * @param text The new text to place in the text view.
2550     *
2551     * @see #setText(CharSequence)
2552     */
2553    @android.view.RemotableViewMethod
2554    public final void setTextKeepState(CharSequence text) {
2555        setTextKeepState(text, mBufferType);
2556    }
2557
2558    /**
2559     * Sets the text that this TextView is to display (see
2560     * {@link #setText(CharSequence)}) and also sets whether it is stored
2561     * in a styleable/spannable buffer and whether it is editable.
2562     *
2563     * @attr ref android.R.styleable#TextView_text
2564     * @attr ref android.R.styleable#TextView_bufferType
2565     */
2566    public void setText(CharSequence text, BufferType type) {
2567        setText(text, type, true, 0);
2568
2569        if (mCharWrapper != null) {
2570            mCharWrapper.mChars = null;
2571        }
2572    }
2573
2574    private void setText(CharSequence text, BufferType type,
2575                         boolean notifyBefore, int oldlen) {
2576        if (text == null) {
2577            text = "";
2578        }
2579
2580        if (!mUserSetTextScaleX) mTextPaint.setTextScaleX(1.0f);
2581
2582        if (text instanceof Spanned &&
2583            ((Spanned) text).getSpanStart(TextUtils.TruncateAt.MARQUEE) >= 0) {
2584            setHorizontalFadingEdgeEnabled(true);
2585            setEllipsize(TextUtils.TruncateAt.MARQUEE);
2586        }
2587
2588        int n = mFilters.length;
2589        for (int i = 0; i < n; i++) {
2590            CharSequence out = mFilters[i].filter(text, 0, text.length(),
2591                                                  EMPTY_SPANNED, 0, 0);
2592            if (out != null) {
2593                text = out;
2594            }
2595        }
2596
2597        if (notifyBefore) {
2598            if (mText != null) {
2599                oldlen = mText.length();
2600                sendBeforeTextChanged(mText, 0, oldlen, text.length());
2601            } else {
2602                sendBeforeTextChanged("", 0, 0, text.length());
2603            }
2604        }
2605
2606        boolean needEditableForNotification = false;
2607
2608        if (mListeners != null && mListeners.size() != 0) {
2609            needEditableForNotification = true;
2610        }
2611
2612        if (type == BufferType.EDITABLE || mInput != null ||
2613            needEditableForNotification) {
2614            Editable t = mEditableFactory.newEditable(text);
2615            text = t;
2616            setFilters(t, mFilters);
2617            InputMethodManager imm = InputMethodManager.peekInstance();
2618            if (imm != null) imm.restartInput(this);
2619        } else if (type == BufferType.SPANNABLE || mMovement != null) {
2620            text = mSpannableFactory.newSpannable(text);
2621        } else if (!(text instanceof CharWrapper)) {
2622            text = TextUtils.stringOrSpannedString(text);
2623        }
2624
2625        if (mAutoLinkMask != 0) {
2626            Spannable s2;
2627
2628            if (type == BufferType.EDITABLE || text instanceof Spannable) {
2629                s2 = (Spannable) text;
2630            } else {
2631                s2 = mSpannableFactory.newSpannable(text);
2632            }
2633
2634            if (Linkify.addLinks(s2, mAutoLinkMask)) {
2635                text = s2;
2636                type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE;
2637
2638                /*
2639                 * We must go ahead and set the text before changing the
2640                 * movement method, because setMovementMethod() may call
2641                 * setText() again to try to upgrade the buffer type.
2642                 */
2643                mText = text;
2644
2645                if (mLinksClickable) {
2646                    setMovementMethod(LinkMovementMethod.getInstance());
2647                }
2648            }
2649        }
2650
2651        mBufferType = type;
2652        mText = text;
2653
2654        if (mTransformation == null)
2655            mTransformed = text;
2656        else
2657            mTransformed = mTransformation.getTransformation(text, this);
2658
2659        final int textLength = text.length();
2660
2661        if (text instanceof Spannable) {
2662            Spannable sp = (Spannable) text;
2663
2664            // Remove any ChangeWatchers that might have come
2665            // from other TextViews.
2666            final ChangeWatcher[] watchers = sp.getSpans(0, sp.length(), ChangeWatcher.class);
2667            final int count = watchers.length;
2668            for (int i = 0; i < count; i++)
2669                sp.removeSpan(watchers[i]);
2670
2671            if (mChangeWatcher == null)
2672                mChangeWatcher = new ChangeWatcher();
2673
2674            sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE |
2675                       (PRIORITY << Spanned.SPAN_PRIORITY_SHIFT));
2676
2677            if (mInput != null) {
2678                sp.setSpan(mInput, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
2679            }
2680
2681            if (mTransformation != null) {
2682                sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
2683
2684            }
2685
2686            if (mMovement != null) {
2687                mMovement.initialize(this, (Spannable) text);
2688
2689                /*
2690                 * Initializing the movement method will have set the
2691                 * selection, so reset mSelectionMoved to keep that from
2692                 * interfering with the normal on-focus selection-setting.
2693                 */
2694                mSelectionMoved = false;
2695            }
2696        }
2697
2698        if (mLayout != null) {
2699            checkForRelayout();
2700        }
2701
2702        sendOnTextChanged(text, 0, oldlen, textLength);
2703        onTextChanged(text, 0, oldlen, textLength);
2704
2705        if (needEditableForNotification) {
2706            sendAfterTextChanged((Editable) text);
2707        }
2708
2709        // SelectionModifierCursorController depends on canSelectText, which depends on text
2710        prepareCursorControllers();
2711    }
2712
2713    /**
2714     * Sets the TextView to display the specified slice of the specified
2715     * char array.  You must promise that you will not change the contents
2716     * of the array except for right before another call to setText(),
2717     * since the TextView has no way to know that the text
2718     * has changed and that it needs to invalidate and re-layout.
2719     */
2720    public final void setText(char[] text, int start, int len) {
2721        int oldlen = 0;
2722
2723        if (start < 0 || len < 0 || start + len > text.length) {
2724            throw new IndexOutOfBoundsException(start + ", " + len);
2725        }
2726
2727        /*
2728         * We must do the before-notification here ourselves because if
2729         * the old text is a CharWrapper we destroy it before calling
2730         * into the normal path.
2731         */
2732        if (mText != null) {
2733            oldlen = mText.length();
2734            sendBeforeTextChanged(mText, 0, oldlen, len);
2735        } else {
2736            sendBeforeTextChanged("", 0, 0, len);
2737        }
2738
2739        if (mCharWrapper == null) {
2740            mCharWrapper = new CharWrapper(text, start, len);
2741        } else {
2742            mCharWrapper.set(text, start, len);
2743        }
2744
2745        setText(mCharWrapper, mBufferType, false, oldlen);
2746    }
2747
2748    private static class CharWrapper
2749            implements CharSequence, GetChars, GraphicsOperations {
2750        private char[] mChars;
2751        private int mStart, mLength;
2752
2753        public CharWrapper(char[] chars, int start, int len) {
2754            mChars = chars;
2755            mStart = start;
2756            mLength = len;
2757        }
2758
2759        /* package */ void set(char[] chars, int start, int len) {
2760            mChars = chars;
2761            mStart = start;
2762            mLength = len;
2763        }
2764
2765        public int length() {
2766            return mLength;
2767        }
2768
2769        public char charAt(int off) {
2770            return mChars[off + mStart];
2771        }
2772
2773        @Override
2774        public String toString() {
2775            return new String(mChars, mStart, mLength);
2776        }
2777
2778        public CharSequence subSequence(int start, int end) {
2779            if (start < 0 || end < 0 || start > mLength || end > mLength) {
2780                throw new IndexOutOfBoundsException(start + ", " + end);
2781            }
2782
2783            return new String(mChars, start + mStart, end - start);
2784        }
2785
2786        public void getChars(int start, int end, char[] buf, int off) {
2787            if (start < 0 || end < 0 || start > mLength || end > mLength) {
2788                throw new IndexOutOfBoundsException(start + ", " + end);
2789            }
2790
2791            System.arraycopy(mChars, start + mStart, buf, off, end - start);
2792        }
2793
2794        public void drawText(Canvas c, int start, int end,
2795                             float x, float y, Paint p) {
2796            c.drawText(mChars, start + mStart, end - start, x, y, p);
2797        }
2798
2799        public float measureText(int start, int end, Paint p) {
2800            return p.measureText(mChars, start + mStart, end - start);
2801        }
2802
2803        public int getTextWidths(int start, int end, float[] widths, Paint p) {
2804            return p.getTextWidths(mChars, start + mStart, end - start, widths);
2805        }
2806    }
2807
2808    /**
2809     * Like {@link #setText(CharSequence, android.widget.TextView.BufferType)},
2810     * except that the cursor position (if any) is retained in the new text.
2811     *
2812     * @see #setText(CharSequence, android.widget.TextView.BufferType)
2813     */
2814    public final void setTextKeepState(CharSequence text, BufferType type) {
2815        int start = getSelectionStart();
2816        int end = getSelectionEnd();
2817        int len = text.length();
2818
2819        setText(text, type);
2820
2821        if (start >= 0 || end >= 0) {
2822            if (mText instanceof Spannable) {
2823                Selection.setSelection((Spannable) mText,
2824                                       Math.max(0, Math.min(start, len)),
2825                                       Math.max(0, Math.min(end, len)));
2826            }
2827        }
2828    }
2829
2830    @android.view.RemotableViewMethod
2831    public final void setText(int resid) {
2832        setText(getContext().getResources().getText(resid));
2833    }
2834
2835    public final void setText(int resid, BufferType type) {
2836        setText(getContext().getResources().getText(resid), type);
2837    }
2838
2839    /**
2840     * Sets the text to be displayed when the text of the TextView is empty.
2841     * Null means to use the normal empty text. The hint does not currently
2842     * participate in determining the size of the view.
2843     *
2844     * @attr ref android.R.styleable#TextView_hint
2845     */
2846    @android.view.RemotableViewMethod
2847    public final void setHint(CharSequence hint) {
2848        mHint = TextUtils.stringOrSpannedString(hint);
2849
2850        if (mLayout != null) {
2851            checkForRelayout();
2852        }
2853
2854        if (mText.length() == 0) {
2855            invalidate();
2856        }
2857    }
2858
2859    /**
2860     * Sets the text to be displayed when the text of the TextView is empty,
2861     * from a resource.
2862     *
2863     * @attr ref android.R.styleable#TextView_hint
2864     */
2865    @android.view.RemotableViewMethod
2866    public final void setHint(int resid) {
2867        setHint(getContext().getResources().getText(resid));
2868    }
2869
2870    /**
2871     * Returns the hint that is displayed when the text of the TextView
2872     * is empty.
2873     *
2874     * @attr ref android.R.styleable#TextView_hint
2875     */
2876    @ViewDebug.CapturedViewProperty
2877    public CharSequence getHint() {
2878        return mHint;
2879    }
2880
2881    /**
2882     * Set the type of the content with a constant as defined for
2883     * {@link EditorInfo#inputType}.  This will take care of changing
2884     * the key listener, by calling {@link #setKeyListener(KeyListener)}, to
2885     * match the given content type.  If the given content type is
2886     * {@link EditorInfo#TYPE_NULL} then a soft keyboard will
2887     * not be displayed for this text view.
2888     *
2889     * @see #getInputType()
2890     * @see #setRawInputType(int)
2891     * @see android.text.InputType
2892     * @attr ref android.R.styleable#TextView_inputType
2893     */
2894    public void setInputType(int type) {
2895        final boolean wasPassword = isPasswordInputType(mInputType);
2896        final boolean wasVisiblePassword = isVisiblePasswordInputType(mInputType);
2897        setInputType(type, false);
2898        final boolean isPassword = isPasswordInputType(type);
2899        final boolean isVisiblePassword = isVisiblePasswordInputType(type);
2900        boolean forceUpdate = false;
2901        if (isPassword) {
2902            setTransformationMethod(PasswordTransformationMethod.getInstance());
2903            setTypefaceByIndex(MONOSPACE, 0);
2904        } else if (isVisiblePassword) {
2905            if (mTransformation == PasswordTransformationMethod.getInstance()) {
2906                forceUpdate = true;
2907            }
2908            setTypefaceByIndex(MONOSPACE, 0);
2909        } else if (wasPassword || wasVisiblePassword) {
2910            // not in password mode, clean up typeface and transformation
2911            setTypefaceByIndex(-1, -1);
2912            if (mTransformation == PasswordTransformationMethod.getInstance()) {
2913                forceUpdate = true;
2914            }
2915        }
2916
2917        boolean multiLine = (type&(EditorInfo.TYPE_MASK_CLASS
2918                        | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) ==
2919                (EditorInfo.TYPE_CLASS_TEXT
2920                        | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE);
2921
2922        // We need to update the single line mode if it has changed or we
2923        // were previously in password mode.
2924        if (mSingleLine == multiLine || forceUpdate) {
2925            // Change single line mode, but only change the transformation if
2926            // we are not in password mode.
2927            applySingleLine(!multiLine, !isPassword);
2928        }
2929
2930        InputMethodManager imm = InputMethodManager.peekInstance();
2931        if (imm != null) imm.restartInput(this);
2932    }
2933
2934    private boolean isPasswordInputType(int inputType) {
2935        final int variation = inputType & (EditorInfo.TYPE_MASK_CLASS
2936                | EditorInfo.TYPE_MASK_VARIATION);
2937        return variation
2938                == (EditorInfo.TYPE_CLASS_TEXT
2939                        | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD);
2940    }
2941
2942    private boolean isVisiblePasswordInputType(int inputType) {
2943        final int variation = inputType & (EditorInfo.TYPE_MASK_CLASS
2944                | EditorInfo.TYPE_MASK_VARIATION);
2945        return variation
2946                == (EditorInfo.TYPE_CLASS_TEXT
2947                        | EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
2948    }
2949
2950    /**
2951     * Directly change the content type integer of the text view, without
2952     * modifying any other state.
2953     * @see #setInputType(int)
2954     * @see android.text.InputType
2955     * @attr ref android.R.styleable#TextView_inputType
2956     */
2957    public void setRawInputType(int type) {
2958        mInputType = type;
2959    }
2960
2961    private void setInputType(int type, boolean direct) {
2962        final int cls = type & EditorInfo.TYPE_MASK_CLASS;
2963        KeyListener input;
2964        if (cls == EditorInfo.TYPE_CLASS_TEXT) {
2965            boolean autotext = (type & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) != 0;
2966            TextKeyListener.Capitalize cap;
2967            if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
2968                cap = TextKeyListener.Capitalize.CHARACTERS;
2969            } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS) != 0) {
2970                cap = TextKeyListener.Capitalize.WORDS;
2971            } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) {
2972                cap = TextKeyListener.Capitalize.SENTENCES;
2973            } else {
2974                cap = TextKeyListener.Capitalize.NONE;
2975            }
2976            input = TextKeyListener.getInstance(autotext, cap);
2977        } else if (cls == EditorInfo.TYPE_CLASS_NUMBER) {
2978            input = DigitsKeyListener.getInstance(
2979                    (type & EditorInfo.TYPE_NUMBER_FLAG_SIGNED) != 0,
2980                    (type & EditorInfo.TYPE_NUMBER_FLAG_DECIMAL) != 0);
2981        } else if (cls == EditorInfo.TYPE_CLASS_DATETIME) {
2982            switch (type & EditorInfo.TYPE_MASK_VARIATION) {
2983                case EditorInfo.TYPE_DATETIME_VARIATION_DATE:
2984                    input = DateKeyListener.getInstance();
2985                    break;
2986                case EditorInfo.TYPE_DATETIME_VARIATION_TIME:
2987                    input = TimeKeyListener.getInstance();
2988                    break;
2989                default:
2990                    input = DateTimeKeyListener.getInstance();
2991                    break;
2992            }
2993        } else if (cls == EditorInfo.TYPE_CLASS_PHONE) {
2994            input = DialerKeyListener.getInstance();
2995        } else {
2996            input = TextKeyListener.getInstance();
2997        }
2998        setRawInputType(type);
2999        if (direct) mInput = input;
3000        else {
3001            setKeyListenerOnly(input);
3002        }
3003    }
3004
3005    /**
3006     * Get the type of the content.
3007     *
3008     * @see #setInputType(int)
3009     * @see android.text.InputType
3010     */
3011    public int getInputType() {
3012        return mInputType;
3013    }
3014
3015    /**
3016     * Change the editor type integer associated with the text view, which
3017     * will be reported to an IME with {@link EditorInfo#imeOptions} when it
3018     * has focus.
3019     * @see #getImeOptions
3020     * @see android.view.inputmethod.EditorInfo
3021     * @attr ref android.R.styleable#TextView_imeOptions
3022     */
3023    public void setImeOptions(int imeOptions) {
3024        if (mInputContentType == null) {
3025            mInputContentType = new InputContentType();
3026        }
3027        mInputContentType.imeOptions = imeOptions;
3028    }
3029
3030    /**
3031     * Get the type of the IME editor.
3032     *
3033     * @see #setImeOptions(int)
3034     * @see android.view.inputmethod.EditorInfo
3035     */
3036    public int getImeOptions() {
3037        return mInputContentType != null
3038                ? mInputContentType.imeOptions : EditorInfo.IME_NULL;
3039    }
3040
3041    /**
3042     * Change the custom IME action associated with the text view, which
3043     * will be reported to an IME with {@link EditorInfo#actionLabel}
3044     * and {@link EditorInfo#actionId} when it has focus.
3045     * @see #getImeActionLabel
3046     * @see #getImeActionId
3047     * @see android.view.inputmethod.EditorInfo
3048     * @attr ref android.R.styleable#TextView_imeActionLabel
3049     * @attr ref android.R.styleable#TextView_imeActionId
3050     */
3051    public void setImeActionLabel(CharSequence label, int actionId) {
3052        if (mInputContentType == null) {
3053            mInputContentType = new InputContentType();
3054        }
3055        mInputContentType.imeActionLabel = label;
3056        mInputContentType.imeActionId = actionId;
3057    }
3058
3059    /**
3060     * Get the IME action label previous set with {@link #setImeActionLabel}.
3061     *
3062     * @see #setImeActionLabel
3063     * @see android.view.inputmethod.EditorInfo
3064     */
3065    public CharSequence getImeActionLabel() {
3066        return mInputContentType != null
3067                ? mInputContentType.imeActionLabel : null;
3068    }
3069
3070    /**
3071     * Get the IME action ID previous set with {@link #setImeActionLabel}.
3072     *
3073     * @see #setImeActionLabel
3074     * @see android.view.inputmethod.EditorInfo
3075     */
3076    public int getImeActionId() {
3077        return mInputContentType != null
3078                ? mInputContentType.imeActionId : 0;
3079    }
3080
3081    /**
3082     * Set a special listener to be called when an action is performed
3083     * on the text view.  This will be called when the enter key is pressed,
3084     * or when an action supplied to the IME is selected by the user.  Setting
3085     * this means that the normal hard key event will not insert a newline
3086     * into the text view, even if it is multi-line; holding down the ALT
3087     * modifier will, however, allow the user to insert a newline character.
3088     */
3089    public void setOnEditorActionListener(OnEditorActionListener l) {
3090        if (mInputContentType == null) {
3091            mInputContentType = new InputContentType();
3092        }
3093        mInputContentType.onEditorActionListener = l;
3094    }
3095
3096    /**
3097     * Called when an attached input method calls
3098     * {@link InputConnection#performEditorAction(int)
3099     * InputConnection.performEditorAction()}
3100     * for this text view.  The default implementation will call your action
3101     * listener supplied to {@link #setOnEditorActionListener}, or perform
3102     * a standard operation for {@link EditorInfo#IME_ACTION_NEXT
3103     * EditorInfo.IME_ACTION_NEXT} or {@link EditorInfo#IME_ACTION_DONE
3104     * EditorInfo.IME_ACTION_DONE}.
3105     *
3106     * <p>For backwards compatibility, if no IME options have been set and the
3107     * text view would not normally advance focus on enter, then
3108     * the NEXT and DONE actions received here will be turned into an enter
3109     * key down/up pair to go through the normal key handling.
3110     *
3111     * @param actionCode The code of the action being performed.
3112     *
3113     * @see #setOnEditorActionListener
3114     */
3115    public void onEditorAction(int actionCode) {
3116        final InputContentType ict = mInputContentType;
3117        if (ict != null) {
3118            if (ict.onEditorActionListener != null) {
3119                if (ict.onEditorActionListener.onEditorAction(this,
3120                        actionCode, null)) {
3121                    return;
3122                }
3123            }
3124
3125            // This is the handling for some default action.
3126            // Note that for backwards compatibility we don't do this
3127            // default handling if explicit ime options have not been given,
3128            // instead turning this into the normal enter key codes that an
3129            // app may be expecting.
3130            if (actionCode == EditorInfo.IME_ACTION_NEXT) {
3131                View v = focusSearch(FOCUS_DOWN);
3132                if (v != null) {
3133                    if (!v.requestFocus(FOCUS_DOWN)) {
3134                        throw new IllegalStateException("focus search returned a view " +
3135                                "that wasn't able to take focus!");
3136                    }
3137                }
3138                return;
3139
3140            } else if (actionCode == EditorInfo.IME_ACTION_DONE) {
3141                InputMethodManager imm = InputMethodManager.peekInstance();
3142                if (imm != null) {
3143                    imm.hideSoftInputFromWindow(getWindowToken(), 0);
3144                }
3145                return;
3146            }
3147        }
3148
3149        Handler h = getHandler();
3150        if (h != null) {
3151            long eventTime = SystemClock.uptimeMillis();
3152            h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME,
3153                    new KeyEvent(eventTime, eventTime,
3154                    KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0, 0, 0,
3155                    KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
3156                    | KeyEvent.FLAG_EDITOR_ACTION)));
3157            h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME,
3158                    new KeyEvent(SystemClock.uptimeMillis(), eventTime,
3159                    KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0, 0, 0,
3160                    KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
3161                    | KeyEvent.FLAG_EDITOR_ACTION)));
3162        }
3163    }
3164
3165    /**
3166     * Set the private content type of the text, which is the
3167     * {@link EditorInfo#privateImeOptions EditorInfo.privateImeOptions}
3168     * field that will be filled in when creating an input connection.
3169     *
3170     * @see #getPrivateImeOptions()
3171     * @see EditorInfo#privateImeOptions
3172     * @attr ref android.R.styleable#TextView_privateImeOptions
3173     */
3174    public void setPrivateImeOptions(String type) {
3175        if (mInputContentType == null) mInputContentType = new InputContentType();
3176        mInputContentType.privateImeOptions = type;
3177    }
3178
3179    /**
3180     * Get the private type of the content.
3181     *
3182     * @see #setPrivateImeOptions(String)
3183     * @see EditorInfo#privateImeOptions
3184     */
3185    public String getPrivateImeOptions() {
3186        return mInputContentType != null
3187                ? mInputContentType.privateImeOptions : null;
3188    }
3189
3190    /**
3191     * Set the extra input data of the text, which is the
3192     * {@link EditorInfo#extras TextBoxAttribute.extras}
3193     * Bundle that will be filled in when creating an input connection.  The
3194     * given integer is the resource ID of an XML resource holding an
3195     * {@link android.R.styleable#InputExtras &lt;input-extras&gt;} XML tree.
3196     *
3197     * @see #getInputExtras(boolean)
3198     * @see EditorInfo#extras
3199     * @attr ref android.R.styleable#TextView_editorExtras
3200     */
3201    public void setInputExtras(int xmlResId)
3202            throws XmlPullParserException, IOException {
3203        XmlResourceParser parser = getResources().getXml(xmlResId);
3204        if (mInputContentType == null) mInputContentType = new InputContentType();
3205        mInputContentType.extras = new Bundle();
3206        getResources().parseBundleExtras(parser, mInputContentType.extras);
3207    }
3208
3209    /**
3210     * Retrieve the input extras currently associated with the text view, which
3211     * can be viewed as well as modified.
3212     *
3213     * @param create If true, the extras will be created if they don't already
3214     * exist.  Otherwise, null will be returned if none have been created.
3215     * @see #setInputExtras(int)
3216     * @see EditorInfo#extras
3217     * @attr ref android.R.styleable#TextView_editorExtras
3218     */
3219    public Bundle getInputExtras(boolean create) {
3220        if (mInputContentType == null) {
3221            if (!create) return null;
3222            mInputContentType = new InputContentType();
3223        }
3224        if (mInputContentType.extras == null) {
3225            if (!create) return null;
3226            mInputContentType.extras = new Bundle();
3227        }
3228        return mInputContentType.extras;
3229    }
3230
3231    /**
3232     * Returns the error message that was set to be displayed with
3233     * {@link #setError}, or <code>null</code> if no error was set
3234     * or if it the error was cleared by the widget after user input.
3235     */
3236    public CharSequence getError() {
3237        return mError;
3238    }
3239
3240    /**
3241     * Sets the right-hand compound drawable of the TextView to the "error"
3242     * icon and sets an error message that will be displayed in a popup when
3243     * the TextView has focus.  The icon and error message will be reset to
3244     * null when any key events cause changes to the TextView's text.  If the
3245     * <code>error</code> is <code>null</code>, the error message and icon
3246     * will be cleared.
3247     */
3248    @android.view.RemotableViewMethod
3249    public void setError(CharSequence error) {
3250        if (error == null) {
3251            setError(null, null);
3252        } else {
3253            Drawable dr = getContext().getResources().
3254                getDrawable(com.android.internal.R.drawable.
3255                            indicator_input_error);
3256
3257            dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight());
3258            setError(error, dr);
3259        }
3260    }
3261
3262    /**
3263     * Sets the right-hand compound drawable of the TextView to the specified
3264     * icon and sets an error message that will be displayed in a popup when
3265     * the TextView has focus.  The icon and error message will be reset to
3266     * null when any key events cause changes to the TextView's text.  The
3267     * drawable must already have had {@link Drawable#setBounds} set on it.
3268     * If the <code>error</code> is <code>null</code>, the error message will
3269     * be cleared (and you should provide a <code>null</code> icon as well).
3270     */
3271    public void setError(CharSequence error, Drawable icon) {
3272        error = TextUtils.stringOrSpannedString(error);
3273
3274        mError = error;
3275        mErrorWasChanged = true;
3276        final Drawables dr = mDrawables;
3277        if (dr != null) {
3278            setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop,
3279                                 icon, dr.mDrawableBottom);
3280        } else {
3281            setCompoundDrawables(null, null, icon, null);
3282        }
3283
3284        if (error == null) {
3285            if (mPopup != null) {
3286                if (mPopup.isShowing()) {
3287                    mPopup.dismiss();
3288                }
3289
3290                mPopup = null;
3291            }
3292        } else {
3293            if (isFocused()) {
3294                showError();
3295            }
3296        }
3297    }
3298
3299    private void showError() {
3300        if (getWindowToken() == null) {
3301            mShowErrorAfterAttach = true;
3302            return;
3303        }
3304
3305        if (mPopup == null) {
3306            LayoutInflater inflater = LayoutInflater.from(getContext());
3307            final TextView err = (TextView) inflater.inflate(com.android.internal.R.layout.textview_hint,
3308                    null);
3309
3310            final float scale = getResources().getDisplayMetrics().density;
3311            mPopup = new ErrorPopup(err, (int) (200 * scale + 0.5f),
3312                    (int) (50 * scale + 0.5f));
3313            mPopup.setFocusable(false);
3314            // The user is entering text, so the input method is needed.  We
3315            // don't want the popup to be displayed on top of it.
3316            mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
3317        }
3318
3319        TextView tv = (TextView) mPopup.getContentView();
3320        chooseSize(mPopup, mError, tv);
3321        tv.setText(mError);
3322
3323        mPopup.showAsDropDown(this, getErrorX(), getErrorY());
3324        mPopup.fixDirection(mPopup.isAboveAnchor());
3325    }
3326
3327    private static class ErrorPopup extends PopupWindow {
3328        private boolean mAbove = false;
3329        private final TextView mView;
3330
3331        ErrorPopup(TextView v, int width, int height) {
3332            super(v, width, height);
3333            mView = v;
3334        }
3335
3336        void fixDirection(boolean above) {
3337            mAbove = above;
3338
3339            if (above) {
3340                mView.setBackgroundResource(com.android.internal.R.drawable.popup_inline_error_above);
3341            } else {
3342                mView.setBackgroundResource(com.android.internal.R.drawable.popup_inline_error);
3343            }
3344        }
3345
3346        @Override
3347        public void update(int x, int y, int w, int h, boolean force) {
3348            super.update(x, y, w, h, force);
3349
3350            boolean above = isAboveAnchor();
3351            if (above != mAbove) {
3352                fixDirection(above);
3353            }
3354        }
3355    }
3356
3357    /**
3358     * Returns the Y offset to make the pointy top of the error point
3359     * at the middle of the error icon.
3360     */
3361    private int getErrorX() {
3362        /*
3363         * The "25" is the distance between the point and the right edge
3364         * of the background
3365         */
3366        final float scale = getResources().getDisplayMetrics().density;
3367
3368        final Drawables dr = mDrawables;
3369        return getWidth() - mPopup.getWidth()
3370                - getPaddingRight()
3371                - (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
3372    }
3373
3374    /**
3375     * Returns the Y offset to make the pointy top of the error point
3376     * at the bottom of the error icon.
3377     */
3378    private int getErrorY() {
3379        /*
3380         * Compound, not extended, because the icon is not clipped
3381         * if the text height is smaller.
3382         */
3383        int vspace = mBottom - mTop -
3384                     getCompoundPaddingBottom() - getCompoundPaddingTop();
3385
3386        final Drawables dr = mDrawables;
3387        int icontop = getCompoundPaddingTop()
3388                + (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2;
3389
3390        /*
3391         * The "2" is the distance between the point and the top edge
3392         * of the background.
3393         */
3394
3395        return icontop + (dr != null ? dr.mDrawableHeightRight : 0)
3396                - getHeight() - 2;
3397    }
3398
3399    private void hideError() {
3400        if (mPopup != null) {
3401            if (mPopup.isShowing()) {
3402                mPopup.dismiss();
3403            }
3404        }
3405
3406        mShowErrorAfterAttach = false;
3407    }
3408
3409    private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
3410        int wid = tv.getPaddingLeft() + tv.getPaddingRight();
3411        int ht = tv.getPaddingTop() + tv.getPaddingBottom();
3412
3413        /*
3414         * Figure out how big the text would be if we laid it out to the
3415         * full width of this view minus the border.
3416         */
3417        int cap = getWidth() - wid;
3418        if (cap < 0) {
3419            cap = 200; // We must not be measured yet -- setFrame() will fix it.
3420        }
3421
3422        Layout l = new StaticLayout(text, tv.getPaint(), cap,
3423                                    Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
3424        float max = 0;
3425        for (int i = 0; i < l.getLineCount(); i++) {
3426            max = Math.max(max, l.getLineWidth(i));
3427        }
3428
3429        /*
3430         * Now set the popup size to be big enough for the text plus the border.
3431         */
3432        pop.setWidth(wid + (int) Math.ceil(max));
3433        pop.setHeight(ht + l.getHeight());
3434    }
3435
3436
3437    @Override
3438    protected boolean setFrame(int l, int t, int r, int b) {
3439        boolean result = super.setFrame(l, t, r, b);
3440
3441        if (mPopup != null) {
3442            TextView tv = (TextView) mPopup.getContentView();
3443            chooseSize(mPopup, mError, tv);
3444            mPopup.update(this, getErrorX(), getErrorY(),
3445                          mPopup.getWidth(), mPopup.getHeight());
3446        }
3447
3448        restartMarqueeIfNeeded();
3449
3450        return result;
3451    }
3452
3453    private void restartMarqueeIfNeeded() {
3454        if (mRestartMarquee && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
3455            mRestartMarquee = false;
3456            startMarquee();
3457        }
3458    }
3459
3460    /**
3461     * Sets the list of input filters that will be used if the buffer is
3462     * Editable.  Has no effect otherwise.
3463     *
3464     * @attr ref android.R.styleable#TextView_maxLength
3465     */
3466    public void setFilters(InputFilter[] filters) {
3467        if (filters == null) {
3468            throw new IllegalArgumentException();
3469        }
3470
3471        mFilters = filters;
3472
3473        if (mText instanceof Editable) {
3474            setFilters((Editable) mText, filters);
3475        }
3476    }
3477
3478    /**
3479     * Sets the list of input filters on the specified Editable,
3480     * and includes mInput in the list if it is an InputFilter.
3481     */
3482    private void setFilters(Editable e, InputFilter[] filters) {
3483        if (mInput instanceof InputFilter) {
3484            InputFilter[] nf = new InputFilter[filters.length + 1];
3485
3486            System.arraycopy(filters, 0, nf, 0, filters.length);
3487            nf[filters.length] = (InputFilter) mInput;
3488
3489            e.setFilters(nf);
3490        } else {
3491            e.setFilters(filters);
3492        }
3493    }
3494
3495    /**
3496     * Returns the current list of input filters.
3497     */
3498    public InputFilter[] getFilters() {
3499        return mFilters;
3500    }
3501
3502    /////////////////////////////////////////////////////////////////////////
3503
3504    private int getVerticalOffset(boolean forceNormal) {
3505        int voffset = 0;
3506        final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
3507
3508        Layout l = mLayout;
3509        if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
3510            l = mHintLayout;
3511        }
3512
3513        if (gravity != Gravity.TOP) {
3514            int boxht;
3515
3516            if (l == mHintLayout) {
3517                boxht = getMeasuredHeight() - getCompoundPaddingTop() -
3518                        getCompoundPaddingBottom();
3519            } else {
3520                boxht = getMeasuredHeight() - getExtendedPaddingTop() -
3521                        getExtendedPaddingBottom();
3522            }
3523            int textht = l.getHeight();
3524
3525            if (textht < boxht) {
3526                if (gravity == Gravity.BOTTOM)
3527                    voffset = boxht - textht;
3528                else // (gravity == Gravity.CENTER_VERTICAL)
3529                    voffset = (boxht - textht) >> 1;
3530            }
3531        }
3532        return voffset;
3533    }
3534
3535    private int getBottomVerticalOffset(boolean forceNormal) {
3536        int voffset = 0;
3537        final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
3538
3539        Layout l = mLayout;
3540        if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
3541            l = mHintLayout;
3542        }
3543
3544        if (gravity != Gravity.BOTTOM) {
3545            int boxht;
3546
3547            if (l == mHintLayout) {
3548                boxht = getMeasuredHeight() - getCompoundPaddingTop() -
3549                        getCompoundPaddingBottom();
3550            } else {
3551                boxht = getMeasuredHeight() - getExtendedPaddingTop() -
3552                        getExtendedPaddingBottom();
3553            }
3554            int textht = l.getHeight();
3555
3556            if (textht < boxht) {
3557                if (gravity == Gravity.TOP)
3558                    voffset = boxht - textht;
3559                else // (gravity == Gravity.CENTER_VERTICAL)
3560                    voffset = (boxht - textht) >> 1;
3561            }
3562        }
3563        return voffset;
3564    }
3565
3566    private void invalidateCursorPath() {
3567        if (mHighlightPathBogus) {
3568            invalidateCursor();
3569        } else {
3570            synchronized (sTempRect) {
3571                /*
3572                 * The reason for this concern about the thickness of the
3573                 * cursor and doing the floor/ceil on the coordinates is that
3574                 * some EditTexts (notably textfields in the Browser) have
3575                 * anti-aliased text where not all the characters are
3576                 * necessarily at integer-multiple locations.  This should
3577                 * make sure the entire cursor gets invalidated instead of
3578                 * sometimes missing half a pixel.
3579                 */
3580
3581                float thick = FloatMath.ceil(mTextPaint.getStrokeWidth());
3582                if (thick < 1.0f) {
3583                    thick = 1.0f;
3584                }
3585
3586                thick /= 2;
3587
3588                mHighlightPath.computeBounds(sTempRect, false);
3589
3590                int left = getCompoundPaddingLeft();
3591                int top = getExtendedPaddingTop() + getVerticalOffset(true);
3592
3593                invalidate((int) FloatMath.floor(left + sTempRect.left - thick),
3594                           (int) FloatMath.floor(top + sTempRect.top - thick),
3595                           (int) FloatMath.ceil(left + sTempRect.right + thick),
3596                           (int) FloatMath.ceil(top + sTempRect.bottom + thick));
3597            }
3598        }
3599    }
3600
3601    private void invalidateCursor() {
3602        int where = getSelectionEnd();
3603
3604        invalidateCursor(where, where, where);
3605    }
3606
3607    private void invalidateCursor(int a, int b, int c) {
3608        if (mLayout == null) {
3609            invalidate();
3610        } else {
3611            if (a >= 0 || b >= 0 || c >= 0) {
3612                int first = Math.min(Math.min(a, b), c);
3613                int last = Math.max(Math.max(a, b), c);
3614
3615                int line = mLayout.getLineForOffset(first);
3616                int top = mLayout.getLineTop(line);
3617
3618                // This is ridiculous, but the descent from the line above
3619                // can hang down into the line we really want to redraw,
3620                // so we have to invalidate part of the line above to make
3621                // sure everything that needs to be redrawn really is.
3622                // (But not the whole line above, because that would cause
3623                // the same problem with the descenders on the line above it!)
3624                if (line > 0) {
3625                    top -= mLayout.getLineDescent(line - 1);
3626                }
3627
3628                int line2;
3629
3630                if (first == last)
3631                    line2 = line;
3632                else
3633                    line2 = mLayout.getLineForOffset(last);
3634
3635                int bottom = mLayout.getLineTop(line2 + 1);
3636                int voffset = getVerticalOffset(true);
3637
3638                int left = getCompoundPaddingLeft() + mScrollX;
3639                invalidate(left, top + voffset + getExtendedPaddingTop(),
3640                           left + getWidth() - getCompoundPaddingLeft() -
3641                           getCompoundPaddingRight(),
3642                           bottom + voffset + getExtendedPaddingTop());
3643            }
3644        }
3645    }
3646
3647    private void registerForPreDraw() {
3648        final ViewTreeObserver observer = getViewTreeObserver();
3649        if (observer == null) {
3650            return;
3651        }
3652
3653        if (mPreDrawState == PREDRAW_NOT_REGISTERED) {
3654            observer.addOnPreDrawListener(this);
3655            mPreDrawState = PREDRAW_PENDING;
3656        } else if (mPreDrawState == PREDRAW_DONE) {
3657            mPreDrawState = PREDRAW_PENDING;
3658        }
3659
3660        // else state is PREDRAW_PENDING, so keep waiting.
3661    }
3662
3663    /**
3664     * {@inheritDoc}
3665     */
3666    public boolean onPreDraw() {
3667        if (mPreDrawState != PREDRAW_PENDING) {
3668            return true;
3669        }
3670
3671        if (mLayout == null) {
3672            assumeLayout();
3673        }
3674
3675        boolean changed = false;
3676
3677        if (mMovement != null) {
3678            /* This code also provides auto-scrolling when a cursor is moved using a
3679             * CursorController (insertion point or selection limits).
3680             * For selection, ensure start or end is visible depending on controller's state.
3681             */
3682            int curs = getSelectionEnd();
3683            if (mSelectionModifierCursorController != null) {
3684                SelectionModifierCursorController selectionController =
3685                    (SelectionModifierCursorController) mSelectionModifierCursorController;
3686                if (selectionController.isSelectionStartDragged()) {
3687                    curs = getSelectionStart();
3688                }
3689            }
3690
3691            /*
3692             * TODO: This should really only keep the end in view if
3693             * it already was before the text changed.  I'm not sure
3694             * of a good way to tell from here if it was.
3695             */
3696            if (curs < 0 &&
3697                  (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
3698                curs = mText.length();
3699            }
3700
3701            if (curs >= 0) {
3702                changed = bringPointIntoView(curs);
3703            }
3704        } else {
3705            changed = bringTextIntoView();
3706        }
3707
3708        if (mShouldStartTextSelectionMode) {
3709            startTextSelectionMode();
3710            mShouldStartTextSelectionMode = false;
3711        }
3712        mPreDrawState = PREDRAW_DONE;
3713        return !changed;
3714    }
3715
3716    @Override
3717    protected void onAttachedToWindow() {
3718        super.onAttachedToWindow();
3719
3720        mTemporaryDetach = false;
3721
3722        if (mShowErrorAfterAttach) {
3723            showError();
3724            mShowErrorAfterAttach = false;
3725        }
3726    }
3727
3728    @Override
3729    protected void onDetachedFromWindow() {
3730        super.onDetachedFromWindow();
3731
3732        if (mPreDrawState != PREDRAW_NOT_REGISTERED) {
3733            final ViewTreeObserver observer = getViewTreeObserver();
3734            if (observer != null) {
3735                observer.removeOnPreDrawListener(this);
3736                mPreDrawState = PREDRAW_NOT_REGISTERED;
3737            }
3738        }
3739
3740        if (mError != null) {
3741            hideError();
3742        }
3743    }
3744
3745    @Override
3746    protected boolean isPaddingOffsetRequired() {
3747        return mShadowRadius != 0 || mDrawables != null;
3748    }
3749
3750    @Override
3751    protected int getLeftPaddingOffset() {
3752        return getCompoundPaddingLeft() - mPaddingLeft +
3753                (int) Math.min(0, mShadowDx - mShadowRadius);
3754    }
3755
3756    @Override
3757    protected int getTopPaddingOffset() {
3758        return (int) Math.min(0, mShadowDy - mShadowRadius);
3759    }
3760
3761    @Override
3762    protected int getBottomPaddingOffset() {
3763        return (int) Math.max(0, mShadowDy + mShadowRadius);
3764    }
3765
3766    @Override
3767    protected int getRightPaddingOffset() {
3768        return -(getCompoundPaddingRight() - mPaddingRight) +
3769                (int) Math.max(0, mShadowDx + mShadowRadius);
3770    }
3771
3772    @Override
3773    protected boolean verifyDrawable(Drawable who) {
3774        final boolean verified = super.verifyDrawable(who);
3775        if (!verified && mDrawables != null) {
3776            return who == mDrawables.mDrawableLeft || who == mDrawables.mDrawableTop ||
3777                    who == mDrawables.mDrawableRight || who == mDrawables.mDrawableBottom;
3778        }
3779        return verified;
3780    }
3781
3782    @Override
3783    public void invalidateDrawable(Drawable drawable) {
3784        if (verifyDrawable(drawable)) {
3785            final Rect dirty = drawable.getBounds();
3786            int scrollX = mScrollX;
3787            int scrollY = mScrollY;
3788
3789            // IMPORTANT: The coordinates below are based on the coordinates computed
3790            // for each compound drawable in onDraw(). Make sure to update each section
3791            // accordingly.
3792            final TextView.Drawables drawables = mDrawables;
3793            if (drawables != null) {
3794                if (drawable == drawables.mDrawableLeft) {
3795                    final int compoundPaddingTop = getCompoundPaddingTop();
3796                    final int compoundPaddingBottom = getCompoundPaddingBottom();
3797                    final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
3798
3799                    scrollX += mPaddingLeft;
3800                    scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightLeft) / 2;
3801                } else if (drawable == drawables.mDrawableRight) {
3802                    final int compoundPaddingTop = getCompoundPaddingTop();
3803                    final int compoundPaddingBottom = getCompoundPaddingBottom();
3804                    final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
3805
3806                    scrollX += (mRight - mLeft - mPaddingRight - drawables.mDrawableSizeRight);
3807                    scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightRight) / 2;
3808                } else if (drawable == drawables.mDrawableTop) {
3809                    final int compoundPaddingLeft = getCompoundPaddingLeft();
3810                    final int compoundPaddingRight = getCompoundPaddingRight();
3811                    final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
3812
3813                    scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthTop) / 2;
3814                    scrollY += mPaddingTop;
3815                } else if (drawable == drawables.mDrawableBottom) {
3816                    final int compoundPaddingLeft = getCompoundPaddingLeft();
3817                    final int compoundPaddingRight = getCompoundPaddingRight();
3818                    final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
3819
3820                    scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthBottom) / 2;
3821                    scrollY += (mBottom - mTop - mPaddingBottom - drawables.mDrawableSizeBottom);
3822                }
3823            }
3824
3825            invalidate(dirty.left + scrollX, dirty.top + scrollY,
3826                    dirty.right + scrollX, dirty.bottom + scrollY);
3827        }
3828    }
3829
3830    @Override
3831    protected void onDraw(Canvas canvas) {
3832        restartMarqueeIfNeeded();
3833
3834        // Draw the background for this view
3835        super.onDraw(canvas);
3836
3837        final int compoundPaddingLeft = getCompoundPaddingLeft();
3838        final int compoundPaddingTop = getCompoundPaddingTop();
3839        final int compoundPaddingRight = getCompoundPaddingRight();
3840        final int compoundPaddingBottom = getCompoundPaddingBottom();
3841        final int scrollX = mScrollX;
3842        final int scrollY = mScrollY;
3843        final int right = mRight;
3844        final int left = mLeft;
3845        final int bottom = mBottom;
3846        final int top = mTop;
3847
3848        final Drawables dr = mDrawables;
3849        if (dr != null) {
3850            /*
3851             * Compound, not extended, because the icon is not clipped
3852             * if the text height is smaller.
3853             */
3854
3855            int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
3856            int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;
3857
3858            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
3859            // Make sure to update invalidateDrawable() when changing this code.
3860            if (dr.mDrawableLeft != null) {
3861                canvas.save();
3862                canvas.translate(scrollX + mPaddingLeft,
3863                                 scrollY + compoundPaddingTop +
3864                                 (vspace - dr.mDrawableHeightLeft) / 2);
3865                dr.mDrawableLeft.draw(canvas);
3866                canvas.restore();
3867            }
3868
3869            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
3870            // Make sure to update invalidateDrawable() when changing this code.
3871            if (dr.mDrawableRight != null) {
3872                canvas.save();
3873                canvas.translate(scrollX + right - left - mPaddingRight - dr.mDrawableSizeRight,
3874                         scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2);
3875                dr.mDrawableRight.draw(canvas);
3876                canvas.restore();
3877            }
3878
3879            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
3880            // Make sure to update invalidateDrawable() when changing this code.
3881            if (dr.mDrawableTop != null) {
3882                canvas.save();
3883                canvas.translate(scrollX + compoundPaddingLeft + (hspace - dr.mDrawableWidthTop) / 2,
3884                        scrollY + mPaddingTop);
3885                dr.mDrawableTop.draw(canvas);
3886                canvas.restore();
3887            }
3888
3889            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
3890            // Make sure to update invalidateDrawable() when changing this code.
3891            if (dr.mDrawableBottom != null) {
3892                canvas.save();
3893                canvas.translate(scrollX + compoundPaddingLeft +
3894                        (hspace - dr.mDrawableWidthBottom) / 2,
3895                         scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom);
3896                dr.mDrawableBottom.draw(canvas);
3897                canvas.restore();
3898            }
3899        }
3900
3901        if (mPreDrawState == PREDRAW_DONE) {
3902            final ViewTreeObserver observer = getViewTreeObserver();
3903            if (observer != null) {
3904                observer.removeOnPreDrawListener(this);
3905                mPreDrawState = PREDRAW_NOT_REGISTERED;
3906            }
3907        }
3908
3909        int color = mCurTextColor;
3910
3911        if (mLayout == null) {
3912            assumeLayout();
3913        }
3914
3915        Layout layout = mLayout;
3916        int cursorcolor = color;
3917
3918        if (mHint != null && mText.length() == 0) {
3919            if (mHintTextColor != null) {
3920                color = mCurHintTextColor;
3921            }
3922
3923            layout = mHintLayout;
3924        }
3925
3926        mTextPaint.setColor(color);
3927        mTextPaint.drawableState = getDrawableState();
3928
3929        canvas.save();
3930        /*  Would be faster if we didn't have to do this. Can we chop the
3931            (displayable) text so that we don't need to do this ever?
3932        */
3933
3934        int extendedPaddingTop = getExtendedPaddingTop();
3935        int extendedPaddingBottom = getExtendedPaddingBottom();
3936
3937        float clipLeft = compoundPaddingLeft + scrollX;
3938        float clipTop = extendedPaddingTop + scrollY;
3939        float clipRight = right - left - compoundPaddingRight + scrollX;
3940        float clipBottom = bottom - top - extendedPaddingBottom + scrollY;
3941
3942        if (mShadowRadius != 0) {
3943            clipLeft += Math.min(0, mShadowDx - mShadowRadius);
3944            clipRight += Math.max(0, mShadowDx + mShadowRadius);
3945
3946            clipTop += Math.min(0, mShadowDy - mShadowRadius);
3947            clipBottom += Math.max(0, mShadowDy + mShadowRadius);
3948        }
3949
3950        canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom);
3951
3952        int voffsetText = 0;
3953        int voffsetCursor = 0;
3954
3955        // translate in by our padding
3956        {
3957            /* shortcircuit calling getVerticaOffset() */
3958            if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
3959                voffsetText = getVerticalOffset(false);
3960                voffsetCursor = getVerticalOffset(true);
3961            }
3962            canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText);
3963        }
3964
3965        if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
3966            if (!mSingleLine && getLineCount() == 1 && canMarquee() &&
3967                    (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
3968                canvas.translate(mLayout.getLineRight(0) - (mRight - mLeft -
3969                        getCompoundPaddingLeft() - getCompoundPaddingRight()), 0.0f);
3970            }
3971
3972            if (mMarquee != null && mMarquee.isRunning()) {
3973                canvas.translate(-mMarquee.mScroll, 0.0f);
3974            }
3975        }
3976
3977        Path highlight = null;
3978        int selStart = -1, selEnd = -1;
3979
3980        //  If there is no movement method, then there can be no selection.
3981        //  Check that first and attempt to skip everything having to do with
3982        //  the cursor.
3983        //  XXX This is not strictly true -- a program could set the
3984        //  selection manually if it really wanted to.
3985        if (mMovement != null && (isFocused() || isPressed())) {
3986            selStart = getSelectionStart();
3987            selEnd = getSelectionEnd();
3988
3989            if (mCursorVisible && selStart >= 0 && isEnabled()) {
3990                if (mHighlightPath == null)
3991                    mHighlightPath = new Path();
3992
3993                if (selStart == selEnd) {
3994                    if ((SystemClock.uptimeMillis() - mShowCursor) % (2 * BLINK) < BLINK) {
3995                        if (mHighlightPathBogus) {
3996                            mHighlightPath.reset();
3997                            mLayout.getCursorPath(selStart, mHighlightPath, mText);
3998                            mHighlightPathBogus = false;
3999                        }
4000
4001                        // XXX should pass to skin instead of drawing directly
4002                        mHighlightPaint.setColor(cursorcolor);
4003                        mHighlightPaint.setStyle(Paint.Style.STROKE);
4004
4005                        highlight = mHighlightPath;
4006                    }
4007                } else {
4008                    if (mHighlightPathBogus) {
4009                        mHighlightPath.reset();
4010                        mLayout.getSelectionPath(selStart, selEnd, mHighlightPath);
4011                        mHighlightPathBogus = false;
4012                    }
4013
4014                    // XXX should pass to skin instead of drawing directly
4015                    mHighlightPaint.setColor(mHighlightColor);
4016                    mHighlightPaint.setStyle(Paint.Style.FILL);
4017
4018                    highlight = mHighlightPath;
4019                }
4020            }
4021        }
4022
4023        /*  Comment out until we decide what to do about animations
4024        boolean isLinearTextOn = false;
4025        if (currentTransformation != null) {
4026            isLinearTextOn = mTextPaint.isLinearTextOn();
4027            Matrix m = currentTransformation.getMatrix();
4028            if (!m.isIdentity()) {
4029                // mTextPaint.setLinearTextOn(true);
4030            }
4031        }
4032        */
4033
4034        final InputMethodState ims = mInputMethodState;
4035        if (ims != null && ims.mBatchEditNesting == 0) {
4036            InputMethodManager imm = InputMethodManager.peekInstance();
4037            if (imm != null) {
4038                if (imm.isActive(this)) {
4039                    boolean reported = false;
4040                    if (ims.mContentChanged || ims.mSelectionModeChanged) {
4041                        // We are in extract mode and the content has changed
4042                        // in some way... just report complete new text to the
4043                        // input method.
4044                        reported = reportExtractedText();
4045                    }
4046                    if (!reported && highlight != null) {
4047                        int candStart = -1;
4048                        int candEnd = -1;
4049                        if (mText instanceof Spannable) {
4050                            Spannable sp = (Spannable)mText;
4051                            candStart = EditableInputConnection.getComposingSpanStart(sp);
4052                            candEnd = EditableInputConnection.getComposingSpanEnd(sp);
4053                        }
4054                        imm.updateSelection(this, selStart, selEnd, candStart, candEnd);
4055                    }
4056                }
4057
4058                if (imm.isWatchingCursor(this) && highlight != null) {
4059                    highlight.computeBounds(ims.mTmpRectF, true);
4060                    ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0;
4061
4062                    canvas.getMatrix().mapPoints(ims.mTmpOffset);
4063                    ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]);
4064
4065                    ims.mTmpRectF.offset(0, voffsetCursor - voffsetText);
4066
4067                    ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5),
4068                            (int)(ims.mTmpRectF.top + 0.5),
4069                            (int)(ims.mTmpRectF.right + 0.5),
4070                            (int)(ims.mTmpRectF.bottom + 0.5));
4071
4072                    imm.updateCursor(this,
4073                            ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top,
4074                            ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom);
4075                }
4076            }
4077        }
4078
4079        layout.draw(canvas, highlight, mHighlightPaint, voffsetCursor - voffsetText);
4080
4081        if (mMarquee != null && mMarquee.shouldDrawGhost()) {
4082            canvas.translate((int) mMarquee.getGhostOffset(), 0.0f);
4083            layout.draw(canvas, highlight, mHighlightPaint, voffsetCursor - voffsetText);
4084        }
4085
4086        /*  Comment out until we decide what to do about animations
4087        if (currentTransformation != null) {
4088            mTextPaint.setLinearTextOn(isLinearTextOn);
4089        }
4090        */
4091
4092        canvas.restore();
4093
4094        if (mInsertionPointCursorController != null) {
4095            mInsertionPointCursorController.draw(canvas);
4096        }
4097        if (mSelectionModifierCursorController != null) {
4098            mSelectionModifierCursorController.draw(canvas);
4099        }
4100    }
4101
4102    @Override
4103    public void getFocusedRect(Rect r) {
4104        if (mLayout == null) {
4105            super.getFocusedRect(r);
4106            return;
4107        }
4108
4109        int sel = getSelectionEnd();
4110        if (sel < 0) {
4111            super.getFocusedRect(r);
4112            return;
4113        }
4114
4115        int line = mLayout.getLineForOffset(sel);
4116        r.top = mLayout.getLineTop(line);
4117        r.bottom = mLayout.getLineBottom(line);
4118
4119        r.left = (int) mLayout.getPrimaryHorizontal(sel);
4120        r.right = r.left + 1;
4121
4122        // Adjust for padding and gravity.
4123        int paddingLeft = getCompoundPaddingLeft();
4124        int paddingTop = getExtendedPaddingTop();
4125        if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
4126            paddingTop += getVerticalOffset(false);
4127        }
4128        r.offset(paddingLeft, paddingTop);
4129    }
4130
4131    /**
4132     * Return the number of lines of text, or 0 if the internal Layout has not
4133     * been built.
4134     */
4135    public int getLineCount() {
4136        return mLayout != null ? mLayout.getLineCount() : 0;
4137    }
4138
4139    /**
4140     * Return the baseline for the specified line (0...getLineCount() - 1)
4141     * If bounds is not null, return the top, left, right, bottom extents
4142     * of the specified line in it. If the internal Layout has not been built,
4143     * return 0 and set bounds to (0, 0, 0, 0)
4144     * @param line which line to examine (0..getLineCount() - 1)
4145     * @param bounds Optional. If not null, it returns the extent of the line
4146     * @return the Y-coordinate of the baseline
4147     */
4148    public int getLineBounds(int line, Rect bounds) {
4149        if (mLayout == null) {
4150            if (bounds != null) {
4151                bounds.set(0, 0, 0, 0);
4152            }
4153            return 0;
4154        }
4155        else {
4156            int baseline = mLayout.getLineBounds(line, bounds);
4157
4158            int voffset = getExtendedPaddingTop();
4159            if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
4160                voffset += getVerticalOffset(true);
4161            }
4162            if (bounds != null) {
4163                bounds.offset(getCompoundPaddingLeft(), voffset);
4164            }
4165            return baseline + voffset;
4166        }
4167    }
4168
4169    @Override
4170    public int getBaseline() {
4171        if (mLayout == null) {
4172            return super.getBaseline();
4173        }
4174
4175        int voffset = 0;
4176        if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
4177            voffset = getVerticalOffset(true);
4178        }
4179
4180        return getExtendedPaddingTop() + voffset + mLayout.getLineBaseline(0);
4181    }
4182
4183    @Override
4184    public boolean onKeyDown(int keyCode, KeyEvent event) {
4185        int which = doKeyDown(keyCode, event, null);
4186        if (which == 0) {
4187            // Go through default dispatching.
4188            return super.onKeyDown(keyCode, event);
4189        }
4190
4191        return true;
4192    }
4193
4194    @Override
4195    public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
4196        KeyEvent down = KeyEvent.changeAction(event, KeyEvent.ACTION_DOWN);
4197
4198        int which = doKeyDown(keyCode, down, event);
4199        if (which == 0) {
4200            // Go through default dispatching.
4201            return super.onKeyMultiple(keyCode, repeatCount, event);
4202        }
4203        if (which == -1) {
4204            // Consumed the whole thing.
4205            return true;
4206        }
4207
4208        repeatCount--;
4209
4210        // We are going to dispatch the remaining events to either the input
4211        // or movement method.  To do this, we will just send a repeated stream
4212        // of down and up events until we have done the complete repeatCount.
4213        // It would be nice if those interfaces had an onKeyMultiple() method,
4214        // but adding that is a more complicated change.
4215        KeyEvent up = KeyEvent.changeAction(event, KeyEvent.ACTION_UP);
4216        if (which == 1) {
4217            mInput.onKeyUp(this, (Editable)mText, keyCode, up);
4218            while (--repeatCount > 0) {
4219                mInput.onKeyDown(this, (Editable)mText, keyCode, down);
4220                mInput.onKeyUp(this, (Editable)mText, keyCode, up);
4221            }
4222            if (mError != null && !mErrorWasChanged) {
4223                setError(null, null);
4224            }
4225
4226        } else if (which == 2) {
4227            mMovement.onKeyUp(this, (Spannable)mText, keyCode, up);
4228            while (--repeatCount > 0) {
4229                mMovement.onKeyDown(this, (Spannable)mText, keyCode, down);
4230                mMovement.onKeyUp(this, (Spannable)mText, keyCode, up);
4231            }
4232        }
4233
4234        return true;
4235    }
4236
4237    /**
4238     * Returns true if pressing ENTER in this field advances focus instead
4239     * of inserting the character.  This is true mostly in single-line fields,
4240     * but also in mail addresses and subjects which will display on multiple
4241     * lines but where it doesn't make sense to insert newlines.
4242     */
4243    private boolean shouldAdvanceFocusOnEnter() {
4244        if (mInput == null) {
4245            return false;
4246        }
4247
4248        if (mSingleLine) {
4249            return true;
4250        }
4251
4252        if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
4253            int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
4254
4255            if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
4256                variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT) {
4257                return true;
4258            }
4259        }
4260
4261        return false;
4262    }
4263
4264    private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) {
4265        if (!isEnabled()) {
4266            return 0;
4267        }
4268
4269        switch (keyCode) {
4270            case KeyEvent.KEYCODE_ENTER:
4271                // If ALT modifier is held, then we always insert a
4272                // newline character.
4273                if ((event.getMetaState()&KeyEvent.META_ALT_ON) == 0) {
4274
4275                    // When mInputContentType is set, we know that we are
4276                    // running in a "modern" cupcake environment, so don't need
4277                    // to worry about the application trying to capture
4278                    // enter key events.
4279                    if (mInputContentType != null) {
4280
4281                        // If there is an action listener, given them a
4282                        // chance to consume the event.
4283                        if (mInputContentType.onEditorActionListener != null &&
4284                                mInputContentType.onEditorActionListener.onEditorAction(
4285                                this, EditorInfo.IME_NULL, event)) {
4286                            mInputContentType.enterDown = true;
4287                            // We are consuming the enter key for them.
4288                            return -1;
4289                        }
4290                    }
4291
4292                    // If our editor should move focus when enter is pressed, or
4293                    // this is a generated event from an IME action button, then
4294                    // don't let it be inserted into the text.
4295                    if ((event.getFlags()&KeyEvent.FLAG_EDITOR_ACTION) != 0
4296                            || shouldAdvanceFocusOnEnter()) {
4297                        return -1;
4298                    }
4299                }
4300                break;
4301
4302            case KeyEvent.KEYCODE_DPAD_CENTER:
4303                if (shouldAdvanceFocusOnEnter()) {
4304                    return 0;
4305                }
4306                break;
4307
4308                // Has to be done on key down (and not on key up) to correctly be intercepted.
4309            case KeyEvent.KEYCODE_BACK:
4310                if (mIsInTextSelectionMode) {
4311                    stopTextSelectionMode();
4312                    return -1;
4313                }
4314                break;
4315        }
4316
4317        if (mInput != null) {
4318            /*
4319             * Keep track of what the error was before doing the input
4320             * so that if an input filter changed the error, we leave
4321             * that error showing.  Otherwise, we take down whatever
4322             * error was showing when the user types something.
4323             */
4324            mErrorWasChanged = false;
4325
4326            boolean doDown = true;
4327            if (otherEvent != null) {
4328                try {
4329                    beginBatchEdit();
4330                    boolean handled = mInput.onKeyOther(this, (Editable) mText,
4331                            otherEvent);
4332                    if (mError != null && !mErrorWasChanged) {
4333                        setError(null, null);
4334                    }
4335                    doDown = false;
4336                    if (handled) {
4337                        return -1;
4338                    }
4339                } catch (AbstractMethodError e) {
4340                    // onKeyOther was added after 1.0, so if it isn't
4341                    // implemented we need to try to dispatch as a regular down.
4342                } finally {
4343                    endBatchEdit();
4344                }
4345            }
4346
4347            if (doDown) {
4348                beginBatchEdit();
4349                if (mInput.onKeyDown(this, (Editable) mText, keyCode, event)) {
4350                    endBatchEdit();
4351                    if (mError != null && !mErrorWasChanged) {
4352                        setError(null, null);
4353                    }
4354                    return 1;
4355                }
4356                endBatchEdit();
4357            }
4358        }
4359
4360        // bug 650865: sometimes we get a key event before a layout.
4361        // don't try to move around if we don't know the layout.
4362
4363        if (mMovement != null && mLayout != null) {
4364            boolean doDown = true;
4365            if (otherEvent != null) {
4366                try {
4367                    boolean handled = mMovement.onKeyOther(this, (Spannable) mText,
4368                            otherEvent);
4369                    doDown = false;
4370                    if (handled) {
4371                        return -1;
4372                    }
4373                } catch (AbstractMethodError e) {
4374                    // onKeyOther was added after 1.0, so if it isn't
4375                    // implemented we need to try to dispatch as a regular down.
4376                }
4377            }
4378            if (doDown) {
4379                if (mMovement.onKeyDown(this, (Spannable)mText, keyCode, event))
4380                    return 2;
4381            }
4382        }
4383
4384        return 0;
4385    }
4386
4387    @Override
4388    public boolean onKeyUp(int keyCode, KeyEvent event) {
4389        if (!isEnabled()) {
4390            return super.onKeyUp(keyCode, event);
4391        }
4392
4393        hideControllers();
4394
4395        switch (keyCode) {
4396            case KeyEvent.KEYCODE_DPAD_CENTER:
4397                /*
4398                 * If there is a click listener, just call through to
4399                 * super, which will invoke it.
4400                 *
4401                 * If there isn't a click listener, try to show the soft
4402                 * input method.  (It will also
4403                 * call performClick(), but that won't do anything in
4404                 * this case.)
4405                 */
4406                if (mOnClickListener == null) {
4407                    if (mMovement != null && mText instanceof Editable
4408                            && mLayout != null && onCheckIsTextEditor()) {
4409                        InputMethodManager imm = (InputMethodManager)
4410                                getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
4411                        imm.showSoftInput(this, 0);
4412                    }
4413                }
4414                return super.onKeyUp(keyCode, event);
4415
4416            case KeyEvent.KEYCODE_ENTER:
4417                if (mInputContentType != null
4418                        && mInputContentType.onEditorActionListener != null
4419                        && mInputContentType.enterDown) {
4420                    mInputContentType.enterDown = false;
4421                    if (mInputContentType.onEditorActionListener.onEditorAction(
4422                            this, EditorInfo.IME_NULL, event)) {
4423                        return true;
4424                    }
4425                }
4426
4427                if ((event.getFlags()&KeyEvent.FLAG_EDITOR_ACTION) != 0
4428                        || shouldAdvanceFocusOnEnter()) {
4429                    /*
4430                     * If there is a click listener, just call through to
4431                     * super, which will invoke it.
4432                     *
4433                     * If there isn't a click listener, try to advance focus,
4434                     * but still call through to super, which will reset the
4435                     * pressed state and longpress state.  (It will also
4436                     * call performClick(), but that won't do anything in
4437                     * this case.)
4438                     */
4439                    if (mOnClickListener == null) {
4440                        View v = focusSearch(FOCUS_DOWN);
4441
4442                        if (v != null) {
4443                            if (!v.requestFocus(FOCUS_DOWN)) {
4444                                throw new IllegalStateException("focus search returned a view " +
4445                                        "that wasn't able to take focus!");
4446                            }
4447
4448                            /*
4449                             * Return true because we handled the key; super
4450                             * will return false because there was no click
4451                             * listener.
4452                             */
4453                            super.onKeyUp(keyCode, event);
4454                            return true;
4455                        } else if ((event.getFlags()
4456                                & KeyEvent.FLAG_EDITOR_ACTION) != 0) {
4457                            // No target for next focus, but make sure the IME
4458                            // if this came from it.
4459                            InputMethodManager imm = InputMethodManager.peekInstance();
4460                            if (imm != null) {
4461                                imm.hideSoftInputFromWindow(getWindowToken(), 0);
4462                            }
4463                        }
4464                    }
4465
4466                    return super.onKeyUp(keyCode, event);
4467                }
4468                break;
4469        }
4470
4471        if (mInput != null)
4472            if (mInput.onKeyUp(this, (Editable) mText, keyCode, event))
4473                return true;
4474
4475        if (mMovement != null && mLayout != null)
4476            if (mMovement.onKeyUp(this, (Spannable) mText, keyCode, event))
4477                return true;
4478
4479        return super.onKeyUp(keyCode, event);
4480    }
4481
4482    @Override public boolean onCheckIsTextEditor() {
4483        return mInputType != EditorInfo.TYPE_NULL;
4484    }
4485
4486    @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
4487        if (onCheckIsTextEditor()) {
4488            if (mInputMethodState == null) {
4489                mInputMethodState = new InputMethodState();
4490            }
4491            outAttrs.inputType = mInputType;
4492            if (mInputContentType != null) {
4493                outAttrs.imeOptions = mInputContentType.imeOptions;
4494                outAttrs.privateImeOptions = mInputContentType.privateImeOptions;
4495                outAttrs.actionLabel = mInputContentType.imeActionLabel;
4496                outAttrs.actionId = mInputContentType.imeActionId;
4497                outAttrs.extras = mInputContentType.extras;
4498            } else {
4499                outAttrs.imeOptions = EditorInfo.IME_NULL;
4500            }
4501            if ((outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION)
4502                    == EditorInfo.IME_ACTION_UNSPECIFIED) {
4503                if (focusSearch(FOCUS_DOWN) != null) {
4504                    // An action has not been set, but the enter key will move to
4505                    // the next focus, so set the action to that.
4506                    outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
4507                } else {
4508                    // An action has not been set, and there is no focus to move
4509                    // to, so let's just supply a "done" action.
4510                    outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
4511                }
4512                if (!shouldAdvanceFocusOnEnter()) {
4513                    outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
4514                }
4515            }
4516            if ((outAttrs.inputType & (InputType.TYPE_MASK_CLASS
4517                    | InputType.TYPE_TEXT_FLAG_MULTI_LINE))
4518                    == (InputType.TYPE_CLASS_TEXT
4519                            | InputType.TYPE_TEXT_FLAG_MULTI_LINE)) {
4520                // Multi-line text editors should always show an enter key.
4521                outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
4522            }
4523            outAttrs.hintText = mHint;
4524            if (mText instanceof Editable) {
4525                InputConnection ic = new EditableInputConnection(this);
4526                outAttrs.initialSelStart = getSelectionStart();
4527                outAttrs.initialSelEnd = getSelectionEnd();
4528                outAttrs.initialCapsMode = ic.getCursorCapsMode(mInputType);
4529                return ic;
4530            }
4531        }
4532        return null;
4533    }
4534
4535    /**
4536     * If this TextView contains editable content, extract a portion of it
4537     * based on the information in <var>request</var> in to <var>outText</var>.
4538     * @return Returns true if the text was successfully extracted, else false.
4539     */
4540    public boolean extractText(ExtractedTextRequest request,
4541            ExtractedText outText) {
4542        return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
4543                EXTRACT_UNKNOWN, outText);
4544    }
4545
4546    static final int EXTRACT_NOTHING = -2;
4547    static final int EXTRACT_UNKNOWN = -1;
4548
4549    boolean extractTextInternal(ExtractedTextRequest request,
4550            int partialStartOffset, int partialEndOffset, int delta,
4551            ExtractedText outText) {
4552        final CharSequence content = mText;
4553        if (content != null) {
4554            if (partialStartOffset != EXTRACT_NOTHING) {
4555                final int N = content.length();
4556                if (partialStartOffset < 0) {
4557                    outText.partialStartOffset = outText.partialEndOffset = -1;
4558                    partialStartOffset = 0;
4559                    partialEndOffset = N;
4560                } else {
4561                    // Adjust offsets to ensure we contain full spans.
4562                    if (content instanceof Spanned) {
4563                        Spanned spanned = (Spanned)content;
4564                        Object[] spans = spanned.getSpans(partialStartOffset,
4565                                partialEndOffset, ParcelableSpan.class);
4566                        int i = spans.length;
4567                        while (i > 0) {
4568                            i--;
4569                            int j = spanned.getSpanStart(spans[i]);
4570                            if (j < partialStartOffset) partialStartOffset = j;
4571                            j = spanned.getSpanEnd(spans[i]);
4572                            if (j > partialEndOffset) partialEndOffset = j;
4573                        }
4574                    }
4575                    outText.partialStartOffset = partialStartOffset;
4576                    outText.partialEndOffset = partialEndOffset;
4577                    // Now use the delta to determine the actual amount of text
4578                    // we need.
4579                    partialEndOffset += delta;
4580                    if (partialStartOffset > N) {
4581                        partialStartOffset = N;
4582                    } else if (partialStartOffset < 0) {
4583                        partialStartOffset = 0;
4584                    }
4585                    if (partialEndOffset > N) {
4586                        partialEndOffset = N;
4587                    } else if (partialEndOffset < 0) {
4588                        partialEndOffset = 0;
4589                    }
4590                }
4591                if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) {
4592                    outText.text = content.subSequence(partialStartOffset,
4593                            partialEndOffset);
4594                } else {
4595                    outText.text = TextUtils.substring(content, partialStartOffset,
4596                            partialEndOffset);
4597                }
4598            } else {
4599                outText.partialStartOffset = 0;
4600                outText.partialEndOffset = 0;
4601                outText.text = "";
4602            }
4603            outText.flags = 0;
4604            if (MetaKeyKeyListener.getMetaState(mText, MetaKeyKeyListener.META_SELECTING) != 0) {
4605                outText.flags |= ExtractedText.FLAG_SELECTING;
4606            }
4607            if (mSingleLine) {
4608                outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
4609            }
4610            outText.startOffset = 0;
4611            outText.selectionStart = getSelectionStart();
4612            outText.selectionEnd = getSelectionEnd();
4613            return true;
4614        }
4615        return false;
4616    }
4617
4618    boolean reportExtractedText() {
4619        final InputMethodState ims = mInputMethodState;
4620        if (ims != null) {
4621            final boolean contentChanged = ims.mContentChanged;
4622            if (contentChanged || ims.mSelectionModeChanged) {
4623                ims.mContentChanged = false;
4624                ims.mSelectionModeChanged = false;
4625                final ExtractedTextRequest req = mInputMethodState.mExtracting;
4626                if (req != null) {
4627                    InputMethodManager imm = InputMethodManager.peekInstance();
4628                    if (imm != null) {
4629                        if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Retrieving extracted start="
4630                                + ims.mChangedStart + " end=" + ims.mChangedEnd
4631                                + " delta=" + ims.mChangedDelta);
4632                        if (ims.mChangedStart < 0 && !contentChanged) {
4633                            ims.mChangedStart = EXTRACT_NOTHING;
4634                        }
4635                        if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
4636                                ims.mChangedDelta, ims.mTmpExtracted)) {
4637                            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Reporting extracted start="
4638                                    + ims.mTmpExtracted.partialStartOffset
4639                                    + " end=" + ims.mTmpExtracted.partialEndOffset
4640                                    + ": " + ims.mTmpExtracted.text);
4641                            imm.updateExtractedText(this, req.token,
4642                                    mInputMethodState.mTmpExtracted);
4643                            return true;
4644                        }
4645                    }
4646                }
4647            }
4648        }
4649        return false;
4650    }
4651
4652    /**
4653     * This is used to remove all style-impacting spans from text before new
4654     * extracted text is being replaced into it, so that we don't have any
4655     * lingering spans applied during the replace.
4656     */
4657    static void removeParcelableSpans(Spannable spannable, int start, int end) {
4658        Object[] spans = spannable.getSpans(start, end, ParcelableSpan.class);
4659        int i = spans.length;
4660        while (i > 0) {
4661            i--;
4662            spannable.removeSpan(spans[i]);
4663        }
4664    }
4665
4666    /**
4667     * Apply to this text view the given extracted text, as previously
4668     * returned by {@link #extractText(ExtractedTextRequest, ExtractedText)}.
4669     */
4670    public void setExtractedText(ExtractedText text) {
4671        Editable content = getEditableText();
4672        if (text.text != null) {
4673            if (content == null) {
4674                setText(text.text, TextView.BufferType.EDITABLE);
4675            } else if (text.partialStartOffset < 0) {
4676                removeParcelableSpans(content, 0, content.length());
4677                content.replace(0, content.length(), text.text);
4678            } else {
4679                final int N = content.length();
4680                int start = text.partialStartOffset;
4681                if (start > N) start = N;
4682                int end = text.partialEndOffset;
4683                if (end > N) end = N;
4684                removeParcelableSpans(content, start, end);
4685                content.replace(start, end, text.text);
4686            }
4687        }
4688
4689        // Now set the selection position...  make sure it is in range, to
4690        // avoid crashes.  If this is a partial update, it is possible that
4691        // the underlying text may have changed, causing us problems here.
4692        // Also we just don't want to trust clients to do the right thing.
4693        Spannable sp = (Spannable)getText();
4694        final int N = sp.length();
4695        int start = text.selectionStart;
4696        if (start < 0) start = 0;
4697        else if (start > N) start = N;
4698        int end = text.selectionEnd;
4699        if (end < 0) end = 0;
4700        else if (end > N) end = N;
4701        Selection.setSelection(sp, start, end);
4702
4703        // Finally, update the selection mode.
4704        if ((text.flags&ExtractedText.FLAG_SELECTING) != 0) {
4705            MetaKeyKeyListener.startSelecting(this, sp);
4706        } else {
4707            MetaKeyKeyListener.stopSelecting(this, sp);
4708        }
4709    }
4710
4711    /**
4712     * @hide
4713     */
4714    public void setExtracting(ExtractedTextRequest req) {
4715        if (mInputMethodState != null) {
4716            mInputMethodState.mExtracting = req;
4717        }
4718    }
4719
4720    /**
4721     * Called by the framework in response to a text completion from
4722     * the current input method, provided by it calling
4723     * {@link InputConnection#commitCompletion
4724     * InputConnection.commitCompletion()}.  The default implementation does
4725     * nothing; text views that are supporting auto-completion should override
4726     * this to do their desired behavior.
4727     *
4728     * @param text The auto complete text the user has selected.
4729     */
4730    public void onCommitCompletion(CompletionInfo text) {
4731    }
4732
4733    public void beginBatchEdit() {
4734        final InputMethodState ims = mInputMethodState;
4735        if (ims != null) {
4736            int nesting = ++ims.mBatchEditNesting;
4737            if (nesting == 1) {
4738                ims.mCursorChanged = false;
4739                ims.mChangedDelta = 0;
4740                if (ims.mContentChanged) {
4741                    // We already have a pending change from somewhere else,
4742                    // so turn this into a full update.
4743                    ims.mChangedStart = 0;
4744                    ims.mChangedEnd = mText.length();
4745                } else {
4746                    ims.mChangedStart = EXTRACT_UNKNOWN;
4747                    ims.mChangedEnd = EXTRACT_UNKNOWN;
4748                    ims.mContentChanged = false;
4749                }
4750                onBeginBatchEdit();
4751            }
4752        }
4753    }
4754
4755    public void endBatchEdit() {
4756        final InputMethodState ims = mInputMethodState;
4757        if (ims != null) {
4758            int nesting = --ims.mBatchEditNesting;
4759            if (nesting == 0) {
4760                finishBatchEdit(ims);
4761            }
4762        }
4763    }
4764
4765    void ensureEndedBatchEdit() {
4766        final InputMethodState ims = mInputMethodState;
4767        if (ims != null && ims.mBatchEditNesting != 0) {
4768            ims.mBatchEditNesting = 0;
4769            finishBatchEdit(ims);
4770        }
4771    }
4772
4773    void finishBatchEdit(final InputMethodState ims) {
4774        onEndBatchEdit();
4775
4776        if (ims.mContentChanged || ims.mSelectionModeChanged) {
4777            updateAfterEdit();
4778            reportExtractedText();
4779        } else if (ims.mCursorChanged) {
4780            // Cheezy way to get us to report the current cursor location.
4781            invalidateCursor();
4782        }
4783    }
4784
4785    void updateAfterEdit() {
4786        invalidate();
4787        int curs = getSelectionStart();
4788
4789        if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) ==
4790                             Gravity.BOTTOM) {
4791            registerForPreDraw();
4792        }
4793
4794        if (curs >= 0) {
4795            mHighlightPathBogus = true;
4796
4797            if (isFocused()) {
4798                mShowCursor = SystemClock.uptimeMillis();
4799                makeBlink();
4800            }
4801        }
4802
4803        checkForResize();
4804    }
4805
4806    /**
4807     * Called by the framework in response to a request to begin a batch
4808     * of edit operations through a call to link {@link #beginBatchEdit()}.
4809     */
4810    public void onBeginBatchEdit() {
4811    }
4812
4813    /**
4814     * Called by the framework in response to a request to end a batch
4815     * of edit operations through a call to link {@link #endBatchEdit}.
4816     */
4817    public void onEndBatchEdit() {
4818    }
4819
4820    /**
4821     * Called by the framework in response to a private command from the
4822     * current method, provided by it calling
4823     * {@link InputConnection#performPrivateCommand
4824     * InputConnection.performPrivateCommand()}.
4825     *
4826     * @param action The action name of the command.
4827     * @param data Any additional data for the command.  This may be null.
4828     * @return Return true if you handled the command, else false.
4829     */
4830    public boolean onPrivateIMECommand(String action, Bundle data) {
4831        return false;
4832    }
4833
4834    private void nullLayouts() {
4835        if (mLayout instanceof BoringLayout && mSavedLayout == null) {
4836            mSavedLayout = (BoringLayout) mLayout;
4837        }
4838        if (mHintLayout instanceof BoringLayout && mSavedHintLayout == null) {
4839            mSavedHintLayout = (BoringLayout) mHintLayout;
4840        }
4841
4842        mLayout = mHintLayout = null;
4843    }
4844
4845    /**
4846     * Make a new Layout based on the already-measured size of the view,
4847     * on the assumption that it was measured correctly at some point.
4848     */
4849    private void assumeLayout() {
4850        int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
4851
4852        if (width < 1) {
4853            width = 0;
4854        }
4855
4856        int physicalWidth = width;
4857
4858        if (mHorizontallyScrolling) {
4859            width = VERY_WIDE;
4860        }
4861
4862        makeNewLayout(width, physicalWidth, UNKNOWN_BORING, UNKNOWN_BORING,
4863                      physicalWidth, false);
4864    }
4865
4866    /**
4867     * The width passed in is now the desired layout width,
4868     * not the full view width with padding.
4869     * {@hide}
4870     */
4871    protected void makeNewLayout(int w, int hintWidth,
4872                                 BoringLayout.Metrics boring,
4873                                 BoringLayout.Metrics hintBoring,
4874                                 int ellipsisWidth, boolean bringIntoView) {
4875        stopMarquee();
4876
4877        mHighlightPathBogus = true;
4878
4879        if (w < 0) {
4880            w = 0;
4881        }
4882        if (hintWidth < 0) {
4883            hintWidth = 0;
4884        }
4885
4886        Layout.Alignment alignment;
4887        switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
4888            case Gravity.CENTER_HORIZONTAL:
4889                alignment = Layout.Alignment.ALIGN_CENTER;
4890                break;
4891
4892            case Gravity.RIGHT:
4893                alignment = Layout.Alignment.ALIGN_OPPOSITE;
4894                break;
4895
4896            default:
4897                alignment = Layout.Alignment.ALIGN_NORMAL;
4898        }
4899
4900        boolean shouldEllipsize = mEllipsize != null && mInput == null;
4901
4902        if (mText instanceof Spannable) {
4903            mLayout = new DynamicLayout(mText, mTransformed, mTextPaint, w,
4904                    alignment, mSpacingMult,
4905                    mSpacingAdd, mIncludePad, mInput == null ? mEllipsize : null,
4906                    ellipsisWidth);
4907        } else {
4908            if (boring == UNKNOWN_BORING) {
4909                boring = BoringLayout.isBoring(mTransformed, mTextPaint,
4910                                               mBoring);
4911                if (boring != null) {
4912                    mBoring = boring;
4913                }
4914            }
4915
4916            if (boring != null) {
4917                if (boring.width <= w &&
4918                    (mEllipsize == null || boring.width <= ellipsisWidth)) {
4919                    if (mSavedLayout != null) {
4920                        mLayout = mSavedLayout.
4921                                replaceOrMake(mTransformed, mTextPaint,
4922                                w, alignment, mSpacingMult, mSpacingAdd,
4923                                boring, mIncludePad);
4924                    } else {
4925                        mLayout = BoringLayout.make(mTransformed, mTextPaint,
4926                                w, alignment, mSpacingMult, mSpacingAdd,
4927                                boring, mIncludePad);
4928                    }
4929
4930                    mSavedLayout = (BoringLayout) mLayout;
4931                } else if (shouldEllipsize && boring.width <= w) {
4932                    if (mSavedLayout != null) {
4933                        mLayout = mSavedLayout.
4934                                replaceOrMake(mTransformed, mTextPaint,
4935                                w, alignment, mSpacingMult, mSpacingAdd,
4936                                boring, mIncludePad, mEllipsize,
4937                                ellipsisWidth);
4938                    } else {
4939                        mLayout = BoringLayout.make(mTransformed, mTextPaint,
4940                                w, alignment, mSpacingMult, mSpacingAdd,
4941                                boring, mIncludePad, mEllipsize,
4942                                ellipsisWidth);
4943                    }
4944                } else if (shouldEllipsize) {
4945                    mLayout = new StaticLayout(mTransformed,
4946                                0, mTransformed.length(),
4947                                mTextPaint, w, alignment, mSpacingMult,
4948                                mSpacingAdd, mIncludePad, mEllipsize,
4949                                ellipsisWidth);
4950                } else {
4951                    mLayout = new StaticLayout(mTransformed, mTextPaint,
4952                            w, alignment, mSpacingMult, mSpacingAdd,
4953                            mIncludePad);
4954                }
4955            } else if (shouldEllipsize) {
4956                mLayout = new StaticLayout(mTransformed,
4957                            0, mTransformed.length(),
4958                            mTextPaint, w, alignment, mSpacingMult,
4959                            mSpacingAdd, mIncludePad, mEllipsize,
4960                            ellipsisWidth);
4961            } else {
4962                mLayout = new StaticLayout(mTransformed, mTextPaint,
4963                        w, alignment, mSpacingMult, mSpacingAdd,
4964                        mIncludePad);
4965            }
4966        }
4967
4968        shouldEllipsize = mEllipsize != null;
4969        mHintLayout = null;
4970
4971        if (mHint != null) {
4972            if (shouldEllipsize) hintWidth = w;
4973
4974            if (hintBoring == UNKNOWN_BORING) {
4975                hintBoring = BoringLayout.isBoring(mHint, mTextPaint,
4976                                                   mHintBoring);
4977                if (hintBoring != null) {
4978                    mHintBoring = hintBoring;
4979                }
4980            }
4981
4982            if (hintBoring != null) {
4983                if (hintBoring.width <= hintWidth &&
4984                    (!shouldEllipsize || hintBoring.width <= ellipsisWidth)) {
4985                    if (mSavedHintLayout != null) {
4986                        mHintLayout = mSavedHintLayout.
4987                                replaceOrMake(mHint, mTextPaint,
4988                                hintWidth, alignment, mSpacingMult, mSpacingAdd,
4989                                hintBoring, mIncludePad);
4990                    } else {
4991                        mHintLayout = BoringLayout.make(mHint, mTextPaint,
4992                                hintWidth, alignment, mSpacingMult, mSpacingAdd,
4993                                hintBoring, mIncludePad);
4994                    }
4995
4996                    mSavedHintLayout = (BoringLayout) mHintLayout;
4997                } else if (shouldEllipsize && hintBoring.width <= hintWidth) {
4998                    if (mSavedHintLayout != null) {
4999                        mHintLayout = mSavedHintLayout.
5000                                replaceOrMake(mHint, mTextPaint,
5001                                hintWidth, alignment, mSpacingMult, mSpacingAdd,
5002                                hintBoring, mIncludePad, mEllipsize,
5003                                ellipsisWidth);
5004                    } else {
5005                        mHintLayout = BoringLayout.make(mHint, mTextPaint,
5006                                hintWidth, alignment, mSpacingMult, mSpacingAdd,
5007                                hintBoring, mIncludePad, mEllipsize,
5008                                ellipsisWidth);
5009                    }
5010                } else if (shouldEllipsize) {
5011                    mHintLayout = new StaticLayout(mHint,
5012                                0, mHint.length(),
5013                                mTextPaint, hintWidth, alignment, mSpacingMult,
5014                                mSpacingAdd, mIncludePad, mEllipsize,
5015                                ellipsisWidth);
5016                } else {
5017                    mHintLayout = new StaticLayout(mHint, mTextPaint,
5018                            hintWidth, alignment, mSpacingMult, mSpacingAdd,
5019                            mIncludePad);
5020                }
5021            } else if (shouldEllipsize) {
5022                mHintLayout = new StaticLayout(mHint,
5023                            0, mHint.length(),
5024                            mTextPaint, hintWidth, alignment, mSpacingMult,
5025                            mSpacingAdd, mIncludePad, mEllipsize,
5026                            ellipsisWidth);
5027            } else {
5028                mHintLayout = new StaticLayout(mHint, mTextPaint,
5029                        hintWidth, alignment, mSpacingMult, mSpacingAdd,
5030                        mIncludePad);
5031            }
5032        }
5033
5034        if (bringIntoView) {
5035            registerForPreDraw();
5036        }
5037
5038        if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
5039            if (!compressText(ellipsisWidth)) {
5040                final int height = mLayoutParams.height;
5041                // If the size of the view does not depend on the size of the text, try to
5042                // start the marquee immediately
5043                if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT) {
5044                    startMarquee();
5045                } else {
5046                    // Defer the start of the marquee until we know our width (see setFrame())
5047                    mRestartMarquee = true;
5048                }
5049            }
5050        }
5051
5052        // CursorControllers need a non-null mLayout
5053        prepareCursorControllers();
5054    }
5055
5056    private boolean compressText(float width) {
5057        // Only compress the text if it hasn't been compressed by the previous pass
5058        if (width > 0.0f && mLayout != null && getLineCount() == 1 && !mUserSetTextScaleX &&
5059                mTextPaint.getTextScaleX() == 1.0f) {
5060            final float textWidth = mLayout.getLineWidth(0);
5061            final float overflow = (textWidth + 1.0f - width) / width;
5062            if (overflow > 0.0f && overflow <= Marquee.MARQUEE_DELTA_MAX) {
5063                mTextPaint.setTextScaleX(1.0f - overflow - 0.005f);
5064                post(new Runnable() {
5065                    public void run() {
5066                        requestLayout();
5067                    }
5068                });
5069                return true;
5070            }
5071        }
5072
5073        return false;
5074    }
5075
5076    private static int desired(Layout layout) {
5077        int n = layout.getLineCount();
5078        CharSequence text = layout.getText();
5079        float max = 0;
5080
5081        // if any line was wrapped, we can't use it.
5082        // but it's ok for the last line not to have a newline
5083
5084        for (int i = 0; i < n - 1; i++) {
5085            if (text.charAt(layout.getLineEnd(i) - 1) != '\n')
5086                return -1;
5087        }
5088
5089        for (int i = 0; i < n; i++) {
5090            max = Math.max(max, layout.getLineWidth(i));
5091        }
5092
5093        return (int) FloatMath.ceil(max);
5094    }
5095
5096    /**
5097     * Set whether the TextView includes extra top and bottom padding to make
5098     * room for accents that go above the normal ascent and descent.
5099     * The default is true.
5100     *
5101     * @attr ref android.R.styleable#TextView_includeFontPadding
5102     */
5103    public void setIncludeFontPadding(boolean includepad) {
5104        mIncludePad = includepad;
5105
5106        if (mLayout != null) {
5107            nullLayouts();
5108            requestLayout();
5109            invalidate();
5110        }
5111    }
5112
5113    private static final BoringLayout.Metrics UNKNOWN_BORING = new BoringLayout.Metrics();
5114
5115    @Override
5116    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
5117        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
5118        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
5119        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
5120        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
5121
5122        int width;
5123        int height;
5124
5125        BoringLayout.Metrics boring = UNKNOWN_BORING;
5126        BoringLayout.Metrics hintBoring = UNKNOWN_BORING;
5127
5128        int des = -1;
5129        boolean fromexisting = false;
5130
5131        if (widthMode == MeasureSpec.EXACTLY) {
5132            // Parent has told us how big to be. So be it.
5133            width = widthSize;
5134        } else {
5135            if (mLayout != null && mEllipsize == null) {
5136                des = desired(mLayout);
5137            }
5138
5139            if (des < 0) {
5140                boring = BoringLayout.isBoring(mTransformed, mTextPaint, mBoring);
5141                if (boring != null) {
5142                    mBoring = boring;
5143                }
5144            } else {
5145                fromexisting = true;
5146            }
5147
5148            if (boring == null || boring == UNKNOWN_BORING) {
5149                if (des < 0) {
5150                    des = (int) FloatMath.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint));
5151                }
5152
5153                width = des;
5154            } else {
5155                width = boring.width;
5156            }
5157
5158            final Drawables dr = mDrawables;
5159            if (dr != null) {
5160                width = Math.max(width, dr.mDrawableWidthTop);
5161                width = Math.max(width, dr.mDrawableWidthBottom);
5162            }
5163
5164            if (mHint != null) {
5165                int hintDes = -1;
5166                int hintWidth;
5167
5168                if (mHintLayout != null && mEllipsize == null) {
5169                    hintDes = desired(mHintLayout);
5170                }
5171
5172                if (hintDes < 0) {
5173                    hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mHintBoring);
5174                    if (hintBoring != null) {
5175                        mHintBoring = hintBoring;
5176                    }
5177                }
5178
5179                if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
5180                    if (hintDes < 0) {
5181                        hintDes = (int) FloatMath.ceil(
5182                                Layout.getDesiredWidth(mHint, mTextPaint));
5183                    }
5184
5185                    hintWidth = hintDes;
5186                } else {
5187                    hintWidth = hintBoring.width;
5188                }
5189
5190                if (hintWidth > width) {
5191                    width = hintWidth;
5192                }
5193            }
5194
5195            width += getCompoundPaddingLeft() + getCompoundPaddingRight();
5196
5197            if (mMaxWidthMode == EMS) {
5198                width = Math.min(width, mMaxWidth * getLineHeight());
5199            } else {
5200                width = Math.min(width, mMaxWidth);
5201            }
5202
5203            if (mMinWidthMode == EMS) {
5204                width = Math.max(width, mMinWidth * getLineHeight());
5205            } else {
5206                width = Math.max(width, mMinWidth);
5207            }
5208
5209            // Check against our minimum width
5210            width = Math.max(width, getSuggestedMinimumWidth());
5211
5212            if (widthMode == MeasureSpec.AT_MOST) {
5213                width = Math.min(widthSize, width);
5214            }
5215        }
5216
5217        int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
5218        int unpaddedWidth = want;
5219        int hintWant = want;
5220
5221        if (mHorizontallyScrolling)
5222            want = VERY_WIDE;
5223
5224        int hintWidth = mHintLayout == null ? hintWant : mHintLayout.getWidth();
5225
5226        if (mLayout == null) {
5227            makeNewLayout(want, hintWant, boring, hintBoring,
5228                          width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
5229        } else if ((mLayout.getWidth() != want) || (hintWidth != hintWant) ||
5230                   (mLayout.getEllipsizedWidth() !=
5231                        width - getCompoundPaddingLeft() - getCompoundPaddingRight())) {
5232            if (mHint == null && mEllipsize == null &&
5233                    want > mLayout.getWidth() &&
5234                    (mLayout instanceof BoringLayout ||
5235                            (fromexisting && des >= 0 && des <= want))) {
5236                mLayout.increaseWidthTo(want);
5237            } else {
5238                makeNewLayout(want, hintWant, boring, hintBoring,
5239                              width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
5240            }
5241        } else {
5242            // Width has not changed.
5243        }
5244
5245        if (heightMode == MeasureSpec.EXACTLY) {
5246            // Parent has told us how big to be. So be it.
5247            height = heightSize;
5248            mDesiredHeightAtMeasure = -1;
5249        } else {
5250            int desired = getDesiredHeight();
5251
5252            height = desired;
5253            mDesiredHeightAtMeasure = desired;
5254
5255            if (heightMode == MeasureSpec.AT_MOST) {
5256                height = Math.min(desired, heightSize);
5257            }
5258        }
5259
5260        int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
5261        if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
5262            unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
5263        }
5264
5265        /*
5266         * We didn't let makeNewLayout() register to bring the cursor into view,
5267         * so do it here if there is any possibility that it is needed.
5268         */
5269        if (mMovement != null ||
5270            mLayout.getWidth() > unpaddedWidth ||
5271            mLayout.getHeight() > unpaddedHeight) {
5272            registerForPreDraw();
5273        } else {
5274            scrollTo(0, 0);
5275        }
5276
5277        setMeasuredDimension(width, height);
5278    }
5279
5280    private int getDesiredHeight() {
5281        return Math.max(
5282                getDesiredHeight(mLayout, true),
5283                getDesiredHeight(mHintLayout, mEllipsize != null));
5284    }
5285
5286    private int getDesiredHeight(Layout layout, boolean cap) {
5287        if (layout == null) {
5288            return 0;
5289        }
5290
5291        int linecount = layout.getLineCount();
5292        int pad = getCompoundPaddingTop() + getCompoundPaddingBottom();
5293        int desired = layout.getLineTop(linecount);
5294
5295        final Drawables dr = mDrawables;
5296        if (dr != null) {
5297            desired = Math.max(desired, dr.mDrawableHeightLeft);
5298            desired = Math.max(desired, dr.mDrawableHeightRight);
5299        }
5300
5301        desired += pad;
5302
5303        if (mMaxMode == LINES) {
5304            /*
5305             * Don't cap the hint to a certain number of lines.
5306             * (Do cap it, though, if we have a maximum pixel height.)
5307             */
5308            if (cap) {
5309                if (linecount > mMaximum) {
5310                    desired = layout.getLineTop(mMaximum) +
5311                              layout.getBottomPadding();
5312
5313                    if (dr != null) {
5314                        desired = Math.max(desired, dr.mDrawableHeightLeft);
5315                        desired = Math.max(desired, dr.mDrawableHeightRight);
5316                    }
5317
5318                    desired += pad;
5319                    linecount = mMaximum;
5320                }
5321            }
5322        } else {
5323            desired = Math.min(desired, mMaximum);
5324        }
5325
5326        if (mMinMode == LINES) {
5327            if (linecount < mMinimum) {
5328                desired += getLineHeight() * (mMinimum - linecount);
5329            }
5330        } else {
5331            desired = Math.max(desired, mMinimum);
5332        }
5333
5334        // Check against our minimum height
5335        desired = Math.max(desired, getSuggestedMinimumHeight());
5336
5337        return desired;
5338    }
5339
5340    /**
5341     * Check whether a change to the existing text layout requires a
5342     * new view layout.
5343     */
5344    private void checkForResize() {
5345        boolean sizeChanged = false;
5346
5347        if (mLayout != null) {
5348            // Check if our width changed
5349            if (mLayoutParams.width == LayoutParams.WRAP_CONTENT) {
5350                sizeChanged = true;
5351                invalidate();
5352            }
5353
5354            // Check if our height changed
5355            if (mLayoutParams.height == LayoutParams.WRAP_CONTENT) {
5356                int desiredHeight = getDesiredHeight();
5357
5358                if (desiredHeight != this.getHeight()) {
5359                    sizeChanged = true;
5360                }
5361            } else if (mLayoutParams.height == LayoutParams.MATCH_PARENT) {
5362                if (mDesiredHeightAtMeasure >= 0) {
5363                    int desiredHeight = getDesiredHeight();
5364
5365                    if (desiredHeight != mDesiredHeightAtMeasure) {
5366                        sizeChanged = true;
5367                    }
5368                }
5369            }
5370        }
5371
5372        if (sizeChanged) {
5373            requestLayout();
5374            // caller will have already invalidated
5375        }
5376    }
5377
5378    /**
5379     * Check whether entirely new text requires a new view layout
5380     * or merely a new text layout.
5381     */
5382    private void checkForRelayout() {
5383        // If we have a fixed width, we can just swap in a new text layout
5384        // if the text height stays the same or if the view height is fixed.
5385
5386        if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT ||
5387                (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth)) &&
5388                (mHint == null || mHintLayout != null) &&
5389                (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
5390            // Static width, so try making a new text layout.
5391
5392            int oldht = mLayout.getHeight();
5393            int want = mLayout.getWidth();
5394            int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
5395
5396            /*
5397             * No need to bring the text into view, since the size is not
5398             * changing (unless we do the requestLayout(), in which case it
5399             * will happen at measure).
5400             */
5401            makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
5402                          mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
5403                          false);
5404
5405            if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
5406                // In a fixed-height view, so use our new text layout.
5407                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT &&
5408                    mLayoutParams.height != LayoutParams.MATCH_PARENT) {
5409                    invalidate();
5410                    return;
5411                }
5412
5413                // Dynamic height, but height has stayed the same,
5414                // so use our new text layout.
5415                if (mLayout.getHeight() == oldht &&
5416                    (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
5417                    invalidate();
5418                    return;
5419                }
5420            }
5421
5422            // We lose: the height has changed and we have a dynamic height.
5423            // Request a new view layout using our new text layout.
5424            requestLayout();
5425            invalidate();
5426        } else {
5427            // Dynamic width, so we have no choice but to request a new
5428            // view layout with a new text layout.
5429
5430            nullLayouts();
5431            requestLayout();
5432            invalidate();
5433        }
5434    }
5435
5436    /**
5437     * Returns true if anything changed.
5438     */
5439    private boolean bringTextIntoView() {
5440        int line = 0;
5441        if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
5442            line = mLayout.getLineCount() - 1;
5443        }
5444
5445        Layout.Alignment a = mLayout.getParagraphAlignment(line);
5446        int dir = mLayout.getParagraphDirection(line);
5447        int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
5448        int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
5449        int ht = mLayout.getHeight();
5450
5451        int scrollx, scrolly;
5452
5453        if (a == Layout.Alignment.ALIGN_CENTER) {
5454            /*
5455             * Keep centered if possible, or, if it is too wide to fit,
5456             * keep leading edge in view.
5457             */
5458
5459            int left = (int) FloatMath.floor(mLayout.getLineLeft(line));
5460            int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
5461
5462            if (right - left < hspace) {
5463                scrollx = (right + left) / 2 - hspace / 2;
5464            } else {
5465                if (dir < 0) {
5466                    scrollx = right - hspace;
5467                } else {
5468                    scrollx = left;
5469                }
5470            }
5471        } else if (a == Layout.Alignment.ALIGN_NORMAL) {
5472            /*
5473             * Keep leading edge in view.
5474             */
5475
5476            if (dir < 0) {
5477                int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
5478                scrollx = right - hspace;
5479            } else {
5480                scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line));
5481            }
5482        } else /* a == Layout.Alignment.ALIGN_OPPOSITE */ {
5483            /*
5484             * Keep trailing edge in view.
5485             */
5486
5487            if (dir < 0) {
5488                scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line));
5489            } else {
5490                int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
5491                scrollx = right - hspace;
5492            }
5493        }
5494
5495        if (ht < vspace) {
5496            scrolly = 0;
5497        } else {
5498            if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
5499                scrolly = ht - vspace;
5500            } else {
5501                scrolly = 0;
5502            }
5503        }
5504
5505        if (scrollx != mScrollX || scrolly != mScrollY) {
5506            scrollTo(scrollx, scrolly);
5507            return true;
5508        } else {
5509            return false;
5510        }
5511    }
5512
5513    /**
5514     * Move the point, specified by the offset, into the view if it is needed.
5515     * This has to be called after layout. Returns true if anything changed.
5516     */
5517    public boolean bringPointIntoView(int offset) {
5518        boolean changed = false;
5519
5520        int line = mLayout.getLineForOffset(offset);
5521
5522        // FIXME: Is it okay to truncate this, or should we round?
5523        final int x = (int)mLayout.getPrimaryHorizontal(offset);
5524        final int top = mLayout.getLineTop(line);
5525        final int bottom = mLayout.getLineTop(line + 1);
5526
5527        int left = (int) FloatMath.floor(mLayout.getLineLeft(line));
5528        int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
5529        int ht = mLayout.getHeight();
5530
5531        int grav;
5532
5533        switch (mLayout.getParagraphAlignment(line)) {
5534            case ALIGN_NORMAL:
5535                grav = 1;
5536                break;
5537
5538            case ALIGN_OPPOSITE:
5539                grav = -1;
5540                break;
5541
5542            default:
5543                grav = 0;
5544        }
5545
5546        grav *= mLayout.getParagraphDirection(line);
5547
5548        int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
5549        int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
5550
5551        int hslack = (bottom - top) / 2;
5552        int vslack = hslack;
5553
5554        if (vslack > vspace / 4)
5555            vslack = vspace / 4;
5556        if (hslack > hspace / 4)
5557            hslack = hspace / 4;
5558
5559        int hs = mScrollX;
5560        int vs = mScrollY;
5561
5562        if (top - vs < vslack)
5563            vs = top - vslack;
5564        if (bottom - vs > vspace - vslack)
5565            vs = bottom - (vspace - vslack);
5566        if (ht - vs < vspace)
5567            vs = ht - vspace;
5568        if (0 - vs > 0)
5569            vs = 0;
5570
5571        if (grav != 0) {
5572            if (x - hs < hslack) {
5573                hs = x - hslack;
5574            }
5575            if (x - hs > hspace - hslack) {
5576                hs = x - (hspace - hslack);
5577            }
5578        }
5579
5580        if (grav < 0) {
5581            if (left - hs > 0)
5582                hs = left;
5583            if (right - hs < hspace)
5584                hs = right - hspace;
5585        } else if (grav > 0) {
5586            if (right - hs < hspace)
5587                hs = right - hspace;
5588            if (left - hs > 0)
5589                hs = left;
5590        } else /* grav == 0 */ {
5591            if (right - left <= hspace) {
5592                /*
5593                 * If the entire text fits, center it exactly.
5594                 */
5595                hs = left - (hspace - (right - left)) / 2;
5596            } else if (x > right - hslack) {
5597                /*
5598                 * If we are near the right edge, keep the right edge
5599                 * at the edge of the view.
5600                 */
5601                hs = right - hspace;
5602            } else if (x < left + hslack) {
5603                /*
5604                 * If we are near the left edge, keep the left edge
5605                 * at the edge of the view.
5606                 */
5607                hs = left;
5608            } else if (left > hs) {
5609                /*
5610                 * Is there whitespace visible at the left?  Fix it if so.
5611                 */
5612                hs = left;
5613            } else if (right < hs + hspace) {
5614                /*
5615                 * Is there whitespace visible at the right?  Fix it if so.
5616                 */
5617                hs = right - hspace;
5618            } else {
5619                /*
5620                 * Otherwise, float as needed.
5621                 */
5622                if (x - hs < hslack) {
5623                    hs = x - hslack;
5624                }
5625                if (x - hs > hspace - hslack) {
5626                    hs = x - (hspace - hslack);
5627                }
5628            }
5629        }
5630
5631        if (hs != mScrollX || vs != mScrollY) {
5632            if (mScroller == null) {
5633                scrollTo(hs, vs);
5634            } else {
5635                long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
5636                int dx = hs - mScrollX;
5637                int dy = vs - mScrollY;
5638
5639                if (duration > ANIMATED_SCROLL_GAP) {
5640                    mScroller.startScroll(mScrollX, mScrollY, dx, dy);
5641                    awakenScrollBars(mScroller.getDuration());
5642                    invalidate();
5643                } else {
5644                    if (!mScroller.isFinished()) {
5645                        mScroller.abortAnimation();
5646                    }
5647
5648                    scrollBy(dx, dy);
5649                }
5650
5651                mLastScroll = AnimationUtils.currentAnimationTimeMillis();
5652            }
5653
5654            changed = true;
5655        }
5656
5657        if (isFocused()) {
5658            // This offsets because getInterestingRect() is in terms of
5659            // viewport coordinates, but requestRectangleOnScreen()
5660            // is in terms of content coordinates.
5661
5662            Rect r = new Rect(x, top, x + 1, bottom);
5663            getInterestingRect(r, line);
5664            r.offset(mScrollX, mScrollY);
5665
5666            if (requestRectangleOnScreen(r)) {
5667                changed = true;
5668            }
5669        }
5670
5671        return changed;
5672    }
5673
5674    /**
5675     * Move the cursor, if needed, so that it is at an offset that is visible
5676     * to the user.  This will not move the cursor if it represents more than
5677     * one character (a selection range).  This will only work if the
5678     * TextView contains spannable text; otherwise it will do nothing.
5679     *
5680     * @return True if the cursor was actually moved, false otherwise.
5681     */
5682    public boolean moveCursorToVisibleOffset() {
5683        if (!(mText instanceof Spannable)) {
5684            return false;
5685        }
5686        int start = getSelectionStart();
5687        int end = getSelectionEnd();
5688        if (start != end) {
5689            return false;
5690        }
5691
5692        // First: make sure the line is visible on screen:
5693
5694        int line = mLayout.getLineForOffset(start);
5695
5696        final int top = mLayout.getLineTop(line);
5697        final int bottom = mLayout.getLineTop(line + 1);
5698        final int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
5699        int vslack = (bottom - top) / 2;
5700        if (vslack > vspace / 4)
5701            vslack = vspace / 4;
5702        final int vs = mScrollY;
5703
5704        if (top < (vs+vslack)) {
5705            line = mLayout.getLineForVertical(vs+vslack+(bottom-top));
5706        } else if (bottom > (vspace+vs-vslack)) {
5707            line = mLayout.getLineForVertical(vspace+vs-vslack-(bottom-top));
5708        }
5709
5710        // Next: make sure the character is visible on screen:
5711
5712        final int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
5713        final int hs = mScrollX;
5714        final int leftChar = mLayout.getOffsetForHorizontal(line, hs);
5715        final int rightChar = mLayout.getOffsetForHorizontal(line, hspace+hs);
5716
5717        int newStart = start;
5718        if (newStart < leftChar) {
5719            newStart = leftChar;
5720        } else if (newStart > rightChar) {
5721            newStart = rightChar;
5722        }
5723
5724        if (newStart != start) {
5725            Selection.setSelection((Spannable)mText, newStart);
5726            return true;
5727        }
5728
5729        return false;
5730    }
5731
5732    @Override
5733    public void computeScroll() {
5734        if (mScroller != null) {
5735            if (mScroller.computeScrollOffset()) {
5736                mScrollX = mScroller.getCurrX();
5737                mScrollY = mScroller.getCurrY();
5738                postInvalidate();  // So we draw again
5739            }
5740        }
5741    }
5742
5743    private void getInterestingRect(Rect r, int line) {
5744        convertFromViewportToContentCoordinates(r);
5745
5746        // Rectangle can can be expanded on first and last line to take
5747        // padding into account.
5748        // TODO Take left/right padding into account too?
5749        if (line == 0) r.top -= getExtendedPaddingTop();
5750        if (line == mLayout.getLineCount() - 1) r.bottom += getExtendedPaddingBottom();
5751    }
5752
5753    private void convertFromViewportToContentCoordinates(Rect r) {
5754        int paddingTop = getExtendedPaddingTop();
5755        if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
5756            paddingTop += getVerticalOffset(false);
5757        }
5758        r.top += paddingTop;
5759        r.bottom += paddingTop;
5760
5761        int paddingLeft = getCompoundPaddingLeft();
5762        r.left += paddingLeft;
5763        r.right += paddingLeft;
5764
5765        r.offset(-mScrollX, -mScrollY);
5766    }
5767
5768    @Override
5769    public void debug(int depth) {
5770        super.debug(depth);
5771
5772        String output = debugIndent(depth);
5773        output += "frame={" + mLeft + ", " + mTop + ", " + mRight
5774                + ", " + mBottom + "} scroll={" + mScrollX + ", " + mScrollY
5775                + "} ";
5776
5777        if (mText != null) {
5778
5779            output += "mText=\"" + mText + "\" ";
5780            if (mLayout != null) {
5781                output += "mLayout width=" + mLayout.getWidth()
5782                        + " height=" + mLayout.getHeight();
5783            }
5784        } else {
5785            output += "mText=NULL";
5786        }
5787        Log.d(VIEW_LOG_TAG, output);
5788    }
5789
5790    /**
5791     * Convenience for {@link Selection#getSelectionStart}.
5792     */
5793    @ViewDebug.ExportedProperty(category = "text")
5794    public int getSelectionStart() {
5795        return Selection.getSelectionStart(getText());
5796    }
5797
5798    /**
5799     * Convenience for {@link Selection#getSelectionEnd}.
5800     */
5801    @ViewDebug.ExportedProperty(category = "text")
5802    public int getSelectionEnd() {
5803        return Selection.getSelectionEnd(getText());
5804    }
5805
5806    /**
5807     * Return true iff there is a selection inside this text view.
5808     */
5809    public boolean hasSelection() {
5810        return getSelectionStart() != getSelectionEnd();
5811    }
5812
5813    /**
5814     * Sets the properties of this field (lines, horizontally scrolling,
5815     * transformation method) to be for a single-line input.
5816     *
5817     * @attr ref android.R.styleable#TextView_singleLine
5818     */
5819    public void setSingleLine() {
5820        setSingleLine(true);
5821    }
5822
5823    /**
5824     * If true, sets the properties of this field (lines, horizontally
5825     * scrolling, transformation method) to be for a single-line input;
5826     * if false, restores these to the default conditions.
5827     * Note that calling this with false restores default conditions,
5828     * not necessarily those that were in effect prior to calling
5829     * it with true.
5830     *
5831     * @attr ref android.R.styleable#TextView_singleLine
5832     */
5833    @android.view.RemotableViewMethod
5834    public void setSingleLine(boolean singleLine) {
5835        if ((mInputType&EditorInfo.TYPE_MASK_CLASS)
5836                == EditorInfo.TYPE_CLASS_TEXT) {
5837            if (singleLine) {
5838                mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
5839            } else {
5840                mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
5841            }
5842        }
5843        applySingleLine(singleLine, true);
5844    }
5845
5846    private void applySingleLine(boolean singleLine, boolean applyTransformation) {
5847        mSingleLine = singleLine;
5848        if (singleLine) {
5849            setLines(1);
5850            setHorizontallyScrolling(true);
5851            if (applyTransformation) {
5852                setTransformationMethod(SingleLineTransformationMethod.
5853                                        getInstance());
5854            }
5855        } else {
5856            setMaxLines(Integer.MAX_VALUE);
5857            setHorizontallyScrolling(false);
5858            if (applyTransformation) {
5859                setTransformationMethod(null);
5860            }
5861        }
5862    }
5863
5864    /**
5865     * Causes words in the text that are longer than the view is wide
5866     * to be ellipsized instead of broken in the middle.  You may also
5867     * want to {@link #setSingleLine} or {@link #setHorizontallyScrolling}
5868     * to constrain the text to a single line.  Use <code>null</code>
5869     * to turn off ellipsizing.
5870     *
5871     * @attr ref android.R.styleable#TextView_ellipsize
5872     */
5873    public void setEllipsize(TextUtils.TruncateAt where) {
5874        mEllipsize = where;
5875
5876        if (mLayout != null) {
5877            nullLayouts();
5878            requestLayout();
5879            invalidate();
5880        }
5881    }
5882
5883    /**
5884     * Sets how many times to repeat the marquee animation. Only applied if the
5885     * TextView has marquee enabled. Set to -1 to repeat indefinitely.
5886     *
5887     * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
5888     */
5889    public void setMarqueeRepeatLimit(int marqueeLimit) {
5890        mMarqueeRepeatLimit = marqueeLimit;
5891    }
5892
5893    /**
5894     * Returns where, if anywhere, words that are longer than the view
5895     * is wide should be ellipsized.
5896     */
5897    @ViewDebug.ExportedProperty
5898    public TextUtils.TruncateAt getEllipsize() {
5899        return mEllipsize;
5900    }
5901
5902    /**
5903     * Set the TextView so that when it takes focus, all the text is
5904     * selected.
5905     *
5906     * @attr ref android.R.styleable#TextView_selectAllOnFocus
5907     */
5908    @android.view.RemotableViewMethod
5909    public void setSelectAllOnFocus(boolean selectAllOnFocus) {
5910        mSelectAllOnFocus = selectAllOnFocus;
5911
5912        if (selectAllOnFocus && !(mText instanceof Spannable)) {
5913            setText(mText, BufferType.SPANNABLE);
5914        }
5915    }
5916
5917    /**
5918     * Set whether the cursor is visible.  The default is true.
5919     *
5920     * @attr ref android.R.styleable#TextView_cursorVisible
5921     */
5922    @android.view.RemotableViewMethod
5923    public void setCursorVisible(boolean visible) {
5924        mCursorVisible = visible;
5925        invalidate();
5926
5927        if (visible) {
5928            makeBlink();
5929        } else if (mBlink != null) {
5930            mBlink.removeCallbacks(mBlink);
5931        }
5932
5933        // InsertionPointCursorController depends on mCursorVisible
5934        prepareCursorControllers();
5935    }
5936
5937    private boolean canMarquee() {
5938        int width = (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight());
5939        return width > 0 && mLayout.getLineWidth(0) > width;
5940    }
5941
5942    private void startMarquee() {
5943        // Do not ellipsize EditText
5944        if (mInput != null) return;
5945
5946        if (compressText(getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight())) {
5947            return;
5948        }
5949
5950        if ((mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected()) &&
5951                getLineCount() == 1 && canMarquee()) {
5952
5953            if (mMarquee == null) mMarquee = new Marquee(this);
5954            mMarquee.start(mMarqueeRepeatLimit);
5955        }
5956    }
5957
5958    private void stopMarquee() {
5959        if (mMarquee != null && !mMarquee.isStopped()) {
5960            mMarquee.stop();
5961        }
5962    }
5963
5964    private void startStopMarquee(boolean start) {
5965        if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
5966            if (start) {
5967                startMarquee();
5968            } else {
5969                stopMarquee();
5970            }
5971        }
5972    }
5973
5974    private static final class Marquee extends Handler {
5975        // TODO: Add an option to configure this
5976        private static final float MARQUEE_DELTA_MAX = 0.07f;
5977        private static final int MARQUEE_DELAY = 1200;
5978        private static final int MARQUEE_RESTART_DELAY = 1200;
5979        private static final int MARQUEE_RESOLUTION = 1000 / 30;
5980        private static final int MARQUEE_PIXELS_PER_SECOND = 30;
5981
5982        private static final byte MARQUEE_STOPPED = 0x0;
5983        private static final byte MARQUEE_STARTING = 0x1;
5984        private static final byte MARQUEE_RUNNING = 0x2;
5985
5986        private static final int MESSAGE_START = 0x1;
5987        private static final int MESSAGE_TICK = 0x2;
5988        private static final int MESSAGE_RESTART = 0x3;
5989
5990        private final WeakReference<TextView> mView;
5991
5992        private byte mStatus = MARQUEE_STOPPED;
5993        private final float mScrollUnit;
5994        private float mMaxScroll;
5995        float mMaxFadeScroll;
5996        private float mGhostStart;
5997        private float mGhostOffset;
5998        private float mFadeStop;
5999        private int mRepeatLimit;
6000
6001        float mScroll;
6002
6003        Marquee(TextView v) {
6004            final float density = v.getContext().getResources().getDisplayMetrics().density;
6005            mScrollUnit = (MARQUEE_PIXELS_PER_SECOND * density) / MARQUEE_RESOLUTION;
6006            mView = new WeakReference<TextView>(v);
6007        }
6008
6009        @Override
6010        public void handleMessage(Message msg) {
6011            switch (msg.what) {
6012                case MESSAGE_START:
6013                    mStatus = MARQUEE_RUNNING;
6014                    tick();
6015                    break;
6016                case MESSAGE_TICK:
6017                    tick();
6018                    break;
6019                case MESSAGE_RESTART:
6020                    if (mStatus == MARQUEE_RUNNING) {
6021                        if (mRepeatLimit >= 0) {
6022                            mRepeatLimit--;
6023                        }
6024                        start(mRepeatLimit);
6025                    }
6026                    break;
6027            }
6028        }
6029
6030        void tick() {
6031            if (mStatus != MARQUEE_RUNNING) {
6032                return;
6033            }
6034
6035            removeMessages(MESSAGE_TICK);
6036
6037            final TextView textView = mView.get();
6038            if (textView != null && (textView.isFocused() || textView.isSelected())) {
6039                mScroll += mScrollUnit;
6040                if (mScroll > mMaxScroll) {
6041                    mScroll = mMaxScroll;
6042                    sendEmptyMessageDelayed(MESSAGE_RESTART, MARQUEE_RESTART_DELAY);
6043                } else {
6044                    sendEmptyMessageDelayed(MESSAGE_TICK, MARQUEE_RESOLUTION);
6045                }
6046                textView.invalidate();
6047            }
6048        }
6049
6050        void stop() {
6051            mStatus = MARQUEE_STOPPED;
6052            removeMessages(MESSAGE_START);
6053            removeMessages(MESSAGE_RESTART);
6054            removeMessages(MESSAGE_TICK);
6055            resetScroll();
6056        }
6057
6058        private void resetScroll() {
6059            mScroll = 0.0f;
6060            final TextView textView = mView.get();
6061            if (textView != null) textView.invalidate();
6062        }
6063
6064        void start(int repeatLimit) {
6065            if (repeatLimit == 0) {
6066                stop();
6067                return;
6068            }
6069            mRepeatLimit = repeatLimit;
6070            final TextView textView = mView.get();
6071            if (textView != null && textView.mLayout != null) {
6072                mStatus = MARQUEE_STARTING;
6073                mScroll = 0.0f;
6074                final int textWidth = textView.getWidth() - textView.getCompoundPaddingLeft() -
6075                        textView.getCompoundPaddingRight();
6076                final float lineWidth = textView.mLayout.getLineWidth(0);
6077                final float gap = textWidth / 3.0f;
6078                mGhostStart = lineWidth - textWidth + gap;
6079                mMaxScroll = mGhostStart + textWidth;
6080                mGhostOffset = lineWidth + gap;
6081                mFadeStop = lineWidth + textWidth / 6.0f;
6082                mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;
6083
6084                textView.invalidate();
6085                sendEmptyMessageDelayed(MESSAGE_START, MARQUEE_DELAY);
6086            }
6087        }
6088
6089        float getGhostOffset() {
6090            return mGhostOffset;
6091        }
6092
6093        boolean shouldDrawLeftFade() {
6094            return mScroll <= mFadeStop;
6095        }
6096
6097        boolean shouldDrawGhost() {
6098            return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart;
6099        }
6100
6101        boolean isRunning() {
6102            return mStatus == MARQUEE_RUNNING;
6103        }
6104
6105        boolean isStopped() {
6106            return mStatus == MARQUEE_STOPPED;
6107        }
6108    }
6109
6110    /**
6111     * This method is called when the text is changed, in case any
6112     * subclasses would like to know.
6113     *
6114     * @param text The text the TextView is displaying.
6115     * @param start The offset of the start of the range of the text
6116     *              that was modified.
6117     * @param before The offset of the former end of the range of the
6118     *               text that was modified.  If text was simply inserted,
6119     *               this will be the same as <code>start</code>.
6120     *               If text was replaced with new text or deleted, the
6121     *               length of the old text was <code>before-start</code>.
6122     * @param after The offset of the end of the range of the text
6123     *              that was modified.  If text was simply deleted,
6124     *              this will be the same as <code>start</code>.
6125     *              If text was replaced with new text or inserted,
6126     *              the length of the new text is <code>after-start</code>.
6127     */
6128    protected void onTextChanged(CharSequence text,
6129                                 int start, int before, int after) {
6130    }
6131
6132    /**
6133     * This method is called when the selection has changed, in case any
6134     * subclasses would like to know.
6135     *
6136     * @param selStart The new selection start location.
6137     * @param selEnd The new selection end location.
6138     */
6139    protected void onSelectionChanged(int selStart, int selEnd) {
6140    }
6141
6142    /**
6143     * Adds a TextWatcher to the list of those whose methods are called
6144     * whenever this TextView's text changes.
6145     * <p>
6146     * In 1.0, the {@link TextWatcher#afterTextChanged} method was erroneously
6147     * not called after {@link #setText} calls.  Now, doing {@link #setText}
6148     * if there are any text changed listeners forces the buffer type to
6149     * Editable if it would not otherwise be and does call this method.
6150     */
6151    public void addTextChangedListener(TextWatcher watcher) {
6152        if (mListeners == null) {
6153            mListeners = new ArrayList<TextWatcher>();
6154        }
6155
6156        mListeners.add(watcher);
6157    }
6158
6159    /**
6160     * Removes the specified TextWatcher from the list of those whose
6161     * methods are called
6162     * whenever this TextView's text changes.
6163     */
6164    public void removeTextChangedListener(TextWatcher watcher) {
6165        if (mListeners != null) {
6166            int i = mListeners.indexOf(watcher);
6167
6168            if (i >= 0) {
6169                mListeners.remove(i);
6170            }
6171        }
6172    }
6173
6174    private void sendBeforeTextChanged(CharSequence text, int start, int before,
6175                                   int after) {
6176        if (mListeners != null) {
6177            final ArrayList<TextWatcher> list = mListeners;
6178            final int count = list.size();
6179            for (int i = 0; i < count; i++) {
6180                list.get(i).beforeTextChanged(text, start, before, after);
6181            }
6182        }
6183    }
6184
6185    /**
6186     * Not private so it can be called from an inner class without going
6187     * through a thunk.
6188     */
6189    void sendOnTextChanged(CharSequence text, int start, int before,
6190                                   int after) {
6191        if (mListeners != null) {
6192            final ArrayList<TextWatcher> list = mListeners;
6193            final int count = list.size();
6194            for (int i = 0; i < count; i++) {
6195                list.get(i).onTextChanged(text, start, before, after);
6196            }
6197        }
6198    }
6199
6200    /**
6201     * Not private so it can be called from an inner class without going
6202     * through a thunk.
6203     */
6204    void sendAfterTextChanged(Editable text) {
6205        if (mListeners != null) {
6206            final ArrayList<TextWatcher> list = mListeners;
6207            final int count = list.size();
6208            for (int i = 0; i < count; i++) {
6209                list.get(i).afterTextChanged(text);
6210            }
6211        }
6212    }
6213
6214    /**
6215     * Not private so it can be called from an inner class without going
6216     * through a thunk.
6217     */
6218    void handleTextChanged(CharSequence buffer, int start,
6219            int before, int after) {
6220        final InputMethodState ims = mInputMethodState;
6221        if (ims == null || ims.mBatchEditNesting == 0) {
6222            updateAfterEdit();
6223        }
6224        if (ims != null) {
6225            ims.mContentChanged = true;
6226            if (ims.mChangedStart < 0) {
6227                ims.mChangedStart = start;
6228                ims.mChangedEnd = start+before;
6229            } else {
6230                if (ims.mChangedStart > start) ims.mChangedStart = start;
6231                if (ims.mChangedEnd < (start+before)) ims.mChangedEnd = start+before;
6232            }
6233            ims.mChangedDelta += after-before;
6234        }
6235
6236        sendOnTextChanged(buffer, start, before, after);
6237        onTextChanged(buffer, start, before, after);
6238        hideControllers();
6239    }
6240
6241    /**
6242     * Not private so it can be called from an inner class without going
6243     * through a thunk.
6244     */
6245    void spanChange(Spanned buf, Object what, int oldStart, int newStart,
6246            int oldEnd, int newEnd) {
6247        // XXX Make the start and end move together if this ends up
6248        // spending too much time invalidating.
6249
6250        boolean selChanged = false;
6251        int newSelStart=-1, newSelEnd=-1;
6252
6253        final InputMethodState ims = mInputMethodState;
6254
6255        if (what == Selection.SELECTION_END) {
6256            mHighlightPathBogus = true;
6257            selChanged = true;
6258            newSelEnd = newStart;
6259
6260            if (!isFocused()) {
6261                mSelectionMoved = true;
6262            }
6263
6264            if (oldStart >= 0 || newStart >= 0) {
6265                invalidateCursor(Selection.getSelectionStart(buf), oldStart, newStart);
6266                registerForPreDraw();
6267
6268                if (isFocused()) {
6269                    mShowCursor = SystemClock.uptimeMillis();
6270                    makeBlink();
6271                }
6272            }
6273        }
6274
6275        if (what == Selection.SELECTION_START) {
6276            mHighlightPathBogus = true;
6277            selChanged = true;
6278            newSelStart = newStart;
6279
6280            if (!isFocused()) {
6281                mSelectionMoved = true;
6282            }
6283
6284            if (oldStart >= 0 || newStart >= 0) {
6285                int end = Selection.getSelectionEnd(buf);
6286                invalidateCursor(end, oldStart, newStart);
6287            }
6288        }
6289
6290        if (selChanged) {
6291            if ((buf.getSpanFlags(what)&Spanned.SPAN_INTERMEDIATE) == 0) {
6292                if (newSelStart < 0) {
6293                    newSelStart = Selection.getSelectionStart(buf);
6294                }
6295                if (newSelEnd < 0) {
6296                    newSelEnd = Selection.getSelectionEnd(buf);
6297                }
6298                onSelectionChanged(newSelStart, newSelEnd);
6299            }
6300        }
6301
6302        if (what instanceof UpdateAppearance ||
6303            what instanceof ParagraphStyle) {
6304            if (ims == null || ims.mBatchEditNesting == 0) {
6305                invalidate();
6306                mHighlightPathBogus = true;
6307                checkForResize();
6308            } else {
6309                ims.mContentChanged = true;
6310            }
6311        }
6312
6313        if (MetaKeyKeyListener.isMetaTracker(buf, what)) {
6314            mHighlightPathBogus = true;
6315            if (ims != null && MetaKeyKeyListener.isSelectingMetaTracker(buf, what)) {
6316                ims.mSelectionModeChanged = true;
6317            }
6318
6319            if (Selection.getSelectionStart(buf) >= 0) {
6320                if (ims == null || ims.mBatchEditNesting == 0) {
6321                    invalidateCursor();
6322                } else {
6323                    ims.mCursorChanged = true;
6324                }
6325            }
6326        }
6327
6328        if (what instanceof ParcelableSpan) {
6329            // If this is a span that can be sent to a remote process,
6330            // the current extract editor would be interested in it.
6331            if (ims != null && ims.mExtracting != null) {
6332                if (ims.mBatchEditNesting != 0) {
6333                    if (oldStart >= 0) {
6334                        if (ims.mChangedStart > oldStart) {
6335                            ims.mChangedStart = oldStart;
6336                        }
6337                        if (ims.mChangedStart > oldEnd) {
6338                            ims.mChangedStart = oldEnd;
6339                        }
6340                    }
6341                    if (newStart >= 0) {
6342                        if (ims.mChangedStart > newStart) {
6343                            ims.mChangedStart = newStart;
6344                        }
6345                        if (ims.mChangedStart > newEnd) {
6346                            ims.mChangedStart = newEnd;
6347                        }
6348                    }
6349                } else {
6350                    if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Span change outside of batch: "
6351                            + oldStart + "-" + oldEnd + ","
6352                            + newStart + "-" + newEnd + what);
6353                    ims.mContentChanged = true;
6354                }
6355            }
6356        }
6357    }
6358
6359    private class ChangeWatcher
6360    implements TextWatcher, SpanWatcher {
6361
6362        private CharSequence mBeforeText;
6363
6364        public void beforeTextChanged(CharSequence buffer, int start,
6365                                      int before, int after) {
6366            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "beforeTextChanged start=" + start
6367                    + " before=" + before + " after=" + after + ": " + buffer);
6368
6369            if (AccessibilityManager.getInstance(mContext).isEnabled()
6370                    && !isPasswordInputType(mInputType)) {
6371                mBeforeText = buffer.toString();
6372            }
6373
6374            TextView.this.sendBeforeTextChanged(buffer, start, before, after);
6375        }
6376
6377        public void onTextChanged(CharSequence buffer, int start,
6378                                  int before, int after) {
6379            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onTextChanged start=" + start
6380                    + " before=" + before + " after=" + after + ": " + buffer);
6381            TextView.this.handleTextChanged(buffer, start, before, after);
6382
6383            if (AccessibilityManager.getInstance(mContext).isEnabled() &&
6384                    (isFocused() || isSelected() &&
6385                    isShown())) {
6386                sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after);
6387                mBeforeText = null;
6388            }
6389        }
6390
6391        public void afterTextChanged(Editable buffer) {
6392            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "afterTextChanged: " + buffer);
6393            TextView.this.sendAfterTextChanged(buffer);
6394
6395            if (MetaKeyKeyListener.getMetaState(buffer,
6396                                 MetaKeyKeyListener.META_SELECTING) != 0) {
6397                MetaKeyKeyListener.stopSelecting(TextView.this, buffer);
6398            }
6399        }
6400
6401        public void onSpanChanged(Spannable buf,
6402                                  Object what, int s, int e, int st, int en) {
6403            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanChanged s=" + s + " e=" + e
6404                    + " st=" + st + " en=" + en + " what=" + what + ": " + buf);
6405            TextView.this.spanChange(buf, what, s, st, e, en);
6406        }
6407
6408        public void onSpanAdded(Spannable buf, Object what, int s, int e) {
6409            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanAdded s=" + s + " e=" + e
6410                    + " what=" + what + ": " + buf);
6411            TextView.this.spanChange(buf, what, -1, s, -1, e);
6412        }
6413
6414        public void onSpanRemoved(Spannable buf, Object what, int s, int e) {
6415            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanRemoved s=" + s + " e=" + e
6416                    + " what=" + what + ": " + buf);
6417            TextView.this.spanChange(buf, what, s, -1, e, -1);
6418        }
6419    }
6420
6421    private void makeBlink() {
6422        if (!mCursorVisible) {
6423            if (mBlink != null) {
6424                mBlink.removeCallbacks(mBlink);
6425            }
6426
6427            return;
6428        }
6429
6430        if (mBlink == null)
6431            mBlink = new Blink(this);
6432
6433        mBlink.removeCallbacks(mBlink);
6434        mBlink.postAtTime(mBlink, mShowCursor + BLINK);
6435    }
6436
6437    /**
6438     * @hide
6439     */
6440    @Override
6441    public void dispatchFinishTemporaryDetach() {
6442        mDispatchTemporaryDetach = true;
6443        super.dispatchFinishTemporaryDetach();
6444        mDispatchTemporaryDetach = false;
6445    }
6446
6447    @Override
6448    public void onStartTemporaryDetach() {
6449        super.onStartTemporaryDetach();
6450        // Only track when onStartTemporaryDetach() is called directly,
6451        // usually because this instance is an editable field in a list
6452        if (!mDispatchTemporaryDetach) mTemporaryDetach = true;
6453    }
6454
6455    @Override
6456    public void onFinishTemporaryDetach() {
6457        super.onFinishTemporaryDetach();
6458        // Only track when onStartTemporaryDetach() is called directly,
6459        // usually because this instance is an editable field in a list
6460        if (!mDispatchTemporaryDetach) mTemporaryDetach = false;
6461    }
6462
6463    @Override
6464    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
6465        if (mTemporaryDetach) {
6466            // If we are temporarily in the detach state, then do nothing.
6467            super.onFocusChanged(focused, direction, previouslyFocusedRect);
6468            return;
6469        }
6470
6471        mShowCursor = SystemClock.uptimeMillis();
6472
6473        ensureEndedBatchEdit();
6474
6475        if (focused) {
6476            int selStart = getSelectionStart();
6477            int selEnd = getSelectionEnd();
6478
6479            if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
6480                boolean selMoved = mSelectionMoved;
6481
6482                if (mSelectionModifierCursorController != null) {
6483                    final int touchOffset =
6484                        ((SelectionModifierCursorController) mSelectionModifierCursorController).
6485                        getMinTouchOffset();
6486                    Selection.setSelection((Spannable) mText, touchOffset);
6487                }
6488
6489                if (mMovement != null) {
6490                    mMovement.onTakeFocus(this, (Spannable) mText, direction);
6491                }
6492
6493                if (mSelectAllOnFocus) {
6494                    Selection.setSelection((Spannable) mText, 0, mText.length());
6495                }
6496
6497                if (selMoved && selStart >= 0 && selEnd >= 0) {
6498                    /*
6499                     * Someone intentionally set the selection, so let them
6500                     * do whatever it is that they wanted to do instead of
6501                     * the default on-focus behavior.  We reset the selection
6502                     * here instead of just skipping the onTakeFocus() call
6503                     * because some movement methods do something other than
6504                     * just setting the selection in theirs and we still
6505                     * need to go through that path.
6506                     */
6507
6508                    Selection.setSelection((Spannable) mText, selStart, selEnd);
6509                }
6510                mTouchFocusSelected = true;
6511            }
6512
6513            mFrozenWithFocus = false;
6514            mSelectionMoved = false;
6515
6516            if (mText instanceof Spannable) {
6517                Spannable sp = (Spannable) mText;
6518                MetaKeyKeyListener.resetMetaState(sp);
6519            }
6520
6521            makeBlink();
6522
6523            if (mError != null) {
6524                showError();
6525            }
6526
6527            // We cannot start the selection mode immediately. The layout may be null here and is
6528            // needed by the cursor controller. Layout creation is deferred up to drawing. The
6529            // selection action mode will be started in onPreDraw().
6530            if (selStart != selEnd) {
6531                mShouldStartTextSelectionMode = true;
6532            }
6533        } else {
6534            if (mError != null) {
6535                hideError();
6536            }
6537            // Don't leave us in the middle of a batch edit.
6538            onEndBatchEdit();
6539
6540            hideInsertionPointCursorController();
6541            terminateTextSelectionMode();
6542        }
6543
6544        startStopMarquee(focused);
6545
6546        if (mTransformation != null) {
6547            mTransformation.onFocusChanged(this, mText, focused, direction,
6548                                           previouslyFocusedRect);
6549        }
6550
6551        super.onFocusChanged(focused, direction, previouslyFocusedRect);
6552    }
6553
6554    @Override
6555    public void onWindowFocusChanged(boolean hasWindowFocus) {
6556        super.onWindowFocusChanged(hasWindowFocus);
6557
6558        if (hasWindowFocus) {
6559            if (mBlink != null) {
6560                mBlink.uncancel();
6561
6562                if (isFocused()) {
6563                    mShowCursor = SystemClock.uptimeMillis();
6564                    makeBlink();
6565                }
6566            }
6567        } else {
6568            if (mBlink != null) {
6569                mBlink.cancel();
6570            }
6571            // Don't leave us in the middle of a batch edit.
6572            onEndBatchEdit();
6573            if (mInputContentType != null) {
6574                mInputContentType.enterDown = false;
6575            }
6576        }
6577
6578        startStopMarquee(hasWindowFocus);
6579    }
6580
6581    /**
6582     * Use {@link BaseInputConnection#removeComposingSpans
6583     * BaseInputConnection.removeComposingSpans()} to remove any IME composing
6584     * state from this text view.
6585     */
6586    public void clearComposingText() {
6587        if (mText instanceof Spannable) {
6588            BaseInputConnection.removeComposingSpans((Spannable)mText);
6589        }
6590    }
6591
6592    @Override
6593    public void setSelected(boolean selected) {
6594        boolean wasSelected = isSelected();
6595
6596        super.setSelected(selected);
6597
6598        if (selected != wasSelected && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
6599            if (selected) {
6600                startMarquee();
6601            } else {
6602                stopMarquee();
6603            }
6604        }
6605    }
6606
6607    class CommitSelectionReceiver extends ResultReceiver {
6608        private final int mPrevStart, mPrevEnd;
6609        private final int mNewStart, mNewEnd;
6610
6611        public CommitSelectionReceiver(int mPrevStart, int mPrevEnd, int mNewStart, int mNewEnd) {
6612            super(getHandler());
6613            this.mPrevStart = mPrevStart;
6614            this.mPrevEnd = mPrevEnd;
6615            this.mNewStart = mNewStart;
6616            this.mNewEnd = mNewEnd;
6617        }
6618
6619        @Override
6620        protected void onReceiveResult(int resultCode, Bundle resultData) {
6621            int start = mNewStart;
6622            int end = mNewEnd;
6623
6624            // Move the cursor to the new position, unless this tap was actually
6625            // use to show the IMM. Leave cursor unchanged in that case.
6626            if (resultCode == InputMethodManager.RESULT_SHOWN) {
6627                start = mPrevStart;
6628                end = mPrevEnd;
6629            } else {
6630                if ((mPrevStart != mPrevEnd) && (start == end)) {
6631                    if ((start >= mPrevStart) && (start < mPrevEnd)) {
6632                        // Tapping inside the selection does nothing
6633                        Selection.setSelection((Spannable) mText, mPrevStart, mPrevEnd);
6634                        showContextMenu();
6635                        return;
6636                    } else {
6637                        // Tapping outside stops selection mode, if any
6638                        stopTextSelectionMode();
6639                    }
6640                }
6641
6642                if (mInsertionPointCursorController != null) {
6643                    mInsertionPointCursorController.show();
6644                }
6645            }
6646
6647            final int len = mText.length();
6648            if (start > len) {
6649                start = len;
6650            }
6651            if (end > len) {
6652                end = len;
6653            }
6654            Selection.setSelection((Spannable)mText, start, end);
6655        }
6656    }
6657
6658    @Override
6659    public boolean onTouchEvent(MotionEvent event) {
6660        final int action = event.getAction();
6661        if (action == MotionEvent.ACTION_DOWN) {
6662            // Reset this state; it will be re-set if super.onTouchEvent
6663            // causes focus to move to the view.
6664            mTouchFocusSelected = false;
6665            mScrolled = false;
6666        }
6667
6668        final boolean superResult = super.onTouchEvent(event);
6669
6670        /*
6671         * Don't handle the release after a long press, because it will
6672         * move the selection away from whatever the menu action was
6673         * trying to affect.
6674         */
6675        if (mEatTouchRelease && action == MotionEvent.ACTION_UP) {
6676            mEatTouchRelease = false;
6677            return superResult;
6678        }
6679
6680        if ((mMovement != null || onCheckIsTextEditor()) && mText instanceof Spannable && mLayout != null) {
6681
6682            int oldSelStart = getSelectionStart();
6683            int oldSelEnd = getSelectionEnd();
6684
6685            if (mInsertionPointCursorController != null) {
6686                mInsertionPointCursorController.onTouchEvent(event);
6687            }
6688            if (mSelectionModifierCursorController != null) {
6689                mSelectionModifierCursorController.onTouchEvent(event);
6690            }
6691
6692            boolean handled = false;
6693
6694            if (mMovement != null) {
6695                handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
6696            }
6697
6698            if (isTextEditable()) {
6699                if (action == MotionEvent.ACTION_UP && isFocused() && !mScrolled) {
6700                    InputMethodManager imm = (InputMethodManager)
6701                          getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
6702
6703                    final int newSelStart = getSelectionStart();
6704                    final int newSelEnd = getSelectionEnd();
6705
6706                    CommitSelectionReceiver csr = null;
6707                    if (newSelStart != oldSelStart || newSelEnd != oldSelEnd ||
6708                            didTouchFocusSelect()) {
6709                        csr = new CommitSelectionReceiver(oldSelStart, oldSelEnd,
6710                                newSelStart, newSelEnd);
6711                    }
6712
6713                    handled |= imm.showSoftInput(this, 0, csr) && (csr != null);
6714                }
6715            }
6716
6717            if (handled) {
6718                return true;
6719            }
6720        }
6721
6722        return superResult;
6723    }
6724
6725    private void prepareCursorControllers() {
6726        // TODO Add an extra android:cursorController flag to disable the controller?
6727        if (mCursorVisible && mLayout != null) {
6728            if (mInsertionPointCursorController == null) {
6729                mInsertionPointCursorController = new InsertionPointCursorController();
6730            }
6731        } else {
6732            mInsertionPointCursorController = null;
6733        }
6734
6735        if (canSelectText() && mLayout != null) {
6736            if (mSelectionModifierCursorController == null) {
6737                mSelectionModifierCursorController = new SelectionModifierCursorController();
6738            }
6739        } else {
6740            // Stop selection mode if the controller becomes unavailable.
6741            stopTextSelectionMode();
6742            mSelectionModifierCursorController = null;
6743        }
6744    }
6745
6746    /**
6747     * @return True iff this TextView contains a text that can be edited.
6748     */
6749    private boolean isTextEditable() {
6750        return mText instanceof Editable && onCheckIsTextEditor();
6751    }
6752
6753    /**
6754     * Returns true, only while processing a touch gesture, if the initial
6755     * touch down event caused focus to move to the text view and as a result
6756     * its selection changed.  Only valid while processing the touch gesture
6757     * of interest.
6758     */
6759    public boolean didTouchFocusSelect() {
6760        return mTouchFocusSelected;
6761    }
6762
6763    @Override
6764    public void cancelLongPress() {
6765        super.cancelLongPress();
6766        mScrolled = true;
6767    }
6768
6769    @Override
6770    public boolean onTrackballEvent(MotionEvent event) {
6771        if (mMovement != null && mText instanceof Spannable &&
6772            mLayout != null) {
6773            if (mMovement.onTrackballEvent(this, (Spannable) mText, event)) {
6774                return true;
6775            }
6776        }
6777
6778        return super.onTrackballEvent(event);
6779    }
6780
6781    public void setScroller(Scroller s) {
6782        mScroller = s;
6783    }
6784
6785    private static class Blink extends Handler implements Runnable {
6786        private final WeakReference<TextView> mView;
6787        private boolean mCancelled;
6788
6789        public Blink(TextView v) {
6790            mView = new WeakReference<TextView>(v);
6791        }
6792
6793        public void run() {
6794            if (mCancelled) {
6795                return;
6796            }
6797
6798            removeCallbacks(Blink.this);
6799
6800            TextView tv = mView.get();
6801
6802            if (tv != null && tv.isFocused()) {
6803                int st = tv.getSelectionStart();
6804                int en = tv.getSelectionEnd();
6805
6806                if (st == en && st >= 0 && en >= 0) {
6807                    if (tv.mLayout != null) {
6808                        tv.invalidateCursorPath();
6809                    }
6810
6811                    postAtTime(this, SystemClock.uptimeMillis() + BLINK);
6812                }
6813            }
6814        }
6815
6816        void cancel() {
6817            if (!mCancelled) {
6818                removeCallbacks(Blink.this);
6819                mCancelled = true;
6820            }
6821        }
6822
6823        void uncancel() {
6824            mCancelled = false;
6825        }
6826    }
6827
6828    @Override
6829    protected float getLeftFadingEdgeStrength() {
6830        if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
6831            if (mMarquee != null && !mMarquee.isStopped()) {
6832                final Marquee marquee = mMarquee;
6833                if (marquee.shouldDrawLeftFade()) {
6834                    return marquee.mScroll / getHorizontalFadingEdgeLength();
6835                } else {
6836                    return 0.0f;
6837                }
6838            } else if (getLineCount() == 1) {
6839                switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
6840                    case Gravity.LEFT:
6841                        return 0.0f;
6842                    case Gravity.RIGHT:
6843                        return (mLayout.getLineRight(0) - (mRight - mLeft) -
6844                                getCompoundPaddingLeft() - getCompoundPaddingRight() -
6845                                mLayout.getLineLeft(0)) / getHorizontalFadingEdgeLength();
6846                    case Gravity.CENTER_HORIZONTAL:
6847                        return 0.0f;
6848                }
6849            }
6850        }
6851        return super.getLeftFadingEdgeStrength();
6852    }
6853
6854    @Override
6855    protected float getRightFadingEdgeStrength() {
6856        if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
6857            if (mMarquee != null && !mMarquee.isStopped()) {
6858                final Marquee marquee = mMarquee;
6859                return (marquee.mMaxFadeScroll - marquee.mScroll) / getHorizontalFadingEdgeLength();
6860            } else if (getLineCount() == 1) {
6861                switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
6862                    case Gravity.LEFT:
6863                        final int textWidth = (mRight - mLeft) - getCompoundPaddingLeft() -
6864                                getCompoundPaddingRight();
6865                        final float lineWidth = mLayout.getLineWidth(0);
6866                        return (lineWidth - textWidth) / getHorizontalFadingEdgeLength();
6867                    case Gravity.RIGHT:
6868                        return 0.0f;
6869                    case Gravity.CENTER_HORIZONTAL:
6870                        return (mLayout.getLineWidth(0) - ((mRight - mLeft) -
6871                                getCompoundPaddingLeft() - getCompoundPaddingRight())) /
6872                                getHorizontalFadingEdgeLength();
6873                }
6874            }
6875        }
6876        return super.getRightFadingEdgeStrength();
6877    }
6878
6879    @Override
6880    protected int computeHorizontalScrollRange() {
6881        if (mLayout != null)
6882            return mLayout.getWidth();
6883
6884        return super.computeHorizontalScrollRange();
6885    }
6886
6887    @Override
6888    protected int computeVerticalScrollRange() {
6889        if (mLayout != null)
6890            return mLayout.getHeight();
6891
6892        return super.computeVerticalScrollRange();
6893    }
6894
6895    @Override
6896    protected int computeVerticalScrollExtent() {
6897        return getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom();
6898    }
6899
6900    public enum BufferType {
6901        NORMAL, SPANNABLE, EDITABLE,
6902    }
6903
6904    /**
6905     * Returns the TextView_textColor attribute from the
6906     * Resources.StyledAttributes, if set, or the TextAppearance_textColor
6907     * from the TextView_textAppearance attribute, if TextView_textColor
6908     * was not set directly.
6909     */
6910    public static ColorStateList getTextColors(Context context, TypedArray attrs) {
6911        ColorStateList colors;
6912        colors = attrs.getColorStateList(com.android.internal.R.styleable.
6913                                         TextView_textColor);
6914
6915        if (colors == null) {
6916            int ap = attrs.getResourceId(com.android.internal.R.styleable.
6917                                         TextView_textAppearance, -1);
6918            if (ap != -1) {
6919                TypedArray appearance;
6920                appearance = context.obtainStyledAttributes(ap,
6921                                            com.android.internal.R.styleable.TextAppearance);
6922                colors = appearance.getColorStateList(com.android.internal.R.styleable.
6923                                                  TextAppearance_textColor);
6924                appearance.recycle();
6925            }
6926        }
6927
6928        return colors;
6929    }
6930
6931    /**
6932     * Returns the default color from the TextView_textColor attribute
6933     * from the AttributeSet, if set, or the default color from the
6934     * TextAppearance_textColor from the TextView_textAppearance attribute,
6935     * if TextView_textColor was not set directly.
6936     */
6937    public static int getTextColor(Context context,
6938                                   TypedArray attrs,
6939                                   int def) {
6940        ColorStateList colors = getTextColors(context, attrs);
6941
6942        if (colors == null) {
6943            return def;
6944        } else {
6945            return colors.getDefaultColor();
6946        }
6947    }
6948
6949    @Override
6950    public boolean onKeyShortcut(int keyCode, KeyEvent event) {
6951        switch (keyCode) {
6952        case KeyEvent.KEYCODE_A:
6953            if (canSelectAll()) {
6954                return onTextContextMenuItem(ID_SELECT_ALL);
6955            }
6956
6957            break;
6958
6959        case KeyEvent.KEYCODE_X:
6960            if (canCut()) {
6961                return onTextContextMenuItem(ID_CUT);
6962            }
6963
6964            break;
6965
6966        case KeyEvent.KEYCODE_C:
6967            if (canCopy()) {
6968                return onTextContextMenuItem(ID_COPY);
6969            }
6970
6971            break;
6972
6973        case KeyEvent.KEYCODE_V:
6974            if (canPaste()) {
6975                return onTextContextMenuItem(ID_PASTE);
6976            }
6977
6978            break;
6979        }
6980
6981        return super.onKeyShortcut(keyCode, event);
6982    }
6983
6984    private boolean canSelectAll() {
6985        return canSelectText() && mText.length() != 0;
6986    }
6987
6988    private boolean canSelectText() {
6989        // prepareCursorController() relies on this method.
6990        // If you change this condition, make sure prepareCursorController is called anywhere
6991        // the value of this condition might be changed.
6992        return (mText instanceof Spannable &&
6993                mMovement != null &&
6994                mMovement.canSelectArbitrarily());
6995    }
6996
6997    private boolean canCut() {
6998        if (mTransformation instanceof PasswordTransformationMethod) {
6999            return false;
7000        }
7001
7002        if (mText.length() > 0 && getSelectionStart() >= 0) {
7003            if (mText instanceof Editable && mInput != null) {
7004                return true;
7005            }
7006        }
7007
7008        return false;
7009    }
7010
7011    private boolean canCopy() {
7012        if (mTransformation instanceof PasswordTransformationMethod) {
7013            return false;
7014        }
7015
7016        if (mText.length() > 0 && getSelectionStart() >= 0) {
7017            return true;
7018        }
7019
7020        return false;
7021    }
7022
7023    private boolean canPaste() {
7024        return (mText instanceof Editable &&
7025                mInput != null &&
7026                getSelectionStart() >= 0 &&
7027                getSelectionEnd() >= 0 &&
7028                ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)).
7029                hasText());
7030    }
7031
7032    /**
7033     * Returns the offsets delimiting the 'word' located at position offset.
7034     *
7035     * @param offset An offset in the text.
7036     * @return The offsets for the start and end of the word located at <code>offset</code>.
7037     * The two ints offsets are packed in a long, with the starting offset shifted by 32 bits.
7038     * Returns a negative value if no valid word was found.
7039     */
7040    private long getWordLimitsAt(int offset) {
7041        /*
7042         * Quick return if the input type is one where adding words
7043         * to the dictionary doesn't make any sense.
7044         */
7045        int klass = mInputType & InputType.TYPE_MASK_CLASS;
7046        if (klass == InputType.TYPE_CLASS_NUMBER ||
7047            klass == InputType.TYPE_CLASS_PHONE ||
7048            klass == InputType.TYPE_CLASS_DATETIME) {
7049            return -1;
7050        }
7051
7052        int variation = mInputType & InputType.TYPE_MASK_VARIATION;
7053        if (variation == InputType.TYPE_TEXT_VARIATION_URI ||
7054            variation == InputType.TYPE_TEXT_VARIATION_PASSWORD ||
7055            variation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD ||
7056            variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
7057            variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
7058            return -1;
7059        }
7060
7061        int len = mText.length();
7062        int end = Math.min(offset, len);
7063
7064        if (end < 0) {
7065            return -1;
7066        }
7067
7068        int start = end;
7069
7070        for (; start > 0; start--) {
7071            char c = mTransformed.charAt(start - 1);
7072            int type = Character.getType(c);
7073
7074            if (c != '\'' &&
7075                type != Character.UPPERCASE_LETTER &&
7076                type != Character.LOWERCASE_LETTER &&
7077                type != Character.TITLECASE_LETTER &&
7078                type != Character.MODIFIER_LETTER &&
7079                type != Character.DECIMAL_DIGIT_NUMBER) {
7080                break;
7081            }
7082        }
7083
7084        for (; end < len; end++) {
7085            char c = mTransformed.charAt(end);
7086            int type = Character.getType(c);
7087
7088            if (c != '\'' &&
7089                type != Character.UPPERCASE_LETTER &&
7090                type != Character.LOWERCASE_LETTER &&
7091                type != Character.TITLECASE_LETTER &&
7092                type != Character.MODIFIER_LETTER &&
7093                type != Character.DECIMAL_DIGIT_NUMBER) {
7094                break;
7095            }
7096        }
7097
7098        if (start == end) {
7099            return -1;
7100        }
7101
7102        if (end - start > 48) {
7103            return -1;
7104        }
7105
7106        boolean hasLetter = false;
7107        for (int i = start; i < end; i++) {
7108            if (Character.isLetter(mTransformed.charAt(i))) {
7109                hasLetter = true;
7110                break;
7111            }
7112        }
7113
7114        if (!hasLetter) {
7115            return -1;
7116        }
7117
7118        // Two ints packed in a long
7119        return (((long) start) << 32) | end;
7120    }
7121
7122    private void selectCurrentWord() {
7123        // In case selection mode is started after an orientation change or after a select all,
7124        // use the current selection instead of creating one
7125        if (hasSelection()) {
7126            return;
7127        }
7128
7129        int selectionStart, selectionEnd;
7130
7131        // selectionModifierCursorController is not null at that point
7132        SelectionModifierCursorController selectionModifierCursorController =
7133            ((SelectionModifierCursorController) mSelectionModifierCursorController);
7134        int minOffset = selectionModifierCursorController.getMinTouchOffset();
7135        int maxOffset = selectionModifierCursorController.getMaxTouchOffset();
7136
7137        long wordLimits = getWordLimitsAt(minOffset);
7138        if (wordLimits >= 0) {
7139            selectionStart = (int) (wordLimits >>> 32);
7140        } else {
7141            selectionStart = Math.max(minOffset - 5, 0);
7142        }
7143
7144        wordLimits = getWordLimitsAt(maxOffset);
7145        if (wordLimits >= 0) {
7146            selectionEnd = (int) (wordLimits & 0x00000000FFFFFFFFL);
7147        } else {
7148            selectionEnd = Math.min(maxOffset + 5, mText.length());
7149        }
7150
7151        Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
7152    }
7153
7154    private String getWordForDictionary() {
7155        if (mSelectionModifierCursorController == null) {
7156            return null;
7157        }
7158
7159        int offset = ((SelectionModifierCursorController) mSelectionModifierCursorController).
7160                     getMinTouchOffset();
7161
7162        long wordLimits = getWordLimitsAt(offset);
7163        if (wordLimits >= 0) {
7164            int start = (int) (wordLimits >>> 32);
7165            int end = (int) (wordLimits & 0x00000000FFFFFFFFL);
7166            return mTransformed.subSequence(start, end).toString();
7167        } else {
7168            return null;
7169        }
7170
7171    }
7172
7173    @Override
7174    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
7175        if (!isShown()) {
7176            return false;
7177        }
7178
7179        final boolean isPassword = isPasswordInputType(mInputType);
7180
7181        if (!isPassword) {
7182            CharSequence text = getText();
7183            if (TextUtils.isEmpty(text)) {
7184                text = getHint();
7185            }
7186            if (!TextUtils.isEmpty(text)) {
7187                if (text.length() > AccessibilityEvent.MAX_TEXT_LENGTH) {
7188                    text = text.subSequence(0, AccessibilityEvent.MAX_TEXT_LENGTH + 1);
7189                }
7190                event.getText().add(text);
7191            }
7192        } else {
7193            event.setPassword(isPassword);
7194        }
7195        return false;
7196    }
7197
7198    void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText,
7199            int fromIndex, int removedCount, int addedCount) {
7200        AccessibilityEvent event =
7201            AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
7202        event.setFromIndex(fromIndex);
7203        event.setRemovedCount(removedCount);
7204        event.setAddedCount(addedCount);
7205        event.setBeforeText(beforeText);
7206        sendAccessibilityEventUnchecked(event);
7207    }
7208
7209    @Override
7210    protected void onCreateContextMenu(ContextMenu menu) {
7211        super.onCreateContextMenu(menu);
7212        boolean added = false;
7213
7214        if (mIsInTextSelectionMode) {
7215            MenuHandler handler = new MenuHandler();
7216
7217            if (canCut()) {
7218                menu.add(0, ID_CUT, 0, com.android.internal.R.string.cut).
7219                     setOnMenuItemClickListener(handler).
7220                     setAlphabeticShortcut('x');
7221            }
7222
7223            if (canCopy()) {
7224                menu.add(0, ID_COPY, 0, com.android.internal.R.string.copy).
7225                     setOnMenuItemClickListener(handler).
7226                     setAlphabeticShortcut('c');
7227            }
7228
7229            if (canPaste()) {
7230                menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste).
7231                     setOnMenuItemClickListener(handler).
7232                     setAlphabeticShortcut('v');
7233            }
7234
7235            added = true;
7236        } else {
7237            /*
7238            if (!isFocused()) {
7239                if (isFocusable() && mInput != null) {
7240                    if (canCopy()) {
7241                        MenuHandler handler = new MenuHandler();
7242                        menu.add(0, ID_COPY, 0, com.android.internal.R.string.copy).
7243                             setOnMenuItemClickListener(handler).
7244                             setAlphabeticShortcut('c');
7245                        menu.setHeaderTitle(com.android.internal.R.string.editTextMenuTitle);
7246                    }
7247                }
7248
7249                //return;
7250            }
7251             */
7252            MenuHandler handler = new MenuHandler();
7253
7254            if (canSelectText()) {
7255                menu.add(0, ID_START_SELECTING_TEXT, 0, com.android.internal.R.string.selectText).
7256                setOnMenuItemClickListener(handler);
7257                added = true;
7258            }
7259
7260            if (canSelectAll()) {
7261                menu.add(0, ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll).
7262                     setOnMenuItemClickListener(handler).
7263                     setAlphabeticShortcut('a');
7264                added = true;
7265            }
7266
7267            if (mText instanceof Spanned) {
7268                int selStart = getSelectionStart();
7269                int selEnd = getSelectionEnd();
7270
7271                int min = Math.min(selStart, selEnd);
7272                int max = Math.max(selStart, selEnd);
7273
7274                URLSpan[] urls = ((Spanned) mText).getSpans(min, max,
7275                        URLSpan.class);
7276                if (urls.length == 1) {
7277                    menu.add(0, ID_COPY_URL, 0, com.android.internal.R.string.copyUrl).
7278                         setOnMenuItemClickListener(handler);
7279                    added = true;
7280                }
7281            }
7282
7283            // Paste location is too imprecise. Only allow on empty text fields.
7284            if (canPaste() && textIsOnlySpaces()) {
7285                menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste).
7286                     setOnMenuItemClickListener(handler).
7287                     setAlphabeticShortcut('v');
7288                added = true;
7289            }
7290
7291            if (isInputMethodTarget()) {
7292                menu.add(1, ID_SWITCH_INPUT_METHOD, 0, com.android.internal.R.string.inputMethod).
7293                     setOnMenuItemClickListener(handler);
7294                added = true;
7295            }
7296
7297            String word = getWordForDictionary();
7298            if (word != null) {
7299                menu.add(1, ID_ADD_TO_DICTIONARY, 0,
7300                     getContext().getString(com.android.internal.R.string.addToDictionary, word)).
7301                     setOnMenuItemClickListener(handler);
7302                added = true;
7303
7304            }
7305        }
7306
7307        if (added) {
7308            menu.setHeaderTitle(com.android.internal.R.string.editTextMenuTitle);
7309        }
7310    }
7311
7312    private boolean textIsOnlySpaces() {
7313        final int length = mTransformed.length();
7314        for (int i=0; i<length; i++) {
7315            final char c = mTransformed.charAt(i);
7316            final int type = Character.getType(c);
7317            if (type != Character.SPACE_SEPARATOR)
7318                return false;
7319        }
7320        return true;
7321    }
7322
7323    /**
7324     * Returns whether this text view is a current input method target.  The
7325     * default implementation just checks with {@link InputMethodManager}.
7326     */
7327    public boolean isInputMethodTarget() {
7328        InputMethodManager imm = InputMethodManager.peekInstance();
7329        return imm != null && imm.isActive(this);
7330    }
7331
7332    // Context menu entries
7333    private static final int ID_SELECT_ALL = android.R.id.selectAll;
7334    private static final int ID_START_SELECTING_TEXT = android.R.id.startSelectingText;
7335    private static final int ID_CUT = android.R.id.cut;
7336    private static final int ID_COPY = android.R.id.copy;
7337    private static final int ID_PASTE = android.R.id.paste;
7338    private static final int ID_COPY_URL = android.R.id.copyUrl;
7339    private static final int ID_SWITCH_INPUT_METHOD = android.R.id.switchInputMethod;
7340    private static final int ID_ADD_TO_DICTIONARY = android.R.id.addToDictionary;
7341
7342    private class MenuHandler implements MenuItem.OnMenuItemClickListener {
7343        public boolean onMenuItemClick(MenuItem item) {
7344            return onTextContextMenuItem(item.getItemId());
7345        }
7346    }
7347
7348    /**
7349     * Called when a context menu option for the text view is selected.  Currently
7350     * this will be one of: {@link android.R.id#selectAll},
7351     * {@link android.R.id#startSelectingText}, {@link android.R.id#stopSelectingText},
7352     * {@link android.R.id#cut}, {@link android.R.id#copy},
7353     * {@link android.R.id#paste}, {@link android.R.id#copyUrl},
7354     * or {@link android.R.id#switchInputMethod}.
7355     */
7356    public boolean onTextContextMenuItem(int id) {
7357        int min = 0;
7358        int max = mText.length();
7359
7360        if (isFocused()) {
7361            final int selStart = getSelectionStart();
7362            final int selEnd = getSelectionEnd();
7363
7364            min = Math.max(0, Math.min(selStart, selEnd));
7365            max = Math.max(0, Math.max(selStart, selEnd));
7366        }
7367
7368        ClipboardManager clip = (ClipboardManager)getContext()
7369                .getSystemService(Context.CLIPBOARD_SERVICE);
7370
7371        switch (id) {
7372            case ID_SELECT_ALL:
7373                Selection.setSelection((Spannable) mText, 0, mText.length());
7374                startTextSelectionMode();
7375                return true;
7376
7377            case ID_START_SELECTING_TEXT:
7378                startTextSelectionMode();
7379                return true;
7380
7381            case ID_CUT:
7382                clip.setText(mTransformed.subSequence(min, max));
7383                ((Editable) mText).delete(min, max);
7384                stopTextSelectionMode();
7385                return true;
7386
7387            case ID_COPY:
7388                clip.setText(mTransformed.subSequence(min, max));
7389                stopTextSelectionMode();
7390                return true;
7391
7392            case ID_PASTE:
7393                CharSequence paste = clip.getText();
7394
7395                if (paste != null) {
7396                    Selection.setSelection((Spannable) mText, max);
7397                    ((Editable) mText).replace(min, max, paste);
7398                    stopTextSelectionMode();
7399                }
7400                return true;
7401
7402            case ID_COPY_URL:
7403                URLSpan[] urls = ((Spanned) mText).getSpans(min, max, URLSpan.class);
7404                if (urls.length == 1) {
7405                    clip.setText(urls[0].getURL());
7406                }
7407                return true;
7408
7409            case ID_SWITCH_INPUT_METHOD:
7410                InputMethodManager imm = InputMethodManager.peekInstance();
7411                if (imm != null) {
7412                    imm.showInputMethodPicker();
7413                }
7414                return true;
7415
7416            case ID_ADD_TO_DICTIONARY:
7417                String word = getWordForDictionary();
7418
7419                if (word != null) {
7420                    Intent i = new Intent("com.android.settings.USER_DICTIONARY_INSERT");
7421                    i.putExtra("word", word);
7422                    i.setFlags(i.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
7423                    getContext().startActivity(i);
7424                }
7425                return true;
7426            }
7427
7428        return false;
7429    }
7430
7431    @Override
7432    public boolean performLongClick() {
7433        if (super.performLongClick()) {
7434            mEatTouchRelease = true;
7435            return true;
7436        }
7437
7438        return false;
7439    }
7440
7441    private void startTextSelectionMode() {
7442        if (mSelectionModifierCursorController == null) {
7443            Log.w(LOG_TAG, "TextView has no selection controller. Action mode cancelled.");
7444            return;
7445        }
7446
7447        if (!requestFocus()) {
7448            return;
7449        }
7450
7451        selectCurrentWord();
7452        mSelectionModifierCursorController.show();
7453        mIsInTextSelectionMode = true;
7454    }
7455
7456    /**
7457     * Same as {@link #stopTextSelectionMode()}, except that there is no cursor controller
7458     * fade out animation. Needed since the drawable and their alpha values are shared by all
7459     * TextViews. Switching from one TextView to another would fade the cursor controllers in the
7460     * new one otherwise.
7461     */
7462    private void terminateTextSelectionMode() {
7463        stopTextSelectionMode();
7464        if (mSelectionModifierCursorController != null) {
7465            SelectionModifierCursorController selectionModifierCursorController =
7466                (SelectionModifierCursorController) mSelectionModifierCursorController;
7467            selectionModifierCursorController.cancelFadeOutAnimation();
7468        }
7469    }
7470
7471    private void stopTextSelectionMode() {
7472        if (mIsInTextSelectionMode) {
7473            Selection.setSelection((Spannable) mText, getSelectionEnd());
7474            if (mSelectionModifierCursorController != null) {
7475                mSelectionModifierCursorController.hide();
7476            }
7477
7478            mIsInTextSelectionMode = false;
7479        }
7480    }
7481
7482    /**
7483     * A CursorController instance can be used to control a cursor in the text.
7484     *
7485     * It can be passed to an {@link ArrowKeyMovementMethod} which can intercepts events
7486     * and send them to this object instead of the cursor.
7487     */
7488    public interface CursorController {
7489        /* Cursor fade-out animation duration, in milliseconds. */
7490        static final int FADE_OUT_DURATION = 400;
7491
7492        /**
7493         * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}.
7494         * See also {@link #hide()}.
7495         */
7496        public void show();
7497
7498        /**
7499         * Hide the cursor controller from screen.
7500         * See also {@link #show()}.
7501         */
7502        public void hide();
7503
7504        /**
7505         * Update the controller's position.
7506         */
7507        public void updatePosition(int x, int y);
7508
7509        /**
7510         * The controller and the cursor's positions can be link by a fixed offset,
7511         * computed when the controller is touched, and then maintained as it moves
7512         * @return Horizontal offset between the controller and the cursor.
7513         */
7514        public float getOffsetX();
7515
7516        /**
7517         * @return Vertical offset between the controller and the cursor.
7518         */
7519        public float getOffsetY();
7520
7521        /**
7522         * This method is called by {@link #onTouchEvent(MotionEvent)} and gives the controller
7523         * a chance to become active and/or visible.
7524         * @param event The touch event
7525         */
7526        public void onTouchEvent(MotionEvent event);
7527
7528        /**
7529         * Draws a visual representation of the controller on the canvas.
7530         *
7531         * Called at the end of {@link #draw(Canvas)}, in the content coordinates system.
7532         * @param canvas The Canvas used by this TextView.
7533         */
7534        public void draw(Canvas canvas);
7535    }
7536
7537    private class Handle {
7538        Drawable mDrawable;
7539        // Vertical extension of the touch region
7540        int mTopExtension, mBottomExtension;
7541        // Position of the virtual finger position on screen
7542        int mHotSpotVerticalPosition;
7543
7544        Handle(Drawable drawable) {
7545            mDrawable = drawable;
7546        }
7547
7548        void positionAtCursor(final int offset, boolean bottom) {
7549            final int drawableWidth = mDrawable.getIntrinsicWidth();
7550            final int drawableHeight = mDrawable.getIntrinsicHeight();
7551            final int line = mLayout.getLineForOffset(offset);
7552            final int lineTop = mLayout.getLineTop(line);
7553            final int lineBottom = mLayout.getLineBottom(line);
7554
7555            mHotSpotVerticalPosition = lineTop;
7556
7557            final Rect bounds = sCursorControllerTempRect;
7558            bounds.left = (int) (mLayout.getPrimaryHorizontal(offset) - drawableWidth / 2.0);
7559            bounds.top = (bottom ? lineBottom : lineTop) - drawableHeight / 2;
7560
7561            mTopExtension = bottom ? 0 : drawableHeight / 2;
7562            mBottomExtension = drawableHeight;
7563
7564            // Extend touch region up when editing the last line of text (or a single line) so that
7565            // it is easier to grab.
7566            if (line == mLayout.getLineCount() - 1) {
7567                mTopExtension = (lineBottom - lineTop) - drawableHeight / 2;
7568            }
7569
7570            bounds.right = bounds.left + drawableWidth;
7571            bounds.bottom = bounds.top + drawableHeight;
7572
7573            int boundTopBefore = bounds.top;
7574            convertFromViewportToContentCoordinates(bounds);
7575            mHotSpotVerticalPosition += bounds.top - boundTopBefore;
7576            mDrawable.setBounds(bounds);
7577            postInvalidate();
7578        }
7579
7580        boolean hasFingerOn(float x, float y) {
7581            // Simulate a 'fat finger' to ease grabbing of the controller.
7582            // Expands according to controller image size instead of using dip distance.
7583            // Assumes controller imager has a sensible size, proportionnal to screen density.
7584            final int drawableWidth = mDrawable.getIntrinsicWidth();
7585            final Rect fingerRect = sCursorControllerTempRect;
7586            fingerRect.set((int) (x - drawableWidth / 2.0),
7587                           (int) (y - mBottomExtension),
7588                           (int) (x + drawableWidth / 2.0),
7589                           (int) (y + mTopExtension));
7590            return Rect.intersects(mDrawable.getBounds(), fingerRect);
7591        }
7592
7593        void postInvalidate() {
7594            final Rect bounds = mDrawable.getBounds();
7595            TextView.this.postInvalidate(bounds.left, bounds.top, bounds.right, bounds.bottom);
7596        }
7597
7598        void postInvalidateDelayed(long delay) {
7599            final Rect bounds = mDrawable.getBounds();
7600            TextView.this.postInvalidateDelayed(delay, bounds.left, bounds.top,
7601                                                       bounds.right, bounds.bottom);
7602        }
7603    }
7604
7605    class InsertionPointCursorController implements CursorController {
7606        private static final int DELAY_BEFORE_FADE_OUT = 2100;
7607
7608        // Whether or not the cursor control is currently visible
7609        private boolean mIsVisible = false;
7610        // Starting time of the fade timer
7611        private long mFadeOutTimerStart;
7612        // The cursor controller image
7613        private final Handle mHandle;
7614        // Used to detect a tap (vs drag) on the controller
7615        private long mOnDownTimerStart;
7616        // Offset between finger hot point on cursor controller and actual cursor
7617        private float mOffsetX, mOffsetY;
7618
7619        InsertionPointCursorController() {
7620            Resources res = mContext.getResources();
7621            mHandle = new Handle(res.getDrawable(com.android.internal.R.drawable.text_select_handle));
7622        }
7623
7624        public void show() {
7625            updateDrawablePosition();
7626            // Has to be done after updateDrawablePosition, so that previous position invalidate
7627            // in only done if necessary.
7628            mIsVisible = true;
7629        }
7630
7631        public void hide() {
7632            if (mIsVisible) {
7633                long time = System.currentTimeMillis();
7634                // Start fading out, only if not already in progress
7635                if (time - mFadeOutTimerStart < DELAY_BEFORE_FADE_OUT) {
7636                    mFadeOutTimerStart = time - DELAY_BEFORE_FADE_OUT;
7637                    mHandle.postInvalidate();
7638                }
7639            }
7640        }
7641
7642        public void draw(Canvas canvas) {
7643            if (mIsVisible) {
7644                int time = (int) (System.currentTimeMillis() - mFadeOutTimerStart);
7645                if (time <= DELAY_BEFORE_FADE_OUT) {
7646                    mHandle.postInvalidateDelayed(DELAY_BEFORE_FADE_OUT - time);
7647                } else {
7648                    time -= DELAY_BEFORE_FADE_OUT;
7649                    if (time <= FADE_OUT_DURATION) {
7650                        final int alpha = (int)
7651                                         ((255.0 * (FADE_OUT_DURATION - time)) / FADE_OUT_DURATION);
7652                        mHandle.mDrawable.setAlpha(alpha);
7653                        mHandle.postInvalidateDelayed(30);
7654                    } else {
7655                        mHandle.mDrawable.setAlpha(0);
7656                        mIsVisible = false;
7657                    }
7658                }
7659                mHandle.mDrawable.draw(canvas);
7660            }
7661        }
7662
7663        public void updatePosition(int x, int y) {
7664            final int previousOffset = getSelectionStart();
7665            int offset = getHysteresisOffset(x, y, previousOffset);
7666
7667            if (offset != previousOffset) {
7668                Selection.setSelection((Spannable) mText, offset);
7669                updateDrawablePosition();
7670            }
7671        }
7672
7673        private void updateDrawablePosition() {
7674            if (mIsVisible) {
7675                // Clear previous cursor controller before bounds are updated
7676                mHandle.postInvalidate();
7677            }
7678
7679            final int offset = getSelectionStart();
7680
7681            if (offset < 0) {
7682                // Should never happen, safety check.
7683                Log.w(LOG_TAG, "Update cursor controller position called with no cursor");
7684                mIsVisible = false;
7685                return;
7686            }
7687
7688            mHandle.positionAtCursor(offset, true);
7689
7690            mFadeOutTimerStart = System.currentTimeMillis();
7691            mHandle.mDrawable.setAlpha(255);
7692        }
7693
7694        public void onTouchEvent(MotionEvent event) {
7695            if (isFocused() && isTextEditable() && mIsVisible) {
7696                switch (event.getActionMasked()) {
7697                    case MotionEvent.ACTION_DOWN : {
7698                        final float x = event.getX();
7699                        final float y = event.getY();
7700
7701                        if (mHandle.hasFingerOn(x, y)) {
7702                            show();
7703
7704                            if (mMovement instanceof ArrowKeyMovementMethod) {
7705                                ((ArrowKeyMovementMethod)mMovement).setCursorController(this);
7706                            }
7707
7708                            if (mParent != null) {
7709                                // Prevent possible scrollView parent from scrolling, so that
7710                                // we can use auto-scrolling.
7711                                mParent.requestDisallowInterceptTouchEvent(true);
7712                            }
7713
7714                            final Rect bounds = mHandle.mDrawable.getBounds();
7715                            mOffsetX = (bounds.left + bounds.right) / 2.0f - x;
7716                            mOffsetY = mHandle.mHotSpotVerticalPosition - y;
7717
7718                            mOnDownTimerStart = event.getEventTime();
7719                        }
7720                        break;
7721                    }
7722
7723                    case MotionEvent.ACTION_UP : {
7724                        int time = (int) (event.getEventTime() - mOnDownTimerStart);
7725
7726                        if (time <= ViewConfiguration.getTapTimeout()) {
7727                            // A tap on the controller (not a drag) will move the cursor
7728                            int offset = getOffset((int) event.getX(), (int) event.getY());
7729                            Selection.setSelection((Spannable) mText, offset);
7730
7731                            // Modified by cancelLongPress and prevents the cursor from changing
7732                            mScrolled = false;
7733                        }
7734                        break;
7735                    }
7736                }
7737            }
7738        }
7739
7740        public float getOffsetX() {
7741            return mOffsetX;
7742        }
7743
7744        public float getOffsetY() {
7745            return mOffsetY;
7746        }
7747    }
7748
7749    class SelectionModifierCursorController implements CursorController {
7750        // Whether or not the selection controls are currently visible
7751        private boolean mIsVisible = false;
7752        // Whether that start or the end of selection controller is dragged
7753        private boolean mStartIsDragged = false;
7754        // Starting time of the fade timer
7755        private long mFadeOutTimerStart;
7756        // Used to detect a tap (vs drag) on the controller
7757        private long mOnDownTimerStart;
7758        // The cursor controller images
7759        private final Handle mStartHandle, mEndHandle;
7760        // Offset between finger hot point on active cursor controller and actual cursor
7761        private float mOffsetX, mOffsetY;
7762        // The offsets of that last touch down event. Remembered to start selection there.
7763        private int mMinTouchOffset, mMaxTouchOffset;
7764
7765        SelectionModifierCursorController() {
7766            Resources res = mContext.getResources();
7767            mStartHandle = new Handle(res.getDrawable(com.android.internal.R.drawable.text_select_handle));
7768            mEndHandle = new Handle(res.getDrawable(com.android.internal.R.drawable.text_select_handle));
7769        }
7770
7771        public void show() {
7772            updateDrawablesPositions();
7773            // Has to be done after updateDrawablePositions, so that previous position invalidate
7774            // in only done if necessary.
7775            mIsVisible = true;
7776            mFadeOutTimerStart = -1;
7777            hideInsertionPointCursorController();
7778        }
7779
7780        public void hide() {
7781            if (mIsVisible && (mFadeOutTimerStart < 0)) {
7782                mFadeOutTimerStart = System.currentTimeMillis();
7783                mStartHandle.postInvalidate();
7784                mEndHandle.postInvalidate();
7785            }
7786        }
7787
7788        public void cancelFadeOutAnimation() {
7789            mIsVisible = false;
7790            mStartHandle.postInvalidate();
7791            mEndHandle.postInvalidate();
7792        }
7793
7794        public void draw(Canvas canvas) {
7795            if (mIsVisible) {
7796                if (mFadeOutTimerStart >= 0) {
7797                    int time = (int) (System.currentTimeMillis() - mFadeOutTimerStart);
7798                    if (time <= FADE_OUT_DURATION) {
7799                        final int alpha = 255 * (FADE_OUT_DURATION - time) / FADE_OUT_DURATION;
7800                        mStartHandle.mDrawable.setAlpha(alpha);
7801                        mEndHandle.mDrawable.setAlpha(alpha);
7802                        mStartHandle.postInvalidateDelayed(30);
7803                        mEndHandle.postInvalidateDelayed(30);
7804                    } else {
7805                        mStartHandle.mDrawable.setAlpha(0);
7806                        mEndHandle.mDrawable.setAlpha(0);
7807                        mIsVisible = false;
7808                    }
7809                }
7810                mStartHandle.mDrawable.draw(canvas);
7811                mEndHandle.mDrawable.draw(canvas);
7812            }
7813        }
7814
7815        public void updatePosition(int x, int y) {
7816            int selectionStart = getSelectionStart();
7817            int selectionEnd = getSelectionEnd();
7818
7819            final int previousOffset = mStartIsDragged ? selectionStart : selectionEnd;
7820            int offset = getHysteresisOffset(x, y, previousOffset);
7821
7822            // Handle the case where start and end are swapped, making sure start <= end
7823            if (mStartIsDragged) {
7824                if (offset <= selectionEnd) {
7825                    if (selectionStart == offset) {
7826                        return; // no change, no need to redraw;
7827                    }
7828                    selectionStart = offset;
7829                } else {
7830                    selectionStart = selectionEnd;
7831                    selectionEnd = offset;
7832                    mStartIsDragged = false;
7833                }
7834            } else {
7835                if (offset >= selectionStart) {
7836                    if (selectionEnd == offset) {
7837                        return; // no change, no need to redraw;
7838                    }
7839                    selectionEnd = offset;
7840                } else {
7841                    selectionEnd = selectionStart;
7842                    selectionStart = offset;
7843                    mStartIsDragged = true;
7844                }
7845            }
7846
7847            Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
7848            updateDrawablesPositions();
7849        }
7850
7851        private void updateDrawablesPositions() {
7852            if (mIsVisible) {
7853                // Clear previous cursor controller before bounds are updated
7854                mStartHandle.postInvalidate();
7855                mEndHandle.postInvalidate();
7856            }
7857
7858            final int selectionStart = getSelectionStart();
7859            final int selectionEnd = getSelectionEnd();
7860
7861            if ((selectionStart < 0) || (selectionEnd < 0)) {
7862                // Should never happen, safety check.
7863                Log.w(LOG_TAG, "Update selection controller position called with no cursor");
7864                mIsVisible = false;
7865                return;
7866            }
7867
7868            boolean oneLineSelection = mLayout.getLineForOffset(selectionStart) == mLayout.getLineForOffset(selectionEnd);
7869            mStartHandle.positionAtCursor(selectionStart, oneLineSelection);
7870            mEndHandle.positionAtCursor(selectionEnd, true);
7871
7872            mStartHandle.mDrawable.setAlpha(255);
7873            mEndHandle.mDrawable.setAlpha(255);
7874        }
7875
7876        public void onTouchEvent(MotionEvent event) {
7877            if (isTextEditable()) {
7878                switch (event.getActionMasked()) {
7879                    case MotionEvent.ACTION_DOWN:
7880                        final int x = (int) event.getX();
7881                        final int y = (int) event.getY();
7882
7883                        // Remember finger down position, to be able to start selection from there
7884                        mMinTouchOffset = mMaxTouchOffset = getOffset(x, y);
7885
7886                        if (mIsVisible) {
7887                            if (mMovement instanceof ArrowKeyMovementMethod) {
7888                                boolean isOnStart = mStartHandle.hasFingerOn(x, y);
7889                                boolean isOnEnd = mEndHandle.hasFingerOn(x, y);
7890                                if (isOnStart || isOnEnd) {
7891                                    if (mParent != null) {
7892                                        // Prevent possible scrollView parent from scrolling, so
7893                                        // that we can use auto-scrolling.
7894                                        mParent.requestDisallowInterceptTouchEvent(true);
7895                                    }
7896
7897                                    // In case both controllers are under finger (very small
7898                                    // selection region), arbitrarily pick end controller.
7899                                    mStartIsDragged = !isOnEnd;
7900                                    final Handle draggedHandle = mStartIsDragged ? mStartHandle : mEndHandle;
7901                                    final Rect bounds = draggedHandle.mDrawable.getBounds();
7902                                    mOffsetX = (bounds.left + bounds.right) / 2.0f - x;
7903                                    mOffsetY = draggedHandle.mHotSpotVerticalPosition - y;
7904
7905                                    mOnDownTimerStart = event.getEventTime();
7906                                    ((ArrowKeyMovementMethod)mMovement).setCursorController(this);
7907                                }
7908                            }
7909                        }
7910                        break;
7911
7912                    case MotionEvent.ACTION_UP:
7913                        int time = (int) (event.getEventTime() - mOnDownTimerStart);
7914
7915                        if (time <= ViewConfiguration.getTapTimeout()) {
7916                            // A tap on the controller (not a drag) opens the contextual Copy menu
7917                            showContextMenu();
7918                        }
7919                        break;
7920
7921                    case MotionEvent.ACTION_POINTER_DOWN:
7922                    case MotionEvent.ACTION_POINTER_UP:
7923                        // Handle multi-point gestures. Keep min and max offset positions.
7924                        // Only activated for devices that correctly handle multi-touch.
7925                        if (mContext.getPackageManager().hasSystemFeature(
7926                                PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
7927                            updateMinAndMaxOffsets(event);
7928                        }
7929                        break;
7930                }
7931            }
7932        }
7933
7934        /**
7935         * @param event
7936         */
7937        private void updateMinAndMaxOffsets(MotionEvent event) {
7938            int pointerCount = event.getPointerCount();
7939            for (int index = 0; index < pointerCount; index++) {
7940                final int x = (int) event.getX(index);
7941                final int y = (int) event.getY(index);
7942                int offset = getOffset(x, y);
7943                if (offset < mMinTouchOffset) mMinTouchOffset = offset;
7944                if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
7945            }
7946        }
7947
7948        public int getMinTouchOffset() {
7949            return mMinTouchOffset;
7950        }
7951
7952        public int getMaxTouchOffset() {
7953            return mMaxTouchOffset;
7954        }
7955
7956        public float getOffsetX() {
7957            return mOffsetX;
7958        }
7959
7960        public float getOffsetY() {
7961            return mOffsetY;
7962        }
7963
7964        /**
7965         * @return true iff this controller is currently used to move the selection start.
7966         */
7967        public boolean isSelectionStartDragged() {
7968            return mIsVisible && mStartIsDragged;
7969        }
7970    }
7971
7972    private void hideInsertionPointCursorController() {
7973        if (mInsertionPointCursorController != null) {
7974            mInsertionPointCursorController.hide();
7975        }
7976    }
7977
7978    private void hideControllers() {
7979        hideInsertionPointCursorController();
7980        stopTextSelectionMode();
7981    }
7982
7983    private int getOffsetForHorizontal(int line, int x) {
7984        x -= getTotalPaddingLeft();
7985        // Clamp the position to inside of the view.
7986        x = Math.max(0, x);
7987        x = Math.min(getWidth() - getTotalPaddingRight() - 1, x);
7988        x += getScrollX();
7989        return getLayout().getOffsetForHorizontal(line, x);
7990    }
7991
7992    /**
7993     * Get the offset character closest to the specified absolute position.
7994     *
7995     * @param x The horizontal absolute position of a point on screen
7996     * @param y The vertical absolute position of a point on screen
7997     * @return the character offset for the character whose position is closest to the specified
7998     *  position. Returns -1 if there is no layout.
7999     *
8000     * @hide
8001     */
8002    public int getOffset(int x, int y) {
8003        if (getLayout() == null) return -1;
8004
8005        y -= getTotalPaddingTop();
8006        // Clamp the position to inside of the view.
8007        y = Math.max(0, y);
8008        y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y);
8009        y += getScrollY();
8010
8011        final int line = getLayout().getLineForVertical(y);
8012        final int offset = getOffsetForHorizontal(line, x);
8013        return offset;
8014    }
8015
8016    int getHysteresisOffset(int x, int y, int previousOffset) {
8017        final Layout layout = getLayout();
8018        if (layout == null) return -1;
8019
8020        y -= getTotalPaddingTop();
8021        // Clamp the position to inside of the view.
8022        y = Math.max(0, y);
8023        y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y);
8024        y += getScrollY();
8025
8026        int line = getLayout().getLineForVertical(y);
8027
8028        final int previousLine = layout.getLineForOffset(previousOffset);
8029        final int previousLineTop = layout.getLineTop(previousLine);
8030        final int previousLineBottom = layout.getLineBottom(previousLine);
8031        final int hysteresisThreshold = (previousLineBottom - previousLineTop) / 2;
8032
8033        // If new line is just before or after previous line and y position is less than
8034        // hysteresisThreshold away from previous line, keep cursor on previous line.
8035        if (((line == previousLine + 1) && ((y - previousLineBottom) < hysteresisThreshold)) ||
8036            ((line == previousLine - 1) && ((previousLineTop - y)    < hysteresisThreshold))) {
8037            line = previousLine;
8038        }
8039
8040        return getOffsetForHorizontal(line, x);
8041    }
8042
8043    @ViewDebug.ExportedProperty
8044    private CharSequence            mText;
8045    private CharSequence            mTransformed;
8046    private BufferType              mBufferType = BufferType.NORMAL;
8047
8048    private int                     mInputType = EditorInfo.TYPE_NULL;
8049    private CharSequence            mHint;
8050    private Layout                  mHintLayout;
8051
8052    private KeyListener             mInput;
8053
8054    private MovementMethod          mMovement;
8055    private TransformationMethod    mTransformation;
8056    private ChangeWatcher           mChangeWatcher;
8057
8058    private ArrayList<TextWatcher>  mListeners = null;
8059
8060    // display attributes
8061    private final TextPaint         mTextPaint;
8062    private boolean                 mUserSetTextScaleX;
8063    private final Paint             mHighlightPaint;
8064    private int                     mHighlightColor = 0xCC475925;
8065    private Layout                  mLayout;
8066
8067    private long                    mShowCursor;
8068    private Blink                   mBlink;
8069    private boolean                 mCursorVisible = true;
8070
8071    // Cursor Controllers. Null when disabled.
8072    private CursorController        mInsertionPointCursorController;
8073    private CursorController        mSelectionModifierCursorController;
8074    private boolean                 mShouldStartTextSelectionMode = false;
8075    private boolean                 mIsInTextSelectionMode = false;
8076    // Created once and shared by different CursorController helper methods.
8077    // Only one cursor controller is active at any time which prevent race conditions.
8078    private static Rect             sCursorControllerTempRect = new Rect();
8079
8080    private boolean                 mSelectAllOnFocus = false;
8081
8082    private int                     mGravity = Gravity.TOP | Gravity.LEFT;
8083    private boolean                 mHorizontallyScrolling;
8084
8085    private int                     mAutoLinkMask;
8086    private boolean                 mLinksClickable = true;
8087
8088    private float                   mSpacingMult = 1;
8089    private float                   mSpacingAdd = 0;
8090
8091    private static final int        LINES = 1;
8092    private static final int        EMS = LINES;
8093    private static final int        PIXELS = 2;
8094
8095    private int                     mMaximum = Integer.MAX_VALUE;
8096    private int                     mMaxMode = LINES;
8097    private int                     mMinimum = 0;
8098    private int                     mMinMode = LINES;
8099
8100    private int                     mMaxWidth = Integer.MAX_VALUE;
8101    private int                     mMaxWidthMode = PIXELS;
8102    private int                     mMinWidth = 0;
8103    private int                     mMinWidthMode = PIXELS;
8104
8105    private boolean                 mSingleLine;
8106    private int                     mDesiredHeightAtMeasure = -1;
8107    private boolean                 mIncludePad = true;
8108
8109    // tmp primitives, so we don't alloc them on each draw
8110    private Path                    mHighlightPath;
8111    private boolean                 mHighlightPathBogus = true;
8112    private static final RectF      sTempRect = new RectF();
8113
8114    // XXX should be much larger
8115    private static final int        VERY_WIDE = 16384;
8116
8117    private static final int        BLINK = 500;
8118
8119    private static final int ANIMATED_SCROLL_GAP = 250;
8120    private long mLastScroll;
8121    private Scroller mScroller = null;
8122
8123    private BoringLayout.Metrics mBoring;
8124    private BoringLayout.Metrics mHintBoring;
8125
8126    private BoringLayout mSavedLayout, mSavedHintLayout;
8127
8128    private static final InputFilter[] NO_FILTERS = new InputFilter[0];
8129    private InputFilter[] mFilters = NO_FILTERS;
8130    private static final Spanned EMPTY_SPANNED = new SpannedString("");
8131}
8132