| OLD | NEW |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 package org.chromium.content.browser; | 5 package org.chromium.content.browser; |
| 6 | 6 |
| 7 import android.content.Context; | 7 import android.content.Context; |
| 8 import android.os.Handler; | 8 import android.os.Handler; |
| 9 import android.os.ResultReceiver; | 9 import android.os.ResultReceiver; |
| 10 import android.text.Editable; | 10 import android.text.Editable; |
| 11 import android.text.InputType; | 11 import android.text.InputType; |
| 12 import android.text.Selection; | 12 import android.text.Selection; |
| 13 import android.view.KeyCharacterMap; |
| 13 import android.view.KeyEvent; | 14 import android.view.KeyEvent; |
| 14 import android.view.View; | 15 import android.view.View; |
| 15 import android.view.inputmethod.BaseInputConnection; | 16 import android.view.inputmethod.BaseInputConnection; |
| 16 import android.view.inputmethod.EditorInfo; | 17 import android.view.inputmethod.EditorInfo; |
| 17 import android.view.inputmethod.ExtractedText; | 18 import android.view.inputmethod.ExtractedText; |
| 18 import android.view.inputmethod.ExtractedTextRequest; | 19 import android.view.inputmethod.ExtractedTextRequest; |
| 19 import android.view.inputmethod.InputMethodManager; | 20 import android.view.inputmethod.InputMethodManager; |
| 20 | 21 |
| 21 import org.chromium.base.CalledByNative; | 22 import org.chromium.base.CalledByNative; |
| 22 import org.chromium.base.JNINamespace; | 23 import org.chromium.base.JNINamespace; |
| 23 | 24 |
| 24 // We have to adapt and plumb android IME service and chrome text input API. | 25 /** |
| 25 // ImeAdapter provides an interface in both ways native <-> java: | 26 We have to adapt and plumb android IME service and chrome text input API. |
| 26 // 1. InputConnectionAdapter notifies native code of text composition state and | 27 ImeAdapter provides an interface in both ways native <-> java: |
| 27 // dispatch key events from java -> WebKit. | 28 1. InputConnectionAdapter notifies native code of text composition state and |
| 28 // 2. Native ImeAdapter notifies java side to clear composition text. | 29 dispatch key events from java -> WebKit. |
| 29 // | 30 2. Native ImeAdapter notifies java side to clear composition text. |
| 30 // The basic flow is: | 31 |
| 31 // 1. Intercept dispatchKeyEventPreIme() to record the current key event, but do | 32 The basic flow is: |
| 32 // nothing else. | 33 1. When InputConnectionAdapter gets called with composition or result text: |
| 33 // 2. When InputConnectionAdapter gets called with composition or result text: | 34 If we receive a composition text or a result text, then we just need to |
| 34 // a) If a key event has been recorded in dispatchKeyEventPreIme() and we | 35 dispatch a synthetic key event with special keycode 229, and then dispatch |
| 35 // receive a result text with single character, then we probably need to | 36 the composition or result text. |
| 36 // send the result text as a Char event rather than a ConfirmComposition | 37 2. Intercept dispatchKeyEvent() method for key events not handled by IME, we |
| 37 // event. So we need to dispatch the recorded key event followed by a | 38 need to dispatch them to webkit and check webkit's reply. Then inject a |
| 38 // synthetic Char event. | 39 new key event for further processing if webkit didn't handle it. |
| 39 // b) If we receive a composition text or a result text with more than one | 40 */ |
| 40 // characters, then no matter if we recorded a key event or not in | |
| 41 // dispatchKeyEventPreIme(), we just need to dispatch a synthetic key | |
| 42 // event with special keycode 229, and then dispatch the composition or | |
| 43 // result text. | |
| 44 // 3. Intercept dispatchKeyEvent() method for key events not handled by IME, we | |
| 45 // need to dispatch them to webkit and check webkit's reply. Then inject a | |
| 46 // new key event for further processing if webkit didn't handle it. | |
| 47 @JNINamespace("content") | 41 @JNINamespace("content") |
| 48 class ImeAdapter { | 42 class ImeAdapter { |
| 49 interface ViewEmbedder { | 43 interface ViewEmbedder { |
| 50 /** | 44 /** |
| 51 * @param isFinish whether the event is occuring because input is finish
ed. | 45 * @param isFinish whether the event is occuring because input is finish
ed. |
| 52 */ | 46 */ |
| 53 public void onImeEvent(boolean isFinish); | 47 public void onImeEvent(boolean isFinish); |
| 54 public void onSetFieldValue(); | 48 public void onSetFieldValue(); |
| 55 public void onDismissInput(); | 49 public void onDismissInput(); |
| 56 public View getAttachedView(); | 50 public View getAttachedView(); |
| (...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 110 sTextInputTypeTel = textInputTypeTel; | 104 sTextInputTypeTel = textInputTypeTel; |
| 111 sTextInputTypeNumber = textInputTypeNumber; | 105 sTextInputTypeNumber = textInputTypeNumber; |
| 112 sTextInputTypeWeek = textInputTypeWeek; | 106 sTextInputTypeWeek = textInputTypeWeek; |
| 113 sTextInputTypeContentEditable = textInputTypeContentEditable; | 107 sTextInputTypeContentEditable = textInputTypeContentEditable; |
| 114 InputDialogContainer.initializeInputTypes(textInputTypeDate, textInputTy
peDateTime, | 108 InputDialogContainer.initializeInputTypes(textInputTypeDate, textInputTy
peDateTime, |
| 115 textInputTypeDateTimeLocal, textInputTypeMonth, textInputTypeTim
e); | 109 textInputTypeDateTimeLocal, textInputTypeMonth, textInputTypeTim
e); |
| 116 } | 110 } |
| 117 | 111 |
| 118 private int mNativeImeAdapterAndroid; | 112 private int mNativeImeAdapterAndroid; |
| 119 private int mTextInputType; | 113 private int mTextInputType; |
| 120 private int mPreImeEventCount; | |
| 121 | 114 |
| 122 private Context mContext; | 115 private Context mContext; |
| 123 private SelectionHandleController mSelectionHandleController; | 116 private SelectionHandleController mSelectionHandleController; |
| 124 private InsertionHandleController mInsertionHandleController; | 117 private InsertionHandleController mInsertionHandleController; |
| 125 private AdapterInputConnection mInputConnection; | 118 private AdapterInputConnection mInputConnection; |
| 126 private ViewEmbedder mViewEmbedder; | 119 private ViewEmbedder mViewEmbedder; |
| 127 private Handler mHandler; | 120 private Handler mHandler; |
| 128 private InputDialogContainer mInputDialogContainer; | 121 private InputDialogContainer mInputDialogContainer; |
| 129 | 122 |
| 130 private class DelayedDismissInput implements Runnable { | 123 private class DelayedDismissInput implements Runnable { |
| (...skipping 14 matching lines...) Expand all Loading... |
| 145 | 138 |
| 146 // Delay introduced to avoid hiding the keyboard if new show requests are re
ceived. | 139 // Delay introduced to avoid hiding the keyboard if new show requests are re
ceived. |
| 147 // The time required by the unfocus-focus events triggered by tab has been m
easured in soju: | 140 // The time required by the unfocus-focus events triggered by tab has been m
easured in soju: |
| 148 // Mean: 18.633 ms, Standard deviation: 7.9837 ms. | 141 // Mean: 18.633 ms, Standard deviation: 7.9837 ms. |
| 149 // The value here should be higher enough to cover these cases, but not too
high to avoid | 142 // The value here should be higher enough to cover these cases, but not too
high to avoid |
| 150 // letting the user perceiving important delays. | 143 // letting the user perceiving important delays. |
| 151 private static final int INPUT_DISMISS_DELAY = 150; | 144 private static final int INPUT_DISMISS_DELAY = 150; |
| 152 | 145 |
| 153 ImeAdapter(Context context, SelectionHandleController selectionHandleControl
ler, | 146 ImeAdapter(Context context, SelectionHandleController selectionHandleControl
ler, |
| 154 InsertionHandleController insertionHandleController, ViewEmbedder em
bedder) { | 147 InsertionHandleController insertionHandleController, ViewEmbedder em
bedder) { |
| 155 mPreImeEventCount = 0; | |
| 156 mContext = context; | 148 mContext = context; |
| 157 mSelectionHandleController = selectionHandleController; | 149 mSelectionHandleController = selectionHandleController; |
| 158 mInsertionHandleController = insertionHandleController; | 150 mInsertionHandleController = insertionHandleController; |
| 159 mViewEmbedder = embedder; | 151 mViewEmbedder = embedder; |
| 160 mHandler = new Handler(); | 152 mHandler = new Handler(); |
| 161 mInputDialogContainer = new InputDialogContainer(context, | 153 mInputDialogContainer = new InputDialogContainer(context, |
| 162 new InputDialogContainer.InputActionDelegate() { | 154 new InputDialogContainer.InputActionDelegate() { |
| 163 | 155 |
| 164 @Override | 156 @Override |
| 165 public void replaceDateTime(String text) { | 157 public void replaceDateTime(String text) { |
| (...skipping 129 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 295 } | 287 } |
| 296 | 288 |
| 297 boolean hasTextInputType() { | 289 boolean hasTextInputType() { |
| 298 return isTextInputType(mTextInputType); | 290 return isTextInputType(mTextInputType); |
| 299 } | 291 } |
| 300 | 292 |
| 301 boolean hasDialogInputType() { | 293 boolean hasDialogInputType() { |
| 302 return InputDialogContainer.isDialogInputType(mTextInputType); | 294 return InputDialogContainer.isDialogInputType(mTextInputType); |
| 303 } | 295 } |
| 304 | 296 |
| 305 void dispatchKeyEventPreIme(KeyEvent event) { | |
| 306 // We only register that a key was pressed, but we don't actually interc
ept | |
| 307 // it. | |
| 308 ++mPreImeEventCount; | |
| 309 } | |
| 310 | |
| 311 boolean dispatchKeyEvent(KeyEvent event) { | 297 boolean dispatchKeyEvent(KeyEvent event) { |
| 312 mPreImeEventCount = 0; | |
| 313 return translateAndSendNativeEvents(event); | 298 return translateAndSendNativeEvents(event); |
| 314 } | 299 } |
| 315 | 300 |
| 316 void commitText() { | 301 void commitText() { |
| 317 cancelComposition(); | 302 cancelComposition(); |
| 318 if (mNativeImeAdapterAndroid != 0) { | 303 if (mNativeImeAdapterAndroid != 0) { |
| 319 nativeCommitText(mNativeImeAdapterAndroid, ""); | 304 nativeCommitText(mNativeImeAdapterAndroid, ""); |
| 320 } | 305 } |
| 321 } | 306 } |
| 322 | 307 |
| (...skipping 11 matching lines...) Expand all Loading... |
| 334 return false; | 319 return false; |
| 335 } | 320 } |
| 336 | 321 |
| 337 // Committing an empty string finishes the current composition. | 322 // Committing an empty string finishes the current composition. |
| 338 boolean isFinish = text.isEmpty(); | 323 boolean isFinish = text.isEmpty(); |
| 339 if (!isFinish) { | 324 if (!isFinish) { |
| 340 mSelectionHandleController.hideAndDisallowAutomaticShowing(); | 325 mSelectionHandleController.hideAndDisallowAutomaticShowing(); |
| 341 mInsertionHandleController.hideAndDisallowAutomaticShowing(); | 326 mInsertionHandleController.hideAndDisallowAutomaticShowing(); |
| 342 } | 327 } |
| 343 mViewEmbedder.onImeEvent(isFinish); | 328 mViewEmbedder.onImeEvent(isFinish); |
| 344 boolean hasSingleChar = mPreImeEventCount == 1 && text.length() == 1; | 329 int keyCode = shouldSendKeyEventWithKeyCode(text); |
| 345 int keyCode = hasSingleChar ? text.codePointAt(0) : COMPOSITION_KEY_CODE
; | |
| 346 int keyChar = hasSingleChar ? text.codePointAt(0) : 0; | |
| 347 long timeStampMs = System.currentTimeMillis(); | 330 long timeStampMs = System.currentTimeMillis(); |
| 348 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeRawKeyDo
wn, | 331 |
| 349 timeStampMs, keyCode, keyChar); | 332 if (keyCode != COMPOSITION_KEY_CODE) { |
| 350 if (hasSingleChar) { | 333 sendKeyEventWithKeyCode(keyCode, |
| 351 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeChar
, | 334 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)
; |
| 352 timeStampMs, text.codePointAt(0), text.codePointAt(0)); | |
| 353 } else { | 335 } else { |
| 336 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeRawK
eyDown, |
| 337 timeStampMs, keyCode, 0); |
| 354 if (isCommit) { | 338 if (isCommit) { |
| 355 nativeCommitText(mNativeImeAdapterAndroid, text); | 339 nativeCommitText(mNativeImeAdapterAndroid, text); |
| 356 } else { | 340 } else { |
| 357 nativeSetComposingText(mNativeImeAdapterAndroid, text, newCursor
Position); | 341 nativeSetComposingText(mNativeImeAdapterAndroid, text, newCursor
Position); |
| 358 } | 342 } |
| 343 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeKeyU
p, |
| 344 timeStampMs, keyCode, 0); |
| 359 } | 345 } |
| 360 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeKeyUp, | 346 |
| 361 timeStampMs, keyCode, keyChar); | |
| 362 mPreImeEventCount = 0; | |
| 363 return true; | 347 return true; |
| 364 } | 348 } |
| 365 | 349 |
| 350 private int shouldSendKeyEventWithKeyCode(String text) { |
| 351 if (text.length() != 1) return COMPOSITION_KEY_CODE; |
| 352 |
| 353 if (text.equals("\n")) return KeyEvent.KEYCODE_ENTER; |
| 354 else if (text.equals("\t")) return KeyEvent.KEYCODE_TAB; |
| 355 else return COMPOSITION_KEY_CODE; |
| 356 } |
| 357 |
| 358 private void sendKeyEventWithKeyCode(int keyCode, int flags) { |
| 359 long eventTime = System.currentTimeMillis(); |
| 360 translateAndSendNativeEvents(new KeyEvent(eventTime, eventTime, |
| 361 KeyEvent.ACTION_DOWN, keyCode, 0, 0, |
| 362 KeyCharacterMap.VIRTUAL_KEYBOARD, 0, |
| 363 flags)); |
| 364 translateAndSendNativeEvents(new KeyEvent(System.currentTimeMillis(), ev
entTime, |
| 365 KeyEvent.ACTION_UP, keyCode, 0, 0, |
| 366 KeyCharacterMap.VIRTUAL_KEYBOARD, 0, |
| 367 flags)); |
| 368 } |
| 369 |
| 366 private boolean translateAndSendNativeEvents(KeyEvent event) { | 370 private boolean translateAndSendNativeEvents(KeyEvent event) { |
| 367 if (mNativeImeAdapterAndroid == 0) { | 371 if (mNativeImeAdapterAndroid == 0) { |
| 368 return false; | 372 return false; |
| 369 } | 373 } |
| 370 int action = event.getAction(); | 374 int action = event.getAction(); |
| 371 if (action != KeyEvent.ACTION_DOWN && | 375 if (action != KeyEvent.ACTION_DOWN && |
| 372 action != KeyEvent.ACTION_UP) { | 376 action != KeyEvent.ACTION_UP) { |
| 373 // action == KeyEvent.ACTION_MULTIPLE | 377 // action == KeyEvent.ACTION_MULTIPLE |
| 374 // TODO(bulach): confirm the actual behavior. Apparently: | 378 // TODO(bulach): confirm the actual behavior. Apparently: |
| 375 // If event.getKeyCode() == KEYCODE_UNKNOWN, we can send a | 379 // If event.getKeyCode() == KEYCODE_UNKNOWN, we can send a |
| (...skipping 226 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 602 // Send TAB key event | 606 // Send TAB key event |
| 603 long timeStampMs = System.currentTimeMillis(); | 607 long timeStampMs = System.currentTimeMillis(); |
| 604 mImeAdapter.sendSyntheticKeyEvent( | 608 mImeAdapter.sendSyntheticKeyEvent( |
| 605 sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_
TAB, 0); | 609 sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_
TAB, 0); |
| 606 return true; | 610 return true; |
| 607 case EditorInfo.IME_ACTION_GO: | 611 case EditorInfo.IME_ACTION_GO: |
| 608 case EditorInfo.IME_ACTION_SEARCH: | 612 case EditorInfo.IME_ACTION_SEARCH: |
| 609 mImeAdapter.dismissInput(true); | 613 mImeAdapter.dismissInput(true); |
| 610 break; | 614 break; |
| 611 } | 615 } |
| 612 | 616 mImeAdapter.sendKeyEventWithKeyCode(KeyEvent.KEYCODE_ENTER, |
| 613 return super.performEditorAction(actionCode); | 617 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE |
| 618 | KeyEvent.FLAG_EDITOR_ACTION); |
| 619 return true; |
| 614 } | 620 } |
| 615 | 621 |
| 616 @Override | 622 @Override |
| 617 public boolean performContextMenuAction(int id) { | 623 public boolean performContextMenuAction(int id) { |
| 618 switch (id) { | 624 switch (id) { |
| 619 case android.R.id.selectAll: | 625 case android.R.id.selectAll: |
| 620 return mImeAdapter.selectAll(); | 626 return mImeAdapter.selectAll(); |
| 621 case android.R.id.cut: | 627 case android.R.id.cut: |
| 622 return mImeAdapter.cut(); | 628 return mImeAdapter.cut(); |
| 623 case android.R.id.copy: | 629 case android.R.id.copy: |
| (...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 674 if (selectionStart > selectionEnd) { | 680 if (selectionStart > selectionEnd) { |
| 675 int temp = selectionStart; | 681 int temp = selectionStart; |
| 676 selectionStart = selectionEnd; | 682 selectionStart = selectionEnd; |
| 677 selectionEnd = temp; | 683 selectionEnd = temp; |
| 678 } | 684 } |
| 679 editable.replace(selectionStart, selectionEnd, | 685 editable.replace(selectionStart, selectionEnd, |
| 680 Character.toString((char)unicodeChar)); | 686 Character.toString((char)unicodeChar)); |
| 681 } | 687 } |
| 682 } | 688 } |
| 683 } | 689 } |
| 684 return super.sendKeyEvent(event); | 690 mImeAdapter.translateAndSendNativeEvents(event); |
| 691 return true; |
| 685 } | 692 } |
| 686 | 693 |
| 687 @Override | 694 @Override |
| 688 public boolean finishComposingText() { | 695 public boolean finishComposingText() { |
| 689 if (mEditable == null | 696 if (mEditable == null |
| 690 || (getComposingSpanStart(mEditable) == getComposingSpanEnd(
mEditable))) { | 697 || (getComposingSpanStart(mEditable) == getComposingSpanEnd(
mEditable))) { |
| 691 return true; | 698 return true; |
| 692 } | 699 } |
| 693 super.finishComposingText(); | 700 super.finishComposingText(); |
| 694 return mImeAdapter.checkCompositionQueueAndCallNative("", 0, true); | 701 return mImeAdapter.checkCompositionQueueAndCallNative("", 0, true); |
| (...skipping 109 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 804 | 811 |
| 805 private native void nativeDeleteSurroundingText(int nativeImeAdapterAndroid, | 812 private native void nativeDeleteSurroundingText(int nativeImeAdapterAndroid, |
| 806 int before, int after); | 813 int before, int after); |
| 807 | 814 |
| 808 private native void nativeUnselect(int nativeImeAdapterAndroid); | 815 private native void nativeUnselect(int nativeImeAdapterAndroid); |
| 809 private native void nativeSelectAll(int nativeImeAdapterAndroid); | 816 private native void nativeSelectAll(int nativeImeAdapterAndroid); |
| 810 private native void nativeCut(int nativeImeAdapterAndroid); | 817 private native void nativeCut(int nativeImeAdapterAndroid); |
| 811 private native void nativeCopy(int nativeImeAdapterAndroid); | 818 private native void nativeCopy(int nativeImeAdapterAndroid); |
| 812 private native void nativePaste(int nativeImeAdapterAndroid); | 819 private native void nativePaste(int nativeImeAdapterAndroid); |
| 813 } | 820 } |
| OLD | NEW |