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