Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1995)

Unified Diff: content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnection.java

Issue 1278593004: Introduce ThreadedInputConnection behind a switch (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: removed ImeTest#testDoesNotHang_rendererCrashes which does not test anything Created 4 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnection.java
diff --git a/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnection.java b/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnection.java
new file mode 100644
index 0000000000000000000000000000000000000000..582da88d17f10f58dff3f06169adf132aa5221f0
--- /dev/null
+++ b/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnection.java
@@ -0,0 +1,608 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.content.browser.input;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+
+import org.chromium.base.Log;
+import org.chromium.base.ThreadUtils;
+import org.chromium.base.VisibleForTesting;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * An implementation of {@link InputConnection} to communicate with external input method
+ * apps. Note that it is running on IME thread (except for constructor and calls from ImeAdapter)
+ * such that it does not block UI thread and returns text values immediately after any change
+ * to them.
+ */
+public class ThreadedInputConnection implements ChromiumBaseInputConnection {
+ private static final String TAG = "cr_Ime";
+ private static final boolean DEBUG_LOGS = false;
+
+ private static final TextInputState UNBLOCKER = new TextInputState(
+ "", new Range(0, 0), new Range(-1, -1), false, false /* notFromIme */) {
+
+ @Override
+ public boolean shouldUnblock() {
+ return true;
+ }
+ };
+
+ private final Runnable mProcessPendingInputStatesRunnable = new Runnable() {
+ @Override
+ public void run() {
+ processPendingInputStates();
+ }
+ };
+
+ private final Runnable mMoveCursorSelectionEndRunnable = new Runnable() {
+ @Override
+ public void run() {
+ TextInputState textInputState = requestAndWaitForTextInputState();
+ if (textInputState == null) return;
+ Range selection = textInputState.selection();
+ setSelection(selection.end(), selection.end());
+ }
+ };
+
+ private final Runnable mRequestTextInputStateUpdate = new Runnable() {
+ @Override
+ public void run() {
+ boolean result = mImeAdapter.requestTextInputStateUpdate();
+ if (!result) unblockOnUiThread();
+ }
+ };
+
+ private final Runnable mNotifyUserActionRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mImeAdapter.notifyUserAction();
+ }
+ };
+
+ private final Runnable mFinishComposingTextRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mImeAdapter.finishComposingText();
+ }
+ };
+
+ private final ImeAdapter mImeAdapter;
+ private final Handler mHandler;
+ private int mNumNestedBatchEdits;
+
+ // TODO(changwan): check if we can keep a pool of TextInputState to avoid creating
+ // a bunch of new objects for each key stroke.
+ private final BlockingQueue<TextInputState> mQueue = new LinkedBlockingQueue<>();
+ private int mPendingAccent;
+
+ ThreadedInputConnection(ImeAdapter imeAdapter, Handler handler) {
+ if (DEBUG_LOGS) Log.w(TAG, "constructor");
+ ImeUtils.checkOnUiThread();
+ mImeAdapter = imeAdapter;
+ mHandler = handler;
+ }
+
+ void initializeOutAttrsOnUiThread(int inputType, int inputFlags, int selectionStart,
+ int selectionEnd, EditorInfo outAttrs) {
+ ImeUtils.checkOnUiThread();
+ mNumNestedBatchEdits = 0;
+ mPendingAccent = 0;
+ ImeUtils.computeEditorInfo(inputType, inputFlags, selectionStart, selectionEnd, outAttrs);
+ if (DEBUG_LOGS) {
+ Log.w(TAG, "initializeOutAttrs: " + ImeUtils.getEditorInfoDebugString(outAttrs));
+ }
+ }
+
+ @Override
+ public void updateStateOnUiThread(final String text, final int selectionStart,
+ final int selectionEnd, final int compositionStart, final int compositionEnd,
+ boolean singleLine, final boolean isNonImeChange) {
+ ImeUtils.checkOnUiThread();
+
+ final TextInputState newState =
+ new TextInputState(text, new Range(selectionStart, selectionEnd),
+ new Range(compositionStart, compositionEnd), singleLine, !isNonImeChange);
+ if (DEBUG_LOGS) Log.w(TAG, "updateState: %s", newState);
+
+ addToQueueOnUiThread(newState);
+ if (isNonImeChange) {
+ mHandler.post(mProcessPendingInputStatesRunnable);
+ }
+ }
+
+ /**
+ * @see ChromiumBaseInputConnection#getHandler()
+ */
+ @Override
+ public Handler getHandler() {
+ return mHandler;
+ }
+
+ /**
+ * @see ChromiumBaseInputConnection#onRestartInputOnUiThread()
+ */
+ @Override
+ public void onRestartInputOnUiThread() {}
+
+ /**
+ * @see ChromiumBaseInputConnection#sendKeyEventOnUiThread(KeyEvent)
+ */
+ @Override
+ public boolean sendKeyEventOnUiThread(final KeyEvent event) {
+ ImeUtils.checkOnUiThread();
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ sendKeyEvent(event);
+ }
+ });
+ return true;
+ }
+
+ /**
+ * @see ChromiumBaseInputConnection#moveCursorToSelectionEndOnUiThread()
+ */
+ @Override
+ public void moveCursorToSelectionEndOnUiThread() {
+ mHandler.post(mMoveCursorSelectionEndRunnable);
+ }
+
+ @Override
+ @VisibleForTesting
+ public void unblockOnUiThread() {
+ if (DEBUG_LOGS) Log.w(TAG, "unblockOnUiThread");
+ ImeUtils.checkOnUiThread();
+ addToQueueOnUiThread(UNBLOCKER);
+ mHandler.post(mProcessPendingInputStatesRunnable);
+ }
+
+ private void processPendingInputStates() {
+ if (DEBUG_LOGS) Log.w(TAG, "checkQueue");
+ assertOnImeThread();
+ // Handle all the remaining states in the queue.
+ while (true) {
+ TextInputState state = mQueue.poll();
+ if (state == null) {
+ if (DEBUG_LOGS) Log.w(TAG, "checkQueue - finished");
+ return;
+ }
+ // Unblocker was not used. Ignore.
+ if (state.shouldUnblock()) {
+ if (DEBUG_LOGS) Log.w(TAG, "checkQueue - ignoring one unblocker");
+ continue;
+ }
+ if (DEBUG_LOGS) Log.w(TAG, "checkQueue: " + state);
+ ImeUtils.checkCondition(!state.fromIme());
+ updateSelection(state);
+ }
+ }
+
+ private void updateSelection(TextInputState textInputState) {
+ if (textInputState == null) return;
+ assertOnImeThread();
+ if (mNumNestedBatchEdits != 0) return;
+ Range selection = textInputState.selection();
+ Range composition = textInputState.composition();
+ mImeAdapter.updateSelection(
+ selection.start(), selection.end(), composition.start(), composition.end());
+ }
+
+ private TextInputState requestAndWaitForTextInputState() {
+ if (DEBUG_LOGS) Log.w(TAG, "requestAndWaitForTextInputState");
+ ThreadUtils.postOnUiThread(mRequestTextInputStateUpdate);
+ return blockAndGetStateUpdate();
+ }
+
+ private void addToQueueOnUiThread(TextInputState textInputState) {
+ ImeUtils.checkOnUiThread();
+ try {
+ mQueue.put(textInputState);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "addToQueueOnUiThread interrupted", e);
+ }
+ if (DEBUG_LOGS) Log.w(TAG, "addToQueueOnUiThread finished: %d", mQueue.size());
+ }
+
+ /**
+ * @return BlockingQueue for white box unit testing.
+ */
+ BlockingQueue<TextInputState> getQueueForTest() {
+ return mQueue;
+ }
+
+ private void assertOnImeThread() {
+ ImeUtils.checkCondition(mHandler.getLooper() == Looper.myLooper());
+ }
+
+ /**
+ * Block until we get the expected state update.
+ * @return TextInputState if we get it successfully. null otherwise.
+ */
+ private TextInputState blockAndGetStateUpdate() {
+ if (DEBUG_LOGS) Log.w(TAG, "blockAndGetStateUpdate");
+ assertOnImeThread();
+ boolean shouldUpdateSelection = false;
+ while (true) {
+ TextInputState state;
+ try {
+ state = mQueue.take();
+ } catch (InterruptedException e) {
+ // This should never happen since IME thread is artificial and is not exposed
+ // to other components.
+ e.printStackTrace();
+ ImeUtils.checkCondition(false);
+ return null;
+ }
+ if (state.shouldUnblock()) {
+ if (DEBUG_LOGS) Log.w(TAG, "blockAndGetStateUpdate: unblocked");
+ return null;
+ } else if (state.fromIme()) {
+ if (shouldUpdateSelection) updateSelection(state);
+ if (DEBUG_LOGS) Log.w(TAG, "blockAndGetStateUpdate done: %d", mQueue.size());
+ return state;
+ }
+ // Ignore when state is not from IME, but make sure to update state when we handle
+ // state from IME.
+ shouldUpdateSelection = true;
+ }
+ }
+
+ private void notifyUserAction() {
+ ThreadUtils.postOnUiThread(mNotifyUserActionRunnable);
+ }
+
+ /**
+ * @see InputConnection#setComposingText(java.lang.CharSequence, int)
+ */
+ @Override
+ public boolean setComposingText(final CharSequence text, final int newCursorPosition) {
+ if (DEBUG_LOGS) Log.w(TAG, "setComposingText [%s] [%d]", text, newCursorPosition);
+ assertOnImeThread();
+ cancelCombiningAccent();
+ ThreadUtils.postOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mImeAdapter.sendCompositionToNative(text, newCursorPosition, false);
+ }
+ });
+ notifyUserAction();
+ return true;
+ }
+
+ /**
+ * @see InputConnection#commitText(java.lang.CharSequence, int)
+ */
+ @Override
+ public boolean commitText(final CharSequence text, final int newCursorPosition) {
+ if (DEBUG_LOGS) Log.w(TAG, "commitText [%s] [%d]", text, newCursorPosition);
+ assertOnImeThread();
+ cancelCombiningAccent();
+ ThreadUtils.postOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mImeAdapter.sendCompositionToNative(text, newCursorPosition, text.length() > 0);
+ }
+ });
+ notifyUserAction();
+ return true;
+ }
+
+ /**
+ * @see InputConnection#performEditorAction(int)
+ */
+ @Override
+ public boolean performEditorAction(final int actionCode) {
+ if (DEBUG_LOGS) Log.w(TAG, "performEditorAction [%d]", actionCode);
+ assertOnImeThread();
+ ThreadUtils.postOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mImeAdapter.performEditorAction(actionCode);
+ }
+ });
+ return true;
+ }
+
+ /**
+ * @see InputConnection#performContextMenuAction(int)
+ */
+ @Override
+ public boolean performContextMenuAction(final int id) {
+ if (DEBUG_LOGS) Log.w(TAG, "performContextMenuAction [%d]", id);
+ assertOnImeThread();
+ ThreadUtils.postOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mImeAdapter.performContextMenuAction(id);
+ }
+ });
+ return true;
+ }
+
+ /**
+ * @see InputConnection#getExtractedText(android.view.inputmethod.ExtractedTextRequest, int)
+ */
+ @Override
+ public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
+ if (DEBUG_LOGS) Log.w(TAG, "getExtractedText");
+ assertOnImeThread();
+ TextInputState textInputState = requestAndWaitForTextInputState();
+ if (textInputState == null) return null;
+ ExtractedText extractedText = new ExtractedText();
+ extractedText.text = textInputState.text();
+ extractedText.partialEndOffset = textInputState.text().length();
+ extractedText.selectionStart = textInputState.selection().start();
+ extractedText.selectionEnd = textInputState.selection().end();
+ extractedText.flags = textInputState.singleLine() ? ExtractedText.FLAG_SINGLE_LINE : 0;
+ return extractedText;
+ }
+
+ /**
+ * @see InputConnection#beginBatchEdit()
+ */
+ @Override
+ public boolean beginBatchEdit() {
+ if (DEBUG_LOGS) Log.w(TAG, "beginBatchEdit [%b]", (mNumNestedBatchEdits == 0));
+ assertOnImeThread();
+ mNumNestedBatchEdits++;
+ return true;
+ }
+
+ /**
+ * @see InputConnection#endBatchEdit()
+ */
+ @Override
+ public boolean endBatchEdit() {
+ assertOnImeThread();
+ if (mNumNestedBatchEdits == 0) return false;
+ --mNumNestedBatchEdits;
+ if (DEBUG_LOGS) Log.w(TAG, "endBatchEdit [%b]", (mNumNestedBatchEdits == 0));
+ if (mNumNestedBatchEdits == 0) {
+ updateSelection(requestAndWaitForTextInputState());
+ }
+ return mNumNestedBatchEdits != 0;
+ }
+
+ /**
+ * @see InputConnection#deleteSurroundingText(int, int)
+ */
+ @Override
+ public boolean deleteSurroundingText(final int beforeLength, final int afterLength) {
+ if (DEBUG_LOGS) Log.w(TAG, "deleteSurroundingText [%d %d]", beforeLength, afterLength);
+ assertOnImeThread();
+ if (mPendingAccent != 0) {
+ finishComposingText();
+ }
+ ThreadUtils.postOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mImeAdapter.deleteSurroundingText(beforeLength, afterLength);
+ }
+ });
+ return true;
+ }
+
+ /**
+ * @see InputConnection#sendKeyEvent(android.view.KeyEvent)
+ */
+ @Override
+ public boolean sendKeyEvent(final KeyEvent event) {
+ if (DEBUG_LOGS) Log.w(TAG, "sendKeyEvent [%d %d]", event.getAction(), event.getKeyCode());
+ assertOnImeThread();
+
+ if (handleCombiningAccent(event)) return true;
+
+ ThreadUtils.postOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mImeAdapter.sendKeyEvent(event);
+ }
+ });
+ notifyUserAction();
+ return true;
+ }
+
+ private boolean handleCombiningAccent(final KeyEvent event) {
+ // TODO(changwan): this will break the current composition. check if we can
+ // implement it in the renderer instead.
+ int action = event.getAction();
+ int unicodeChar = event.getUnicodeChar();
+
+ if (action != KeyEvent.ACTION_DOWN) return false;
+ if ((unicodeChar & KeyCharacterMap.COMBINING_ACCENT) != 0) {
+ int pendingAccent = unicodeChar & KeyCharacterMap.COMBINING_ACCENT_MASK;
+ StringBuilder builder = new StringBuilder();
+ builder.appendCodePoint(pendingAccent);
+ setComposingText(builder.toString(), 1);
+ mPendingAccent = pendingAccent;
+ return true;
+ } else if (mPendingAccent != 0 && unicodeChar != 0) {
+ int combined = KeyEvent.getDeadChar(mPendingAccent, unicodeChar);
+ if (combined != 0) {
+ StringBuilder builder = new StringBuilder();
+ builder.appendCodePoint(combined);
+ commitText(builder.toString(), 1);
+ return true;
+ }
+ // Noncombinable character; commit the accent character and fall through to sending
+ // the key event for the character afterwards.
+ finishComposingText();
+ }
+ return false;
+ }
+
+ private void cancelCombiningAccent() {
+ mPendingAccent = 0;
+ }
+
+ /**
+ * @see InputConnection#finishComposingText()
+ */
+ @Override
+ public boolean finishComposingText() {
+ if (DEBUG_LOGS) Log.w(TAG, "finishComposingText");
+ cancelCombiningAccent();
+ // This is the only function that may be called on UI thread because
+ // of direct calls from InputMethodManager.
+ ThreadUtils.postOnUiThread(mFinishComposingTextRunnable);
+ return true;
+ }
+
+ /**
+ * @see InputConnection#setSelection(int, int)
+ */
+ @Override
+ public boolean setSelection(final int start, final int end) {
+ if (DEBUG_LOGS) Log.w(TAG, "setSelection [%d %d]", start, end);
+ assertOnImeThread();
+ ThreadUtils.postOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mImeAdapter.setEditableSelectionOffsets(start, end);
+ }
+ });
+ return true;
+ }
+
+ /**
+ * @see InputConnection#setComposingRegion(int, int)
+ */
+ @Override
+ public boolean setComposingRegion(final int start, final int end) {
+ if (DEBUG_LOGS) Log.w(TAG, "setComposingRegion [%d %d]", start, end);
+ assertOnImeThread();
+ ThreadUtils.postOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mImeAdapter.setComposingRegion(start, end);
+ }
+ });
+ return true;
+ }
+
+ /**
+ * @see InputConnection#getTextBeforeCursor(int, int)
+ */
+ @Override
+ public CharSequence getTextBeforeCursor(int maxChars, int flags) {
+ if (DEBUG_LOGS) Log.w(TAG, "getTextBeforeCursor [%d %x]", maxChars, flags);
+ assertOnImeThread();
+ TextInputState textInputState = requestAndWaitForTextInputState();
+ if (textInputState == null) return null;
+ return textInputState.getTextBeforeSelection(maxChars);
+ }
+
+ /**
+ * @see InputConnection#getTextAfterCursor(int, int)
+ */
+ @Override
+ public CharSequence getTextAfterCursor(int maxChars, int flags) {
+ if (DEBUG_LOGS) Log.w(TAG, "getTextAfterCursor [%d %x]", maxChars, flags);
+ assertOnImeThread();
+ TextInputState textInputState = requestAndWaitForTextInputState();
+ if (textInputState == null) return null;
+ return textInputState.getTextAfterSelection(maxChars);
+ }
+
+ /**
+ * @see InputConnection#getSelectedText(int)
+ */
+ @Override
+ public CharSequence getSelectedText(int flags) {
+ if (DEBUG_LOGS) Log.w(TAG, "getSelectedText [%x]", flags);
+ assertOnImeThread();
+ TextInputState textInputState = requestAndWaitForTextInputState();
+ if (textInputState == null) return null;
+ return textInputState.getSelectedText();
+ }
+
+ /**
+ * @see InputConnection#getCursorCapsMode(int)
+ */
+ @Override
+ public int getCursorCapsMode(int reqModes) {
+ if (DEBUG_LOGS) Log.w(TAG, "getCursorCapsMode [%x]", reqModes);
+ assertOnImeThread();
+ // TODO(changwan): implement this.
+ return 0;
+ }
+
+ /**
+ * @see InputConnection#commitCompletion(android.view.inputmethod.CompletionInfo)
+ */
+ @Override
+ public boolean commitCompletion(CompletionInfo text) {
+ if (DEBUG_LOGS) Log.w(TAG, "commitCompletion [%s]", text);
+ assertOnImeThread();
+ return false;
+ }
+
+ /**
+ * @see InputConnection#commitCorrection(android.view.inputmethod.CorrectionInfo)
+ */
+ @Override
+ public boolean commitCorrection(CorrectionInfo correctionInfo) {
+ if (DEBUG_LOGS) {
+ Log.w(TAG, "commitCorrection [%s]", ImeUtils.getCorrectInfoDebugString(correctionInfo));
+ }
+ assertOnImeThread();
+ return false;
+ }
+
+ /**
+ * @see InputConnection#clearMetaKeyStates(int)
+ */
+ @Override
+ public boolean clearMetaKeyStates(int states) {
+ if (DEBUG_LOGS) Log.w(TAG, "clearMetaKeyStates [%x]", states);
+ assertOnImeThread();
+ return false;
+ }
+
+ /**
+ * @see InputConnection#reportFullscreenMode(boolean)
+ */
+ @Override
+ public boolean reportFullscreenMode(boolean enabled) {
+ if (DEBUG_LOGS) Log.w(TAG, "reportFullscreenMode [%b]", enabled);
+ // We ignore fullscreen mode for now. That's why we set
+ // EditorInfo.IME_FLAG_NO_FULLSCREEN in constructor.
+ // Note that this may be called on UI thread.
+ return false;
+ }
+
+ /**
+ * @see InputConnection#performPrivateCommand(java.lang.String, android.os.Bundle)
+ */
+ @Override
+ public boolean performPrivateCommand(String action, Bundle data) {
+ if (DEBUG_LOGS) Log.w(TAG, "performPrivateCommand [%s]", action);
+ assertOnImeThread();
+ return false;
+ }
+
+ /**
+ * @see InputConnection#requestCursorUpdates(int)
+ */
+ @Override
+ public boolean requestCursorUpdates(int cursorUpdateMode) {
+ if (DEBUG_LOGS) Log.w(TAG, "requestCursorUpdates [%x]", cursorUpdateMode);
+ assertOnImeThread();
+ return false;
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698