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