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