Home | History | Annotate | Download | only in webkit
      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 
     17 package android.webkit;
     18 
     19 import com.android.internal.widget.EditableInputConnection;
     20 
     21 import android.content.Context;
     22 import android.graphics.Canvas;
     23 import android.graphics.Color;
     24 import android.graphics.ColorFilter;
     25 import android.graphics.Paint;
     26 import android.graphics.PixelFormat;
     27 import android.graphics.Rect;
     28 import android.graphics.drawable.ColorDrawable;
     29 import android.os.Bundle;
     30 import android.os.Handler;
     31 import android.os.Message;
     32 import android.os.ResultReceiver;
     33 import android.text.BoringLayout.Metrics;
     34 import android.text.DynamicLayout;
     35 import android.text.Editable;
     36 import android.text.InputFilter;
     37 import android.text.InputType;
     38 import android.text.Layout;
     39 import android.text.Selection;
     40 import android.text.Spannable;
     41 import android.text.TextPaint;
     42 import android.text.TextUtils;
     43 import android.text.method.MovementMethod;
     44 import android.text.method.Touch;
     45 import android.util.Log;
     46 import android.util.TypedValue;
     47 import android.view.Gravity;
     48 import android.view.KeyCharacterMap;
     49 import android.view.KeyEvent;
     50 import android.view.MotionEvent;
     51 import android.view.View;
     52 import android.view.ViewConfiguration;
     53 import android.view.ViewGroup;
     54 import android.view.inputmethod.EditorInfo;
     55 import android.view.inputmethod.InputConnection;
     56 import android.view.inputmethod.InputMethodManager;
     57 import android.widget.AbsoluteLayout.LayoutParams;
     58 import android.widget.AdapterView;
     59 import android.widget.ArrayAdapter;
     60 import android.widget.AutoCompleteTextView;
     61 import android.widget.TextView;
     62 
     63 import java.net.MalformedURLException;
     64 import java.net.URL;
     65 import java.util.ArrayList;
     66 
     67 import 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         // Prefer sending javascript events, so when adding one character,
    611         // don't replace the unchanged text.
    612         if (count > 1 && before == count - 1) {
    613             String replaceButOne =  s.subSequence(start,
    614                     start + before).toString();
    615             String replacedString = getText().subSequence(start,
    616                     start + before).toString();
    617             if (replaceButOne.equals(replacedString)) {
    618                 // we're just adding one character
    619                 start += before;
    620                 before = 0;
    621                 count = 1;
    622             }
    623         }
    624         // Find the last character being replaced.  If it can be represented by
    625         // events, we will pass them to native so we can see javascript events.
    626         // Otherwise, replace the text being changed in the textfield.
    627         KeyEvent[] events = null;
    628         if (count == 1) {
    629             TextUtils.getChars(s, start + count - 1, start + count, mCharacter, 0);
    630             KeyCharacterMap kmap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
    631             events = kmap.getEvents(mCharacter);
    632         }
    633         boolean useKeyEvents = (events != null);
    634         if (useKeyEvents) {
    635             // This corrects the selection which may have been affected by the
    636             // trackball or auto-correct.
    637             if (DebugFlags.WEB_TEXT_VIEW) {
    638                 Log.v(LOGTAG, "onTextChanged start=" + start
    639                         + " start + before=" + (start + before));
    640             }
    641             if (!mInSetTextAndKeepSelection) {
    642                 mWebView.setSelection(start, start + before);
    643             }
    644             int length = events.length;
    645             for (int i = 0; i < length; i++) {
    646                 // We never send modifier keys to native code so don't send them
    647                 // here either.
    648                 if (!KeyEvent.isModifierKey(events[i].getKeyCode())) {
    649                     sendDomEvent(events[i]);
    650                 }
    651             }
    652         } else {
    653             String replace = s.subSequence(start,
    654                     start + count).toString();
    655             mWebView.replaceTextfieldText(start, start + before, replace,
    656                     start + count,
    657                     start + count);
    658         }
    659         updateCachedTextfield();
    660     }
    661 
    662     @Override
    663     public boolean onTouchEvent(MotionEvent event) {
    664         switch (event.getAction()) {
    665         case MotionEvent.ACTION_DOWN:
    666             super.onTouchEvent(event);
    667             // This event may be the start of a drag, so store it to pass to the
    668             // WebView if it is.
    669             mDragStartX = event.getX();
    670             mDragStartY = event.getY();
    671             mDragStartTime = event.getEventTime();
    672             mDragSent = false;
    673             mScrolled = false;
    674             mGotTouchDown = true;
    675             mHasPerformedLongClick = false;
    676             break;
    677         case MotionEvent.ACTION_MOVE:
    678             if (mHasPerformedLongClick) {
    679                 mGotTouchDown = false;
    680                 return false;
    681             }
    682             int slop = ViewConfiguration.get(mContext).getScaledTouchSlop();
    683             Spannable buffer = getText();
    684             int initialScrollX = Touch.getInitialScrollX(this, buffer);
    685             int initialScrollY = Touch.getInitialScrollY(this, buffer);
    686             super.onTouchEvent(event);
    687             int dx = Math.abs(mScrollX - initialScrollX);
    688             int dy = Math.abs(mScrollY - initialScrollY);
    689             // Use a smaller slop when checking to see if we've moved far enough
    690             // to scroll the text, because experimentally, slop has shown to be
    691             // to big for the case of a small textfield.
    692             int smallerSlop = slop/2;
    693             if (dx > smallerSlop || dy > smallerSlop) {
    694                 // Scrolling is handled in onScrollChanged.
    695                 mScrolled = true;
    696                 cancelLongPress();
    697                 return true;
    698             }
    699             if (Math.abs((int) event.getX() - mDragStartX) < slop
    700                     && Math.abs((int) event.getY() - mDragStartY) < slop) {
    701                 // If the user has not scrolled further than slop, we should not
    702                 // send the drag.  Instead, do nothing, and when the user lifts
    703                 // their finger, we will change the selection.
    704                 return true;
    705             }
    706             if (mWebView != null) {
    707                 // Only want to set the initial state once.
    708                 if (!mDragSent) {
    709                     mWebView.initiateTextFieldDrag(mDragStartX, mDragStartY,
    710                             mDragStartTime);
    711                     mDragSent = true;
    712                 }
    713                 boolean scrolled = mWebView.textFieldDrag(event);
    714                 if (scrolled) {
    715                     mScrolled = true;
    716                     cancelLongPress();
    717                     return true;
    718                 }
    719             }
    720             return false;
    721         case MotionEvent.ACTION_UP:
    722         case MotionEvent.ACTION_CANCEL:
    723             super.onTouchEvent(event);
    724             if (mHasPerformedLongClick) {
    725                 mGotTouchDown = false;
    726                 return false;
    727             }
    728             if (!mScrolled) {
    729                 // If the page scrolled, or the TextView scrolled, we do not
    730                 // want to change the selection
    731                 cancelLongPress();
    732                 if (mGotTouchDown && mWebView != null) {
    733                     mWebView.touchUpOnTextField(event);
    734                 }
    735             }
    736             // Necessary for the WebView to reset its state
    737             if (mWebView != null && mDragSent) {
    738                 mWebView.onTouchEvent(event);
    739             }
    740             mGotTouchDown = false;
    741             break;
    742         default:
    743             break;
    744         }
    745         return true;
    746     }
    747 
    748     @Override
    749     public boolean onTrackballEvent(MotionEvent event) {
    750         if (isPopupShowing()) {
    751             return super.onTrackballEvent(event);
    752         }
    753         if (event.getAction() != MotionEvent.ACTION_MOVE) {
    754             return false;
    755         }
    756         Spannable text = getText();
    757         MovementMethod move = getMovementMethod();
    758         if (move != null && getLayout() != null &&
    759             move.onTrackballEvent(this, text, event)) {
    760             // Selection is changed in onSelectionChanged
    761             return true;
    762         }
    763         return false;
    764     }
    765 
    766     @Override
    767     public boolean performLongClick() {
    768         mHasPerformedLongClick = true;
    769         return super.performLongClick();
    770     }
    771 
    772     /**
    773      * Remove this WebTextView from its host WebView, and return
    774      * focus to the host.
    775      */
    776     /* package */ void remove() {
    777         // hide the soft keyboard when the edit text is out of focus
    778         InputMethodManager imm = InputMethodManager.getInstance(mContext);
    779         if (imm.isActive(this)) {
    780             imm.hideSoftInputFromWindow(getWindowToken(), 0);
    781         }
    782         mInsideRemove = true;
    783         boolean isFocused = hasFocus();
    784         mWebView.removeView(this);
    785         if (isFocused) {
    786             mWebView.requestFocus();
    787         }
    788         mInsideRemove = false;
    789         mHandler.removeCallbacksAndMessages(null);
    790     }
    791 
    792     @Override
    793     public boolean requestRectangleOnScreen(Rect rectangle, boolean immediate) {
    794         // Do nothing, since webkit will put the textfield on screen.
    795         return true;
    796     }
    797 
    798     /**
    799      *  Send the DOM events for the specified event.
    800      *  @param event    KeyEvent to be translated into a DOM event.
    801      */
    802     private void sendDomEvent(KeyEvent event) {
    803         mWebView.passToJavaScript(getText().toString(), event);
    804     }
    805 
    806     /**
    807      *  Always use this instead of setAdapter, as this has features specific to
    808      *  the WebTextView.
    809      */
    810     public void setAdapterCustom(AutoCompleteAdapter adapter) {
    811         if (adapter != null) {
    812             setInputType(getInputType()
    813                     | InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE);
    814             adapter.setTextView(this);
    815             if (mAutoFillable) {
    816                 setOnItemClickListener(this);
    817             } else {
    818                 setOnItemClickListener(null);
    819             }
    820             showDropDown();
    821         } else {
    822             dismissDropDown();
    823         }
    824         super.setAdapter(adapter);
    825     }
    826 
    827     /**
    828      *  This is a special version of ArrayAdapter which changes its text size
    829      *  to match the text size of its host TextView.
    830      */
    831     public static class AutoCompleteAdapter extends ArrayAdapter<String> {
    832         private TextView mTextView;
    833 
    834         public AutoCompleteAdapter(Context context, ArrayList<String> entries) {
    835             super(context, com.android.internal.R.layout
    836                     .web_text_view_dropdown, entries);
    837         }
    838 
    839         /**
    840          * {@inheritDoc}
    841          */
    842         @Override
    843         public View getView(int position, View convertView, ViewGroup parent) {
    844             TextView tv =
    845                     (TextView) super.getView(position, convertView, parent);
    846             if (tv != null && mTextView != null) {
    847                 tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextView.getTextSize());
    848             }
    849             return tv;
    850         }
    851 
    852         /**
    853          * Set the TextView so we can match its text size.
    854          */
    855         private void setTextView(TextView tv) {
    856             mTextView = tv;
    857         }
    858     }
    859 
    860     /**
    861      * Sets the selection when the user clicks on a textfield or textarea with
    862      * the trackball or center key, or starts typing into it without clicking on
    863      * it.
    864      */
    865     /* package */ void setDefaultSelection() {
    866         Spannable text = (Spannable) getText();
    867         int selection = mSingle ? text.length() : 0;
    868         if (Selection.getSelectionStart(text) == selection
    869                 && Selection.getSelectionEnd(text) == selection) {
    870             // The selection of the UI copy is set correctly, but the
    871             // WebTextView still needs to inform the webkit thread to set the
    872             // selection.  Normally that is done in onSelectionChanged, but
    873             // onSelectionChanged will not be called because the UI copy is not
    874             // changing.  (This can happen when the WebTextView takes focus.
    875             // That onSelectionChanged was blocked because the selection set
    876             // when focusing is not necessarily the desirable selection for
    877             // WebTextView.)
    878             if (mWebView != null) {
    879                 mWebView.setSelection(selection, selection);
    880             }
    881         } else {
    882             Selection.setSelection(text, selection, selection);
    883         }
    884         if (mWebView != null) mWebView.incrementTextGeneration();
    885     }
    886 
    887     @Override
    888     public void setInputType(int type) {
    889         mFromSetInputType = true;
    890         super.setInputType(type);
    891         mFromSetInputType = false;
    892     }
    893 
    894     private void setMaxLength(int maxLength) {
    895         mMaxLength = maxLength;
    896         if (-1 == maxLength) {
    897             setFilters(NO_FILTERS);
    898         } else {
    899             setFilters(new InputFilter[] {
    900                 new InputFilter.LengthFilter(maxLength) });
    901         }
    902     }
    903 
    904     /**
    905      *  Set the pointer for this node so it can be determined which node this
    906      *  WebTextView represents.
    907      *  @param  ptr Integer representing the pointer to the node which this
    908      *          WebTextView represents.
    909      */
    910     /* package */ void setNodePointer(int ptr) {
    911         if (ptr != mNodePointer) {
    912             mNodePointer = ptr;
    913             setAdapterCustom(null);
    914         }
    915     }
    916 
    917     /**
    918      * Determine the position and size of WebTextView, and add it to the
    919      * WebView's view heirarchy.  All parameters are presumed to be in
    920      * view coordinates.  Also requests Focus and sets the cursor to not
    921      * request to be in view.
    922      * @param x         x-position of the textfield.
    923      * @param y         y-position of the textfield.
    924      * @param width     width of the textfield.
    925      * @param height    height of the textfield.
    926      */
    927     /* package */ void setRect(int x, int y, int width, int height) {
    928         LayoutParams lp = (LayoutParams) getLayoutParams();
    929         if (null == lp) {
    930             lp = new LayoutParams(width, height, x, y);
    931         } else {
    932             lp.x = x;
    933             lp.y = y;
    934             lp.width = width;
    935             lp.height = height;
    936         }
    937         if (getParent() == null) {
    938             // Insert the view so that it's drawn first (at index 0)
    939             mWebView.addView(this, 0, lp);
    940         } else {
    941             setLayoutParams(lp);
    942         }
    943         // Set up a measure spec so a layout can always be recreated.
    944         mWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
    945         mHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
    946     }
    947 
    948     /**
    949      * Set the selection, and disable our onSelectionChanged action.
    950      */
    951     /* package */ void setSelectionFromWebKit(int start, int end) {
    952         if (start < 0 || end < 0) return;
    953         Spannable text = (Spannable) getText();
    954         int length = text.length();
    955         if (start > length || end > length) return;
    956         mFromWebKit = true;
    957         Selection.setSelection(text, start, end);
    958         mFromWebKit = false;
    959     }
    960 
    961     /**
    962      * Update the text size according to the size of the focus candidate's text
    963      * size in mWebView.  Should only be called from mWebView.
    964      */
    965     /* package */ void updateTextSize() {
    966         Assert.assertNotNull("updateTextSize should only be called from "
    967                 + "mWebView, so mWebView should never be null!", mWebView);
    968         // Note that this is approximately WebView.contentToViewDimension,
    969         // without being rounded.
    970         float size = mWebView.nativeFocusCandidateTextSize()
    971                 * mWebView.getScale();
    972         setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
    973     }
    974 
    975     /**
    976      * Set the text to the new string, but use the old selection, making sure
    977      * to keep it within the new string.
    978      * @param   text    The new text to place in the textfield.
    979      */
    980     /* package */ void setTextAndKeepSelection(String text) {
    981         Editable edit = getText();
    982         mPreChange = text;
    983         if (edit.toString().equals(text)) {
    984             return;
    985         }
    986         int selStart = Selection.getSelectionStart(edit);
    987         int selEnd = Selection.getSelectionEnd(edit);
    988         mInSetTextAndKeepSelection = true;
    989         edit.replace(0, edit.length(), text);
    990         int newLength = edit.length();
    991         if (selStart > newLength) selStart = newLength;
    992         if (selEnd > newLength) selEnd = newLength;
    993         Selection.setSelection(edit, selStart, selEnd);
    994         mInSetTextAndKeepSelection = false;
    995         InputMethodManager imm = InputMethodManager.peekInstance();
    996         if (imm != null && imm.isActive(this)) {
    997             // Since the text has changed, do not allow the IME to replace the
    998             // existing text as though it were a completion.
    999             imm.restartInput(this);
   1000         }
   1001         updateCachedTextfield();
   1002     }
   1003 
   1004     /**
   1005      * Called by WebView.rebuildWebTextView().  Based on the type of the <input>
   1006      * element, set up the WebTextView, its InputType, and IME Options properly.
   1007      * @param type int corresponding to enum "Type" defined in CachedInput.h.
   1008      *              Does not correspond to HTMLInputElement::InputType so this
   1009      *              is unaffected if that changes, and also because that has no
   1010      *              type corresponding to textarea (which is its own tag).
   1011      */
   1012     /* package */ void setType(int type) {
   1013         if (mWebView == null) return;
   1014         boolean single = true;
   1015         int maxLength = -1;
   1016         int inputType = InputType.TYPE_CLASS_TEXT
   1017                 | InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT;
   1018         int imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
   1019                 | EditorInfo.IME_FLAG_NO_FULLSCREEN;
   1020         if (!mWebView.nativeFocusCandidateIsSpellcheck()) {
   1021             inputType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
   1022         }
   1023         if (TEXT_AREA != type
   1024                 && mWebView.nativeFocusCandidateHasNextTextfield()) {
   1025             imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT;
   1026         }
   1027         switch (type) {
   1028             case NORMAL_TEXT_FIELD:
   1029                 imeOptions |= EditorInfo.IME_ACTION_GO;
   1030                 break;
   1031             case TEXT_AREA:
   1032                 single = false;
   1033                 inputType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE
   1034                         | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
   1035                         | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
   1036                 imeOptions |= EditorInfo.IME_ACTION_NONE;
   1037                 break;
   1038             case PASSWORD:
   1039                 inputType |= EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
   1040                 imeOptions |= EditorInfo.IME_ACTION_GO;
   1041                 break;
   1042             case SEARCH:
   1043                 imeOptions |= EditorInfo.IME_ACTION_SEARCH;
   1044                 break;
   1045             case EMAIL:
   1046                 // inputType needs to be overwritten because of the different text variation.
   1047                 inputType = InputType.TYPE_CLASS_TEXT
   1048                         | InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
   1049                 imeOptions |= EditorInfo.IME_ACTION_GO;
   1050                 break;
   1051             case NUMBER:
   1052                 // inputType needs to be overwritten because of the different class.
   1053                 inputType = InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_NORMAL
   1054                         | InputType.TYPE_NUMBER_FLAG_SIGNED | InputType.TYPE_NUMBER_FLAG_DECIMAL;
   1055                 // Number and telephone do not have both a Tab key and an
   1056                 // action, so set the action to NEXT
   1057                 imeOptions |= EditorInfo.IME_ACTION_NEXT;
   1058                 break;
   1059             case TELEPHONE:
   1060                 // inputType needs to be overwritten because of the different class.
   1061                 inputType = InputType.TYPE_CLASS_PHONE;
   1062                 imeOptions |= EditorInfo.IME_ACTION_NEXT;
   1063                 break;
   1064             case URL:
   1065                 // TYPE_TEXT_VARIATION_URI prevents Tab key from showing, so
   1066                 // exclude it for now.
   1067                 imeOptions |= EditorInfo.IME_ACTION_GO;
   1068                 break;
   1069             default:
   1070                 imeOptions |= EditorInfo.IME_ACTION_GO;
   1071                 break;
   1072         }
   1073         setHint(null);
   1074         setThreshold(1);
   1075         boolean autoComplete = false;
   1076         if (single) {
   1077             mWebView.requestLabel(mWebView.nativeFocusCandidateFramePointer(),
   1078                     mNodePointer);
   1079             maxLength = mWebView.nativeFocusCandidateMaxLength();
   1080             autoComplete = mWebView.nativeFocusCandidateIsAutoComplete();
   1081             if (type != PASSWORD && (mAutoFillable || autoComplete)) {
   1082                 String name = mWebView.nativeFocusCandidateName();
   1083                 if (name != null && name.length() > 0) {
   1084                     mWebView.requestFormData(name, mNodePointer, mAutoFillable,
   1085                             autoComplete);
   1086                 }
   1087             }
   1088         }
   1089         mSingle = single;
   1090         setMaxLength(maxLength);
   1091         setHorizontallyScrolling(single);
   1092         setInputType(inputType);
   1093         clearComposingText();
   1094         setImeOptions(imeOptions);
   1095         setVisibility(VISIBLE);
   1096         if (!autoComplete) {
   1097             setAdapterCustom(null);
   1098         }
   1099     }
   1100 
   1101     /**
   1102      *  Update the cache to reflect the current text.
   1103      */
   1104     /* package */ void updateCachedTextfield() {
   1105         mWebView.updateCachedTextfield(getText().toString());
   1106     }
   1107 
   1108     /* package */ void setAutoFillProfileIsSet(boolean autoFillProfileIsSet) {
   1109         mAutoFillProfileIsSet = autoFillProfileIsSet;
   1110     }
   1111 
   1112     static String urlForAutoCompleteData(String urlString) {
   1113         // Remove any fragment or query string.
   1114         URL url = null;
   1115         try {
   1116             url = new URL(urlString);
   1117         } catch (MalformedURLException e) {
   1118             Log.e(LOGTAG, "Unable to parse URL "+url);
   1119         }
   1120 
   1121         return url != null ? url.getProtocol() + "://" + url.getHost() + url.getPath() : null;
   1122     }
   1123 
   1124     public void setGravityForRtl(boolean rtl) {
   1125         int gravity = rtl ? Gravity.RIGHT : Gravity.LEFT;
   1126         gravity |= mSingle ? Gravity.CENTER_VERTICAL : Gravity.TOP;
   1127         setGravity(gravity);
   1128     }
   1129 
   1130 }
   1131