diff options
4 files changed, 294 insertions, 12 deletions
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java index 6dbb362db030..4d67311db150 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java @@ -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(); } } diff --git a/services/tests/InputMethodSystemServerTests/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/AndroidManifest.xml index 12e7cfc28a56..212ec14b4939 100644 --- a/services/tests/InputMethodSystemServerTests/AndroidManifest.xml +++ b/services/tests/InputMethodSystemServerTests/AndroidManifest.xml @@ -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 diff --git a/services/tests/InputMethodSystemServerTests/res/xml/method.xml b/services/tests/InputMethodSystemServerTests/res/xml/method.xml new file mode 100644 index 000000000000..89b06bb6e5a1 --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/res/xml/method.xml @@ -0,0 +1,18 @@ +<?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 diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java new file mode 100644 index 000000000000..42d373b9bf3e --- /dev/null +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java @@ -0,0 +1,224 @@ +/* + * 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(); + } +} |