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