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

Commit 95171feb authored by Liran Binyamin's avatar Liran Binyamin Committed by Android (Google) Code Review
Browse files

Merge "Add a unit test for switching bubbles" into main

parents dd7f5629 37bb9671
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) {