ImeAdapter.java revision 5c02ac1a9c1b504631c0a3d2b6e737b5d738bae1
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 public boolean dispatchKeyEvent(KeyEvent event) { 299 return translateAndSendNativeEvents(event); 300 } 301 302 private int shouldSendKeyEventWithKeyCode(String text) { 303 if (text.length() != 1) return COMPOSITION_KEY_CODE; 304 305 if (text.equals("\n")) return KeyEvent.KEYCODE_ENTER; 306 else if (text.equals("\t")) return KeyEvent.KEYCODE_TAB; 307 else return COMPOSITION_KEY_CODE; 308 } 309 310 void sendKeyEventWithKeyCode(int keyCode, int flags) { 311 long eventTime = SystemClock.uptimeMillis(); 312 translateAndSendNativeEvents(new KeyEvent(eventTime, eventTime, 313 KeyEvent.ACTION_DOWN, keyCode, 0, 0, 314 KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 315 flags)); 316 translateAndSendNativeEvents(new KeyEvent(SystemClock.uptimeMillis(), eventTime, 317 KeyEvent.ACTION_UP, keyCode, 0, 0, 318 KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 319 flags)); 320 } 321 322 // Calls from Java to C++ 323 324 boolean checkCompositionQueueAndCallNative(String text, int newCursorPosition, 325 boolean isCommit) { 326 if (mNativeImeAdapterAndroid == 0) return false; 327 328 // Committing an empty string finishes the current composition. 329 boolean isFinish = text.isEmpty(); 330 mViewEmbedder.onImeEvent(isFinish); 331 int keyCode = shouldSendKeyEventWithKeyCode(text); 332 long timeStampMs = SystemClock.uptimeMillis(); 333 334 if (keyCode != COMPOSITION_KEY_CODE) { 335 sendKeyEventWithKeyCode(keyCode, 336 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE); 337 } else { 338 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeRawKeyDown, 339 timeStampMs, keyCode, 0); 340 if (isCommit) { 341 nativeCommitText(mNativeImeAdapterAndroid, text); 342 } else { 343 nativeSetComposingText(mNativeImeAdapterAndroid, text, newCursorPosition); 344 } 345 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeKeyUp, 346 timeStampMs, keyCode, 0); 347 } 348 349 return true; 350 } 351 352 void finishComposingText() { 353 if (mNativeImeAdapterAndroid == 0) return; 354 nativeFinishComposingText(mNativeImeAdapterAndroid); 355 } 356 357 boolean translateAndSendNativeEvents(KeyEvent event) { 358 if (mNativeImeAdapterAndroid == 0) return false; 359 360 int action = event.getAction(); 361 if (action != KeyEvent.ACTION_DOWN && 362 action != KeyEvent.ACTION_UP) { 363 // action == KeyEvent.ACTION_MULTIPLE 364 // TODO(bulach): confirm the actual behavior. Apparently: 365 // If event.getKeyCode() == KEYCODE_UNKNOWN, we can send a 366 // composition key down (229) followed by a commit text with the 367 // string from event.getUnicodeChars(). 368 // Otherwise, we'd need to send an event with a 369 // WebInputEvent::IsAutoRepeat modifier. We also need to verify when 370 // we receive ACTION_MULTIPLE: we may receive it after an ACTION_DOWN, 371 // and if that's the case, we'll need to review when to send the Char 372 // event. 373 return false; 374 } 375 mViewEmbedder.onImeEvent(false); 376 return nativeSendKeyEvent(mNativeImeAdapterAndroid, event, event.getAction(), 377 getModifiers(event.getMetaState()), event.getEventTime(), event.getKeyCode(), 378 event.isSystem(), event.getUnicodeChar()); 379 } 380 381 boolean sendSyntheticKeyEvent(int eventType, long timestampMs, int keyCode, int unicodeChar) { 382 if (mNativeImeAdapterAndroid == 0) return false; 383 384 nativeSendSyntheticKeyEvent( 385 mNativeImeAdapterAndroid, eventType, timestampMs, keyCode, unicodeChar); 386 return true; 387 } 388 389 /** 390 * Send a request to the native counterpart to delete a given range of characters. 391 * @param beforeLength Number of characters to extend the selection by before the existing 392 * selection. 393 * @param afterLength Number of characters to extend the selection by after the existing 394 * selection. 395 * @return Whether the native counterpart of ImeAdapter received the call. 396 */ 397 boolean deleteSurroundingText(int beforeLength, int afterLength) { 398 if (mNativeImeAdapterAndroid == 0) return false; 399 nativeDeleteSurroundingText(mNativeImeAdapterAndroid, beforeLength, afterLength); 400 return true; 401 } 402 403 /** 404 * Send a request to the native counterpart to set the selection to given range. 405 * @param start Selection start index. 406 * @param end Selection end index. 407 * @return Whether the native counterpart of ImeAdapter received the call. 408 */ 409 boolean setEditableSelectionOffsets(int start, int end) { 410 if (mNativeImeAdapterAndroid == 0) return false; 411 nativeSetEditableSelectionOffsets(mNativeImeAdapterAndroid, start, end); 412 return true; 413 } 414 415 /** 416 * Send a request to the native counterpart to set compositing region to given indices. 417 * @param start The start of the composition. 418 * @param end The end of the composition. 419 * @return Whether the native counterpart of ImeAdapter received the call. 420 */ 421 boolean setComposingRegion(int start, int end) { 422 if (mNativeImeAdapterAndroid == 0) return false; 423 nativeSetComposingRegion(mNativeImeAdapterAndroid, start, end); 424 return true; 425 } 426 427 /** 428 * Send a request to the native counterpart to unselect text. 429 * @return Whether the native counterpart of ImeAdapter received the call. 430 */ 431 public boolean unselect() { 432 if (mNativeImeAdapterAndroid == 0) return false; 433 nativeUnselect(mNativeImeAdapterAndroid); 434 return true; 435 } 436 437 /** 438 * Send a request to the native counterpart of ImeAdapter to select all the text. 439 * @return Whether the native counterpart of ImeAdapter received the call. 440 */ 441 public boolean selectAll() { 442 if (mNativeImeAdapterAndroid == 0) return false; 443 nativeSelectAll(mNativeImeAdapterAndroid); 444 return true; 445 } 446 447 /** 448 * Send a request to the native counterpart of ImeAdapter to cut the selected text. 449 * @return Whether the native counterpart of ImeAdapter received the call. 450 */ 451 public boolean cut() { 452 if (mNativeImeAdapterAndroid == 0) return false; 453 nativeCut(mNativeImeAdapterAndroid); 454 return true; 455 } 456 457 /** 458 * Send a request to the native counterpart of ImeAdapter to copy the selected text. 459 * @return Whether the native counterpart of ImeAdapter received the call. 460 */ 461 public boolean copy() { 462 if (mNativeImeAdapterAndroid == 0) return false; 463 nativeCopy(mNativeImeAdapterAndroid); 464 return true; 465 } 466 467 /** 468 * Send a request to the native counterpart of ImeAdapter to paste the text from the clipboard. 469 * @return Whether the native counterpart of ImeAdapter received the call. 470 */ 471 public boolean paste() { 472 if (mNativeImeAdapterAndroid == 0) return false; 473 nativePaste(mNativeImeAdapterAndroid); 474 return true; 475 } 476 477 // Calls from C++ to Java 478 479 @CalledByNative 480 private static void initializeWebInputEvents(int eventTypeRawKeyDown, int eventTypeKeyUp, 481 int eventTypeChar, int modifierShift, int modifierAlt, int modifierCtrl, 482 int modifierCapsLockOn, int modifierNumLockOn) { 483 sEventTypeRawKeyDown = eventTypeRawKeyDown; 484 sEventTypeKeyUp = eventTypeKeyUp; 485 sEventTypeChar = eventTypeChar; 486 sModifierShift = modifierShift; 487 sModifierAlt = modifierAlt; 488 sModifierCtrl = modifierCtrl; 489 sModifierCapsLockOn = modifierCapsLockOn; 490 sModifierNumLockOn = modifierNumLockOn; 491 } 492 493 @CalledByNative 494 private static void initializeTextInputTypes(int textInputTypeNone, int textInputTypeText, 495 int textInputTypeTextArea, int textInputTypePassword, int textInputTypeSearch, 496 int textInputTypeUrl, int textInputTypeEmail, int textInputTypeTel, 497 int textInputTypeNumber, int textInputTypeContentEditable) { 498 sTextInputTypeNone = textInputTypeNone; 499 sTextInputTypeText = textInputTypeText; 500 sTextInputTypeTextArea = textInputTypeTextArea; 501 sTextInputTypePassword = textInputTypePassword; 502 sTextInputTypeSearch = textInputTypeSearch; 503 sTextInputTypeUrl = textInputTypeUrl; 504 sTextInputTypeEmail = textInputTypeEmail; 505 sTextInputTypeTel = textInputTypeTel; 506 sTextInputTypeNumber = textInputTypeNumber; 507 sTextInputTypeContentEditable = textInputTypeContentEditable; 508 } 509 510 @CalledByNative 511 private void focusedNodeChanged(boolean isEditable) { 512 if (mInputConnection != null && isEditable) mInputConnection.restartInput(); 513 } 514 515 @CalledByNative 516 private void cancelComposition() { 517 if (mInputConnection != null) mInputConnection.restartInput(); 518 } 519 520 @CalledByNative 521 void detach() { 522 if (mDismissInput != null) mHandler.removeCallbacks(mDismissInput); 523 mNativeImeAdapterAndroid = 0; 524 mTextInputType = 0; 525 } 526 527 private native boolean nativeSendSyntheticKeyEvent(long nativeImeAdapterAndroid, 528 int eventType, long timestampMs, int keyCode, int unicodeChar); 529 530 private native boolean nativeSendKeyEvent(long nativeImeAdapterAndroid, KeyEvent event, 531 int action, int modifiers, long timestampMs, int keyCode, boolean isSystemKey, 532 int unicodeChar); 533 534 private native void nativeSetComposingText(long nativeImeAdapterAndroid, String text, 535 int newCursorPosition); 536 537 private native void nativeCommitText(long nativeImeAdapterAndroid, String text); 538 539 private native void nativeFinishComposingText(long nativeImeAdapterAndroid); 540 541 private native void nativeAttachImeAdapter(long nativeImeAdapterAndroid); 542 543 private native void nativeSetEditableSelectionOffsets(long nativeImeAdapterAndroid, 544 int start, int end); 545 546 private native void nativeSetComposingRegion(long nativeImeAdapterAndroid, int start, int end); 547 548 private native void nativeDeleteSurroundingText(long nativeImeAdapterAndroid, 549 int before, int after); 550 551 private native void nativeUnselect(long nativeImeAdapterAndroid); 552 private native void nativeSelectAll(long nativeImeAdapterAndroid); 553 private native void nativeCut(long nativeImeAdapterAndroid); 554 private native void nativeCopy(long nativeImeAdapterAndroid); 555 private native void nativePaste(long nativeImeAdapterAndroid); 556 private native void nativeResetImeAdapter(long nativeImeAdapterAndroid); 557} 558