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