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