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