ImeAdapter.java revision 4e180b6a0b4720a9b8e9e959a882386f690f08ff
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 sTextInputTypeContentEditable; 92 static int sModifierShift; 93 static int sModifierAlt; 94 static int sModifierCtrl; 95 static int sModifierCapsLockOn; 96 static int sModifierNumLockOn; 97 98 private int mNativeImeAdapterAndroid; 99 private InputMethodManagerWrapper mInputMethodManagerWrapper; 100 private AdapterInputConnection mInputConnection; 101 private final ImeAdapterDelegate mViewEmbedder; 102 private final Handler mHandler; 103 private DelayedDismissInput mDismissInput = null; 104 private int mTextInputType; 105 private int mInitialSelectionStart; 106 private int mInitialSelectionEnd; 107 108 @VisibleForTesting 109 boolean mIsShowWithoutHideOutstanding = false; 110 111 /** 112 * @param wrapper InputMethodManagerWrapper that should receive all the call directed to 113 * InputMethodManager. 114 * @param embedder The view that is used for callbacks from ImeAdapter. 115 */ 116 public ImeAdapter(InputMethodManagerWrapper wrapper, ImeAdapterDelegate embedder) { 117 mInputMethodManagerWrapper = wrapper; 118 mViewEmbedder = embedder; 119 mHandler = new Handler(); 120 } 121 122 public static class AdapterInputConnectionFactory { 123 public AdapterInputConnection get(View view, ImeAdapter imeAdapter, 124 EditorInfo outAttrs) { 125 return new AdapterInputConnection(view, imeAdapter, outAttrs); 126 } 127 } 128 129 @VisibleForTesting 130 public void setInputMethodManagerWrapper(InputMethodManagerWrapper immw) { 131 mInputMethodManagerWrapper = immw; 132 } 133 134 /** 135 * Should be only used by AdapterInputConnection. 136 * @return InputMethodManagerWrapper that should receive all the calls directed to 137 * InputMethodManager. 138 */ 139 InputMethodManagerWrapper getInputMethodManagerWrapper() { 140 return mInputMethodManagerWrapper; 141 } 142 143 /** 144 * Set the current active InputConnection when a new InputConnection is constructed. 145 * @param inputConnection The input connection that is currently used with IME. 146 */ 147 void setInputConnection(AdapterInputConnection inputConnection) { 148 mInputConnection = inputConnection; 149 } 150 151 /** 152 * Should be only used by AdapterInputConnection. 153 * @return The input type of currently focused element. 154 */ 155 int getTextInputType() { 156 return mTextInputType; 157 } 158 159 /** 160 * Should be only used by AdapterInputConnection. 161 * @return The starting index of the initial text selection. 162 */ 163 int getInitialSelectionStart() { 164 return mInitialSelectionStart; 165 } 166 167 /** 168 * Should be only used by AdapterInputConnection. 169 * @return The ending index of the initial text selection. 170 */ 171 int getInitialSelectionEnd() { 172 return mInitialSelectionEnd; 173 } 174 175 public static int getTextInputTypeNone() { 176 return sTextInputTypeNone; 177 } 178 179 private static int getModifiers(int metaState) { 180 int modifiers = 0; 181 if ((metaState & KeyEvent.META_SHIFT_ON) != 0) { 182 modifiers |= sModifierShift; 183 } 184 if ((metaState & KeyEvent.META_ALT_ON) != 0) { 185 modifiers |= sModifierAlt; 186 } 187 if ((metaState & KeyEvent.META_CTRL_ON) != 0) { 188 modifiers |= sModifierCtrl; 189 } 190 if ((metaState & KeyEvent.META_CAPS_LOCK_ON) != 0) { 191 modifiers |= sModifierCapsLockOn; 192 } 193 if ((metaState & KeyEvent.META_NUM_LOCK_ON) != 0) { 194 modifiers |= sModifierNumLockOn; 195 } 196 return modifiers; 197 } 198 199 public boolean isActive() { 200 return mInputConnection != null && mInputConnection.isActive(); 201 } 202 203 private boolean isFor(int nativeImeAdapter, int textInputType) { 204 return mNativeImeAdapterAndroid == nativeImeAdapter && 205 mTextInputType == textInputType; 206 } 207 208 public void attachAndShowIfNeeded(int nativeImeAdapter, int textInputType, 209 int selectionStart, int selectionEnd, boolean showIfNeeded) { 210 mHandler.removeCallbacks(mDismissInput); 211 212 // If current input type is none and showIfNeeded is false, IME should not be shown 213 // and input type should remain as none. 214 if (mTextInputType == sTextInputTypeNone && !showIfNeeded) { 215 return; 216 } 217 218 if (!isFor(nativeImeAdapter, textInputType)) { 219 // Set a delayed task to perform unfocus. This avoids hiding the keyboard when tabbing 220 // through text inputs or when JS rapidly changes focus to another text element. 221 if (textInputType == sTextInputTypeNone) { 222 mDismissInput = new DelayedDismissInput(nativeImeAdapter); 223 mHandler.postDelayed(mDismissInput, INPUT_DISMISS_DELAY); 224 return; 225 } 226 227 attach(nativeImeAdapter, textInputType, selectionStart, selectionEnd); 228 229 mInputMethodManagerWrapper.restartInput(mViewEmbedder.getAttachedView()); 230 if (showIfNeeded) { 231 showKeyboard(); 232 } 233 } else if (hasInputType() && showIfNeeded) { 234 showKeyboard(); 235 } 236 } 237 238 public void attach(int nativeImeAdapter, int textInputType, int selectionStart, 239 int selectionEnd) { 240 if (mNativeImeAdapterAndroid != 0) { 241 nativeResetImeAdapter(mNativeImeAdapterAndroid); 242 } 243 mNativeImeAdapterAndroid = nativeImeAdapter; 244 mTextInputType = textInputType; 245 mInitialSelectionStart = selectionStart; 246 mInitialSelectionEnd = selectionEnd; 247 if (nativeImeAdapter != 0) { 248 nativeAttachImeAdapter(mNativeImeAdapterAndroid); 249 } 250 } 251 252 /** 253 * Attaches the imeAdapter to its native counterpart. This is needed to start forwarding 254 * keyboard events to WebKit. 255 * @param nativeImeAdapter The pointer to the native ImeAdapter object. 256 */ 257 public void attach(int nativeImeAdapter) { 258 if (mNativeImeAdapterAndroid != 0) { 259 nativeResetImeAdapter(mNativeImeAdapterAndroid); 260 } 261 mNativeImeAdapterAndroid = nativeImeAdapter; 262 if (nativeImeAdapter != 0) { 263 nativeAttachImeAdapter(mNativeImeAdapterAndroid); 264 } 265 } 266 267 /** 268 * Used to check whether the native counterpart of the ImeAdapter has been attached yet. 269 * @return Whether native ImeAdapter has been attached and its pointer is currently nonzero. 270 */ 271 public boolean isNativeImeAdapterAttached() { 272 return mNativeImeAdapterAndroid != 0; 273 } 274 275 private void showKeyboard() { 276 mIsShowWithoutHideOutstanding = true; 277 mInputMethodManagerWrapper.showSoftInput(mViewEmbedder.getAttachedView(), 0, 278 mViewEmbedder.getNewShowKeyboardReceiver()); 279 } 280 281 private void dismissInput(boolean unzoomIfNeeded) { 282 hideKeyboard(unzoomIfNeeded); 283 mViewEmbedder.onDismissInput(); 284 } 285 286 private void hideKeyboard(boolean unzoomIfNeeded) { 287 mIsShowWithoutHideOutstanding = false; 288 View view = mViewEmbedder.getAttachedView(); 289 if (mInputMethodManagerWrapper.isActive(view)) { 290 mInputMethodManagerWrapper.hideSoftInputFromWindow(view.getWindowToken(), 0, 291 unzoomIfNeeded ? mViewEmbedder.getNewShowKeyboardReceiver() : null); 292 } 293 } 294 295 private boolean hasInputType() { 296 return mTextInputType != sTextInputTypeNone; 297 } 298 299 static boolean isTextInputType(int type) { 300 return type != sTextInputTypeNone && !InputDialogContainer.isDialogInputType(type); 301 } 302 303 public boolean hasTextInputType() { 304 return isTextInputType(mTextInputType); 305 } 306 307 public boolean dispatchKeyEvent(KeyEvent event) { 308 return translateAndSendNativeEvents(event); 309 } 310 311 private int shouldSendKeyEventWithKeyCode(String text) { 312 if (text.length() != 1) return COMPOSITION_KEY_CODE; 313 314 if (text.equals("\n")) return KeyEvent.KEYCODE_ENTER; 315 else if (text.equals("\t")) return KeyEvent.KEYCODE_TAB; 316 else return COMPOSITION_KEY_CODE; 317 } 318 319 void sendKeyEventWithKeyCode(int keyCode, int flags) { 320 long eventTime = System.currentTimeMillis(); 321 translateAndSendNativeEvents(new KeyEvent(eventTime, eventTime, 322 KeyEvent.ACTION_DOWN, keyCode, 0, 0, 323 KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 324 flags)); 325 translateAndSendNativeEvents(new KeyEvent(System.currentTimeMillis(), eventTime, 326 KeyEvent.ACTION_UP, keyCode, 0, 0, 327 KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 328 flags)); 329 } 330 331 // Calls from Java to C++ 332 333 @VisibleForTesting 334 boolean checkCompositionQueueAndCallNative(String text, int newCursorPosition, 335 boolean isCommit) { 336 if (mNativeImeAdapterAndroid == 0) return false; 337 338 // Committing an empty string finishes the current composition. 339 boolean isFinish = text.isEmpty(); 340 mViewEmbedder.onImeEvent(isFinish); 341 int keyCode = shouldSendKeyEventWithKeyCode(text); 342 long timeStampMs = System.currentTimeMillis(); 343 344 if (keyCode != COMPOSITION_KEY_CODE) { 345 sendKeyEventWithKeyCode(keyCode, 346 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE); 347 } else { 348 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeRawKeyDown, 349 timeStampMs, keyCode, 0); 350 if (isCommit) { 351 nativeCommitText(mNativeImeAdapterAndroid, text); 352 } else { 353 nativeSetComposingText(mNativeImeAdapterAndroid, text, newCursorPosition); 354 } 355 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeKeyUp, 356 timeStampMs, keyCode, 0); 357 } 358 359 return true; 360 } 361 362 void finishComposingText() { 363 if (mNativeImeAdapterAndroid == 0) return; 364 nativeFinishComposingText(mNativeImeAdapterAndroid); 365 } 366 367 boolean translateAndSendNativeEvents(KeyEvent event) { 368 if (mNativeImeAdapterAndroid == 0) return false; 369 370 int action = event.getAction(); 371 if (action != KeyEvent.ACTION_DOWN && 372 action != KeyEvent.ACTION_UP) { 373 // action == KeyEvent.ACTION_MULTIPLE 374 // TODO(bulach): confirm the actual behavior. Apparently: 375 // If event.getKeyCode() == KEYCODE_UNKNOWN, we can send a 376 // composition key down (229) followed by a commit text with the 377 // string from event.getUnicodeChars(). 378 // Otherwise, we'd need to send an event with a 379 // WebInputEvent::IsAutoRepeat modifier. We also need to verify when 380 // we receive ACTION_MULTIPLE: we may receive it after an ACTION_DOWN, 381 // and if that's the case, we'll need to review when to send the Char 382 // event. 383 return false; 384 } 385 mViewEmbedder.onImeEvent(false); 386 return nativeSendKeyEvent(mNativeImeAdapterAndroid, event, event.getAction(), 387 getModifiers(event.getMetaState()), event.getEventTime(), event.getKeyCode(), 388 event.isSystem(), event.getUnicodeChar()); 389 } 390 391 boolean sendSyntheticKeyEvent( 392 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 boolean deleteSurroundingText(int leftLength, int rightLength) { 401 if (mNativeImeAdapterAndroid == 0) return false; 402 nativeDeleteSurroundingText(mNativeImeAdapterAndroid, leftLength, rightLength); 403 return true; 404 } 405 406 @VisibleForTesting 407 protected boolean setEditableSelectionOffsets(int start, int end) { 408 if (mNativeImeAdapterAndroid == 0) return false; 409 nativeSetEditableSelectionOffsets(mNativeImeAdapterAndroid, start, end); 410 return true; 411 } 412 413 void commitText() { 414 cancelComposition(); 415 if (mNativeImeAdapterAndroid != 0) { 416 nativeCommitText(mNativeImeAdapterAndroid, ""); 417 } 418 } 419 420 /** 421 * Send a request to the native counterpart to set compositing region to given indices. 422 * @param start The start of the composition. 423 * @param end The end of the composition. 424 * @return Whether the native counterpart of ImeAdapter received the call. 425 */ 426 boolean setComposingRegion(int start, int end) { 427 if (mNativeImeAdapterAndroid == 0) return false; 428 nativeSetComposingRegion(mNativeImeAdapterAndroid, start, end); 429 return true; 430 } 431 432 /** 433 * Send a request to the native counterpart to unselect text. 434 * @return Whether the native counterpart of ImeAdapter received the call. 435 */ 436 public boolean unselect() { 437 if (mNativeImeAdapterAndroid == 0) return false; 438 nativeUnselect(mNativeImeAdapterAndroid); 439 return true; 440 } 441 442 /** 443 * Send a request to the native counterpart of ImeAdapter to select all the text. 444 * @return Whether the native counterpart of ImeAdapter received the call. 445 */ 446 public boolean selectAll() { 447 if (mNativeImeAdapterAndroid == 0) return false; 448 nativeSelectAll(mNativeImeAdapterAndroid); 449 return true; 450 } 451 452 /** 453 * Send a request to the native counterpart of ImeAdapter to cut the selected text. 454 * @return Whether the native counterpart of ImeAdapter received the call. 455 */ 456 public boolean cut() { 457 if (mNativeImeAdapterAndroid == 0) return false; 458 nativeCut(mNativeImeAdapterAndroid); 459 return true; 460 } 461 462 /** 463 * Send a request to the native counterpart of ImeAdapter to copy the selected text. 464 * @return Whether the native counterpart of ImeAdapter received the call. 465 */ 466 public boolean copy() { 467 if (mNativeImeAdapterAndroid == 0) return false; 468 nativeCopy(mNativeImeAdapterAndroid); 469 return true; 470 } 471 472 /** 473 * Send a request to the native counterpart of ImeAdapter to paste the text from the clipboard. 474 * @return Whether the native counterpart of ImeAdapter received the call. 475 */ 476 public boolean paste() { 477 if (mNativeImeAdapterAndroid == 0) return false; 478 nativePaste(mNativeImeAdapterAndroid); 479 return true; 480 } 481 482 // Calls from C++ to Java 483 484 @CalledByNative 485 private static void initializeWebInputEvents(int eventTypeRawKeyDown, int eventTypeKeyUp, 486 int eventTypeChar, int modifierShift, int modifierAlt, int modifierCtrl, 487 int modifierCapsLockOn, int modifierNumLockOn) { 488 sEventTypeRawKeyDown = eventTypeRawKeyDown; 489 sEventTypeKeyUp = eventTypeKeyUp; 490 sEventTypeChar = eventTypeChar; 491 sModifierShift = modifierShift; 492 sModifierAlt = modifierAlt; 493 sModifierCtrl = modifierCtrl; 494 sModifierCapsLockOn = modifierCapsLockOn; 495 sModifierNumLockOn = modifierNumLockOn; 496 } 497 498 @CalledByNative 499 private static void initializeTextInputTypes(int textInputTypeNone, int textInputTypeText, 500 int textInputTypeTextArea, int textInputTypePassword, int textInputTypeSearch, 501 int textInputTypeUrl, int textInputTypeEmail, int textInputTypeTel, 502 int textInputTypeNumber, int textInputTypeContentEditable) { 503 sTextInputTypeNone = textInputTypeNone; 504 sTextInputTypeText = textInputTypeText; 505 sTextInputTypeTextArea = textInputTypeTextArea; 506 sTextInputTypePassword = textInputTypePassword; 507 sTextInputTypeSearch = textInputTypeSearch; 508 sTextInputTypeUrl = textInputTypeUrl; 509 sTextInputTypeEmail = textInputTypeEmail; 510 sTextInputTypeTel = textInputTypeTel; 511 sTextInputTypeNumber = textInputTypeNumber; 512 sTextInputTypeContentEditable = textInputTypeContentEditable; 513 } 514 515 @CalledByNative 516 private void cancelComposition() { 517 if (mInputConnection != null) { 518 mInputConnection.restartInput(); 519 } 520 } 521 522 @CalledByNative 523 void detach() { 524 mNativeImeAdapterAndroid = 0; 525 mTextInputType = 0; 526 } 527 528 private native boolean nativeSendSyntheticKeyEvent(int nativeImeAdapterAndroid, 529 int eventType, long timestampMs, int keyCode, int unicodeChar); 530 531 private native boolean nativeSendKeyEvent(int nativeImeAdapterAndroid, KeyEvent event, 532 int action, int modifiers, long timestampMs, int keyCode, boolean isSystemKey, 533 int unicodeChar); 534 535 private native void nativeSetComposingText(int nativeImeAdapterAndroid, String text, 536 int newCursorPosition); 537 538 private native void nativeCommitText(int nativeImeAdapterAndroid, String text); 539 540 private native void nativeFinishComposingText(int nativeImeAdapterAndroid); 541 542 private native void nativeAttachImeAdapter(int nativeImeAdapterAndroid); 543 544 private native void nativeSetEditableSelectionOffsets(int nativeImeAdapterAndroid, 545 int start, int end); 546 547 private native void nativeSetComposingRegion(int nativeImeAdapterAndroid, int start, int end); 548 549 private native void nativeDeleteSurroundingText(int nativeImeAdapterAndroid, 550 int before, int after); 551 552 private native void nativeUnselect(int nativeImeAdapterAndroid); 553 private native void nativeSelectAll(int nativeImeAdapterAndroid); 554 private native void nativeCut(int nativeImeAdapterAndroid); 555 private native void nativeCopy(int nativeImeAdapterAndroid); 556 private native void nativePaste(int nativeImeAdapterAndroid); 557 private native void nativeResetImeAdapter(int nativeImeAdapterAndroid); 558} 559