• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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.wm.shell.bubbles
18 
19 import android.content.Context
20 import android.content.Intent
21 import android.content.pm.ShortcutInfo
22 import android.content.res.Resources
23 import android.graphics.Color
24 import android.graphics.drawable.Icon
25 import android.os.UserHandle
26 import android.platform.test.annotations.DisableFlags
27 import android.platform.test.annotations.EnableFlags
28 import android.platform.test.flag.junit.SetFlagsRule
29 import android.view.WindowManager
30 import androidx.test.core.app.ApplicationProvider
31 import androidx.test.ext.junit.runners.AndroidJUnit4
32 import androidx.test.filters.SmallTest
33 import androidx.test.platform.app.InstrumentationRegistry
34 import com.android.internal.logging.testing.UiEventLoggerFake
35 import com.android.internal.protolog.ProtoLog
36 import com.android.launcher3.icons.BubbleIconFactory
37 import com.android.wm.shell.Flags
38 import com.android.wm.shell.R
39 import com.android.wm.shell.bubbles.BubbleStackView.SurfaceSynchronizer
40 import com.android.wm.shell.bubbles.Bubbles.BubbleExpandListener
41 import com.android.wm.shell.bubbles.Bubbles.SysuiProxy
42 import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix
43 import com.android.wm.shell.common.FloatingContentCoordinator
44 import com.android.wm.shell.common.TestShellExecutor
45 import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
46 import com.google.common.truth.Truth.assertThat
47 import com.google.common.util.concurrent.MoreExecutors.directExecutor
48 import org.junit.After
49 import org.junit.Before
50 import org.junit.Rule
51 import org.junit.Test
52 import org.junit.runner.RunWith
53 import org.mockito.kotlin.any
54 import org.mockito.kotlin.mock
55 import org.mockito.kotlin.never
56 import org.mockito.kotlin.spy
57 import org.mockito.kotlin.verify
58 import java.util.concurrent.Semaphore
59 import java.util.concurrent.TimeUnit
60 import java.util.function.Consumer
61 
62 /** Unit tests for [BubbleStackView]. */
63 @SmallTest
64 @RunWith(AndroidJUnit4::class)
65 class BubbleStackViewTest {
66 
67     @get:Rule val setFlagsRule = SetFlagsRule()
68 
69     private val context = ApplicationProvider.getApplicationContext<Context>()
70     private lateinit var positioner: BubblePositioner
71     private lateinit var bubbleLogger: BubbleLogger
72     private lateinit var iconFactory: BubbleIconFactory
73     private lateinit var expandedViewManager: FakeBubbleExpandedViewManager
74     private lateinit var bubbleStackView: BubbleStackView
75     private lateinit var shellExecutor: TestShellExecutor
76     private lateinit var windowManager: WindowManager
77     private lateinit var bubbleTaskViewFactory: BubbleTaskViewFactory
78     private lateinit var bubbleData: BubbleData
79     private lateinit var bubbleStackViewManager: FakeBubbleStackViewManager
80     private lateinit var surfaceSynchronizer: FakeSurfaceSynchronizer
81     private var sysuiProxy = mock<SysuiProxy>()
82 
83     @Before
84     fun setUp() {
85         PhysicsAnimatorTestUtils.prepareForTest()
86         // Disable protolog tool when running the tests from studio
87         ProtoLog.REQUIRE_PROTOLOGTOOL = false
88         shellExecutor = TestShellExecutor()
89         windowManager = context.getSystemService(WindowManager::class.java)
90         iconFactory =
91             BubbleIconFactory(
92                 context,
93                 context.resources.getDimensionPixelSize(R.dimen.bubble_size),
94                 context.resources.getDimensionPixelSize(R.dimen.bubble_badge_size),
95                 Color.BLACK,
96                 context.resources.getDimensionPixelSize(
97                     com.android.internal.R.dimen.importance_ring_stroke_width
98                 )
99             )
100         positioner = BubblePositioner(context, windowManager)
101         bubbleLogger = BubbleLogger(UiEventLoggerFake())
102         bubbleData =
103             BubbleData(
104                 context,
105                 bubbleLogger,
106                 positioner,
107                 BubbleEducationController(context),
108                 shellExecutor,
109                 shellExecutor
110             )
111         bubbleStackViewManager = FakeBubbleStackViewManager()
112         expandedViewManager = FakeBubbleExpandedViewManager()
113         bubbleTaskViewFactory = FakeBubbleTaskViewFactory(context, shellExecutor)
114         surfaceSynchronizer = FakeSurfaceSynchronizer()
115         bubbleStackView =
116             BubbleStackView(
117                 context,
118                 bubbleStackViewManager,
119                 positioner,
120                 bubbleData,
121                 surfaceSynchronizer,
122                 FloatingContentCoordinator(),
123                 { sysuiProxy },
124                 shellExecutor
125             )
126 
127         context
128             .getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
129             .edit()
130             .putBoolean(StackEducationView.PREF_STACK_EDUCATION, true)
131             .apply()
132     }
133 
134     @After
135     fun tearDown() {
136         PhysicsAnimatorTestUtils.tearDown()
137     }
138 
139     @Test
140     fun addBubble() {
141         val bubble = createAndInflateBubble()
142         InstrumentationRegistry.getInstrumentation().runOnMainSync {
143             bubbleStackView.addBubble(bubble)
144         }
145         InstrumentationRegistry.getInstrumentation().waitForIdleSync()
146         assertThat(bubbleStackView.bubbleCount).isEqualTo(1)
147     }
148 
149     @Test
150     fun tapBubbleToExpand() {
151         val bubble = createAndInflateBubble()
152 
153         InstrumentationRegistry.getInstrumentation().runOnMainSync {
154             bubbleStackView.addBubble(bubble)
155         }
156 
157         InstrumentationRegistry.getInstrumentation().waitForIdleSync()
158         assertThat(bubbleStackView.bubbleCount).isEqualTo(1)
159         var lastUpdate: BubbleData.Update? = null
160         val semaphore = Semaphore(0)
161         val listener =
162             BubbleData.Listener { update ->
163                 lastUpdate = update
164                 semaphore.release()
165             }
166         bubbleData.setListener(listener)
167 
168         InstrumentationRegistry.getInstrumentation().runOnMainSync {
169             bubble.iconView!!.performClick()
170             // we're checking the expanded state in BubbleData because that's the source of truth.
171             // This will eventually propagate an update back to the stack view, but setting the
172             // entire pipeline is outside the scope of a unit test.
173             assertThat(bubbleData.isExpanded).isTrue()
174             shellExecutor.flushAll()
175         }
176 
177         assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
178         assertThat(lastUpdate).isNotNull()
179         assertThat(lastUpdate!!.expandedChanged).isTrue()
180         assertThat(lastUpdate!!.expanded).isTrue()
181     }
182 
183     @Test
184     fun expandStack_imeHidden() {
185         val bubble = createAndInflateBubble()
186 
187         InstrumentationRegistry.getInstrumentation().runOnMainSync {
188             bubbleStackView.addBubble(bubble)
189         }
190 
191         InstrumentationRegistry.getInstrumentation().waitForIdleSync()
192         assertThat(bubbleStackView.bubbleCount).isEqualTo(1)
193 
194         positioner.setImeVisible(false, 0)
195 
196         InstrumentationRegistry.getInstrumentation().runOnMainSync {
197             // simulate a request from the bubble data listener to expand the stack
198             bubbleStackView.isExpanded = true
199             verify(sysuiProxy).onStackExpandChanged(true)
200             shellExecutor.flushAll()
201         }
202 
203         assertThat(bubbleStackViewManager.onImeHidden).isNull()
204     }
205 
206     @Test
207     fun collapseStack_imeHidden() {
208         val bubble = createAndInflateBubble()
209 
210         InstrumentationRegistry.getInstrumentation().runOnMainSync {
211             bubbleStackView.addBubble(bubble)
212         }
213 
214         InstrumentationRegistry.getInstrumentation().waitForIdleSync()
215         assertThat(bubbleStackView.bubbleCount).isEqualTo(1)
216 
217         positioner.setImeVisible(false, 0)
218 
219         InstrumentationRegistry.getInstrumentation().runOnMainSync {
220             // simulate a request from the bubble data listener to expand the stack
221             bubbleStackView.isExpanded = true
222             verify(sysuiProxy).onStackExpandChanged(true)
223             shellExecutor.flushAll()
224         }
225 
226         assertThat(bubbleStackViewManager.onImeHidden).isNull()
227 
228         InstrumentationRegistry.getInstrumentation().runOnMainSync {
229             // simulate a request from the bubble data listener to collapse the stack
230             bubbleStackView.isExpanded = false
231             verify(sysuiProxy).onStackExpandChanged(false)
232             shellExecutor.flushAll()
233         }
234 
235         assertThat(bubbleStackViewManager.onImeHidden).isNull()
236     }
237 
238     @Test
239     fun expandStack_waitsForIme() {
240         val bubble = createAndInflateBubble()
241 
242         InstrumentationRegistry.getInstrumentation().runOnMainSync {
243             bubbleStackView.addBubble(bubble)
244         }
245 
246         InstrumentationRegistry.getInstrumentation().waitForIdleSync()
247         assertThat(bubbleStackView.bubbleCount).isEqualTo(1)
248 
249         positioner.setImeVisible(true, 100)
250 
251         InstrumentationRegistry.getInstrumentation().runOnMainSync {
252             // simulate a request from the bubble data listener to expand the stack
253             bubbleStackView.isExpanded = true
254         }
255 
256         val onImeHidden = bubbleStackViewManager.onImeHidden
257         assertThat(onImeHidden).isNotNull()
258         verify(sysuiProxy, never()).onStackExpandChanged(any())
259         positioner.setImeVisible(false, 0)
260         InstrumentationRegistry.getInstrumentation().runOnMainSync {
261             onImeHidden!!.run()
262             verify(sysuiProxy).onStackExpandChanged(true)
263             shellExecutor.flushAll()
264         }
265     }
266 
267     @Test
268     fun collapseStack_waitsForIme() {
269         val bubble = createAndInflateBubble()
270 
271         InstrumentationRegistry.getInstrumentation().runOnMainSync {
272             bubbleStackView.addBubble(bubble)
273         }
274 
275         InstrumentationRegistry.getInstrumentation().waitForIdleSync()
276         assertThat(bubbleStackView.bubbleCount).isEqualTo(1)
277 
278         positioner.setImeVisible(true, 100)
279 
280         InstrumentationRegistry.getInstrumentation().runOnMainSync {
281             // simulate a request from the bubble data listener to expand the stack
282             bubbleStackView.isExpanded = true
283         }
284 
285         var onImeHidden = bubbleStackViewManager.onImeHidden
286         assertThat(onImeHidden).isNotNull()
287         verify(sysuiProxy, never()).onStackExpandChanged(any())
288         positioner.setImeVisible(false, 0)
289         InstrumentationRegistry.getInstrumentation().runOnMainSync {
290             onImeHidden!!.run()
291             verify(sysuiProxy).onStackExpandChanged(true)
292             shellExecutor.flushAll()
293         }
294 
295         bubbleStackViewManager.onImeHidden = null
296         positioner.setImeVisible(true, 100)
297 
298         InstrumentationRegistry.getInstrumentation().runOnMainSync {
299             // simulate a request from the bubble data listener to collapse the stack
300             bubbleStackView.isExpanded = false
301         }
302 
303         onImeHidden = bubbleStackViewManager.onImeHidden
304         assertThat(onImeHidden).isNotNull()
305         verify(sysuiProxy, never()).onStackExpandChanged(false)
306         positioner.setImeVisible(false, 0)
307         InstrumentationRegistry.getInstrumentation().runOnMainSync {
308             onImeHidden!!.run()
309             verify(sysuiProxy).onStackExpandChanged(false)
310             shellExecutor.flushAll()
311         }
312     }
313 
314     @Test
315     fun tapDifferentBubble_shouldReorder() {
316         surfaceSynchronizer.isActive = false
317         val bubble1 = createAndInflateChatBubble(key = "bubble1")
318         val bubble2 = createAndInflateChatBubble(key = "bubble2")
319         InstrumentationRegistry.getInstrumentation().runOnMainSync {
320             bubbleStackView.addBubble(bubble1)
321             bubbleStackView.addBubble(bubble2)
322         }
323         InstrumentationRegistry.getInstrumentation().waitForIdleSync()
324 
325         assertThat(bubbleStackView.bubbleCount).isEqualTo(2)
326         assertThat(bubbleData.bubbles).hasSize(2)
327         assertThat(bubbleData.selectedBubble).isEqualTo(bubble2)
328         assertThat(bubble2.iconView).isNotNull()
329 
330         var lastUpdate: BubbleData.Update? = null
331         val semaphore = Semaphore(0)
332         val listener =
333             BubbleData.Listener { update ->
334                 lastUpdate = update
335                 semaphore.release()
336             }
337         bubbleData.setListener(listener)
338 
339         InstrumentationRegistry.getInstrumentation().runOnMainSync {
340             bubble2.iconView!!.performClick()
341             assertThat(bubbleData.isExpanded).isTrue()
342 
343             bubbleStackView.setSelectedBubble(bubble2)
344             bubbleStackView.isExpanded = true
345             shellExecutor.flushAll()
346         }
347 
348         assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
349         assertThat(lastUpdate!!.expanded).isTrue()
350         assertThat(lastUpdate!!.bubbles.map { it.key })
351             .containsExactly("bubble2", "bubble1")
352             .inOrder()
353 
354         // wait for idle to allow the animation to start
355         InstrumentationRegistry.getInstrumentation().waitForIdleSync()
356         // wait for the expansion animation to complete before interacting with the bubbles
357         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(
358                 AnimatableScaleMatrix.SCALE_X, AnimatableScaleMatrix.SCALE_Y)
359 
360         // tap on bubble1 to select it
361         InstrumentationRegistry.getInstrumentation().runOnMainSync {
362             bubble1.iconView!!.performClick()
363             shellExecutor.flushAll()
364         }
365         assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
366         assertThat(bubbleData.selectedBubble).isEqualTo(bubble1)
367 
368         // tap on bubble1 again to collapse the stack
369         InstrumentationRegistry.getInstrumentation().runOnMainSync {
370             // we have to set the selected bubble in the stack view manually because we don't have a
371             // listener wired up.
372             bubbleStackView.setSelectedBubble(bubble1)
373             bubble1.iconView!!.performClick()
374             shellExecutor.flushAll()
375         }
376 
377         assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
378         assertThat(bubbleData.selectedBubble).isEqualTo(bubble1)
379         assertThat(bubbleData.isExpanded).isFalse()
380         assertThat(lastUpdate!!.orderChanged).isTrue()
381         assertThat(lastUpdate!!.bubbles.map { it.key })
382             .containsExactly("bubble1", "bubble2")
383             .inOrder()
384     }
385 
386     @Test
387     fun tapDifferentBubble_imeVisible_shouldWaitForIme() {
388         val bubble1 = createAndInflateChatBubble(key = "bubble1")
389         val bubble2 = createAndInflateChatBubble(key = "bubble2")
390         InstrumentationRegistry.getInstrumentation().runOnMainSync {
391             bubbleStackView.addBubble(bubble1)
392             bubbleStackView.addBubble(bubble2)
393         }
394         InstrumentationRegistry.getInstrumentation().waitForIdleSync()
395 
396         assertThat(bubbleStackView.bubbleCount).isEqualTo(2)
397         assertThat(bubbleData.bubbles).hasSize(2)
398         assertThat(bubbleData.selectedBubble).isEqualTo(bubble2)
399         assertThat(bubble2.iconView).isNotNull()
400 
401         val expandListener = FakeBubbleExpandListener()
402         bubbleStackView.setExpandListener(expandListener)
403 
404         var lastUpdate: BubbleData.Update? = null
405         val semaphore = Semaphore(0)
406         val listener =
407             BubbleData.Listener { update ->
408                 lastUpdate = update
409                 semaphore.release()
410             }
411         bubbleData.setListener(listener)
412 
413         InstrumentationRegistry.getInstrumentation().runOnMainSync {
414             bubble2.iconView!!.performClick()
415             assertThat(bubbleData.isExpanded).isTrue()
416 
417             bubbleStackView.setSelectedBubble(bubble2)
418             bubbleStackView.isExpanded = true
419             shellExecutor.flushAll()
420         }
421 
422         assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
423         assertThat(lastUpdate!!.expanded).isTrue()
424         assertThat(lastUpdate!!.bubbles.map { it.key })
425             .containsExactly("bubble2", "bubble1")
426             .inOrder()
427 
428         // wait for idle to allow the animation to start
429         InstrumentationRegistry.getInstrumentation().waitForIdleSync()
430         // wait for the expansion animation to complete before interacting with the bubbles
431         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(
432             AnimatableScaleMatrix.SCALE_X, AnimatableScaleMatrix.SCALE_Y)
433 
434         // make the IME visible and tap on bubble1 to select it
435         InstrumentationRegistry.getInstrumentation().runOnMainSync {
436             positioner.setImeVisible(true, 100)
437             bubble1.iconView!!.performClick()
438             // we have to set the selected bubble in the stack view manually because we don't have a
439             // listener wired up.
440             bubbleStackView.setSelectedBubble(bubble1)
441             shellExecutor.flushAll()
442         }
443 
444         val onImeHidden = bubbleStackViewManager.onImeHidden
445         assertThat(onImeHidden).isNotNull()
446 
447         assertThat(expandListener.bubblesExpandedState).isEqualTo(mapOf("bubble2" to true))
448 
449         InstrumentationRegistry.getInstrumentation().runOnMainSync {
450             onImeHidden!!.run()
451             shellExecutor.flushAll()
452         }
453 
454         assertThat(expandListener.bubblesExpandedState)
455             .isEqualTo(mapOf("bubble1" to true, "bubble2" to false))
456         assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
457         assertThat(bubbleData.selectedBubble).isEqualTo(bubble1)
458     }
459 
460     @Test
461     fun tapDifferentBubble_imeHidden_updatesImmediately() {
462         val bubble1 = createAndInflateChatBubble(key = "bubble1")
463         val bubble2 = createAndInflateChatBubble(key = "bubble2")
464         InstrumentationRegistry.getInstrumentation().runOnMainSync {
465             bubbleStackView.addBubble(bubble1)
466             bubbleStackView.addBubble(bubble2)
467         }
468         InstrumentationRegistry.getInstrumentation().waitForIdleSync()
469 
470         assertThat(bubbleStackView.bubbleCount).isEqualTo(2)
471         assertThat(bubbleData.bubbles).hasSize(2)
472         assertThat(bubbleData.selectedBubble).isEqualTo(bubble2)
473         assertThat(bubble2.iconView).isNotNull()
474 
475         val expandListener = FakeBubbleExpandListener()
476         bubbleStackView.setExpandListener(expandListener)
477 
478         var lastUpdate: BubbleData.Update? = null
479         val semaphore = Semaphore(0)
480         val listener =
481             BubbleData.Listener { update ->
482                 lastUpdate = update
483                 semaphore.release()
484             }
485         bubbleData.setListener(listener)
486 
487         InstrumentationRegistry.getInstrumentation().runOnMainSync {
488             bubble2.iconView!!.performClick()
489             assertThat(bubbleData.isExpanded).isTrue()
490 
491             bubbleStackView.setSelectedBubble(bubble2)
492             bubbleStackView.isExpanded = true
493             shellExecutor.flushAll()
494         }
495 
496         assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
497         assertThat(lastUpdate!!.expanded).isTrue()
498         assertThat(lastUpdate!!.bubbles.map { it.key })
499             .containsExactly("bubble2", "bubble1")
500             .inOrder()
501 
502         // wait for idle to allow the animation to start
503         InstrumentationRegistry.getInstrumentation().waitForIdleSync()
504         // wait for the expansion animation to complete before interacting with the bubbles
505         PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(
506             AnimatableScaleMatrix.SCALE_X, AnimatableScaleMatrix.SCALE_Y)
507 
508         // make the IME hidden and tap on bubble1 to select it
509         InstrumentationRegistry.getInstrumentation().runOnMainSync {
510             positioner.setImeVisible(false, 0)
511             bubble1.iconView!!.performClick()
512             // we have to set the selected bubble in the stack view manually because we don't have a
513             // listener wired up.
514             bubbleStackView.setSelectedBubble(bubble1)
515             shellExecutor.flushAll()
516         }
517 
518         val onImeHidden = bubbleStackViewManager.onImeHidden
519         assertThat(onImeHidden).isNull()
520 
521         assertThat(expandListener.bubblesExpandedState)
522             .isEqualTo(mapOf("bubble1" to true, "bubble2" to false))
523         assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
524         assertThat(bubbleData.selectedBubble).isEqualTo(bubble1)
525     }
526 
527     @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
528     @Test
529     fun testCreateStackView_noOverflowContents_noOverflow() {
530         bubbleStackView =
531                 BubbleStackView(
532                         context,
533                         bubbleStackViewManager,
534                         positioner,
535                         bubbleData,
536                         null,
537                         FloatingContentCoordinator(),
538                         { sysuiProxy },
539                         shellExecutor
540                 )
541 
542         assertThat(bubbleData.overflowBubbles).isEmpty()
543         val bubbleOverflow = bubbleData.overflow
544         // Overflow shouldn't be attached
545         assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isEqualTo(-1)
546     }
547 
548     @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
549     @Test
550     fun testCreateStackView_hasOverflowContents_hasOverflow() {
551         // Add a bubble to the overflow
552         val bubble1 = createAndInflateChatBubble(key = "bubble1")
553         bubbleData.notificationEntryUpdated(bubble1, false, false)
554         bubbleData.dismissBubbleWithKey(bubble1.key, Bubbles.DISMISS_USER_GESTURE)
555         assertThat(bubbleData.overflowBubbles).isNotEmpty()
556 
557         bubbleStackView =
558                 BubbleStackView(
559                         context,
560                         bubbleStackViewManager,
561                         positioner,
562                         bubbleData,
563                         null,
564                         FloatingContentCoordinator(),
565                         { sysuiProxy },
566                         shellExecutor
567                 )
568         val bubbleOverflow = bubbleData.overflow
569         assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1)
570     }
571 
572     @DisableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
573     @Test
574     fun testCreateStackView_noOverflowContents_hasOverflow() {
575         bubbleStackView =
576                 BubbleStackView(
577                         context,
578                         bubbleStackViewManager,
579                         positioner,
580                         bubbleData,
581                         null,
582                         FloatingContentCoordinator(),
583                         { sysuiProxy },
584                         shellExecutor
585                 )
586 
587         assertThat(bubbleData.overflowBubbles).isEmpty()
588         val bubbleOverflow = bubbleData.overflow
589         assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1)
590     }
591 
592     @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
593     @Test
594     fun showOverflow_true() {
595         InstrumentationRegistry.getInstrumentation().runOnMainSync {
596             bubbleStackView.showOverflow(true)
597         }
598         InstrumentationRegistry.getInstrumentation().waitForIdleSync()
599 
600         val bubbleOverflow = bubbleData.overflow
601         assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1)
602     }
603 
604     @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
605     @Test
606     fun showOverflow_false() {
607         InstrumentationRegistry.getInstrumentation().runOnMainSync {
608             bubbleStackView.showOverflow(true)
609         }
610         InstrumentationRegistry.getInstrumentation().waitForIdleSync()
611         val bubbleOverflow = bubbleData.overflow
612         assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1)
613 
614         InstrumentationRegistry.getInstrumentation().runOnMainSync {
615             bubbleStackView.showOverflow(false)
616         }
617         InstrumentationRegistry.getInstrumentation().waitForIdleSync()
618 
619         // The overflow should've been removed
620         assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isEqualTo(-1)
621     }
622 
623     @DisableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
624     @Test
625     fun showOverflow_ignored() {
626         InstrumentationRegistry.getInstrumentation().runOnMainSync {
627             bubbleStackView.showOverflow(false)
628         }
629         InstrumentationRegistry.getInstrumentation().waitForIdleSync()
630 
631         // showOverflow should've been ignored, so the overflow would be attached
632         val bubbleOverflow = bubbleData.overflow
633         assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1)
634     }
635 
636     @Test
637     fun removeFromWindow_stopMonitoringSwipeUpGesture() {
638         bubbleStackView = spy(bubbleStackView)
639         InstrumentationRegistry.getInstrumentation().runOnMainSync {
640             // No way to add to window in the test environment right now so just pretend
641             bubbleStackView.onDetachedFromWindow()
642         }
643         verify(bubbleStackView).stopMonitoringSwipeUpGesture()
644     }
645 
646     private fun createAndInflateChatBubble(key: String): Bubble {
647         val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button)
648         val shortcutInfo = ShortcutInfo.Builder(context, "fakeId").setIcon(icon).build()
649         val bubble =
650             Bubble(
651                 key,
652                 shortcutInfo,
653                 /* desiredHeight= */ 6,
654                 Resources.ID_NULL,
655                 "title",
656                 /* taskId= */ 0,
657                 "locus",
658                 /* isDismissable= */ true,
659                 directExecutor(),
660                 directExecutor()
661             ) {}
662         inflateBubble(bubble)
663         return bubble
664     }
665 
666     private fun createAndInflateBubble(): Bubble {
667         val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
668         val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button)
669         val bubble =
670             Bubble.createAppBubble(intent, UserHandle(1), icon, directExecutor(), directExecutor())
671         inflateBubble(bubble)
672         return bubble
673     }
674 
675     private fun inflateBubble(bubble: Bubble) {
676         bubble.setInflateSynchronously(true)
677         bubbleData.notificationEntryUpdated(bubble, true, false)
678 
679         val semaphore = Semaphore(0)
680         val callback: BubbleViewInfoTask.Callback =
681             BubbleViewInfoTask.Callback { semaphore.release() }
682         bubble.inflate(
683             callback,
684             context,
685             expandedViewManager,
686             bubbleTaskViewFactory,
687             positioner,
688             bubbleStackView,
689             null,
690             iconFactory,
691             false
692         )
693 
694         assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
695         assertThat(bubble.isInflated).isTrue()
696     }
697 
698     private class FakeBubbleStackViewManager : BubbleStackViewManager {
699         var onImeHidden: Runnable? = null
700 
701         override fun onAllBubblesAnimatedOut() {}
702 
703         override fun updateWindowFlagsForBackpress(interceptBack: Boolean) {}
704 
705         override fun checkNotificationPanelExpandedState(callback: Consumer<Boolean>) {}
706 
707         override fun hideCurrentInputMethod(onImeHidden: Runnable?) {
708             this.onImeHidden = onImeHidden
709         }
710     }
711 
712     private class FakeBubbleExpandListener : BubbleExpandListener {
713         val bubblesExpandedState = mutableMapOf<String, Boolean>()
714         override fun onBubbleExpandChanged(isExpanding: Boolean, key: String) {
715             bubblesExpandedState[key] = isExpanding
716         }
717     }
718 
719     private class FakeSurfaceSynchronizer : SurfaceSynchronizer {
720         var isActive = true
721         override fun syncSurfaceAndRun(callback: Runnable) {
722             if (isActive) callback.run()
723         }
724     }
725 }
726