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