Merge "YouTube and YTMusic trigger a race which causes the AnimatorSet to hang" into tm-dev

This commit is contained in:
Hawkwood Glazier
2022-04-30 16:47:58 +00:00
committed by Android (Google) Code Review
4 changed files with 52 additions and 63 deletions

View File

@@ -50,7 +50,6 @@ import android.util.Log;
import android.util.Pair; import android.util.Pair;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.Interpolator; import android.view.animation.Interpolator;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
@@ -553,30 +552,6 @@ public class MediaControlPanel {
// refreshState is required here to resize the text views (and prevent ellipsis) // refreshState is required here to resize the text views (and prevent ellipsis)
mMediaViewController.refreshState(); mMediaViewController.refreshState();
// Use OnPreDrawListeners to enforce zero alpha on these views for a frame.
// TransitionLayout insists on resetting the alpha of these views to 1 when onLayout
// is called which causes the animation to look bad. These suppress that behavior.
titleText.getViewTreeObserver().addOnPreDrawListener(
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
titleText.setAlpha(0);
titleText.getViewTreeObserver().removeOnPreDrawListener(this);
return true;
}
});
artistText.getViewTreeObserver().addOnPreDrawListener(
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
artistText.setAlpha(0);
artistText.getViewTreeObserver().removeOnPreDrawListener(this);
return true;
}
});
return Unit.INSTANCE; return Unit.INSTANCE;
}, },
() -> { () -> {

View File

@@ -18,8 +18,6 @@ package com.android.systemui.media
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorListenerAdapter import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorSet
import com.android.internal.annotations.VisibleForTesting
/** /**
* MetadataAnimationHandler controls the current state of the MediaControlPanel's transition motion. * MetadataAnimationHandler controls the current state of the MediaControlPanel's transition motion.
@@ -33,37 +31,37 @@ internal open class MetadataAnimationHandler(
private val enterAnimator: Animator private val enterAnimator: Animator
) : AnimatorListenerAdapter() { ) : AnimatorListenerAdapter() {
private val animator: AnimatorSet
private var postExitUpdate: (() -> Unit)? = null private var postExitUpdate: (() -> Unit)? = null
private var postEnterUpdate: (() -> Unit)? = null private var postEnterUpdate: (() -> Unit)? = null
private var targetData: Any? = null private var targetData: Any? = null
val isRunning: Boolean val isRunning: Boolean
get() = animator.isRunning get() = enterAnimator.isRunning || exitAnimator.isRunning
fun setNext(targetData: Any, postExit: () -> Unit, postEnter: () -> Unit): Boolean { fun setNext(targetData: Any, postExit: () -> Unit, postEnter: () -> Unit): Boolean {
if (targetData != this.targetData) { if (targetData != this.targetData) {
this.targetData = targetData this.targetData = targetData
postExitUpdate = postExit postExitUpdate = postExit
postEnterUpdate = postEnter postEnterUpdate = postEnter
if (!animator.isRunning) { if (!isRunning) {
animator.start() exitAnimator.start()
} }
return true return true
} }
return false return false
} }
override fun onAnimationEnd(animator: Animator) { override fun onAnimationEnd(anim: Animator) {
if (animator === exitAnimator) { if (anim === exitAnimator) {
postExitUpdate?.let { it() } postExitUpdate?.let { it() }
postExitUpdate = null postExitUpdate = null
enterAnimator.start()
} }
if (animator === enterAnimator) { if (anim === enterAnimator) {
// Another new update appeared while entering // Another new update appeared while entering
if (postExitUpdate != null) { if (postExitUpdate != null) {
this.animator.start() exitAnimator.start()
} else { } else {
postEnterUpdate?.let { it() } postEnterUpdate?.let { it() }
postEnterUpdate = null postEnterUpdate = null
@@ -74,13 +72,5 @@ internal open class MetadataAnimationHandler(
init { init {
exitAnimator.addListener(this) exitAnimator.addListener(this)
enterAnimator.addListener(this) enterAnimator.addListener(this)
animator = buildAnimatorSet(exitAnimator, enterAnimator)
}
@VisibleForTesting
protected open fun buildAnimatorSet(exit: Animator, enter: Animator): AnimatorSet {
val result = AnimatorSet()
result.playSequentially(exitAnimator, enterAnimator)
return result
} }
} }

View File

@@ -947,7 +947,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
// Rebinding should not trigger animation // Rebinding should not trigger animation
player.bindPlayer(mediaData, PACKAGE) player.bindPlayer(mediaData, PACKAGE)
verify(mockAnimator, times(1)).start() verify(mockAnimator, times(2)).start()
} }
@Test @Test
@@ -969,7 +969,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
// Bind trigges new animation // Bind trigges new animation
player.bindPlayer(data1, PACKAGE) player.bindPlayer(data1, PACKAGE)
verify(mockAnimator, times(2)).start() verify(mockAnimator, times(3)).start()
whenever(mockAnimator.isRunning()).thenReturn(true) whenever(mockAnimator.isRunning()).thenReturn(true)
// Rebind before animation end binds corrct data // Rebind before animation end binds corrct data

View File

@@ -18,7 +18,6 @@ package com.android.systemui.media
import org.mockito.Mockito.`when` as whenever import org.mockito.Mockito.`when` as whenever
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorSet
import android.test.suitebuilder.annotation.SmallTest import android.test.suitebuilder.annotation.SmallTest
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
import android.testing.TestableLooper import android.testing.TestableLooper
@@ -44,7 +43,6 @@ class MetadataAnimationHandlerTest : SysuiTestCase() {
private interface Callback : () -> Unit private interface Callback : () -> Unit
private lateinit var handler: MetadataAnimationHandler private lateinit var handler: MetadataAnimationHandler
@Mock private lateinit var animatorSet: AnimatorSet
@Mock private lateinit var enterAnimator: Animator @Mock private lateinit var enterAnimator: Animator
@Mock private lateinit var exitAnimator: Animator @Mock private lateinit var exitAnimator: Animator
@Mock private lateinit var postExitCB: Callback @Mock private lateinit var postExitCB: Callback
@@ -54,11 +52,7 @@ class MetadataAnimationHandlerTest : SysuiTestCase() {
@Before @Before
fun setUp() { fun setUp() {
handler = object : MetadataAnimationHandler(exitAnimator, enterAnimator) { handler = MetadataAnimationHandler(exitAnimator, enterAnimator)
override fun buildAnimatorSet(exit: Animator, enter: Animator): AnimatorSet {
return animatorSet
}
}
} }
@After @After
@@ -69,22 +63,31 @@ class MetadataAnimationHandlerTest : SysuiTestCase() {
val cb = { fail("Unexpected callback") } val cb = { fail("Unexpected callback") }
handler.setNext("data-1", cb, cb) handler.setNext("data-1", cb, cb)
verify(animatorSet).start() verify(exitAnimator).start()
} }
@Test @Test
fun executeAnimationEnd_runsCallacks() { fun executeAnimationEnd_runsCallacks() {
// We expect this first call to only start the exit animator
handler.setNext("data-1", postExitCB, postEnterCB) handler.setNext("data-1", postExitCB, postEnterCB)
verify(animatorSet, times(1)).start() verify(exitAnimator, times(1)).start()
verify(enterAnimator, never()).start()
verify(postExitCB, never()).invoke() verify(postExitCB, never()).invoke()
verify(postEnterCB, never()).invoke()
// After the exit animator completes,
// the exit cb should run, and enter animation should start
handler.onAnimationEnd(exitAnimator) handler.onAnimationEnd(exitAnimator)
verify(animatorSet, times(1)).start() verify(exitAnimator, times(1)).start()
verify(enterAnimator, times(1)).start()
verify(postExitCB, times(1)).invoke() verify(postExitCB, times(1)).invoke()
verify(postEnterCB, never()).invoke() verify(postEnterCB, never()).invoke()
// After the exit animator completes,
// the enter cb should run without other state changes
handler.onAnimationEnd(enterAnimator) handler.onAnimationEnd(enterAnimator)
verify(animatorSet, times(1)).start() verify(exitAnimator, times(1)).start()
verify(enterAnimator, times(1)).start()
verify(postExitCB, times(1)).invoke() verify(postExitCB, times(1)).invoke()
verify(postEnterCB, times(1)).invoke() verify(postEnterCB, times(1)).invoke()
} }
@@ -120,38 +123,58 @@ class MetadataAnimationHandlerTest : SysuiTestCase() {
val postExitCB2 = mock(Callback::class.java) val postExitCB2 = mock(Callback::class.java)
val postEnterCB2 = mock(Callback::class.java) val postEnterCB2 = mock(Callback::class.java)
// We expect this first call to only start the exit animator
handler.setNext("data-1", postExitCB, postEnterCB) handler.setNext("data-1", postExitCB, postEnterCB)
verify(animatorSet, times(1)).start() verify(exitAnimator, times(1)).start()
verify(enterAnimator, never()).start()
verify(postExitCB, never()).invoke() verify(postExitCB, never()).invoke()
verify(postExitCB2, never()).invoke() verify(postExitCB2, never()).invoke()
verify(postEnterCB, never()).invoke() verify(postEnterCB, never()).invoke()
verify(postEnterCB2, never()).invoke() verify(postEnterCB2, never()).invoke()
whenever(animatorSet.isRunning()).thenReturn(true) // After the exit animator completes,
// the exit cb should run, and enter animation should start
whenever(exitAnimator.isRunning()).thenReturn(true)
whenever(enterAnimator.isRunning()).thenReturn(false)
handler.onAnimationEnd(exitAnimator) handler.onAnimationEnd(exitAnimator)
verify(animatorSet, times(1)).start() verify(exitAnimator, times(1)).start()
verify(enterAnimator, times(1)).start()
verify(postExitCB, times(1)).invoke() verify(postExitCB, times(1)).invoke()
verify(postExitCB2, never()).invoke() verify(postExitCB2, never()).invoke()
verify(postEnterCB, never()).invoke() verify(postEnterCB, never()).invoke()
verify(postEnterCB2, never()).invoke() verify(postEnterCB2, never()).invoke()
// Setting new data before the enter animator completes should not trigger
// the exit animator an additional time (since it's already running)
whenever(exitAnimator.isRunning()).thenReturn(false)
whenever(enterAnimator.isRunning()).thenReturn(true)
handler.setNext("data-2", postExitCB2, postEnterCB2) handler.setNext("data-2", postExitCB2, postEnterCB2)
verify(exitAnimator, times(1)).start()
// Finishing the enterAnimator should cause the exitAnimator to fire again
// since the data change and additional time. No enterCB should be executed.
handler.onAnimationEnd(enterAnimator) handler.onAnimationEnd(enterAnimator)
verify(animatorSet, times(2)).start() verify(exitAnimator, times(2)).start()
verify(enterAnimator, times(1)).start()
verify(postExitCB, times(1)).invoke() verify(postExitCB, times(1)).invoke()
verify(postExitCB2, never()).invoke() verify(postExitCB2, never()).invoke()
verify(postEnterCB, never()).invoke() verify(postEnterCB, never()).invoke()
verify(postEnterCB2, never()).invoke() verify(postEnterCB2, never()).invoke()
// Continuing the sequence, this triggers the enter animator an additional time
handler.onAnimationEnd(exitAnimator) handler.onAnimationEnd(exitAnimator)
verify(animatorSet, times(2)).start() verify(exitAnimator, times(2)).start()
verify(enterAnimator, times(2)).start()
verify(postExitCB, times(1)).invoke() verify(postExitCB, times(1)).invoke()
verify(postExitCB2, times(1)).invoke() verify(postExitCB2, times(1)).invoke()
verify(postEnterCB, never()).invoke() verify(postEnterCB, never()).invoke()
verify(postEnterCB2, never()).invoke() verify(postEnterCB2, never()).invoke()
// And finally the enter animator completes,
// triggering the correct postEnterCallback to fire
handler.onAnimationEnd(enterAnimator) handler.onAnimationEnd(enterAnimator)
verify(animatorSet, times(2)).start() verify(exitAnimator, times(2)).start()
verify(enterAnimator, times(2)).start()
verify(postExitCB, times(1)).invoke() verify(postExitCB, times(1)).invoke()
verify(postExitCB2, times(1)).invoke() verify(postExitCB2, times(1)).invoke()
verify(postEnterCB, never()).invoke() verify(postEnterCB, never()).invoke()
@@ -172,6 +195,7 @@ class MetadataAnimationHandlerTest : SysuiTestCase() {
fun enterAnimatorEndsWithoutCallback_noAnimatiorStart() { fun enterAnimatorEndsWithoutCallback_noAnimatiorStart() {
handler.onAnimationEnd(enterAnimator) handler.onAnimationEnd(enterAnimator)
verify(animatorSet, never()).start() verify(exitAnimator, never()).start()
verify(enterAnimator, never()).start()
} }
} }