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