1 /*
2  * Copyright (C) 2021 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.systemui.statusbar.phone.ongoingcall
18 
19 import android.app.ActivityManager
20 import android.app.IActivityManager
21 import android.app.IUidObserver
22 import android.app.Notification
23 import android.app.PendingIntent
24 import android.app.Person
25 import android.service.notification.NotificationListenerService.REASON_USER_STOPPED
26 import android.testing.AndroidTestingRunner
27 import android.testing.TestableLooper
28 import android.view.LayoutInflater
29 import android.view.View
30 import android.widget.LinearLayout
31 import androidx.test.filters.SmallTest
32 import com.android.internal.logging.testing.UiEventLoggerFake
33 import com.android.systemui.R
34 import com.android.systemui.SysuiTestCase
35 import com.android.systemui.dump.DumpManager
36 import com.android.systemui.plugins.ActivityStarter
37 import com.android.systemui.plugins.statusbar.StatusBarStateController
38 import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler
39 import com.android.systemui.statusbar.notification.collection.NotificationEntry
40 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
41 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
42 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
43 import com.android.systemui.statusbar.window.StatusBarWindowController
44 import com.android.systemui.util.concurrency.FakeExecutor
45 import com.android.systemui.util.mockito.any
46 import com.android.systemui.util.time.FakeSystemClock
47 import com.google.common.truth.Truth.assertThat
48 import org.junit.After
49 import org.junit.Before
50 import org.junit.Test
51 import org.junit.runner.RunWith
52 import org.mockito.ArgumentCaptor
53 import org.mockito.ArgumentMatchers.anyBoolean
54 import org.mockito.ArgumentMatchers.anyString
55 import org.mockito.ArgumentMatchers.nullable
56 import org.mockito.Mock
57 import org.mockito.Mockito.`when`
58 import org.mockito.Mockito.eq
59 import org.mockito.Mockito.mock
60 import org.mockito.Mockito.never
61 import org.mockito.Mockito.reset
62 import org.mockito.Mockito.times
63 import org.mockito.Mockito.verify
64 import org.mockito.MockitoAnnotations
65 import java.util.*
66 
67 private const val CALL_UID = 900
68 
69 // A process state that represents the process being visible to the user.
70 private const val PROC_STATE_VISIBLE = ActivityManager.PROCESS_STATE_TOP
71 
72 // A process state that represents the process being invisible to the user.
73 private const val PROC_STATE_INVISIBLE = ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
74 
75 @SmallTest
76 @RunWith(AndroidTestingRunner::class)
77 @TestableLooper.RunWithLooper
78 class OngoingCallControllerTest : SysuiTestCase() {
79 
80     private val clock = FakeSystemClock()
81     private val mainExecutor = FakeExecutor(clock)
82     private val uiEventLoggerFake = UiEventLoggerFake()
83 
84     private lateinit var controller: OngoingCallController
85     private lateinit var notifCollectionListener: NotifCollectionListener
86 
87     @Mock private lateinit var mockOngoingCallFlags: OngoingCallFlags
88     @Mock private lateinit var mockSwipeStatusBarAwayGestureHandler:
89         SwipeStatusBarAwayGestureHandler
90     @Mock private lateinit var mockOngoingCallListener: OngoingCallListener
91     @Mock private lateinit var mockActivityStarter: ActivityStarter
92     @Mock private lateinit var mockIActivityManager: IActivityManager
93     @Mock private lateinit var mockStatusBarWindowController: StatusBarWindowController
94     @Mock private lateinit var mockStatusBarStateController: StatusBarStateController
95 
96     private lateinit var chipView: View
97 
98     @Before
99     fun setUp() {
100         allowTestableLooperAsMainThread()
101         TestableLooper.get(this).runWithLooper {
102             chipView = LayoutInflater.from(mContext).inflate(R.layout.ongoing_call_chip, null)
103         }
104 
105         MockitoAnnotations.initMocks(this)
106         `when`(mockOngoingCallFlags.isStatusBarChipEnabled()).thenReturn(true)
107         val notificationCollection = mock(CommonNotifCollection::class.java)
108 
109         controller = OngoingCallController(
110                 context,
111                 notificationCollection,
112                 mockOngoingCallFlags,
113                 clock,
114                 mockActivityStarter,
115                 mainExecutor,
116                 mockIActivityManager,
117                 OngoingCallLogger(uiEventLoggerFake),
118                 DumpManager(),
119                 Optional.of(mockStatusBarWindowController),
120                 Optional.of(mockSwipeStatusBarAwayGestureHandler),
121                 mockStatusBarStateController,
122             )
123         controller.init()
124         controller.addCallback(mockOngoingCallListener)
125         controller.setChipView(chipView)
126 
127         val collectionListenerCaptor = ArgumentCaptor.forClass(NotifCollectionListener::class.java)
128         verify(notificationCollection).addCollectionListener(collectionListenerCaptor.capture())
129         notifCollectionListener = collectionListenerCaptor.value!!
130 
131         `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
132                 .thenReturn(PROC_STATE_INVISIBLE)
133     }
134 
135     @After
136     fun tearDown() {
137         controller.tearDownChipView()
138     }
139 
140     @Test
141     fun onEntryUpdated_isOngoingCallNotif_listenerNotified() {
142         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
143 
144         verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
145     }
146 
147     @Test
148     fun onEntryUpdated_isOngoingCallNotif_windowControllerUpdated() {
149         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
150 
151         verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(true)
152     }
153 
154     @Test
155     fun onEntryUpdated_notOngoingCallNotif_listenerNotNotified() {
156         notifCollectionListener.onEntryUpdated(createNotCallNotifEntry())
157 
158         verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean())
159     }
160 
161     @Test
162     fun onEntryUpdated_ongoingCallNotifThenScreeningCallNotif_listenerNotifiedTwice() {
163         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
164         notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry())
165 
166         verify(mockOngoingCallListener, times(2))
167                 .onOngoingCallStateChanged(anyBoolean())
168     }
169 
170     /** Regression test for b/191472854. */
171     @Test
172     fun onEntryUpdated_notifHasNullContentIntent_noCrash() {
173         notifCollectionListener.onEntryUpdated(
174                 createCallNotifEntry(ongoingCallStyle, nullContentIntent = true))
175     }
176 
177     /** Regression test for b/192379214. */
178     @Test
179     fun onEntryUpdated_notificationWhenIsZero_timeHidden() {
180         val notification = NotificationEntryBuilder(createOngoingCallNotifEntry())
181         notification.modifyNotification(context).setWhen(0)
182 
183         notifCollectionListener.onEntryUpdated(notification.build())
184         chipView.measure(
185                 View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
186                 View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
187         )
188 
189         assertThat(chipView.findViewById<View>(R.id.ongoing_call_chip_time)?.measuredWidth)
190                 .isEqualTo(0)
191     }
192 
193     @Test
194     fun onEntryUpdated_notificationWhenIsValid_timeShown() {
195         val notification = NotificationEntryBuilder(createOngoingCallNotifEntry())
196         notification.modifyNotification(context).setWhen(clock.currentTimeMillis())
197 
198         notifCollectionListener.onEntryUpdated(notification.build())
199         chipView.measure(
200                 View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
201                 View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
202         )
203 
204         assertThat(chipView.findViewById<View>(R.id.ongoing_call_chip_time)?.measuredWidth)
205                 .isGreaterThan(0)
206     }
207 
208     /** Regression test for b/194731244. */
209     @Test
210     fun onEntryUpdated_calledManyTimes_uidObserverOnlyRegisteredOnce() {
211         for (i in 0 until 4) {
212             // Re-create the notification each time so that it's considered a different object and
213             // will re-trigger the whole flow.
214             notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
215         }
216 
217         verify(mockIActivityManager, times(1))
218             .registerUidObserver(any(), any(), any(), any())
219     }
220 
221     /** Regression test for b/216248574. */
222     @Test
223     fun entryUpdated_getUidProcessStateThrowsException_noCrash() {
224         `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
225                 .thenThrow(SecurityException())
226 
227         // No assert required, just check no crash
228         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
229     }
230 
231     /** Regression test for b/216248574. */
232     @Test
233     fun entryUpdated_registerUidObserverThrowsException_noCrash() {
234         `when`(mockIActivityManager.registerUidObserver(
235             any(), any(), any(), nullable(String::class.java)
236         )).thenThrow(SecurityException())
237 
238         // No assert required, just check no crash
239         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
240     }
241 
242     /** Regression test for b/216248574. */
243     @Test
244     fun entryUpdated_packageNameProvidedToActivityManager() {
245         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
246 
247         val packageNameCaptor = ArgumentCaptor.forClass(String::class.java)
248         verify(mockIActivityManager).registerUidObserver(
249             any(), any(), any(), packageNameCaptor.capture()
250         )
251         assertThat(packageNameCaptor.value).isNotNull()
252     }
253 
254     /**
255      * If a call notification is never added before #onEntryRemoved is called, then the listener
256      * should never be notified.
257      */
258     @Test
259     fun onEntryRemoved_callNotifNeverAddedBeforehand_listenerNotNotified() {
260         notifCollectionListener.onEntryRemoved(createOngoingCallNotifEntry(), REASON_USER_STOPPED)
261 
262         verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean())
263     }
264 
265     @Test
266     fun onEntryRemoved_callNotifAddedThenRemoved_listenerNotified() {
267         val ongoingCallNotifEntry = createOngoingCallNotifEntry()
268         notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
269         reset(mockOngoingCallListener)
270 
271         notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED)
272 
273         verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
274     }
275 
276     @Test
277     fun onEntryUpdated_callNotifAddedThenRemoved_windowControllerUpdated() {
278         val ongoingCallNotifEntry = createOngoingCallNotifEntry()
279         notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
280 
281         notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED)
282 
283         verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(false)
284     }
285 
286     /** Regression test for b/188491504. */
287     @Test
288     fun onEntryRemoved_removedNotifHasSameKeyAsAddedNotif_listenerNotified() {
289         val ongoingCallNotifEntry = createOngoingCallNotifEntry()
290         notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
291         reset(mockOngoingCallListener)
292 
293         // Create another notification based on the ongoing call one, but remove the features that
294         // made it a call notification.
295         val removedEntryBuilder = NotificationEntryBuilder(ongoingCallNotifEntry)
296         removedEntryBuilder.modifyNotification(context).style = null
297 
298         notifCollectionListener.onEntryRemoved(removedEntryBuilder.build(), REASON_USER_STOPPED)
299 
300         verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
301     }
302 
303     @Test
304     fun onEntryRemoved_notifKeyDoesNotMatchOngoingCallNotif_listenerNotNotified() {
305         notifCollectionListener.onEntryAdded(createOngoingCallNotifEntry())
306         reset(mockOngoingCallListener)
307 
308         notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED)
309 
310         verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean())
311     }
312 
313     @Test
314     fun hasOngoingCall_noOngoingCallNotifSent_returnsFalse() {
315         assertThat(controller.hasOngoingCall()).isFalse()
316     }
317 
318     @Test
319     fun hasOngoingCall_unrelatedNotifSent_returnsFalse() {
320         notifCollectionListener.onEntryUpdated(createNotCallNotifEntry())
321 
322         assertThat(controller.hasOngoingCall()).isFalse()
323     }
324 
325     @Test
326     fun hasOngoingCall_screeningCallNotifSent_returnsFalse() {
327         notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry())
328 
329         assertThat(controller.hasOngoingCall()).isFalse()
330     }
331 
332     @Test
333     fun hasOngoingCall_ongoingCallNotifSentAndCallAppNotVisible_returnsTrue() {
334         `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
335                 .thenReturn(PROC_STATE_INVISIBLE)
336 
337         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
338 
339         assertThat(controller.hasOngoingCall()).isTrue()
340     }
341 
342     @Test
343     fun hasOngoingCall_ongoingCallNotifSentButCallAppVisible_returnsFalse() {
344         `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
345                 .thenReturn(PROC_STATE_VISIBLE)
346 
347         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
348 
349         assertThat(controller.hasOngoingCall()).isFalse()
350     }
351 
352     @Test
353     fun hasOngoingCall_ongoingCallNotifSentButInvalidChipView_returnsFalse() {
354         val invalidChipView = LinearLayout(context)
355         controller.setChipView(invalidChipView)
356 
357         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
358 
359         assertThat(controller.hasOngoingCall()).isFalse()
360     }
361 
362     @Test
363     fun hasOngoingCall_ongoingCallNotifSentThenRemoved_returnsFalse() {
364         val ongoingCallNotifEntry = createOngoingCallNotifEntry()
365 
366         notifCollectionListener.onEntryUpdated(ongoingCallNotifEntry)
367         notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED)
368 
369         assertThat(controller.hasOngoingCall()).isFalse()
370     }
371 
372     @Test
373     fun hasOngoingCall_ongoingCallNotifSentThenScreeningCallNotifSent_returnsFalse() {
374         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
375         notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry())
376 
377         assertThat(controller.hasOngoingCall()).isFalse()
378     }
379 
380     @Test
381     fun hasOngoingCall_ongoingCallNotifSentThenUnrelatedNotifSent_returnsTrue() {
382         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
383         notifCollectionListener.onEntryUpdated(createNotCallNotifEntry())
384 
385         assertThat(controller.hasOngoingCall()).isTrue()
386     }
387 
388     /**
389      * This test fakes a theme change during an ongoing call.
390      *
391      * When a theme change happens, [CollapsedStatusBarFragment] and its views get re-created, so
392      * [OngoingCallController.setChipView] gets called with a new view. If there's an active ongoing
393      * call when the theme changes, the new view needs to be updated with the call information.
394      */
395     @Test
396     fun setChipView_whenHasOngoingCallIsTrue_listenerNotifiedWithNewView() {
397         // Start an ongoing call.
398         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
399         reset(mockOngoingCallListener)
400 
401         lateinit var newChipView: View
402         TestableLooper.get(this).runWithLooper {
403             newChipView = LayoutInflater.from(mContext).inflate(R.layout.ongoing_call_chip, null)
404         }
405 
406         // Change the chip view associated with the controller.
407         controller.setChipView(newChipView)
408 
409         verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
410     }
411 
412     @Test
413     fun callProcessChangesToVisible_listenerNotified() {
414         // Start the call while the process is invisible.
415         `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
416                 .thenReturn(PROC_STATE_INVISIBLE)
417         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
418         reset(mockOngoingCallListener)
419 
420         val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java)
421         verify(mockIActivityManager).registerUidObserver(
422                 captor.capture(), any(), any(), nullable(String::class.java))
423         val uidObserver = captor.value
424 
425         // Update the process to visible.
426         uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_VISIBLE, 0, 0)
427         mainExecutor.advanceClockToLast()
428         mainExecutor.runAllReady()
429 
430         verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
431     }
432 
433     @Test
434     fun callProcessChangesToInvisible_listenerNotified() {
435         // Start the call while the process is visible.
436         `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java)))
437                 .thenReturn(PROC_STATE_VISIBLE)
438         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
439         reset(mockOngoingCallListener)
440 
441         val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java)
442         verify(mockIActivityManager).registerUidObserver(
443                 captor.capture(), any(), any(), nullable(String::class.java))
444         val uidObserver = captor.value
445 
446         // Update the process to invisible.
447         uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_INVISIBLE, 0, 0)
448         mainExecutor.advanceClockToLast()
449         mainExecutor.runAllReady()
450 
451         verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
452     }
453 
454     @Test
455     fun chipClicked_clickEventLogged() {
456         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
457 
458         chipView.performClick()
459 
460         assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
461         assertThat(uiEventLoggerFake.eventId(0))
462                 .isEqualTo(OngoingCallLogger.OngoingCallEvents.ONGOING_CALL_CLICKED.id)
463     }
464 
465     /** Regression test for b/212467440. */
466     @Test
467     fun chipClicked_activityStarterTriggeredWithUnmodifiedIntent() {
468         val notifEntry = createOngoingCallNotifEntry()
469         val pendingIntent = notifEntry.sbn.notification.contentIntent
470         notifCollectionListener.onEntryUpdated(notifEntry)
471 
472         chipView.performClick()
473 
474         // Ensure that the sysui didn't modify the notification's intent -- see b/212467440.
475         verify(mockActivityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent), any())
476     }
477 
478     @Test
479     fun notifyChipVisibilityChanged_visibleEventLogged() {
480         controller.notifyChipVisibilityChanged(true)
481 
482         assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
483         assertThat(uiEventLoggerFake.eventId(0))
484                 .isEqualTo(OngoingCallLogger.OngoingCallEvents.ONGOING_CALL_VISIBLE.id)
485     }
486     // Other tests for notifyChipVisibilityChanged are in [OngoingCallLogger], since
487     // [OngoingCallController.notifyChipVisibilityChanged] just delegates to that class.
488 
489     @Test
490     fun callNotificationAdded_chipIsClickable() {
491         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
492 
493         assertThat(chipView.hasOnClickListeners()).isTrue()
494     }
495 
496     @Test
497     fun fullscreenIsTrue_thenCallNotificationAdded_chipNotClickable() {
498         `when`(mockOngoingCallFlags.isInImmersiveChipTapEnabled()).thenReturn(false)
499 
500         getStateListener().onFullscreenStateChanged(/* isFullscreen= */ true)
501         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
502 
503         assertThat(chipView.hasOnClickListeners()).isFalse()
504     }
505 
506     @Test
507     fun callNotificationAdded_thenFullscreenIsTrue_chipNotClickable() {
508         `when`(mockOngoingCallFlags.isInImmersiveChipTapEnabled()).thenReturn(false)
509 
510         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
511         getStateListener().onFullscreenStateChanged(/* isFullscreen= */ true)
512 
513         assertThat(chipView.hasOnClickListeners()).isFalse()
514     }
515 
516     @Test
517     fun fullscreenChangesToFalse_chipClickable() {
518         `when`(mockOngoingCallFlags.isInImmersiveChipTapEnabled()).thenReturn(false)
519 
520         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
521         // First, update to true
522         getStateListener().onFullscreenStateChanged(/* isFullscreen= */ true)
523         // Then, update to false
524         getStateListener().onFullscreenStateChanged(/* isFullscreen= */ false)
525 
526         assertThat(chipView.hasOnClickListeners()).isTrue()
527     }
528 
529     @Test
530     fun fullscreenIsTrue_butChipClickInImmersiveEnabled_chipClickable() {
531         `when`(mockOngoingCallFlags.isInImmersiveChipTapEnabled()).thenReturn(true)
532 
533         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
534         getStateListener().onFullscreenStateChanged(/* isFullscreen= */ true)
535 
536         assertThat(chipView.hasOnClickListeners()).isTrue()
537     }
538 
539     // Swipe gesture tests
540 
541     @Test
542     fun callStartedInImmersiveMode_swipeGestureCallbackAdded() {
543         getStateListener().onFullscreenStateChanged(true)
544 
545         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
546 
547         verify(mockSwipeStatusBarAwayGestureHandler)
548             .addOnGestureDetectedCallback(anyString(), any())
549     }
550 
551     @Test
552     fun callStartedNotInImmersiveMode_swipeGestureCallbackNotAdded() {
553         getStateListener().onFullscreenStateChanged(false)
554 
555         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
556 
557         verify(mockSwipeStatusBarAwayGestureHandler, never())
558             .addOnGestureDetectedCallback(anyString(), any())
559     }
560 
561     @Test
562     fun transitionToImmersiveMode_swipeGestureCallbackAdded() {
563         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
564 
565         getStateListener().onFullscreenStateChanged(true)
566 
567         verify(mockSwipeStatusBarAwayGestureHandler)
568             .addOnGestureDetectedCallback(anyString(), any())
569     }
570 
571     @Test
572     fun transitionOutOfImmersiveMode_swipeGestureCallbackRemoved() {
573         getStateListener().onFullscreenStateChanged(true)
574         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
575         reset(mockSwipeStatusBarAwayGestureHandler)
576 
577         getStateListener().onFullscreenStateChanged(false)
578 
579         verify(mockSwipeStatusBarAwayGestureHandler)
580             .removeOnGestureDetectedCallback(anyString())
581     }
582 
583     @Test
584     fun callEndedWhileInImmersiveMode_swipeGestureCallbackRemoved() {
585         getStateListener().onFullscreenStateChanged(true)
586         val ongoingCallNotifEntry = createOngoingCallNotifEntry()
587         notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
588         reset(mockSwipeStatusBarAwayGestureHandler)
589 
590         notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED)
591 
592         verify(mockSwipeStatusBarAwayGestureHandler)
593             .removeOnGestureDetectedCallback(anyString())
594     }
595 
596     // TODO(b/195839150): Add test
597     //  swipeGesturedTriggeredPreviously_entersImmersiveModeAgain_callbackNotAdded(). That's
598     //  difficult to add now because we have no way to trigger [SwipeStatusBarAwayGestureHandler]'s
599     //  callbacks in test.
600 
601     // END swipe gesture tests
602 
603     private fun createOngoingCallNotifEntry() = createCallNotifEntry(ongoingCallStyle)
604 
605     private fun createScreeningCallNotifEntry() = createCallNotifEntry(screeningCallStyle)
606 
607     private fun createCallNotifEntry(
608         callStyle: Notification.CallStyle,
609         nullContentIntent: Boolean = false
610     ): NotificationEntry {
611         val notificationEntryBuilder = NotificationEntryBuilder()
612         notificationEntryBuilder.modifyNotification(context).style = callStyle
613         notificationEntryBuilder.setUid(CALL_UID)
614 
615         if (nullContentIntent) {
616             notificationEntryBuilder.modifyNotification(context).setContentIntent(null)
617         } else {
618             val contentIntent = mock(PendingIntent::class.java)
619             notificationEntryBuilder.modifyNotification(context).setContentIntent(contentIntent)
620         }
621 
622         return notificationEntryBuilder.build()
623     }
624 
625     private fun createNotCallNotifEntry() = NotificationEntryBuilder().build()
626 
627     private fun getStateListener(): StatusBarStateController.StateListener {
628         val statusBarStateListenerCaptor = ArgumentCaptor.forClass(
629             StatusBarStateController.StateListener::class.java)
630         verify(mockStatusBarStateController).addCallback(statusBarStateListenerCaptor.capture())
631         return statusBarStateListenerCaptor.value!!
632     }
633 }
634 
635 private val person = Person.Builder().setName("name").build()
636 private val hangUpIntent = mock(PendingIntent::class.java)
637 
638 private val ongoingCallStyle = Notification.CallStyle.forOngoingCall(person, hangUpIntent)
639 private val screeningCallStyle = Notification.CallStyle.forScreeningCall(
640         person, hangUpIntent, /* answerIntent= */ mock(PendingIntent::class.java))