• 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         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