| 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;
 | 
| +    }
 | 
| +}
 | 
| 
 |