ImeAdapter.java revision a36e5920737c6adbddd3e43b760e5de8431db6e0
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 } 52 53 private class DelayedDismissInput implements Runnable { 54 private final int mNativeImeAdapter; 55 56 DelayedDismissInput(int nativeImeAdapter) { 57 mNativeImeAdapter = nativeImeAdapter; 58 } 59 60 @Override 61 public void run() { 62 attach(mNativeImeAdapter, sTextInputTypeNone, AdapterInputConnection.INVALID_SELECTION, 63 AdapterInputConnection.INVALID_SELECTION); 64 dismissInput(true); 65 } 66 } 67 68 private static final int COMPOSITION_KEY_CODE = 229; 69 70 // Delay introduced to avoid hiding the keyboard if new show requests are received. 71 // The time required by the unfocus-focus events triggered by tab has been measured in soju: 72 // Mean: 18.633 ms, Standard deviation: 7.9837 ms. 73 // The value here should be higher enough to cover these cases, but not too high to avoid 74 // letting the user perceiving important delays. 75 private static final int INPUT_DISMISS_DELAY = 150; 76 77 // All the constants that are retrieved from the C++ code. 78 // They get set through initializeWebInputEvents and initializeTextInputTypes calls. 79 static int sEventTypeRawKeyDown; 80 static int sEventTypeKeyUp; 81 static int sEventTypeChar; 82 static int sTextInputTypeNone; 83 static int sTextInputTypeText; 84 static int sTextInputTypeTextArea; 85 static int sTextInputTypePassword; 86 static int sTextInputTypeSearch; 87 static int sTextInputTypeUrl; 88 static int sTextInputTypeEmail; 89 static int sTextInputTypeTel; 90 static int sTextInputTypeNumber; 91 static int sTextInputTypeWeek; 92 static int sTextInputTypeContentEditable; 93 static int sModifierShift; 94 static int sModifierAlt; 95 static int sModifierCtrl; 96 static int sModifierCapsLockOn; 97 static int sModifierNumLockOn; 98 99 private int mNativeImeAdapterAndroid; 100 private InputMethodManagerWrapper mInputMethodManagerWrapper; 101 private AdapterInputConnection mInputConnection; 102 private final ImeAdapterDelegate mViewEmbedder; 103 private final Handler mHandler; 104 private DelayedDismissInput mDismissInput = null; 105 private int mTextInputType; 106 private int mInitialSelectionStart; 107 private int mInitialSelectionEnd; 108 109 @VisibleForTesting 110 boolean mIsShowWithoutHideOutstanding = false; 111 112 /** 113 * @param wrapper InputMethodManagerWrapper that should receive all the call directed to 114 * InputMethodManager. 115 * @param embedder The view that is used for callbacks from ImeAdapter. 116 */ 117 public ImeAdapter(InputMethodManagerWrapper wrapper, ImeAdapterDelegate embedder) { 118 mInputMethodManagerWrapper = wrapper; 119 mViewEmbedder = embedder; 120 mHandler = new Handler(); 121 } 122 123 public static class AdapterInputConnectionFactory { 124 public AdapterInputConnection get(View view, ImeAdapter imeAdapter, 125 EditorInfo outAttrs) { 126 return new AdapterInputConnection(view, imeAdapter, outAttrs); 127 } 128 } 129 130 @VisibleForTesting 131 public void setInputMethodManagerWrapper(InputMethodManagerWrapper immw) { 132 mInputMethodManagerWrapper = immw; 133 } 134 135 /** 136 * Should be only used by AdapterInputConnection. 137 * @return InputMethodManagerWrapper that should receive all the calls directed to 138 * InputMethodManager. 139 */ 140 InputMethodManagerWrapper getInputMethodManagerWrapper() { 141 return mInputMethodManagerWrapper; 142 } 143 144 /** 145 * Set the current active InputConnection when a new InputConnection is constructed. 146 * @param inputConnection The input connection that is currently used with IME. 147 */ 148 void setInputConnection(AdapterInputConnection inputConnection) { 149 mInputConnection = inputConnection; 150 } 151 152 /** 153 * Should be only used by AdapterInputConnection. 154 * @return The input type of currently focused element. 155 */ 156 int getTextInputType() { 157 return mTextInputType; 158 } 159 160 /** 161 * Should be only used by AdapterInputConnection. 162 * @return The starting index of the initial text selection. 163 */ 164 int getInitialSelectionStart() { 165 return mInitialSelectionStart; 166 } 167 168 /** 169 * Should be only used by AdapterInputConnection. 170 * @return The ending index of the initial text selection. 171 */ 172 int getInitialSelectionEnd() { 173 return mInitialSelectionEnd; 174 } 175 176 public static int getTextInputTypeNone() { 177 return sTextInputTypeNone; 178 } 179 180 private static int getModifiers(int metaState) { 181 int modifiers = 0; 182 if ((metaState & KeyEvent.META_SHIFT_ON) != 0) { 183 modifiers |= sModifierShift; 184 } 185 if ((metaState & KeyEvent.META_ALT_ON) != 0) { 186 modifiers |= sModifierAlt; 187 } 188 if ((metaState & KeyEvent.META_CTRL_ON) != 0) { 189 modifiers |= sModifierCtrl; 190 } 191 if ((metaState & KeyEvent.META_CAPS_LOCK_ON) != 0) { 192 modifiers |= sModifierCapsLockOn; 193 } 194 if ((metaState & KeyEvent.META_NUM_LOCK_ON) != 0) { 195 modifiers |= sModifierNumLockOn; 196 } 197 return modifiers; 198 } 199 200 public boolean isActive() { 201 return mInputConnection != null && mInputConnection.isActive(); 202 } 203 204 private boolean isFor(int nativeImeAdapter, int textInputType) { 205 return mNativeImeAdapterAndroid == nativeImeAdapter && 206 mTextInputType == textInputType; 207 } 208 209 public void attachAndShowIfNeeded(int nativeImeAdapter, int textInputType, 210 int selectionStart, int selectionEnd, boolean showIfNeeded) { 211 mHandler.removeCallbacks(mDismissInput); 212 213 // If current input type is none and showIfNeeded is false, IME should not be shown 214 // and input type should remain as none. 215 if (mTextInputType == sTextInputTypeNone && !showIfNeeded) { 216 return; 217 } 218 219 if (!isFor(nativeImeAdapter, textInputType)) { 220 // Set a delayed task to perform unfocus. This avoids hiding the keyboard when tabbing 221 // through text inputs or when JS rapidly changes focus to another text element. 222 if (textInputType == sTextInputTypeNone) { 223 mDismissInput = new DelayedDismissInput(nativeImeAdapter); 224 mHandler.postDelayed(mDismissInput, INPUT_DISMISS_DELAY); 225 return; 226 } 227 228 int previousType = mTextInputType; 229 attach(nativeImeAdapter, textInputType, selectionStart, selectionEnd); 230 231 mInputMethodManagerWrapper.restartInput(mViewEmbedder.getAttachedView()); 232 if (showIfNeeded) { 233 showKeyboard(); 234 } 235 } else if (hasInputType() && showIfNeeded) { 236 showKeyboard(); 237 } 238 } 239 240 public void attach(int nativeImeAdapter, int textInputType, int selectionStart, 241 int selectionEnd) { 242 if (mNativeImeAdapterAndroid != 0) { 243 nativeResetImeAdapter(mNativeImeAdapterAndroid); 244 } 245 mNativeImeAdapterAndroid = nativeImeAdapter; 246 mTextInputType = textInputType; 247 mInitialSelectionStart = selectionStart; 248 mInitialSelectionEnd = selectionEnd; 249 if (nativeImeAdapter != 0) { 250 nativeAttachImeAdapter(mNativeImeAdapterAndroid); 251 } 252 } 253 254 /** 255 * Attaches the imeAdapter to its native counterpart. This is needed to start forwarding 256 * keyboard events to WebKit. 257 * @param nativeImeAdapter The pointer to the native ImeAdapter object. 258 */ 259 public void attach(int nativeImeAdapter) { 260 if (mNativeImeAdapterAndroid != 0) { 261 nativeResetImeAdapter(mNativeImeAdapterAndroid); 262 } 263 mNativeImeAdapterAndroid = nativeImeAdapter; 264 if (nativeImeAdapter != 0) { 265 nativeAttachImeAdapter(mNativeImeAdapterAndroid); 266 } 267 } 268 269 /** 270 * Used to check whether the native counterpart of the ImeAdapter has been attached yet. 271 * @return Whether native ImeAdapter has been attached and its pointer is currently nonzero. 272 */ 273 public boolean isNativeImeAdapterAttached() { 274 return mNativeImeAdapterAndroid != 0; 275 } 276 277 private void showKeyboard() { 278 mIsShowWithoutHideOutstanding = true; 279 mInputMethodManagerWrapper.showSoftInput(mViewEmbedder.getAttachedView(), 0, 280 mViewEmbedder.getNewShowKeyboardReceiver()); 281 } 282 283 private void dismissInput(boolean unzoomIfNeeded) { 284 hideKeyboard(unzoomIfNeeded); 285 mViewEmbedder.onDismissInput(); 286 } 287 288 private void hideKeyboard(boolean unzoomIfNeeded) { 289 mIsShowWithoutHideOutstanding = false; 290 View view = mViewEmbedder.getAttachedView(); 291 if (mInputMethodManagerWrapper.isActive(view)) { 292 mInputMethodManagerWrapper.hideSoftInputFromWindow(view.getWindowToken(), 0, 293 unzoomIfNeeded ? mViewEmbedder.getNewShowKeyboardReceiver() : null); 294 } 295 } 296 297 private boolean hasInputType() { 298 return mTextInputType != sTextInputTypeNone; 299 } 300 301 static boolean isTextInputType(int type) { 302 return type != sTextInputTypeNone && !InputDialogContainer.isDialogInputType(type); 303 } 304 305 public boolean hasTextInputType() { 306 return isTextInputType(mTextInputType); 307 } 308 309 public boolean dispatchKeyEvent(KeyEvent event) { 310 return translateAndSendNativeEvents(event); 311 } 312 313 private int shouldSendKeyEventWithKeyCode(String text) { 314 if (text.length() != 1) return COMPOSITION_KEY_CODE; 315 316 if (text.equals("\n")) return KeyEvent.KEYCODE_ENTER; 317 else if (text.equals("\t")) return KeyEvent.KEYCODE_TAB; 318 else return COMPOSITION_KEY_CODE; 319 } 320 321 void sendKeyEventWithKeyCode(int keyCode, int flags) { 322 long eventTime = System.currentTimeMillis(); 323 translateAndSendNativeEvents(new KeyEvent(eventTime, eventTime, 324 KeyEvent.ACTION_DOWN, keyCode, 0, 0, 325 KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 326 flags)); 327 translateAndSendNativeEvents(new KeyEvent(System.currentTimeMillis(), eventTime, 328 KeyEvent.ACTION_UP, keyCode, 0, 0, 329 KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 330 flags)); 331 } 332 333 // Calls from Java to C++ 334 335 @VisibleForTesting 336 boolean checkCompositionQueueAndCallNative(String text, int newCursorPosition, 337 boolean isCommit) { 338 if (mNativeImeAdapterAndroid == 0) return false; 339 340 // Committing an empty string finishes the current composition. 341 boolean isFinish = text.isEmpty(); 342 mViewEmbedder.onImeEvent(isFinish); 343 int keyCode = shouldSendKeyEventWithKeyCode(text); 344 long timeStampMs = System.currentTimeMillis(); 345 346 if (keyCode != COMPOSITION_KEY_CODE) { 347 sendKeyEventWithKeyCode(keyCode, 348 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE); 349 } else { 350 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeRawKeyDown, 351 timeStampMs, keyCode, 0); 352 if (isCommit) { 353 nativeCommitText(mNativeImeAdapterAndroid, text); 354 } else { 355 nativeSetComposingText(mNativeImeAdapterAndroid, text, newCursorPosition); 356 } 357 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeKeyUp, 358 timeStampMs, keyCode, 0); 359 } 360 361 return true; 362 } 363 364 void finishComposingText() { 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 event.isSystem(), event.getUnicodeChar()); 390 } 391 392 boolean sendSyntheticKeyEvent( 393 int eventType, long timestampMs, int keyCode, int unicodeChar) { 394 if (mNativeImeAdapterAndroid == 0) return false; 395 396 nativeSendSyntheticKeyEvent( 397 mNativeImeAdapterAndroid, eventType, timestampMs, keyCode, unicodeChar); 398 return true; 399 } 400 401 boolean deleteSurroundingText(int leftLength, int rightLength) { 402 if (mNativeImeAdapterAndroid == 0) return false; 403 nativeDeleteSurroundingText(mNativeImeAdapterAndroid, leftLength, rightLength); 404 return true; 405 } 406 407 @VisibleForTesting 408 protected boolean setEditableSelectionOffsets(int start, int end) { 409 if (mNativeImeAdapterAndroid == 0) return false; 410 nativeSetEditableSelectionOffsets(mNativeImeAdapterAndroid, start, end); 411 return true; 412 } 413 414 void batchStateChanged(boolean isBegin) { 415 if (mNativeImeAdapterAndroid == 0) return; 416 nativeImeBatchStateChanged(mNativeImeAdapterAndroid, isBegin); 417 } 418 419 void commitText() { 420 cancelComposition(); 421 if (mNativeImeAdapterAndroid != 0) { 422 nativeCommitText(mNativeImeAdapterAndroid, ""); 423 } 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 textInputTypeDate, int textInputTypeDateTime, 509 int textInputTypeDateTimeLocal, int textInputTypeMonth, int textInputTypeTime, 510 int textInputTypeWeek, int textInputTypeContentEditable) { 511 sTextInputTypeNone = textInputTypeNone; 512 sTextInputTypeText = textInputTypeText; 513 sTextInputTypeTextArea = textInputTypeTextArea; 514 sTextInputTypePassword = textInputTypePassword; 515 sTextInputTypeSearch = textInputTypeSearch; 516 sTextInputTypeUrl = textInputTypeUrl; 517 sTextInputTypeEmail = textInputTypeEmail; 518 sTextInputTypeTel = textInputTypeTel; 519 sTextInputTypeNumber = textInputTypeNumber; 520 sTextInputTypeWeek = textInputTypeWeek; 521 sTextInputTypeContentEditable = textInputTypeContentEditable; 522 } 523 524 @CalledByNative 525 private void cancelComposition() { 526 if (mInputConnection != null) { 527 mInputConnection.restartInput(); 528 } 529 } 530 531 @CalledByNative 532 void detach() { 533 mNativeImeAdapterAndroid = 0; 534 mTextInputType = 0; 535 } 536 537 private native boolean nativeSendSyntheticKeyEvent(int nativeImeAdapterAndroid, 538 int eventType, long timestampMs, int keyCode, int unicodeChar); 539 540 private native boolean nativeSendKeyEvent(int nativeImeAdapterAndroid, KeyEvent event, 541 int action, int modifiers, long timestampMs, int keyCode, boolean isSystemKey, 542 int unicodeChar); 543 544 private native void nativeSetComposingText(int nativeImeAdapterAndroid, String text, 545 int newCursorPosition); 546 547 private native void nativeCommitText(int nativeImeAdapterAndroid, String text); 548 549 private native void nativeFinishComposingText(int nativeImeAdapterAndroid); 550 551 private native void nativeAttachImeAdapter(int nativeImeAdapterAndroid); 552 553 private native void nativeSetEditableSelectionOffsets(int nativeImeAdapterAndroid, 554 int start, int end); 555 556 private native void nativeSetComposingRegion(int nativeImeAdapterAndroid, int start, int end); 557 558 private native void nativeDeleteSurroundingText(int nativeImeAdapterAndroid, 559 int before, int after); 560 561 private native void nativeImeBatchStateChanged(int nativeImeAdapterAndroid, boolean isBegin); 562 563 private native void nativeUnselect(int nativeImeAdapterAndroid); 564 private native void nativeSelectAll(int nativeImeAdapterAndroid); 565 private native void nativeCut(int nativeImeAdapterAndroid); 566 private native void nativeCopy(int nativeImeAdapterAndroid); 567 private native void nativePaste(int nativeImeAdapterAndroid); 568 private native void nativeResetImeAdapter(int nativeImeAdapterAndroid); 569} 570