diff options
| author | 2024-03-12 15:13:21 -0400 | |
|---|---|---|
| committer | 2024-03-14 09:54:09 -0400 | |
| commit | 37bb96710774de831b719a641f64418535f1e1f1 (patch) | |
| tree | e5de221a2f975a33e1363375e526ff95b4402552 | |
| parent | 102745066f84c8e62c79dbfa26b5305ee9ae1c57 (diff) | |
Add a unit test for switching bubbles
Adds a unit test to BubbleStackViewTest to verify that bubbles are ordered correctly after selecting a different bubble.
This change also moves BubbleStackViewTest away from @UiThreadTest because otherwise we can't wait until animations complete without also blocking the test thread.
Additionally, update PhysicsAnimatorTestUtils to add a callback to be notified when an animator is created rather than overriding the factory.
When multiple tests call prepareForTest, they each override the factory, adding another layer to it and we end up creating a deeper and deeper stack.
Bug: 323310200
Test: atest WMShellMultivalentTestsOnDevice:BubbleStackViewTest
Change-Id: I5371c2c68b82f92e7f2d824f40e75ec668505216
3 files changed, 150 insertions, 28 deletions
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt index 8989fc543044..f3d70f7c160b 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt @@ -18,27 +18,32 @@ package com.android.wm.shell.bubbles import android.content.Context import android.content.Intent +import android.content.pm.ShortcutInfo +import android.content.res.Resources import android.graphics.Color import android.graphics.drawable.Icon import android.os.UserHandle import android.view.IWindowManager import android.view.WindowManager import android.view.WindowManagerGlobal -import androidx.test.annotation.UiThreadTest import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry import com.android.internal.logging.testing.UiEventLoggerFake import com.android.internal.protolog.common.ProtoLog import com.android.launcher3.icons.BubbleIconFactory import com.android.wm.shell.R +import com.android.wm.shell.animation.PhysicsAnimatorTestUtils import com.android.wm.shell.bubbles.Bubbles.SysuiProxy +import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix import com.android.wm.shell.common.FloatingContentCoordinator import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.taskview.TaskView import com.android.wm.shell.taskview.TaskViewTaskController import com.google.common.truth.Truth.assertThat import com.google.common.util.concurrent.MoreExecutors.directExecutor +import org.junit.After import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit import java.util.function.Consumer @@ -64,6 +69,7 @@ class BubbleStackViewTest { @Before fun setUp() { + PhysicsAnimatorTestUtils.prepareForTest() // Disable protolog tool when running the tests from studio ProtoLog.REQUIRE_PROTOLOGTOOL = false windowManager = WindowManagerGlobal.getWindowManagerService()!! @@ -104,34 +110,158 @@ class BubbleStackViewTest { { sysuiProxy }, shellExecutor ) + + context + .getSharedPreferences(context.packageName, Context.MODE_PRIVATE) + .edit() + .putBoolean(StackEducationView.PREF_STACK_EDUCATION, true) + .apply() + } + + @After + fun tearDown() { + PhysicsAnimatorTestUtils.tearDown() } - @UiThreadTest @Test fun addBubble() { val bubble = createAndInflateBubble() - bubbleStackView.addBubble(bubble) + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.addBubble(bubble) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() assertThat(bubbleStackView.bubbleCount).isEqualTo(1) } - @UiThreadTest @Test fun tapBubbleToExpand() { val bubble = createAndInflateBubble() - bubbleStackView.addBubble(bubble) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.addBubble(bubble) + } + + InstrumentationRegistry.getInstrumentation().waitForIdleSync() assertThat(bubbleStackView.bubbleCount).isEqualTo(1) + var lastUpdate: BubbleData.Update? = null + val semaphore = Semaphore(0) + val listener = + BubbleData.Listener { update -> + lastUpdate = update + semaphore.release() + } + bubbleData.setListener(listener) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubble.iconView!!.performClick() + // we're checking the expanded state in BubbleData because that's the source of truth. + // This will eventually propagate an update back to the stack view, but setting the + // entire pipeline is outside the scope of a unit test. + assertThat(bubbleData.isExpanded).isTrue() + } - bubble.iconView!!.performClick() - // we're checking the expanded state in BubbleData because that's the source of truth. This - // will eventually propagate an update back to the stack view, but setting the entire - // pipeline is outside the scope of a unit test. - assertThat(bubbleData.isExpanded).isTrue() + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(lastUpdate).isNotNull() + assertThat(lastUpdate!!.expandedChanged).isTrue() + assertThat(lastUpdate!!.expanded).isTrue() + } + + @Test + fun tapDifferentBubble_shouldReorder() { + val bubble1 = createAndInflateChatBubble(key = "bubble1") + val bubble2 = createAndInflateChatBubble(key = "bubble2") + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleStackView.addBubble(bubble1) + bubbleStackView.addBubble(bubble2) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + + assertThat(bubbleStackView.bubbleCount).isEqualTo(2) + assertThat(bubbleData.bubbles).hasSize(2) + assertThat(bubbleData.selectedBubble).isEqualTo(bubble2) + assertThat(bubble2.iconView).isNotNull() + + var lastUpdate: BubbleData.Update? = null + val semaphore = Semaphore(0) + val listener = + BubbleData.Listener { update -> + lastUpdate = update + semaphore.release() + } + bubbleData.setListener(listener) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubble2.iconView!!.performClick() + assertThat(bubbleData.isExpanded).isTrue() + + bubbleStackView.setSelectedBubble(bubble2) + bubbleStackView.isExpanded = true + } + + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(lastUpdate!!.expanded).isTrue() + assertThat(lastUpdate!!.bubbles.map { it.key }) + .containsExactly("bubble2", "bubble1") + .inOrder() + + // wait for idle to allow the animation to start + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + // wait for the expansion animation to complete before interacting with the bubbles + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd( + AnimatableScaleMatrix.SCALE_X, AnimatableScaleMatrix.SCALE_Y) + + // tap on bubble1 to select it + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubble1.iconView!!.performClick() + } + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(bubbleData.selectedBubble).isEqualTo(bubble1) + + // tap on bubble1 again to collapse the stack + InstrumentationRegistry.getInstrumentation().runOnMainSync { + // we have to set the selected bubble in the stack view manually because we don't have a + // listener wired up. + bubbleStackView.setSelectedBubble(bubble1) + bubble1.iconView!!.performClick() + } + + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + assertThat(bubbleData.selectedBubble).isEqualTo(bubble1) + assertThat(bubbleData.isExpanded).isFalse() + assertThat(lastUpdate!!.orderChanged).isTrue() + assertThat(lastUpdate!!.bubbles.map { it.key }) + .containsExactly("bubble1", "bubble2") + .inOrder() + } + + private fun createAndInflateChatBubble(key: String): Bubble { + val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button) + val shortcutInfo = ShortcutInfo.Builder(context, "fakeId").setIcon(icon).build() + val bubble = + Bubble( + key, + shortcutInfo, + /* desiredHeight= */ 6, + Resources.ID_NULL, + "title", + /* taskId= */ 0, + "locus", + /* isDismissable= */ true, + directExecutor() + ) {} + inflateBubble(bubble) + return bubble } private fun createAndInflateBubble(): Bubble { val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName) val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button) val bubble = Bubble.createAppBubble(intent, UserHandle(1), icon, directExecutor()) + inflateBubble(bubble) + return bubble + } + + private fun inflateBubble(bubble: Bubble) { bubble.setInflateSynchronously(true) bubbleData.notificationEntryUpdated(bubble, true, false) @@ -152,7 +282,6 @@ class BubbleStackViewTest { assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() assertThat(bubble.isInflated).isTrue() - return bubble } private class FakeBubbleStackViewManager : BubbleStackViewManager { @@ -176,7 +305,7 @@ class BubbleStackViewTest { r.run() } - override fun removeCallbacks(r: Runnable) {} + override fun removeCallbacks(r: Runnable?) {} override fun hasCallback(r: Runnable): Boolean = false } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt index ee8c41417458..b7f0890ec2bd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt @@ -505,7 +505,6 @@ class PhysicsAnimator<T> private constructor (target: T) { // Check for a spring configuration. If one is present, we're either springing, or // flinging-then-springing. if (springConfig != null) { - // If there is no corresponding fling config, we're only springing. if (flingConfig == null) { // Apply the configuration and start the animation. @@ -679,7 +678,6 @@ class PhysicsAnimator<T> private constructor (target: T) { value: Float, velocity: Float ) { - // If this property animation isn't relevant to this listener, ignore it. if (!properties.contains(property)) { return @@ -702,7 +700,6 @@ class PhysicsAnimator<T> private constructor (target: T) { finalVelocity: Float, isFling: Boolean ): Boolean { - // If this property animation isn't relevant to this listener, ignore it. if (!properties.contains(property)) { return false @@ -971,17 +968,18 @@ class PhysicsAnimator<T> private constructor (target: T) { companion object { /** - * Constructor to use to for new physics animator instances in [getInstance]. This is - * typically the default constructor, but [PhysicsAnimatorTestUtils] can change it so that - * all code using the physics animator is given testable instances instead. + * Callback to notify that a new animator was created. Used in [PhysicsAnimatorTestUtils] + * to be able to keep track of animators and wait for them to finish. */ - internal var instanceConstructor: (Any) -> PhysicsAnimator<*> = ::PhysicsAnimator + internal var onAnimatorCreated: (PhysicsAnimator<*>, Any) -> Unit = { _, _ -> } @JvmStatic @Suppress("UNCHECKED_CAST") fun <T : Any> getInstance(target: T): PhysicsAnimator<T> { if (!animators.containsKey(target)) { - animators[target] = instanceConstructor(target) + val animator = PhysicsAnimator(target) + onAnimatorCreated(animator, target) + animators[target] = animator } return animators[target] as PhysicsAnimator<T> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimatorTestUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimatorTestUtils.kt index 86eb8da952f1..7defc26eef35 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimatorTestUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimatorTestUtils.kt @@ -62,12 +62,9 @@ object PhysicsAnimatorTestUtils { */ @JvmStatic fun prepareForTest() { - val defaultConstructor = PhysicsAnimator.instanceConstructor - PhysicsAnimator.instanceConstructor = fun(target: Any): PhysicsAnimator<*> { - val animator = defaultConstructor(target) + PhysicsAnimator.onAnimatorCreated = { animator, target -> allAnimatedObjects.add(target) animatorTestHelpers[animator] = AnimatorTestHelper(animator) - return animator } timeoutMs = 2000 @@ -158,12 +155,12 @@ object PhysicsAnimatorTestUtils { @Throws(InterruptedException::class) @Suppress("UNCHECKED_CAST") fun <T : Any> blockUntilAnimationsEnd( - properties: FloatPropertyCompat<in T> + vararg properties: FloatPropertyCompat<in T> ) { for (target in allAnimatedObjects) { try { blockUntilAnimationsEnd( - PhysicsAnimator.getInstance(target) as PhysicsAnimator<T>, properties) + PhysicsAnimator.getInstance(target) as PhysicsAnimator<T>, *properties) } catch (e: ClassCastException) { // Keep checking the other objects for ones whose types match the provided // properties. @@ -267,10 +264,8 @@ object PhysicsAnimatorTestUtils { // Loop through the updates from the testable animator. for (update in framesForProperty) { - // Check whether this frame satisfies the current matcher. if (curMatcher(update)) { - // If that was the last unsatisfied matcher, we're good here. 'Verify' all remaining // frames and return without failing. if (matchers.size == 0) { |