chromium/content/public/android/junit/src/org/chromium/content/browser/input/ThreadedInputConnectionFactoryTest.java

// Copyright 2016 The Chromium Authors
// 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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.os.Handler;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.content_public.browser.ContentFeatureList;
import org.chromium.content_public.browser.InputMethodManagerWrapper;

import java.util.concurrent.Callable;

/** Unit tests for {@ThreadedInputConnectionFactory}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
@EnableFeatures({ContentFeatureList.OPTIMIZE_IMM_HIDE_CALLS})
public class ThreadedInputConnectionFactoryTest {
    /** A testable version of ThreadedInputConnectionFactory. */
    private class TestFactory extends ThreadedInputConnectionFactory {

        private boolean mSucceeded;
        private boolean mFailed;
        private long mDelayMs;

        TestFactory(InputMethodManagerWrapper inputMethodManagerWrapper) {
            super(inputMethodManagerWrapper);
        }

        @Override
        protected ThreadedInputConnectionProxyView createProxyView(
                Handler handler, View containerView) {
            return mProxyView;
        }

        @Override
        protected void onRegisterProxyViewSuccess() {
            mSucceeded = true;
        }

        @Override
        protected void onRegisterProxyViewFailure() {
            mFailed = true;
        }

        public boolean hasFailed() {
            return mFailed;
        }

        public boolean hasSucceeded() {
            return mSucceeded;
        }

        public long delayMs() {
            return mDelayMs;
        }

        @Override
        public void onWindowFocusChanged(boolean gainFocus) {
            mHasWindowFocus = gainFocus;
            super.onWindowFocusChanged(gainFocus);
        }

        @Override
        protected void postDelayed(View view, Runnable r, long delayMs) {
            mDelayMs = delayMs;
            // Note that robolectric will run this immediately in runOneTask(). We can only test
            // the delay MS value.
            super.postDelayed(view, r, delayMs);
        }
    }

    @Mock private ImeAdapterImpl mImeAdapter;
    @Mock private View mContainerView;
    @Mock private ThreadedInputConnectionProxyView mProxyView;
    @Mock private InputMethodManager mInputMethodManager;
    @Mock private Context mContext;

    private EditorInfo mEditorInfo;
    private Handler mImeHandler;
    private Handler mUiHandler;
    private ShadowLooper mImeShadowLooper;
    private TestFactory mFactory;
    private InputConnection mInputConnection;
    private InOrder mInOrder;
    private boolean mHasWindowFocus;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);

        mEditorInfo = new EditorInfo();
        mUiHandler = new Handler();

        mContext = Mockito.mock(Context.class);
        mContainerView = Mockito.mock(View.class);
        mImeAdapter = Mockito.mock(ImeAdapterImpl.class);
        mInputMethodManager = Mockito.mock(InputMethodManager.class);

        mFactory = new TestFactory(new InputMethodManagerWrapperImpl(mContext, null, null));
        mFactory.onWindowFocusChanged(true);
        mImeHandler = mFactory.getHandler();
        mImeShadowLooper = (ShadowLooper) Shadow.extract(mImeHandler.getLooper());

        when(mContext.getSystemService(Context.INPUT_METHOD_SERVICE))
                .thenReturn(mInputMethodManager);
        // ThreadedInputConnectionFactory#initializeAndGet() logic is activated when the package is
        // "com.htc.android.mail"
        when(mContext.getPackageName()).thenReturn("com.htc.android.mail");
        when(mContainerView.getContext()).thenReturn(mContext);
        when(mContainerView.getHandler()).thenReturn(mUiHandler);
        when(mContainerView.hasFocus()).thenReturn(true);
        when(mContainerView.hasWindowFocus()).thenReturn(true);

        mProxyView = Mockito.mock(ThreadedInputConnectionProxyView.class);
        when(mProxyView.getContext()).thenReturn(mContext);
        when(mProxyView.requestFocus()).thenReturn(true);
        when(mProxyView.getHandler()).thenReturn(mImeHandler);
        final Callable<InputConnection> callable =
                new Callable<InputConnection>() {
                    @Override
                    public InputConnection call() {
                        return mFactory.initializeAndGet(
                                mContainerView, mImeAdapter, 1, 0, 0, 0, 0, 0, "", mEditorInfo);
                    }
                };
        when(mProxyView.onCreateInputConnection(any(EditorInfo.class)))
                .thenAnswer(
                        (InvocationOnMock invocation) -> {
                            mFactory.setTriggerDelayedOnCreateInputConnection(false);
                            InputConnection connection =
                                    ThreadUtils.runOnUiThreadBlocking(callable);
                            mFactory.setTriggerDelayedOnCreateInputConnection(true);
                            return connection;
                        });

        when(mInputMethodManager.isActive(mContainerView))
                .thenAnswer(
                        new Answer<Boolean>() {
                            private int mCount;

                            @Override
                            public Boolean answer(InvocationOnMock invocation) {
                                mCount++;
                                // To simplify IMM's behavior, let's say that it succeeds input
                                // method activation only when the view has a window focus.
                                if (!mHasWindowFocus) return false;
                                if (mCount == 1) {
                                    mInputConnection =
                                            mProxyView.onCreateInputConnection(mEditorInfo);
                                    return false;
                                }
                                return mHasWindowFocus;
                            }
                        });
        when(mInputMethodManager.isActive(mProxyView))
                .thenAnswer(
                        new Answer<Boolean>() {
                            @Override
                            public Boolean answer(InvocationOnMock invocation) {
                                return mInputConnection != null;
                            }
                        });

        mInOrder = inOrder(mImeAdapter, mInputMethodManager, mContainerView, mProxyView);
    }

    private void activateInput() {
        mUiHandler.post(
                new Runnable() {
                    @Override
                    public void run() {
                        assertNull(
                                mFactory.initializeAndGet(
                                        mContainerView,
                                        mImeAdapter,
                                        1,
                                        0,
                                        0,
                                        0,
                                        0,
                                        0,
                                        "",
                                        mEditorInfo));
                    }
                });
    }

    private void runOneUiTask() {
        assertTrue(Robolectric.getForegroundThreadScheduler().runOneTask());
    }

    @Test
    @Feature({"TextInput"})
    public void testCreateInputConnection_Success() {
        // Pause all the loopers.
        Robolectric.getForegroundThreadScheduler().pause();
        mImeShadowLooper.pause();

        activateInput();

        // The first onCreateInputConnection().
        runOneUiTask();
        assertEquals(0, mFactory.delayMs());

        mInOrder.verify(mContainerView).hasFocus();
        mInOrder.verify(mContainerView).hasWindowFocus();
        mInOrder.verify(mProxyView).requestFocus();
        mInOrder.verify(mContainerView).getHandler();
        mInOrder.verifyNoMoreInteractions();
        assertNull(mInputConnection);

        // The second onCreateInputConnection().
        runOneUiTask();
        mInOrder.verify(mProxyView).onWindowFocusChanged(true);
        mInOrder.verify(mInputMethodManager).isActive(mContainerView);
        mInOrder.verify(mProxyView).onCreateInputConnection(any(EditorInfo.class));
        assertNotNull(mInputConnection);
        assertTrue(ThreadedInputConnection.class.isInstance(mInputConnection));

        // Verification process.
        mImeShadowLooper.runOneTask();
        runOneUiTask();

        mInOrder.verify(mInputMethodManager).isActive(mProxyView);
        mInOrder.verifyNoMoreInteractions();

        assertTrue(mFactory.hasSucceeded());
        assertFalse(mFactory.hasFailed());
    }

    @Test
    @Feature({"TextInput"})
    public void testCreateInputConnection_Failure() {
        // Pause all the loopers.
        Robolectric.getForegroundThreadScheduler().pause();
        mImeShadowLooper.pause();

        activateInput();

        // The first onCreateInputConnection().
        runOneUiTask();
        assertEquals(0, mFactory.delayMs());

        mInOrder.verify(mContainerView).hasFocus();
        mInOrder.verify(mContainerView).hasWindowFocus();
        mInOrder.verify(mProxyView).requestFocus();
        mInOrder.verify(mContainerView).getHandler();
        mInOrder.verifyNoMoreInteractions();
        assertNull(mInputConnection);

        // Now window focus was lost before the second onCreateInputConnection().
        mFactory.onWindowFocusChanged(false);
        mInOrder.verify(mProxyView).onOriginalViewWindowFocusChanged(false);

        // The second onCreateInputConnection().
        runOneUiTask();
        mInOrder.verify(mProxyView).onWindowFocusChanged(true);
        mInOrder.verify(mInputMethodManager).isActive(mContainerView);
        mInOrder.verifyNoMoreInteractions();

        // Window focus is lost and we fail to activate.
        assertNull(mInputConnection);

        // Verification process.
        mImeShadowLooper.runOneTask();
        mInOrder.verify(mContainerView).getHandler();
        runOneUiTask();
        mInOrder.verify(mInputMethodManager).isActive(mProxyView);

        // Wait one more UI loop.
        mInOrder.verify(mContainerView).getHandler();
        runOneUiTask();
        mInOrder.verify(mInputMethodManager).isActive(mProxyView);

        mInOrder.verifyNoMoreInteractions();
        // Failed, but no logging because check has been invalidated.
        assertNull(mInputConnection);
        assertFalse(mFactory.hasSucceeded());
        assertFalse(mFactory.hasFailed());
    }

    // Test for https://crbug.com/1108237
    @Test
    @Feature({"TextInput"})
    public void testCreateInputConnection_Delayed() {
        // Pause all the loopers.
        Robolectric.getForegroundThreadScheduler().pause();
        mImeShadowLooper.pause();

        mFactory.onViewFocusChanged(false);
        mFactory.onWindowFocusChanged(false);

        // Note that we gained view focus before gaining window focus.
        // We will delay the keyboard activation.
        mFactory.onViewFocusChanged(true);
        mFactory.onWindowFocusChanged(true);

        activateInput();

        // The first onCreateInputConnection().
        runOneUiTask();

        // We delay the keyboard activation when view gets focused before window does.
        assertEquals(1000, mFactory.delayMs());

        mInOrder.verify(mContainerView).hasFocus();
        mInOrder.verify(mContainerView).hasWindowFocus();
        mInOrder.verify(mProxyView).requestFocus();
        mInOrder.verify(mContainerView).getHandler();
        mInOrder.verifyNoMoreInteractions();
        assertNull(mInputConnection);

        // The second onCreateInputConnection().
        runOneUiTask();
        mInOrder.verify(mProxyView).onWindowFocusChanged(true);
        mInOrder.verify(mInputMethodManager).isActive(mContainerView);
        mInOrder.verify(mProxyView).onCreateInputConnection(any(EditorInfo.class));
        assertNotNull(mInputConnection);
        assertTrue(ThreadedInputConnection.class.isInstance(mInputConnection));

        // Verification process.
        mImeShadowLooper.runOneTask();
        runOneUiTask();

        mInOrder.verify(mInputMethodManager).isActive(mProxyView);
        mInOrder.verifyNoMoreInteractions();

        assertTrue(mFactory.hasSucceeded());
        assertFalse(mFactory.hasFailed());
    }
}