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