WebTextView.java revision 2376f8253f59e70e733bdf338e9cb49535f3d50f
1/*
2 * Copyright (C) 2007 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.webkit;
18
19import com.android.internal.widget.EditableInputConnection;
20
21import android.content.Context;
22import android.graphics.Canvas;
23import android.graphics.Color;
24import android.graphics.ColorFilter;
25import android.graphics.Paint;
26import android.graphics.PixelFormat;
27import android.graphics.Rect;
28import android.graphics.drawable.ColorDrawable;
29import android.os.Bundle;
30import android.os.Handler;
31import android.os.Message;
32import android.os.ResultReceiver;
33import android.text.BoringLayout.Metrics;
34import android.text.DynamicLayout;
35import android.text.Editable;
36import android.text.InputFilter;
37import android.text.InputType;
38import android.text.Layout;
39import android.text.Selection;
40import android.text.Spannable;
41import android.text.TextPaint;
42import android.text.TextUtils;
43import android.text.method.MovementMethod;
44import android.text.method.Touch;
45import android.util.Log;
46import android.util.TypedValue;
47import android.view.Gravity;
48import android.view.KeyCharacterMap;
49import android.view.KeyEvent;
50import android.view.MotionEvent;
51import android.view.View;
52import android.view.ViewConfiguration;
53import android.view.ViewGroup;
54import android.view.inputmethod.EditorInfo;
55import android.view.inputmethod.InputConnection;
56import android.view.inputmethod.InputMethodManager;
57import android.widget.AbsoluteLayout.LayoutParams;
58import android.widget.AdapterView;
59import android.widget.ArrayAdapter;
60import android.widget.AutoCompleteTextView;
61import android.widget.TextView;
62
63import java.net.MalformedURLException;
64import java.net.URL;
65import java.util.ArrayList;
66
67import junit.framework.Assert;
68
69/**
70 * WebTextView is a specialized version of EditText used by WebView
71 * to overlay html textfields (and textareas) to use our standard
72 * text editing.
73 */
74/* package */ class WebTextView extends AutoCompleteTextView
75        implements AdapterView.OnItemClickListener {
76
77    static final String LOGTAG = "webtextview";
78
79    private WebView         mWebView;
80    private boolean         mSingle;
81    private int             mWidthSpec;
82    private int             mHeightSpec;
83    private int             mNodePointer;
84    // FIXME: This is a hack for blocking unmatched key ups, in particular
85    // on the enter key.  The method for blocking unmatched key ups prevents
86    // the shift key from working properly.
87    private boolean         mGotEnterDown;
88    private int             mMaxLength;
89    // Keep track of the text before the change so we know whether we actually
90    // need to send down the DOM events.
91    private String          mPreChange;
92    // Variables for keeping track of the touch down, to send to the WebView
93    // when a drag starts
94    private float           mDragStartX;
95    private float           mDragStartY;
96    private long            mDragStartTime;
97    private boolean         mDragSent;
98    // True if the most recent drag event has caused either the TextView to
99    // scroll or the web page to scroll.  Gets reset after a touch down.
100    private boolean         mScrolled;
101    // Whether or not a selection change was generated from webkit.  If it was,
102    // we do not need to pass the selection back to webkit.
103    private boolean         mFromWebKit;
104    // Whether or not a selection change was generated from the WebTextView
105    // gaining focus.  If it is, we do not want to pass it to webkit.  This
106    // selection comes from the MovementMethod, but we behave differently.  If
107    // WebTextView gained focus from a touch, webkit will determine the
108    // selection.
109    private boolean         mFromFocusChange;
110    // Whether or not a selection change was generated from setInputType.  We
111    // do not want to pass this change to webkit.
112    private boolean         mFromSetInputType;
113    private boolean         mGotTouchDown;
114    // Keep track of whether a long press has happened.  Only meaningful after
115    // an ACTION_DOWN MotionEvent
116    private boolean         mHasPerformedLongClick;
117    private boolean         mInSetTextAndKeepSelection;
118    // Array to store the final character added in onTextChanged, so that its
119    // KeyEvents may be determined.
120    private char[]          mCharacter = new char[1];
121    // This is used to reset the length filter when on a textfield
122    // with no max length.
123    // FIXME: This can be replaced with TextView.NO_FILTERS if that
124    // is made public/protected.
125    private static final InputFilter[] NO_FILTERS = new InputFilter[0];
126    // For keeping track of the fact that the delete key was pressed, so
127    // we can simply pass a delete key instead of calling deleteSelection.
128    private boolean mGotDelete;
129    private int mDelSelStart;
130    private int mDelSelEnd;
131
132    // Keep in sync with native constant in
133    // external/webkit/WebKit/android/WebCoreSupport/autofill/WebAutoFill.cpp
134    /* package */ static final int FORM_NOT_AUTOFILLABLE = -1;
135
136    private boolean mAutoFillable; // Is this textview part of an autofillable form?
137    private int mQueryId;
138    private boolean mAutoFillProfileIsSet;
139    // Used to determine whether onFocusChanged was called as a result of
140    // calling remove().
141    private boolean mInsideRemove;
142    private class MyResultReceiver extends ResultReceiver {
143        @Override
144        protected void onReceiveResult(int resultCode, Bundle resultData) {
145            if (resultCode == InputMethodManager.RESULT_SHOWN
146                    && mWebView != null) {
147                mWebView.revealSelection();
148            }
149        }
150
151        /**
152         * @param handler
153         */
154        public MyResultReceiver(Handler handler) {
155            super(handler);
156        }
157    }
158    private MyResultReceiver mReceiver;
159
160    // Types used with setType.  Keep in sync with CachedInput.h
161    private static final int NORMAL_TEXT_FIELD = 0;
162    private static final int TEXT_AREA = 1;
163    private static final int PASSWORD = 2;
164    private static final int SEARCH = 3;
165    private static final int EMAIL = 4;
166    private static final int NUMBER = 5;
167    private static final int TELEPHONE = 6;
168    private static final int URL = 7;
169
170    private static final int AUTOFILL_FORM = 100;
171    private Handler mHandler;
172
173    /**
174     * Create a new WebTextView.
175     * @param   context The Context for this WebTextView.
176     * @param   webView The WebView that created this.
177     */
178    /* package */ WebTextView(Context context, WebView webView, int autoFillQueryId) {
179        super(context, null, com.android.internal.R.attr.webTextViewStyle);
180        mWebView = webView;
181        mMaxLength = -1;
182        setAutoFillable(autoFillQueryId);
183        // Turn on subpixel text, and turn off kerning, so it better matches
184        // the text in webkit.
185        TextPaint paint = getPaint();
186        int flags = paint.getFlags() & ~Paint.DEV_KERN_TEXT_FLAG
187                | Paint.SUBPIXEL_TEXT_FLAG | Paint.DITHER_FLAG;
188        paint.setFlags(flags);
189
190        // Set the text color to black, regardless of the theme.  This ensures
191        // that other applications that use embedded WebViews will properly
192        // display the text in password textfields.
193        setTextColor(DebugFlags.DRAW_WEBTEXTVIEW ? Color.RED : Color.BLACK);
194        setBackgroundDrawable(DebugFlags.DRAW_WEBTEXTVIEW ? null : new ColorDrawable(Color.WHITE));
195
196        // This helps to align the text better with the text in the web page.
197        setIncludeFontPadding(false);
198
199        mHandler = new Handler() {
200            @Override
201            public void handleMessage(Message msg) {
202                switch (msg.what) {
203                case AUTOFILL_FORM:
204                    mWebView.autoFillForm(mQueryId);
205                    break;
206                }
207            }
208        };
209        mReceiver = new MyResultReceiver(mHandler);
210    }
211
212    public void setAutoFillable(int queryId) {
213        mAutoFillable = mWebView.getSettings().getAutoFillEnabled()
214                && (queryId != FORM_NOT_AUTOFILLABLE);
215        mQueryId = queryId;
216    }
217
218    @Override
219    public boolean dispatchKeyEvent(KeyEvent event) {
220        if (event.isSystem()) {
221            return super.dispatchKeyEvent(event);
222        }
223        // Treat ACTION_DOWN and ACTION MULTIPLE the same
224        boolean down = event.getAction() != KeyEvent.ACTION_UP;
225        int keyCode = event.getKeyCode();
226
227        boolean isArrowKey = false;
228        switch(keyCode) {
229            case KeyEvent.KEYCODE_DPAD_LEFT:
230            case KeyEvent.KEYCODE_DPAD_RIGHT:
231            case KeyEvent.KEYCODE_DPAD_UP:
232            case KeyEvent.KEYCODE_DPAD_DOWN:
233                isArrowKey = true;
234                break;
235        }
236
237        if (KeyEvent.KEYCODE_TAB == keyCode) {
238            if (down) {
239                onEditorAction(EditorInfo.IME_ACTION_NEXT);
240            }
241            return true;
242        }
243        Spannable text = (Spannable) getText();
244        int oldStart = Selection.getSelectionStart(text);
245        int oldEnd = Selection.getSelectionEnd(text);
246        // Normally the delete key's dom events are sent via onTextChanged.
247        // However, if the cursor is at the beginning of the field, which
248        // includes the case where it has zero length, then the text is not
249        // changed, so send the events immediately.
250        if (KeyEvent.KEYCODE_DEL == keyCode) {
251            if (oldStart == 0 && oldEnd == 0) {
252                sendDomEvent(event);
253                return true;
254            }
255            if (down) {
256                mGotDelete = true;
257                mDelSelStart = oldStart;
258                mDelSelEnd = oldEnd;
259            }
260        }
261
262        if (mSingle && (KeyEvent.KEYCODE_ENTER == keyCode
263                    || KeyEvent.KEYCODE_NUMPAD_ENTER == keyCode)) {
264            if (isPopupShowing()) {
265                return super.dispatchKeyEvent(event);
266            }
267            if (!down) {
268                // Hide the keyboard, since the user has just submitted this
269                // form.  The submission happens thanks to the two calls
270                // to sendDomEvent.
271                InputMethodManager.getInstance(mContext)
272                        .hideSoftInputFromWindow(getWindowToken(), 0);
273                sendDomEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
274                sendDomEvent(event);
275            }
276            return super.dispatchKeyEvent(event);
277        } else if (KeyEvent.KEYCODE_DPAD_CENTER == keyCode) {
278            // Note that this handles center key and trackball.
279            if (isPopupShowing()) {
280                return super.dispatchKeyEvent(event);
281            }
282            // Center key should be passed to a potential onClick
283            if (!down) {
284                mWebView.centerKeyPressOnTextField();
285            }
286            // Pass to super to handle longpress.
287            return super.dispatchKeyEvent(event);
288        }
289
290        // Ensure there is a layout so arrow keys are handled properly.
291        if (getLayout() == null) {
292            measure(mWidthSpec, mHeightSpec);
293        }
294
295        int oldLength = text.length();
296        boolean maxedOut = mMaxLength != -1 && oldLength == mMaxLength;
297        // If we are at max length, and there is a selection rather than a
298        // cursor, we need to store the text to compare later, since the key
299        // may have changed the string.
300        String oldText;
301        if (maxedOut && oldEnd != oldStart) {
302            oldText = text.toString();
303        } else {
304            oldText = "";
305        }
306        if (super.dispatchKeyEvent(event)) {
307            // If the WebTextView handled the key it was either an alphanumeric
308            // key, a delete, or a movement within the text. All of those are
309            // ok to pass to javascript.
310
311            // UNLESS there is a max length determined by the html.  In that
312            // case, if the string was already at the max length, an
313            // alphanumeric key will be erased by the LengthFilter,
314            // so do not pass down to javascript, and instead
315            // return true.  If it is an arrow key or a delete key, we can go
316            // ahead and pass it down.
317            if (KeyEvent.KEYCODE_ENTER == keyCode
318                        || KeyEvent.KEYCODE_NUMPAD_ENTER == keyCode) {
319                // For multi-line text boxes, newlines will
320                // trigger onTextChanged for key down (which will send both
321                // key up and key down) but not key up.
322                mGotEnterDown = true;
323            }
324            if (maxedOut && !isArrowKey && keyCode != KeyEvent.KEYCODE_DEL) {
325                if (oldEnd == oldStart) {
326                    // Return true so the key gets dropped.
327                    return true;
328                } else if (!oldText.equals(getText().toString())) {
329                    // FIXME: This makes the text work properly, but it
330                    // does not pass down the key event, so it may not
331                    // work for a textfield that has the type of
332                    // behavior of GoogleSuggest.  That said, it is
333                    // unlikely that a site would combine the two in
334                    // one textfield.
335                    Spannable span = (Spannable) getText();
336                    int newStart = Selection.getSelectionStart(span);
337                    int newEnd = Selection.getSelectionEnd(span);
338                    mWebView.replaceTextfieldText(0, oldLength, span.toString(),
339                            newStart, newEnd);
340                    return true;
341                }
342            }
343            /* FIXME:
344             * In theory, we would like to send the events for the arrow keys.
345             * However, the TextView can arbitrarily change the selection (i.e.
346             * long press followed by using the trackball).  Therefore, we keep
347             * in sync with the TextView via onSelectionChanged.  If we also
348             * send the DOM event, we lose the correct selection.
349            if (isArrowKey) {
350                // Arrow key does not change the text, but we still want to send
351                // the DOM events.
352                sendDomEvent(event);
353            }
354             */
355            return true;
356        }
357        // Ignore the key up event for newlines. This prevents
358        // multiple newlines in the native textarea.
359        if (mGotEnterDown && !down) {
360            return true;
361        }
362        // if it is a navigation key, pass it to WebView
363        if (isArrowKey) {
364            // WebView check the trackballtime in onKeyDown to avoid calling
365            // native from both trackball and key handling. As this is called
366            // from WebTextView, we always want WebView to check with native.
367            // Reset trackballtime to ensure it.
368            mWebView.resetTrackballTime();
369            return down ? mWebView.onKeyDown(keyCode, event) : mWebView
370                    .onKeyUp(keyCode, event);
371        }
372        return false;
373    }
374
375    void ensureLayout() {
376        if (getLayout() == null) {
377            // Ensure we have a Layout
378            measure(mWidthSpec, mHeightSpec);
379            LayoutParams params = (LayoutParams) getLayoutParams();
380            if (params != null) {
381                layout(params.x, params.y, params.x + params.width,
382                        params.y + params.height);
383            }
384        }
385    }
386
387    /* package */ ResultReceiver getResultReceiver() { return mReceiver; }
388
389    /**
390     *  Determine whether this WebTextView currently represents the node
391     *  represented by ptr.
392     *  @param  ptr Pointer to a node to compare to.
393     *  @return boolean Whether this WebTextView already represents the node
394     *          pointed to by ptr.
395     */
396    /* package */ boolean isSameTextField(int ptr) {
397        return ptr == mNodePointer;
398    }
399
400    /**
401     * Ensure that the underlying text field/area is lined up with the WebTextView.
402     */
403    private void lineUpScroll() {
404        Layout layout = getLayout();
405        if (mWebView != null && layout != null) {
406            if (mSingle) {
407                // textfields only need to be lined up horizontally.
408                float maxScrollX = layout.getLineRight(0) - getWidth();
409                if (DebugFlags.WEB_TEXT_VIEW) {
410                    Log.v(LOGTAG, "onTouchEvent x=" + mScrollX + " y="
411                            + mScrollY + " maxX=" + maxScrollX);
412                }
413                mWebView.scrollFocusedTextInputX(maxScrollX > 0 ?
414                        mScrollX / maxScrollX : 0);
415            } else {
416                // textareas only need to be lined up vertically.
417                mWebView.scrollFocusedTextInputY(mScrollY);
418            }
419        }
420    }
421
422    @Override
423    protected void makeNewLayout(int w, int hintWidth, Metrics boring,
424            Metrics hintBoring, int ellipsisWidth, boolean bringIntoView) {
425        // Necessary to get a Layout to work with, and to do the other work that
426        // makeNewLayout does.
427        super.makeNewLayout(w, hintWidth, boring, hintBoring, ellipsisWidth,
428                bringIntoView);
429        lineUpScroll();
430    }
431
432    /**
433     * Custom layout which figures out its line spacing.  If -1 is passed in for
434     * the height, it will use the ascent and descent from the paint to
435     * determine the line spacing.  Otherwise it will use the spacing provided.
436     */
437    private static class WebTextViewLayout extends DynamicLayout {
438        private float mLineHeight;
439        private float mDifference;
440        public WebTextViewLayout(CharSequence base, CharSequence display,
441                TextPaint paint,
442                int width, Alignment align,
443                float spacingMult, float spacingAdd,
444                boolean includepad,
445                TextUtils.TruncateAt ellipsize, int ellipsizedWidth,
446                float lineHeight) {
447            super(base, display, paint, width, align, spacingMult, spacingAdd,
448                    includepad, ellipsize, ellipsizedWidth);
449            float paintLineHeight = paint.descent() - paint.ascent();
450            if (lineHeight == -1f) {
451                mLineHeight = paintLineHeight;
452                mDifference = 0f;
453            } else {
454                mLineHeight = lineHeight;
455                // Through trial and error, I found this calculation to improve
456                // the accuracy of line placement.
457                mDifference = (lineHeight - paintLineHeight) / 2;
458            }
459        }
460
461        @Override
462        public int getLineTop(int line) {
463            return Math.round(mLineHeight * line - mDifference);
464        }
465    }
466
467    @Override public InputConnection onCreateInputConnection(
468            EditorInfo outAttrs) {
469        InputConnection connection = super.onCreateInputConnection(outAttrs);
470        if (mWebView != null) {
471            // Use the name of the textfield + the url.  Use backslash as an
472            // arbitrary separator.
473            outAttrs.fieldName = mWebView.nativeFocusCandidateName() + "\\"
474                    + mWebView.getUrl();
475        }
476        return connection;
477    }
478
479    @Override
480    public void onEditorAction(int actionCode) {
481        switch (actionCode) {
482        case EditorInfo.IME_ACTION_NEXT:
483            if (mWebView.nativeMoveCursorToNextTextInput()) {
484                // Preemptively rebuild the WebTextView, so that the action will
485                // be set properly.
486                mWebView.rebuildWebTextView();
487                setDefaultSelection();
488                mWebView.invalidate();
489            }
490            break;
491        case EditorInfo.IME_ACTION_DONE:
492            super.onEditorAction(actionCode);
493            break;
494        case EditorInfo.IME_ACTION_GO:
495        case EditorInfo.IME_ACTION_SEARCH:
496            // Send an enter and hide the soft keyboard
497            InputMethodManager.getInstance(mContext)
498                    .hideSoftInputFromWindow(getWindowToken(), 0);
499            sendDomEvent(new KeyEvent(KeyEvent.ACTION_DOWN,
500                    KeyEvent.KEYCODE_ENTER));
501            sendDomEvent(new KeyEvent(KeyEvent.ACTION_UP,
502                    KeyEvent.KEYCODE_ENTER));
503
504        default:
505            break;
506        }
507    }
508
509    @Override
510    protected void onFocusChanged(boolean focused, int direction,
511            Rect previouslyFocusedRect) {
512        mFromFocusChange = true;
513        super.onFocusChanged(focused, direction, previouslyFocusedRect);
514        if (focused) {
515            mWebView.setActive(true);
516        } else if (!mInsideRemove) {
517            mWebView.setActive(false);
518        }
519        mFromFocusChange = false;
520    }
521
522    // AdapterView.OnItemClickListener implementation
523
524    @Override
525    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
526        if (id == 0 && position == 0) {
527            // Blank out the text box while we wait for WebCore to fill the form.
528            replaceText("");
529            WebSettings settings = mWebView.getSettings();
530            if (mAutoFillProfileIsSet) {
531                // Call a webview method to tell WebCore to autofill the form.
532                mWebView.autoFillForm(mQueryId);
533            } else {
534                // There is no autofill profile setup yet and the user has
535                // elected to try and set one up. Call through to the
536                // embedder to action that.
537                mWebView.getWebChromeClient().setupAutoFill(
538                        mHandler.obtainMessage(AUTOFILL_FORM));
539            }
540        }
541    }
542
543    @Override
544    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
545        super.onScrollChanged(l, t, oldl, oldt);
546        lineUpScroll();
547    }
548
549    @Override
550    protected void onSelectionChanged(int selStart, int selEnd) {
551        if (!mFromWebKit && !mFromFocusChange && !mFromSetInputType
552                && mWebView != null && !mInSetTextAndKeepSelection) {
553            if (DebugFlags.WEB_TEXT_VIEW) {
554                Log.v(LOGTAG, "onSelectionChanged selStart=" + selStart
555                        + " selEnd=" + selEnd);
556            }
557            mWebView.setSelection(selStart, selEnd);
558            lineUpScroll();
559        }
560    }
561
562    @Override
563    protected void onTextChanged(CharSequence s,int start,int before,int count){
564        super.onTextChanged(s, start, before, count);
565        String postChange = s.toString();
566        // Prevent calls to setText from invoking onTextChanged (since this will
567        // mean we are on a different textfield).  Also prevent the change when
568        // going from a textfield with a string of text to one with a smaller
569        // limit on text length from registering the onTextChanged event.
570        if (mPreChange == null || mPreChange.equals(postChange) ||
571                (mMaxLength > -1 && mPreChange.length() > mMaxLength &&
572                mPreChange.substring(0, mMaxLength).equals(postChange))) {
573            return;
574        }
575        mPreChange = postChange;
576        if (0 == count) {
577            if (before > 0) {
578                // For this and all changes to the text, update our cache
579                updateCachedTextfield();
580                if (mGotDelete) {
581                    mGotDelete = false;
582                    int oldEnd = start + before;
583                    if (mDelSelEnd == oldEnd
584                            && (mDelSelStart == start
585                            || (mDelSelStart == oldEnd && before == 1))) {
586                        // If the selection is set up properly before the
587                        // delete, send the DOM events.
588                        sendDomEvent(new KeyEvent(KeyEvent.ACTION_DOWN,
589                                KeyEvent.KEYCODE_DEL));
590                        sendDomEvent(new KeyEvent(KeyEvent.ACTION_UP,
591                                KeyEvent.KEYCODE_DEL));
592                        return;
593                    }
594                }
595                // This was simply a delete or a cut, so just delete the
596                // selection.
597                mWebView.deleteSelection(start, start + before);
598            }
599            mGotDelete = false;
600            // before should never be negative, so whether it was a cut
601            // (handled above), or before is 0, in which case nothing has
602            // changed, we should return.
603            return;
604        }
605        // Ensure that this flag gets cleared, since with autocorrect on, a
606        // delete key press may have a more complex result than deleting one
607        // character or the existing selection, so it will not get cleared
608        // above.
609        mGotDelete = false;
610        // Find the last character being replaced.  If it can be represented by
611        // events, we will pass them to native (after replacing the beginning
612        // of the changed text), so we can see javascript events.
613        // Otherwise, replace the text being changed (including the last
614        // character) in the textfield.
615        TextUtils.getChars(s, start + count - 1, start + count, mCharacter, 0);
616        KeyCharacterMap kmap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
617        KeyEvent[] events = kmap.getEvents(mCharacter);
618        boolean cannotUseKeyEvents = null == events;
619        int charactersFromKeyEvents = cannotUseKeyEvents ? 0 : 1;
620        if (count > 1 || cannotUseKeyEvents) {
621            String replace = s.subSequence(start,
622                    start + count - charactersFromKeyEvents).toString();
623            mWebView.replaceTextfieldText(start, start + before, replace,
624                    start + count - charactersFromKeyEvents,
625                    start + count - charactersFromKeyEvents);
626        } else {
627            // This corrects the selection which may have been affected by the
628            // trackball or auto-correct.
629            if (DebugFlags.WEB_TEXT_VIEW) {
630                Log.v(LOGTAG, "onTextChanged start=" + start
631                        + " start + before=" + (start + before));
632            }
633            if (!mInSetTextAndKeepSelection) {
634                mWebView.setSelection(start, start + before);
635            }
636        }
637        if (!cannotUseKeyEvents) {
638            int length = events.length;
639            for (int i = 0; i < length; i++) {
640                // We never send modifier keys to native code so don't send them
641                // here either.
642                if (!KeyEvent.isModifierKey(events[i].getKeyCode())) {
643                    sendDomEvent(events[i]);
644                }
645            }
646        }
647        updateCachedTextfield();
648    }
649
650    @Override
651    public boolean onTouchEvent(MotionEvent event) {
652        switch (event.getAction()) {
653        case MotionEvent.ACTION_DOWN:
654            super.onTouchEvent(event);
655            // This event may be the start of a drag, so store it to pass to the
656            // WebView if it is.
657            mDragStartX = event.getX();
658            mDragStartY = event.getY();
659            mDragStartTime = event.getEventTime();
660            mDragSent = false;
661            mScrolled = false;
662            mGotTouchDown = true;
663            mHasPerformedLongClick = false;
664            break;
665        case MotionEvent.ACTION_MOVE:
666            if (mHasPerformedLongClick) {
667                mGotTouchDown = false;
668                return false;
669            }
670            int slop = ViewConfiguration.get(mContext).getScaledTouchSlop();
671            Spannable buffer = getText();
672            int initialScrollX = Touch.getInitialScrollX(this, buffer);
673            int initialScrollY = Touch.getInitialScrollY(this, buffer);
674            super.onTouchEvent(event);
675            int dx = Math.abs(mScrollX - initialScrollX);
676            int dy = Math.abs(mScrollY - initialScrollY);
677            // Use a smaller slop when checking to see if we've moved far enough
678            // to scroll the text, because experimentally, slop has shown to be
679            // to big for the case of a small textfield.
680            int smallerSlop = slop/2;
681            if (dx > smallerSlop || dy > smallerSlop) {
682                // Scrolling is handled in onScrollChanged.
683                mScrolled = true;
684                cancelLongPress();
685                return true;
686            }
687            if (Math.abs((int) event.getX() - mDragStartX) < slop
688                    && Math.abs((int) event.getY() - mDragStartY) < slop) {
689                // If the user has not scrolled further than slop, we should not
690                // send the drag.  Instead, do nothing, and when the user lifts
691                // their finger, we will change the selection.
692                return true;
693            }
694            if (mWebView != null) {
695                // Only want to set the initial state once.
696                if (!mDragSent) {
697                    mWebView.initiateTextFieldDrag(mDragStartX, mDragStartY,
698                            mDragStartTime);
699                    mDragSent = true;
700                }
701                boolean scrolled = mWebView.textFieldDrag(event);
702                if (scrolled) {
703                    mScrolled = true;
704                    cancelLongPress();
705                    return true;
706                }
707            }
708            return false;
709        case MotionEvent.ACTION_UP:
710        case MotionEvent.ACTION_CANCEL:
711            super.onTouchEvent(event);
712            if (mHasPerformedLongClick) {
713                mGotTouchDown = false;
714                return false;
715            }
716            if (!mScrolled) {
717                // If the page scrolled, or the TextView scrolled, we do not
718                // want to change the selection
719                cancelLongPress();
720                if (mGotTouchDown && mWebView != null) {
721                    mWebView.touchUpOnTextField(event);
722                }
723            }
724            // Necessary for the WebView to reset its state
725            if (mWebView != null && mDragSent) {
726                mWebView.onTouchEvent(event);
727            }
728            mGotTouchDown = false;
729            break;
730        default:
731            break;
732        }
733        return true;
734    }
735
736    @Override
737    public boolean onTrackballEvent(MotionEvent event) {
738        if (isPopupShowing()) {
739            return super.onTrackballEvent(event);
740        }
741        if (event.getAction() != MotionEvent.ACTION_MOVE) {
742            return false;
743        }
744        Spannable text = getText();
745        MovementMethod move = getMovementMethod();
746        if (move != null && getLayout() != null &&
747            move.onTrackballEvent(this, text, event)) {
748            // Selection is changed in onSelectionChanged
749            return true;
750        }
751        return false;
752    }
753
754    @Override
755    public boolean performLongClick() {
756        mHasPerformedLongClick = true;
757        return super.performLongClick();
758    }
759
760    /**
761     * Remove this WebTextView from its host WebView, and return
762     * focus to the host.
763     */
764    /* package */ void remove() {
765        // hide the soft keyboard when the edit text is out of focus
766        InputMethodManager imm = InputMethodManager.getInstance(mContext);
767        if (imm.isActive(this)) {
768            imm.hideSoftInputFromWindow(getWindowToken(), 0);
769        }
770        mInsideRemove = true;
771        mWebView.removeView(this);
772        mWebView.requestFocus();
773        mInsideRemove = false;
774    }
775
776    @Override
777    public boolean requestRectangleOnScreen(Rect rectangle, boolean immediate) {
778        // Do nothing, since webkit will put the textfield on screen.
779        return true;
780    }
781
782    /**
783     *  Send the DOM events for the specified event.
784     *  @param event    KeyEvent to be translated into a DOM event.
785     */
786    private void sendDomEvent(KeyEvent event) {
787        mWebView.passToJavaScript(getText().toString(), event);
788    }
789
790    /**
791     *  Always use this instead of setAdapter, as this has features specific to
792     *  the WebTextView.
793     */
794    public void setAdapterCustom(AutoCompleteAdapter adapter) {
795        if (adapter != null) {
796            setInputType(getInputType()
797                    | InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE);
798            adapter.setTextView(this);
799            if (mAutoFillable) {
800                setOnItemClickListener(this);
801            } else {
802                setOnItemClickListener(null);
803            }
804            showDropDown();
805        } else {
806            dismissDropDown();
807        }
808        super.setAdapter(adapter);
809    }
810
811    /**
812     *  This is a special version of ArrayAdapter which changes its text size
813     *  to match the text size of its host TextView.
814     */
815    public static class AutoCompleteAdapter extends ArrayAdapter<String> {
816        private TextView mTextView;
817
818        public AutoCompleteAdapter(Context context, ArrayList<String> entries) {
819            super(context, com.android.internal.R.layout
820                    .web_text_view_dropdown, entries);
821        }
822
823        /**
824         * {@inheritDoc}
825         */
826        @Override
827        public View getView(int position, View convertView, ViewGroup parent) {
828            TextView tv =
829                    (TextView) super.getView(position, convertView, parent);
830            if (tv != null && mTextView != null) {
831                tv.setTextSize(mTextView.getTextSize());
832            }
833            return tv;
834        }
835
836        /**
837         * Set the TextView so we can match its text size.
838         */
839        private void setTextView(TextView tv) {
840            mTextView = tv;
841        }
842    }
843
844    /**
845     * Sets the selection when the user clicks on a textfield or textarea with
846     * the trackball or center key, or starts typing into it without clicking on
847     * it.
848     */
849    /* package */ void setDefaultSelection() {
850        Spannable text = (Spannable) getText();
851        int selection = mSingle ? text.length() : 0;
852        if (Selection.getSelectionStart(text) == selection
853                && Selection.getSelectionEnd(text) == selection) {
854            // The selection of the UI copy is set correctly, but the
855            // WebTextView still needs to inform the webkit thread to set the
856            // selection.  Normally that is done in onSelectionChanged, but
857            // onSelectionChanged will not be called because the UI copy is not
858            // changing.  (This can happen when the WebTextView takes focus.
859            // That onSelectionChanged was blocked because the selection set
860            // when focusing is not necessarily the desirable selection for
861            // WebTextView.)
862            if (mWebView != null) {
863                mWebView.setSelection(selection, selection);
864            }
865        } else {
866            Selection.setSelection(text, selection, selection);
867        }
868        if (mWebView != null) mWebView.incrementTextGeneration();
869    }
870
871    @Override
872    public void setInputType(int type) {
873        mFromSetInputType = true;
874        super.setInputType(type);
875        mFromSetInputType = false;
876    }
877
878    private void setMaxLength(int maxLength) {
879        mMaxLength = maxLength;
880        if (-1 == maxLength) {
881            setFilters(NO_FILTERS);
882        } else {
883            setFilters(new InputFilter[] {
884                new InputFilter.LengthFilter(maxLength) });
885        }
886    }
887
888    /**
889     *  Set the pointer for this node so it can be determined which node this
890     *  WebTextView represents.
891     *  @param  ptr Integer representing the pointer to the node which this
892     *          WebTextView represents.
893     */
894    /* package */ void setNodePointer(int ptr) {
895        mNodePointer = ptr;
896    }
897
898    /**
899     * Determine the position and size of WebTextView, and add it to the
900     * WebView's view heirarchy.  All parameters are presumed to be in
901     * view coordinates.  Also requests Focus and sets the cursor to not
902     * request to be in view.
903     * @param x         x-position of the textfield.
904     * @param y         y-position of the textfield.
905     * @param width     width of the textfield.
906     * @param height    height of the textfield.
907     */
908    /* package */ void setRect(int x, int y, int width, int height) {
909        LayoutParams lp = (LayoutParams) getLayoutParams();
910        if (null == lp) {
911            lp = new LayoutParams(width, height, x, y);
912        } else {
913            lp.x = x;
914            lp.y = y;
915            lp.width = width;
916            lp.height = height;
917        }
918        if (getParent() == null) {
919            // Insert the view so that it's drawn first (at index 0)
920            mWebView.addView(this, 0, lp);
921        } else {
922            setLayoutParams(lp);
923        }
924        // Set up a measure spec so a layout can always be recreated.
925        mWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
926        mHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
927    }
928
929    /**
930     * Set the selection, and disable our onSelectionChanged action.
931     */
932    /* package */ void setSelectionFromWebKit(int start, int end) {
933        if (start < 0 || end < 0) return;
934        Spannable text = (Spannable) getText();
935        int length = text.length();
936        if (start > length || end > length) return;
937        mFromWebKit = true;
938        Selection.setSelection(text, start, end);
939        mFromWebKit = false;
940    }
941
942    /**
943     * Update the text size according to the size of the focus candidate's text
944     * size in mWebView.  Should only be called from mWebView.
945     */
946    /* package */ void updateTextSize() {
947        Assert.assertNotNull("updateTextSize should only be called from "
948                + "mWebView, so mWebView should never be null!", mWebView);
949        // Note that this is approximately WebView.contentToViewDimension,
950        // without being rounded.
951        float size = mWebView.nativeFocusCandidateTextSize()
952                * mWebView.getScale();
953        setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
954    }
955
956    /**
957     * Set the text to the new string, but use the old selection, making sure
958     * to keep it within the new string.
959     * @param   text    The new text to place in the textfield.
960     */
961    /* package */ void setTextAndKeepSelection(String text) {
962        mPreChange = text.toString();
963        Editable edit = getText();
964        int selStart = Selection.getSelectionStart(edit);
965        int selEnd = Selection.getSelectionEnd(edit);
966        mInSetTextAndKeepSelection = true;
967        edit.replace(0, edit.length(), text);
968        int newLength = edit.length();
969        if (selStart > newLength) selStart = newLength;
970        if (selEnd > newLength) selEnd = newLength;
971        Selection.setSelection(edit, selStart, selEnd);
972        mInSetTextAndKeepSelection = false;
973        InputMethodManager imm = InputMethodManager.peekInstance();
974        if (imm != null && imm.isActive(this)) {
975            // Since the text has changed, do not allow the IME to replace the
976            // existing text as though it were a completion.
977            imm.restartInput(this);
978        }
979        updateCachedTextfield();
980    }
981
982    /**
983     * Called by WebView.rebuildWebTextView().  Based on the type of the <input>
984     * element, set up the WebTextView, its InputType, and IME Options properly.
985     * @param type int corresponding to enum "Type" defined in CachedInput.h.
986     *              Does not correspond to HTMLInputElement::InputType so this
987     *              is unaffected if that changes, and also because that has no
988     *              type corresponding to textarea (which is its own tag).
989     */
990    /* package */ void setType(int type) {
991        if (mWebView == null) return;
992        boolean single = true;
993        int maxLength = -1;
994        int inputType = InputType.TYPE_CLASS_TEXT
995                | InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT;
996        int imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
997                | EditorInfo.IME_FLAG_NO_FULLSCREEN;
998        if (TEXT_AREA != type
999                && mWebView.nativeFocusCandidateHasNextTextfield()) {
1000            imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT;
1001        }
1002        switch (type) {
1003            case NORMAL_TEXT_FIELD:
1004                imeOptions |= EditorInfo.IME_ACTION_GO;
1005                break;
1006            case TEXT_AREA:
1007                single = false;
1008                inputType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE
1009                        | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
1010                        | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
1011                imeOptions |= EditorInfo.IME_ACTION_NONE;
1012                break;
1013            case PASSWORD:
1014                inputType |= EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
1015                imeOptions |= EditorInfo.IME_ACTION_GO;
1016                break;
1017            case SEARCH:
1018                imeOptions |= EditorInfo.IME_ACTION_SEARCH;
1019                break;
1020            case EMAIL:
1021                // inputType needs to be overwritten because of the different text variation.
1022                inputType = InputType.TYPE_CLASS_TEXT
1023                        | InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
1024                imeOptions |= EditorInfo.IME_ACTION_GO;
1025                break;
1026            case NUMBER:
1027                // inputType needs to be overwritten because of the different class.
1028                inputType = InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_NORMAL
1029                        | InputType.TYPE_NUMBER_FLAG_SIGNED | InputType.TYPE_NUMBER_FLAG_DECIMAL;
1030                // Number and telephone do not have both a Tab key and an
1031                // action, so set the action to NEXT
1032                imeOptions |= EditorInfo.IME_ACTION_NEXT;
1033                break;
1034            case TELEPHONE:
1035                // inputType needs to be overwritten because of the different class.
1036                inputType = InputType.TYPE_CLASS_PHONE;
1037                imeOptions |= EditorInfo.IME_ACTION_NEXT;
1038                break;
1039            case URL:
1040                // TYPE_TEXT_VARIATION_URI prevents Tab key from showing, so
1041                // exclude it for now.
1042                imeOptions |= EditorInfo.IME_ACTION_GO;
1043                break;
1044            default:
1045                imeOptions |= EditorInfo.IME_ACTION_GO;
1046                break;
1047        }
1048        setHint(null);
1049        setThreshold(1);
1050        if (single) {
1051            mWebView.requestLabel(mWebView.nativeFocusCandidateFramePointer(),
1052                    mNodePointer);
1053            maxLength = mWebView.nativeFocusCandidateMaxLength();
1054            boolean autoComplete = mWebView.nativeFocusCandidateIsAutoComplete();
1055            if (type != PASSWORD && (mAutoFillable || autoComplete)) {
1056                String name = mWebView.nativeFocusCandidateName();
1057                if (name != null && name.length() > 0) {
1058                    mWebView.requestFormData(name, mNodePointer, mAutoFillable,
1059                            autoComplete);
1060                }
1061            }
1062        }
1063        mSingle = single;
1064        setMaxLength(maxLength);
1065        setHorizontallyScrolling(single);
1066        setInputType(inputType);
1067        setImeOptions(imeOptions);
1068        setVisibility(VISIBLE);
1069        AutoCompleteAdapter adapter = null;
1070        setAdapterCustom(adapter);
1071    }
1072
1073    /**
1074     *  Update the cache to reflect the current text.
1075     */
1076    /* package */ void updateCachedTextfield() {
1077        mWebView.updateCachedTextfield(getText().toString());
1078    }
1079
1080    /* package */ void setAutoFillProfileIsSet(boolean autoFillProfileIsSet) {
1081        mAutoFillProfileIsSet = autoFillProfileIsSet;
1082    }
1083
1084    static String urlForAutoCompleteData(String urlString) {
1085        // Remove any fragment or query string.
1086        URL url = null;
1087        try {
1088            url = new URL(urlString);
1089        } catch (MalformedURLException e) {
1090            Log.e(LOGTAG, "Unable to parse URL "+url);
1091        }
1092
1093        return url != null ? url.getProtocol() + "://" + url.getHost() + url.getPath() : null;
1094    }
1095}
1096