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