1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.inputmethod; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 import static org.junit.Assert.assertThrows; 22 import static org.junit.Assert.fail; 23 import static org.mockito.ArgumentMatchers.any; 24 import static org.mockito.ArgumentMatchers.anyInt; 25 import static org.mockito.Mockito.doReturn; 26 import static org.mockito.Mockito.eq; 27 import static org.mockito.Mockito.times; 28 import static org.mockito.Mockito.verify; 29 30 import android.app.Instrumentation; 31 import android.content.Context; 32 import android.content.Intent; 33 import android.content.ServiceConnection; 34 import android.inputmethodservice.InputMethodService; 35 import android.os.Process; 36 import android.os.RemoteException; 37 import android.os.UserHandle; 38 import android.view.inputmethod.InputMethodInfo; 39 40 import androidx.test.ext.junit.runners.AndroidJUnit4; 41 import androidx.test.platform.app.InstrumentationRegistry; 42 43 import com.android.internal.inputmethod.InputBindResult; 44 45 import org.junit.Before; 46 import org.junit.Test; 47 import org.junit.runner.RunWith; 48 49 import java.util.concurrent.Callable; 50 import java.util.concurrent.CountDownLatch; 51 import java.util.concurrent.TimeUnit; 52 import java.util.concurrent.atomic.AtomicReference; 53 54 @RunWith(AndroidJUnit4.class) 55 public class InputMethodBindingControllerTest extends InputMethodManagerServiceTestBase { 56 57 private static final String PACKAGE_NAME = "com.android.frameworks.inputmethodtests"; 58 private static final String TEST_SERVICE_NAME = 59 "com.android.server.inputmethod.InputMethodBindingControllerTest" 60 + "$EmptyInputMethodService"; 61 private static final String TEST_IME_ID = PACKAGE_NAME + "/" + TEST_SERVICE_NAME; 62 private static final long TIMEOUT_IN_SECONDS = 3; 63 64 private InputMethodBindingController mBindingController; 65 private Instrumentation mInstrumentation; 66 private final int mImeConnectionBindFlags = 67 InputMethodBindingController.IME_CONNECTION_BIND_FLAGS 68 & ~Context.BIND_SCHEDULE_LIKE_TOP_APP; 69 private CountDownLatch mCountDownLatch; 70 71 public static class EmptyInputMethodService extends InputMethodService {} 72 73 @Before setUp()74 public void setUp() throws RemoteException { 75 super.setUp(); 76 mInstrumentation = InstrumentationRegistry.getInstrumentation(); 77 mCountDownLatch = new CountDownLatch(1); 78 // Remove flag Context.BIND_SCHEDULE_LIKE_TOP_APP because in tests we are not calling 79 // from system. 80 synchronized (ImfLock.class) { 81 mBindingController = 82 new InputMethodBindingController(mUserId, mInputMethodManagerService, 83 mImeConnectionBindFlags, mCountDownLatch); 84 } 85 } 86 87 @Test testBindCurrentMethod_noIme()88 public void testBindCurrentMethod_noIme() { 89 synchronized (ImfLock.class) { 90 mBindingController.setSelectedMethodId(null); 91 InputBindResult result = mBindingController.bindCurrentMethod(); 92 assertThat(result).isEqualTo(InputBindResult.NO_IME); 93 } 94 } 95 96 @Test testBindCurrentMethod_unknownId()97 public void testBindCurrentMethod_unknownId() { 98 synchronized (ImfLock.class) { 99 mBindingController.setSelectedMethodId("unknown ime id"); 100 } 101 assertThrows(IllegalArgumentException.class, () -> { 102 synchronized (ImfLock.class) { 103 mBindingController.bindCurrentMethod(); 104 } 105 }); 106 } 107 108 @Test testBindCurrentMethod_notConnected()109 public void testBindCurrentMethod_notConnected() { 110 synchronized (ImfLock.class) { 111 mBindingController.setSelectedMethodId(TEST_IME_ID); 112 doReturn(false) 113 .when(mContext) 114 .bindServiceAsUser( 115 any(Intent.class), 116 any(ServiceConnection.class), 117 anyInt(), 118 any(UserHandle.class)); 119 120 InputBindResult result = mBindingController.bindCurrentMethod(); 121 assertThat(result).isEqualTo(InputBindResult.IME_NOT_CONNECTED); 122 } 123 } 124 125 @Test testBindAndUnbindMethod()126 public void testBindAndUnbindMethod() throws Exception { 127 // Bind with main connection 128 testBindCurrentMethodWithMainConnection(); 129 130 // Bind with visible connection 131 testBindCurrentMethodWithVisibleConnection(); 132 133 // Unbind both main and visible connections 134 testUnbindCurrentMethod(); 135 } 136 testBindCurrentMethodWithMainConnection()137 private void testBindCurrentMethodWithMainConnection() throws Exception { 138 final InputMethodInfo info; 139 synchronized (ImfLock.class) { 140 mBindingController.setSelectedMethodId(TEST_IME_ID); 141 info = InputMethodSettingsRepository.get(mUserId).getMethodMap().get(TEST_IME_ID); 142 } 143 assertThat(info).isNotNull(); 144 assertThat(info.getId()).isEqualTo(TEST_IME_ID); 145 assertThat(info.getServiceName()).isEqualTo(TEST_SERVICE_NAME); 146 147 // Bind input method with main connection. It is called on another thread because we should 148 // wait for onServiceConnected() to finish. 149 InputBindResult result = callOnMainSync(() -> { 150 synchronized (ImfLock.class) { 151 return mBindingController.bindCurrentMethod(); 152 } 153 }); 154 155 verify(mContext, times(1)) 156 .bindServiceAsUser( 157 any(Intent.class), 158 any(ServiceConnection.class), 159 eq(mImeConnectionBindFlags), 160 any(UserHandle.class)); 161 assertThat(result.result).isEqualTo(InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING); 162 assertThat(result.id).isEqualTo(info.getId()); 163 synchronized (ImfLock.class) { 164 assertThat(mBindingController.hasMainConnection()).isTrue(); 165 assertThat(mBindingController.getCurId()).isEqualTo(info.getId()); 166 assertThat(mBindingController.getCurToken()).isNotNull(); 167 } 168 // Wait for onServiceConnected() 169 boolean completed = mCountDownLatch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS); 170 if (!completed) { 171 fail("Timed out waiting for onServiceConnected()"); 172 } 173 174 // Verify onServiceConnected() is called and bound successfully. 175 synchronized (ImfLock.class) { 176 assertThat(mBindingController.getCurMethod()).isNotNull(); 177 assertThat(mBindingController.getCurMethodUid()).isNotEqualTo(Process.INVALID_UID); 178 } 179 } 180 testBindCurrentMethodWithVisibleConnection()181 private void testBindCurrentMethodWithVisibleConnection() { 182 mInstrumentation.runOnMainSync(() -> { 183 synchronized (ImfLock.class) { 184 mBindingController.setCurrentMethodVisible(); 185 } 186 }); 187 // Bind input method with visible connection 188 verify(mContext, times(1)) 189 .bindServiceAsUser( 190 any(Intent.class), 191 any(ServiceConnection.class), 192 eq(InputMethodBindingController.IME_VISIBLE_BIND_FLAGS), 193 any(UserHandle.class)); 194 synchronized (ImfLock.class) { 195 assertThat(mBindingController.isVisibleBound()).isTrue(); 196 } 197 } 198 testUnbindCurrentMethod()199 private void testUnbindCurrentMethod() { 200 mInstrumentation.runOnMainSync(() -> { 201 synchronized (ImfLock.class) { 202 mBindingController.unbindCurrentMethod(); 203 } 204 }); 205 206 synchronized (ImfLock.class) { 207 // Unbind both main connection and visible connection 208 assertThat(mBindingController.hasMainConnection()).isFalse(); 209 assertThat(mBindingController.isVisibleBound()).isFalse(); 210 verify(mContext, times(2)).unbindService(any(ServiceConnection.class)); 211 assertThat(mBindingController.getCurToken()).isNull(); 212 assertThat(mBindingController.getCurId()).isNull(); 213 assertThat(mBindingController.getCurMethod()).isNull(); 214 assertThat(mBindingController.getCurMethodUid()).isEqualTo(Process.INVALID_UID); 215 } 216 } 217 callOnMainSync(Callable<V> callable)218 private static <V> V callOnMainSync(Callable<V> callable) { 219 AtomicReference<V> result = new AtomicReference<>(); 220 InstrumentationRegistry.getInstrumentation() 221 .runOnMainSync( 222 () -> { 223 try { 224 result.set(callable.call()); 225 } catch (Exception e) { 226 throw new RuntimeException("Exception was thrown", e); 227 } 228 }); 229 return result.get(); 230 } 231 } 232