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( 83 mInputMethodManagerService.getCurrentImeUserIdLocked(), 84 mInputMethodManagerService, mImeConnectionBindFlags, 85 mCountDownLatch); 86 } 87 } 88 89 @Test testBindCurrentMethod_noIme()90 public void testBindCurrentMethod_noIme() { 91 synchronized (ImfLock.class) { 92 mBindingController.setSelectedMethodId(null); 93 InputBindResult result = mBindingController.bindCurrentMethod(); 94 assertThat(result).isEqualTo(InputBindResult.NO_IME); 95 } 96 } 97 98 @Test testBindCurrentMethod_unknownId()99 public void testBindCurrentMethod_unknownId() { 100 synchronized (ImfLock.class) { 101 mBindingController.setSelectedMethodId("unknown ime id"); 102 } 103 assertThrows(IllegalArgumentException.class, () -> { 104 synchronized (ImfLock.class) { 105 mBindingController.bindCurrentMethod(); 106 } 107 }); 108 } 109 110 @Test testBindCurrentMethod_notConnected()111 public void testBindCurrentMethod_notConnected() { 112 synchronized (ImfLock.class) { 113 mBindingController.setSelectedMethodId(TEST_IME_ID); 114 doReturn(false) 115 .when(mContext) 116 .bindServiceAsUser( 117 any(Intent.class), 118 any(ServiceConnection.class), 119 anyInt(), 120 any(UserHandle.class)); 121 122 InputBindResult result = mBindingController.bindCurrentMethod(); 123 assertThat(result).isEqualTo(InputBindResult.IME_NOT_CONNECTED); 124 } 125 } 126 127 @Test testBindAndUnbindMethod()128 public void testBindAndUnbindMethod() throws Exception { 129 // Bind with main connection 130 testBindCurrentMethodWithMainConnection(); 131 132 // Bind with visible connection 133 testBindCurrentMethodWithVisibleConnection(); 134 135 // Unbind both main and visible connections 136 testUnbindCurrentMethod(); 137 } 138 testBindCurrentMethodWithMainConnection()139 private void testBindCurrentMethodWithMainConnection() throws Exception { 140 final InputMethodInfo info; 141 synchronized (ImfLock.class) { 142 mBindingController.setSelectedMethodId(TEST_IME_ID); 143 info = mInputMethodManagerService.queryInputMethodForCurrentUserLocked(TEST_IME_ID); 144 } 145 assertThat(info).isNotNull(); 146 assertThat(info.getId()).isEqualTo(TEST_IME_ID); 147 assertThat(info.getServiceName()).isEqualTo(TEST_SERVICE_NAME); 148 149 // Bind input method with main connection. It is called on another thread because we should 150 // wait for onServiceConnected() to finish. 151 InputBindResult result = callOnMainSync(() -> { 152 synchronized (ImfLock.class) { 153 return mBindingController.bindCurrentMethod(); 154 } 155 }); 156 157 verify(mContext, times(1)) 158 .bindServiceAsUser( 159 any(Intent.class), 160 any(ServiceConnection.class), 161 eq(mImeConnectionBindFlags), 162 any(UserHandle.class)); 163 assertThat(result.result).isEqualTo(InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING); 164 assertThat(result.id).isEqualTo(info.getId()); 165 synchronized (ImfLock.class) { 166 assertThat(mBindingController.hasMainConnection()).isTrue(); 167 assertThat(mBindingController.getCurId()).isEqualTo(info.getId()); 168 assertThat(mBindingController.getCurToken()).isNotNull(); 169 } 170 // Wait for onServiceConnected() 171 boolean completed = mCountDownLatch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS); 172 if (!completed) { 173 fail("Timed out waiting for onServiceConnected()"); 174 } 175 176 // Verify onServiceConnected() is called and bound successfully. 177 synchronized (ImfLock.class) { 178 assertThat(mBindingController.getCurMethod()).isNotNull(); 179 assertThat(mBindingController.getCurMethodUid()).isNotEqualTo(Process.INVALID_UID); 180 } 181 } 182 testBindCurrentMethodWithVisibleConnection()183 private void testBindCurrentMethodWithVisibleConnection() { 184 mInstrumentation.runOnMainSync(() -> { 185 synchronized (ImfLock.class) { 186 mBindingController.setCurrentMethodVisible(); 187 } 188 }); 189 // Bind input method with visible connection 190 verify(mContext, times(1)) 191 .bindServiceAsUser( 192 any(Intent.class), 193 any(ServiceConnection.class), 194 eq(InputMethodBindingController.IME_VISIBLE_BIND_FLAGS), 195 any(UserHandle.class)); 196 synchronized (ImfLock.class) { 197 assertThat(mBindingController.isVisibleBound()).isTrue(); 198 } 199 } 200 testUnbindCurrentMethod()201 private void testUnbindCurrentMethod() { 202 mInstrumentation.runOnMainSync(() -> { 203 synchronized (ImfLock.class) { 204 mBindingController.unbindCurrentMethod(); 205 } 206 }); 207 208 synchronized (ImfLock.class) { 209 // Unbind both main connection and visible connection 210 assertThat(mBindingController.hasMainConnection()).isFalse(); 211 assertThat(mBindingController.isVisibleBound()).isFalse(); 212 verify(mContext, times(2)).unbindService(any(ServiceConnection.class)); 213 assertThat(mBindingController.getCurToken()).isNull(); 214 assertThat(mBindingController.getCurId()).isNull(); 215 assertThat(mBindingController.getCurMethod()).isNull(); 216 assertThat(mBindingController.getCurMethodUid()).isEqualTo(Process.INVALID_UID); 217 } 218 } 219 callOnMainSync(Callable<V> callable)220 private static <V> V callOnMainSync(Callable<V> callable) { 221 AtomicReference<V> result = new AtomicReference<>(); 222 InstrumentationRegistry.getInstrumentation() 223 .runOnMainSync( 224 () -> { 225 try { 226 result.set(callable.call()); 227 } catch (Exception e) { 228 throw new RuntimeException("Exception was thrown", e); 229 } 230 }); 231 return result.get(); 232 } 233 } 234