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