Loading services/core/java/com/android/server/inputmethod/InputMethodBindingController.java +33 −12 Original line number Diff line number Diff line Loading @@ -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. */ Loading Loading @@ -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 Loading @@ -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; } /** Loading Loading @@ -242,7 +259,7 @@ final class InputMethodBindingController { @Override public void onBindingDied(ComponentName name) { synchronized (ImfLock.class) { mService.invalidateAutofillSessionLocked(); if (mVisibleBound) { if (isVisibleBound()) { unbindVisibleConnection(); } } Loading Loading @@ -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") Loading Loading @@ -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(); } Loading Loading @@ -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; } Loading @@ -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); } Loading @@ -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."); } Loading Loading @@ -512,7 +533,7 @@ final class InputMethodBindingController { */ @GuardedBy("ImfLock.class") void setCurrentMethodNotVisible() { if (mVisibleBound) { if (isVisibleBound()) { unbindVisibleConnection(); } } Loading services/tests/InputMethodSystemServerTests/AndroidManifest.xml +19 −0 Original line number Diff line number Diff line Loading @@ -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"/> Loading @@ -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 Loading services/tests/InputMethodSystemServerTests/res/xml/method.xml 0 → 100644 +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 services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java 0 → 100644 +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(); } } Loading
services/core/java/com/android/server/inputmethod/InputMethodBindingController.java +33 −12 Original line number Diff line number Diff line Loading @@ -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. */ Loading Loading @@ -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 Loading @@ -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; } /** Loading Loading @@ -242,7 +259,7 @@ final class InputMethodBindingController { @Override public void onBindingDied(ComponentName name) { synchronized (ImfLock.class) { mService.invalidateAutofillSessionLocked(); if (mVisibleBound) { if (isVisibleBound()) { unbindVisibleConnection(); } } Loading Loading @@ -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") Loading Loading @@ -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(); } Loading Loading @@ -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; } Loading @@ -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); } Loading @@ -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."); } Loading Loading @@ -512,7 +533,7 @@ final class InputMethodBindingController { */ @GuardedBy("ImfLock.class") void setCurrentMethodNotVisible() { if (mVisibleBound) { if (isVisibleBound()) { unbindVisibleConnection(); } } Loading
services/tests/InputMethodSystemServerTests/AndroidManifest.xml +19 −0 Original line number Diff line number Diff line Loading @@ -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"/> Loading @@ -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 Loading
services/tests/InputMethodSystemServerTests/res/xml/method.xml 0 → 100644 +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
services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java 0 → 100644 +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(); } }