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