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

// Copyright 2020 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.mockito.Mockito.doReturn;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.when;

import android.app.Activity;
import android.content.Context;
import android.os.Build;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;
import org.robolectric.shadows.ShadowLog;

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.content_public.browser.ContentFeatureList;
import org.chromium.content_public.browser.InputMethodManagerWrapper;
import org.chromium.ui.base.WindowAndroid;

import java.lang.ref.WeakReference;

/** A robolectric test for {@link InputMethodManagerWrapperImpl} class. */
@RunWith(BaseRobolectricTestRunner.class)
// Any VERSION_CODE >= O is fine.
@Config(manifest = Config.NONE, sdk = Build.VERSION_CODES.O)
@LooperMode(LooperMode.Mode.LEGACY)
@EnableFeatures({ContentFeatureList.OPTIMIZE_IMM_HIDE_CALLS})
public class InputMethodManagerWrapperImplTest {
    private static final boolean DEBUG = false;

    private class TestInputMethodManagerWrapperImpl extends InputMethodManagerWrapperImpl {
        public TestInputMethodManagerWrapperImpl(
                Context context, WindowAndroid windowAndroid, Delegate delegate) {
            super(context, windowAndroid, delegate);
        }

        @Override
        protected int getDisplayId(Context context) {
            if (context == mContext) {
                assert mContextDisplayId != -1;
                return mContextDisplayId;
            }
            if (context == mActivity) {
                assert mActivityDisplayId != -1;
                return mActivityDisplayId;
            }
            return super.getDisplayId(context);
        }
    }

    @Mock private Context mContext;
    @Mock private Activity mActivity;
    @Mock private Window mWindow;
    @Mock private WindowAndroid mWindowAndroid;
    @Mock private InputMethodManagerWrapper.Delegate mDelegate;
    @Mock private View mView;
    @Mock private InputMethodManager mInputMethodManager;
    @Mock private WindowManager mContextWindowManager;
    @Mock private WindowManager mActivityWindowManager;

    private int mContextDisplayId = -1; // uninitialized
    private int mActivityDisplayId = -1; // uninitialized

    private InOrder mInOrder;

    private InputMethodManagerWrapperImpl mImmw;

    public InputMethodManagerWrapperImplTest() {
        if (DEBUG) ShadowLog.stream = System.out;
    }

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        mImmw = new TestInputMethodManagerWrapperImpl(mContext, mWindowAndroid, mDelegate);
        when(mContext.getSystemService(Context.INPUT_METHOD_SERVICE))
                .thenReturn(mInputMethodManager);
        when(mActivity.getSystemService(Context.INPUT_METHOD_SERVICE))
                .thenReturn(mInputMethodManager);
        when(mActivity.getWindow()).thenReturn(mWindow);

        mInOrder = inOrder(mInputMethodManager, mWindow);
    }

    @After
    public void tearDown() throws Exception {
        mInOrder.verifyNoMoreInteractions();
    }

    @Test
    public void testWebViewHasNoActivity() throws Exception {
        when(mWindowAndroid.getActivity()).thenReturn(null);

        mImmw.showSoftInput(mView, 0, null);

        mInOrder.verify(mInputMethodManager).showSoftInput(mView, 0, null);
    }

    private void setDisplayIds(int contextDisplayId, int activityDisplayId) {
        mContextDisplayId = contextDisplayId;
        mActivityDisplayId = activityDisplayId;
    }

    @Test
    public void testSingleDisplay() throws Exception {
        when(mWindowAndroid.getActivity()).thenReturn(new WeakReference<Activity>(mActivity));
        setDisplayIds(0, 0);

        mImmw.showSoftInput(mView, 0, null);

        mInOrder.verify(mInputMethodManager).showSoftInput(mView, 0, null);
    }

    @Test
    public void testMultiDisplaysWithInputConnection() throws Exception {
        when(mWindowAndroid.getActivity()).thenReturn(new WeakReference<Activity>(mActivity));
        setDisplayIds(0, 1); // context and activity have different display IDs
        when(mDelegate.hasInputConnection()).thenReturn(true);

        mImmw.showSoftInput(mView, 0, null);

        // Run a workaround.
        mInOrder.verify(mWindow).setLocalFocus(true, true);

        // When InputConnection is available, then show soft input immediately.
        mInOrder.verify(mInputMethodManager).showSoftInput(mView, 0, null);
    }

    @Test
    public void testMultiDisplaysWithoutInputConnection() throws Exception {
        when(mWindowAndroid.getActivity()).thenReturn(new WeakReference<Activity>(mActivity));
        setDisplayIds(0, 1); // context and activity have different display Ids
        when(mDelegate.hasInputConnection()).thenReturn(false);
        when(mInputMethodManager.isActive(mView)).thenReturn(true);

        mImmw.showSoftInput(mView, 0, null);

        // Run a workaround.
        mInOrder.verify(mWindow).setLocalFocus(true, true);

        // InputConnection is not available, then wait for onInputConnectionCreated().
        mInOrder.verifyNoMoreInteractions();

        mImmw.onInputConnectionCreated();

        // Post task: note that PostTask actually does not require
        // Robolectric.getForegroundThreadScheduler().runOneTask() to be called.

        // Check first if input method is still valid on the current view.
        mInOrder.verify(mInputMethodManager).isActive(mView);

        mInOrder.verify(mInputMethodManager).showSoftInput(mView, 0, null);
    }

    @Test
    public void testMultiDisplaysWithoutInputConnection_hideKeyboard() throws Exception {
        when(mWindowAndroid.getActivity()).thenReturn(new WeakReference<Activity>(mActivity));
        setDisplayIds(0, 1); // context and activity have different display Ids
        when(mDelegate.hasInputConnection()).thenReturn(false);
        when(mInputMethodManager.isActive(mView)).thenReturn(true);
        doReturn(true).when(mInputMethodManager).isAcceptingText();

        mImmw.showSoftInput(mView, 0, null);

        // Run a workaround.
        mInOrder.verify(mWindow).setLocalFocus(true, true);

        // InputConnection is not available, then wait for onInputConnectionCreated().
        mInOrder.verifyNoMoreInteractions();

        // Hide called before input connection is created.
        mImmw.hideSoftInputFromWindow(null, 0, null);

        mImmw.onInputConnectionCreated();

        mInOrder.verify(mInputMethodManager).hideSoftInputFromWindow(null, 0, null);
        // Do not call showSoftInput.
    }

    @Test
    public void testMultiDisplaysWithoutInputConnection_notActive() throws Exception {
        when(mWindowAndroid.getActivity()).thenReturn(new WeakReference<Activity>(mActivity));
        setDisplayIds(0, 1); // context and activity have different display Ids
        when(mDelegate.hasInputConnection()).thenReturn(false);
        when(mInputMethodManager.isActive(mView)).thenReturn(false);

        mImmw.showSoftInput(mView, 0, null);

        // Run a workaround.
        mInOrder.verify(mWindow).setLocalFocus(true, true);

        // InputConnection is not available, then wait for onInputConnectionCreated().
        mInOrder.verifyNoMoreInteractions();

        // Another showSoftInput before input connection gets created.
        mImmw.showSoftInput(mView, 1, null);

        mImmw.onInputConnectionCreated();

        // Post task: note that PostTask actually does not require
        // Robolectric.getForegroundThreadScheduler().runOneTask() to be called.

        // Check first if input method is still valid on the current view.
        mInOrder.verify(mInputMethodManager).isActive(mView);

        // Do not call showSoftInput since it is not active.
    }

    @Test
    public void testMultiDisplaysWithoutInputConnection_showSoftInputAgain() throws Exception {
        when(mWindowAndroid.getActivity()).thenReturn(new WeakReference<Activity>(mActivity));
        setDisplayIds(0, 1); // context and activity have different display Ids
        when(mDelegate.hasInputConnection()).thenReturn(false);
        when(mInputMethodManager.isActive(mView)).thenReturn(true);

        mImmw.showSoftInput(mView, 0, null);

        // Run a workaround.
        mInOrder.verify(mWindow).setLocalFocus(true, true);

        // InputConnection is not available, then wait for onInputConnectionCreated().
        mInOrder.verifyNoMoreInteractions();

        // Another showSoftInput before input connection gets created.
        mImmw.showSoftInput(mView, 1, null);

        mImmw.onInputConnectionCreated();

        // Post task: note that PostTask actually does not require
        // Robolectric.getForegroundThreadScheduler().runOneTask() to be called.

        // Check first if input method is still valid on the current view.
        mInOrder.verify(mInputMethodManager).isActive(mView);

        // Note that the first call to showSoftInput was ignored.
        mInOrder.verify(mInputMethodManager).showSoftInput(mView, 1, null);
    }
}