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