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