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