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.mockito.Mockito.when;
20 
21 import android.content.Context;
22 import android.os.Handler;
23 import android.os.Looper;
24 import android.view.DisplayCutout;
25 import android.view.View;
26 import android.view.ViewGroup;
27 import android.view.WindowInsets;
28 import android.widget.FrameLayout;
29 
30 import androidx.dynamicanimation.animation.DynamicAnimation;
31 import androidx.dynamicanimation.animation.SpringForce;
32 
33 import com.android.wm.shell.R;
34 import com.android.wm.shell.ShellTestCase;
35 
36 import org.junit.Before;
37 import org.mockito.Mock;
38 import org.mockito.MockitoAnnotations;
39 
40 import java.util.ArrayList;
41 import java.util.List;
42 import java.util.Set;
43 import java.util.concurrent.CountDownLatch;
44 import java.util.concurrent.TimeUnit;
45 
46 /**
47  * Test case for tests that involve the {@link PhysicsAnimationLayout}. This test case constructs a
48  * testable version of the layout, and provides some helpful methods to add views to the layout and
49  * wait for physics animations to finish running.
50  *
51  * See physics-animation-testing.md.
52  */
53 public class PhysicsAnimationLayoutTestCase extends ShellTestCase {
54     TestablePhysicsAnimationLayout mLayout;
55     List<View> mViews = new ArrayList<>();
56 
57     Handler mMainThreadHandler;
58 
59     int mSystemWindowInsetSize = 50;
60     int mCutoutInsetSize = 100;
61 
62     int mWidth = 1000;
63     int mHeight = 1000;
64 
65     @Mock
66     private WindowInsets mWindowInsets;
67 
68     @Mock
69     private DisplayCutout mCutout;
70 
71     protected int mMaxBubbles;
72 
73     @Before
setUp()74     public void setUp() throws Exception {
75         MockitoAnnotations.initMocks(this);
76 
77         mLayout = new TestablePhysicsAnimationLayout(mContext);
78         mLayout.setLeft(0);
79         mLayout.setRight(mWidth);
80         mLayout.setTop(0);
81         mLayout.setBottom(mHeight);
82 
83         mMaxBubbles =
84                 getContext().getResources().getInteger(R.integer.bubbles_max_rendered);
85         mMainThreadHandler = new Handler(Looper.getMainLooper());
86 
87         when(mWindowInsets.getSystemWindowInsetTop()).thenReturn(mSystemWindowInsetSize);
88         when(mWindowInsets.getSystemWindowInsetBottom()).thenReturn(mSystemWindowInsetSize);
89         when(mWindowInsets.getSystemWindowInsetLeft()).thenReturn(mSystemWindowInsetSize);
90         when(mWindowInsets.getSystemWindowInsetRight()).thenReturn(mSystemWindowInsetSize);
91 
92         when(mWindowInsets.getDisplayCutout()).thenReturn(mCutout);
93         when(mCutout.getSafeInsetTop()).thenReturn(mCutoutInsetSize);
94         when(mCutout.getSafeInsetBottom()).thenReturn(mCutoutInsetSize);
95         when(mCutout.getSafeInsetLeft()).thenReturn(mCutoutInsetSize);
96         when(mCutout.getSafeInsetRight()).thenReturn(mCutoutInsetSize);
97     }
98 
99     /** Add one extra bubble over the limit, so we can make sure it's gone/chains appropriately. */
addOneMoreThanBubbleLimitBubbles()100     void addOneMoreThanBubbleLimitBubbles() throws InterruptedException {
101         for (int i = 0; i < mMaxBubbles + 1; i++) {
102             final View newView = new FrameLayout(mContext);
103             mLayout.addView(newView, 0);
104             mViews.add(0, newView);
105 
106             newView.setTranslationX(0);
107             newView.setTranslationY(0);
108         }
109     }
110 
111     /**
112      * Uses a {@link java.util.concurrent.CountDownLatch} to wait for the given properties'
113      * animations to finish before allowing the test to proceed.
114      */
waitForPropertyAnimations(DynamicAnimation.ViewProperty... properties)115     void waitForPropertyAnimations(DynamicAnimation.ViewProperty... properties)
116             throws InterruptedException {
117         final CountDownLatch animLatch = new CountDownLatch(properties.length);
118         for (DynamicAnimation.ViewProperty property : properties) {
119             mLayout.setTestEndActionForProperty(animLatch::countDown, property);
120         }
121 
122         animLatch.await(2, TimeUnit.SECONDS);
123     }
124 
125     /** Uses a latch to wait for the main thread message queue to finish. */
waitForLayoutMessageQueue()126     void waitForLayoutMessageQueue() throws InterruptedException {
127         CountDownLatch layoutLatch = new CountDownLatch(1);
128         mMainThreadHandler.post(layoutLatch::countDown);
129         layoutLatch.await(2, TimeUnit.SECONDS);
130     }
131 
132     /**
133      * Testable subclass of the PhysicsAnimationLayout that ensures methods that trigger animations
134      * are run on the main thread, which is a requirement of DynamicAnimation.
135      */
136     protected class TestablePhysicsAnimationLayout extends PhysicsAnimationLayout {
TestablePhysicsAnimationLayout(Context context)137         public TestablePhysicsAnimationLayout(Context context) {
138             super(context);
139         }
140 
141         @Override
isActiveController(PhysicsAnimationController controller)142         protected boolean isActiveController(PhysicsAnimationController controller) {
143             // Return true since otherwise all test controllers will be seen as inactive since they
144             // are wrapped by MainThreadAnimationControllerWrapper.
145             return true;
146         }
147 
148         @Override
post(Runnable action)149         public boolean post(Runnable action) {
150             return mMainThreadHandler.post(action);
151         }
152 
153         @Override
postDelayed(Runnable action, long delayMillis)154         public boolean postDelayed(Runnable action, long delayMillis) {
155             return mMainThreadHandler.postDelayed(action, delayMillis);
156         }
157 
158         @Override
setActiveController(PhysicsAnimationController controller)159         public void setActiveController(PhysicsAnimationController controller) {
160             runOnMainThreadAndBlock(
161                     () -> super.setActiveController(
162                             new MainThreadAnimationControllerWrapper(controller)));
163         }
164 
165         @Override
cancelAllAnimations()166         public void cancelAllAnimations() {
167             if (mLayout.getChildCount() == 0) {
168                 return;
169             }
170             mMainThreadHandler.post(super::cancelAllAnimations);
171         }
172 
173         @Override
cancelAnimationsOnView(View view)174         public void cancelAnimationsOnView(View view) {
175             if (mLayout.getChildCount() == 0) {
176                 return;
177             }
178             mMainThreadHandler.post(() -> super.cancelAnimationsOnView(view));
179         }
180 
181         @Override
getRootWindowInsets()182         public WindowInsets getRootWindowInsets() {
183             return mWindowInsets;
184         }
185 
186         @Override
addView(View child, int index)187         public void addView(View child, int index) {
188             child.setTag(R.id.physics_animator_tag, new TestablePhysicsPropertyAnimator(child));
189             super.addView(child, index);
190         }
191 
192         @Override
addView(View child, int index, ViewGroup.LayoutParams params)193         public void addView(View child, int index, ViewGroup.LayoutParams params) {
194             child.setTag(R.id.physics_animator_tag, new TestablePhysicsPropertyAnimator(child));
195             super.addView(child, index, params);
196         }
197 
198         /**
199          * Sets an end action that will be called after the 'real' end action that was already set.
200          */
setTestEndActionForProperty( Runnable action, DynamicAnimation.ViewProperty property)201         private void setTestEndActionForProperty(
202                 Runnable action, DynamicAnimation.ViewProperty property) {
203             final Runnable realEndAction = mEndActionForProperty.get(property);
204             mLayout.mEndActionForProperty.put(property, () -> {
205                 if (realEndAction != null) {
206                     realEndAction.run();
207                 }
208 
209                 action.run();
210             });
211         }
212 
213         /** PhysicsPropertyAnimator that posts its animations to the main thread. */
214         protected class TestablePhysicsPropertyAnimator extends PhysicsPropertyAnimator {
TestablePhysicsPropertyAnimator(View view)215             public TestablePhysicsPropertyAnimator(View view) {
216                 super(view);
217             }
218 
219             @Override
animateValueForChild(DynamicAnimation.ViewProperty property, View view, float value, float startVel, long startDelay, float stiffness, float dampingRatio, Runnable[] afterCallbacks)220             protected void animateValueForChild(DynamicAnimation.ViewProperty property, View view,
221                     float value, float startVel, long startDelay, float stiffness,
222                     float dampingRatio, Runnable[] afterCallbacks) {
223                 mMainThreadHandler.post(() -> super.animateValueForChild(
224                         property, view, value, startVel, startDelay, stiffness, dampingRatio,
225                         afterCallbacks));
226             }
227 
228             @Override
startPathAnimation()229             protected void startPathAnimation() {
230                 if (mLayout.getChildCount() == 0) {
231                     return;
232                 }
233                 mMainThreadHandler.post(super::startPathAnimation);
234             }
235         }
236 
237         /**
238          * Wrapper around an animation controller that dispatches methods that could start
239          * animations to the main thread.
240          */
241         protected class MainThreadAnimationControllerWrapper extends PhysicsAnimationController {
242 
243             private final PhysicsAnimationController mWrappedController;
244 
MainThreadAnimationControllerWrapper(PhysicsAnimationController controller)245             protected MainThreadAnimationControllerWrapper(PhysicsAnimationController controller) {
246                 mWrappedController = controller;
247             }
248 
249             @Override
setLayout(PhysicsAnimationLayout layout)250             protected void setLayout(PhysicsAnimationLayout layout) {
251                 mWrappedController.setLayout(layout);
252             }
253 
254             @Override
getLayout()255             protected PhysicsAnimationLayout getLayout() {
256                 return mWrappedController.getLayout();
257             }
258 
259             @Override
getAnimatedProperties()260             Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
261                 return mWrappedController.getAnimatedProperties();
262             }
263 
264             @Override
getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index)265             int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
266                 return mWrappedController.getNextAnimationInChain(property, index);
267             }
268 
269             @Override
getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property, int index)270             float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property,
271                     int index) {
272                 return mWrappedController.getOffsetForChainedPropertyAnimation(property, index);
273             }
274 
275             @Override
getSpringForce(DynamicAnimation.ViewProperty property, View view)276             SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
277                 return mWrappedController.getSpringForce(property, view);
278             }
279 
280             @Override
onChildAdded(View child, int index)281             void onChildAdded(View child, int index) {
282                 runOnMainThreadAndBlock(() -> mWrappedController.onChildAdded(child, index));
283             }
284 
285             @Override
onChildRemoved(View child, int index, Runnable finishRemoval)286             void onChildRemoved(View child, int index, Runnable finishRemoval) {
287                 runOnMainThreadAndBlock(
288                         () -> mWrappedController.onChildRemoved(child, index, finishRemoval));
289             }
290 
291             @Override
onChildReordered(View child, int oldIndex, int newIndex)292             void onChildReordered(View child, int oldIndex, int newIndex) {
293                 runOnMainThreadAndBlock(
294                         () -> mWrappedController.onChildReordered(child, oldIndex, newIndex));
295             }
296 
297             @Override
onActiveControllerForLayout(PhysicsAnimationLayout layout)298             void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
299                 runOnMainThreadAndBlock(
300                         () -> mWrappedController.onActiveControllerForLayout(layout));
301             }
302 
303             @Override
animationForChild(View child)304             protected PhysicsPropertyAnimator animationForChild(View child) {
305                 PhysicsPropertyAnimator animator =
306                         (PhysicsPropertyAnimator) child.getTag(R.id.physics_animator_tag);
307 
308                 if (!(animator instanceof TestablePhysicsPropertyAnimator)) {
309                     animator = new TestablePhysicsPropertyAnimator(child);
310                     child.setTag(R.id.physics_animator_tag, animator);
311                 }
312 
313                 return animator;
314             }
315         }
316     }
317 
318     /**
319      * Posts the given Runnable on the main thread, and blocks the calling thread until it's run.
320      */
runOnMainThreadAndBlock(Runnable action)321     private void runOnMainThreadAndBlock(Runnable action) {
322         final CountDownLatch latch = new CountDownLatch(1);
323         mMainThreadHandler.post(() -> {
324             action.run();
325             latch.countDown();
326         });
327 
328         try {
329             latch.await(5, TimeUnit.SECONDS);
330         } catch (InterruptedException e) {
331             e.printStackTrace();
332         }
333     }
334 
335     /** Waits for the main thread to finish processing all pending runnables. */
waitForMainThread()336     public void waitForMainThread() {
337         runOnMainThreadAndBlock(() -> {});
338     }
339 }
340