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