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