Loading libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt +141 −12 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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()!! Loading Loading @@ -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) Loading @@ -152,7 +282,6 @@ class BubbleStackViewTest { assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() assertThat(bubble.isInflated).isTrue() return bubble } private class FakeBubbleStackViewManager : BubbleStackViewManager { Loading @@ -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 } Loading libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt +6 −8 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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 Loading @@ -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 Loading Loading @@ -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> Loading libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimatorTestUtils.kt +3 −8 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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. Loading Loading @@ -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) { Loading Loading
libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt +141 −12 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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()!! Loading Loading @@ -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) Loading @@ -152,7 +282,6 @@ class BubbleStackViewTest { assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() assertThat(bubble.isInflated).isTrue() return bubble } private class FakeBubbleStackViewManager : BubbleStackViewManager { Loading @@ -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 } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt +6 −8 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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 Loading @@ -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 Loading Loading @@ -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> Loading
libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimatorTestUtils.kt +3 −8 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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. Loading Loading @@ -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) { Loading