blob: e87a34ea17d7eb8ab988f5aac8f779b5fec19a92 [file] [log] [blame]
/*
* 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.junit.Assert.fail;
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()
boolean completed = mCountDownLatch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
if (!completed) {
fail("Timed out waiting for onServiceConnected()");
}
// 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();
}
}