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