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.temporarydisplay.chipbar
18 
19 import android.os.PowerManager
20 import android.os.VibrationAttributes
21 import android.os.VibrationEffect
22 import android.testing.TestableLooper
23 import android.view.MotionEvent
24 import android.view.View
25 import android.view.ViewGroup
26 import android.view.WindowManager
27 import android.view.accessibility.AccessibilityManager
28 import android.widget.ImageView
29 import android.widget.TextView
30 import androidx.core.animation.doOnCancel
31 import androidx.test.ext.junit.runners.AndroidJUnit4
32 import androidx.test.filters.SmallTest
33 import com.android.app.viewcapture.ViewCapture
34 import com.android.app.viewcapture.ViewCaptureAwareWindowManager
35 import com.android.internal.logging.InstanceId
36 import com.android.internal.logging.testing.UiEventLoggerFake
37 import com.android.systemui.SysuiTestCase
38 import com.android.systemui.classifier.FalsingCollector
39 import com.android.systemui.common.shared.model.ContentDescription
40 import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
41 import com.android.systemui.common.shared.model.Icon
42 import com.android.systemui.common.shared.model.Text
43 import com.android.systemui.common.shared.model.TintedIcon
44 import com.android.systemui.dump.DumpManager
45 import com.android.systemui.plugins.FalsingManager
46 import com.android.systemui.res.R
47 import com.android.systemui.statusbar.VibratorHelper
48 import com.android.systemui.statusbar.policy.ConfigurationController
49 import com.android.systemui.temporarydisplay.TemporaryViewUiEvent
50 import com.android.systemui.temporarydisplay.TemporaryViewUiEventLogger
51 import com.android.systemui.temporarydisplay.ViewPriority
52 import com.android.systemui.util.concurrency.FakeExecutor
53 import com.android.systemui.util.mockito.any
54 import com.android.systemui.util.mockito.argumentCaptor
55 import com.android.systemui.util.mockito.capture
56 import com.android.systemui.util.mockito.eq
57 import com.android.systemui.util.time.FakeSystemClock
58 import com.android.systemui.util.view.ViewUtil
59 import com.android.systemui.util.wakelock.WakeLockFake
60 import com.google.common.truth.Truth.assertThat
61 import org.junit.Before
62 import org.junit.Test
63 import org.junit.runner.RunWith
64 import org.mockito.ArgumentCaptor
65 import org.mockito.ArgumentMatchers.anyInt
66 import org.mockito.Mock
67 import org.mockito.Mockito.never
68 import org.mockito.Mockito.verify
69 import org.mockito.Mockito.`when` as whenever
70 import org.mockito.MockitoAnnotations
71 
72 @SmallTest
73 @RunWith(AndroidJUnit4::class)
74 @TestableLooper.RunWithLooper
75 class ChipbarCoordinatorTest : SysuiTestCase() {
76     private lateinit var underTest: ChipbarCoordinator
77 
78     @Mock private lateinit var logger: ChipbarLogger
79     @Mock private lateinit var accessibilityManager: AccessibilityManager
80     @Mock private lateinit var configurationController: ConfigurationController
81     @Mock private lateinit var dumpManager: DumpManager
82     @Mock private lateinit var powerManager: PowerManager
83     @Mock private lateinit var windowManager: WindowManager
84     @Mock private lateinit var falsingManager: FalsingManager
85     @Mock private lateinit var falsingCollector: FalsingCollector
86     @Mock private lateinit var viewUtil: ViewUtil
87     @Mock private lateinit var vibratorHelper: VibratorHelper
88     @Mock private lateinit var swipeGestureHandler: SwipeChipbarAwayGestureHandler
89     @Mock private lateinit var lazyViewCapture: Lazy<ViewCapture>
90     private lateinit var chipbarAnimator: TestChipbarAnimator
91     private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder
92     private lateinit var fakeWakeLock: WakeLockFake
93     private lateinit var fakeClock: FakeSystemClock
94     private lateinit var fakeExecutor: FakeExecutor
95     private lateinit var uiEventLoggerFake: UiEventLoggerFake
96     private lateinit var uiEventLogger: TemporaryViewUiEventLogger
97 
98     @Before
setUpnull99     fun setUp() {
100         MockitoAnnotations.initMocks(this)
101         whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT)
102 
103         fakeClock = FakeSystemClock()
104         fakeExecutor = FakeExecutor(fakeClock)
105 
106         fakeWakeLock = WakeLockFake()
107         fakeWakeLockBuilder = WakeLockFake.Builder(context)
108         fakeWakeLockBuilder.setWakeLock(fakeWakeLock)
109 
110         uiEventLoggerFake = UiEventLoggerFake()
111         uiEventLogger = TemporaryViewUiEventLogger(uiEventLoggerFake)
112         chipbarAnimator = TestChipbarAnimator()
113 
114         underTest =
115             ChipbarCoordinator(
116                 context,
117                 logger,
118                 ViewCaptureAwareWindowManager(windowManager, lazyViewCapture,
119                         isViewCaptureEnabled = false),
120                 fakeExecutor,
121                 accessibilityManager,
122                 configurationController,
123                 dumpManager,
124                 powerManager,
125                 chipbarAnimator,
126                 falsingManager,
127                 falsingCollector,
128                 swipeGestureHandler,
129                 viewUtil,
130                 vibratorHelper,
131                 fakeWakeLockBuilder,
132                 fakeClock,
133                 uiEventLogger,
134             )
135         underTest.start()
136     }
137 
138     @Test
displayView_contentDescription_iconHasDescriptionnull139     fun displayView_contentDescription_iconHasDescription() {
140         underTest.displayView(
141             createChipbarInfo(
142                 Icon.Resource(R.drawable.ic_cake, ContentDescription.Loaded("loadedCD")),
143                 Text.Loaded("text"),
144                 endItem = null,
145             )
146         )
147 
148         val contentDescView = getChipbarView().getInnerView()
149         assertThat(contentDescView.contentDescription.toString()).contains("loadedCD")
150         assertThat(contentDescView.contentDescription.toString()).contains("text")
151     }
152 
153     @Test
displayView_contentDescription_iconHasNoDescriptionnull154     fun displayView_contentDescription_iconHasNoDescription() {
155         underTest.displayView(
156             createChipbarInfo(
157                 Icon.Resource(R.drawable.ic_cake, contentDescription = null),
158                 Text.Loaded("text"),
159                 endItem = null,
160             )
161         )
162 
163         val contentDescView = getChipbarView().getInnerView()
164         assertThat(contentDescView.contentDescription.toString()).isEqualTo("text")
165     }
166 
167     @Test
displayView_contentDescription_endIsLoadingnull168     fun displayView_contentDescription_endIsLoading() {
169         underTest.displayView(
170             createChipbarInfo(
171                 Icon.Resource(R.drawable.ic_cake, ContentDescription.Loaded("loadedCD")),
172                 Text.Loaded("text"),
173                 endItem = ChipbarEndItem.Loading,
174             )
175         )
176 
177         val contentDescView = getChipbarView().getInnerView()
178         val loadingDesc = context.resources.getString(R.string.media_transfer_loading)
179         assertThat(contentDescView.contentDescription.toString()).contains("text")
180         assertThat(contentDescView.contentDescription.toString()).contains(loadingDesc)
181     }
182 
183     @Test
displayView_contentDescription_endNotLoadingnull184     fun displayView_contentDescription_endNotLoading() {
185         underTest.displayView(
186             createChipbarInfo(
187                 Icon.Resource(R.drawable.ic_cake, ContentDescription.Loaded("loadedCD")),
188                 Text.Loaded("text"),
189                 endItem = ChipbarEndItem.Error,
190             )
191         )
192 
193         val contentDescView = getChipbarView().getInnerView()
194         val loadingDesc = context.resources.getString(R.string.media_transfer_loading)
195         assertThat(contentDescView.contentDescription.toString()).contains("text")
196         assertThat(contentDescView.contentDescription.toString()).doesNotContain(loadingDesc)
197     }
198 
199     @Test
displayView_loadedIcon_correctlyRenderednull200     fun displayView_loadedIcon_correctlyRendered() {
201         val drawable = context.getDrawable(R.drawable.ic_celebration)!!
202 
203         underTest.displayView(
204             createChipbarInfo(
205                 Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")),
206                 Text.Loaded("text"),
207                 endItem = null,
208             )
209         )
210 
211         val iconView = getChipbarView().getStartIconView()
212         assertThat(iconView.drawable).isEqualTo(drawable)
213         assertThat(iconView.contentDescription).isEqualTo("loadedCD")
214     }
215 
216     @Test
displayView_resourceIcon_correctlyRenderednull217     fun displayView_resourceIcon_correctlyRendered() {
218         val contentDescription = ContentDescription.Resource(R.string.controls_error_timeout)
219         underTest.displayView(
220             createChipbarInfo(
221                 Icon.Resource(R.drawable.ic_cake, contentDescription),
222                 Text.Loaded("text"),
223                 endItem = null,
224             )
225         )
226 
227         val iconView = getChipbarView().getStartIconView()
228         assertThat(iconView.contentDescription)
229             .isEqualTo(contentDescription.loadContentDescription(context))
230     }
231 
232     @Test
displayView_loadedText_correctlyRenderednull233     fun displayView_loadedText_correctlyRendered() {
234         underTest.displayView(
235             createChipbarInfo(
236                 Icon.Resource(R.id.check_box, null),
237                 Text.Loaded("display view text here"),
238                 endItem = null,
239             )
240         )
241 
242         assertThat(getChipbarView().getChipText()).isEqualTo("display view text here")
243     }
244 
245     @Test
displayView_resourceText_correctlyRenderednull246     fun displayView_resourceText_correctlyRendered() {
247         underTest.displayView(
248             createChipbarInfo(
249                 Icon.Resource(R.id.check_box, null),
250                 Text.Resource(R.string.screenrecord_start_error),
251                 endItem = null,
252             )
253         )
254 
255         assertThat(getChipbarView().getChipText())
256             .isEqualTo(context.getString(R.string.screenrecord_start_error))
257     }
258 
259     @Test
displayView_endItemNull_correctlyRenderednull260     fun displayView_endItemNull_correctlyRendered() {
261         underTest.displayView(
262             createChipbarInfo(
263                 Icon.Resource(R.id.check_box, null),
264                 Text.Loaded("text"),
265                 endItem = null,
266             )
267         )
268 
269         val chipbarView = getChipbarView()
270         assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
271         assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
272         assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE)
273     }
274 
275     @Test
displayView_endItemLoading_correctlyRenderednull276     fun displayView_endItemLoading_correctlyRendered() {
277         underTest.displayView(
278             createChipbarInfo(
279                 Icon.Resource(R.id.check_box, null),
280                 Text.Loaded("text"),
281                 endItem = ChipbarEndItem.Loading,
282             )
283         )
284 
285         val chipbarView = getChipbarView()
286         assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE)
287         assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
288         assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE)
289     }
290 
291     @Test
displayView_endItemError_correctlyRenderednull292     fun displayView_endItemError_correctlyRendered() {
293         underTest.displayView(
294             createChipbarInfo(
295                 Icon.Resource(R.id.check_box, null),
296                 Text.Loaded("text"),
297                 endItem = ChipbarEndItem.Error,
298             )
299         )
300 
301         val chipbarView = getChipbarView()
302         assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
303         assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE)
304         assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE)
305     }
306 
307     @Test
displayView_endItemButton_correctlyRenderednull308     fun displayView_endItemButton_correctlyRendered() {
309         underTest.displayView(
310             createChipbarInfo(
311                 Icon.Resource(R.id.check_box, null),
312                 Text.Loaded("text"),
313                 endItem =
314                     ChipbarEndItem.Button(
315                         Text.Loaded("button text"),
316                         onClickListener = {},
317                     ),
318             )
319         )
320 
321         val chipbarView = getChipbarView()
322         assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
323         assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
324         assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.VISIBLE)
325         assertThat(chipbarView.getEndButton().text).isEqualTo("button text")
326         assertThat(chipbarView.getEndButton().hasOnClickListeners()).isTrue()
327     }
328 
329     @Test
displayView_endItemButtonClicked_falseTap_listenerNotRunnull330     fun displayView_endItemButtonClicked_falseTap_listenerNotRun() {
331         whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true)
332         var isClicked = false
333         val buttonClickListener = View.OnClickListener { isClicked = true }
334 
335         underTest.displayView(
336             createChipbarInfo(
337                 Icon.Resource(R.id.check_box, null),
338                 Text.Loaded("text"),
339                 endItem =
340                     ChipbarEndItem.Button(
341                         Text.Loaded("button text"),
342                         buttonClickListener,
343                     ),
344             )
345         )
346 
347         getChipbarView().getEndButton().performClick()
348 
349         assertThat(isClicked).isFalse()
350     }
351 
352     @Test
displayView_endItemButtonClicked_notFalseTap_listenerRunnull353     fun displayView_endItemButtonClicked_notFalseTap_listenerRun() {
354         whenever(falsingManager.isFalseTap(anyInt())).thenReturn(false)
355         var isClicked = false
356         val buttonClickListener = View.OnClickListener { isClicked = true }
357 
358         underTest.displayView(
359             createChipbarInfo(
360                 Icon.Resource(R.id.check_box, null),
361                 Text.Loaded("text"),
362                 endItem =
363                     ChipbarEndItem.Button(
364                         Text.Loaded("button text"),
365                         buttonClickListener,
366                     ),
367             )
368         )
369 
370         getChipbarView().getEndButton().performClick()
371 
372         assertThat(isClicked).isTrue()
373     }
374 
375     @Test
displayView_loading_animationStartednull376     fun displayView_loading_animationStarted() {
377         underTest.displayView(
378             createChipbarInfo(
379                 Icon.Resource(R.id.check_box, null),
380                 Text.Loaded("text"),
381                 endItem = ChipbarEndItem.Loading,
382             )
383         )
384 
385         assertThat(underTest.loadingDetails!!.animator.isStarted).isTrue()
386     }
387 
388     @Test
displayView_notLoading_noAnimationnull389     fun displayView_notLoading_noAnimation() {
390         underTest.displayView(
391             createChipbarInfo(
392                 Icon.Resource(R.id.check_box, null),
393                 Text.Loaded("text"),
394                 endItem = ChipbarEndItem.Error,
395             )
396         )
397 
398         assertThat(underTest.loadingDetails).isNull()
399     }
400 
401     @Test
displayView_loadingThenNotLoading_animationStoppednull402     fun displayView_loadingThenNotLoading_animationStopped() {
403         underTest.displayView(
404             createChipbarInfo(
405                 Icon.Resource(R.id.check_box, null),
406                 Text.Loaded("text"),
407                 endItem = ChipbarEndItem.Loading,
408             )
409         )
410 
411         val animator = underTest.loadingDetails!!.animator
412         var cancelled = false
413         animator.doOnCancel { cancelled = true }
414 
415         underTest.displayView(
416             createChipbarInfo(
417                 Icon.Resource(R.id.check_box, null),
418                 Text.Loaded("text"),
419                 endItem = ChipbarEndItem.Button(Text.Loaded("button")) {},
420             )
421         )
422 
423         assertThat(cancelled).isTrue()
424         assertThat(underTest.loadingDetails).isNull()
425     }
426 
427     @Test
displayView_loadingThenHideView_animationStoppednull428     fun displayView_loadingThenHideView_animationStopped() {
429         underTest.displayView(
430             createChipbarInfo(
431                 Icon.Resource(R.id.check_box, null),
432                 Text.Loaded("text"),
433                 endItem = ChipbarEndItem.Loading,
434             )
435         )
436 
437         val animator = underTest.loadingDetails!!.animator
438         var cancelled = false
439         animator.doOnCancel { cancelled = true }
440 
441         underTest.removeView(DEVICE_ID, "TestReason")
442 
443         assertThat(cancelled).isTrue()
444         assertThat(underTest.loadingDetails).isNull()
445     }
446 
447     @Test
displayView_loadingThenNewLoading_animationStaysTheSamenull448     fun displayView_loadingThenNewLoading_animationStaysTheSame() {
449         underTest.displayView(
450             createChipbarInfo(
451                 Icon.Resource(R.id.check_box, null),
452                 Text.Loaded("text"),
453                 endItem = ChipbarEndItem.Loading,
454             )
455         )
456 
457         val animator = underTest.loadingDetails!!.animator
458         var cancelled = false
459         animator.doOnCancel { cancelled = true }
460 
461         underTest.displayView(
462             createChipbarInfo(
463                 Icon.Resource(R.id.check_box, null),
464                 Text.Loaded("new text"),
465                 endItem = ChipbarEndItem.Loading,
466             )
467         )
468 
469         assertThat(underTest.loadingDetails!!.animator).isEqualTo(animator)
470         assertThat(underTest.loadingDetails!!.animator.isStarted).isTrue()
471         assertThat(cancelled).isFalse()
472     }
473 
474     @Test
displayView_vibrationEffect_doubleClickEffectWithHardwareFeedbacknull475     fun displayView_vibrationEffect_doubleClickEffectWithHardwareFeedback() {
476         underTest.displayView(
477             createChipbarInfo(
478                 Icon.Resource(R.id.check_box, null),
479                 Text.Loaded("text"),
480                 endItem = null,
481                 vibrationEffect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK),
482             )
483         )
484 
485         verify(vibratorHelper)
486             .vibrate(
487                 any(),
488                 any(),
489                 eq(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)),
490                 any(),
491                 eq(VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK)),
492             )
493     }
494 
495     /** Regression test for b/266119467. */
496     @Test
displayView_animationFailure_viewsStillBecomeVisiblenull497     fun displayView_animationFailure_viewsStillBecomeVisible() {
498         chipbarAnimator.allowAnimation = false
499 
500         underTest.displayView(
501             createChipbarInfo(
502                 Icon.Resource(R.id.check_box, null),
503                 Text.Loaded("text"),
504                 endItem = ChipbarEndItem.Loading,
505             )
506         )
507 
508         val view = getChipbarView()
509         assertThat(view.getInnerView().alpha).isEqualTo(1f)
510         assertThat(view.getStartIconView().alpha).isEqualTo(1f)
511         assertThat(view.getLoadingIcon().alpha).isEqualTo(1f)
512         assertThat(view.getChipTextView().alpha).isEqualTo(1f)
513     }
514 
515     @Test
updateView_viewUpdatednull516     fun updateView_viewUpdated() {
517         // First, display a view
518         val drawable = context.getDrawable(R.drawable.ic_celebration)!!
519 
520         underTest.displayView(
521             createChipbarInfo(
522                 Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")),
523                 Text.Loaded("title text"),
524                 endItem = ChipbarEndItem.Loading,
525             )
526         )
527 
528         val chipbarView = getChipbarView()
529         assertThat(chipbarView.getStartIconView().drawable).isEqualTo(drawable)
530         assertThat(chipbarView.getStartIconView().contentDescription).isEqualTo("loadedCD")
531         assertThat(chipbarView.getChipText()).isEqualTo("title text")
532         assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE)
533         assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
534         assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE)
535 
536         // WHEN the view is updated
537         val newDrawable = context.getDrawable(R.drawable.ic_cake)!!
538         underTest.updateView(
539             createChipbarInfo(
540                 Icon.Loaded(newDrawable, ContentDescription.Loaded("new CD")),
541                 Text.Loaded("new title text"),
542                 endItem = ChipbarEndItem.Error,
543             ),
544             chipbarView
545         )
546 
547         // THEN we display the new view
548         assertThat(chipbarView.getStartIconView().drawable).isEqualTo(newDrawable)
549         assertThat(chipbarView.getStartIconView().contentDescription).isEqualTo("new CD")
550         assertThat(chipbarView.getChipText()).isEqualTo("new title text")
551         assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
552         assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE)
553         assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE)
554     }
555 
556     @Test
viewUpdates_loggednull557     fun viewUpdates_logged() {
558         val drawable = context.getDrawable(R.drawable.ic_celebration)!!
559         underTest.displayView(
560             createChipbarInfo(
561                 Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")),
562                 Text.Loaded("title text"),
563                 endItem = ChipbarEndItem.Loading,
564             )
565         )
566 
567         verify(logger).logViewUpdate(eq(WINDOW_TITLE), eq("title text"), any())
568 
569         underTest.displayView(
570             createChipbarInfo(
571                 Icon.Loaded(drawable, ContentDescription.Loaded("new CD")),
572                 Text.Loaded("new title text"),
573                 endItem = ChipbarEndItem.Error,
574             )
575         )
576 
577         verify(logger).logViewUpdate(eq(WINDOW_TITLE), eq("new title text"), any())
578     }
579 
580     /** Regression test for b/266209420. */
581     @Test
displayViewThenImmediateRemoval_viewStillRemovednull582     fun displayViewThenImmediateRemoval_viewStillRemoved() {
583         underTest.displayView(
584             createChipbarInfo(
585                 Icon.Resource(R.drawable.ic_cake, contentDescription = null),
586                 Text.Loaded("title text"),
587                 endItem = ChipbarEndItem.Error,
588             ),
589         )
590         val chipbarView = getChipbarView()
591 
592         underTest.removeView(DEVICE_ID, "test reason")
593 
594         verify(windowManager).removeView(chipbarView)
595     }
596 
597     /** Regression test for b/266209420. */
598     @Test
removeView_animationFailure_viewStillRemovednull599     fun removeView_animationFailure_viewStillRemoved() {
600         chipbarAnimator.allowAnimation = false
601 
602         underTest.displayView(
603             createChipbarInfo(
604                 Icon.Resource(R.drawable.ic_cake, contentDescription = null),
605                 Text.Loaded("title text"),
606                 endItem = ChipbarEndItem.Error,
607             ),
608         )
609         val chipbarView = getChipbarView()
610 
611         underTest.removeView(DEVICE_ID, "test reason")
612 
613         verify(windowManager).removeView(chipbarView)
614     }
615 
616     @Test
swipeToDismiss_false_neverListensForGesturenull617     fun swipeToDismiss_false_neverListensForGesture() {
618         underTest.displayView(
619             createChipbarInfo(
620                 Icon.Resource(R.drawable.ic_cake, contentDescription = null),
621                 Text.Loaded("title text"),
622                 endItem = ChipbarEndItem.Loading,
623                 allowSwipeToDismiss = false,
624             )
625         )
626 
627         verify(swipeGestureHandler, never()).addOnGestureDetectedCallback(any(), any())
628     }
629 
630     @Test
swipeToDismiss_true_listensForGesturenull631     fun swipeToDismiss_true_listensForGesture() {
632         underTest.displayView(
633             createChipbarInfo(
634                 Icon.Resource(R.drawable.ic_cake, contentDescription = null),
635                 Text.Loaded("title text"),
636                 endItem = ChipbarEndItem.Loading,
637                 allowSwipeToDismiss = true,
638             )
639         )
640 
641         verify(swipeGestureHandler).addOnGestureDetectedCallback(any(), any())
642     }
643 
644     @Test
swipeToDismiss_swipeOccurs_viewDismissed_manuallyDismissedLoggednull645     fun swipeToDismiss_swipeOccurs_viewDismissed_manuallyDismissedLogged() {
646         underTest.displayView(
647             createChipbarInfo(
648                 Icon.Resource(R.drawable.ic_cake, contentDescription = null),
649                 Text.Loaded("title text"),
650                 endItem = ChipbarEndItem.Loading,
651                 allowSwipeToDismiss = true,
652             )
653         )
654         val view = getChipbarView()
655 
656         val callbackCaptor = argumentCaptor<(MotionEvent) -> Unit>()
657         verify(swipeGestureHandler).addOnGestureDetectedCallback(any(), capture(callbackCaptor))
658 
659         callbackCaptor.value.invoke(MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0))
660 
661         verify(windowManager).removeView(view)
662         assertThat(uiEventLoggerFake.numLogs()).isEqualTo(2)
663         assertThat(uiEventLoggerFake.eventId(1))
664             .isEqualTo(TemporaryViewUiEvent.TEMPORARY_VIEW_MANUALLY_DISMISSED.id)
665     }
666 
667     @Test
swipeToDismiss_viewUpdatedToFalse_swipeOccurs_viewNotDismissednull668     fun swipeToDismiss_viewUpdatedToFalse_swipeOccurs_viewNotDismissed() {
669         underTest.displayView(
670             createChipbarInfo(
671                 Icon.Resource(R.drawable.ic_cake, contentDescription = null),
672                 Text.Loaded("title text"),
673                 endItem = ChipbarEndItem.Loading,
674                 allowSwipeToDismiss = true,
675             )
676         )
677         val view = getChipbarView()
678         val callbackCaptor = argumentCaptor<(MotionEvent) -> Unit>()
679         verify(swipeGestureHandler).addOnGestureDetectedCallback(any(), capture(callbackCaptor))
680 
681         // only one log for view addition
682         assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
683         assertThat(uiEventLoggerFake.eventId(0))
684             .isEqualTo(TemporaryViewUiEvent.TEMPORARY_VIEW_ADDED.id)
685 
686         // WHEN the view is updated to not allow swipe-to-dismiss
687         underTest.displayView(
688             createChipbarInfo(
689                 Icon.Resource(R.drawable.ic_cake, contentDescription = null),
690                 Text.Loaded("title text"),
691                 endItem = ChipbarEndItem.Loading,
692                 allowSwipeToDismiss = false,
693             )
694         )
695 
696         // THEN the callback is removed
697         verify(swipeGestureHandler).removeOnGestureDetectedCallback(any())
698 
699         // And WHEN the old callback is invoked
700         callbackCaptor.value.invoke(MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0))
701 
702         // THEN it is ignored and view isn't removed
703         verify(windowManager, never()).removeView(view)
704         assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
705     }
706 
createChipbarInfonull707     private fun createChipbarInfo(
708         startIcon: Icon,
709         text: Text,
710         endItem: ChipbarEndItem?,
711         vibrationEffect: VibrationEffect? = null,
712         allowSwipeToDismiss: Boolean = false,
713     ): ChipbarInfo {
714         return ChipbarInfo(
715             TintedIcon(startIcon, tint = null),
716             text,
717             endItem,
718             vibrationEffect,
719             allowSwipeToDismiss,
720             windowTitle = WINDOW_TITLE,
721             wakeReason = WAKE_REASON,
722             timeoutMs = TIMEOUT,
723             id = DEVICE_ID,
724             priority = ViewPriority.NORMAL,
725             instanceId = InstanceId.fakeInstanceId(0),
726         )
727     }
728 
getInnerViewnull729     private fun ViewGroup.getInnerView() = this.requireViewById<ViewGroup>(R.id.chipbar_inner)
730 
731     private fun ViewGroup.getStartIconView() = this.requireViewById<ImageView>(R.id.start_icon)
732 
733     private fun ViewGroup.getChipTextView() = this.requireViewById<TextView>(R.id.text)
734 
735     private fun ViewGroup.getChipText(): String = this.getChipTextView().text as String
736 
737     private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading)
738 
739     private fun ViewGroup.getEndButton(): TextView = this.requireViewById(R.id.end_button)
740 
741     private fun ViewGroup.getErrorIcon(): View = this.requireViewById(R.id.error)
742 
743     private fun getChipbarView(): ViewGroup {
744         val viewCaptor = ArgumentCaptor.forClass(View::class.java)
745         verify(windowManager).addView(viewCaptor.capture(), any())
746         return viewCaptor.value as ViewGroup
747     }
748 
749     /** Test class that lets us disallow animations. */
750     inner class TestChipbarAnimator : ChipbarAnimator() {
751         var allowAnimation: Boolean = true
752 
animateViewInnull753         override fun animateViewIn(innerView: ViewGroup, onAnimationEnd: Runnable): Boolean {
754             if (!allowAnimation) {
755                 return false
756             }
757             return super.animateViewIn(innerView, onAnimationEnd)
758         }
759 
animateViewOutnull760         override fun animateViewOut(innerView: ViewGroup, onAnimationEnd: Runnable): Boolean {
761             if (!allowAnimation) {
762                 return false
763             }
764             return super.animateViewOut(innerView, onAnimationEnd)
765         }
766     }
767 }
768 
769 private const val TIMEOUT = 10000
770 private const val WINDOW_TITLE = "Test Chipbar Window Title"
771 private const val WAKE_REASON = "TEST_CHIPBAR_WAKE_REASON"
772 private const val DEVICE_ID = "id"
773