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