summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--services/core/java/com/android/server/inputmethod/InputMethodBindingController.java45
-rw-r--r--services/tests/InputMethodSystemServerTests/AndroidManifest.xml19
-rw-r--r--services/tests/InputMethodSystemServerTests/res/xml/method.xml18
-rw-r--r--services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java224
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();
+ }
+}