| Index: content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnectionFactory.java
|
| diff --git a/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnectionFactory.java b/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnectionFactory.java
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..b79340864c28d8e2f3fbe2ea6e70aeed498e6139
|
| --- /dev/null
|
| +++ b/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnectionFactory.java
|
| @@ -0,0 +1,156 @@
|
| +// 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.Handler;
|
| +import android.os.HandlerThread;
|
| +import android.view.View;
|
| +import android.view.inputmethod.EditorInfo;
|
| +
|
| +import org.chromium.base.Log;
|
| +import org.chromium.base.VisibleForTesting;
|
| +
|
| +/**
|
| + * A factory class for {@link ThreadedInputConnection}. The class also includes triggering
|
| + * mechanism (hack) to run our InputConnection on non-UI thread.
|
| + */
|
| +// TODO(changwan): add unit tests once Robolectric supports Android API level >= 21.
|
| +// See crbug.com/588547 for details.
|
| +public class ThreadedInputConnectionFactory implements ChromiumBaseInputConnection.Factory {
|
| + private static final String TAG = "cr_Ime";
|
| + private static final boolean DEBUG_LOGS = false;
|
| +
|
| + private final Handler mHandler;
|
| + private final InputMethodManagerWrapper mInputMethodManagerWrapper;
|
| + private final InputMethodUma mInputMethodUma;
|
| + private ThreadedInputConnectionProxyView mProxyView;
|
| + private ThreadedInputConnection mThreadedInputConnection;
|
| +
|
| + ThreadedInputConnectionFactory(
|
| + InputMethodManagerWrapper inputMethodManagerWrapper) {
|
| + mInputMethodManagerWrapper = inputMethodManagerWrapper;
|
| + mHandler = createHandler();
|
| + mInputMethodUma = createInputMethodUma();
|
| + }
|
| +
|
| + @VisibleForTesting
|
| + protected Handler createHandler() {
|
| + HandlerThread thread =
|
| + new HandlerThread("InputConnectionHandlerThread", HandlerThread.NORM_PRIORITY);
|
| + thread.start();
|
| + return new Handler(thread.getLooper());
|
| + }
|
| +
|
| + @VisibleForTesting
|
| + protected ThreadedInputConnectionProxyView createProxyView(
|
| + Handler handler, View containerView) {
|
| + return new ThreadedInputConnectionProxyView(
|
| + containerView.getContext(), handler, containerView);
|
| + }
|
| +
|
| + @VisibleForTesting
|
| + protected InputMethodUma createInputMethodUma() {
|
| + return new InputMethodUma();
|
| + }
|
| +
|
| + private boolean shouldTriggerDelayedOnCreateInputConnection() {
|
| + // Note that ThreadedInputConnectionProxyView intentionally calls
|
| + // View#onCreateInputConnection() and not a separate method in this class.
|
| + // There are third party apps that override WebView#onCreateInputConnection(),
|
| + // and we still want to call them for consistency. The setback here is that the only
|
| + // way to distinguish calls from InputMethodManager and from ProxyView is by looking at
|
| + // the call stack.
|
| + for (StackTraceElement ste : Thread.currentThread().getStackTrace()) {
|
| + String className = ste.getClassName();
|
| + if (className != null
|
| + && (className.contains(ThreadedInputConnectionProxyView.class.getName())
|
| + || className.contains("TestInputMethodManagerWrapper"))) {
|
| + return false;
|
| + }
|
| + }
|
| + return true;
|
| + }
|
| +
|
| + @Override
|
| + public ThreadedInputConnection initializeAndGet(
|
| + View view, ImeAdapter imeAdapter, int inputType, int inputFlags, int selectionStart,
|
| + int selectionEnd, EditorInfo outAttrs) {
|
| + ImeUtils.checkOnUiThread();
|
| + if (shouldTriggerDelayedOnCreateInputConnection()) {
|
| + triggerDelayedOnCreateInputConnection(view);
|
| + return null;
|
| + }
|
| + if (DEBUG_LOGS) Log.w(TAG, "initializeAndGet: called from proxy view");
|
| + if (mThreadedInputConnection == null) {
|
| + if (DEBUG_LOGS) Log.w(TAG, "Creating ThreadedInputConnection...");
|
| + mThreadedInputConnection = new ThreadedInputConnection(imeAdapter, mHandler);
|
| + }
|
| + mThreadedInputConnection.initializeOutAttrsOnUiThread(inputType, inputFlags,
|
| + selectionStart, selectionEnd, outAttrs);
|
| + return mThreadedInputConnection;
|
| + }
|
| +
|
| + private void triggerDelayedOnCreateInputConnection(final View view) {
|
| + if (DEBUG_LOGS) Log.w(TAG, "triggerDelayedOnCreateInputConnection");
|
| + if (mProxyView == null) {
|
| + mProxyView = createProxyView(mHandler, view);
|
| + }
|
| + mProxyView.requestFocus();
|
| + view.getHandler().post(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + // This is a hack to make InputMethodManager believe that the proxy view
|
| + // now has a focus. As a result, InputMethodManager will think that mProxyView
|
| + // is focused, and will call getHandler() of the view when creating input
|
| + // connection.
|
| +
|
| + // Step 1: Set mProxyView as InputMethodManager#mNextServedView.
|
| + mProxyView.onWindowFocusChanged(true);
|
| +
|
| + // Step 2: Have InputMethodManager focus in on mNextServedView.
|
| + // As a result, IMM will call onCreateInputConnection() on mProxyView on the same
|
| + // thread as mProxyView.getHandler(). It will also call subsequent InputConnection
|
| + // methods on this IME thread.
|
| + mInputMethodManagerWrapper.isActive(view);
|
| +
|
| + // Step 3: Check that the above hack worked.
|
| + mHandler.post(new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + // Some other view already took focus. Container view should be active
|
| + // otherwise regardless of whether proxy view is registered or not.
|
| + if (!mInputMethodManagerWrapper.isActive(view)) return;
|
| +
|
| + // Success.
|
| + if (mInputMethodManagerWrapper.isActive(mProxyView)) {
|
| + mInputMethodUma.recordProxyViewSuccess();
|
| + return;
|
| + }
|
| +
|
| + if (mThreadedInputConnection == null) {
|
| + // First time and failed. It is highly likely that this does not work
|
| + // systematically.
|
| + mInputMethodUma.recordProxyViewFailure();
|
| + onRegisterProxyViewFailed();
|
| + } else {
|
| + // Most likely that we already lost view focus.
|
| + mInputMethodUma.recordProxyViewDetectionFailure();
|
| + }
|
| + }
|
| + });
|
| + }
|
| + });
|
| + }
|
| +
|
| + @VisibleForTesting
|
| + protected void onRegisterProxyViewFailed() {
|
| + throw new AssertionError("Failed to register proxy view");
|
| + }
|
| +
|
| + @Override
|
| + public Handler getHandler() {
|
| + return mHandler;
|
| + }
|
| +}
|
|
|