TextView.java revision d5ea0a1bc2fae7c6084b0c5cc9b68a925a0f50f6
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        updateSpellCheckSpans(start, start + after);
7613
7614        // Hide the controllers if the amount of content changed
7615        if (before != after) {
7616            // We do not hide the span controllers, as they can be added when a new text is
7617            // inserted into the text view
7618            hideCursorControllers();
7619        }
7620    }
7621
7622    /**
7623     * Not private so it can be called from an inner class without going
7624     * through a thunk.
7625     */
7626    void spanChange(Spanned buf, Object what, int oldStart, int newStart, int oldEnd, int newEnd) {
7627        // XXX Make the start and end move together if this ends up
7628        // spending too much time invalidating.
7629
7630        boolean selChanged = false;
7631        int newSelStart=-1, newSelEnd=-1;
7632
7633        final InputMethodState ims = mInputMethodState;
7634
7635        if (what == Selection.SELECTION_END) {
7636            mHighlightPathBogus = true;
7637            selChanged = true;
7638            newSelEnd = newStart;
7639
7640            if (!isFocused()) {
7641                mSelectionMoved = true;
7642            }
7643
7644            if (oldStart >= 0 || newStart >= 0) {
7645                invalidateCursor(Selection.getSelectionStart(buf), oldStart, newStart);
7646                registerForPreDraw();
7647                makeBlink();
7648            }
7649        }
7650
7651        if (what == Selection.SELECTION_START) {
7652            mHighlightPathBogus = true;
7653            selChanged = true;
7654            newSelStart = newStart;
7655
7656            if (!isFocused()) {
7657                mSelectionMoved = true;
7658            }
7659
7660            if (oldStart >= 0 || newStart >= 0) {
7661                int end = Selection.getSelectionEnd(buf);
7662                invalidateCursor(end, oldStart, newStart);
7663            }
7664        }
7665
7666        if (selChanged) {
7667            if ((buf.getSpanFlags(what)&Spanned.SPAN_INTERMEDIATE) == 0) {
7668                if (newSelStart < 0) {
7669                    newSelStart = Selection.getSelectionStart(buf);
7670                }
7671                if (newSelEnd < 0) {
7672                    newSelEnd = Selection.getSelectionEnd(buf);
7673                }
7674                onSelectionChanged(newSelStart, newSelEnd);
7675            }
7676        }
7677
7678        if (what instanceof UpdateAppearance ||
7679            what instanceof ParagraphStyle) {
7680            if (ims == null || ims.mBatchEditNesting == 0) {
7681                invalidate();
7682                mHighlightPathBogus = true;
7683                checkForResize();
7684            } else {
7685                ims.mContentChanged = true;
7686            }
7687        }
7688
7689        if (MetaKeyKeyListener.isMetaTracker(buf, what)) {
7690            mHighlightPathBogus = true;
7691            if (ims != null && MetaKeyKeyListener.isSelectingMetaTracker(buf, what)) {
7692                ims.mSelectionModeChanged = true;
7693            }
7694
7695            if (Selection.getSelectionStart(buf) >= 0) {
7696                if (ims == null || ims.mBatchEditNesting == 0) {
7697                    invalidateCursor();
7698                } else {
7699                    ims.mCursorChanged = true;
7700                }
7701            }
7702        }
7703
7704        if (what instanceof ParcelableSpan) {
7705            // If this is a span that can be sent to a remote process,
7706            // the current extract editor would be interested in it.
7707            if (ims != null && ims.mExtracting != null) {
7708                if (ims.mBatchEditNesting != 0) {
7709                    if (oldStart >= 0) {
7710                        if (ims.mChangedStart > oldStart) {
7711                            ims.mChangedStart = oldStart;
7712                        }
7713                        if (ims.mChangedStart > oldEnd) {
7714                            ims.mChangedStart = oldEnd;
7715                        }
7716                    }
7717                    if (newStart >= 0) {
7718                        if (ims.mChangedStart > newStart) {
7719                            ims.mChangedStart = newStart;
7720                        }
7721                        if (ims.mChangedStart > newEnd) {
7722                            ims.mChangedStart = newEnd;
7723                        }
7724                    }
7725                } else {
7726                    if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Span change outside of batch: "
7727                            + oldStart + "-" + oldEnd + ","
7728                            + newStart + "-" + newEnd + what);
7729                    ims.mContentChanged = true;
7730                }
7731            }
7732        }
7733
7734        if (newStart < 0 && what instanceof SpellCheckSpan) {
7735            getSpellChecker().removeSpellCheckSpan((SpellCheckSpan) what);
7736        }
7737    }
7738
7739    /**
7740     * Create new SpellCheckSpans on the modified region.
7741     */
7742    private void updateSpellCheckSpans(int start, int end) {
7743        if (!isTextEditable() || !isSuggestionsEnabled() || !getSpellChecker().isSessionActive())
7744            return;
7745        Editable text = (Editable) mText;
7746
7747        final int shift = prepareWordIterator(start, end);
7748        final int shiftedStart = start - shift;
7749        final int shiftedEnd = end - shift;
7750
7751        // Move back to the beginning of the current word, if any
7752        int wordStart = mWordIterator.preceding(shiftedStart);
7753        int wordEnd;
7754        if (wordStart == BreakIterator.DONE) {
7755            wordEnd = mWordIterator.following(shiftedStart);
7756            if (wordEnd != BreakIterator.DONE) {
7757                wordStart = mWordIterator.getBeginning(wordEnd);
7758            }
7759        } else {
7760            wordEnd = mWordIterator.getEnd(wordStart);
7761        }
7762        if (wordEnd == BreakIterator.DONE) {
7763            return;
7764        }
7765
7766        // We need to expand by one character because we want to include the spans that end/start
7767        // at position start/end respectively.
7768        SpellCheckSpan[] spellCheckSpans = text.getSpans(start - 1, end + 1, SpellCheckSpan.class);
7769        SuggestionSpan[] suggestionSpans = text.getSpans(start - 1, end + 1, SuggestionSpan.class);
7770        final int numberOfSpellCheckSpans = spellCheckSpans.length;
7771
7772        // Iterate over the newly added text and schedule new SpellCheckSpans
7773        while (wordStart <= shiftedEnd) {
7774            if (wordEnd >= shiftedStart) {
7775                // A new word has been created across the interval boundaries. Remove previous spans
7776                if (wordStart < shiftedStart && wordEnd > shiftedStart) {
7777                    removeSpansAt(start, spellCheckSpans, text);
7778                    removeSpansAt(start, suggestionSpans, text);
7779                }
7780
7781                if (wordStart < shiftedEnd && wordEnd > shiftedEnd) {
7782                    removeSpansAt(end, spellCheckSpans, text);
7783                    removeSpansAt(end, suggestionSpans, text);
7784                }
7785
7786                // Do not create new boundary spans if they already exist
7787                boolean createSpellCheckSpan = true;
7788                if (wordEnd == shiftedStart) {
7789                    for (int i = 0; i < numberOfSpellCheckSpans; i++) {
7790                        final int spanEnd = text.getSpanEnd(spellCheckSpans[i]);
7791                        if (spanEnd == start) {
7792                            createSpellCheckSpan = false;
7793                            break;
7794                        }
7795                    }
7796                }
7797
7798                if (wordStart == shiftedEnd) {
7799                    for (int i = 0; i < numberOfSpellCheckSpans; i++) {
7800                        final int spanStart = text.getSpanStart(spellCheckSpans[i]);
7801                        if (spanStart == end) {
7802                            createSpellCheckSpan = false;
7803                            break;
7804                        }
7805                    }
7806                }
7807
7808                if (createSpellCheckSpan) {
7809                    mSpellChecker.addSpellCheckSpan(wordStart + shift, wordEnd + shift);
7810                }
7811            }
7812
7813            // iterate word by word
7814            wordEnd = mWordIterator.following(wordEnd);
7815            if (wordEnd == BreakIterator.DONE) break;
7816            wordStart = mWordIterator.getBeginning(wordEnd);
7817            if (wordStart == BreakIterator.DONE) {
7818                Log.e(LOG_TAG, "No word beginning from " + (wordEnd + shift) + "in " + mText);
7819                break;
7820            }
7821        }
7822
7823        mSpellChecker.spellCheck();
7824    }
7825
7826    private static <T> void removeSpansAt(int offset, T[] spans, Editable text) {
7827        final int length = spans.length;
7828        for (int i = 0; i < length; i++) {
7829            final T span = spans[i];
7830            final int start = text.getSpanStart(span);
7831            if (start > offset) continue;
7832            final int end = text.getSpanEnd(span);
7833            if (end < offset) continue;
7834            text.removeSpan(span);
7835        }
7836    }
7837
7838    /**
7839     * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
7840     * pop-up should be displayed.
7841     */
7842    private class EasyEditSpanController {
7843
7844        private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
7845
7846        private EasyEditPopupWindow mPopupWindow;
7847
7848        private EasyEditSpan mEasyEditSpan;
7849
7850        private Runnable mHidePopup;
7851
7852        private void hide() {
7853            if (mPopupWindow != null) {
7854                mPopupWindow.hide();
7855                TextView.this.removeCallbacks(mHidePopup);
7856            }
7857            removeSpans(mText);
7858            mEasyEditSpan = null;
7859        }
7860
7861        /**
7862         * Monitors the changes in the text.
7863         *
7864         * <p>{@link ChangeWatcher#onSpanAdded(Spannable, Object, int, int)} cannot be used,
7865         * as the notifications are not sent when a spannable (with spans) is inserted.
7866         */
7867        public void onTextChange(CharSequence buffer) {
7868            adjustSpans(mText);
7869
7870            if (getWindowVisibility() != View.VISIBLE) {
7871                // The window is not visible yet, ignore the text change.
7872                return;
7873            }
7874
7875            if (mLayout == null) {
7876                // The view has not been layout yet, ignore the text change
7877                return;
7878            }
7879
7880            InputMethodManager imm = InputMethodManager.peekInstance();
7881            if (!(TextView.this instanceof ExtractEditText)
7882                    && imm != null && imm.isFullscreenMode()) {
7883                // The input is in extract mode. We do not have to handle the easy edit in the
7884                // original TextView, as the ExtractEditText will do
7885                return;
7886            }
7887
7888            // Remove the current easy edit span, as the text changed, and remove the pop-up
7889            // (if any)
7890            if (mEasyEditSpan != null) {
7891                if (mText instanceof Spannable) {
7892                    ((Spannable) mText).removeSpan(mEasyEditSpan);
7893                }
7894                mEasyEditSpan = null;
7895            }
7896            if (mPopupWindow != null && mPopupWindow.isShowing()) {
7897                mPopupWindow.hide();
7898            }
7899
7900            // Display the new easy edit span (if any).
7901            if (buffer instanceof Spanned) {
7902                mEasyEditSpan = getSpan((Spanned) buffer);
7903                if (mEasyEditSpan != null) {
7904                    if (mPopupWindow == null) {
7905                        mPopupWindow = new EasyEditPopupWindow();
7906                        mHidePopup = new Runnable() {
7907                            @Override
7908                            public void run() {
7909                                hide();
7910                            }
7911                        };
7912                    }
7913                    mPopupWindow.show(mEasyEditSpan);
7914                    TextView.this.removeCallbacks(mHidePopup);
7915                    TextView.this.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
7916                }
7917            }
7918        }
7919
7920        /**
7921         * Adjusts the spans by removing all of them except the last one.
7922         */
7923        private void adjustSpans(CharSequence buffer) {
7924            // This method enforces that only one easy edit span is attached to the text.
7925            // A better way to enforce this would be to listen for onSpanAdded, but this method
7926            // cannot be used in this scenario as no notification is triggered when a text with
7927            // spans is inserted into a text.
7928            if (buffer instanceof Spannable) {
7929                Spannable spannable = (Spannable) buffer;
7930                EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(),
7931                        EasyEditSpan.class);
7932                for (int i = 0; i < spans.length - 1; i++) {
7933                    spannable.removeSpan(spans[i]);
7934                }
7935            }
7936        }
7937
7938        /**
7939         * Removes all the {@link EasyEditSpan} currently attached.
7940         */
7941        private void removeSpans(CharSequence buffer) {
7942            if (buffer instanceof Spannable) {
7943                Spannable spannable = (Spannable) buffer;
7944                EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(),
7945                        EasyEditSpan.class);
7946                for (int i = 0; i < spans.length; i++) {
7947                    spannable.removeSpan(spans[i]);
7948                }
7949            }
7950        }
7951
7952        private EasyEditSpan getSpan(Spanned spanned) {
7953            EasyEditSpan[] easyEditSpans = spanned.getSpans(0, spanned.length(),
7954                    EasyEditSpan.class);
7955            if (easyEditSpans.length == 0) {
7956                return null;
7957            } else {
7958                return easyEditSpans[0];
7959            }
7960        }
7961    }
7962
7963    /**
7964     * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
7965     * by {@link EasyEditSpanController}.
7966     */
7967    private class EasyEditPopupWindow extends PinnedPopupWindow
7968            implements OnClickListener {
7969        private static final int POPUP_TEXT_LAYOUT =
7970                com.android.internal.R.layout.text_edit_action_popup_text;
7971        private TextView mDeleteTextView;
7972        private EasyEditSpan mEasyEditSpan;
7973
7974        @Override
7975        protected void createPopupWindow() {
7976            mPopupWindow = new PopupWindow(TextView.this.mContext, null,
7977                    com.android.internal.R.attr.textSelectHandleWindowStyle);
7978            mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
7979            mPopupWindow.setClippingEnabled(true);
7980        }
7981
7982        @Override
7983        protected void initContentView() {
7984            LinearLayout linearLayout = new LinearLayout(TextView.this.getContext());
7985            linearLayout.setOrientation(LinearLayout.HORIZONTAL);
7986            mContentView = linearLayout;
7987            mContentView.setBackgroundResource(
7988                    com.android.internal.R.drawable.text_edit_side_paste_window);
7989
7990            LayoutInflater inflater = (LayoutInflater)TextView.this.mContext.
7991                    getSystemService(Context.LAYOUT_INFLATER_SERVICE);
7992
7993            LayoutParams wrapContent = new LayoutParams(
7994                    ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
7995
7996            mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
7997            mDeleteTextView.setLayoutParams(wrapContent);
7998            mDeleteTextView.setText(com.android.internal.R.string.delete);
7999            mDeleteTextView.setOnClickListener(this);
8000            mContentView.addView(mDeleteTextView);
8001        }
8002
8003        public void show(EasyEditSpan easyEditSpan) {
8004            mEasyEditSpan = easyEditSpan;
8005            super.show();
8006        }
8007
8008        @Override
8009        public void onClick(View view) {
8010            if (view == mDeleteTextView) {
8011                deleteText();
8012            }
8013        }
8014
8015        private void deleteText() {
8016            Editable editable = (Editable) mText;
8017            int start = editable.getSpanStart(mEasyEditSpan);
8018            int end = editable.getSpanEnd(mEasyEditSpan);
8019            if (start >= 0 && end >= 0) {
8020                editable.delete(start, end);
8021            }
8022        }
8023
8024        @Override
8025        protected int getTextOffset() {
8026            // Place the pop-up at the end of the span
8027            Editable editable = (Editable) mText;
8028            return editable.getSpanEnd(mEasyEditSpan);
8029        }
8030
8031        @Override
8032        protected int getVerticalLocalPosition(int line) {
8033            return mLayout.getLineBottom(line);
8034        }
8035
8036        @Override
8037        protected int clipVertically(int positionY) {
8038            // As we display the pop-up below the span, no vertical clipping is required.
8039            return positionY;
8040        }
8041    }
8042
8043    private class ChangeWatcher implements TextWatcher, SpanWatcher {
8044
8045        private CharSequence mBeforeText;
8046
8047        private EasyEditSpanController mEasyEditSpanController;
8048
8049        private ChangeWatcher() {
8050            mEasyEditSpanController = new EasyEditSpanController();
8051        }
8052
8053        public void beforeTextChanged(CharSequence buffer, int start,
8054                                      int before, int after) {
8055            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "beforeTextChanged start=" + start
8056                    + " before=" + before + " after=" + after + ": " + buffer);
8057
8058            if (AccessibilityManager.getInstance(mContext).isEnabled()
8059                    && !isPasswordInputType(mInputType)
8060                    && !hasPasswordTransformationMethod()) {
8061                mBeforeText = buffer.toString();
8062            }
8063
8064            TextView.this.sendBeforeTextChanged(buffer, start, before, after);
8065        }
8066
8067        public void onTextChanged(CharSequence buffer, int start,
8068                                  int before, int after) {
8069            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onTextChanged start=" + start
8070                    + " before=" + before + " after=" + after + ": " + buffer);
8071            TextView.this.handleTextChanged(buffer, start, before, after);
8072
8073            mEasyEditSpanController.onTextChange(buffer);
8074
8075            if (AccessibilityManager.getInstance(mContext).isEnabled() &&
8076                    (isFocused() || isSelected() && isShown())) {
8077                sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after);
8078                mBeforeText = null;
8079            }
8080        }
8081
8082        public void afterTextChanged(Editable buffer) {
8083            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "afterTextChanged: " + buffer);
8084            TextView.this.sendAfterTextChanged(buffer);
8085
8086            if (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0) {
8087                MetaKeyKeyListener.stopSelecting(TextView.this, buffer);
8088            }
8089        }
8090
8091        public void onSpanChanged(Spannable buf,
8092                                  Object what, int s, int e, int st, int en) {
8093            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanChanged s=" + s + " e=" + e
8094                    + " st=" + st + " en=" + en + " what=" + what + ": " + buf);
8095            TextView.this.spanChange(buf, what, s, st, e, en);
8096        }
8097
8098        public void onSpanAdded(Spannable buf, Object what, int s, int e) {
8099            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanAdded s=" + s + " e=" + e
8100                    + " what=" + what + ": " + buf);
8101            TextView.this.spanChange(buf, what, -1, s, -1, e);
8102        }
8103
8104        public void onSpanRemoved(Spannable buf, Object what, int s, int e) {
8105            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanRemoved s=" + s + " e=" + e
8106                    + " what=" + what + ": " + buf);
8107            TextView.this.spanChange(buf, what, s, -1, e, -1);
8108        }
8109
8110        private void hideControllers() {
8111            mEasyEditSpanController.hide();
8112        }
8113    }
8114
8115    /**
8116     * @hide
8117     */
8118    @Override
8119    public void dispatchFinishTemporaryDetach() {
8120        mDispatchTemporaryDetach = true;
8121        super.dispatchFinishTemporaryDetach();
8122        mDispatchTemporaryDetach = false;
8123    }
8124
8125    @Override
8126    public void onStartTemporaryDetach() {
8127        super.onStartTemporaryDetach();
8128        // Only track when onStartTemporaryDetach() is called directly,
8129        // usually because this instance is an editable field in a list
8130        if (!mDispatchTemporaryDetach) mTemporaryDetach = true;
8131
8132        // Because of View recycling in ListView, there is no easy way to know when a TextView with
8133        // selection becomes visible again. Until a better solution is found, stop text selection
8134        // mode (if any) as soon as this TextView is recycled.
8135        hideControllers();
8136    }
8137
8138    @Override
8139    public void onFinishTemporaryDetach() {
8140        super.onFinishTemporaryDetach();
8141        // Only track when onStartTemporaryDetach() is called directly,
8142        // usually because this instance is an editable field in a list
8143        if (!mDispatchTemporaryDetach) mTemporaryDetach = false;
8144    }
8145
8146    @Override
8147    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
8148        if (mTemporaryDetach) {
8149            // If we are temporarily in the detach state, then do nothing.
8150            super.onFocusChanged(focused, direction, previouslyFocusedRect);
8151            return;
8152        }
8153
8154        mShowCursor = SystemClock.uptimeMillis();
8155
8156        ensureEndedBatchEdit();
8157
8158        if (focused) {
8159            int selStart = getSelectionStart();
8160            int selEnd = getSelectionEnd();
8161
8162            // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
8163            // mode for these, unless there was a specific selection already started.
8164            final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0 &&
8165                    selEnd == mText.length();
8166            mCreatedWithASelection = mFrozenWithFocus && hasSelection() && !isFocusHighlighted;
8167
8168            if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
8169                // If a tap was used to give focus to that view, move cursor at tap position.
8170                // Has to be done before onTakeFocus, which can be overloaded.
8171                final int lastTapPosition = getLastTapPosition();
8172                if (lastTapPosition >= 0) {
8173                    Selection.setSelection((Spannable) mText, lastTapPosition);
8174                }
8175
8176                if (mMovement != null) {
8177                    mMovement.onTakeFocus(this, (Spannable) mText, direction);
8178                }
8179
8180                // The DecorView does not have focus when the 'Done' ExtractEditText button is
8181                // pressed. Since it is the ViewAncestor's mView, it requests focus before
8182                // ExtractEditText clears focus, which gives focus to the ExtractEditText.
8183                // This special case ensure that we keep current selection in that case.
8184                // It would be better to know why the DecorView does not have focus at that time.
8185                if (((this instanceof ExtractEditText) || mSelectionMoved) &&
8186                        selStart >= 0 && selEnd >= 0) {
8187                    /*
8188                     * Someone intentionally set the selection, so let them
8189                     * do whatever it is that they wanted to do instead of
8190                     * the default on-focus behavior.  We reset the selection
8191                     * here instead of just skipping the onTakeFocus() call
8192                     * because some movement methods do something other than
8193                     * just setting the selection in theirs and we still
8194                     * need to go through that path.
8195                     */
8196                    Selection.setSelection((Spannable) mText, selStart, selEnd);
8197                }
8198
8199                if (mSelectAllOnFocus) {
8200                    selectAll();
8201                }
8202
8203                mTouchFocusSelected = true;
8204            }
8205
8206            mFrozenWithFocus = false;
8207            mSelectionMoved = false;
8208
8209            if (mText instanceof Spannable) {
8210                Spannable sp = (Spannable) mText;
8211                MetaKeyKeyListener.resetMetaState(sp);
8212            }
8213
8214            makeBlink();
8215
8216            if (mError != null) {
8217                showError();
8218            }
8219        } else {
8220            if (mError != null) {
8221                hideError();
8222            }
8223            // Don't leave us in the middle of a batch edit.
8224            onEndBatchEdit();
8225
8226            if (this instanceof ExtractEditText) {
8227                // terminateTextSelectionMode removes selection, which we want to keep when
8228                // ExtractEditText goes out of focus.
8229                final int selStart = getSelectionStart();
8230                final int selEnd = getSelectionEnd();
8231                hideControllers();
8232                Selection.setSelection((Spannable) mText, selStart, selEnd);
8233            } else {
8234                hideControllers();
8235                downgradeEasyCorrectionSpans();
8236            }
8237
8238            // No need to create the controller
8239            if (mSelectionModifierCursorController != null) {
8240                mSelectionModifierCursorController.resetTouchOffsets();
8241            }
8242        }
8243
8244        startStopMarquee(focused);
8245
8246        if (mTransformation != null) {
8247            mTransformation.onFocusChanged(this, mText, focused, direction, previouslyFocusedRect);
8248        }
8249
8250        super.onFocusChanged(focused, direction, previouslyFocusedRect);
8251    }
8252
8253    private int getLastTapPosition() {
8254        // No need to create the controller at that point, no last tap position saved
8255        if (mSelectionModifierCursorController != null) {
8256            int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
8257            if (lastTapPosition >= 0) {
8258                // Safety check, should not be possible.
8259                if (lastTapPosition > mText.length()) {
8260                    Log.e(LOG_TAG, "Invalid tap focus position (" + lastTapPosition + " vs "
8261                            + mText.length() + ")");
8262                    lastTapPosition = mText.length();
8263                }
8264                return lastTapPosition;
8265            }
8266        }
8267
8268        return -1;
8269    }
8270
8271    @Override
8272    public void onWindowFocusChanged(boolean hasWindowFocus) {
8273        super.onWindowFocusChanged(hasWindowFocus);
8274
8275        if (hasWindowFocus) {
8276            if (mBlink != null) {
8277                mBlink.uncancel();
8278                makeBlink();
8279            }
8280        } else {
8281            if (mBlink != null) {
8282                mBlink.cancel();
8283            }
8284            // Don't leave us in the middle of a batch edit.
8285            onEndBatchEdit();
8286            if (mInputContentType != null) {
8287                mInputContentType.enterDown = false;
8288            }
8289
8290            hideControllers();
8291        }
8292
8293        startStopMarquee(hasWindowFocus);
8294    }
8295
8296    @Override
8297    protected void onVisibilityChanged(View changedView, int visibility) {
8298        super.onVisibilityChanged(changedView, visibility);
8299        if (visibility != VISIBLE) {
8300            hideControllers();
8301        }
8302    }
8303
8304    /**
8305     * Use {@link BaseInputConnection#removeComposingSpans
8306     * BaseInputConnection.removeComposingSpans()} to remove any IME composing
8307     * state from this text view.
8308     */
8309    public void clearComposingText() {
8310        if (mText instanceof Spannable) {
8311            BaseInputConnection.removeComposingSpans((Spannable)mText);
8312        }
8313    }
8314
8315    @Override
8316    public void setSelected(boolean selected) {
8317        boolean wasSelected = isSelected();
8318
8319        super.setSelected(selected);
8320
8321        if (selected != wasSelected && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
8322            if (selected) {
8323                startMarquee();
8324            } else {
8325                stopMarquee();
8326            }
8327        }
8328    }
8329
8330    @Override
8331    public boolean onTouchEvent(MotionEvent event) {
8332        final int action = event.getActionMasked();
8333
8334        if (hasSelectionController()) {
8335            getSelectionController().onTouchEvent(event);
8336        }
8337
8338        if (action == MotionEvent.ACTION_DOWN) {
8339            mLastDownPositionX = event.getX();
8340            mLastDownPositionY = event.getY();
8341
8342            // Reset this state; it will be re-set if super.onTouchEvent
8343            // causes focus to move to the view.
8344            mTouchFocusSelected = false;
8345            mIgnoreActionUpEvent = false;
8346        }
8347
8348        final boolean superResult = super.onTouchEvent(event);
8349
8350        /*
8351         * Don't handle the release after a long press, because it will
8352         * move the selection away from whatever the menu action was
8353         * trying to affect.
8354         */
8355        if (mDiscardNextActionUp && action == MotionEvent.ACTION_UP) {
8356            mDiscardNextActionUp = false;
8357            return superResult;
8358        }
8359
8360        final boolean touchIsFinished = (action == MotionEvent.ACTION_UP) &&
8361                !shouldIgnoreActionUpEvent() && isFocused();
8362
8363         if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
8364                && mText instanceof Spannable && mLayout != null) {
8365            boolean handled = false;
8366
8367            if (mMovement != null) {
8368                handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
8369            }
8370
8371            if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && mTextIsSelectable) {
8372                // The LinkMovementMethod which should handle taps on links has not been installed
8373                // on non editable text that support text selection.
8374                // We reproduce its behavior here to open links for these.
8375                ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(),
8376                        getSelectionEnd(), ClickableSpan.class);
8377
8378                if (links.length != 0) {
8379                    links[0].onClick(this);
8380                    handled = true;
8381                }
8382            }
8383
8384            if (touchIsFinished && (isTextEditable() || mTextIsSelectable)) {
8385                // Show the IME, except when selecting in read-only text.
8386                final InputMethodManager imm = InputMethodManager.peekInstance();
8387                viewClicked(imm);
8388                if (!mTextIsSelectable) {
8389                    handled |= imm != null && imm.showSoftInput(this, 0);
8390                }
8391
8392                boolean selectAllGotFocus = mSelectAllOnFocus && didTouchFocusSelect();
8393                hideControllers();
8394                if (!selectAllGotFocus && mText.length() > 0) {
8395                    if (mSpellChecker != null) {
8396                        // When the cursor moves, the word that was typed may need spell check
8397                        mSpellChecker.onSelectionChanged();
8398                    }
8399                    if (isCursorInsideEasyCorrectionSpan()) {
8400                        showSuggestions();
8401                    } else if (hasInsertionController()) {
8402                        getInsertionController().show();
8403                    }
8404                }
8405
8406                handled = true;
8407            }
8408
8409            if (handled) {
8410                return true;
8411            }
8412        }
8413
8414        return superResult;
8415    }
8416
8417    /**
8418     * @return <code>true</code> if the cursor/current selection overlaps a {@link SuggestionSpan}.
8419     */
8420    private boolean isCursorInsideSuggestionSpan() {
8421        if (!(mText instanceof Spannable)) return false;
8422
8423        SuggestionSpan[] suggestionSpans = ((Spannable) mText).getSpans(getSelectionStart(),
8424                getSelectionEnd(), SuggestionSpan.class);
8425        return (suggestionSpans.length > 0);
8426    }
8427
8428    /**
8429     * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
8430     * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
8431     */
8432    private boolean isCursorInsideEasyCorrectionSpan() {
8433        Spannable spannable = (Spannable) mText;
8434        SuggestionSpan[] suggestionSpans = spannable.getSpans(getSelectionStart(),
8435                getSelectionEnd(), SuggestionSpan.class);
8436        for (int i = 0; i < suggestionSpans.length; i++) {
8437            if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
8438                return true;
8439            }
8440        }
8441        return false;
8442    }
8443
8444    /**
8445     * Downgrades to simple suggestions all the easy correction spans that are not a spell check
8446     * span.
8447     */
8448    private void downgradeEasyCorrectionSpans() {
8449        if (mText instanceof Spannable) {
8450            Spannable spannable = (Spannable) mText;
8451            SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
8452                    spannable.length(), SuggestionSpan.class);
8453            for (int i = 0; i < suggestionSpans.length; i++) {
8454                int flags = suggestionSpans[i].getFlags();
8455                if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
8456                        && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
8457                    flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
8458                    suggestionSpans[i].setFlags(flags);
8459                }
8460            }
8461        }
8462    }
8463
8464    /**
8465     * Removes the suggestion spans for misspelled words.
8466     */
8467    private void removeMisspelledSpans() {
8468        if (mText instanceof Spannable) {
8469            Spannable spannable = (Spannable) mText;
8470            SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
8471                    spannable.length(), SuggestionSpan.class);
8472            for (int i = 0; i < suggestionSpans.length; i++) {
8473                int flags = suggestionSpans[i].getFlags();
8474                if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
8475                        && (flags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
8476                    spannable.removeSpan(suggestionSpans[i]);
8477                }
8478            }
8479        }
8480    }
8481
8482    @Override
8483    public boolean onGenericMotionEvent(MotionEvent event) {
8484        if (mMovement != null && mText instanceof Spannable && mLayout != null) {
8485            try {
8486                if (mMovement.onGenericMotionEvent(this, (Spannable) mText, event)) {
8487                    return true;
8488                }
8489            } catch (AbstractMethodError ex) {
8490                // onGenericMotionEvent was added to the MovementMethod interface in API 12.
8491                // Ignore its absence in case third party applications implemented the
8492                // interface directly.
8493            }
8494        }
8495        return super.onGenericMotionEvent(event);
8496    }
8497
8498    private void prepareCursorControllers() {
8499        boolean windowSupportsHandles = false;
8500
8501        ViewGroup.LayoutParams params = getRootView().getLayoutParams();
8502        if (params instanceof WindowManager.LayoutParams) {
8503            WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
8504            windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
8505                    || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
8506        }
8507
8508        mInsertionControllerEnabled = windowSupportsHandles && isCursorVisible() && mLayout != null;
8509        mSelectionControllerEnabled = windowSupportsHandles && textCanBeSelected() &&
8510                mLayout != null;
8511
8512        if (!mInsertionControllerEnabled) {
8513            hideInsertionPointCursorController();
8514            if (mInsertionPointCursorController != null) {
8515                mInsertionPointCursorController.onDetached();
8516                mInsertionPointCursorController = null;
8517            }
8518        }
8519
8520        if (!mSelectionControllerEnabled) {
8521            stopSelectionActionMode();
8522            if (mSelectionModifierCursorController != null) {
8523                mSelectionModifierCursorController.onDetached();
8524                mSelectionModifierCursorController = null;
8525            }
8526        }
8527    }
8528
8529    /**
8530     * @return True iff this TextView contains a text that can be edited, or if this is
8531     * a selectable TextView.
8532     */
8533    private boolean isTextEditable() {
8534        return mText instanceof Editable && onCheckIsTextEditor() && isEnabled();
8535    }
8536
8537    /**
8538     * Returns true, only while processing a touch gesture, if the initial
8539     * touch down event caused focus to move to the text view and as a result
8540     * its selection changed.  Only valid while processing the touch gesture
8541     * of interest.
8542     */
8543    public boolean didTouchFocusSelect() {
8544        return mTouchFocusSelected;
8545    }
8546
8547    @Override
8548    public void cancelLongPress() {
8549        super.cancelLongPress();
8550        mIgnoreActionUpEvent = true;
8551    }
8552
8553    /**
8554     * This method is only valid during a touch event.
8555     *
8556     * @return true when the ACTION_UP event should be ignored, false otherwise.
8557     *
8558     * @hide
8559     */
8560    public boolean shouldIgnoreActionUpEvent() {
8561        return mIgnoreActionUpEvent;
8562    }
8563
8564    @Override
8565    public boolean onTrackballEvent(MotionEvent event) {
8566        if (mMovement != null && mText instanceof Spannable &&
8567            mLayout != null) {
8568            if (mMovement.onTrackballEvent(this, (Spannable) mText, event)) {
8569                return true;
8570            }
8571        }
8572
8573        return super.onTrackballEvent(event);
8574    }
8575
8576    public void setScroller(Scroller s) {
8577        mScroller = s;
8578    }
8579
8580    private static class Blink extends Handler implements Runnable {
8581        private final WeakReference<TextView> mView;
8582        private boolean mCancelled;
8583
8584        public Blink(TextView v) {
8585            mView = new WeakReference<TextView>(v);
8586        }
8587
8588        public void run() {
8589            if (mCancelled) {
8590                return;
8591            }
8592
8593            removeCallbacks(Blink.this);
8594
8595            TextView tv = mView.get();
8596
8597            if (tv != null && tv.shouldBlink()) {
8598                if (tv.mLayout != null) {
8599                    tv.invalidateCursorPath();
8600                }
8601
8602                postAtTime(this, SystemClock.uptimeMillis() + BLINK);
8603            }
8604        }
8605
8606        void cancel() {
8607            if (!mCancelled) {
8608                removeCallbacks(Blink.this);
8609                mCancelled = true;
8610            }
8611        }
8612
8613        void uncancel() {
8614            mCancelled = false;
8615        }
8616    }
8617
8618    /**
8619     * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
8620     */
8621    private boolean shouldBlink() {
8622        if (!isFocused()) return false;
8623
8624        final int start = getSelectionStart();
8625        if (start < 0) return false;
8626
8627        final int end = getSelectionEnd();
8628        if (end < 0) return false;
8629
8630        return start == end;
8631    }
8632
8633    private void makeBlink() {
8634        if (isCursorVisible()) {
8635            if (shouldBlink()) {
8636                mShowCursor = SystemClock.uptimeMillis();
8637                if (mBlink == null) mBlink = new Blink(this);
8638                mBlink.removeCallbacks(mBlink);
8639                mBlink.postAtTime(mBlink, mShowCursor + BLINK);
8640            }
8641        } else {
8642            if (mBlink != null) mBlink.removeCallbacks(mBlink);
8643        }
8644    }
8645
8646    @Override
8647    protected float getLeftFadingEdgeStrength() {
8648        if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return 0.0f;
8649        if (mEllipsize == TextUtils.TruncateAt.MARQUEE &&
8650                mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
8651            if (mMarquee != null && !mMarquee.isStopped()) {
8652                final Marquee marquee = mMarquee;
8653                if (marquee.shouldDrawLeftFade()) {
8654                    return marquee.mScroll / getHorizontalFadingEdgeLength();
8655                } else {
8656                    return 0.0f;
8657                }
8658            } else if (getLineCount() == 1) {
8659                final int layoutDirection = getResolvedLayoutDirection();
8660                final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
8661                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
8662                    case Gravity.LEFT:
8663                        return 0.0f;
8664                    case Gravity.RIGHT:
8665                        return (mLayout.getLineRight(0) - (mRight - mLeft) -
8666                                getCompoundPaddingLeft() - getCompoundPaddingRight() -
8667                                mLayout.getLineLeft(0)) / getHorizontalFadingEdgeLength();
8668                    case Gravity.CENTER_HORIZONTAL:
8669                        return 0.0f;
8670                }
8671            }
8672        }
8673        return super.getLeftFadingEdgeStrength();
8674    }
8675
8676    @Override
8677    protected float getRightFadingEdgeStrength() {
8678        if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return 0.0f;
8679        if (mEllipsize == TextUtils.TruncateAt.MARQUEE &&
8680                mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
8681            if (mMarquee != null && !mMarquee.isStopped()) {
8682                final Marquee marquee = mMarquee;
8683                return (marquee.mMaxFadeScroll - marquee.mScroll) / getHorizontalFadingEdgeLength();
8684            } else if (getLineCount() == 1) {
8685                final int layoutDirection = getResolvedLayoutDirection();
8686                final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
8687                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
8688                    case Gravity.LEFT:
8689                        final int textWidth = (mRight - mLeft) - getCompoundPaddingLeft() -
8690                                getCompoundPaddingRight();
8691                        final float lineWidth = mLayout.getLineWidth(0);
8692                        return (lineWidth - textWidth) / getHorizontalFadingEdgeLength();
8693                    case Gravity.RIGHT:
8694                        return 0.0f;
8695                    case Gravity.CENTER_HORIZONTAL:
8696                    case Gravity.FILL_HORIZONTAL:
8697                        return (mLayout.getLineWidth(0) - ((mRight - mLeft) -
8698                                getCompoundPaddingLeft() - getCompoundPaddingRight())) /
8699                                getHorizontalFadingEdgeLength();
8700                }
8701            }
8702        }
8703        return super.getRightFadingEdgeStrength();
8704    }
8705
8706    @Override
8707    protected int computeHorizontalScrollRange() {
8708        if (mLayout != null) {
8709            return mSingleLine && (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT ?
8710                    (int) mLayout.getLineWidth(0) : mLayout.getWidth();
8711        }
8712
8713        return super.computeHorizontalScrollRange();
8714    }
8715
8716    @Override
8717    protected int computeVerticalScrollRange() {
8718        if (mLayout != null)
8719            return mLayout.getHeight();
8720
8721        return super.computeVerticalScrollRange();
8722    }
8723
8724    @Override
8725    protected int computeVerticalScrollExtent() {
8726        return getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom();
8727    }
8728
8729    @Override
8730    public void findViewsWithText(ArrayList<View> outViews, CharSequence searched, int flags) {
8731        super.findViewsWithText(outViews, searched, flags);
8732        if (!outViews.contains(this) && (flags & FIND_VIEWS_WITH_TEXT) != 0
8733                && !TextUtils.isEmpty(searched) && !TextUtils.isEmpty(mText)) {
8734            String searchedLowerCase = searched.toString().toLowerCase();
8735            String textLowerCase = mText.toString().toLowerCase();
8736            if (textLowerCase.contains(searchedLowerCase)) {
8737                outViews.add(this);
8738            }
8739        }
8740    }
8741
8742    public enum BufferType {
8743        NORMAL, SPANNABLE, EDITABLE,
8744    }
8745
8746    /**
8747     * Returns the TextView_textColor attribute from the
8748     * Resources.StyledAttributes, if set, or the TextAppearance_textColor
8749     * from the TextView_textAppearance attribute, if TextView_textColor
8750     * was not set directly.
8751     */
8752    public static ColorStateList getTextColors(Context context, TypedArray attrs) {
8753        ColorStateList colors;
8754        colors = attrs.getColorStateList(com.android.internal.R.styleable.
8755                                         TextView_textColor);
8756
8757        if (colors == null) {
8758            int ap = attrs.getResourceId(com.android.internal.R.styleable.
8759                                         TextView_textAppearance, -1);
8760            if (ap != -1) {
8761                TypedArray appearance;
8762                appearance = context.obtainStyledAttributes(ap,
8763                                            com.android.internal.R.styleable.TextAppearance);
8764                colors = appearance.getColorStateList(com.android.internal.R.styleable.
8765                                                  TextAppearance_textColor);
8766                appearance.recycle();
8767            }
8768        }
8769
8770        return colors;
8771    }
8772
8773    /**
8774     * Returns the default color from the TextView_textColor attribute
8775     * from the AttributeSet, if set, or the default color from the
8776     * TextAppearance_textColor from the TextView_textAppearance attribute,
8777     * if TextView_textColor was not set directly.
8778     */
8779    public static int getTextColor(Context context,
8780                                   TypedArray attrs,
8781                                   int def) {
8782        ColorStateList colors = getTextColors(context, attrs);
8783
8784        if (colors == null) {
8785            return def;
8786        } else {
8787            return colors.getDefaultColor();
8788        }
8789    }
8790
8791    @Override
8792    public boolean onKeyShortcut(int keyCode, KeyEvent event) {
8793        final int filteredMetaState = event.getMetaState() & ~KeyEvent.META_CTRL_MASK;
8794        if (KeyEvent.metaStateHasNoModifiers(filteredMetaState)) {
8795            switch (keyCode) {
8796            case KeyEvent.KEYCODE_A:
8797                if (canSelectText()) {
8798                    return onTextContextMenuItem(ID_SELECT_ALL);
8799                }
8800                break;
8801            case KeyEvent.KEYCODE_X:
8802                if (canCut()) {
8803                    return onTextContextMenuItem(ID_CUT);
8804                }
8805                break;
8806            case KeyEvent.KEYCODE_C:
8807                if (canCopy()) {
8808                    return onTextContextMenuItem(ID_COPY);
8809                }
8810                break;
8811            case KeyEvent.KEYCODE_V:
8812                if (canPaste()) {
8813                    return onTextContextMenuItem(ID_PASTE);
8814                }
8815                break;
8816            }
8817        }
8818        return super.onKeyShortcut(keyCode, event);
8819    }
8820
8821    /**
8822     * Unlike {@link #textCanBeSelected()}, this method is based on the <i>current</i> state of the
8823     * TextView. {@link #textCanBeSelected()} has to be true (this is one of the conditions to have
8824     * a selection controller (see {@link #prepareCursorControllers()}), but this is not sufficient.
8825     */
8826    private boolean canSelectText() {
8827        return hasSelectionController() && mText.length() != 0;
8828    }
8829
8830    /**
8831     * Test based on the <i>intrinsic</i> charateristics of the TextView.
8832     * The text must be spannable and the movement method must allow for arbitary selection.
8833     *
8834     * See also {@link #canSelectText()}.
8835     */
8836    private boolean textCanBeSelected() {
8837        // prepareCursorController() relies on this method.
8838        // If you change this condition, make sure prepareCursorController is called anywhere
8839        // the value of this condition might be changed.
8840        if (mMovement == null || !mMovement.canSelectArbitrarily()) return false;
8841        return isTextEditable() || (mTextIsSelectable && mText instanceof Spannable && isEnabled());
8842    }
8843
8844    private boolean canCut() {
8845        if (hasPasswordTransformationMethod()) {
8846            return false;
8847        }
8848
8849        if (mText.length() > 0 && hasSelection() && mText instanceof Editable && mInput != null) {
8850            return true;
8851        }
8852
8853        return false;
8854    }
8855
8856    private boolean canCopy() {
8857        if (hasPasswordTransformationMethod()) {
8858            return false;
8859        }
8860
8861        if (mText.length() > 0 && hasSelection()) {
8862            return true;
8863        }
8864
8865        return false;
8866    }
8867
8868    private boolean canPaste() {
8869        return (mText instanceof Editable &&
8870                mInput != null &&
8871                getSelectionStart() >= 0 &&
8872                getSelectionEnd() >= 0 &&
8873                ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)).
8874                hasPrimaryClip());
8875    }
8876
8877    private static long packRangeInLong(int start, int end) {
8878        return (((long) start) << 32) | end;
8879    }
8880
8881    private static int extractRangeStartFromLong(long range) {
8882        return (int) (range >>> 32);
8883    }
8884
8885    private static int extractRangeEndFromLong(long range) {
8886        return (int) (range & 0x00000000FFFFFFFFL);
8887    }
8888
8889    private boolean selectAll() {
8890        final int length = mText.length();
8891        Selection.setSelection((Spannable) mText, 0, length);
8892        return length > 0;
8893    }
8894
8895    /**
8896     * Adjusts selection to the word under last touch offset.
8897     * Return true if the operation was successfully performed.
8898     */
8899    private boolean selectCurrentWord() {
8900        if (!canSelectText()) {
8901            return false;
8902        }
8903
8904        if (hasPasswordTransformationMethod()) {
8905            // Always select all on a password field.
8906            // Cut/copy menu entries are not available for passwords, but being able to select all
8907            // is however useful to delete or paste to replace the entire content.
8908            return selectAll();
8909        }
8910
8911        int klass = mInputType & InputType.TYPE_MASK_CLASS;
8912        int variation = mInputType & InputType.TYPE_MASK_VARIATION;
8913
8914        // Specific text field types: select the entire text for these
8915        if (klass == InputType.TYPE_CLASS_NUMBER ||
8916                klass == InputType.TYPE_CLASS_PHONE ||
8917                klass == InputType.TYPE_CLASS_DATETIME ||
8918                variation == InputType.TYPE_TEXT_VARIATION_URI ||
8919                variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
8920                variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ||
8921                variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
8922            return selectAll();
8923        }
8924
8925        long lastTouchOffsets = getLastTouchOffsets();
8926        final int minOffset = extractRangeStartFromLong(lastTouchOffsets);
8927        final int maxOffset = extractRangeEndFromLong(lastTouchOffsets);
8928
8929        // Safety check in case standard touch event handling has been bypassed
8930        if (minOffset < 0 || minOffset >= mText.length()) return false;
8931        if (maxOffset < 0 || maxOffset >= mText.length()) return false;
8932
8933        int selectionStart, selectionEnd;
8934
8935        // If a URLSpan (web address, email, phone...) is found at that position, select it.
8936        URLSpan[] urlSpans = ((Spanned) mText).getSpans(minOffset, maxOffset, URLSpan.class);
8937        if (urlSpans.length == 1) {
8938            URLSpan url = urlSpans[0];
8939            selectionStart = ((Spanned) mText).getSpanStart(url);
8940            selectionEnd = ((Spanned) mText).getSpanEnd(url);
8941        } else {
8942            final int shift = prepareWordIterator(minOffset, maxOffset);
8943
8944            selectionStart = mWordIterator.getBeginning(minOffset - shift);
8945            if (selectionStart == BreakIterator.DONE) return false;
8946            selectionStart += shift;
8947
8948            selectionEnd = mWordIterator.getEnd(maxOffset - shift);
8949            if (selectionEnd == BreakIterator.DONE) return false;
8950            selectionEnd += shift;
8951        }
8952
8953        Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
8954        return true;
8955    }
8956
8957    int prepareWordIterator(int start, int end) {
8958        if (mWordIterator == null) {
8959            mWordIterator = new WordIterator();
8960        }
8961
8962        final int TEXT_WINDOW_WIDTH = 50; // Should be larger than the longest word's length
8963        final int windowStart = Math.max(0, start - TEXT_WINDOW_WIDTH);
8964        final int windowEnd = Math.min(mText.length(), end + TEXT_WINDOW_WIDTH);
8965        mWordIterator.setCharSequence(mText.subSequence(windowStart, windowEnd));
8966
8967        return windowStart;
8968    }
8969
8970    private SpellChecker getSpellChecker() {
8971        if (mSpellChecker == null) {
8972            mSpellChecker = new SpellChecker(this);
8973        }
8974        return mSpellChecker;
8975    }
8976
8977    private long getLastTouchOffsets() {
8978        int minOffset, maxOffset;
8979
8980        if (mContextMenuTriggeredByKey) {
8981            minOffset = getSelectionStart();
8982            maxOffset = getSelectionEnd();
8983        } else {
8984            SelectionModifierCursorController selectionController = getSelectionController();
8985            minOffset = selectionController.getMinTouchOffset();
8986            maxOffset = selectionController.getMaxTouchOffset();
8987        }
8988
8989        return packRangeInLong(minOffset, maxOffset);
8990    }
8991
8992    @Override
8993    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
8994        super.onPopulateAccessibilityEvent(event);
8995
8996        final boolean isPassword = hasPasswordTransformationMethod();
8997        if (!isPassword) {
8998            CharSequence text = getTextForAccessibility();
8999            if (!TextUtils.isEmpty(text)) {
9000                event.getText().add(text);
9001            }
9002        }
9003    }
9004
9005    @Override
9006    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
9007        super.onInitializeAccessibilityEvent(event);
9008
9009        final boolean isPassword = hasPasswordTransformationMethod();
9010        event.setPassword(isPassword);
9011
9012        if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) {
9013            event.setFromIndex(Selection.getSelectionStart(mText));
9014            event.setToIndex(Selection.getSelectionEnd(mText));
9015            event.setItemCount(mText.length());
9016        }
9017    }
9018
9019    @Override
9020    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
9021        super.onInitializeAccessibilityNodeInfo(info);
9022
9023        final boolean isPassword = hasPasswordTransformationMethod();
9024        if (!isPassword) {
9025            info.setText(getTextForAccessibility());
9026        }
9027        info.setPassword(isPassword);
9028    }
9029
9030    @Override
9031    public void sendAccessibilityEvent(int eventType) {
9032        // Do not send scroll events since first they are not interesting for
9033        // accessibility and second such events a generated too frequently.
9034        // For details see the implementation of bringTextIntoView().
9035        if (eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
9036            return;
9037        }
9038        super.sendAccessibilityEvent(eventType);
9039    }
9040
9041    /**
9042     * Gets the text reported for accessibility purposes. It is the
9043     * text if not empty or the hint.
9044     *
9045     * @return The accessibility text.
9046     */
9047    private CharSequence getTextForAccessibility() {
9048        CharSequence text = getText();
9049        if (TextUtils.isEmpty(text)) {
9050            text = getHint();
9051        }
9052        return text;
9053    }
9054
9055    void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText,
9056            int fromIndex, int removedCount, int addedCount) {
9057        AccessibilityEvent event =
9058            AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
9059        event.setFromIndex(fromIndex);
9060        event.setRemovedCount(removedCount);
9061        event.setAddedCount(addedCount);
9062        event.setBeforeText(beforeText);
9063        sendAccessibilityEventUnchecked(event);
9064    }
9065
9066    @Override
9067    protected void onCreateContextMenu(ContextMenu menu) {
9068        super.onCreateContextMenu(menu);
9069        boolean added = false;
9070        mContextMenuTriggeredByKey = mDPadCenterIsDown || mEnterKeyIsDown;
9071        // Problem with context menu on long press: the menu appears while the key in down and when
9072        // the key is released, the view does not receive the key_up event.
9073        // We need two layers of flags: mDPadCenterIsDown and mEnterKeyIsDown are set in key down/up
9074        // events. We cannot simply clear these flags in onTextContextMenuItem since
9075        // it may not be called (if the user/ discards the context menu with the back key).
9076        // We clear these flags here and mContextMenuTriggeredByKey saves that state so that it is
9077        // available in onTextContextMenuItem.
9078        mDPadCenterIsDown = mEnterKeyIsDown = false;
9079
9080        MenuHandler handler = new MenuHandler();
9081
9082        if (mText instanceof Spanned && hasSelectionController()) {
9083            long lastTouchOffset = getLastTouchOffsets();
9084            final int selStart = extractRangeStartFromLong(lastTouchOffset);
9085            final int selEnd = extractRangeEndFromLong(lastTouchOffset);
9086
9087            URLSpan[] urls = ((Spanned) mText).getSpans(selStart, selEnd, URLSpan.class);
9088            if (urls.length > 0) {
9089                menu.add(0, ID_COPY_URL, 0, com.android.internal.R.string.copyUrl).
9090                        setOnMenuItemClickListener(handler);
9091
9092                added = true;
9093            }
9094        }
9095
9096        // The context menu is not empty, which will prevent the selection mode from starting.
9097        // Add a entry to start it in the context menu.
9098        // TODO Does not handle the case where a subclass does not call super.thisMethod or
9099        // populates the menu AFTER this call.
9100        if (menu.size() > 0) {
9101            menu.add(0, ID_SELECTION_MODE, 0, com.android.internal.R.string.selectTextMode).
9102                    setOnMenuItemClickListener(handler);
9103            added = true;
9104        }
9105
9106        if (added) {
9107            menu.setHeaderTitle(com.android.internal.R.string.editTextMenuTitle);
9108        }
9109    }
9110
9111    /**
9112     * Returns whether this text view is a current input method target.  The
9113     * default implementation just checks with {@link InputMethodManager}.
9114     */
9115    public boolean isInputMethodTarget() {
9116        InputMethodManager imm = InputMethodManager.peekInstance();
9117        return imm != null && imm.isActive(this);
9118    }
9119
9120    // Selection context mode
9121    private static final int ID_SELECT_ALL = android.R.id.selectAll;
9122    private static final int ID_CUT = android.R.id.cut;
9123    private static final int ID_COPY = android.R.id.copy;
9124    private static final int ID_PASTE = android.R.id.paste;
9125    // Context menu entries
9126    private static final int ID_COPY_URL = android.R.id.copyUrl;
9127    private static final int ID_SELECTION_MODE = android.R.id.selectTextMode;
9128
9129    private class MenuHandler implements MenuItem.OnMenuItemClickListener {
9130        public boolean onMenuItemClick(MenuItem item) {
9131            return onTextContextMenuItem(item.getItemId());
9132        }
9133    }
9134
9135    /**
9136     * Called when a context menu option for the text view is selected.  Currently
9137     * this will be {@link android.R.id#copyUrl}, {@link android.R.id#selectTextMode},
9138     * {@link android.R.id#selectAll}, {@link android.R.id#paste}, {@link android.R.id#cut}
9139     * or {@link android.R.id#copy}.
9140     *
9141     * @return true if the context menu item action was performed.
9142     */
9143    public boolean onTextContextMenuItem(int id) {
9144        int min = 0;
9145        int max = mText.length();
9146
9147        if (isFocused()) {
9148            final int selStart = getSelectionStart();
9149            final int selEnd = getSelectionEnd();
9150
9151            min = Math.max(0, Math.min(selStart, selEnd));
9152            max = Math.max(0, Math.max(selStart, selEnd));
9153        }
9154
9155        switch (id) {
9156            case ID_COPY_URL:
9157                URLSpan[] urls = ((Spanned) mText).getSpans(min, max, URLSpan.class);
9158                if (urls.length >= 1) {
9159                    ClipData clip = null;
9160                    for (int i=0; i<urls.length; i++) {
9161                        Uri uri = Uri.parse(urls[0].getURL());
9162                        if (clip == null) {
9163                            clip = ClipData.newRawUri(null, uri);
9164                        } else {
9165                            clip.addItem(new ClipData.Item(uri));
9166                        }
9167                    }
9168                    if (clip != null) {
9169                        setPrimaryClip(clip);
9170                    }
9171                }
9172                stopSelectionActionMode();
9173                return true;
9174
9175            case ID_SELECTION_MODE:
9176                if (mSelectionActionMode != null) {
9177                    // Selection mode is already started, simply change selected part.
9178                    selectCurrentWord();
9179                } else {
9180                    startSelectionActionMode();
9181                }
9182                return true;
9183
9184            case ID_SELECT_ALL:
9185                // This does not enter text selection mode. Text is highlighted, so that it can be
9186                // bulk edited, like selectAllOnFocus does. Returns true even if text is empty.
9187                selectAll();
9188                return true;
9189
9190            case ID_PASTE:
9191                paste(min, max);
9192                return true;
9193
9194            case ID_CUT:
9195                setPrimaryClip(ClipData.newPlainText(null, mTransformed.subSequence(min, max)));
9196                ((Editable) mText).delete(min, max);
9197                stopSelectionActionMode();
9198                return true;
9199
9200            case ID_COPY:
9201                setPrimaryClip(ClipData.newPlainText(null, mTransformed.subSequence(min, max)));
9202                stopSelectionActionMode();
9203                return true;
9204        }
9205        return false;
9206    }
9207
9208    /**
9209     * Prepare text so that there are not zero or two spaces at beginning and end of region defined
9210     * by [min, max] when replacing this region by paste.
9211     * Note that if there were two spaces (or more) at that position before, they are kept. We just
9212     * make sure we do not add an extra one from the paste content.
9213     */
9214    private long prepareSpacesAroundPaste(int min, int max, CharSequence paste) {
9215        if (paste.length() > 0) {
9216            if (min > 0) {
9217                final char charBefore = mTransformed.charAt(min - 1);
9218                final char charAfter = paste.charAt(0);
9219
9220                if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
9221                    // Two spaces at beginning of paste: remove one
9222                    final int originalLength = mText.length();
9223                    ((Editable) mText).delete(min - 1, min);
9224                    // Due to filters, there is no guarantee that exactly one character was
9225                    // removed: count instead.
9226                    final int delta = mText.length() - originalLength;
9227                    min += delta;
9228                    max += delta;
9229                } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
9230                        !Character.isSpaceChar(charAfter) && charAfter != '\n') {
9231                    // No space at beginning of paste: add one
9232                    final int originalLength = mText.length();
9233                    ((Editable) mText).replace(min, min, " ");
9234                    // Taking possible filters into account as above.
9235                    final int delta = mText.length() - originalLength;
9236                    min += delta;
9237                    max += delta;
9238                }
9239            }
9240
9241            if (max < mText.length()) {
9242                final char charBefore = paste.charAt(paste.length() - 1);
9243                final char charAfter = mTransformed.charAt(max);
9244
9245                if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
9246                    // Two spaces at end of paste: remove one
9247                    ((Editable) mText).delete(max, max + 1);
9248                } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
9249                        !Character.isSpaceChar(charAfter) && charAfter != '\n') {
9250                    // No space at end of paste: add one
9251                    ((Editable) mText).replace(max, max, " ");
9252                }
9253            }
9254        }
9255
9256        return packRangeInLong(min, max);
9257    }
9258
9259    private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) {
9260        TextView shadowView = (TextView) inflate(mContext,
9261                com.android.internal.R.layout.text_drag_thumbnail, null);
9262
9263        if (shadowView == null) {
9264            throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
9265        }
9266
9267        if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) {
9268            text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH);
9269        }
9270        shadowView.setText(text);
9271        shadowView.setTextColor(getTextColors());
9272
9273        shadowView.setTextAppearance(mContext, R.styleable.Theme_textAppearanceLarge);
9274        shadowView.setGravity(Gravity.CENTER);
9275
9276        shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
9277                ViewGroup.LayoutParams.WRAP_CONTENT));
9278
9279        final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
9280        shadowView.measure(size, size);
9281
9282        shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
9283        shadowView.invalidate();
9284        return new DragShadowBuilder(shadowView);
9285    }
9286
9287    private static class DragLocalState {
9288        public TextView sourceTextView;
9289        public int start, end;
9290
9291        public DragLocalState(TextView sourceTextView, int start, int end) {
9292            this.sourceTextView = sourceTextView;
9293            this.start = start;
9294            this.end = end;
9295        }
9296    }
9297
9298    @Override
9299    public boolean performLongClick() {
9300        boolean handled = false;
9301        boolean vibrate = true;
9302
9303        if (super.performLongClick()) {
9304            mDiscardNextActionUp = true;
9305            handled = true;
9306        }
9307
9308        // Long press in empty space moves cursor and shows the Paste affordance if available.
9309        if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY) &&
9310                mInsertionControllerEnabled) {
9311            final int offset = getOffsetForPosition(mLastDownPositionX, mLastDownPositionY);
9312            stopSelectionActionMode();
9313            Selection.setSelection((Spannable) mText, offset);
9314            getInsertionController().showWithActionPopup();
9315            handled = true;
9316            vibrate = false;
9317        }
9318
9319        if (!handled && mSelectionActionMode != null) {
9320            if (touchPositionIsInSelection()) {
9321                // Start a drag
9322                final int start = getSelectionStart();
9323                final int end = getSelectionEnd();
9324                CharSequence selectedText = mTransformed.subSequence(start, end);
9325                ClipData data = ClipData.newPlainText(null, selectedText);
9326                DragLocalState localState = new DragLocalState(this, start, end);
9327                startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0);
9328                stopSelectionActionMode();
9329            } else {
9330                getSelectionController().hide();
9331                selectCurrentWord();
9332                getSelectionController().show();
9333            }
9334            handled = true;
9335        }
9336
9337        // Start a new selection
9338        if (!handled) {
9339            handled = startSelectionActionMode();
9340        }
9341
9342        if (vibrate) {
9343            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
9344        }
9345
9346        if (handled) {
9347            mDiscardNextActionUp = true;
9348        }
9349
9350        return handled;
9351    }
9352
9353    private boolean touchPositionIsInSelection() {
9354        int selectionStart = getSelectionStart();
9355        int selectionEnd = getSelectionEnd();
9356
9357        if (selectionStart == selectionEnd) {
9358            return false;
9359        }
9360
9361        if (selectionStart > selectionEnd) {
9362            int tmp = selectionStart;
9363            selectionStart = selectionEnd;
9364            selectionEnd = tmp;
9365            Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
9366        }
9367
9368        SelectionModifierCursorController selectionController = getSelectionController();
9369        int minOffset = selectionController.getMinTouchOffset();
9370        int maxOffset = selectionController.getMaxTouchOffset();
9371
9372        return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
9373    }
9374
9375    private PositionListener getPositionListener() {
9376        if (mPositionListener == null) {
9377            mPositionListener = new PositionListener();
9378        }
9379        return mPositionListener;
9380    }
9381
9382    private interface TextViewPositionListener {
9383        public void updatePosition(int parentPositionX, int parentPositionY,
9384                boolean parentPositionChanged, boolean parentScrolled);
9385    }
9386
9387    private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
9388        // 3 handles
9389        // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
9390        private final int MAXIMUM_NUMBER_OF_LISTENERS = 6;
9391        private TextViewPositionListener[] mPositionListeners =
9392                new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
9393        private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
9394        private boolean mPositionHasChanged = true;
9395        // Absolute position of the TextView with respect to its parent window
9396        private int mPositionX, mPositionY;
9397        private int mNumberOfListeners;
9398        private boolean mScrollHasChanged;
9399
9400        public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
9401            if (mNumberOfListeners == 0) {
9402                updatePosition();
9403                ViewTreeObserver vto = TextView.this.getViewTreeObserver();
9404                vto.addOnPreDrawListener(this);
9405            }
9406
9407            int emptySlotIndex = -1;
9408            for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
9409                TextViewPositionListener listener = mPositionListeners[i];
9410                if (listener == positionListener) {
9411                    return;
9412                } else if (emptySlotIndex < 0 && listener == null) {
9413                    emptySlotIndex = i;
9414                }
9415            }
9416
9417            mPositionListeners[emptySlotIndex] = positionListener;
9418            mCanMove[emptySlotIndex] = canMove;
9419            mNumberOfListeners++;
9420        }
9421
9422        public void removeSubscriber(TextViewPositionListener positionListener) {
9423            for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
9424                if (mPositionListeners[i] == positionListener) {
9425                    mPositionListeners[i] = null;
9426                    mNumberOfListeners--;
9427                    break;
9428                }
9429            }
9430
9431            if (mNumberOfListeners == 0) {
9432                ViewTreeObserver vto = TextView.this.getViewTreeObserver();
9433                vto.removeOnPreDrawListener(this);
9434            }
9435        }
9436
9437        public int getPositionX() {
9438            return mPositionX;
9439        }
9440
9441        public int getPositionY() {
9442            return mPositionY;
9443        }
9444
9445        @Override
9446        public boolean onPreDraw() {
9447            updatePosition();
9448
9449            for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
9450                if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
9451                    TextViewPositionListener positionListener = mPositionListeners[i];
9452                    if (positionListener != null) {
9453                        positionListener.updatePosition(mPositionX, mPositionY,
9454                                mPositionHasChanged, mScrollHasChanged);
9455                    }
9456                }
9457            }
9458
9459            mScrollHasChanged = false;
9460            return true;
9461        }
9462
9463        private void updatePosition() {
9464            TextView.this.getLocationInWindow(mTempCoords);
9465
9466            mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
9467
9468            mPositionX = mTempCoords[0];
9469            mPositionY = mTempCoords[1];
9470        }
9471
9472        public boolean isVisible(int positionX, int positionY) {
9473            final TextView textView = TextView.this;
9474
9475            if (mTempRect == null) mTempRect = new Rect();
9476            final Rect clip = mTempRect;
9477            clip.left = getCompoundPaddingLeft();
9478            clip.top = getExtendedPaddingTop();
9479            clip.right = textView.getWidth() - getCompoundPaddingRight();
9480            clip.bottom = textView.getHeight() - getExtendedPaddingBottom();
9481
9482            final ViewParent parent = textView.getParent();
9483            if (parent == null || !parent.getChildVisibleRect(textView, clip, null)) {
9484                return false;
9485            }
9486
9487            int posX = mPositionX + positionX;
9488            int posY = mPositionY + positionY;
9489
9490            // Offset by 1 to take into account 0.5 and int rounding around getPrimaryHorizontal.
9491            return posX >= clip.left - 1 && posX <= clip.right + 1 &&
9492                    posY >= clip.top && posY <= clip.bottom;
9493        }
9494
9495        public boolean isOffsetVisible(int offset) {
9496            final int line = mLayout.getLineForOffset(offset);
9497            final int lineBottom = mLayout.getLineBottom(line);
9498            final int primaryHorizontal = (int) mLayout.getPrimaryHorizontal(offset);
9499            return isVisible(primaryHorizontal, lineBottom);
9500        }
9501
9502        public void onScrollChanged() {
9503            mScrollHasChanged = true;
9504        }
9505    }
9506
9507    @Override
9508    protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) {
9509        super.onScrollChanged(horiz, vert, oldHoriz, oldVert);
9510        if (mPositionListener != null) {
9511            mPositionListener.onScrollChanged();
9512        }
9513    }
9514
9515    private abstract class PinnedPopupWindow implements TextViewPositionListener {
9516        protected PopupWindow mPopupWindow;
9517        protected ViewGroup mContentView;
9518        int mPositionX, mPositionY;
9519
9520        protected abstract void createPopupWindow();
9521        protected abstract void initContentView();
9522        protected abstract int getTextOffset();
9523        protected abstract int getVerticalLocalPosition(int line);
9524        protected abstract int clipVertically(int positionY);
9525
9526        public PinnedPopupWindow() {
9527            createPopupWindow();
9528
9529            mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
9530            mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
9531            mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
9532
9533            initContentView();
9534
9535            LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
9536                    ViewGroup.LayoutParams.WRAP_CONTENT);
9537            mContentView.setLayoutParams(wrapContent);
9538
9539            mPopupWindow.setContentView(mContentView);
9540        }
9541
9542        public void show() {
9543            TextView.this.getPositionListener().addSubscriber(this, false /* offset is fixed */);
9544
9545            computeLocalPosition();
9546
9547            final PositionListener positionListener = TextView.this.getPositionListener();
9548            updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
9549        }
9550
9551        protected void measureContent() {
9552            final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
9553            mContentView.measure(
9554                    View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
9555                            View.MeasureSpec.AT_MOST),
9556                    View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
9557                            View.MeasureSpec.AT_MOST));
9558        }
9559
9560        /* The popup window will be horizontally centered on the getTextOffset() and vertically
9561         * positioned according to viewportToContentHorizontalOffset.
9562         *
9563         * This method assumes that mContentView has properly been measured from its content. */
9564        private void computeLocalPosition() {
9565            measureContent();
9566            final int width = mContentView.getMeasuredWidth();
9567            final int offset = getTextOffset();
9568            mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - width / 2.0f);
9569            mPositionX += viewportToContentHorizontalOffset();
9570
9571            final int line = mLayout.getLineForOffset(offset);
9572            mPositionY = getVerticalLocalPosition(line);
9573            mPositionY += viewportToContentVerticalOffset();
9574        }
9575
9576        private void updatePosition(int parentPositionX, int parentPositionY) {
9577            int positionX = parentPositionX + mPositionX;
9578            int positionY = parentPositionY + mPositionY;
9579
9580            positionY = clipVertically(positionY);
9581
9582            // Horizontal clipping
9583            final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
9584            final int width = mContentView.getMeasuredWidth();
9585            positionX = Math.min(displayMetrics.widthPixels - width, positionX);
9586            positionX = Math.max(0, positionX);
9587
9588            if (isShowing()) {
9589                mPopupWindow.update(positionX, positionY, -1, -1);
9590            } else {
9591                mPopupWindow.showAtLocation(TextView.this, Gravity.NO_GRAVITY,
9592                        positionX, positionY);
9593            }
9594        }
9595
9596        public void hide() {
9597            mPopupWindow.dismiss();
9598            TextView.this.getPositionListener().removeSubscriber(this);
9599        }
9600
9601        @Override
9602        public void updatePosition(int parentPositionX, int parentPositionY,
9603                boolean parentPositionChanged, boolean parentScrolled) {
9604            // Either parentPositionChanged or parentScrolled is true, check if still visible
9605            if (isShowing() && getPositionListener().isOffsetVisible(getTextOffset())) {
9606                if (parentScrolled) computeLocalPosition();
9607                updatePosition(parentPositionX, parentPositionY);
9608            } else {
9609                hide();
9610            }
9611        }
9612
9613        public boolean isShowing() {
9614            return mPopupWindow.isShowing();
9615        }
9616    }
9617
9618    private class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
9619        private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
9620        private SuggestionInfo[] mSuggestionInfos;
9621        private int mNumberOfSuggestions;
9622        private boolean mCursorWasVisibleBeforeSuggestions;
9623        private SuggestionAdapter mSuggestionsAdapter;
9624        private final Comparator<SuggestionSpan> mSuggestionSpanComparator;
9625        private final HashMap<SuggestionSpan, Integer> mSpansLengths;
9626
9627        private class CustomPopupWindow extends PopupWindow {
9628            public CustomPopupWindow(Context context, int defStyle) {
9629                super(context, null, defStyle);
9630            }
9631
9632            @Override
9633            public void dismiss() {
9634                super.dismiss();
9635
9636                TextView.this.getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
9637
9638                // Safe cast since show() checks that mText is an Editable
9639                ((Spannable) mText).removeSpan(mSuggestionRangeSpan);
9640
9641                setCursorVisible(mCursorWasVisibleBeforeSuggestions);
9642                if (hasInsertionController()) {
9643                    getInsertionController().show();
9644                }
9645            }
9646        }
9647
9648        public SuggestionsPopupWindow() {
9649            mCursorWasVisibleBeforeSuggestions = mCursorVisible;
9650            mSuggestionSpanComparator = new SuggestionSpanComparator();
9651            mSpansLengths = new HashMap<SuggestionSpan, Integer>();
9652        }
9653
9654        @Override
9655        protected void createPopupWindow() {
9656            mPopupWindow = new CustomPopupWindow(TextView.this.mContext,
9657                com.android.internal.R.attr.textSuggestionsWindowStyle);
9658            mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
9659            mPopupWindow.setFocusable(true);
9660            mPopupWindow.setClippingEnabled(false);
9661        }
9662
9663        @Override
9664        protected void initContentView() {
9665            ListView listView = new ListView(TextView.this.getContext());
9666            mSuggestionsAdapter = new SuggestionAdapter();
9667            listView.setAdapter(mSuggestionsAdapter);
9668            listView.setOnItemClickListener(this);
9669            mContentView = listView;
9670
9671            // Inflate the suggestion items once and for all. +1 for add to dictionary
9672            mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS + 1];
9673            for (int i = 0; i < MAX_NUMBER_SUGGESTIONS + 1; i++) {
9674                mSuggestionInfos[i] = new SuggestionInfo();
9675            }
9676        }
9677
9678        private class SuggestionInfo {
9679            int suggestionStart, suggestionEnd; // range of actual suggestion within text
9680            SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents
9681            int suggestionIndex; // the index of this suggestion inside suggestionSpan
9682            SpannableStringBuilder text = new SpannableStringBuilder();
9683            TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mContext,
9684                    android.R.style.TextAppearance_SuggestionHighlight);
9685
9686            void removeMisspelledFlag() {
9687                int suggestionSpanFlags = suggestionSpan.getFlags();
9688                if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) > 0) {
9689                    suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
9690                    suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
9691                    suggestionSpan.setFlags(suggestionSpanFlags);
9692                }
9693            }
9694        }
9695
9696        private class SuggestionAdapter extends BaseAdapter {
9697            private LayoutInflater mInflater = (LayoutInflater) TextView.this.mContext.
9698                    getSystemService(Context.LAYOUT_INFLATER_SERVICE);
9699
9700            @Override
9701            public int getCount() {
9702                return mNumberOfSuggestions;
9703            }
9704
9705            @Override
9706            public Object getItem(int position) {
9707                return mSuggestionInfos[position];
9708            }
9709
9710            @Override
9711            public long getItemId(int position) {
9712                return position;
9713            }
9714
9715            @Override
9716            public View getView(int position, View convertView, ViewGroup parent) {
9717                TextView textView = (TextView) convertView;
9718
9719                if (textView == null) {
9720                    textView = (TextView) mInflater.inflate(mTextEditSuggestionItemLayout, parent,
9721                            false);
9722                }
9723
9724                textView.setText(mSuggestionInfos[position].text);
9725                return textView;
9726            }
9727        }
9728
9729        private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
9730            public int compare(SuggestionSpan span1, SuggestionSpan span2) {
9731                final int flag1 = span1.getFlags();
9732                final int flag2 = span2.getFlags();
9733                if (flag1 != flag2) {
9734                    // The order here should match what is used in updateDrawState
9735                    final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
9736                    final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
9737                    final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
9738                    final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
9739                    if (easy1 && !misspelled1) return -1;
9740                    if (easy2 && !misspelled2) return 1;
9741                    if (misspelled1) return -1;
9742                    if (misspelled2) return 1;
9743                }
9744
9745                return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
9746            }
9747        }
9748
9749        /**
9750         * Returns the suggestion spans that cover the current cursor position. The suggestion
9751         * spans are sorted according to the length of text that they are attached to.
9752         */
9753        private SuggestionSpan[] getSuggestionSpans() {
9754            int pos = TextView.this.getSelectionStart();
9755            Spannable spannable = (Spannable) TextView.this.mText;
9756            SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
9757
9758            mSpansLengths.clear();
9759            for (SuggestionSpan suggestionSpan : suggestionSpans) {
9760                int start = spannable.getSpanStart(suggestionSpan);
9761                int end = spannable.getSpanEnd(suggestionSpan);
9762                mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
9763            }
9764
9765            // The suggestions are sorted according to their types (easy correction first, then
9766            // misspelled) and to the length of the text that they cover (shorter first).
9767            Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
9768            return suggestionSpans;
9769        }
9770
9771        @Override
9772        public void show() {
9773            if (!(mText instanceof Editable)) return;
9774
9775            if (updateSuggestions()) {
9776                mCursorWasVisibleBeforeSuggestions = mCursorVisible;
9777                setCursorVisible(false);
9778                super.show();
9779            }
9780        }
9781
9782        @Override
9783        protected void measureContent() {
9784            final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
9785            final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
9786                    displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
9787            final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
9788                    displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
9789
9790            int width = 0;
9791            View view = null;
9792            for (int i = 0; i < mNumberOfSuggestions; i++) {
9793                view = mSuggestionsAdapter.getView(i, view, mContentView);
9794                view.measure(horizontalMeasure, verticalMeasure);
9795                width = Math.max(width, view.getMeasuredWidth());
9796            }
9797
9798            // Enforce the width based on actual text widths
9799            mContentView.measure(
9800                    View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
9801                    verticalMeasure);
9802
9803            Drawable popupBackground = mPopupWindow.getBackground();
9804            if (popupBackground != null) {
9805                if (mTempRect == null) mTempRect = new Rect();
9806                popupBackground.getPadding(mTempRect);
9807                width += mTempRect.left + mTempRect.right;
9808            }
9809            mPopupWindow.setWidth(width);
9810        }
9811
9812        @Override
9813        protected int getTextOffset() {
9814            return getSelectionStart();
9815        }
9816
9817        @Override
9818        protected int getVerticalLocalPosition(int line) {
9819            return mLayout.getLineBottom(line);
9820        }
9821
9822        @Override
9823        protected int clipVertically(int positionY) {
9824            final int height = mContentView.getMeasuredHeight();
9825            final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
9826            return Math.min(positionY, displayMetrics.heightPixels - height);
9827        }
9828
9829        @Override
9830        public void hide() {
9831            super.hide();
9832        }
9833
9834        private boolean updateSuggestions() {
9835            Spannable spannable = (Spannable) TextView.this.mText;
9836            SuggestionSpan[] suggestionSpans = getSuggestionSpans();
9837
9838            final int nbSpans = suggestionSpans.length;
9839
9840            mNumberOfSuggestions = 0;
9841            int spanUnionStart = mText.length();
9842            int spanUnionEnd = 0;
9843
9844            SuggestionSpan misspelledSpan = null;
9845            int underlineColor = 0;
9846
9847            for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) {
9848                SuggestionSpan suggestionSpan = suggestionSpans[spanIndex];
9849                final int spanStart = spannable.getSpanStart(suggestionSpan);
9850                final int spanEnd = spannable.getSpanEnd(suggestionSpan);
9851                spanUnionStart = Math.min(spanStart, spanUnionStart);
9852                spanUnionEnd = Math.max(spanEnd, spanUnionEnd);
9853
9854                if ((suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
9855                    misspelledSpan = suggestionSpan;
9856                }
9857
9858                // The first span dictates the background color of the highlighted text
9859                if (spanIndex == 0) underlineColor = suggestionSpan.getUnderlineColor();
9860
9861                String[] suggestions = suggestionSpan.getSuggestions();
9862                int nbSuggestions = suggestions.length;
9863                for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
9864                    SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
9865                    suggestionInfo.suggestionSpan = suggestionSpan;
9866                    suggestionInfo.suggestionIndex = suggestionIndex;
9867                    suggestionInfo.text.replace(0, suggestionInfo.text.length(),
9868                            suggestions[suggestionIndex]);
9869
9870                    mNumberOfSuggestions++;
9871                    if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) {
9872                        // Also end outer for loop
9873                        spanIndex = nbSpans;
9874                        break;
9875                    }
9876                }
9877            }
9878
9879            for (int i = 0; i < mNumberOfSuggestions; i++) {
9880                highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
9881            }
9882
9883            if (misspelledSpan != null) {
9884                final int misspelledStart = spannable.getSpanStart(misspelledSpan);
9885                final int misspelledEnd = spannable.getSpanEnd(misspelledSpan);
9886                if (misspelledStart >= 0 && misspelledEnd > misspelledStart) {
9887                    SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
9888                    suggestionInfo.suggestionSpan = misspelledSpan;
9889                    suggestionInfo.suggestionIndex = -1;
9890                    suggestionInfo.text.replace(0, suggestionInfo.text.length(),
9891                            getContext().getString(com.android.internal.R.string.addToDictionary));
9892
9893                    mNumberOfSuggestions++;
9894                }
9895            }
9896
9897            if (mNumberOfSuggestions == 0) return false;
9898
9899            if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
9900            if (underlineColor == 0) {
9901                // Fallback on the default highlight color when the first span does not provide one
9902                mSuggestionRangeSpan.setBackgroundColor(mHighlightColor);
9903            } else {
9904                final float BACKGROUND_TRANSPARENCY = 0.3f;
9905                final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
9906                mSuggestionRangeSpan.setBackgroundColor(
9907                        (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
9908            }
9909            spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
9910                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
9911
9912            mSuggestionsAdapter.notifyDataSetChanged();
9913
9914            return true;
9915        }
9916
9917        private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
9918                int unionEnd) {
9919            final Spannable text = (Spannable) mText;
9920            final int spanStart = text.getSpanStart(suggestionInfo.suggestionSpan);
9921            final int spanEnd = text.getSpanEnd(suggestionInfo.suggestionSpan);
9922
9923            // Adjust the start/end of the suggestion span
9924            suggestionInfo.suggestionStart = spanStart - unionStart;
9925            suggestionInfo.suggestionEnd = suggestionInfo.suggestionStart
9926                    + suggestionInfo.text.length();
9927
9928            suggestionInfo.text.clearSpans();
9929            suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0,
9930                    suggestionInfo.text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
9931
9932            // Add the text before and after the span.
9933            suggestionInfo.text.insert(0, mText.subSequence(unionStart, spanStart).toString());
9934            suggestionInfo.text.append(mText.subSequence(spanEnd, unionEnd).toString());
9935        }
9936
9937        @Override
9938        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
9939            hide();
9940
9941            if (view instanceof TextView) {
9942                TextView textView = (TextView) view;
9943                Editable editable = (Editable) mText;
9944
9945                SuggestionInfo suggestionInfo = mSuggestionInfos[position];
9946                final int spanStart = editable.getSpanStart(suggestionInfo.suggestionSpan);
9947                final int spanEnd = editable.getSpanEnd(suggestionInfo.suggestionSpan);
9948                if (spanStart < 0 || spanEnd < 0) return; // Span has been removed
9949                final String originalText = mText.subSequence(spanStart, spanEnd).toString();
9950
9951                if (suggestionInfo.suggestionIndex < 0) {
9952                    Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
9953                    intent.putExtra("word", originalText);
9954                    intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
9955                    getContext().startActivity(intent);
9956                    suggestionInfo.removeMisspelledFlag();
9957                } else {
9958                    // SuggestionSpans are removed by replace: save them before
9959                    SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
9960                            SuggestionSpan.class);
9961                    final int length = suggestionSpans.length;
9962                    int[] suggestionSpansStarts = new int[length];
9963                    int[] suggestionSpansEnds = new int[length];
9964                    int[] suggestionSpansFlags = new int[length];
9965                    for (int i = 0; i < length; i++) {
9966                        final SuggestionSpan suggestionSpan = suggestionSpans[i];
9967                        suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
9968                        suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
9969                        suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
9970                    }
9971
9972                    final int suggestionStart = suggestionInfo.suggestionStart;
9973                    final int suggestionEnd = suggestionInfo.suggestionEnd;
9974                    final String suggestion = textView.getText().subSequence(
9975                            suggestionStart, suggestionEnd).toString();
9976                    editable.replace(spanStart, spanEnd, suggestion);
9977
9978                    suggestionInfo.removeMisspelledFlag();
9979
9980                    // Notify source IME of the suggestion pick. Do this before swaping texts.
9981                    if (!TextUtils.isEmpty(
9982                            suggestionInfo.suggestionSpan.getNotificationTargetClassName())) {
9983                        InputMethodManager imm = InputMethodManager.peekInstance();
9984                        imm.notifySuggestionPicked(suggestionInfo.suggestionSpan, originalText,
9985                                suggestionInfo.suggestionIndex);
9986                    }
9987
9988                    // Swap text content between actual text and Suggestion span
9989                    String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions();
9990                    suggestions[suggestionInfo.suggestionIndex] = originalText;
9991
9992                    // Restore previous SuggestionSpans
9993                    final int lengthDifference = suggestion.length() - (spanEnd - spanStart);
9994                    for (int i = 0; i < length; i++) {
9995                        // Only spans that include the modified region make sense after replacement
9996                        // Spans partially included in the replaced region are removed, there is no
9997                        // way to assign them a valid range after replacement
9998                        if (suggestionSpansStarts[i] <= spanStart &&
9999                                suggestionSpansEnds[i] >= spanEnd) {
10000                            editable.setSpan(suggestionSpans[i], suggestionSpansStarts[i],
10001                                    suggestionSpansEnds[i] + lengthDifference,
10002                                    suggestionSpansFlags[i]);
10003                        }
10004                    }
10005
10006                    // Move cursor at the end of the replacement word
10007                    Selection.setSelection(editable, spanEnd + lengthDifference);
10008                }
10009            }
10010        }
10011    }
10012
10013    /**
10014     * Removes the suggestion spans.
10015     */
10016    CharSequence removeSuggestionSpans(CharSequence text) {
10017       if (text instanceof Spanned) {
10018           Spannable spannable;
10019           if (text instanceof Spannable) {
10020               spannable = (Spannable) text;
10021           } else {
10022               spannable = new SpannableString(text);
10023               text = spannable;
10024           }
10025
10026           SuggestionSpan[] spans = spannable.getSpans(0, text.length(), SuggestionSpan.class);
10027           for (int i = 0; i < spans.length; i++) {
10028               spannable.removeSpan(spans[i]);
10029           }
10030       }
10031       return text;
10032    }
10033
10034    void showSuggestions() {
10035        if (mSuggestionsPopupWindow == null) {
10036            mSuggestionsPopupWindow = new SuggestionsPopupWindow();
10037        }
10038        hideControllers();
10039        mSuggestionsPopupWindow.show();
10040    }
10041
10042    boolean areSuggestionsShown() {
10043        return mSuggestionsPopupWindow != null && mSuggestionsPopupWindow.isShowing();
10044    }
10045
10046    /**
10047     * Return whether or not suggestions are enabled on this TextView. The suggestions are generated
10048     * by the IME or by the spell checker as the user types. This is done by adding
10049     * {@link SuggestionSpan}s to the text.
10050     *
10051     * When suggestions are enabled (default), this list of suggestions will be displayed when the
10052     * user asks for them on these parts of the text. This value depends on the inputType of this
10053     * TextView.
10054     *
10055     * The class of the input type must be {@link InputType#TYPE_CLASS_TEXT}.
10056     *
10057     * In addition, the type variation must be one of
10058     * {@link InputType#TYPE_TEXT_VARIATION_NORMAL},
10059     * {@link InputType#TYPE_TEXT_VARIATION_EMAIL_SUBJECT},
10060     * {@link InputType#TYPE_TEXT_VARIATION_LONG_MESSAGE},
10061     * {@link InputType#TYPE_TEXT_VARIATION_SHORT_MESSAGE} or
10062     * {@link InputType#TYPE_TEXT_VARIATION_WEB_EDIT_TEXT}.
10063     *
10064     * And finally, the {@link InputType#TYPE_TEXT_FLAG_NO_SUGGESTIONS} flag must <i>not</i> be set.
10065     *
10066     * @return true if the suggestions popup window is enabled, based on the inputType.
10067     */
10068    public boolean isSuggestionsEnabled() {
10069        if ((mInputType & InputType.TYPE_MASK_CLASS) != InputType.TYPE_CLASS_TEXT) return false;
10070        if ((mInputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) > 0) return false;
10071
10072        final int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
10073        return (variation == EditorInfo.TYPE_TEXT_VARIATION_NORMAL ||
10074                variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT ||
10075                variation == EditorInfo.TYPE_TEXT_VARIATION_LONG_MESSAGE ||
10076                variation == EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE ||
10077                variation == EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
10078    }
10079
10080    /**
10081     * If provided, this ActionMode.Callback will be used to create the ActionMode when text
10082     * selection is initiated in this View.
10083     *
10084     * The standard implementation populates the menu with a subset of Select All, Cut, Copy and
10085     * Paste actions, depending on what this View supports.
10086     *
10087     * A custom implementation can add new entries in the default menu in its
10088     * {@link android.view.ActionMode.Callback#onPrepareActionMode(ActionMode, Menu)} method. The
10089     * default actions can also be removed from the menu using {@link Menu#removeItem(int)} and
10090     * passing {@link android.R.id#selectAll}, {@link android.R.id#cut}, {@link android.R.id#copy}
10091     * or {@link android.R.id#paste} ids as parameters.
10092     *
10093     * Returning false from
10094     * {@link android.view.ActionMode.Callback#onCreateActionMode(ActionMode, Menu)} will prevent
10095     * the action mode from being started.
10096     *
10097     * Action click events should be handled by the custom implementation of
10098     * {@link android.view.ActionMode.Callback#onActionItemClicked(ActionMode, MenuItem)}.
10099     *
10100     * Note that text selection mode is not started when a TextView receives focus and the
10101     * {@link android.R.attr#selectAllOnFocus} flag has been set. The content is highlighted in
10102     * that case, to allow for quick replacement.
10103     */
10104    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
10105        mCustomSelectionActionModeCallback = actionModeCallback;
10106    }
10107
10108    /**
10109     * Retrieves the value set in {@link #setCustomSelectionActionModeCallback}. Default is null.
10110     *
10111     * @return The current custom selection callback.
10112     */
10113    public ActionMode.Callback getCustomSelectionActionModeCallback() {
10114        return mCustomSelectionActionModeCallback;
10115    }
10116
10117    /**
10118     *
10119     * @return true if the selection mode was actually started.
10120     */
10121    private boolean startSelectionActionMode() {
10122        if (mSelectionActionMode != null) {
10123            // Selection action mode is already started
10124            return false;
10125        }
10126
10127        if (!canSelectText() || !requestFocus()) {
10128            Log.w(LOG_TAG, "TextView does not support text selection. Action mode cancelled.");
10129            return false;
10130        }
10131
10132        if (!hasSelection()) {
10133            // There may already be a selection on device rotation
10134            boolean currentWordSelected = selectCurrentWord();
10135            if (!currentWordSelected) {
10136                // No word found under cursor or text selection not permitted.
10137                return false;
10138            }
10139        }
10140
10141        final InputMethodManager imm = InputMethodManager.peekInstance();
10142        boolean extractedTextModeWillBeStartedFullScreen = !(this instanceof ExtractEditText) &&
10143                imm != null && imm.isFullscreenMode();
10144
10145        // Do not start the action mode when extracted text will show up full screen, thus
10146        // immediately hiding the newly created action bar, which would be visually distracting.
10147        if (!extractedTextModeWillBeStartedFullScreen) {
10148            ActionMode.Callback actionModeCallback = new SelectionActionModeCallback();
10149            mSelectionActionMode = startActionMode(actionModeCallback);
10150        }
10151        final boolean selectionStarted = mSelectionActionMode != null ||
10152                extractedTextModeWillBeStartedFullScreen;
10153
10154        if (selectionStarted && !mTextIsSelectable && imm != null) {
10155            // Show the IME to be able to replace text, except when selecting non editable text.
10156            imm.showSoftInput(this, 0, null);
10157        }
10158
10159        return selectionStarted;
10160    }
10161
10162    private void stopSelectionActionMode() {
10163        if (mSelectionActionMode != null) {
10164            // This will hide the mSelectionModifierCursorController
10165            mSelectionActionMode.finish();
10166        }
10167    }
10168
10169    /**
10170     * Paste clipboard content between min and max positions.
10171     */
10172    private void paste(int min, int max) {
10173        ClipboardManager clipboard =
10174            (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
10175        ClipData clip = clipboard.getPrimaryClip();
10176        if (clip != null) {
10177            boolean didFirst = false;
10178            for (int i=0; i<clip.getItemCount(); i++) {
10179                CharSequence paste = clip.getItemAt(i).coerceToText(getContext());
10180                if (paste != null) {
10181                    if (!didFirst) {
10182                        long minMax = prepareSpacesAroundPaste(min, max, paste);
10183                        min = extractRangeStartFromLong(minMax);
10184                        max = extractRangeEndFromLong(minMax);
10185                        Selection.setSelection((Spannable) mText, max);
10186                        ((Editable) mText).replace(min, max, paste);
10187                        didFirst = true;
10188                    } else {
10189                        ((Editable) mText).insert(getSelectionEnd(), "\n");
10190                        ((Editable) mText).insert(getSelectionEnd(), paste);
10191                    }
10192                }
10193            }
10194            stopSelectionActionMode();
10195            sLastCutOrCopyTime = 0;
10196        }
10197    }
10198
10199    private void setPrimaryClip(ClipData clip) {
10200        ClipboardManager clipboard = (ClipboardManager) getContext().
10201                getSystemService(Context.CLIPBOARD_SERVICE);
10202        clipboard.setPrimaryClip(clip);
10203        sLastCutOrCopyTime = SystemClock.uptimeMillis();
10204    }
10205
10206    /**
10207     * An ActionMode Callback class that is used to provide actions while in text selection mode.
10208     *
10209     * The default callback provides a subset of Select All, Cut, Copy and Paste actions, depending
10210     * on which of these this TextView supports.
10211     */
10212    private class SelectionActionModeCallback implements ActionMode.Callback {
10213
10214        @Override
10215        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
10216            TypedArray styledAttributes = mContext.obtainStyledAttributes(R.styleable.Theme);
10217
10218            boolean allowText = getContext().getResources().getBoolean(
10219                    com.android.internal.R.bool.config_allowActionMenuItemTextWithIcon);
10220
10221            mode.setTitle(allowText ?
10222                    mContext.getString(com.android.internal.R.string.textSelectionCABTitle) : null);
10223            mode.setSubtitle(null);
10224
10225            int selectAllIconId = 0; // No icon by default
10226            if (!allowText) {
10227                // Provide an icon, text will not be displayed on smaller screens.
10228                selectAllIconId = styledAttributes.getResourceId(
10229                        R.styleable.Theme_actionModeSelectAllDrawable, 0);
10230            }
10231
10232            menu.add(0, ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll).
10233                    setIcon(selectAllIconId).
10234                    setAlphabeticShortcut('a').
10235                    setShowAsAction(
10236                            MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
10237
10238            if (canCut()) {
10239                menu.add(0, ID_CUT, 0, com.android.internal.R.string.cut).
10240                    setIcon(styledAttributes.getResourceId(
10241                            R.styleable.Theme_actionModeCutDrawable, 0)).
10242                    setAlphabeticShortcut('x').
10243                    setShowAsAction(
10244                            MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
10245            }
10246
10247            if (canCopy()) {
10248                menu.add(0, ID_COPY, 0, com.android.internal.R.string.copy).
10249                    setIcon(styledAttributes.getResourceId(
10250                            R.styleable.Theme_actionModeCopyDrawable, 0)).
10251                    setAlphabeticShortcut('c').
10252                    setShowAsAction(
10253                            MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
10254            }
10255
10256            if (canPaste()) {
10257                menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste).
10258                        setIcon(styledAttributes.getResourceId(
10259                                R.styleable.Theme_actionModePasteDrawable, 0)).
10260                        setAlphabeticShortcut('v').
10261                        setShowAsAction(
10262                                MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
10263            }
10264
10265            styledAttributes.recycle();
10266
10267            if (mCustomSelectionActionModeCallback != null) {
10268                if (!mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) {
10269                    // The custom mode can choose to cancel the action mode
10270                    return false;
10271                }
10272            }
10273
10274            if (menu.hasVisibleItems() || mode.getCustomView() != null) {
10275                getSelectionController().show();
10276                return true;
10277            } else {
10278                return false;
10279            }
10280        }
10281
10282        @Override
10283        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
10284            if (mCustomSelectionActionModeCallback != null) {
10285                return mCustomSelectionActionModeCallback.onPrepareActionMode(mode, menu);
10286            }
10287            return true;
10288        }
10289
10290        @Override
10291        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
10292            if (mCustomSelectionActionModeCallback != null &&
10293                 mCustomSelectionActionModeCallback.onActionItemClicked(mode, item)) {
10294                return true;
10295            }
10296            return onTextContextMenuItem(item.getItemId());
10297        }
10298
10299        @Override
10300        public void onDestroyActionMode(ActionMode mode) {
10301            if (mCustomSelectionActionModeCallback != null) {
10302                mCustomSelectionActionModeCallback.onDestroyActionMode(mode);
10303            }
10304            Selection.setSelection((Spannable) mText, getSelectionEnd());
10305
10306            if (mSelectionModifierCursorController != null) {
10307                mSelectionModifierCursorController.hide();
10308            }
10309
10310            mSelectionActionMode = null;
10311        }
10312    }
10313
10314    private class ActionPopupWindow extends PinnedPopupWindow implements OnClickListener {
10315        private static final int POPUP_TEXT_LAYOUT =
10316                com.android.internal.R.layout.text_edit_action_popup_text;
10317        private TextView mPasteTextView;
10318        private TextView mReplaceTextView;
10319
10320        @Override
10321        protected void createPopupWindow() {
10322            mPopupWindow = new PopupWindow(TextView.this.mContext, null,
10323                    com.android.internal.R.attr.textSelectHandleWindowStyle);
10324            mPopupWindow.setClippingEnabled(true);
10325        }
10326
10327        @Override
10328        protected void initContentView() {
10329            LinearLayout linearLayout = new LinearLayout(TextView.this.getContext());
10330            linearLayout.setOrientation(LinearLayout.HORIZONTAL);
10331            mContentView = linearLayout;
10332            mContentView.setBackgroundResource(
10333                    com.android.internal.R.drawable.text_edit_paste_window);
10334
10335            LayoutInflater inflater = (LayoutInflater)TextView.this.mContext.
10336                    getSystemService(Context.LAYOUT_INFLATER_SERVICE);
10337
10338            LayoutParams wrapContent = new LayoutParams(
10339                    ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
10340
10341            mPasteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
10342            mPasteTextView.setLayoutParams(wrapContent);
10343            mContentView.addView(mPasteTextView);
10344            mPasteTextView.setText(com.android.internal.R.string.paste);
10345            mPasteTextView.setOnClickListener(this);
10346
10347            mReplaceTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
10348            mReplaceTextView.setLayoutParams(wrapContent);
10349            mContentView.addView(mReplaceTextView);
10350            mReplaceTextView.setText(com.android.internal.R.string.replace);
10351            mReplaceTextView.setOnClickListener(this);
10352        }
10353
10354        @Override
10355        public void show() {
10356            boolean canPaste = canPaste();
10357            boolean canSuggest = isSuggestionsEnabled() && isCursorInsideSuggestionSpan();
10358            mPasteTextView.setVisibility(canPaste ? View.VISIBLE : View.GONE);
10359            mReplaceTextView.setVisibility(canSuggest ? View.VISIBLE : View.GONE);
10360
10361            if (!canPaste && !canSuggest) return;
10362
10363            super.show();
10364        }
10365
10366        @Override
10367        public void onClick(View view) {
10368            if (view == mPasteTextView && canPaste()) {
10369                onTextContextMenuItem(ID_PASTE);
10370                hide();
10371            } else if (view == mReplaceTextView) {
10372                final int middle = (getSelectionStart() + getSelectionEnd()) / 2;
10373                stopSelectionActionMode();
10374                Selection.setSelection((Spannable) mText, middle);
10375                showSuggestions();
10376            }
10377        }
10378
10379        @Override
10380        protected int getTextOffset() {
10381            return (getSelectionStart() + getSelectionEnd()) / 2;
10382        }
10383
10384        @Override
10385        protected int getVerticalLocalPosition(int line) {
10386            return mLayout.getLineTop(line) - mContentView.getMeasuredHeight();
10387        }
10388
10389        @Override
10390        protected int clipVertically(int positionY) {
10391            if (positionY < 0) {
10392                final int offset = getTextOffset();
10393                final int line = mLayout.getLineForOffset(offset);
10394                positionY += mLayout.getLineBottom(line) - mLayout.getLineTop(line);
10395                positionY += mContentView.getMeasuredHeight();
10396
10397                // Assumes insertion and selection handles share the same height
10398                final Drawable handle = mContext.getResources().getDrawable(mTextSelectHandleRes);
10399                positionY += handle.getIntrinsicHeight();
10400            }
10401
10402            return positionY;
10403        }
10404    }
10405
10406    private abstract class HandleView extends View implements TextViewPositionListener {
10407        protected Drawable mDrawable;
10408        protected Drawable mDrawableLtr;
10409        protected Drawable mDrawableRtl;
10410        private final PopupWindow mContainer;
10411        // Position with respect to the parent TextView
10412        private int mPositionX, mPositionY;
10413        private boolean mIsDragging;
10414        // Offset from touch position to mPosition
10415        private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
10416        protected int mHotspotX;
10417        // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
10418        private float mTouchOffsetY;
10419        // Where the touch position should be on the handle to ensure a maximum cursor visibility
10420        private float mIdealVerticalOffset;
10421        // Parent's (TextView) previous position in window
10422        private int mLastParentX, mLastParentY;
10423        // Transient action popup window for Paste and Replace actions
10424        protected ActionPopupWindow mActionPopupWindow;
10425        // Previous text character offset
10426        private int mPreviousOffset = -1;
10427        // Previous text character offset
10428        private boolean mPositionHasChanged = true;
10429        // Used to delay the appearance of the action popup window
10430        private Runnable mActionPopupShower;
10431
10432        public HandleView(Drawable drawableLtr, Drawable drawableRtl) {
10433            super(TextView.this.mContext);
10434            mContainer = new PopupWindow(TextView.this.mContext, null,
10435                    com.android.internal.R.attr.textSelectHandleWindowStyle);
10436            mContainer.setSplitTouchEnabled(true);
10437            mContainer.setClippingEnabled(false);
10438            mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
10439            mContainer.setContentView(this);
10440
10441            mDrawableLtr = drawableLtr;
10442            mDrawableRtl = drawableRtl;
10443
10444            updateDrawable();
10445
10446            final int handleHeight = mDrawable.getIntrinsicHeight();
10447            mTouchOffsetY = -0.3f * handleHeight;
10448            mIdealVerticalOffset = 0.7f * handleHeight;
10449        }
10450
10451        protected void updateDrawable() {
10452            final int offset = getCurrentCursorOffset();
10453            final boolean isRtlCharAtOffset = mLayout.isRtlCharAt(offset);
10454            mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
10455            mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
10456        }
10457
10458        protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
10459
10460        // Touch-up filter: number of previous positions remembered
10461        private static final int HISTORY_SIZE = 5;
10462        private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
10463        private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
10464        private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
10465        private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
10466        private int mPreviousOffsetIndex = 0;
10467        private int mNumberPreviousOffsets = 0;
10468
10469        private void startTouchUpFilter(int offset) {
10470            mNumberPreviousOffsets = 0;
10471            addPositionToTouchUpFilter(offset);
10472        }
10473
10474        private void addPositionToTouchUpFilter(int offset) {
10475            mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
10476            mPreviousOffsets[mPreviousOffsetIndex] = offset;
10477            mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
10478            mNumberPreviousOffsets++;
10479        }
10480
10481        private void filterOnTouchUp() {
10482            final long now = SystemClock.uptimeMillis();
10483            int i = 0;
10484            int index = mPreviousOffsetIndex;
10485            final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
10486            while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
10487                i++;
10488                index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
10489            }
10490
10491            if (i > 0 && i < iMax &&
10492                    (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
10493                positionAtCursorOffset(mPreviousOffsets[index], false);
10494            }
10495        }
10496
10497        public boolean offsetHasBeenChanged() {
10498            return mNumberPreviousOffsets > 1;
10499        }
10500
10501        @Override
10502        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
10503            setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
10504        }
10505
10506        public void show() {
10507            if (isShowing()) return;
10508
10509            getPositionListener().addSubscriber(this, true /* local position may change */);
10510
10511            // Make sure the offset is always considered new, even when focusing at same position
10512            mPreviousOffset = -1;
10513            positionAtCursorOffset(getCurrentCursorOffset(), false);
10514
10515            hideActionPopupWindow();
10516        }
10517
10518        protected void dismiss() {
10519            mIsDragging = false;
10520            mContainer.dismiss();
10521            onDetached();
10522        }
10523
10524        public void hide() {
10525            dismiss();
10526
10527            TextView.this.getPositionListener().removeSubscriber(this);
10528        }
10529
10530        void showActionPopupWindow(int delay) {
10531            if (mActionPopupWindow == null) {
10532                mActionPopupWindow = new ActionPopupWindow();
10533            }
10534            if (mActionPopupShower == null) {
10535                mActionPopupShower = new Runnable() {
10536                    public void run() {
10537                        mActionPopupWindow.show();
10538                    }
10539                };
10540            } else {
10541                TextView.this.removeCallbacks(mActionPopupShower);
10542            }
10543            TextView.this.postDelayed(mActionPopupShower, delay);
10544        }
10545
10546        protected void hideActionPopupWindow() {
10547            if (mActionPopupShower != null) {
10548                TextView.this.removeCallbacks(mActionPopupShower);
10549            }
10550            if (mActionPopupWindow != null) {
10551                mActionPopupWindow.hide();
10552            }
10553        }
10554
10555        public boolean isShowing() {
10556            return mContainer.isShowing();
10557        }
10558
10559        private boolean isVisible() {
10560            // Always show a dragging handle.
10561            if (mIsDragging) {
10562                return true;
10563            }
10564
10565            if (isInBatchEditMode()) {
10566                return false;
10567            }
10568
10569            return getPositionListener().isVisible(mPositionX + mHotspotX, mPositionY);
10570        }
10571
10572        public abstract int getCurrentCursorOffset();
10573
10574        protected abstract void updateSelection(int offset);
10575
10576        public abstract void updatePosition(float x, float y);
10577
10578        protected void positionAtCursorOffset(int offset, boolean parentScrolled) {
10579            // A HandleView relies on the layout, which may be nulled by external methods
10580            if (mLayout == null) {
10581                // Will update controllers' state, hiding them and stopping selection mode if needed
10582                prepareCursorControllers();
10583                return;
10584            }
10585
10586            if (offset != mPreviousOffset || parentScrolled) {
10587                updateSelection(offset);
10588                addPositionToTouchUpFilter(offset);
10589                final int line = mLayout.getLineForOffset(offset);
10590
10591                mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX);
10592                mPositionY = mLayout.getLineBottom(line);
10593
10594                // Take TextView's padding into account.
10595                mPositionX += viewportToContentHorizontalOffset();
10596                mPositionY += viewportToContentVerticalOffset();
10597
10598                mPreviousOffset = offset;
10599                mPositionHasChanged = true;
10600            }
10601        }
10602
10603        public void updatePosition(int parentPositionX, int parentPositionY,
10604                boolean parentPositionChanged, boolean parentScrolled) {
10605            positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled);
10606            if (parentPositionChanged || mPositionHasChanged) {
10607                if (mIsDragging) {
10608                    // Update touchToWindow offset in case of parent scrolling while dragging
10609                    if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
10610                        mTouchToWindowOffsetX += parentPositionX - mLastParentX;
10611                        mTouchToWindowOffsetY += parentPositionY - mLastParentY;
10612                        mLastParentX = parentPositionX;
10613                        mLastParentY = parentPositionY;
10614                    }
10615
10616                    onHandleMoved();
10617                }
10618
10619                if (isVisible()) {
10620                    final int positionX = parentPositionX + mPositionX;
10621                    final int positionY = parentPositionY + mPositionY;
10622                    if (isShowing()) {
10623                        mContainer.update(positionX, positionY, -1, -1);
10624                    } else {
10625                        mContainer.showAtLocation(TextView.this, Gravity.NO_GRAVITY,
10626                                positionX, positionY);
10627                    }
10628                } else {
10629                    if (isShowing()) {
10630                        dismiss();
10631                    }
10632                }
10633
10634                mPositionHasChanged = false;
10635            }
10636        }
10637
10638        @Override
10639        protected void onDraw(Canvas c) {
10640            mDrawable.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
10641            mDrawable.draw(c);
10642        }
10643
10644        @Override
10645        public boolean onTouchEvent(MotionEvent ev) {
10646            switch (ev.getActionMasked()) {
10647                case MotionEvent.ACTION_DOWN: {
10648                    startTouchUpFilter(getCurrentCursorOffset());
10649                    mTouchToWindowOffsetX = ev.getRawX() - mPositionX;
10650                    mTouchToWindowOffsetY = ev.getRawY() - mPositionY;
10651
10652                    final PositionListener positionListener = getPositionListener();
10653                    mLastParentX = positionListener.getPositionX();
10654                    mLastParentY = positionListener.getPositionY();
10655                    mIsDragging = true;
10656                    break;
10657                }
10658
10659                case MotionEvent.ACTION_MOVE: {
10660                    final float rawX = ev.getRawX();
10661                    final float rawY = ev.getRawY();
10662
10663                    // Vertical hysteresis: vertical down movement tends to snap to ideal offset
10664                    final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
10665                    final float currentVerticalOffset = rawY - mPositionY - mLastParentY;
10666                    float newVerticalOffset;
10667                    if (previousVerticalOffset < mIdealVerticalOffset) {
10668                        newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
10669                        newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
10670                    } else {
10671                        newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
10672                        newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
10673                    }
10674                    mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
10675
10676                    final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX;
10677                    final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY;
10678
10679                    updatePosition(newPosX, newPosY);
10680                    break;
10681                }
10682
10683                case MotionEvent.ACTION_UP:
10684                    filterOnTouchUp();
10685                    mIsDragging = false;
10686                    break;
10687
10688                case MotionEvent.ACTION_CANCEL:
10689                    mIsDragging = false;
10690                    break;
10691            }
10692            return true;
10693        }
10694
10695        public boolean isDragging() {
10696            return mIsDragging;
10697        }
10698
10699        void onHandleMoved() {
10700            hideActionPopupWindow();
10701        }
10702
10703        public void onDetached() {
10704            hideActionPopupWindow();
10705        }
10706    }
10707
10708    private class InsertionHandleView extends HandleView {
10709        private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
10710        private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
10711
10712        // Used to detect taps on the insertion handle, which will affect the ActionPopupWindow
10713        private float mDownPositionX, mDownPositionY;
10714        private Runnable mHider;
10715
10716        public InsertionHandleView(Drawable drawable) {
10717            super(drawable, drawable);
10718        }
10719
10720        @Override
10721        public void show() {
10722            super.show();
10723
10724            final long durationSinceCutOrCopy = SystemClock.uptimeMillis() - sLastCutOrCopyTime;
10725            if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) {
10726                showActionPopupWindow(0);
10727            }
10728
10729            hideAfterDelay();
10730        }
10731
10732        public void showWithActionPopup() {
10733            show();
10734            showActionPopupWindow(0);
10735        }
10736
10737        private void hideAfterDelay() {
10738            removeHiderCallback();
10739            if (mHider == null) {
10740                mHider = new Runnable() {
10741                    public void run() {
10742                        hide();
10743                    }
10744                };
10745            }
10746            TextView.this.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
10747        }
10748
10749        private void removeHiderCallback() {
10750            if (mHider != null) {
10751                TextView.this.removeCallbacks(mHider);
10752            }
10753        }
10754
10755        @Override
10756        protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
10757            return drawable.getIntrinsicWidth() / 2;
10758        }
10759
10760        @Override
10761        public boolean onTouchEvent(MotionEvent ev) {
10762            final boolean result = super.onTouchEvent(ev);
10763
10764            switch (ev.getActionMasked()) {
10765                case MotionEvent.ACTION_DOWN:
10766                    mDownPositionX = ev.getRawX();
10767                    mDownPositionY = ev.getRawY();
10768                    break;
10769
10770                case MotionEvent.ACTION_UP:
10771                    if (!offsetHasBeenChanged()) {
10772                        final float deltaX = mDownPositionX - ev.getRawX();
10773                        final float deltaY = mDownPositionY - ev.getRawY();
10774                        final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
10775                        if (distanceSquared < mSquaredTouchSlopDistance) {
10776                            if (mActionPopupWindow != null && mActionPopupWindow.isShowing()) {
10777                                // Tapping on the handle dismisses the displayed action popup
10778                                mActionPopupWindow.hide();
10779                            } else {
10780                                showWithActionPopup();
10781                            }
10782                        }
10783                    }
10784                    hideAfterDelay();
10785                    break;
10786
10787                case MotionEvent.ACTION_CANCEL:
10788                    hideAfterDelay();
10789                    break;
10790
10791                default:
10792                    break;
10793            }
10794
10795            return result;
10796        }
10797
10798        @Override
10799        public int getCurrentCursorOffset() {
10800            return TextView.this.getSelectionStart();
10801        }
10802
10803        @Override
10804        public void updateSelection(int offset) {
10805            Selection.setSelection((Spannable) mText, offset);
10806        }
10807
10808        @Override
10809        public void updatePosition(float x, float y) {
10810            positionAtCursorOffset(getOffsetForPosition(x, y), false);
10811        }
10812
10813        @Override
10814        void onHandleMoved() {
10815            super.onHandleMoved();
10816            removeHiderCallback();
10817        }
10818
10819        @Override
10820        public void onDetached() {
10821            super.onDetached();
10822            removeHiderCallback();
10823        }
10824    }
10825
10826    private class SelectionStartHandleView extends HandleView {
10827
10828        public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) {
10829            super(drawableLtr, drawableRtl);
10830        }
10831
10832        @Override
10833        protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
10834            if (isRtlRun) {
10835                return drawable.getIntrinsicWidth() / 4;
10836            } else {
10837                return (drawable.getIntrinsicWidth() * 3) / 4;
10838            }
10839        }
10840
10841        @Override
10842        public int getCurrentCursorOffset() {
10843            return TextView.this.getSelectionStart();
10844        }
10845
10846        @Override
10847        public void updateSelection(int offset) {
10848            Selection.setSelection((Spannable) mText, offset, getSelectionEnd());
10849            updateDrawable();
10850        }
10851
10852        @Override
10853        public void updatePosition(float x, float y) {
10854            int offset = getOffsetForPosition(x, y);
10855
10856            // Handles can not cross and selection is at least one character
10857            final int selectionEnd = getSelectionEnd();
10858            if (offset >= selectionEnd) offset = selectionEnd - 1;
10859
10860            positionAtCursorOffset(offset, false);
10861        }
10862
10863        public ActionPopupWindow getActionPopupWindow() {
10864            return mActionPopupWindow;
10865        }
10866    }
10867
10868    private class SelectionEndHandleView extends HandleView {
10869
10870        public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) {
10871            super(drawableLtr, drawableRtl);
10872        }
10873
10874        @Override
10875        protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
10876            if (isRtlRun) {
10877                return (drawable.getIntrinsicWidth() * 3) / 4;
10878            } else {
10879                return drawable.getIntrinsicWidth() / 4;
10880            }
10881        }
10882
10883        @Override
10884        public int getCurrentCursorOffset() {
10885            return TextView.this.getSelectionEnd();
10886        }
10887
10888        @Override
10889        public void updateSelection(int offset) {
10890            Selection.setSelection((Spannable) mText, getSelectionStart(), offset);
10891            updateDrawable();
10892        }
10893
10894        @Override
10895        public void updatePosition(float x, float y) {
10896            int offset = getOffsetForPosition(x, y);
10897
10898            // Handles can not cross and selection is at least one character
10899            final int selectionStart = getSelectionStart();
10900            if (offset <= selectionStart) offset = selectionStart + 1;
10901
10902            positionAtCursorOffset(offset, false);
10903        }
10904
10905        public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) {
10906            mActionPopupWindow = actionPopupWindow;
10907        }
10908    }
10909
10910    /**
10911     * A CursorController instance can be used to control a cursor in the text.
10912     * It is not used outside of {@link TextView}.
10913     * @hide
10914     */
10915    private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
10916        /**
10917         * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}.
10918         * See also {@link #hide()}.
10919         */
10920        public void show();
10921
10922        /**
10923         * Hide the cursor controller from screen.
10924         * See also {@link #show()}.
10925         */
10926        public void hide();
10927
10928        /**
10929         * Called when the view is detached from window. Perform house keeping task, such as
10930         * stopping Runnable thread that would otherwise keep a reference on the context, thus
10931         * preventing the activity from being recycled.
10932         */
10933        public void onDetached();
10934    }
10935
10936    private class InsertionPointCursorController implements CursorController {
10937        private InsertionHandleView mHandle;
10938
10939        public void show() {
10940            getHandle().show();
10941        }
10942
10943        public void showWithActionPopup() {
10944            getHandle().showWithActionPopup();
10945        }
10946
10947        public void hide() {
10948            if (mHandle != null) {
10949                mHandle.hide();
10950            }
10951        }
10952
10953        public void onTouchModeChanged(boolean isInTouchMode) {
10954            if (!isInTouchMode) {
10955                hide();
10956            }
10957        }
10958
10959        private InsertionHandleView getHandle() {
10960            if (mSelectHandleCenter == null) {
10961                mSelectHandleCenter = mContext.getResources().getDrawable(
10962                        mTextSelectHandleRes);
10963            }
10964            if (mHandle == null) {
10965                mHandle = new InsertionHandleView(mSelectHandleCenter);
10966            }
10967            return mHandle;
10968        }
10969
10970        @Override
10971        public void onDetached() {
10972            final ViewTreeObserver observer = getViewTreeObserver();
10973            observer.removeOnTouchModeChangeListener(this);
10974
10975            if (mHandle != null) mHandle.onDetached();
10976        }
10977    }
10978
10979    private class SelectionModifierCursorController implements CursorController {
10980        private static final int DELAY_BEFORE_REPLACE_ACTION = 200; // milliseconds
10981        // The cursor controller handles, lazily created when shown.
10982        private SelectionStartHandleView mStartHandle;
10983        private SelectionEndHandleView mEndHandle;
10984        // The offsets of that last touch down event. Remembered to start selection there.
10985        private int mMinTouchOffset, mMaxTouchOffset;
10986
10987        // Double tap detection
10988        private long mPreviousTapUpTime = 0;
10989        private float mPreviousTapPositionX, mPreviousTapPositionY;
10990
10991        SelectionModifierCursorController() {
10992            resetTouchOffsets();
10993        }
10994
10995        public void show() {
10996            if (isInBatchEditMode()) {
10997                return;
10998            }
10999            initDrawables();
11000            initHandles();
11001            hideInsertionPointCursorController();
11002        }
11003
11004        private void initDrawables() {
11005            if (mSelectHandleLeft == null) {
11006                mSelectHandleLeft = mContext.getResources().getDrawable(
11007                        mTextSelectHandleLeftRes);
11008            }
11009            if (mSelectHandleRight == null) {
11010                mSelectHandleRight = mContext.getResources().getDrawable(
11011                        mTextSelectHandleRightRes);
11012            }
11013        }
11014
11015        private void initHandles() {
11016            // Lazy object creation has to be done before updatePosition() is called.
11017            if (mStartHandle == null) {
11018                mStartHandle = new SelectionStartHandleView(mSelectHandleLeft, mSelectHandleRight);
11019            }
11020            if (mEndHandle == null) {
11021                mEndHandle = new SelectionEndHandleView(mSelectHandleRight, mSelectHandleLeft);
11022            }
11023
11024            mStartHandle.show();
11025            mEndHandle.show();
11026
11027            // Make sure both left and right handles share the same ActionPopupWindow (so that
11028            // moving any of the handles hides the action popup).
11029            mStartHandle.showActionPopupWindow(DELAY_BEFORE_REPLACE_ACTION);
11030            mEndHandle.setActionPopupWindow(mStartHandle.getActionPopupWindow());
11031
11032            hideInsertionPointCursorController();
11033        }
11034
11035        public void hide() {
11036            if (mStartHandle != null) mStartHandle.hide();
11037            if (mEndHandle != null) mEndHandle.hide();
11038        }
11039
11040        public void onTouchEvent(MotionEvent event) {
11041            // This is done even when the View does not have focus, so that long presses can start
11042            // selection and tap can move cursor from this tap position.
11043            switch (event.getActionMasked()) {
11044                case MotionEvent.ACTION_DOWN:
11045                    final float x = event.getX();
11046                    final float y = event.getY();
11047
11048                    // Remember finger down position, to be able to start selection from there
11049                    mMinTouchOffset = mMaxTouchOffset = getOffsetForPosition(x, y);
11050
11051                    // Double tap detection
11052                    long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime;
11053                    if (duration <= ViewConfiguration.getDoubleTapTimeout() &&
11054                            isPositionOnText(x, y)) {
11055                        final float deltaX = x - mPreviousTapPositionX;
11056                        final float deltaY = y - mPreviousTapPositionY;
11057                        final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
11058                        if (distanceSquared < mSquaredTouchSlopDistance) {
11059                            startSelectionActionMode();
11060                            mDiscardNextActionUp = true;
11061                        }
11062                    }
11063
11064                    mPreviousTapPositionX = x;
11065                    mPreviousTapPositionY = y;
11066                    break;
11067
11068                case MotionEvent.ACTION_POINTER_DOWN:
11069                case MotionEvent.ACTION_POINTER_UP:
11070                    // Handle multi-point gestures. Keep min and max offset positions.
11071                    // Only activated for devices that correctly handle multi-touch.
11072                    if (mContext.getPackageManager().hasSystemFeature(
11073                            PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
11074                        updateMinAndMaxOffsets(event);
11075                    }
11076                    break;
11077
11078                case MotionEvent.ACTION_UP:
11079                    mPreviousTapUpTime = SystemClock.uptimeMillis();
11080                    break;
11081            }
11082        }
11083
11084        /**
11085         * @param event
11086         */
11087        private void updateMinAndMaxOffsets(MotionEvent event) {
11088            int pointerCount = event.getPointerCount();
11089            for (int index = 0; index < pointerCount; index++) {
11090                int offset = getOffsetForPosition(event.getX(index), event.getY(index));
11091                if (offset < mMinTouchOffset) mMinTouchOffset = offset;
11092                if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
11093            }
11094        }
11095
11096        public int getMinTouchOffset() {
11097            return mMinTouchOffset;
11098        }
11099
11100        public int getMaxTouchOffset() {
11101            return mMaxTouchOffset;
11102        }
11103
11104        public void resetTouchOffsets() {
11105            mMinTouchOffset = mMaxTouchOffset = -1;
11106        }
11107
11108        /**
11109         * @return true iff this controller is currently used to move the selection start.
11110         */
11111        public boolean isSelectionStartDragged() {
11112            return mStartHandle != null && mStartHandle.isDragging();
11113        }
11114
11115        public void onTouchModeChanged(boolean isInTouchMode) {
11116            if (!isInTouchMode) {
11117                hide();
11118            }
11119        }
11120
11121        @Override
11122        public void onDetached() {
11123            final ViewTreeObserver observer = getViewTreeObserver();
11124            observer.removeOnTouchModeChangeListener(this);
11125
11126            if (mStartHandle != null) mStartHandle.onDetached();
11127            if (mEndHandle != null) mEndHandle.onDetached();
11128        }
11129    }
11130
11131    private void hideInsertionPointCursorController() {
11132        // No need to create the controller to hide it.
11133        if (mInsertionPointCursorController != null) {
11134            mInsertionPointCursorController.hide();
11135        }
11136    }
11137
11138    /**
11139     * Hides the insertion controller and stops text selection mode, hiding the selection controller
11140     */
11141    private void hideControllers() {
11142        hideCursorControllers();
11143        hideSpanControllers();
11144    }
11145
11146    private void hideSpanControllers() {
11147        if (mChangeWatcher != null) {
11148            mChangeWatcher.hideControllers();
11149        }
11150    }
11151
11152    private void hideCursorControllers() {
11153        hideInsertionPointCursorController();
11154        stopSelectionActionMode();
11155    }
11156
11157    /**
11158     * Get the character offset closest to the specified absolute position. A typical use case is to
11159     * pass the result of {@link MotionEvent#getX()} and {@link MotionEvent#getY()} to this method.
11160     *
11161     * @param x The horizontal absolute position of a point on screen
11162     * @param y The vertical absolute position of a point on screen
11163     * @return the character offset for the character whose position is closest to the specified
11164     *  position. Returns -1 if there is no layout.
11165     */
11166    public int getOffsetForPosition(float x, float y) {
11167        if (getLayout() == null) return -1;
11168        final int line = getLineAtCoordinate(y);
11169        final int offset = getOffsetAtCoordinate(line, x);
11170        return offset;
11171    }
11172
11173    private float convertToLocalHorizontalCoordinate(float x) {
11174        x -= getTotalPaddingLeft();
11175        // Clamp the position to inside of the view.
11176        x = Math.max(0.0f, x);
11177        x = Math.min(getWidth() - getTotalPaddingRight() - 1, x);
11178        x += getScrollX();
11179        return x;
11180    }
11181
11182    private int getLineAtCoordinate(float y) {
11183        y -= getTotalPaddingTop();
11184        // Clamp the position to inside of the view.
11185        y = Math.max(0.0f, y);
11186        y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y);
11187        y += getScrollY();
11188        return getLayout().getLineForVertical((int) y);
11189    }
11190
11191    private int getOffsetAtCoordinate(int line, float x) {
11192        x = convertToLocalHorizontalCoordinate(x);
11193        return getLayout().getOffsetForHorizontal(line, x);
11194    }
11195
11196    /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
11197     * in the view. Returns false when the position is in the empty space of left/right of text.
11198     */
11199    private boolean isPositionOnText(float x, float y) {
11200        if (getLayout() == null) return false;
11201
11202        final int line = getLineAtCoordinate(y);
11203        x = convertToLocalHorizontalCoordinate(x);
11204
11205        if (x < getLayout().getLineLeft(line)) return false;
11206        if (x > getLayout().getLineRight(line)) return false;
11207        return true;
11208    }
11209
11210    @Override
11211    public boolean onDragEvent(DragEvent event) {
11212        switch (event.getAction()) {
11213            case DragEvent.ACTION_DRAG_STARTED:
11214                return hasInsertionController();
11215
11216            case DragEvent.ACTION_DRAG_ENTERED:
11217                TextView.this.requestFocus();
11218                return true;
11219
11220            case DragEvent.ACTION_DRAG_LOCATION:
11221                final int offset = getOffsetForPosition(event.getX(), event.getY());
11222                Selection.setSelection((Spannable)mText, offset);
11223                return true;
11224
11225            case DragEvent.ACTION_DROP:
11226                onDrop(event);
11227                return true;
11228
11229            case DragEvent.ACTION_DRAG_ENDED:
11230            case DragEvent.ACTION_DRAG_EXITED:
11231            default:
11232                return true;
11233        }
11234    }
11235
11236    private void onDrop(DragEvent event) {
11237        StringBuilder content = new StringBuilder("");
11238        ClipData clipData = event.getClipData();
11239        final int itemCount = clipData.getItemCount();
11240        for (int i=0; i < itemCount; i++) {
11241            Item item = clipData.getItemAt(i);
11242            content.append(item.coerceToText(TextView.this.mContext));
11243        }
11244
11245        final int offset = getOffsetForPosition(event.getX(), event.getY());
11246
11247        Object localState = event.getLocalState();
11248        DragLocalState dragLocalState = null;
11249        if (localState instanceof DragLocalState) {
11250            dragLocalState = (DragLocalState) localState;
11251        }
11252        boolean dragDropIntoItself = dragLocalState != null &&
11253                dragLocalState.sourceTextView == this;
11254
11255        if (dragDropIntoItself) {
11256            if (offset >= dragLocalState.start && offset < dragLocalState.end) {
11257                // A drop inside the original selection discards the drop.
11258                return;
11259            }
11260        }
11261
11262        final int originalLength = mText.length();
11263        long minMax = prepareSpacesAroundPaste(offset, offset, content);
11264        int min = extractRangeStartFromLong(minMax);
11265        int max = extractRangeEndFromLong(minMax);
11266
11267        Selection.setSelection((Spannable) mText, max);
11268        ((Editable) mText).replace(min, max, content);
11269
11270        if (dragDropIntoItself) {
11271            int dragSourceStart = dragLocalState.start;
11272            int dragSourceEnd = dragLocalState.end;
11273            if (max <= dragSourceStart) {
11274                // Inserting text before selection has shifted positions
11275                final int shift = mText.length() - originalLength;
11276                dragSourceStart += shift;
11277                dragSourceEnd += shift;
11278            }
11279
11280            // Delete original selection
11281            ((Editable) mText).delete(dragSourceStart, dragSourceEnd);
11282
11283            // Make sure we do not leave two adjacent spaces.
11284            if ((dragSourceStart == 0 ||
11285                    Character.isSpaceChar(mTransformed.charAt(dragSourceStart - 1))) &&
11286                    (dragSourceStart == mText.length() ||
11287                    Character.isSpaceChar(mTransformed.charAt(dragSourceStart)))) {
11288                final int pos = dragSourceStart == mText.length() ?
11289                        dragSourceStart - 1 : dragSourceStart;
11290                ((Editable) mText).delete(pos, pos + 1);
11291            }
11292        }
11293    }
11294
11295    /**
11296     * @return True if this view supports insertion handles.
11297     */
11298    boolean hasInsertionController() {
11299        return mInsertionControllerEnabled;
11300    }
11301
11302    /**
11303     * @return True if this view supports selection handles.
11304     */
11305    boolean hasSelectionController() {
11306        return mSelectionControllerEnabled;
11307    }
11308
11309    InsertionPointCursorController getInsertionController() {
11310        if (!mInsertionControllerEnabled) {
11311            return null;
11312        }
11313
11314        if (mInsertionPointCursorController == null) {
11315            mInsertionPointCursorController = new InsertionPointCursorController();
11316
11317            final ViewTreeObserver observer = getViewTreeObserver();
11318            observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
11319        }
11320
11321        return mInsertionPointCursorController;
11322    }
11323
11324    SelectionModifierCursorController getSelectionController() {
11325        if (!mSelectionControllerEnabled) {
11326            return null;
11327        }
11328
11329        if (mSelectionModifierCursorController == null) {
11330            mSelectionModifierCursorController = new SelectionModifierCursorController();
11331
11332            final ViewTreeObserver observer = getViewTreeObserver();
11333            observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
11334        }
11335
11336        return mSelectionModifierCursorController;
11337    }
11338
11339    boolean isInBatchEditMode() {
11340        final InputMethodState ims = mInputMethodState;
11341        if (ims != null) {
11342            return ims.mBatchEditNesting > 0;
11343        }
11344        return mInBatchEditControllers;
11345    }
11346
11347    @Override
11348    protected void resolveTextDirection() {
11349        if (hasPasswordTransformationMethod()) {
11350            mTextDir = TextDirectionHeuristics.LOCALE;
11351            return;
11352        }
11353
11354        // Always need to resolve layout direction first
11355        final boolean defaultIsRtl = (getResolvedLayoutDirection() == LAYOUT_DIRECTION_RTL);
11356
11357        // Then resolve text direction on the parent
11358        super.resolveTextDirection();
11359
11360        // Now, we can select the heuristic
11361        int textDir = getResolvedTextDirection();
11362        switch (textDir) {
11363            default:
11364            case TEXT_DIRECTION_FIRST_STRONG:
11365                mTextDir = (defaultIsRtl ? TextDirectionHeuristics.FIRSTSTRONG_RTL :
11366                        TextDirectionHeuristics.FIRSTSTRONG_LTR);
11367                break;
11368            case TEXT_DIRECTION_ANY_RTL:
11369                mTextDir = TextDirectionHeuristics.ANYRTL_LTR;
11370                break;
11371            case TEXT_DIRECTION_LTR:
11372                mTextDir = TextDirectionHeuristics.LTR;
11373                break;
11374            case TEXT_DIRECTION_RTL:
11375                mTextDir = TextDirectionHeuristics.RTL;
11376                break;
11377        }
11378    }
11379
11380    /**
11381     * Subclasses will need to override this method to implement their own way of resolving
11382     * drawables depending on the layout direction.
11383     *
11384     * A call to the super method will be required from the subclasses implementation.
11385     *
11386     */
11387    protected void resolveDrawables() {
11388        // No need to resolve twice
11389        if (mResolvedDrawables) {
11390            return;
11391        }
11392        // No drawable to resolve
11393        if (mDrawables == null) {
11394            return;
11395        }
11396        // No relative drawable to resolve
11397        if (mDrawables.mDrawableStart == null && mDrawables.mDrawableEnd == null) {
11398            mResolvedDrawables = true;
11399            return;
11400        }
11401
11402        Drawables dr = mDrawables;
11403        switch(getResolvedLayoutDirection()) {
11404            case LAYOUT_DIRECTION_RTL:
11405                if (dr.mDrawableStart != null) {
11406                    dr.mDrawableRight = dr.mDrawableStart;
11407
11408                    dr.mDrawableSizeRight = dr.mDrawableSizeStart;
11409                    dr.mDrawableHeightRight = dr.mDrawableHeightStart;
11410                }
11411                if (dr.mDrawableEnd != null) {
11412                    dr.mDrawableLeft = dr.mDrawableEnd;
11413
11414                    dr.mDrawableSizeLeft = dr.mDrawableSizeEnd;
11415                    dr.mDrawableHeightLeft = dr.mDrawableHeightEnd;
11416                }
11417                break;
11418
11419            case LAYOUT_DIRECTION_LTR:
11420            default:
11421                if (dr.mDrawableStart != null) {
11422                    dr.mDrawableLeft = dr.mDrawableStart;
11423
11424                    dr.mDrawableSizeLeft = dr.mDrawableSizeStart;
11425                    dr.mDrawableHeightLeft = dr.mDrawableHeightStart;
11426                }
11427                if (dr.mDrawableEnd != null) {
11428                    dr.mDrawableRight = dr.mDrawableEnd;
11429
11430                    dr.mDrawableSizeRight = dr.mDrawableSizeEnd;
11431                    dr.mDrawableHeightRight = dr.mDrawableHeightEnd;
11432                }
11433                break;
11434        }
11435        mResolvedDrawables = true;
11436    }
11437
11438    protected void resetResolvedDrawables() {
11439        mResolvedDrawables = false;
11440    }
11441
11442    /**
11443     * @hide
11444     */
11445    protected void viewClicked(InputMethodManager imm) {
11446        if (imm != null) {
11447            imm.viewClicked(this);
11448        }
11449    }
11450
11451    @ViewDebug.ExportedProperty(category = "text")
11452    private CharSequence            mText;
11453    private CharSequence            mTransformed;
11454    private BufferType              mBufferType = BufferType.NORMAL;
11455
11456    private int                     mInputType = EditorInfo.TYPE_NULL;
11457    private CharSequence            mHint;
11458    private Layout                  mHintLayout;
11459
11460    private KeyListener             mInput;
11461
11462    private MovementMethod          mMovement;
11463    private TransformationMethod    mTransformation;
11464    private boolean                 mAllowTransformationLengthChange;
11465    private ChangeWatcher           mChangeWatcher;
11466
11467    private ArrayList<TextWatcher>  mListeners = null;
11468
11469    // display attributes
11470    private final TextPaint         mTextPaint;
11471    private boolean                 mUserSetTextScaleX;
11472    private final Paint             mHighlightPaint;
11473    private int                     mHighlightColor = 0x6633B5E5;
11474    /**
11475     * This is temporarily visible to fix bug 3085564 in webView. Do not rely on
11476     * this field being protected. Will be restored as private when lineHeight
11477     * feature request 3215097 is implemented
11478     * @hide
11479     */
11480    protected Layout                mLayout;
11481
11482    private long                    mShowCursor;
11483    private Blink                   mBlink;
11484    private boolean                 mCursorVisible = true;
11485
11486    // Cursor Controllers.
11487    private InsertionPointCursorController mInsertionPointCursorController;
11488    private SelectionModifierCursorController mSelectionModifierCursorController;
11489    private ActionMode              mSelectionActionMode;
11490    private boolean                 mInsertionControllerEnabled;
11491    private boolean                 mSelectionControllerEnabled;
11492    private boolean                 mInBatchEditControllers;
11493
11494    // These are needed to desambiguate a long click. If the long click comes from ones of these, we
11495    // select from the current cursor position. Otherwise, select from long pressed position.
11496    private boolean                 mDPadCenterIsDown = false;
11497    private boolean                 mEnterKeyIsDown = false;
11498    private boolean                 mContextMenuTriggeredByKey = false;
11499
11500    private boolean                 mSelectAllOnFocus = false;
11501
11502    private int                     mGravity = Gravity.TOP | Gravity.START;
11503    private boolean                 mHorizontallyScrolling;
11504
11505    private int                     mAutoLinkMask;
11506    private boolean                 mLinksClickable = true;
11507
11508    private float                   mSpacingMult = 1.0f;
11509    private float                   mSpacingAdd = 0.0f;
11510    private boolean                 mTextIsSelectable = false;
11511
11512    private static final int        LINES = 1;
11513    private static final int        EMS = LINES;
11514    private static final int        PIXELS = 2;
11515
11516    private int                     mMaximum = Integer.MAX_VALUE;
11517    private int                     mMaxMode = LINES;
11518    private int                     mMinimum = 0;
11519    private int                     mMinMode = LINES;
11520
11521    private int                     mOldMaximum = mMaximum;
11522    private int                     mOldMaxMode = mMaxMode;
11523
11524    private int                     mMaxWidth = Integer.MAX_VALUE;
11525    private int                     mMaxWidthMode = PIXELS;
11526    private int                     mMinWidth = 0;
11527    private int                     mMinWidthMode = PIXELS;
11528
11529    private boolean                 mSingleLine;
11530    private int                     mDesiredHeightAtMeasure = -1;
11531    private boolean                 mIncludePad = true;
11532
11533    // tmp primitives, so we don't alloc them on each draw
11534    private Path                    mHighlightPath;
11535    private boolean                 mHighlightPathBogus = true;
11536    private static final RectF      sTempRect = new RectF();
11537
11538    // XXX should be much larger
11539    private static final int        VERY_WIDE = 16384;
11540
11541    private static final int        BLINK = 500;
11542
11543    private static final int ANIMATED_SCROLL_GAP = 250;
11544    private long mLastScroll;
11545    private Scroller mScroller = null;
11546
11547    private BoringLayout.Metrics mBoring;
11548    private BoringLayout.Metrics mHintBoring;
11549
11550    private BoringLayout mSavedLayout, mSavedHintLayout;
11551
11552    private TextDirectionHeuristic mTextDir = null;
11553
11554    private static final InputFilter[] NO_FILTERS = new InputFilter[0];
11555    private InputFilter[] mFilters = NO_FILTERS;
11556    private static final Spanned EMPTY_SPANNED = new SpannedString("");
11557    private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
11558    // System wide time for last cut or copy action.
11559    private static long sLastCutOrCopyTime;
11560    // Used to highlight a word when it is corrected by the IME
11561    private CorrectionHighlighter mCorrectionHighlighter;
11562    // New state used to change background based on whether this TextView is multiline.
11563    private static final int[] MULTILINE_STATE_SET = { R.attr.state_multiline };
11564}
11565