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