Merge "Fix flaky tests in ExpandedAnimationControllerTest" into main
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
index 5b0239f..02af2d0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
@@ -563,7 +563,7 @@
                         ? p.x - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR
                         : p.x + mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR;
                 animationForChild(child)
-                        .translationX(fromX, p.y)
+                        .translationX(fromX, p.x)
                         .start();
             } else {
                 float fromY = p.y - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR;
@@ -634,4 +634,9 @@
                     .start();
         }
     }
+
+    /** Returns true if we're in the middle of a collapse or expand animation. */
+    boolean isAnimating() {
+        return mAnimatingCollapse || mAnimatingExpand;
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java
index c1ff260..60f1d271 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java
@@ -16,52 +16,51 @@
 
 package com.android.wm.shell.bubbles.animation;
 
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.annotation.SuppressLint;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Insets;
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.testing.AndroidTestingRunner;
 import android.view.View;
 import android.view.WindowManager;
 import android.widget.FrameLayout;
 
 import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.wm.shell.R;
 import com.android.wm.shell.bubbles.BubblePositioner;
 import com.android.wm.shell.bubbles.BubbleStackView;
 
+import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
 @SmallTest
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
 public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestCase {
 
-    private int mDisplayWidth = 500;
-    private int mDisplayHeight = 1000;
-
-    private Runnable mOnBubbleAnimatedOutAction = mock(Runnable.class);
+    private final Semaphore mBubbleRemovedSemaphore = new Semaphore(0);
+    private final Runnable mOnBubbleAnimatedOutAction = mBubbleRemovedSemaphore::release;
     ExpandedAnimationController mExpandedController;
 
     private int mStackOffset;
     private PointF mExpansionPoint;
     private BubblePositioner mPositioner;
-    private BubbleStackView.StackViewState mStackViewState = new BubbleStackView.StackViewState();
+    private final BubbleStackView.StackViewState mStackViewState =
+            new BubbleStackView.StackViewState();
 
-    @SuppressLint("VisibleForTests")
     @Before
     public void setUp() throws Exception {
         super.setUp();
@@ -70,15 +69,13 @@
                 getContext().getSystemService(WindowManager.class));
         mPositioner.updateInternal(Configuration.ORIENTATION_PORTRAIT,
                 Insets.of(0, 0, 0, 0),
-                new Rect(0, 0, mDisplayWidth, mDisplayHeight));
+                new Rect(0, 0, 500, 1000));
 
         BubbleStackView stackView = mock(BubbleStackView.class);
-        when(stackView.getState()).thenReturn(getStackViewState());
 
         mExpandedController = new ExpandedAnimationController(mPositioner,
                 mOnBubbleAnimatedOutAction,
                 stackView);
-        spyOn(mExpandedController);
 
         addOneMoreThanBubbleLimitBubbles();
         mLayout.setActiveController(mExpandedController);
@@ -86,9 +83,18 @@
         Resources res = mLayout.getResources();
         mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
         mExpansionPoint = new PointF(100, 100);
+
+        getStackViewState();
+        when(stackView.getState()).thenAnswer(i -> getStackViewState());
+        waitForMainThread();
     }
 
-    public BubbleStackView.StackViewState getStackViewState() {
+    @After
+    public void tearDown() {
+        waitForMainThread();
+    }
+
+    private BubbleStackView.StackViewState getStackViewState() {
         mStackViewState.numberOfBubbles = mLayout.getChildCount();
         mStackViewState.selectedIndex = 0;
         mStackViewState.onLeft = mPositioner.isStackOnLeft(mExpansionPoint);
@@ -96,68 +102,71 @@
     }
 
     @Test
-    @Ignore
-    public void testExpansionAndCollapse() throws InterruptedException {
-        Runnable afterExpand = mock(Runnable.class);
-        mExpandedController.expandFromStack(afterExpand);
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
-
+    public void testExpansionAndCollapse() throws Exception {
+        expand();
         testBubblesInCorrectExpandedPositions();
-        verify(afterExpand).run();
+        waitForMainThread();
 
-        Runnable afterCollapse = mock(Runnable.class);
+        final Semaphore semaphore = new Semaphore(0);
+        Runnable afterCollapse = semaphore::release;
         mExpandedController.collapseBackToStack(mExpansionPoint, false, afterCollapse);
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
-
-        testStackedAtPosition(mExpansionPoint.x, mExpansionPoint.y, -1);
-        verify(afterExpand).run();
+        assertThat(semaphore.tryAcquire(1, 2, TimeUnit.SECONDS)).isTrue();
+        waitForAnimation();
+        testStackedAtPosition(mExpansionPoint.x, mExpansionPoint.y);
     }
 
     @Test
-    @Ignore
-    public void testOnChildAdded() throws InterruptedException {
+    public void testOnChildAdded() throws Exception {
         expand();
+        waitForMainThread();
 
         // Add another new view and wait for its animation.
         final View newView = new FrameLayout(getContext());
         mLayout.addView(newView, 0);
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
 
+        waitForAnimation();
         testBubblesInCorrectExpandedPositions();
     }
 
     @Test
-    @Ignore
-    public void testOnChildRemoved() throws InterruptedException {
+    public void testOnChildRemoved() throws Exception {
         expand();
+        waitForMainThread();
 
-        // Remove some views and see if the remaining child views still pass the expansion test.
+        // Remove some views and verify the remaining child views still pass the expansion test.
         mLayout.removeView(mViews.get(0));
         mLayout.removeView(mViews.get(3));
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
+
+        // Removing a view will invoke onBubbleAnimatedOutAction. Block until it gets called twice.
+        assertThat(mBubbleRemovedSemaphore.tryAcquire(2, 2, TimeUnit.SECONDS)).isTrue();
+
+        waitForAnimation();
         testBubblesInCorrectExpandedPositions();
     }
 
     @Test
-    public void testDragBubbleOutDoesntNPE() throws InterruptedException {
+    public void testDragBubbleOutDoesntNPE() {
         mExpandedController.onGestureFinished();
         mExpandedController.dragBubbleOut(mViews.get(0), 1, 1);
     }
 
     /** Expand the stack and wait for animations to finish. */
     private void expand() throws InterruptedException {
-        mExpandedController.expandFromStack(mock(Runnable.class));
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
+        final Semaphore semaphore = new Semaphore(0);
+        Runnable afterExpand = semaphore::release;
+
+        mExpandedController.expandFromStack(afterExpand);
+        assertThat(semaphore.tryAcquire(1, TimeUnit.SECONDS)).isTrue();
     }
 
     /** Check that children are in the correct positions for being stacked. */
-    private void testStackedAtPosition(float x, float y, int offsetMultiplier) {
+    private void testStackedAtPosition(float x, float y) {
         // Make sure the rest of the stack moved again, including the first bubble not moving, and
         // is stacked to the right now that we're on the right side of the screen.
         for (int i = 0; i < mLayout.getChildCount(); i++) {
-            assertEquals(x + i * offsetMultiplier * mStackOffset,
-                    mLayout.getChildAt(i).getTranslationX(), 2f);
-            assertEquals(y, mLayout.getChildAt(i).getTranslationY(), 2f);
+            assertEquals(x, mLayout.getChildAt(i).getTranslationX(), 2f);
+            assertEquals(y + Math.min(i, 1) * mStackOffset, mLayout.getChildAt(i).getTranslationY(),
+                    2f);
             assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f);
         }
     }
@@ -175,4 +184,22 @@
                     mLayout.getChildAt(i).getTranslationY(), 2f);
         }
     }
+
+    private void waitForAnimation() throws Exception {
+        final Semaphore semaphore = new Semaphore(0);
+        boolean[] animating = new boolean[]{ true };
+        for (int i = 0; i < 4; i++) {
+            if (animating[0]) {
+                mMainThreadHandler.post(() -> {
+                    if (!mExpandedController.isAnimating()) {
+                        animating[0] = false;
+                        semaphore.release();
+                    }
+                });
+                Thread.sleep(500);
+            }
+        }
+        assertThat(semaphore.tryAcquire(1, 2, TimeUnit.SECONDS)).isTrue();
+        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java
index 48ae296..2ed5add 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java
@@ -164,11 +164,17 @@
 
         @Override
         public void cancelAllAnimations() {
+            if (mLayout.getChildCount() == 0) {
+                return;
+            }
             mMainThreadHandler.post(super::cancelAllAnimations);
         }
 
         @Override
         public void cancelAnimationsOnView(View view) {
+            if (mLayout.getChildCount() == 0) {
+                return;
+            }
             mMainThreadHandler.post(() -> super.cancelAnimationsOnView(view));
         }
 
@@ -221,6 +227,9 @@
 
             @Override
             protected void startPathAnimation() {
+                if (mLayout.getChildCount() == 0) {
+                    return;
+                }
                 mMainThreadHandler.post(super::startPathAnimation);
             }
         }
@@ -322,4 +331,9 @@
             e.printStackTrace();
         }
     }
+
+    /** Waits for the main thread to finish processing all pending runnables. */
+    public void waitForMainThread() {
+        runOnMainThreadAndBlock(() -> {});
+    }
 }