diff options
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) { |