ImeAdapter.java revision c2db58bd994c04d98e4ee2cd7565b71548655fe3
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 if (mNativeImeAdapterAndroid == 0) return; 366 nativeFinishComposingText(mNativeImeAdapterAndroid); 367 } 368 369 boolean translateAndSendNativeEvents(KeyEvent event) { 370 if (mNativeImeAdapterAndroid == 0) return false; 371 372 int action = event.getAction(); 373 if (action != KeyEvent.ACTION_DOWN && 374 action != KeyEvent.ACTION_UP) { 375 // action == KeyEvent.ACTION_MULTIPLE 376 // TODO(bulach): confirm the actual behavior. Apparently: 377 // If event.getKeyCode() == KEYCODE_UNKNOWN, we can send a 378 // composition key down (229) followed by a commit text with the 379 // string from event.getUnicodeChars(). 380 // Otherwise, we'd need to send an event with a 381 // WebInputEvent::IsAutoRepeat modifier. We also need to verify when 382 // we receive ACTION_MULTIPLE: we may receive it after an ACTION_DOWN, 383 // and if that's the case, we'll need to review when to send the Char 384 // event. 385 return false; 386 } 387 mViewEmbedder.onImeEvent(false); 388 return nativeSendKeyEvent(mNativeImeAdapterAndroid, event, event.getAction(), 389 getModifiers(event.getMetaState()), event.getEventTime(), event.getKeyCode(), 390 event.isSystem(), event.getUnicodeChar()); 391 } 392 393 boolean sendSyntheticKeyEvent( 394 int eventType, long timestampMs, int keyCode, int unicodeChar) { 395 if (mNativeImeAdapterAndroid == 0) return false; 396 397 nativeSendSyntheticKeyEvent( 398 mNativeImeAdapterAndroid, eventType, timestampMs, keyCode, unicodeChar); 399 return true; 400 } 401 402 boolean deleteSurroundingText(int leftLength, int rightLength) { 403 if (mNativeImeAdapterAndroid == 0) return false; 404 nativeDeleteSurroundingText(mNativeImeAdapterAndroid, leftLength, rightLength); 405 return true; 406 } 407 408 @VisibleForTesting 409 protected boolean setEditableSelectionOffsets(int start, int end) { 410 if (mNativeImeAdapterAndroid == 0) return false; 411 nativeSetEditableSelectionOffsets(mNativeImeAdapterAndroid, start, end); 412 return true; 413 } 414 415 void batchStateChanged(boolean isBegin) { 416 if (mNativeImeAdapterAndroid == 0) return; 417 nativeImeBatchStateChanged(mNativeImeAdapterAndroid, isBegin); 418 } 419 420 void commitText() { 421 cancelComposition(); 422 if (mNativeImeAdapterAndroid != 0) { 423 nativeCommitText(mNativeImeAdapterAndroid, ""); 424 } 425 } 426 427 /** 428 * Send a request to the native counterpart to set compositing region to given indices. 429 * @param start The start of the composition. 430 * @param end The end of the composition. 431 * @return Whether the native counterpart of ImeAdapter received the call. 432 */ 433 boolean setComposingRegion(int start, int end) { 434 if (mNativeImeAdapterAndroid == 0) return false; 435 nativeSetComposingRegion(mNativeImeAdapterAndroid, start, end); 436 return true; 437 } 438 439 /** 440 * Send a request to the native counterpart to unselect text. 441 * @return Whether the native counterpart of ImeAdapter received the call. 442 */ 443 public boolean unselect() { 444 if (mNativeImeAdapterAndroid == 0) return false; 445 nativeUnselect(mNativeImeAdapterAndroid); 446 return true; 447 } 448 449 /** 450 * Send a request to the native counterpart of ImeAdapter to select all the text. 451 * @return Whether the native counterpart of ImeAdapter received the call. 452 */ 453 public boolean selectAll() { 454 if (mNativeImeAdapterAndroid == 0) return false; 455 nativeSelectAll(mNativeImeAdapterAndroid); 456 return true; 457 } 458 459 /** 460 * Send a request to the native counterpart of ImeAdapter to cut the selected text. 461 * @return Whether the native counterpart of ImeAdapter received the call. 462 */ 463 public boolean cut() { 464 if (mNativeImeAdapterAndroid == 0) return false; 465 nativeCut(mNativeImeAdapterAndroid); 466 return true; 467 } 468 469 /** 470 * Send a request to the native counterpart of ImeAdapter to copy the selected text. 471 * @return Whether the native counterpart of ImeAdapter received the call. 472 */ 473 public boolean copy() { 474 if (mNativeImeAdapterAndroid == 0) return false; 475 nativeCopy(mNativeImeAdapterAndroid); 476 return true; 477 } 478 479 /** 480 * Send a request to the native counterpart of ImeAdapter to paste the text from the clipboard. 481 * @return Whether the native counterpart of ImeAdapter received the call. 482 */ 483 public boolean paste() { 484 if (mNativeImeAdapterAndroid == 0) return false; 485 nativePaste(mNativeImeAdapterAndroid); 486 return true; 487 } 488 489 // Calls from C++ to Java 490 491 @CalledByNative 492 private static void initializeWebInputEvents(int eventTypeRawKeyDown, int eventTypeKeyUp, 493 int eventTypeChar, int modifierShift, int modifierAlt, int modifierCtrl, 494 int modifierCapsLockOn, int modifierNumLockOn) { 495 sEventTypeRawKeyDown = eventTypeRawKeyDown; 496 sEventTypeKeyUp = eventTypeKeyUp; 497 sEventTypeChar = eventTypeChar; 498 sModifierShift = modifierShift; 499 sModifierAlt = modifierAlt; 500 sModifierCtrl = modifierCtrl; 501 sModifierCapsLockOn = modifierCapsLockOn; 502 sModifierNumLockOn = modifierNumLockOn; 503 } 504 505 @CalledByNative 506 private static void initializeTextInputTypes(int textInputTypeNone, int textInputTypeText, 507 int textInputTypeTextArea, int textInputTypePassword, int textInputTypeSearch, 508 int textInputTypeUrl, int textInputTypeEmail, int textInputTypeTel, 509 int textInputTypeNumber, int textInputTypeDate, int textInputTypeDateTime, 510 int textInputTypeDateTimeLocal, int textInputTypeMonth, int textInputTypeTime, 511 int textInputTypeWeek, int textInputTypeContentEditable) { 512 sTextInputTypeNone = textInputTypeNone; 513 sTextInputTypeText = textInputTypeText; 514 sTextInputTypeTextArea = textInputTypeTextArea; 515 sTextInputTypePassword = textInputTypePassword; 516 sTextInputTypeSearch = textInputTypeSearch; 517 sTextInputTypeUrl = textInputTypeUrl; 518 sTextInputTypeEmail = textInputTypeEmail; 519 sTextInputTypeTel = textInputTypeTel; 520 sTextInputTypeNumber = textInputTypeNumber; 521 sTextInputTypeWeek = textInputTypeWeek; 522 sTextInputTypeContentEditable = textInputTypeContentEditable; 523 } 524 525 @CalledByNative 526 private void cancelComposition() { 527 if (mInputConnection != null) { 528 mInputConnection.restartInput(); 529 } 530 } 531 532 @CalledByNative 533 void detach() { 534 mNativeImeAdapterAndroid = 0; 535 mTextInputType = 0; 536 } 537 538 private native boolean nativeSendSyntheticKeyEvent(int nativeImeAdapterAndroid, 539 int eventType, long timestampMs, int keyCode, int unicodeChar); 540 541 private native boolean nativeSendKeyEvent(int nativeImeAdapterAndroid, KeyEvent event, 542 int action, int modifiers, long timestampMs, int keyCode, boolean isSystemKey, 543 int unicodeChar); 544 545 private native void nativeSetComposingText(int nativeImeAdapterAndroid, String text, 546 int newCursorPosition); 547 548 private native void nativeCommitText(int nativeImeAdapterAndroid, String text); 549 550 private native void nativeFinishComposingText(int nativeImeAdapterAndroid); 551 552 private native void nativeAttachImeAdapter(int nativeImeAdapterAndroid); 553 554 private native void nativeSetEditableSelectionOffsets(int nativeImeAdapterAndroid, 555 int start, int end); 556 557 private native void nativeSetComposingRegion(int nativeImeAdapterAndroid, int start, int end); 558 559 private native void nativeDeleteSurroundingText(int nativeImeAdapterAndroid, 560 int before, int after); 561 562 private native void nativeImeBatchStateChanged(int nativeImeAdapterAndroid, boolean isBegin); 563 564 private native void nativeUnselect(int nativeImeAdapterAndroid); 565 private native void nativeSelectAll(int nativeImeAdapterAndroid); 566 private native void nativeCut(int nativeImeAdapterAndroid); 567 private native void nativeCopy(int nativeImeAdapterAndroid); 568 private native void nativePaste(int nativeImeAdapterAndroid); 569 private native void nativeResetImeAdapter(int nativeImeAdapterAndroid); 570} 571