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