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