1 /* 2 * Copyright (C) 2025 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 android.car.projection; 18 19 import static android.car.CarProjectionManager.ProjectionAccessPointCallback.ERROR_GENERIC; 20 21 import static com.google.common.truth.Truth.assertThat; 22 import static com.google.common.truth.Truth.assertWithMessage; 23 24 import static org.junit.Assert.assertThrows; 25 import static org.mockito.ArgumentMatchers.any; 26 import static org.mockito.ArgumentMatchers.anyInt; 27 import static org.mockito.ArgumentMatchers.eq; 28 import static org.mockito.Mockito.inOrder; 29 import static org.mockito.Mockito.mock; 30 import static org.mockito.Mockito.never; 31 import static org.mockito.Mockito.verify; 32 33 import android.car.Car; 34 import android.car.CarNotConnectedException; 35 import android.car.CarProjectionManager; 36 import android.car.CarProjectionManager.ProjectionAccessPointCallback; 37 import android.car.testapi.CarProjectionController; 38 import android.car.testapi.FakeCar; 39 import android.content.Context; 40 import android.content.Intent; 41 import android.net.MacAddress; 42 import android.net.wifi.SoftApConfiguration; 43 import android.net.wifi.WifiConfiguration; 44 import android.util.ArraySet; 45 46 import androidx.test.core.app.ApplicationProvider; 47 48 import org.junit.Before; 49 import org.junit.Test; 50 import org.junit.runner.RunWith; 51 import org.mockito.ArgumentCaptor; 52 import org.mockito.Captor; 53 import org.mockito.InOrder; 54 import org.mockito.Spy; 55 import org.mockito.junit.MockitoJUnitRunner; 56 57 import java.util.Arrays; 58 import java.util.Collections; 59 import java.util.Set; 60 import java.util.concurrent.CountDownLatch; 61 import java.util.concurrent.Executor; 62 import java.util.concurrent.TimeUnit; 63 64 @RunWith(MockitoJUnitRunner.class) 65 public class CarProjectionManagerUnitTest { 66 67 @Captor 68 private ArgumentCaptor<Intent> mIntentArgumentCaptor; 69 70 @Spy 71 private final Context mContext = ApplicationProvider.getApplicationContext(); 72 73 private static final int DEFAULT_TIMEOUT_MS = 1000; 74 75 /** An {@link Executor} that immediately runs its callbacks synchronously. */ 76 private static final Executor DIRECT_EXECUTOR = Runnable::run; 77 78 private CarProjectionManager mProjectionManager; 79 private CarProjectionController mController; 80 private ApCallback mApCallback; 81 82 @Before setUp()83 public void setUp() { 84 FakeCar fakeCar = FakeCar.createFakeCar(mContext); 85 mController = fakeCar.getCarProjectionController(); 86 mProjectionManager = 87 (CarProjectionManager) fakeCar.getCar().getCarManager(Car.PROJECTION_SERVICE); 88 assertThat(mProjectionManager).isNotNull(); 89 90 mApCallback = new ApCallback(); 91 } 92 93 @Test startAp_fail()94 public void startAp_fail() throws InterruptedException { 95 mController.setSoftApConfiguration(null); 96 97 mProjectionManager.startProjectionAccessPoint(mApCallback); 98 mApCallback.mFailed.await(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 99 assertThat(mApCallback.mFailureReason).isEqualTo(ERROR_GENERIC); 100 } 101 102 @Test startAp_success()103 public void startAp_success() throws InterruptedException { 104 SoftApConfiguration config = new SoftApConfiguration.Builder() 105 .setSsid("Hello") 106 .setBssid(MacAddress.fromString("AA:BB:CC:CC:DD:EE")) 107 .setPassphrase("password", SoftApConfiguration.SECURITY_TYPE_WPA2_PSK) 108 .setMacRandomizationSetting(SoftApConfiguration.RANDOMIZATION_NONE) 109 .build(); 110 mController.setSoftApConfiguration(config); 111 112 mProjectionManager.startProjectionAccessPoint(mApCallback); 113 mApCallback.mStarted.await(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 114 assertThat(mApCallback.mSoftApConfiguration).isEqualTo(config); 115 } 116 117 @Test startAp_success_setWifiConfiguration()118 public void startAp_success_setWifiConfiguration() throws InterruptedException { 119 SoftApConfiguration config = new SoftApConfiguration.Builder() 120 .setSsid("Hello") 121 .setBssid(MacAddress.fromString("AA:BB:CC:CC:DD:EE")) 122 .setPassphrase("password", SoftApConfiguration.SECURITY_TYPE_WPA2_PSK) 123 .setMacRandomizationSetting(SoftApConfiguration.RANDOMIZATION_NONE) 124 .build(); 125 WifiConfiguration wifiConfig = config.toWifiConfiguration(); 126 mController.setWifiConfiguration(wifiConfig); 127 128 mProjectionManager.startProjectionAccessPoint(mApCallback); 129 mApCallback.mStarted.await(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 130 131 assertThat(mApCallback.mSoftApConfiguration).isNull(); 132 assertThat(mApCallback.mWifiConfiguration).isEqualTo(wifiConfig); 133 } 134 135 @Test registerProjectionRunner()136 public void registerProjectionRunner() throws CarNotConnectedException { 137 Intent intent = new Intent("my_action"); 138 intent.setPackage("my.package"); 139 mProjectionManager.registerProjectionRunner(intent); 140 141 verify(mContext).bindService(mIntentArgumentCaptor.capture(), any(), 142 eq(Context.BIND_AUTO_CREATE)); 143 assertThat(mIntentArgumentCaptor.getValue()).isEqualTo(intent); 144 } 145 146 @Test keyEventListener_registerMultipleEventListeners()147 public void keyEventListener_registerMultipleEventListeners() { 148 CarProjectionManager.ProjectionKeyEventHandler eventHandler1 = 149 mock(CarProjectionManager.ProjectionKeyEventHandler.class); 150 CarProjectionManager.ProjectionKeyEventHandler eventHandler2 = 151 mock(CarProjectionManager.ProjectionKeyEventHandler.class); 152 153 mProjectionManager.addKeyEventHandler( 154 Collections.singleton(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP), 155 DIRECT_EXECUTOR, 156 eventHandler1); 157 mProjectionManager.addKeyEventHandler( 158 new ArraySet<>( 159 Arrays.asList( 160 CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP, 161 CarProjectionManager.KEY_EVENT_CALL_LONG_PRESS_KEY_DOWN)), 162 DIRECT_EXECUTOR, 163 eventHandler2); 164 165 mController.fireKeyEvent(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP); 166 verify(eventHandler1).onKeyEvent(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP); 167 verify(eventHandler2).onKeyEvent(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP); 168 169 mController.fireKeyEvent(CarProjectionManager.KEY_EVENT_CALL_LONG_PRESS_KEY_DOWN); 170 verify(eventHandler1, never()) 171 .onKeyEvent(CarProjectionManager.KEY_EVENT_CALL_LONG_PRESS_KEY_DOWN); 172 verify(eventHandler2).onKeyEvent(CarProjectionManager.KEY_EVENT_CALL_LONG_PRESS_KEY_DOWN); 173 174 mController.fireKeyEvent(CarProjectionManager.KEY_EVENT_CALL_KEY_DOWN); 175 verify(eventHandler1, never()).onKeyEvent(CarProjectionManager.KEY_EVENT_CALL_KEY_DOWN); 176 verify(eventHandler2, never()).onKeyEvent(CarProjectionManager.KEY_EVENT_CALL_KEY_DOWN); 177 } 178 179 @Test keyEventHandler_canRegisterAllEvents()180 public void keyEventHandler_canRegisterAllEvents() { 181 CarProjectionManager.ProjectionKeyEventHandler eventHandler = 182 mock(CarProjectionManager.ProjectionKeyEventHandler.class); 183 184 Set<Integer> events = new ArraySet<>(CarProjectionManager.NUM_KEY_EVENTS); 185 for (int evt = 0; evt < CarProjectionManager.NUM_KEY_EVENTS; evt++) { 186 events.add(evt); 187 } 188 189 mProjectionManager.addKeyEventHandler(events, DIRECT_EXECUTOR, eventHandler); 190 191 for (int evt : events) { 192 mController.fireKeyEvent(evt); 193 verify(eventHandler).onKeyEvent(evt); 194 } 195 } 196 197 @Test keyEventHandler_eventsOutOfRange_throw()198 public void keyEventHandler_eventsOutOfRange_throw() { 199 CarProjectionManager.ProjectionKeyEventHandler eventHandler = 200 mock(CarProjectionManager.ProjectionKeyEventHandler.class); 201 202 IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> 203 mProjectionManager.addKeyEventHandler(Collections.singleton(-1), 204 eventHandler)); 205 assertWithMessage("Exception is thrown").that(thrown).hasMessageThat() 206 .contains("Invalid key event"); 207 208 thrown = assertThrows(IllegalArgumentException.class, ()-> 209 mProjectionManager.addKeyEventHandler(Collections.singleton( 210 CarProjectionManager.NUM_KEY_EVENTS), eventHandler)); 211 assertWithMessage("Exception is thrown").that(thrown).hasMessageThat() 212 .contains("Invalid key event"); 213 } 214 215 @Test keyEventHandler_whenRegisteredAgain_replacesEventList()216 public void keyEventHandler_whenRegisteredAgain_replacesEventList() { 217 CarProjectionManager.ProjectionKeyEventHandler eventHandler = 218 mock(CarProjectionManager.ProjectionKeyEventHandler.class); 219 InOrder inOrder = inOrder(eventHandler); 220 221 mProjectionManager.addKeyEventHandler( 222 Collections.singleton(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP), 223 DIRECT_EXECUTOR, 224 eventHandler); 225 mController.fireKeyEvent(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP); 226 inOrder.verify(eventHandler) 227 .onKeyEvent(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP); 228 229 mProjectionManager.addKeyEventHandler( 230 Collections.singleton(CarProjectionManager.KEY_EVENT_CALL_LONG_PRESS_KEY_DOWN), 231 DIRECT_EXECUTOR, 232 eventHandler); 233 mController.fireKeyEvent(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP); 234 inOrder.verify(eventHandler, never()) 235 .onKeyEvent(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP); 236 } 237 238 @Test keyEventHandler_removed_noLongerFires()239 public void keyEventHandler_removed_noLongerFires() { 240 CarProjectionManager.ProjectionKeyEventHandler eventHandler = 241 mock(CarProjectionManager.ProjectionKeyEventHandler.class); 242 243 mProjectionManager.addKeyEventHandler( 244 Collections.singleton(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP), 245 DIRECT_EXECUTOR, 246 eventHandler); 247 mProjectionManager.removeKeyEventHandler(eventHandler); 248 249 mController.fireKeyEvent(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP); 250 verify(eventHandler, never()) 251 .onKeyEvent(CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP); 252 } 253 254 @Test keyEventHandler_withAlternateExecutor_usesExecutor()255 public void keyEventHandler_withAlternateExecutor_usesExecutor() { 256 CarProjectionManager.ProjectionKeyEventHandler eventHandler = 257 mock(CarProjectionManager.ProjectionKeyEventHandler.class); 258 Executor executor = mock(Executor.class); 259 ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class); 260 261 mProjectionManager.addKeyEventHandler( 262 Collections.singleton( 263 CarProjectionManager.KEY_EVENT_VOICE_SEARCH_SHORT_PRESS_KEY_UP), 264 executor, 265 eventHandler); 266 267 mController.fireKeyEvent(CarProjectionManager.KEY_EVENT_VOICE_SEARCH_SHORT_PRESS_KEY_UP); 268 verify(eventHandler, never()).onKeyEvent(anyInt()); 269 verify(executor).execute(runnableCaptor.capture()); 270 271 runnableCaptor.getValue().run(); 272 verify(eventHandler) 273 .onKeyEvent(CarProjectionManager.KEY_EVENT_VOICE_SEARCH_SHORT_PRESS_KEY_UP); 274 } 275 276 private static class ApCallback extends ProjectionAccessPointCallback { 277 CountDownLatch mStarted = new CountDownLatch(1); 278 CountDownLatch mFailed = new CountDownLatch(1); 279 int mFailureReason = -1; 280 SoftApConfiguration mSoftApConfiguration; 281 WifiConfiguration mWifiConfiguration; 282 283 @Override onStarted(WifiConfiguration wifiConfiguration)284 public void onStarted(WifiConfiguration wifiConfiguration) { 285 mWifiConfiguration = wifiConfiguration; 286 mStarted.countDown(); 287 } 288 289 @Override onStarted(SoftApConfiguration softApConfiguration)290 public void onStarted(SoftApConfiguration softApConfiguration) { 291 mSoftApConfiguration = softApConfiguration; 292 mStarted.countDown(); 293 } 294 295 @Override onStopped()296 public void onStopped() { 297 } 298 299 @Override onFailed(int reason)300 public void onFailed(int reason) { 301 mFailureReason = reason; 302 mFailed.countDown(); 303 } 304 } 305 } 306