1 // Copyright 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.content.browser.input; 6 7 import android.os.Handler; 8 import android.os.ResultReceiver; 9 import android.os.SystemClock; 10 import android.text.Editable; 11 import android.text.SpannableString; 12 import android.text.style.BackgroundColorSpan; 13 import android.text.style.CharacterStyle; 14 import android.text.style.UnderlineSpan; 15 import android.view.KeyCharacterMap; 16 import android.view.KeyEvent; 17 import android.view.View; 18 import android.view.inputmethod.EditorInfo; 19 20 import com.google.common.annotations.VisibleForTesting; 21 22 import java.lang.CharSequence; 23 24 import org.chromium.base.CalledByNative; 25 import org.chromium.base.JNINamespace; 26 27 /** 28 * Adapts and plumbs android IME service onto the chrome text input API. 29 * ImeAdapter provides an interface in both ways native <-> java: 30 * 1. InputConnectionAdapter notifies native code of text composition state and 31 * dispatch key events from java -> WebKit. 32 * 2. Native ImeAdapter notifies java side to clear composition text. 33 * 34 * The basic flow is: 35 * 1. When InputConnectionAdapter gets called with composition or result text: 36 * If we receive a composition text or a result text, then we just need to 37 * dispatch a synthetic key event with special keycode 229, and then dispatch 38 * the composition or result text. 39 * 2. Intercept dispatchKeyEvent() method for key events not handled by IME, we 40 * need to dispatch them to webkit and check webkit's reply. Then inject a 41 * new key event for further processing if webkit didn't handle it. 42 * 43 * Note that the native peer object does not take any strong reference onto the 44 * instance of this java object, hence it is up to the client of this class (e.g. 45 * the ViewEmbedder implementor) to hold a strong reference to it for the required 46 * lifetime of the object. 47 */ 48 @JNINamespace("content") 49 public class ImeAdapter { 50 51 /** 52 * Interface for the delegate that needs to be notified of IME changes. 53 */ 54 public interface ImeAdapterDelegate { 55 /** 56 * @param isFinish whether the event is occurring because input is finished. 57 */ 58 void onImeEvent(boolean isFinish); 59 60 /** 61 * Called when a request to hide the keyboard is sent to InputMethodManager. 62 */ 63 void onDismissInput(); 64 65 /** 66 * @return View that the keyboard should be attached to. 67 */ 68 View getAttachedView(); 69 70 /** 71 * @return Object that should be called for all keyboard show and hide requests. 72 */ 73 ResultReceiver getNewShowKeyboardReceiver(); 74 } 75 76 private class DelayedDismissInput implements Runnable { 77 private final long mNativeImeAdapter; 78 79 DelayedDismissInput(long nativeImeAdapter) { 80 mNativeImeAdapter = nativeImeAdapter; 81 } 82 83 @Override 84 public void run() { 85 attach(mNativeImeAdapter, sTextInputTypeNone); 86 dismissInput(true); 87 } 88 } 89 90 private static final int COMPOSITION_KEY_CODE = 229; 91 92 // Delay introduced to avoid hiding the keyboard if new show requests are received. 93 // The time required by the unfocus-focus events triggered by tab has been measured in soju: 94 // Mean: 18.633 ms, Standard deviation: 7.9837 ms. 95 // The value here should be higher enough to cover these cases, but not too high to avoid 96 // letting the user perceiving important delays. 97 private static final int INPUT_DISMISS_DELAY = 150; 98 99 // All the constants that are retrieved from the C++ code. 100 // They get set through initializeWebInputEvents and initializeTextInputTypes calls. 101 static int sEventTypeRawKeyDown; 102 static int sEventTypeKeyUp; 103 static int sEventTypeChar; 104 static int sTextInputTypeNone; 105 static int sTextInputTypeText; 106 static int sTextInputTypeTextArea; 107 static int sTextInputTypePassword; 108 static int sTextInputTypeSearch; 109 static int sTextInputTypeUrl; 110 static int sTextInputTypeEmail; 111 static int sTextInputTypeTel; 112 static int sTextInputTypeNumber; 113 static int sTextInputTypeContentEditable; 114 static int sModifierShift; 115 static int sModifierAlt; 116 static int sModifierCtrl; 117 static int sModifierCapsLockOn; 118 static int sModifierNumLockOn; 119 120 private long mNativeImeAdapterAndroid; 121 private InputMethodManagerWrapper mInputMethodManagerWrapper; 122 private AdapterInputConnection mInputConnection; 123 private final ImeAdapterDelegate mViewEmbedder; 124 private final Handler mHandler; 125 private DelayedDismissInput mDismissInput = null; 126 private int mTextInputType; 127 128 @VisibleForTesting 129 boolean mIsShowWithoutHideOutstanding = false; 130 131 /** 132 * @param wrapper InputMethodManagerWrapper that should receive all the call directed to 133 * InputMethodManager. 134 * @param embedder The view that is used for callbacks from ImeAdapter. 135 */ 136 public ImeAdapter(InputMethodManagerWrapper wrapper, ImeAdapterDelegate embedder) { 137 mInputMethodManagerWrapper = wrapper; 138 mViewEmbedder = embedder; 139 mHandler = new Handler(); 140 } 141 142 /** 143 * Default factory for AdapterInputConnection classes. 144 */ 145 public static class AdapterInputConnectionFactory { 146 public AdapterInputConnection get(View view, ImeAdapter imeAdapter, 147 Editable editable, EditorInfo outAttrs) { 148 return new AdapterInputConnection(view, imeAdapter, editable, outAttrs); 149 } 150 } 151 152 /** 153 * Overrides the InputMethodManagerWrapper that ImeAdapter uses to make calls to 154 * InputMethodManager. 155 * @param immw InputMethodManagerWrapper that should be used to call InputMethodManager. 156 */ 157 @VisibleForTesting 158 public void setInputMethodManagerWrapper(InputMethodManagerWrapper immw) { 159 mInputMethodManagerWrapper = immw; 160 } 161 162 /** 163 * Should be only used by AdapterInputConnection. 164 * @return InputMethodManagerWrapper that should receive all the calls directed to 165 * InputMethodManager. 166 */ 167 InputMethodManagerWrapper getInputMethodManagerWrapper() { 168 return mInputMethodManagerWrapper; 169 } 170 171 /** 172 * Set the current active InputConnection when a new InputConnection is constructed. 173 * @param inputConnection The input connection that is currently used with IME. 174 */ 175 void setInputConnection(AdapterInputConnection inputConnection) { 176 mInputConnection = inputConnection; 177 } 178 179 /** 180 * Should be only used by AdapterInputConnection. 181 * @return The input type of currently focused element. 182 */ 183 int getTextInputType() { 184 return mTextInputType; 185 } 186 187 /** 188 * @return Constant representing that a focused node is not an input field. 189 */ 190 public static int getTextInputTypeNone() { 191 return sTextInputTypeNone; 192 } 193 194 private static int getModifiers(int metaState) { 195 int modifiers = 0; 196 if ((metaState & KeyEvent.META_SHIFT_ON) != 0) { 197 modifiers |= sModifierShift; 198 } 199 if ((metaState & KeyEvent.META_ALT_ON) != 0) { 200 modifiers |= sModifierAlt; 201 } 202 if ((metaState & KeyEvent.META_CTRL_ON) != 0) { 203 modifiers |= sModifierCtrl; 204 } 205 if ((metaState & KeyEvent.META_CAPS_LOCK_ON) != 0) { 206 modifiers |= sModifierCapsLockOn; 207 } 208 if ((metaState & KeyEvent.META_NUM_LOCK_ON) != 0) { 209 modifiers |= sModifierNumLockOn; 210 } 211 return modifiers; 212 } 213 214 /** 215 * Shows or hides the keyboard based on passed parameters. 216 * @param nativeImeAdapter Pointer to the ImeAdapterAndroid object that is sending the update. 217 * @param textInputType Text input type for the currently focused field in renderer. 218 * @param showIfNeeded Whether the keyboard should be shown if it is currently hidden. 219 */ 220 public void updateKeyboardVisibility(long nativeImeAdapter, int textInputType, 221 boolean showIfNeeded) { 222 mHandler.removeCallbacks(mDismissInput); 223 224 // If current input type is none and showIfNeeded is false, IME should not be shown 225 // and input type should remain as none. 226 if (mTextInputType == sTextInputTypeNone && !showIfNeeded) { 227 return; 228 } 229 230 if (mNativeImeAdapterAndroid != nativeImeAdapter || mTextInputType != textInputType) { 231 // Set a delayed task to perform unfocus. This avoids hiding the keyboard when tabbing 232 // through text inputs or when JS rapidly changes focus to another text element. 233 if (textInputType == sTextInputTypeNone) { 234 mDismissInput = new DelayedDismissInput(nativeImeAdapter); 235 mHandler.postDelayed(mDismissInput, INPUT_DISMISS_DELAY); 236 return; 237 } 238 239 attach(nativeImeAdapter, textInputType); 240 241 mInputMethodManagerWrapper.restartInput(mViewEmbedder.getAttachedView()); 242 if (showIfNeeded) { 243 showKeyboard(); 244 } 245 } else if (hasInputType() && showIfNeeded) { 246 showKeyboard(); 247 } 248 } 249 250 public void attach(long nativeImeAdapter, int textInputType) { 251 if (mNativeImeAdapterAndroid != 0) { 252 nativeResetImeAdapter(mNativeImeAdapterAndroid); 253 } 254 mNativeImeAdapterAndroid = nativeImeAdapter; 255 mTextInputType = textInputType; 256 if (nativeImeAdapter != 0) { 257 nativeAttachImeAdapter(mNativeImeAdapterAndroid); 258 } 259 if (mTextInputType == sTextInputTypeNone) { 260 dismissInput(false); 261 } 262 } 263 264 /** 265 * Attaches the imeAdapter to its native counterpart. This is needed to start forwarding 266 * keyboard events to WebKit. 267 * @param nativeImeAdapter The pointer to the native ImeAdapter object. 268 */ 269 public void attach(long nativeImeAdapter) { 270 attach(nativeImeAdapter, sTextInputTypeNone); 271 } 272 273 private void showKeyboard() { 274 mIsShowWithoutHideOutstanding = true; 275 mInputMethodManagerWrapper.showSoftInput(mViewEmbedder.getAttachedView(), 0, 276 mViewEmbedder.getNewShowKeyboardReceiver()); 277 } 278 279 private void dismissInput(boolean unzoomIfNeeded) { 280 mIsShowWithoutHideOutstanding = false; 281 View view = mViewEmbedder.getAttachedView(); 282 if (mInputMethodManagerWrapper.isActive(view)) { 283 mInputMethodManagerWrapper.hideSoftInputFromWindow(view.getWindowToken(), 0, 284 unzoomIfNeeded ? mViewEmbedder.getNewShowKeyboardReceiver() : null); 285 } 286 mViewEmbedder.onDismissInput(); 287 } 288 289 private boolean hasInputType() { 290 return mTextInputType != sTextInputTypeNone; 291 } 292 293 private static boolean isTextInputType(int type) { 294 return type != sTextInputTypeNone && !InputDialogContainer.isDialogInputType(type); 295 } 296 297 public boolean hasTextInputType() { 298 return isTextInputType(mTextInputType); 299 } 300 301 /** 302 * @return true if the selected text is of password. 303 */ 304 public boolean isSelectionPassword() { 305 return mTextInputType == sTextInputTypePassword; 306 } 307 308 public boolean dispatchKeyEvent(KeyEvent event) { 309 return translateAndSendNativeEvents(event); 310 } 311 312 private int shouldSendKeyEventWithKeyCode(String text) { 313 if (text.length() != 1) return COMPOSITION_KEY_CODE; 314 315 if (text.equals("\n")) return KeyEvent.KEYCODE_ENTER; 316 else if (text.equals("\t")) return KeyEvent.KEYCODE_TAB; 317 else return COMPOSITION_KEY_CODE; 318 } 319 320 void sendKeyEventWithKeyCode(int keyCode, int flags) { 321 long eventTime = SystemClock.uptimeMillis(); 322 translateAndSendNativeEvents(new KeyEvent(eventTime, eventTime, 323 KeyEvent.ACTION_DOWN, keyCode, 0, 0, 324 KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 325 flags)); 326 translateAndSendNativeEvents(new KeyEvent(SystemClock.uptimeMillis(), eventTime, 327 KeyEvent.ACTION_UP, keyCode, 0, 0, 328 KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 329 flags)); 330 } 331 332 // Calls from Java to C++ 333 334 boolean checkCompositionQueueAndCallNative(CharSequence text, int newCursorPosition, 335 boolean isCommit) { 336 if (mNativeImeAdapterAndroid == 0) return false; 337 String textStr = text.toString(); 338 339 // Committing an empty string finishes the current composition. 340 boolean isFinish = textStr.isEmpty(); 341 mViewEmbedder.onImeEvent(isFinish); 342 int keyCode = shouldSendKeyEventWithKeyCode(textStr); 343 long timeStampMs = SystemClock.uptimeMillis(); 344 345 if (keyCode != COMPOSITION_KEY_CODE) { 346 sendKeyEventWithKeyCode(keyCode, 347 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE); 348 } else { 349 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeRawKeyDown, 350 timeStampMs, keyCode, 0); 351 if (isCommit) { 352 nativeCommitText(mNativeImeAdapterAndroid, textStr); 353 } else { 354 nativeSetComposingText(mNativeImeAdapterAndroid, text, textStr, newCursorPosition); 355 } 356 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeKeyUp, 357 timeStampMs, keyCode, 0); 358 } 359 360 return true; 361 } 362 363 void finishComposingText() { 364 if (mNativeImeAdapterAndroid == 0) return; 365 nativeFinishComposingText(mNativeImeAdapterAndroid); 366 } 367 368 boolean translateAndSendNativeEvents(KeyEvent event) { 369 if (mNativeImeAdapterAndroid == 0) return false; 370 371 int action = event.getAction(); 372 if (action != KeyEvent.ACTION_DOWN && 373 action != KeyEvent.ACTION_UP) { 374 // action == KeyEvent.ACTION_MULTIPLE 375 // TODO(bulach): confirm the actual behavior. Apparently: 376 // If event.getKeyCode() == KEYCODE_UNKNOWN, we can send a 377 // composition key down (229) followed by a commit text with the 378 // string from event.getUnicodeChars(). 379 // Otherwise, we'd need to send an event with a 380 // WebInputEvent::IsAutoRepeat modifier. We also need to verify when 381 // we receive ACTION_MULTIPLE: we may receive it after an ACTION_DOWN, 382 // and if that's the case, we'll need to review when to send the Char 383 // event. 384 return false; 385 } 386 mViewEmbedder.onImeEvent(false); 387 return nativeSendKeyEvent(mNativeImeAdapterAndroid, event, event.getAction(), 388 getModifiers(event.getMetaState()), event.getEventTime(), event.getKeyCode(), 389 /*isSystemKey=*/false, event.getUnicodeChar()); 390 } 391 392 boolean sendSyntheticKeyEvent(int eventType, long timestampMs, int keyCode, int unicodeChar) { 393 if (mNativeImeAdapterAndroid == 0) return false; 394 395 nativeSendSyntheticKeyEvent( 396 mNativeImeAdapterAndroid, eventType, timestampMs, keyCode, unicodeChar); 397 return true; 398 } 399 400 /** 401 * Send a request to the native counterpart to delete a given range of characters. 402 * @param beforeLength Number of characters to extend the selection by before the existing 403 * selection. 404 * @param afterLength Number of characters to extend the selection by after the existing 405 * selection. 406 * @return Whether the native counterpart of ImeAdapter received the call. 407 */ 408 boolean deleteSurroundingText(int beforeLength, int afterLength) { 409 if (mNativeImeAdapterAndroid == 0) return false; 410 nativeDeleteSurroundingText(mNativeImeAdapterAndroid, beforeLength, afterLength); 411 return true; 412 } 413 414 /** 415 * Send a request to the native counterpart to set the selection to given range. 416 * @param start Selection start index. 417 * @param end Selection end index. 418 * @return Whether the native counterpart of ImeAdapter received the call. 419 */ 420 boolean setEditableSelectionOffsets(int start, int end) { 421 if (mNativeImeAdapterAndroid == 0) return false; 422 nativeSetEditableSelectionOffsets(mNativeImeAdapterAndroid, start, end); 423 return true; 424 } 425 426 /** 427 * Send a request to the native counterpart to set compositing region to given indices. 428 * @param start The start of the composition. 429 * @param end The end of the composition. 430 * @return Whether the native counterpart of ImeAdapter received the call. 431 */ 432 boolean setComposingRegion(int start, int end) { 433 if (mNativeImeAdapterAndroid == 0) return false; 434 nativeSetComposingRegion(mNativeImeAdapterAndroid, start, end); 435 return true; 436 } 437 438 /** 439 * Send a request to the native counterpart to unselect text. 440 * @return Whether the native counterpart of ImeAdapter received the call. 441 */ 442 public boolean unselect() { 443 if (mNativeImeAdapterAndroid == 0) return false; 444 nativeUnselect(mNativeImeAdapterAndroid); 445 return true; 446 } 447 448 /** 449 * Send a request to the native counterpart of ImeAdapter to select all the text. 450 * @return Whether the native counterpart of ImeAdapter received the call. 451 */ 452 public boolean selectAll() { 453 if (mNativeImeAdapterAndroid == 0) return false; 454 nativeSelectAll(mNativeImeAdapterAndroid); 455 return true; 456 } 457 458 /** 459 * Send a request to the native counterpart of ImeAdapter to cut the selected text. 460 * @return Whether the native counterpart of ImeAdapter received the call. 461 */ 462 public boolean cut() { 463 if (mNativeImeAdapterAndroid == 0) return false; 464 nativeCut(mNativeImeAdapterAndroid); 465 return true; 466 } 467 468 /** 469 * Send a request to the native counterpart of ImeAdapter to copy the selected text. 470 * @return Whether the native counterpart of ImeAdapter received the call. 471 */ 472 public boolean copy() { 473 if (mNativeImeAdapterAndroid == 0) return false; 474 nativeCopy(mNativeImeAdapterAndroid); 475 return true; 476 } 477 478 /** 479 * Send a request to the native counterpart of ImeAdapter to paste the text from the clipboard. 480 * @return Whether the native counterpart of ImeAdapter received the call. 481 */ 482 public boolean paste() { 483 if (mNativeImeAdapterAndroid == 0) return false; 484 nativePaste(mNativeImeAdapterAndroid); 485 return true; 486 } 487 488 // Calls from C++ to Java 489 490 @CalledByNative 491 private static void initializeWebInputEvents(int eventTypeRawKeyDown, int eventTypeKeyUp, 492 int eventTypeChar, int modifierShift, int modifierAlt, int modifierCtrl, 493 int modifierCapsLockOn, int modifierNumLockOn) { 494 sEventTypeRawKeyDown = eventTypeRawKeyDown; 495 sEventTypeKeyUp = eventTypeKeyUp; 496 sEventTypeChar = eventTypeChar; 497 sModifierShift = modifierShift; 498 sModifierAlt = modifierAlt; 499 sModifierCtrl = modifierCtrl; 500 sModifierCapsLockOn = modifierCapsLockOn; 501 sModifierNumLockOn = modifierNumLockOn; 502 } 503 504 @CalledByNative 505 private static void initializeTextInputTypes(int textInputTypeNone, int textInputTypeText, 506 int textInputTypeTextArea, int textInputTypePassword, int textInputTypeSearch, 507 int textInputTypeUrl, int textInputTypeEmail, int textInputTypeTel, 508 int textInputTypeNumber, int textInputTypeContentEditable) { 509 sTextInputTypeNone = textInputTypeNone; 510 sTextInputTypeText = textInputTypeText; 511 sTextInputTypeTextArea = textInputTypeTextArea; 512 sTextInputTypePassword = textInputTypePassword; 513 sTextInputTypeSearch = textInputTypeSearch; 514 sTextInputTypeUrl = textInputTypeUrl; 515 sTextInputTypeEmail = textInputTypeEmail; 516 sTextInputTypeTel = textInputTypeTel; 517 sTextInputTypeNumber = textInputTypeNumber; 518 sTextInputTypeContentEditable = textInputTypeContentEditable; 519 } 520 521 @CalledByNative 522 private void focusedNodeChanged(boolean isEditable) { 523 if (mInputConnection != null && isEditable) mInputConnection.restartInput(); 524 } 525 526 @CalledByNative 527 private void populateUnderlinesFromSpans(CharSequence text, long underlines) { 528 if (!(text instanceof SpannableString)) return; 529 530 SpannableString spannableString = ((SpannableString) text); 531 CharacterStyle spans[] = 532 spannableString.getSpans(0, text.length(), CharacterStyle.class); 533 for (CharacterStyle span : spans) { 534 if (span instanceof BackgroundColorSpan) { 535 nativeAppendBackgroundColorSpan(underlines, spannableString.getSpanStart(span), 536 spannableString.getSpanEnd(span), 537 ((BackgroundColorSpan) span).getBackgroundColor()); 538 } else if (span instanceof UnderlineSpan) { 539 nativeAppendUnderlineSpan(underlines, spannableString.getSpanStart(span), 540 spannableString.getSpanEnd(span)); 541 } 542 } 543 } 544 545 @CalledByNative 546 private void cancelComposition() { 547 if (mInputConnection != null) mInputConnection.restartInput(); 548 } 549 550 @CalledByNative 551 void detach() { 552 if (mDismissInput != null) mHandler.removeCallbacks(mDismissInput); 553 mNativeImeAdapterAndroid = 0; 554 mTextInputType = 0; 555 } 556 557 private native boolean nativeSendSyntheticKeyEvent(long nativeImeAdapterAndroid, 558 int eventType, long timestampMs, int keyCode, int unicodeChar); 559 560 private native boolean nativeSendKeyEvent(long nativeImeAdapterAndroid, KeyEvent event, 561 int action, int modifiers, long timestampMs, int keyCode, boolean isSystemKey, 562 int unicodeChar); 563 564 private static native void nativeAppendUnderlineSpan(long underlinePtr, int start, int end); 565 566 private static native void nativeAppendBackgroundColorSpan(long underlinePtr, int start, 567 int end, int backgroundColor); 568 569 private native void nativeSetComposingText(long nativeImeAdapterAndroid, CharSequence text, 570 String textStr, int newCursorPosition); 571 572 private native void nativeCommitText(long nativeImeAdapterAndroid, String textStr); 573 574 private native void nativeFinishComposingText(long nativeImeAdapterAndroid); 575 576 private native void nativeAttachImeAdapter(long nativeImeAdapterAndroid); 577 578 private native void nativeSetEditableSelectionOffsets(long nativeImeAdapterAndroid, 579 int start, int end); 580 581 private native void nativeSetComposingRegion(long nativeImeAdapterAndroid, int start, int end); 582 583 private native void nativeDeleteSurroundingText(long nativeImeAdapterAndroid, 584 int before, int after); 585 586 private native void nativeUnselect(long nativeImeAdapterAndroid); 587 private native void nativeSelectAll(long nativeImeAdapterAndroid); 588 private native void nativeCut(long nativeImeAdapterAndroid); 589 private native void nativeCopy(long nativeImeAdapterAndroid); 590 private native void nativePaste(long nativeImeAdapterAndroid); 591 private native void nativeResetImeAdapter(long nativeImeAdapterAndroid); 592 } 593