YouTube and YTMusic trigger a race which causes the AnimatorSet to hang

Fixes: 230195336
Test: Manual
Change-Id: Ib72e4bd68bda56534ef06c38c977b2f931ab7a11
This commit is contained in:
Hawkwood Glazier
2022-04-28 16:02:09 +00:00
parent d5bb81348c
commit f2e36f49f5
4 changed files with 52 additions and 63 deletions

View File

@@ -48,7 +48,6 @@ import android.util.Log;
import android.util.Pair;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.Interpolator;
import android.widget.ImageButton;
import android.widget.ImageView;
@@ -552,30 +551,6 @@ public class MediaControlPanel {
// refreshState is required here to resize the text views (and prevent ellipsis)
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;
},
() -> {

View File

@@ -18,8 +18,6 @@ package com.android.systemui.media
import android.animation.Animator
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.
@@ -33,37 +31,37 @@ internal open class MetadataAnimationHandler(
private val enterAnimator: Animator
) : AnimatorListenerAdapter() {
private val animator: AnimatorSet
private var postExitUpdate: (() -> Unit)? = null
private var postEnterUpdate: (() -> Unit)? = null
private var targetData: Any? = null
val isRunning: Boolean
get() = animator.isRunning
get() = enterAnimator.isRunning || exitAnimator.isRunning
fun setNext(targetData: Any, postExit: () -> Unit, postEnter: () -> Unit): Boolean {
if (targetData != this.targetData) {
this.targetData = targetData
postExitUpdate = postExit
postEnterUpdate = postEnter
if (!animator.isRunning) {
animator.start()
if (!isRunning) {
exitAnimator.start()
}
return true
}
return false
}
override fun onAnimationEnd(animator: Animator) {
if (animator === exitAnimator) {
override fun onAnimationEnd(anim: Animator) {
if (anim === exitAnimator) {
postExitUpdate?.let { it() }
postExitUpdate = null
enterAnimator.start()
}
if (animator === enterAnimator) {
if (anim === enterAnimator) {
// Another new update appeared while entering
if (postExitUpdate != null) {
this.animator.start()
exitAnimator.start()
} else {
postEnterUpdate?.let { it() }
postEnterUpdate = null
@@ -74,13 +72,5 @@ internal open class MetadataAnimationHandler(
init {
exitAnimator.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

@@ -951,7 +951,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
// Rebinding should not trigger animation
player.bindPlayer(mediaData, PACKAGE)
verify(mockAnimator, times(1)).start()
verify(mockAnimator, times(2)).start()
}
@Test
@@ -973,7 +973,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
// Bind trigges new animation
player.bindPlayer(data1, PACKAGE)
verify(mockAnimator, times(2)).start()
verify(mockAnimator, times(3)).start()
whenever(mockAnimator.isRunning()).thenReturn(true)
// 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 android.animation.Animator
import android.animation.AnimatorSet
import android.test.suitebuilder.annotation.SmallTest
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
@@ -44,7 +43,6 @@ class MetadataAnimationHandlerTest : SysuiTestCase() {
private interface Callback : () -> Unit
private lateinit var handler: MetadataAnimationHandler
@Mock private lateinit var animatorSet: AnimatorSet
@Mock private lateinit var enterAnimator: Animator
@Mock private lateinit var exitAnimator: Animator
@Mock private lateinit var postExitCB: Callback
@@ -54,11 +52,7 @@ class MetadataAnimationHandlerTest : SysuiTestCase() {
@Before
fun setUp() {
handler = object : MetadataAnimationHandler(exitAnimator, enterAnimator) {
override fun buildAnimatorSet(exit: Animator, enter: Animator): AnimatorSet {
return animatorSet
}
}
handler = MetadataAnimationHandler(exitAnimator, enterAnimator)
}
@After
@@ -69,22 +63,31 @@ class MetadataAnimationHandlerTest : SysuiTestCase() {
val cb = { fail("Unexpected callback") }
handler.setNext("data-1", cb, cb)
verify(animatorSet).start()
verify(exitAnimator).start()
}
@Test
fun executeAnimationEnd_runsCallacks() {
// We expect this first call to only start the exit animator
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(postEnterCB, never()).invoke()
// After the exit animator completes,
// the exit cb should run, and enter animation should start
handler.onAnimationEnd(exitAnimator)
verify(animatorSet, times(1)).start()
verify(exitAnimator, times(1)).start()
verify(enterAnimator, times(1)).start()
verify(postExitCB, times(1)).invoke()
verify(postEnterCB, never()).invoke()
// After the exit animator completes,
// the enter cb should run without other state changes
handler.onAnimationEnd(enterAnimator)
verify(animatorSet, times(1)).start()
verify(exitAnimator, times(1)).start()
verify(enterAnimator, times(1)).start()
verify(postExitCB, times(1)).invoke()
verify(postEnterCB, times(1)).invoke()
}
@@ -120,38 +123,58 @@ class MetadataAnimationHandlerTest : SysuiTestCase() {
val postExitCB2 = 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)
verify(animatorSet, times(1)).start()
verify(exitAnimator, times(1)).start()
verify(enterAnimator, never()).start()
verify(postExitCB, never()).invoke()
verify(postExitCB2, never()).invoke()
verify(postEnterCB, 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)
verify(animatorSet, times(1)).start()
verify(exitAnimator, times(1)).start()
verify(enterAnimator, times(1)).start()
verify(postExitCB, times(1)).invoke()
verify(postExitCB2, never()).invoke()
verify(postEnterCB, 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)
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)
verify(animatorSet, times(2)).start()
verify(exitAnimator, times(2)).start()
verify(enterAnimator, times(1)).start()
verify(postExitCB, times(1)).invoke()
verify(postExitCB2, never()).invoke()
verify(postEnterCB, never()).invoke()
verify(postEnterCB2, never()).invoke()
// Continuing the sequence, this triggers the enter animator an additional time
handler.onAnimationEnd(exitAnimator)
verify(animatorSet, times(2)).start()
verify(exitAnimator, times(2)).start()
verify(enterAnimator, times(2)).start()
verify(postExitCB, times(1)).invoke()
verify(postExitCB2, times(1)).invoke()
verify(postEnterCB, never()).invoke()
verify(postEnterCB2, never()).invoke()
// And finally the enter animator completes,
// triggering the correct postEnterCallback to fire
handler.onAnimationEnd(enterAnimator)
verify(animatorSet, times(2)).start()
verify(exitAnimator, times(2)).start()
verify(enterAnimator, times(2)).start()
verify(postExitCB, times(1)).invoke()
verify(postExitCB2, times(1)).invoke()
verify(postEnterCB, never()).invoke()
@@ -172,6 +195,7 @@ class MetadataAnimationHandlerTest : SysuiTestCase() {
fun enterAnimatorEndsWithoutCallback_noAnimatiorStart() {
handler.onAnimationEnd(enterAnimator)
verify(animatorSet, never()).start()
verify(exitAnimator, never()).start()
verify(enterAnimator, never()).start()
}
}