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 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 mLayout.mEndActionForProperty.put(property, () -> { 199 if (realEndAction != null) { 200 realEndAction.run(); 201 } 202 203 action.run(); 204 }); 205 } 206 207 /** PhysicsPropertyAnimator that posts its animations to the main thread. */ 208 protected class TestablePhysicsPropertyAnimator extends PhysicsPropertyAnimator { TestablePhysicsPropertyAnimator(View view)209 public TestablePhysicsPropertyAnimator(View view) { 210 super(view); 211 } 212 213 @Override animateValueForChild(DynamicAnimation.ViewProperty property, View view, float value, float startVel, long startDelay, float stiffness, float dampingRatio, Runnable[] afterCallbacks)214 protected void animateValueForChild(DynamicAnimation.ViewProperty property, View view, 215 float value, float startVel, long startDelay, float stiffness, 216 float dampingRatio, Runnable[] afterCallbacks) { 217 mMainThreadHandler.post(() -> super.animateValueForChild( 218 property, view, value, startVel, startDelay, stiffness, dampingRatio, 219 afterCallbacks)); 220 } 221 222 @Override startPathAnimation()223 protected void startPathAnimation() { 224 mMainThreadHandler.post(super::startPathAnimation); 225 } 226 } 227 228 /** 229 * Wrapper around an animation controller that dispatches methods that could start 230 * animations to the main thread. 231 */ 232 protected class MainThreadAnimationControllerWrapper extends PhysicsAnimationController { 233 234 private final PhysicsAnimationController mWrappedController; 235 MainThreadAnimationControllerWrapper(PhysicsAnimationController controller)236 protected MainThreadAnimationControllerWrapper(PhysicsAnimationController controller) { 237 mWrappedController = controller; 238 } 239 240 @Override setLayout(PhysicsAnimationLayout layout)241 protected void setLayout(PhysicsAnimationLayout layout) { 242 mWrappedController.setLayout(layout); 243 } 244 245 @Override getLayout()246 protected PhysicsAnimationLayout getLayout() { 247 return mWrappedController.getLayout(); 248 } 249 250 @Override getAnimatedProperties()251 Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { 252 return mWrappedController.getAnimatedProperties(); 253 } 254 255 @Override getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index)256 int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) { 257 return mWrappedController.getNextAnimationInChain(property, index); 258 } 259 260 @Override getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property, int index)261 float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property, 262 int index) { 263 return mWrappedController.getOffsetForChainedPropertyAnimation(property, index); 264 } 265 266 @Override getSpringForce(DynamicAnimation.ViewProperty property, View view)267 SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) { 268 return mWrappedController.getSpringForce(property, view); 269 } 270 271 @Override onChildAdded(View child, int index)272 void onChildAdded(View child, int index) { 273 runOnMainThreadAndBlock(() -> mWrappedController.onChildAdded(child, index)); 274 } 275 276 @Override onChildRemoved(View child, int index, Runnable finishRemoval)277 void onChildRemoved(View child, int index, Runnable finishRemoval) { 278 runOnMainThreadAndBlock( 279 () -> mWrappedController.onChildRemoved(child, index, finishRemoval)); 280 } 281 282 @Override onChildReordered(View child, int oldIndex, int newIndex)283 void onChildReordered(View child, int oldIndex, int newIndex) { 284 runOnMainThreadAndBlock( 285 () -> mWrappedController.onChildReordered(child, oldIndex, newIndex)); 286 } 287 288 @Override onActiveControllerForLayout(PhysicsAnimationLayout layout)289 void onActiveControllerForLayout(PhysicsAnimationLayout layout) { 290 runOnMainThreadAndBlock( 291 () -> mWrappedController.onActiveControllerForLayout(layout)); 292 } 293 294 @Override animationForChild(View child)295 protected PhysicsPropertyAnimator animationForChild(View child) { 296 PhysicsPropertyAnimator animator = 297 (PhysicsPropertyAnimator) child.getTag(R.id.physics_animator_tag); 298 299 if (!(animator instanceof TestablePhysicsPropertyAnimator)) { 300 animator = new TestablePhysicsPropertyAnimator(child); 301 child.setTag(R.id.physics_animator_tag, animator); 302 } 303 304 return animator; 305 } 306 } 307 } 308 309 /** 310 * Posts the given Runnable on the main thread, and blocks the calling thread until it's run. 311 */ runOnMainThreadAndBlock(Runnable action)312 private void runOnMainThreadAndBlock(Runnable action) { 313 final CountDownLatch latch = new CountDownLatch(1); 314 mMainThreadHandler.post(() -> { 315 action.run(); 316 latch.countDown(); 317 }); 318 319 try { 320 latch.await(5, TimeUnit.SECONDS); 321 } catch (InterruptedException e) { 322 e.printStackTrace(); 323 } 324 } 325 } 326