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