• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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