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