• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
onReceiveResult(int resultCode, Bundle resultData)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          */
MyResultReceiver(Handler handler)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      */
WebTextView(Context context, WebView webView, int autoFillQueryId)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 
setAutoFillable(int queryId)212     public void setAutoFillable(int queryId) {
213         mAutoFillable = mWebView.getSettings().getAutoFillEnabled()
214                 && (queryId != FORM_NOT_AUTOFILLABLE);
215         mQueryId = queryId;
216     }
217 
218     @Override
dispatchKeyEvent(KeyEvent event)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 
ensureLayout()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 
getResultReceiver()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      */
isSameTextField(int ptr)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      */
lineUpScroll()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
makeNewLayout(int w, int hintWidth, Metrics boring, Metrics hintBoring, int ellipsisWidth, boolean bringIntoView)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;
WebTextViewLayout(CharSequence base, CharSequence display, TextPaint paint, int width, Alignment align, float spacingMult, float spacingAdd, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth, float lineHeight)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
getLineTop(int line)462         public int getLineTop(int line) {
463             return Math.round(mLineHeight * line - mDifference);
464         }
465     }
466 
onCreateInputConnection( EditorInfo outAttrs)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
onEditorAction(int actionCode)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
onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)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
onItemClick(AdapterView<?> parent, View view, int position, long id)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
onScrollChanged(int l, int t, int oldl, int oldt)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
onSelectionChanged(int selStart, int selEnd)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
onTextChanged(CharSequence s,int start,int before,int count)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
onTouchEvent(MotionEvent event)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
onTrackballEvent(MotionEvent event)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
performLongClick()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      */
remove()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
requestRectangleOnScreen(Rect rectangle, boolean immediate)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      */
sendDomEvent(KeyEvent event)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      */
setAdapterCustom(AutoCompleteAdapter adapter)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 
AutoCompleteAdapter(Context context, ArrayList<String> entries)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
getView(int position, View convertView, ViewGroup parent)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          */
setTextView(TextView tv)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      */
setDefaultSelection()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
setInputType(int type)888     public void setInputType(int type) {
889         mFromSetInputType = true;
890         super.setInputType(type);
891         mFromSetInputType = false;
892     }
893 
setMaxLength(int maxLength)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      */
setNodePointer(int ptr)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      */
setRect(int x, int y, int width, int height)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      */
setSelectionFromWebKit(int start, int end)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      */
updateTextSize()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      */
setTextAndKeepSelection(String text)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      */
setType(int type)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      */
updateCachedTextfield()1104     /* package */ void updateCachedTextfield() {
1105         mWebView.updateCachedTextfield(getText().toString());
1106     }
1107 
setAutoFillProfileIsSet(boolean autoFillProfileIsSet)1108     /* package */ void setAutoFillProfileIsSet(boolean autoFillProfileIsSet) {
1109         mAutoFillProfileIsSet = autoFillProfileIsSet;
1110     }
1111 
urlForAutoCompleteData(String urlString)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 
setGravityForRtl(boolean rtl)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