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