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