1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wm.shell.bubbles.animation;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.mockito.ArgumentMatchers.any;
21 import static org.mockito.Mockito.mock;
22 import static org.mockito.Mockito.never;
23 import static org.mockito.Mockito.spy;
24 import static org.mockito.Mockito.times;
25 import static org.mockito.Mockito.verify;
26 
27 import android.graphics.PointF;
28 import android.testing.AndroidTestingRunner;
29 import android.view.View;
30 import android.view.WindowManager;
31 import android.widget.FrameLayout;
32 
33 import androidx.dynamicanimation.animation.DynamicAnimation;
34 import androidx.dynamicanimation.animation.SpringForce;
35 import androidx.test.filters.SmallTest;
36 
37 import com.android.wm.shell.R;
38 import com.android.wm.shell.bubbles.TestableBubblePositioner;
39 import com.android.wm.shell.common.FloatingContentCoordinator;
40 
41 import org.junit.Before;
42 import org.junit.Ignore;
43 import org.junit.Test;
44 import org.junit.runner.RunWith;
45 import org.mockito.Mock;
46 
47 import java.util.concurrent.CountDownLatch;
48 import java.util.concurrent.TimeUnit;
49 import java.util.function.IntSupplier;
50 
51 @SmallTest
52 @RunWith(AndroidTestingRunner.class)
53 public class StackAnimationControllerTest extends PhysicsAnimationLayoutTestCase {
54 
55     @Mock
56     private FloatingContentCoordinator mFloatingContentCoordinator;
57 
58     private TestableStackController mStackController;
59 
60     private int mStackOffset;
61     private Runnable mCheckStartPosSet;
62 
63     @Before
setUp()64     public void setUp() throws Exception {
65         super.setUp();
66         mStackController = spy(new TestableStackController(
67                 mFloatingContentCoordinator, new IntSupplier() {
68                     @Override
69                     public int getAsInt() {
70                         return mLayout.getChildCount();
71                     }
72                 }, mock(Runnable.class), mock(Runnable.class)));
73         mLayout.setActiveController(mStackController);
74         addOneMoreThanBubbleLimitBubbles();
75         mStackOffset = mLayout.getResources().getDimensionPixelSize(R.dimen.bubble_stack_offset);
76     }
77 
78     /**
79      * Test moving around the stack, and make sure the position is updated correctly, and the stack
80      * direction is correct.
81      */
82     @Test
83     @Ignore("Flaking")
testMoveFirstBubbleWithStackFollowing()84     public void testMoveFirstBubbleWithStackFollowing() throws InterruptedException {
85         mStackController.moveFirstBubbleWithStackFollowing(200, 100);
86 
87         // The first bubble should have moved instantly, the rest should be waiting for animation.
88         assertEquals(200, mViews.get(0).getTranslationX(), .1f);
89         assertEquals(100, mViews.get(0).getTranslationY(), .1f);
90         assertEquals(0, mViews.get(1).getTranslationX(), .1f);
91         assertEquals(0, mViews.get(1).getTranslationY(), .1f);
92 
93         waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
94 
95         // Make sure the rest of the stack got moved to the right place and is stacked to the left.
96         testStackedAtPosition(200, 100, -1);
97         assertEquals(new PointF(200, 100), mStackController.getStackPosition());
98 
99         mStackController.moveFirstBubbleWithStackFollowing(1000, 500);
100 
101         // The first bubble again should have moved instantly while the rest remained where they
102         // were until the animation takes over.
103         assertEquals(1000, mViews.get(0).getTranslationX(), .1f);
104         assertEquals(500, mViews.get(0).getTranslationY(), .1f);
105         assertEquals(200 + -mStackOffset, mViews.get(1).getTranslationX(), .1f);
106         assertEquals(100, mViews.get(1).getTranslationY(), .1f);
107 
108         waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
109 
110         // Make sure the rest of the stack moved again, including the first bubble not moving, and
111         // is stacked to the right now that we're on the right side of the screen.
112         testStackedAtPosition(1000, 500, 1);
113         assertEquals(new PointF(1000, 500), mStackController.getStackPosition());
114     }
115 
116     @Test
117     @Ignore("Sporadically failing due to DynamicAnimation not settling.")
testFlingSideways()118     public void testFlingSideways() throws InterruptedException {
119         // Hard fling directly upwards, no X velocity. The X fling should terminate pretty much
120         // immediately, and spring to 0f, the y fling is hard enough that it will overshoot the top
121         // but should bounce back down.
122         mStackController.flingThenSpringFirstBubbleWithStackFollowing(
123                 DynamicAnimation.TRANSLATION_X,
124                 5000f, 1.15f, new SpringForce(), mWidth * 1f);
125         mStackController.flingThenSpringFirstBubbleWithStackFollowing(
126                 DynamicAnimation.TRANSLATION_Y,
127                 0f, 1.15f, new SpringForce(), 0f);
128 
129         // Nothing should move initially since the animations haven't begun, including the first
130         // view.
131         assertEquals(0f, mViews.get(0).getTranslationX(), 1f);
132         assertEquals(0f, mViews.get(0).getTranslationY(), 1f);
133 
134         // Wait for the flinging.
135         waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X,
136                 DynamicAnimation.TRANSLATION_Y);
137 
138         // Wait for the springing.
139         waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X,
140                 DynamicAnimation.TRANSLATION_Y);
141 
142         // Once the dust has settled, we should have flung all the way to the right side, with the
143         // stack stacked off to the right now.
144         testStackedAtPosition(mWidth * 1f, 0f, 1);
145     }
146 
147     @Test
148     @Ignore("Sporadically failing due to DynamicAnimation not settling.")
testFlingUpFromBelowBottomCenter()149     public void testFlingUpFromBelowBottomCenter() throws InterruptedException {
150         // Move to the center of the screen, just past the bottom.
151         mStackController.moveFirstBubbleWithStackFollowing(mWidth / 2f, mHeight + 100);
152         waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
153 
154         // Hard fling directly upwards, no X velocity. The X fling should terminate pretty much
155         // immediately, and spring to 0f, the y fling is hard enough that it will overshoot the top
156         // but should bounce back down.
157         mStackController.flingThenSpringFirstBubbleWithStackFollowing(
158                 DynamicAnimation.TRANSLATION_X,
159                 0, 1.15f, new SpringForce(), 27f);
160         mStackController.flingThenSpringFirstBubbleWithStackFollowing(
161                 DynamicAnimation.TRANSLATION_Y,
162                 5000f, 1.15f, new SpringForce(), 27f);
163 
164         // Nothing should move initially since the animations haven't begun.
165         assertEquals(mWidth / 2f, mViews.get(0).getTranslationX(), .1f);
166         assertEquals(mHeight + 100, mViews.get(0).getTranslationY(), .1f);
167 
168         waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X,
169                 DynamicAnimation.TRANSLATION_Y);
170 
171         // Once the dust has settled, we should have flung a bit but then sprung to the final
172         // destination which is (27, 27).
173         testStackedAtPosition(27, 27, -1);
174     }
175 
176     @Test
177     @Ignore("Flaking")
testChildAdded()178     public void testChildAdded() throws InterruptedException {
179         // Move the stack to y = 500.
180         mStackController.moveFirstBubbleWithStackFollowing(0f, 500f);
181         waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X,
182                 DynamicAnimation.TRANSLATION_Y);
183 
184         final View newView = new FrameLayout(mContext);
185         mLayout.addView(
186                 newView,
187                 0,
188                 new FrameLayout.LayoutParams(50, 50));
189 
190         waitForStartPosToBeSet();
191         waitForLayoutMessageQueue();
192         waitForPropertyAnimations(
193                 DynamicAnimation.TRANSLATION_X,
194                 DynamicAnimation.TRANSLATION_Y,
195                 DynamicAnimation.SCALE_X,
196                 DynamicAnimation.SCALE_Y);
197 
198         // The new view should be at the top of the stack, in the correct position.
199         assertEquals(0f, newView.getTranslationX(), .1f);
200         assertEquals(500f, newView.getTranslationY(), .1f);
201         assertEquals(1f, newView.getScaleX(), .1f);
202         assertEquals(1f, newView.getScaleY(), .1f);
203         assertEquals(1f, newView.getAlpha(), .1f);
204     }
205 
206     @Test
207     @Ignore("Occasionally flakes, ignoring pending investigation.")
testChildRemoved()208     public void testChildRemoved() throws InterruptedException {
209         assertEquals(0, mLayout.getTransientViewCount());
210 
211         final View firstView = mLayout.getChildAt(0);
212         mLayout.removeView(firstView);
213 
214         // The view should now be transient, and missing from the view's normal hierarchy.
215         assertEquals(1, mLayout.getTransientViewCount());
216         assertEquals(-1, mLayout.indexOfChild(firstView));
217 
218         waitForPropertyAnimations(DynamicAnimation.ALPHA);
219         waitForLayoutMessageQueue();
220 
221         // The view should now be gone entirely, no transient views left.
222         assertEquals(0, mLayout.getTransientViewCount());
223 
224         // The subsequent view should have been translated over to 0, not stacked off to the left.
225         assertEquals(0, mLayout.getChildAt(0).getTranslationX(), .1f);
226     }
227 
228     @Test
229     @Ignore("Flaky")
testRestoredAtRestingPosition()230     public void testRestoredAtRestingPosition() throws InterruptedException {
231         mStackController.flingStackThenSpringToEdge(0, 5000, 5000);
232 
233         waitForPropertyAnimations(
234                 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
235         waitForLayoutMessageQueue();
236 
237         final PointF prevStackPos = mStackController.getStackPosition();
238 
239         mLayout.removeAllViews();
240 
241         waitForLayoutMessageQueue();
242 
243         mLayout.addView(new FrameLayout(getContext()));
244 
245         waitForLayoutMessageQueue();
246         waitForPropertyAnimations(
247                 DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
248 
249         assertEquals(prevStackPos, mStackController.getStackPosition());
250     }
251 
252     @Test
testFloatingCoordinator()253     public void testFloatingCoordinator() {
254         // We should have called onContentAdded only once while adding all of the bubbles in
255         // setup().
256         verify(mFloatingContentCoordinator, times(1)).onContentAdded(any());
257         verify(mFloatingContentCoordinator, never()).onContentRemoved(any());
258 
259         // Remove all views and verify that we called onContentRemoved only once.
260         while (mLayout.getChildCount() > 0) {
261             mLayout.removeView(mLayout.getChildAt(0));
262         }
263 
264         verify(mFloatingContentCoordinator, times(1)).onContentRemoved(any());
265     }
266 
267     /**
268      * Checks every child view to make sure it's stacked at the given coordinates, off to the left
269      * or right side depending on offset multiplier.
270      */
testStackedAtPosition(float x, float y, int offsetMultiplier)271     private void testStackedAtPosition(float x, float y, int offsetMultiplier) {
272         // Make sure the rest of the stack moved again, including the first bubble not moving, and
273         // is stacked to the right now that we're on the right side of the screen.
274         for (int i = 0; i < mLayout.getChildCount(); i++) {
275             assertEquals(x + i * offsetMultiplier * mStackOffset,
276                     mViews.get(i).getTranslationX(), 2f);
277             assertEquals(y, mViews.get(i).getTranslationY(), 2f);
278         }
279     }
280 
281     /** Waits up to 2 seconds for the initial stack position to be initialized. */
waitForStartPosToBeSet()282     private void waitForStartPosToBeSet() throws InterruptedException {
283         final CountDownLatch animLatch = new CountDownLatch(1);
284 
285         mCheckStartPosSet = () -> {
286             if (mStackController.getStackPosition().x >= 0) {
287                 animLatch.countDown();
288             } else {
289                 mMainThreadHandler.post(mCheckStartPosSet);
290             }
291         };
292 
293         mMainThreadHandler.post(mCheckStartPosSet);
294 
295         try {
296             animLatch.await(2, TimeUnit.SECONDS);
297         } catch (InterruptedException e) {
298             mMainThreadHandler.removeCallbacks(mCheckStartPosSet);
299             throw e;
300         }
301     }
302 
303     /**
304      * Testable version of the stack controller that dispatches its animations on the main thread.
305      */
306     private class TestableStackController extends StackAnimationController {
TestableStackController( FloatingContentCoordinator floatingContentCoordinator, IntSupplier bubbleCountSupplier, Runnable onBubbleAnimatedOutAction, Runnable onStackAnimationFinished)307         TestableStackController(
308                 FloatingContentCoordinator floatingContentCoordinator,
309                 IntSupplier bubbleCountSupplier,
310                 Runnable onBubbleAnimatedOutAction,
311                 Runnable onStackAnimationFinished) {
312             super(floatingContentCoordinator,
313                     bubbleCountSupplier,
314                     onBubbleAnimatedOutAction,
315                     onStackAnimationFinished,
316                     new TestableBubblePositioner(mContext,
317                             mContext.getSystemService(WindowManager.class)));
318         }
319 
320         @Override
flingThenSpringFirstBubbleWithStackFollowing( DynamicAnimation.ViewProperty property, float vel, float friction, SpringForce spring, Float finalPosition)321         protected void flingThenSpringFirstBubbleWithStackFollowing(
322                 DynamicAnimation.ViewProperty property, float vel, float friction,
323                 SpringForce spring, Float finalPosition) {
324             mMainThreadHandler.post(() ->
325                     super.flingThenSpringFirstBubbleWithStackFollowing(
326                             property, vel, friction, spring, finalPosition));
327         }
328 
329         @Override
springFirstBubbleWithStackFollowing(DynamicAnimation.ViewProperty property, SpringForce spring, float vel, float finalPosition, Runnable... after)330         protected void springFirstBubbleWithStackFollowing(DynamicAnimation.ViewProperty property,
331                 SpringForce spring, float vel, float finalPosition, Runnable... after) {
332             mMainThreadHandler.post(() ->
333                     super.springFirstBubbleWithStackFollowing(
334                             property, spring, vel, finalPosition, after));
335         }
336     }
337 }
338