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