Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 37bb9671 authored by Liran Binyamin's avatar Liran Binyamin
Browse files

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
parent 10274506
Loading
Loading
Loading
Loading
+141 −12
Original line number Diff line number Diff line
@@ -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()
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            bubbleStackView.addBubble(bubble)
        }
        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
        assertThat(bubbleStackView.bubbleCount).isEqualTo(1)
    }

    @UiThreadTest
    @Test
    fun tapBubbleToExpand() {
        val bubble = createAndInflateBubble()

        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.
            // 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
    }
+6 −8
Original line number Diff line number Diff line
@@ -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>
+3 −8
Original line number Diff line number Diff line
@@ -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) {