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.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.systemui.R; 34 import com.android.systemui.SysuiTestCase; 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 SysuiTestCase { 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 private 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 mMainThreadHandler.post(super::cancelAllAnimations); 168 } 169 170 @Override cancelAnimationsOnView(View view)171 public void cancelAnimationsOnView(View view) { 172 mMainThreadHandler.post(() -> super.cancelAnimationsOnView(view)); 173 } 174 175 @Override getRootWindowInsets()176 public WindowInsets getRootWindowInsets() { 177 return mWindowInsets; 178 } 179 180 @Override addView(View child, int index)181 public void addView(View child, int index) { 182 child.setTag(R.id.physics_animator_tag, new TestablePhysicsPropertyAnimator(child)); 183 super.addView(child, index); 184 } 185 186 @Override addView(View child, int index, ViewGroup.LayoutParams params)187 public void addView(View child, int index, ViewGroup.LayoutParams params) { 188 child.setTag(R.id.physics_animator_tag, new TestablePhysicsPropertyAnimator(child)); 189 super.addView(child, index, params); 190 } 191 192 /** 193 * Sets an end action that will be called after the 'real' end action that was already set. 194 */ setTestEndActionForProperty( Runnable action, DynamicAnimation.ViewProperty property)195 private void setTestEndActionForProperty( 196 Runnable action, DynamicAnimation.ViewProperty property) { 197 final Runnable realEndAction = mEndActionForProperty.get(property); 198 199 setEndActionForProperty(() -> { 200 if (realEndAction != null) { 201 realEndAction.run(); 202 } 203 204 action.run(); 205 }, property); 206 } 207 208 /** PhysicsPropertyAnimator that posts its animations to the main thread. */ 209 protected class TestablePhysicsPropertyAnimator extends PhysicsPropertyAnimator { TestablePhysicsPropertyAnimator(View view)210 public TestablePhysicsPropertyAnimator(View view) { 211 super(view); 212 } 213 214 @Override animateValueForChild(DynamicAnimation.ViewProperty property, View view, float value, float startVel, long startDelay, float stiffness, float dampingRatio, Runnable[] afterCallbacks)215 protected void animateValueForChild(DynamicAnimation.ViewProperty property, View view, 216 float value, float startVel, long startDelay, float stiffness, 217 float dampingRatio, Runnable[] afterCallbacks) { 218 mMainThreadHandler.post(() -> super.animateValueForChild( 219 property, view, value, startVel, startDelay, stiffness, dampingRatio, 220 afterCallbacks)); 221 } 222 } 223 224 /** 225 * Wrapper around an animation controller that dispatches methods that could start 226 * animations to the main thread. 227 */ 228 protected class MainThreadAnimationControllerWrapper extends PhysicsAnimationController { 229 230 private final PhysicsAnimationController mWrappedController; 231 MainThreadAnimationControllerWrapper(PhysicsAnimationController controller)232 protected MainThreadAnimationControllerWrapper(PhysicsAnimationController controller) { 233 mWrappedController = controller; 234 } 235 236 @Override setLayout(PhysicsAnimationLayout layout)237 protected void setLayout(PhysicsAnimationLayout layout) { 238 mWrappedController.setLayout(layout); 239 } 240 241 @Override getLayout()242 protected PhysicsAnimationLayout getLayout() { 243 return mWrappedController.getLayout(); 244 } 245 246 @Override getAnimatedProperties()247 Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { 248 return mWrappedController.getAnimatedProperties(); 249 } 250 251 @Override getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index)252 int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) { 253 return mWrappedController.getNextAnimationInChain(property, index); 254 } 255 256 @Override getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property)257 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) { 258 return mWrappedController.getOffsetForChainedPropertyAnimation(property); 259 } 260 261 @Override getSpringForce(DynamicAnimation.ViewProperty property, View view)262 SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) { 263 return mWrappedController.getSpringForce(property, view); 264 } 265 266 @Override onChildAdded(View child, int index)267 void onChildAdded(View child, int index) { 268 runOnMainThreadAndBlock(() -> mWrappedController.onChildAdded(child, index)); 269 } 270 271 @Override onChildRemoved(View child, int index, Runnable finishRemoval)272 void onChildRemoved(View child, int index, Runnable finishRemoval) { 273 runOnMainThreadAndBlock( 274 () -> mWrappedController.onChildRemoved(child, index, finishRemoval)); 275 } 276 277 @Override onChildReordered(View child, int oldIndex, int newIndex)278 void onChildReordered(View child, int oldIndex, int newIndex) { 279 runOnMainThreadAndBlock( 280 () -> mWrappedController.onChildReordered(child, oldIndex, newIndex)); 281 } 282 283 @Override onActiveControllerForLayout(PhysicsAnimationLayout layout)284 void onActiveControllerForLayout(PhysicsAnimationLayout layout) { 285 runOnMainThreadAndBlock( 286 () -> mWrappedController.onActiveControllerForLayout(layout)); 287 } 288 289 @Override animationForChild(View child)290 protected PhysicsPropertyAnimator animationForChild(View child) { 291 PhysicsPropertyAnimator animator = 292 (PhysicsPropertyAnimator) child.getTag(R.id.physics_animator_tag); 293 294 if (!(animator instanceof TestablePhysicsPropertyAnimator)) { 295 animator = new TestablePhysicsPropertyAnimator(child); 296 child.setTag(R.id.physics_animator_tag, animator); 297 } 298 299 return animator; 300 } 301 } 302 } 303 304 /** 305 * Posts the given Runnable on the main thread, and blocks the calling thread until it's run. 306 */ runOnMainThreadAndBlock(Runnable action)307 private void runOnMainThreadAndBlock(Runnable action) { 308 final CountDownLatch latch = new CountDownLatch(1); 309 mMainThreadHandler.post(() -> { 310 action.run(); 311 latch.countDown(); 312 }); 313 314 try { 315 latch.await(5, TimeUnit.SECONDS); 316 } catch (InterruptedException e) { 317 e.printStackTrace(); 318 } 319 } 320 } 321