/* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3.taskbar.bubbles.animation import android.content.Context import android.graphics.Color import android.graphics.PointF import android.graphics.Rect import android.graphics.drawable.ColorDrawable import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE import android.widget.FrameLayout import android.widget.TextView import androidx.core.animation.AnimatorTestRule import androidx.core.graphics.drawable.toBitmap import androidx.dynamicanimation.animation.DynamicAnimation 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.launcher3.R import com.android.launcher3.icons.BitmapInfo import com.android.launcher3.taskbar.TaskbarInsetsController import com.android.launcher3.taskbar.bubbles.BubbleBarBubble import com.android.launcher3.taskbar.bubbles.BubbleBarOverflow import com.android.launcher3.taskbar.bubbles.BubbleBarParentViewHeightUpdateNotifier import com.android.launcher3.taskbar.bubbles.BubbleBarView import com.android.launcher3.taskbar.bubbles.BubbleBarViewController import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController import com.android.launcher3.taskbar.bubbles.BubbleView import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutMessage import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutPositioner import com.android.launcher3.taskbar.bubbles.flyout.FlyoutCallbacks import com.android.launcher3.taskbar.bubbles.flyout.FlyoutScheduler import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.BubbleLauncherState import com.android.wm.shell.shared.animation.PhysicsAnimator import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils import com.android.wm.shell.shared.bubbles.BubbleBarLocation import com.android.wm.shell.shared.bubbles.BubbleInfo import com.google.common.truth.Truth.assertThat import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class BubbleBarViewAnimatorTest { @get:Rule val animatorTestRule = AnimatorTestRule() private val context = ApplicationProvider.getApplicationContext() private lateinit var animatorScheduler: TestBubbleBarViewAnimatorScheduler private lateinit var bubbleBarParentViewController: TestBubbleBarParentViewHeightUpdateNotifier private lateinit var overflowView: BubbleView private lateinit var bubbleView: BubbleView private lateinit var bubble: BubbleBarBubble private lateinit var bubbleBarView: BubbleBarView private lateinit var flyoutContainer: FrameLayout private lateinit var bubbleStashController: FakeBubbleStashController private lateinit var flyoutController: BubbleBarFlyoutController private val emptyRunnable = Runnable {} private val flyoutView: View? get() = flyoutContainer.findViewById(R.id.bubble_bar_flyout_view) private var animationEnded = false private val onAnimationEnded = { animationEnded = true } @Before fun setUp() { animatorScheduler = TestBubbleBarViewAnimatorScheduler() bubbleBarParentViewController = TestBubbleBarParentViewHeightUpdateNotifier() bubbleStashController = FakeBubbleStashController() PhysicsAnimatorTestUtils.prepareForTest() setupFlyoutController() } @Test fun animateBubbleInForStashed() { setUpBubbleBar() val handle = View(context) val handleAnimator = PhysicsAnimator.getInstance(handle) bubbleStashController.handleAnimator = handleAnimator val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = emptyRunnable, onBubbleBarVisible = emptyRunnable, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateBubbleInForStashed(bubble, isExpanding = false) } // let the animation start and wait for it to complete InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) assertThat(handle.alpha).isEqualTo(0) assertThat(handle.translationY) .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) assertThat(bubbleBarView.scaleX).isEqualTo(1) assertThat(bubbleBarView.scaleY).isEqualTo(1) assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) assertThat(animator.isAnimating).isTrue() assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) waitForFlyoutToShow() // execute the hide bubble animation assertThat(animatorScheduler.delayedBlock).isNotNull() InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) waitForFlyoutToHide() // let the animation start and wait for it to complete InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) assertThat(handle.alpha).isEqualTo(1) assertThat(handle.translationY).isEqualTo(0) assertThat(bubbleBarView.alpha).isEqualTo(0) assertThat(animator.isAnimating).isFalse() assertThat(bubbleStashController.isStashed).isTrue() assertThat(animationEnded).isTrue() } @Test fun animateBubbleInForStashed_tapAnimatingBubble() { setUpBubbleBar() val handle = View(context) val handleAnimator = PhysicsAnimator.getInstance(handle) bubbleStashController.handleAnimator = handleAnimator val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = emptyRunnable, onBubbleBarVisible = emptyRunnable, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateBubbleInForStashed(bubble, isExpanding = false) } // let the animation start and wait for it to complete InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) assertThat(handle.alpha).isEqualTo(0) assertThat(handle.translationY) .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) assertThat(bubbleBarView.scaleX).isEqualTo(1) assertThat(bubbleBarView.scaleY).isEqualTo(1) assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) assertThat(animator.isAnimating).isTrue() assertThat(bubbleStashController.taskbarTouchRegionUpdated).isTrue() assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) waitForFlyoutToShow() // verify the hide bubble animation is pending assertThat(animatorScheduler.delayedBlock).isNotNull() InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.interruptForTouch() } waitForFlyoutToHide() assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) assertThat(animatorScheduler.delayedBlock).isNull() assertThat(bubbleBarView.alpha).isEqualTo(1) assertThat(bubbleBarView.scaleX).isEqualTo(1) assertThat(bubbleBarView.scaleY).isEqualTo(1) assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) assertThat(animator.isAnimating).isFalse() assertThat(animationEnded).isTrue() } @Test fun animateBubbleInForStashed_touchTaskbarArea_whileShowing() { setUpBubbleBar() val handle = View(context) val handleAnimator = PhysicsAnimator.getInstance(handle) bubbleStashController.handleAnimator = handleAnimator val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = emptyRunnable, onBubbleBarVisible = emptyRunnable, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateBubbleInForStashed(bubble, isExpanding = false) } // wait for the animation to start InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true } handleAnimator.assertIsRunning() assertThat(animator.isAnimating).isTrue() // verify the hide bubble animation is pending assertThat(animatorScheduler.delayedBlock).isNotNull() InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.onStashStateChangingWhileAnimating() } // wait for the animation to cancel PhysicsAnimatorTestUtils.blockUntilAnimationsEnd( handleAnimator, DynamicAnimation.TRANSLATION_Y, ) // verify that the hide animation was canceled assertThat(animatorScheduler.delayedBlock).isNull() assertThat(animator.isAnimating).isFalse() assertThat(bubbleStashController.animationInterrupted).isTrue() assertThat(bubbleBarView.scaleX).isEqualTo(1) assertThat(bubbleBarView.scaleY).isEqualTo(1) assertThat(bubbleStashController.isStashed).isTrue() // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait // again InstrumentationRegistry.getInstrumentation().waitForIdleSync() handleAnimator.assertIsNotRunning() assertThat(animationEnded).isTrue() } @Test fun animateBubbleInForStashed_touchTaskbarArea_whileHiding() { setUpBubbleBar() val handle = View(context) val handleAnimator = PhysicsAnimator.getInstance(handle) bubbleStashController.handleAnimator = handleAnimator val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = emptyRunnable, onBubbleBarVisible = emptyRunnable, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateBubbleInForStashed(bubble, isExpanding = false) } // let the animation start and wait for it to complete InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) waitForFlyoutToShow() // execute the hide bubble animation assertThat(animatorScheduler.delayedBlock).isNotNull() InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) waitForFlyoutToHide() // wait for the hide animation to start InstrumentationRegistry.getInstrumentation().runOnMainSync {} handleAnimator.assertIsRunning() InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.onStashStateChangingWhileAnimating() } assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) assertThat(animator.isAnimating).isFalse() assertThat(bubbleStashController.animationInterrupted).isTrue() // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait // again InstrumentationRegistry.getInstrumentation().waitForIdleSync() handleAnimator.assertIsNotRunning() assertThat(animationEnded).isTrue() } @Test fun animateBubbleInForStashed_autoExpanding() { setUpBubbleBar() val handle = View(context) val handleAnimator = PhysicsAnimator.getInstance(handle) bubbleStashController.handleAnimator = handleAnimator var notifiedExpanded = false val onExpanded = Runnable { notifiedExpanded = true } val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = onExpanded, onBubbleBarVisible = emptyRunnable, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateBubbleInForStashed(bubble, isExpanding = true) } // wait for the animation to start InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) assertThat(handle.alpha).isEqualTo(0) assertThat(handle.translationY) .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) assertThat(bubbleBarView.scaleX).isEqualTo(1) assertThat(bubbleBarView.scaleY).isEqualTo(1) assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) assertThat(animator.isAnimating).isFalse() assertThat(bubbleBarView.isExpanded).isTrue() // verify there is no hide animation assertThat(animatorScheduler.delayedBlock).isNull() assertThat(bubbleStashController.isStashed).isFalse() assertThat(notifiedExpanded).isTrue() assertThat(animationEnded).isTrue() } @Test fun animateBubbleInForStashed_expandedWhileAnimatingIn() { setUpBubbleBar() val handle = View(context) val handleAnimator = PhysicsAnimator.getInstance(handle) bubbleStashController.handleAnimator = handleAnimator var notifiedExpanded = false val onExpanded = Runnable { notifiedExpanded = true } val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = onExpanded, onBubbleBarVisible = emptyRunnable, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateBubbleInForStashed(bubble, isExpanding = false) } // wait for the animation to start InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true } handleAnimator.assertIsRunning() assertThat(animator.isAnimating).isTrue() // verify the hide bubble animation is pending assertThat(animatorScheduler.delayedBlock).isNotNull() InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.expandedWhileAnimating() } // let the animation finish PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) // verify that the hide animation was canceled assertThat(animatorScheduler.delayedBlock).isNull() assertThat(handle.alpha).isEqualTo(0) assertThat(handle.translationY) .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_TASKBAR) assertThat(animator.isAnimating).isFalse() assertThat(notifiedExpanded).isTrue() assertThat(animationEnded).isTrue() } @Test fun animateBubbleInForStashed_expandedWhileFullyIn() { setUpBubbleBar() val handle = View(context) val handleAnimator = PhysicsAnimator.getInstance(handle) bubbleStashController.handleAnimator = handleAnimator var notifiedExpanded = false val onExpanded = Runnable { notifiedExpanded = true } val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = onExpanded, onBubbleBarVisible = emptyRunnable, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateBubbleInForStashed(bubble, isExpanding = false) } // wait for the animation to start InstrumentationRegistry.getInstrumentation().runOnMainSync {} // wait for the animation to end PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) assertThat(animator.isAnimating).isTrue() assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) waitForFlyoutToShow() // verify the hide bubble animation is pending assertThat(animatorScheduler.delayedBlock).isNotNull() InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.expandedWhileAnimating() } // verify that the hide animation was canceled assertThat(animatorScheduler.delayedBlock).isNull() waitForFlyoutToHide() assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) assertThat(handle.alpha).isEqualTo(0) assertThat(handle.translationY) .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_TASKBAR) assertThat(animator.isAnimating).isFalse() assertThat(notifiedExpanded).isTrue() assertThat(animationEnded).isTrue() } @Test fun animateToInitialState_inApp() { setUpBubbleBar() bubbleStashController.launcherState = BubbleLauncherState.IN_APP val handle = View(context) val handleAnimator = PhysicsAnimator.getInstance(handle) bubbleStashController.handleAnimator = handleAnimator val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) var notifiedBubbleBarVisible = false val onBubbleBarVisible = Runnable { notifiedBubbleBarVisible = true } val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = emptyRunnable, onBubbleBarVisible = onBubbleBarVisible, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { bubbleBarView.visibility = INVISIBLE animator.animateToInitialState(bubble, isInApp = true, isExpanding = false) } InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) barAnimator.assertIsNotRunning() assertThat(animator.isAnimating).isTrue() assertThat(bubbleBarView.alpha).isEqualTo(1) assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) waitForFlyoutToShow() assertThat(animatorScheduler.delayedBlock).isNotNull() InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) waitForFlyoutToHide() InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) InstrumentationRegistry.getInstrumentation().waitForIdleSync() assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) assertThat(animator.isAnimating).isFalse() assertThat(bubbleBarView.alpha).isEqualTo(0) assertThat(handle.translationY).isEqualTo(0) assertThat(handle.alpha).isEqualTo(1) assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) assertThat(notifiedBubbleBarVisible).isTrue() assertThat(bubbleStashController.isStashed).isTrue() assertThat(animationEnded).isTrue() } @Test fun animateToInitialState_whileDragging_inApp() { setUpBubbleBar() bubbleStashController.launcherState = BubbleLauncherState.IN_APP val handle = View(context) val handleAnimator = PhysicsAnimator.getInstance(handle) bubbleStashController.handleAnimator = handleAnimator val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) var notifiedBubbleBarVisible = false val onBubbleBarVisible = Runnable { notifiedBubbleBarVisible = true } val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = emptyRunnable, onBubbleBarVisible = onBubbleBarVisible, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { bubbleBarView.visibility = INVISIBLE animator.animateToInitialState( bubble, isInApp = true, isExpanding = false, isDragging = true, ) } InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) barAnimator.assertIsNotRunning() assertThat(animator.isAnimating).isTrue() assertThat(bubbleBarView.alpha).isEqualTo(1) assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) waitForFlyoutToShow() assertThat(animatorScheduler.delayedBlock).isNotNull() InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) waitForFlyoutToHide() InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) InstrumentationRegistry.getInstrumentation().waitForIdleSync() assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) assertThat(animator.isAnimating).isFalse() assertThat(bubbleBarView.alpha).isEqualTo(1) assertThat(handle.translationY).isEqualTo(0) assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) assertThat(notifiedBubbleBarVisible).isTrue() assertThat(animationEnded).isTrue() } @Test fun animateToInitialState_inApp_autoExpanding() { setUpBubbleBar() bubbleStashController.launcherState = BubbleLauncherState.IN_APP val handle = View(context) val handleAnimator = PhysicsAnimator.getInstance(handle) bubbleStashController.handleAnimator = handleAnimator val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) var notifiedExpanded = false val onExpanded = Runnable { notifiedExpanded = true } val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = onExpanded, onBubbleBarVisible = emptyRunnable, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateToInitialState(bubble, isInApp = true, isExpanding = true) } InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) barAnimator.assertIsNotRunning() assertThat(animator.isAnimating).isFalse() assertThat(bubbleBarView.alpha).isEqualTo(1) assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) assertThat(animatorScheduler.delayedBlock).isNull() assertThat(bubbleStashController.isStashed).isFalse() assertThat(notifiedExpanded).isTrue() assertThat(animationEnded).isTrue() } @Test fun animateToInitialState_inHome() { setUpBubbleBar() bubbleStashController.launcherState = BubbleLauncherState.HOME val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = emptyRunnable, onBubbleBarVisible = emptyRunnable, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateToInitialState(bubble, isInApp = false, isExpanding = false) } InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) barAnimator.assertIsNotRunning() assertThat(animator.isAnimating).isTrue() assertThat(bubbleBarView.alpha).isEqualTo(1) assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) waitForFlyoutToShow() assertThat(animatorScheduler.delayedBlock).isNotNull() InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) waitForFlyoutToHide() assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) assertThat(animator.isAnimating).isFalse() assertThat(bubbleBarView.alpha).isEqualTo(1) assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) assertThat(bubbleStashController.isStashed).isFalse() assertThat(animationEnded).isTrue() } @Test fun animateToInitialState_expandedWhileAnimatingIn() { setUpBubbleBar() bubbleStashController.launcherState = BubbleLauncherState.HOME var notifiedExpanded = false val onExpanded = Runnable { notifiedExpanded = true } val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = onExpanded, onBubbleBarVisible = emptyRunnable, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateToInitialState(bubble, isInApp = false, isExpanding = false) } val bubbleBarAnimator = PhysicsAnimator.getInstance(bubbleBarView) // wait for the animation to start InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(bubbleBarAnimator) { true } bubbleBarAnimator.assertIsRunning() assertThat(animator.isAnimating).isTrue() // verify the hide bubble animation is pending assertThat(animatorScheduler.delayedBlock).isNotNull() InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.expandedWhileAnimating() } // let the animation finish PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) // verify that the hide animation was canceled assertThat(animatorScheduler.delayedBlock).isNull() verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_HOTSEAT) assertThat(animator.isAnimating).isFalse() assertThat(bubbleStashController.isStashed).isFalse() assertThat(notifiedExpanded).isTrue() assertThat(animationEnded).isTrue() } @Test fun animateToInitialState_expandedWhileFullyIn() { setUpBubbleBar() bubbleStashController.launcherState = BubbleLauncherState.HOME var notifiedExpanded = false val onExpanded = Runnable { notifiedExpanded = true } val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = onExpanded, onBubbleBarVisible = emptyRunnable, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateToInitialState(bubble, isInApp = false, isExpanding = false) } // wait for the animation to start InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) waitForFlyoutToShow() assertThat(animator.isAnimating).isTrue() // verify the hide bubble animation is pending assertThat(animatorScheduler.delayedBlock).isNotNull() InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.expandedWhileAnimating() } waitForFlyoutToHide() // verify that the hide animation was canceled assertThat(animatorScheduler.delayedBlock).isNull() assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_HOTSEAT) assertThat(animator.isAnimating).isFalse() assertThat(notifiedExpanded).isTrue() assertThat(animationEnded).isTrue() } @Test fun animateBubbleBarForCollapsed() { setUpBubbleBar() bubbleStashController.launcherState = BubbleLauncherState.HOME val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = emptyRunnable, onBubbleBarVisible = emptyRunnable, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateBubbleBarForCollapsed(bubble, isExpanding = false) } InstrumentationRegistry.getInstrumentation().runOnMainSync {} // verify we started animating assertThat(animator.isAnimating).isTrue() // advance the animation handler by the duration of the initial lift InstrumentationRegistry.getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(250) } // the lift animation is complete; the spring back animation should start now InstrumentationRegistry.getInstrumentation().runOnMainSync {} barAnimator.assertIsRunning() PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) waitForFlyoutToShow() assertThat(animatorScheduler.delayedBlock).isNotNull() InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) waitForFlyoutToHide() assertThat(animator.isAnimating).isFalse() assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) // the bubble bar translation y should be back to its initial value assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) assertThat(bubbleStashController.isStashed).isFalse() assertThat(animationEnded).isTrue() } @Test fun animateBubbleBarForCollapsed_autoExpanding() { setUpBubbleBar() bubbleStashController.launcherState = BubbleLauncherState.HOME val semaphore = Semaphore(0) var notifiedExpanded = false val onExpanded = Runnable { notifiedExpanded = true semaphore.release() } val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = onExpanded, onBubbleBarVisible = emptyRunnable, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateBubbleBarForCollapsed(bubble, isExpanding = true) } InstrumentationRegistry.getInstrumentation().runOnMainSync {} // verify we started animating assertThat(animator.isAnimating).isTrue() // advance the animation handler by the duration of the initial lift InstrumentationRegistry.getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(250) } // the lift animation is complete; the spring back animation should start now InstrumentationRegistry.getInstrumentation().runOnMainSync {} InstrumentationRegistry.getInstrumentation().waitForIdleSync() assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() // we should be expanded now assertThat(bubbleBarView.isExpanded).isTrue() PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) // verify there is no hide animation assertThat(animatorScheduler.delayedBlock).isNull() assertThat(animator.isAnimating).isFalse() assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) assertThat(bubbleStashController.isStashed).isFalse() assertThat(notifiedExpanded).isTrue() assertThat(animationEnded).isTrue() } @Test fun animateBubbleBarForCollapsed_expandingWhileAnimatingIn() { setUpBubbleBar() bubbleStashController.launcherState = BubbleLauncherState.HOME val semaphore = Semaphore(0) var notifiedExpanded = false val onExpanded = Runnable { notifiedExpanded = true semaphore.release() } val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = onExpanded, onBubbleBarVisible = emptyRunnable, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateBubbleBarForCollapsed(bubble, isExpanding = false) } InstrumentationRegistry.getInstrumentation().runOnMainSync {} // verify we started animating assertThat(animator.isAnimating).isTrue() // advance the animation handler by the duration of the initial lift InstrumentationRegistry.getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(100) } // verify there is a pending hide animation assertThat(animatorScheduler.delayedBlock).isNotNull() assertThat(animator.isAnimating).isTrue() // send the expand signal in the middle of the lift animation InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.expandedWhileAnimating() } // let the lift animation complete InstrumentationRegistry.getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(150) } // the lift animation is complete; the spring back animation should start now. wait for it // to complete InstrumentationRegistry.getInstrumentation().runOnMainSync {} assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) // verify that the hide animation was canceled assertThat(animatorScheduler.delayedBlock).isNull() assertThat(animator.isAnimating).isFalse() assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) assertThat(bubbleBarView.isExpanded).isTrue() assertThat(bubbleStashController.isStashed).isFalse() assertThat(notifiedExpanded).isTrue() assertThat(animationEnded).isTrue() } @Test fun animateBubbleBarForCollapsed_expandingWhileFullyIn() { setUpBubbleBar() bubbleStashController.launcherState = BubbleLauncherState.HOME val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) var notifiedExpanded = false val onExpanded = Runnable { notifiedExpanded = true } val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = onExpanded, onBubbleBarVisible = emptyRunnable, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateBubbleBarForCollapsed(bubble, isExpanding = false) } InstrumentationRegistry.getInstrumentation().runOnMainSync {} // verify we started animating assertThat(animator.isAnimating).isTrue() // advance the animation handler by the duration of the initial lift InstrumentationRegistry.getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(250) } // the lift animation is complete; the spring back animation should start now InstrumentationRegistry.getInstrumentation().runOnMainSync {} barAnimator.assertIsRunning() PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) // verify there is a pending hide animation assertThat(animatorScheduler.delayedBlock).isNotNull() assertThat(animator.isAnimating).isTrue() assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) waitForFlyoutToShow() InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.expandedWhileAnimating() } // verify that the hide animation was canceled assertThat(animatorScheduler.delayedBlock).isNull() waitForFlyoutToHide() assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) assertThat(animator.isAnimating).isFalse() assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) assertThat(bubbleBarView.isExpanded).isTrue() assertThat(bubbleStashController.isStashed).isFalse() assertThat(notifiedExpanded).isTrue() assertThat(animationEnded).isTrue() } @Test fun interruptAnimation_whileAnimatingIn() { setUpBubbleBar() val handle = View(context) val handleAnimator = PhysicsAnimator.getInstance(handle) bubbleStashController.handleAnimator = handleAnimator val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = emptyRunnable, onBubbleBarVisible = emptyRunnable, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateBubbleInForStashed(bubble, isExpanding = false) } // let the animation start and wait until the first frame InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true } handleAnimator.assertIsRunning() assertThat(animator.isAnimating).isTrue() val updatedBubble = bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message")) InstrumentationRegistry.getInstrumentation().runOnMainSync { bubbleView.setBubble(updatedBubble) animator.animateBubbleInForStashed(updatedBubble, isExpanding = false) } PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) assertThat(handle.alpha).isEqualTo(0) assertThat(handle.translationY) .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) assertThat(bubbleBarView.scaleX).isEqualTo(1) assertThat(bubbleBarView.scaleY).isEqualTo(1) assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) assertThat(animator.isAnimating).isTrue() assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) waitForFlyoutToShow() assertThat(flyoutView!!.findViewById(R.id.bubble_flyout_text).text) .isEqualTo("updated message") // run the hide animation assertThat(animatorScheduler.delayedBlock).isNotNull() InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) waitForFlyoutToHide() // let the animation start and wait for it to complete InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) assertThat(handle.alpha).isEqualTo(1) assertThat(handle.translationY).isEqualTo(0) assertThat(bubbleBarView.alpha).isEqualTo(0) assertThat(animator.isAnimating).isFalse() assertThat(bubbleStashController.isStashed).isTrue() assertThat(animationEnded).isTrue() } @Test fun interruptAnimation_whileIn() { setUpBubbleBar() val handle = View(context) val handleAnimator = PhysicsAnimator.getInstance(handle) bubbleStashController.handleAnimator = handleAnimator val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = emptyRunnable, onBubbleBarVisible = emptyRunnable, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateBubbleInForStashed(bubble, isExpanding = false) } // let the animation start and wait for it to complete InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) assertThat(handle.alpha).isEqualTo(0) assertThat(handle.translationY) .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) assertThat(bubbleBarView.scaleX).isEqualTo(1) assertThat(bubbleBarView.scaleY).isEqualTo(1) assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) assertThat(animator.isAnimating).isTrue() assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) waitForFlyoutToShow() assertThat(flyoutView!!.findViewById(R.id.bubble_flyout_text).text) .isEqualTo("message") // verify the hide animation is pending assertThat(animatorScheduler.delayedBlock).isNotNull() val updatedBubble = bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message")) InstrumentationRegistry.getInstrumentation().runOnMainSync { bubbleView.setBubble(updatedBubble) animator.animateBubbleInForStashed(updatedBubble, isExpanding = false) } // verify the hide animation was rescheduled assertThat(animatorScheduler.canceledBlock).isNotNull() assertThat(animatorScheduler.delayedBlock).isNotNull() waitForFlyoutToFadeOutAndBackIn() assertThat(flyoutView!!.findViewById(R.id.bubble_flyout_text).text) .isEqualTo("updated message") InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) waitForFlyoutToHide() // let the animation start and wait for it to complete InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) assertThat(handle.alpha).isEqualTo(1) assertThat(handle.translationY).isEqualTo(0) assertThat(bubbleBarView.alpha).isEqualTo(0) assertThat(animator.isAnimating).isFalse() assertThat(bubbleStashController.isStashed).isTrue() assertThat(animationEnded).isTrue() } @Test fun interruptAnimation_whileAnimatingOut_whileCollapsingFlyout() { setUpBubbleBar() val handle = View(context) val handleAnimator = PhysicsAnimator.getInstance(handle) bubbleStashController.handleAnimator = handleAnimator val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = emptyRunnable, onBubbleBarVisible = emptyRunnable, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateBubbleInForStashed(bubble, isExpanding = false) } // let the animation start and wait for it to complete InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) assertThat(handle.alpha).isEqualTo(0) assertThat(handle.translationY) .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) assertThat(bubbleBarView.scaleX).isEqualTo(1) assertThat(bubbleBarView.scaleY).isEqualTo(1) assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) assertThat(animator.isAnimating).isTrue() assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) waitForFlyoutToShow() assertThat(flyoutView!!.findViewById(R.id.bubble_flyout_text).text) .isEqualTo("message") // run the hide animation assertThat(animatorScheduler.delayedBlock).isNotNull() InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) // interrupt the animation while the flyout is collapsing val updatedBubble = bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message")) InstrumentationRegistry.getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(100) bubbleView.setBubble(updatedBubble) animator.animateBubbleInForStashed(updatedBubble, isExpanding = false) // the flyout should now reverse and expand animatorTestRule.advanceTimeBy(400) } assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) assertThat(flyoutView!!.findViewById(R.id.bubble_flyout_text).text) .isEqualTo("updated message") assertThat(handle.alpha).isEqualTo(0) assertThat(handle.translationY) .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) assertThat(bubbleBarView.scaleX).isEqualTo(1) assertThat(bubbleBarView.scaleY).isEqualTo(1) assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) // verify the hide animation was rescheduled and run it assertThat(animatorScheduler.delayedBlock).isNotNull() InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) waitForFlyoutToHide() // let the animation start and wait for it to complete InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(3) assertThat(handle.alpha).isEqualTo(1) assertThat(handle.translationY).isEqualTo(0) assertThat(bubbleBarView.alpha).isEqualTo(0) assertThat(animator.isAnimating).isFalse() assertThat(bubbleStashController.isStashed).isTrue() assertThat(animationEnded).isTrue() } @Test fun interruptAnimation_whileAnimatingOut_barToHandle() { setUpBubbleBar() val handle = View(context) val handleAnimator = PhysicsAnimator.getInstance(handle) bubbleStashController.handleAnimator = handleAnimator val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = emptyRunnable, onBubbleBarVisible = emptyRunnable, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateBubbleInForStashed(bubble, isExpanding = false) } // let the animation start and wait for it to complete InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) assertThat(handle.alpha).isEqualTo(0) assertThat(handle.translationY) .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) assertThat(bubbleBarView.scaleX).isEqualTo(1) assertThat(bubbleBarView.scaleY).isEqualTo(1) assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) assertThat(animator.isAnimating).isTrue() assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) waitForFlyoutToShow() assertThat(flyoutView!!.findViewById(R.id.bubble_flyout_text).text) .isEqualTo("message") // run the hide animation assertThat(animatorScheduler.delayedBlock).isNotNull() InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) waitForFlyoutToHide() // interrupt the animation while the bar is animating to the handle PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { bubbleBarView.alpha < 0.5 } // we're about to interrupt the animation which will cancel the current animation and start // a new one. pause the scheduler to delay starting the new animation. this allows us to run // the test deterministically animatorScheduler.pauseScheduler = true val updatedBubble = bubble.copy(flyoutMessage = bubble.flyoutMessage!!.copy(message = "updated message")) InstrumentationRegistry.getInstrumentation().runOnMainSync { bubbleView.setBubble(updatedBubble) animator.animateBubbleInForStashed(updatedBubble, isExpanding = false) } // since animation was interrupted there shouldn't be additional calls to adjust window assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) // verify there's a new job scheduled and start it. this is starting the animation from the // handle back to the bar assertThat(animatorScheduler.pausedBlock).isNotNull() animatorScheduler.pauseScheduler = false InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.pausedBlock!!) InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) waitForFlyoutToShow() assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) assertThat(flyoutView!!.findViewById(R.id.bubble_flyout_text).text) .isEqualTo("updated message") assertThat(handle.alpha).isEqualTo(0) assertThat(handle.translationY) .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR) assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) assertThat(bubbleBarView.scaleX).isEqualTo(1) assertThat(bubbleBarView.scaleY).isEqualTo(1) assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) // run the hide animation assertThat(animatorScheduler.delayedBlock).isNotNull() InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) waitForFlyoutToHide() PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) // verify the hide animation was rescheduled and run it assertThat(animatorScheduler.delayedBlock).isNotNull() InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) waitForFlyoutToHide() // let the animation start and wait for it to complete InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(3) assertThat(handle.alpha).isEqualTo(1) assertThat(handle.translationY).isEqualTo(0) assertThat(bubbleBarView.alpha).isEqualTo(0) assertThat(animator.isAnimating).isFalse() assertThat(bubbleStashController.isStashed).isTrue() assertThat(animationEnded).isTrue() } @Test fun interruptForIme() { setUpBubbleBar() val handle = View(context) val handleAnimator = PhysicsAnimator.getInstance(handle) bubbleStashController.handleAnimator = handleAnimator val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = emptyRunnable, onBubbleBarVisible = emptyRunnable, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateBubbleInForStashed(bubble, isExpanding = false) } // wait for the animation to start InstrumentationRegistry.getInstrumentation().runOnMainSync {} PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true } handleAnimator.assertIsRunning() assertThat(animator.isAnimating).isTrue() // verify the hide bubble animation is pending assertThat(animatorScheduler.delayedBlock).isNotNull() InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.interruptForIme() } // verify that the hide animation was canceled assertThat(animatorScheduler.delayedBlock).isNull() assertThat(animator.isAnimating).isFalse() assertThat(bubbleStashController.animationInterrupted).isTrue() assertThat(bubbleStashController.isStashed).isTrue() // PhysicsAnimatorTestUtils posts the cancellation to the main thread so we need to wait // again InstrumentationRegistry.getInstrumentation().waitForIdleSync() handleAnimator.assertIsNotRunning() assertThat(animationEnded).isTrue() } @Test fun collapseWhileAnimating_onHome() { setUpBubbleBar() bubbleStashController.launcherState = BubbleLauncherState.HOME val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) var bubbleBarExpanded = false val onExpanded = { bubbleBarExpanded = true } val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = onExpanded, onBubbleBarVisible = emptyRunnable, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateToInitialState(bubble, isInApp = false, isExpanding = true) } PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(barAnimator) { true } barAnimator.assertIsRunning() assertThat(animator.isAnimating).isTrue() // verify the hide bubble animation is pending assertThat(animatorScheduler.delayedBlock).isNotNull() InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.collapsedWhileAnimating() } barAnimator.assertIsNotRunning() assertThat(animator.isAnimating).isFalse() assertThat(animatorScheduler.delayedBlock).isNull() // wait for the bubble bar translation animation InstrumentationRegistry.getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(250) } assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT) assertThat(bubbleBarExpanded).isFalse() assertThat(bubbleBarView.isExpanded).isFalse() assertThat(animationEnded).isTrue() } @Test fun collapseWhileAnimating_inApp() { setUpBubbleBar() bubbleStashController.launcherState = BubbleLauncherState.IN_APP val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) var bubbleBarExpanded = false val onExpanded = { bubbleBarExpanded = true } val animator = BubbleBarViewAnimator( bubbleBarView, bubbleStashController, flyoutController, bubbleBarParentViewController, onExpanded = onExpanded, onBubbleBarVisible = emptyRunnable, onAnimationEnded = onAnimationEnded, animatorScheduler, ) InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.animateToInitialState(bubble, isInApp = true, isExpanding = true) } PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(barAnimator) { true } barAnimator.assertIsRunning() assertThat(animator.isAnimating).isTrue() // verify the hide bubble animation is pending assertThat(animatorScheduler.delayedBlock).isNotNull() InstrumentationRegistry.getInstrumentation().runOnMainSync { animator.collapsedWhileAnimating() } barAnimator.assertIsNotRunning() assertThat(animator.isAnimating).isFalse() assertThat(animatorScheduler.delayedBlock).isNull() assertThat(bubbleStashController.isStashed).isTrue() assertThat(bubbleBarExpanded).isFalse() assertThat(bubbleBarView.isExpanded).isFalse() assertThat(animationEnded).isTrue() } private fun setUpBubbleBar() { bubbleBarView = BubbleBarView(context) bubbleBarView.setController( object : BubbleBarView.Controller { override fun getBubbleBarTranslationY(): Float = 0f override fun onBubbleBarTouched() {} override fun expandBubbleBar() {} override fun dismissBubbleBar() {} override fun updateBubbleBarLocation(location: BubbleBarLocation?, source: Int) {} override fun setIsDragging(dragging: Boolean) {} override fun onBubbleBarExpandedStateChanged(expanded: Boolean) {} } ) InstrumentationRegistry.getInstrumentation().runOnMainSync { bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0) val inflater = LayoutInflater.from(context) val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20) overflowView = inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap) bubbleBarView.addView(overflowView) val bubbleInfo = BubbleInfo( "key", 0, null, null, 0, context.packageName, null, null, false, true, null, ) bubbleView = inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView bubble = BubbleBarBubble( bubbleInfo, bubbleView, BitmapInfo.of(bitmap, Color.WHITE), bitmap, Color.WHITE, "", BubbleBarFlyoutMessage(icon = null, title = "title", message = "message"), ) bubbleView.setBubble(bubble) bubbleBarView.addView(bubbleView) } InstrumentationRegistry.getInstrumentation().waitForIdleSync() } private fun setupFlyoutController() { flyoutContainer = FrameLayout(context) val flyoutPositioner = object : BubbleBarFlyoutPositioner { override val isOnLeft = true override val targetTy = 100f override val distanceToCollapsedPosition = PointF(0f, 0f) override val collapsedSize = 30f override val collapsedColor = Color.BLUE override val collapsedElevation = 1f override val distanceToRevealTriangle = 10f } val flyoutCallbacks = object : FlyoutCallbacks { override fun flyoutClicked() {} } val flyoutScheduler = FlyoutScheduler { block -> block.invoke() } flyoutController = BubbleBarFlyoutController( flyoutContainer, flyoutPositioner, flyoutCallbacks, flyoutScheduler, ) } private fun verifyBubbleBarIsExpandedWithTranslation(ty: Float) { assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) assertThat(bubbleBarView.scaleX).isEqualTo(1) assertThat(bubbleBarView.scaleY).isEqualTo(1) assertThat(bubbleBarView.translationY).isEqualTo(ty) assertThat(bubbleBarView.isExpanded).isTrue() } private fun waitForFlyoutToShow() { InstrumentationRegistry.getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(400) } assertThat(flyoutView).isNotNull() } private fun waitForFlyoutToHide() { InstrumentationRegistry.getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(350) } assertThat(flyoutView).isNull() } private fun waitForFlyoutToFadeOutAndBackIn() { InstrumentationRegistry.getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(750) } assertThat(flyoutView).isNotNull() } private fun PhysicsAnimator.assertIsRunning() { InstrumentationRegistry.getInstrumentation().runOnMainSync { assertThat(isRunning()).isTrue() } } private fun PhysicsAnimator.assertIsNotRunning() { InstrumentationRegistry.getInstrumentation().runOnMainSync { assertThat(isRunning()).isFalse() } } private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler { var pauseScheduler = false var pausedBlock: Runnable? = null private set var delayedBlock: Runnable? = null private set var canceledBlock: Runnable? = null private set override fun post(block: Runnable) { if (pauseScheduler) { pausedBlock = block return } block.run() } override fun postDelayed(delayMillis: Long, block: Runnable) { delayedBlock = block } override fun cancel(block: Runnable) { canceledBlock = delayedBlock delayedBlock = null } } private class TestBubbleBarParentViewHeightUpdateNotifier : BubbleBarParentViewHeightUpdateNotifier { var timesInvoked: Int = 0 override fun updateTopBoundary() { timesInvoked++ } } private class FakeBubbleStashController : BubbleStashController { var handleAnimator: PhysicsAnimator? = null var taskbarTouchRegionUpdated = false private set var animationInterrupted = false private set private var _isStashed = true override var launcherState = BubbleLauncherState.HOME override val isStashed: Boolean get() = _isStashed override var bubbleBarVerticalCenterForHome = 0 override var isSysuiLocked = false override val isTransientTaskBar = true override val hasHandleView = true override val bubbleBarTranslationYForTaskbar = BAR_TRANSLATION_Y_FOR_TASKBAR override val bubbleBarTranslationYForHotseat = BAR_TRANSLATION_Y_FOR_HOTSEAT override var inAppDisplayOverrideProgress = 0f override fun init( taskbarInsetsController: TaskbarInsetsController, bubbleBarViewController: BubbleBarViewController, bubbleStashedHandleViewController: BubbleStashedHandleViewController?, controllersAfterInitAction: BubbleStashController.ControllersAfterInitAction, ) {} override fun showBubbleBarImmediate() { _isStashed = false } override fun showBubbleBarImmediate(bubbleBarTranslationY: Float) { _isStashed = false } override fun stashBubbleBarImmediate() { _isStashed = true } override fun getTouchableHeight() = 100 override fun isBubbleBarVisible() = true override fun onNewBubbleAnimationInterrupted( isStashed: Boolean, bubbleBarTranslationY: Float, ) { _isStashed = isStashed animationInterrupted = true } override fun isEventOverBubbleBarViews(ev: MotionEvent) = false override fun setBubbleBarLocation(bubbleBarLocation: BubbleBarLocation) {} override fun stashBubbleBar() { _isStashed = true } override fun showBubbleBar(expandBubbles: Boolean, bubbleBarGesture: Boolean) { _isStashed = false } override fun getDiffBetweenHandleAndBarCenters() = DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS override fun getStashedHandleTranslationForNewBubbleAnimation() = HANDLE_TRANSLATION override fun getStashedHandlePhysicsAnimator(): PhysicsAnimator? { return handleAnimator } override fun updateTaskbarTouchRegion() { taskbarTouchRegionUpdated = true } override fun setHandleTranslationY(translationY: Float) {} override fun getHandleTranslationY() = 0f override fun getHandleBounds(bounds: Rect) {} } } private const val DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS = -20f private const val HANDLE_TRANSLATION = -30f private const val BAR_TRANSLATION_Y_FOR_TASKBAR = -50f private const val BAR_TRANSLATION_Y_FOR_HOTSEAT = -40f