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