1 /* 2 * Copyright (C) 2023 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 package com.android.server.inputmethod; 17 18 import static com.android.server.inputmethod.ClientController.ClientControllerCallback; 19 20 import static com.google.common.truth.Truth.assertThat; 21 import static com.google.common.truth.Truth.assertWithMessage; 22 23 import static org.junit.Assert.assertThrows; 24 import static org.mockito.Mockito.any; 25 import static org.mockito.Mockito.anyInt; 26 import static org.mockito.Mockito.anyLong; 27 import static org.mockito.Mockito.eq; 28 import static org.mockito.Mockito.verify; 29 import static org.mockito.Mockito.when; 30 31 import android.content.pm.PackageManagerInternal; 32 import android.os.Handler; 33 import android.os.IBinder; 34 import android.os.Looper; 35 import android.platform.test.ravenwood.RavenwoodRule; 36 import android.view.Display; 37 38 import com.android.internal.inputmethod.IInputMethodClient; 39 import com.android.internal.inputmethod.IRemoteInputConnection; 40 41 import org.junit.Before; 42 import org.junit.Rule; 43 import org.junit.Test; 44 import org.mockito.Mock; 45 import org.mockito.MockitoAnnotations; 46 47 import java.util.concurrent.CountDownLatch; 48 import java.util.concurrent.TimeUnit; 49 50 // This test is designed to run on both device and host (Ravenwood) side. 51 public final class ClientControllerTest { 52 private static final int ANY_DISPLAY_ID = Display.DEFAULT_DISPLAY; 53 private static final int ANY_CALLER_UID = 1; 54 private static final int ANY_CALLER_PID = 2; 55 private static final String SOME_PACKAGE_NAME = "some.package"; 56 57 @Rule 58 public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() 59 .setProvideMainThread(true).build(); 60 61 @Mock 62 private PackageManagerInternal mMockPackageManagerInternal; 63 64 @Mock(extraInterfaces = IBinder.class) 65 private IInputMethodClient mClient; 66 67 @Mock 68 private IRemoteInputConnection mConnection; 69 70 private Handler mHandler; 71 72 private ClientController mController; 73 74 @Before setUp()75 public void setUp() { 76 MockitoAnnotations.initMocks(this); 77 when(mClient.asBinder()).thenReturn((IBinder) mClient); 78 79 mHandler = new Handler(Looper.getMainLooper()); 80 mController = new ClientController(mMockPackageManagerInternal); 81 } 82 83 // TODO(b/322895594): No need to directly invoke create$ravenwood once b/322895594 is fixed. createInvoker(IInputMethodClient client, Handler handler)84 private IInputMethodClientInvoker createInvoker(IInputMethodClient client, Handler handler) { 85 return RavenwoodRule.isOnRavenwood() 86 ? IInputMethodClientInvoker.create$ravenwood(client, handler) : 87 IInputMethodClientInvoker.create(client, handler); 88 } 89 90 @Test testAddClient_cannotAddTheSameClientTwice()91 public void testAddClient_cannotAddTheSameClientTwice() { 92 final var invoker = createInvoker(mClient, mHandler); 93 synchronized (ImfLock.class) { 94 mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID, 95 ANY_CALLER_PID); 96 97 SecurityException thrown = assertThrows(SecurityException.class, 98 () -> { 99 synchronized (ImfLock.class) { 100 mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, 101 ANY_CALLER_UID, ANY_CALLER_PID); 102 } 103 }); 104 assertThat(thrown.getMessage()).isEqualTo( 105 "uid=" + ANY_CALLER_UID + "/pid=" + ANY_CALLER_PID 106 + "/displayId=0 is already registered"); 107 } 108 } 109 110 @Test testAddClient()111 public void testAddClient() throws Exception { 112 final var invoker = createInvoker(mClient, mHandler); 113 synchronized (ImfLock.class) { 114 final var added = mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, 115 ANY_CALLER_UID, 116 ANY_CALLER_PID); 117 118 verify(invoker.asBinder()).linkToDeath(any(IBinder.DeathRecipient.class), eq(0)); 119 assertThat(mController.getClient(invoker.asBinder())).isSameInstanceAs(added); 120 } 121 } 122 123 @Test testRemoveClient()124 public void testRemoveClient() { 125 final var invoker = createInvoker(mClient, mHandler); 126 final var callback = new TestClientControllerCallback(); 127 ClientState added; 128 synchronized (ImfLock.class) { 129 mController.addClientControllerCallback(callback); 130 added = mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID, 131 ANY_CALLER_PID); 132 assertThat(mController.getClient(invoker.asBinder())).isSameInstanceAs(added); 133 assertThat(mController.removeClient(mClient)).isTrue(); 134 } 135 136 // Test callback 137 final var removed = callback.waitForRemovedClient(5, TimeUnit.SECONDS); 138 assertThat(removed).isSameInstanceAs(added); 139 } 140 141 @Test testVerifyClientAndPackageMatch()142 public void testVerifyClientAndPackageMatch() { 143 final var invoker = createInvoker(mClient, mHandler); 144 when(mMockPackageManagerInternal.isSameApp(eq(SOME_PACKAGE_NAME), /* flags= */ 145 anyLong(), eq(ANY_CALLER_UID), /* userId= */ anyInt())).thenReturn(true); 146 147 synchronized (ImfLock.class) { 148 mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID, 149 ANY_CALLER_PID); 150 assertThat( 151 mController.verifyClientAndPackageMatch(mClient, SOME_PACKAGE_NAME)).isTrue(); 152 } 153 } 154 155 @Test testVerifyClientAndPackageMatch_unknownClient()156 public void testVerifyClientAndPackageMatch_unknownClient() { 157 synchronized (ImfLock.class) { 158 assertThrows(IllegalArgumentException.class, 159 () -> { 160 synchronized (ImfLock.class) { 161 mController.verifyClientAndPackageMatch(mClient, SOME_PACKAGE_NAME); 162 } 163 }); 164 } 165 } 166 167 private static class TestClientControllerCallback implements ClientControllerCallback { 168 169 private final CountDownLatch mLatch = new CountDownLatch(1); 170 171 private ClientState mRemoved; 172 173 @Override onClientRemoved(ClientState removed)174 public void onClientRemoved(ClientState removed) { 175 mRemoved = removed; 176 mLatch.countDown(); 177 } 178 waitForRemovedClient(long timeout, TimeUnit unit)179 ClientState waitForRemovedClient(long timeout, TimeUnit unit) { 180 try { 181 assertWithMessage("ClientController callback wasn't called on user removed").that( 182 mLatch.await(timeout, unit)).isTrue(); 183 } catch (InterruptedException e) { 184 Thread.currentThread().interrupt(); 185 throw new IllegalStateException("Unexpected thread interruption", e); 186 } 187 return mRemoved; 188 } 189 } 190 } 191