Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 08866838 authored by Ziqi Chen's avatar Ziqi Chen Committed by Android (Google) Code Review
Browse files

Merge "Add unit test for InputMethodBindingController"

parents 7d1d59ac c841a4ae
Loading
Loading
Loading
Loading
+33 −12
Original line number Diff line number Diff line
@@ -42,12 +42,15 @@ import android.view.inputmethod.InputMethod;
import android.view.inputmethod.InputMethodInfo;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.inputmethod.IInputMethod;
import com.android.internal.inputmethod.InputBindResult;
import com.android.internal.inputmethod.UnbindReason;
import com.android.server.EventLogTags;
import com.android.server.wm.WindowManagerInternal;

import java.util.concurrent.CountDownLatch;

/**
 * A controller managing the state of the input method binding.
 */
@@ -77,19 +80,26 @@ final class InputMethodBindingController {
    @GuardedBy("ImfLock.class") private boolean mVisibleBound;
    @GuardedBy("ImfLock.class") private boolean mSupportsStylusHw;

    @Nullable private CountDownLatch mLatchForTesting;

    /**
     * Binding flags for establishing connection to the {@link InputMethodService}.
     */
    private static final int IME_CONNECTION_BIND_FLAGS =
    @VisibleForTesting
    static final int IME_CONNECTION_BIND_FLAGS =
            Context.BIND_AUTO_CREATE
                    | Context.BIND_NOT_VISIBLE
                    | Context.BIND_NOT_FOREGROUND
                    | Context.BIND_IMPORTANT_BACKGROUND
                    | Context.BIND_SCHEDULE_LIKE_TOP_APP;

    private final int mImeConnectionBindFlags;

    /**
     * Binding flags used only while the {@link InputMethodService} is showing window.
     */
    private static final int IME_VISIBLE_BIND_FLAGS =
    @VisibleForTesting
    static final int IME_VISIBLE_BIND_FLAGS =
            Context.BIND_AUTO_CREATE
                    | Context.BIND_TREAT_LIKE_ACTIVITY
                    | Context.BIND_FOREGROUND_SERVICE
@@ -97,12 +107,19 @@ final class InputMethodBindingController {
                    | Context.BIND_SHOWING_UI;

    InputMethodBindingController(@NonNull InputMethodManagerService service) {
        this(service, IME_CONNECTION_BIND_FLAGS, null /* latchForTesting */);
    }

    InputMethodBindingController(@NonNull InputMethodManagerService service,
            int imeConnectionBindFlags, CountDownLatch latchForTesting) {
        mService = service;
        mContext = mService.mContext;
        mMethodMap = mService.mMethodMap;
        mSettings = mService.mSettings;
        mPackageManagerInternal = mService.mPackageManagerInternal;
        mWindowManagerInternal = mService.mWindowManagerInternal;
        mImeConnectionBindFlags = imeConnectionBindFlags;
        mLatchForTesting = latchForTesting;
    }

    /**
@@ -242,7 +259,7 @@ final class InputMethodBindingController {
        @Override public void onBindingDied(ComponentName name) {
            synchronized (ImfLock.class) {
                mService.invalidateAutofillSessionLocked();
                if (mVisibleBound) {
                if (isVisibleBound()) {
                    unbindVisibleConnection();
                }
            }
@@ -291,6 +308,10 @@ final class InputMethodBindingController {
                mService.scheduleResetStylusHandwriting();
            }
            Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);

            if (mLatchForTesting != null) {
                mLatchForTesting.countDown(); // Notify the finish to tests
            }
        }

        @GuardedBy("ImfLock.class")
@@ -338,15 +359,15 @@ final class InputMethodBindingController {

    @GuardedBy("ImfLock.class")
    void unbindCurrentMethod() {
        if (mVisibleBound) {
        if (isVisibleBound()) {
            unbindVisibleConnection();
        }

        if (mHasConnection) {
        if (hasConnection()) {
            unbindMainConnection();
        }

        if (mCurToken != null) {
        if (getCurToken() != null) {
            removeCurrentToken();
            mService.resetSystemUiLocked();
        }
@@ -448,17 +469,17 @@ final class InputMethodBindingController {

    @GuardedBy("ImfLock.class")
    private boolean bindCurrentInputMethodService(ServiceConnection conn, int flags) {
        if (mCurIntent == null || conn == null) {
        if (getCurIntent() == null || conn == null) {
            Slog.e(TAG, "--- bind failed: service = " + mCurIntent + ", conn = " + conn);
            return false;
        }
        return mContext.bindServiceAsUser(mCurIntent, conn, flags,
        return mContext.bindServiceAsUser(getCurIntent(), conn, flags,
                new UserHandle(mSettings.getCurrentUserId()));
    }

    @GuardedBy("ImfLock.class")
    private boolean bindCurrentInputMethodServiceMainConnection() {
        mHasConnection = bindCurrentInputMethodService(mMainConnection, IME_CONNECTION_BIND_FLAGS);
        mHasConnection = bindCurrentInputMethodService(mMainConnection, mImeConnectionBindFlags);
        return mHasConnection;
    }

@@ -472,7 +493,7 @@ final class InputMethodBindingController {
    void setCurrentMethodVisible() {
        if (mCurMethod != null) {
            if (DEBUG) Slog.d(TAG, "setCurrentMethodVisible: mCurToken=" + mCurToken);
            if (mHasConnection && !mVisibleBound) {
            if (hasConnection() && !isVisibleBound()) {
                mVisibleBound = bindCurrentInputMethodService(mVisibleConnection,
                        IME_VISIBLE_BIND_FLAGS);
            }
@@ -480,7 +501,7 @@ final class InputMethodBindingController {
        }

        // No IME is currently connected. Reestablish the main connection.
        if (!mHasConnection) {
        if (!hasConnection()) {
            if (DEBUG) {
                Slog.d(TAG, "Cannot show input: no IME bound. Rebinding.");
            }
@@ -512,7 +533,7 @@ final class InputMethodBindingController {
     */
    @GuardedBy("ImfLock.class")
    void setCurrentMethodNotVisible() {
        if (mVisibleBound) {
        if (isVisibleBound()) {
            unbindVisibleConnection();
        }
    }
+19 −0
Original line number Diff line number Diff line
@@ -18,6 +18,11 @@
          package="com.android.frameworks.inputmethodtests">

    <uses-sdk android:targetSdkVersion="31" />
    <queries>
        <intent>
            <action android:name="android.view.InputMethod" />
        </intent>
    </queries>

    <!-- Permissions required for granting and logging -->
    <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE"/>
@@ -29,9 +34,23 @@
    <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />

    <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS"/>
    <uses-permission android:name="android.permission.BIND_INPUT_METHOD" />

    <application android:testOnly="true"
                 android:debuggable="true">
        <uses-library android:name="android.test.runner" />
        <service android:name="com.android.server.inputmethod.InputMethodBindingControllerTest$EmptyInputMethodService"
                 android:label="Empty IME"
                 android:permission="android.permission.BIND_INPUT_METHOD"
                 android:process=":service"
                 android:exported="true">
            <intent-filter>
                <action android:name="android.view.InputMethod"/>
            </intent-filter>
            <meta-data android:name="android.view.im"
                       android:resource="@xml/method"/>
        </service>
    </application>

    <instrumentation
+18 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright (C) 2022 The Android Open Source Project
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~      http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->

<input-method xmlns:android="http://schemas.android.com/apk/res/android" />
 No newline at end of file
+224 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.inputmethod;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.app.Instrumentation;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.inputmethodservice.InputMethodService;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.view.inputmethod.InputMethodInfo;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;

import com.android.internal.inputmethod.InputBindResult;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

@RunWith(AndroidJUnit4.class)
public class InputMethodBindingControllerTest extends InputMethodManagerServiceTestBase {

    private static final String PACKAGE_NAME = "com.android.frameworks.inputmethodtests";
    private static final String TEST_SERVICE_NAME =
            "com.android.server.inputmethod.InputMethodBindingControllerTest"
                    + "$EmptyInputMethodService";
    private static final String TEST_IME_ID = PACKAGE_NAME + "/" + TEST_SERVICE_NAME;
    private static final long TIMEOUT_IN_SECONDS = 3;

    private InputMethodBindingController mBindingController;
    private Instrumentation mInstrumentation;
    private final int mImeConnectionBindFlags =
            InputMethodBindingController.IME_CONNECTION_BIND_FLAGS
                    & ~Context.BIND_SCHEDULE_LIKE_TOP_APP;
    private CountDownLatch mCountDownLatch;

    public static class EmptyInputMethodService extends InputMethodService {}

    @Before
    public void setUp() throws RemoteException {
        super.setUp();
        mInstrumentation = InstrumentationRegistry.getInstrumentation();
        mCountDownLatch = new CountDownLatch(1);
        // Remove flag Context.BIND_SCHEDULE_LIKE_TOP_APP because in tests we are not calling
        // from system.
        mBindingController =
                new InputMethodBindingController(
                        mInputMethodManagerService, mImeConnectionBindFlags, mCountDownLatch);
    }

    @Test
    public void testBindCurrentMethod_noIme() {
        synchronized (ImfLock.class) {
            mBindingController.setSelectedMethodId(null);
            InputBindResult result = mBindingController.bindCurrentMethod();
            assertThat(result).isEqualTo(InputBindResult.NO_IME);
        }
    }

    @Test
    public void testBindCurrentMethod_unknownId() {
        synchronized (ImfLock.class) {
            mBindingController.setSelectedMethodId("unknown ime id");
        }
        assertThrows(IllegalArgumentException.class, () -> {
            synchronized (ImfLock.class) {
                mBindingController.bindCurrentMethod();
            }
        });
    }

    @Test
    public void testBindCurrentMethod_notConnected() {
        synchronized (ImfLock.class) {
            mBindingController.setSelectedMethodId(TEST_IME_ID);
            doReturn(false)
                    .when(mContext)
                    .bindServiceAsUser(
                            any(Intent.class),
                            any(ServiceConnection.class),
                            anyInt(),
                            any(UserHandle.class));

            InputBindResult result = mBindingController.bindCurrentMethod();
            assertThat(result).isEqualTo(InputBindResult.IME_NOT_CONNECTED);
        }
    }

    @Test
    public void testBindAndUnbindMethod() throws Exception {
        // Bind with main connection
        testBindCurrentMethodWithMainConnection();

        // Bind with visible connection
        testBindCurrentMethodWithVisibleConnection();

        // Unbind both main and visible connections
        testUnbindCurrentMethod();
    }

    private void testBindCurrentMethodWithMainConnection() throws Exception {
        synchronized (ImfLock.class) {
            mBindingController.setSelectedMethodId(TEST_IME_ID);
        }
        InputMethodInfo info = mInputMethodManagerService.mMethodMap.get(TEST_IME_ID);
        assertThat(info).isNotNull();
        assertThat(info.getId()).isEqualTo(TEST_IME_ID);
        assertThat(info.getServiceName()).isEqualTo(TEST_SERVICE_NAME);

        // Bind input method with main connection. It is called on another thread because we should
        // wait for onServiceConnected() to finish.
        InputBindResult result = callOnMainSync(() -> {
            synchronized (ImfLock.class) {
                return mBindingController.bindCurrentMethod();
            }
        });

        verify(mContext, times(1))
                .bindServiceAsUser(
                        any(Intent.class),
                        any(ServiceConnection.class),
                        eq(mImeConnectionBindFlags),
                        any(UserHandle.class));
        assertThat(result.result).isEqualTo(InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING);
        assertThat(result.id).isEqualTo(info.getId());
        synchronized (ImfLock.class) {
            assertThat(mBindingController.hasConnection()).isTrue();
            assertThat(mBindingController.getCurId()).isEqualTo(info.getId());
            assertThat(mBindingController.getCurToken()).isNotNull();
        }
        // Wait for onServiceConnected()
        mCountDownLatch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);

        // Verify onServiceConnected() is called and bound successfully.
        synchronized (ImfLock.class) {
            assertThat(mBindingController.getCurMethod()).isNotNull();
            assertThat(mBindingController.getCurMethodUid()).isNotEqualTo(Process.INVALID_UID);
        }
    }

    private void testBindCurrentMethodWithVisibleConnection() {
        mInstrumentation.runOnMainSync(() -> {
            synchronized (ImfLock.class) {
                mBindingController.setCurrentMethodVisible();
            }
        });
        // Bind input method with visible connection
        verify(mContext, times(1))
                .bindServiceAsUser(
                        any(Intent.class),
                        any(ServiceConnection.class),
                        eq(InputMethodBindingController.IME_VISIBLE_BIND_FLAGS),
                        any(UserHandle.class));
        synchronized (ImfLock.class) {
            assertThat(mBindingController.isVisibleBound()).isTrue();
        }
    }

    private void testUnbindCurrentMethod() {
        mInstrumentation.runOnMainSync(() -> {
            synchronized (ImfLock.class) {
                mBindingController.unbindCurrentMethod();
            }
        });

        synchronized (ImfLock.class) {
            // Unbind both main connection and visible connection
            assertThat(mBindingController.hasConnection()).isFalse();
            assertThat(mBindingController.isVisibleBound()).isFalse();
            verify(mContext, times(2)).unbindService(any(ServiceConnection.class));
            assertThat(mBindingController.getCurToken()).isNull();
            assertThat(mBindingController.getCurId()).isNull();
            assertThat(mBindingController.getCurMethod()).isNull();
            assertThat(mBindingController.getCurMethodUid()).isEqualTo(Process.INVALID_UID);
        }
    }

    private static <V> V callOnMainSync(Callable<V> callable) {
        AtomicReference<V> result = new AtomicReference<>();
        InstrumentationRegistry.getInstrumentation()
                .runOnMainSync(
                        () -> {
                            try {
                                result.set(callable.call());
                            } catch (Exception e) {
                                throw new RuntimeException("Exception was thrown", e);
                            }
                        });
        return result.get();
    }
}