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