Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 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; |
Joshua Tsuji | 4accf598 | 2019-04-22 17:36:11 -0400 | [diff] [blame] | 20 | import static org.junit.Assert.assertNotEquals; |
| 21 | import static org.mockito.Mockito.verify; |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 22 | |
| 23 | import android.graphics.PointF; |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 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; |
Brett Chabot | 84151d9 | 2019-02-27 15:37:59 -0800 | [diff] [blame] | 30 | import androidx.test.filters.SmallTest; |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 31 | |
| 32 | import com.android.systemui.R; |
| 33 | |
| 34 | import org.junit.Before; |
Joshua Tsuji | 87ebd74 | 2019-01-25 16:01:26 -0500 | [diff] [blame] | 35 | import org.junit.Ignore; |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 36 | import org.junit.Test; |
| 37 | import org.junit.runner.RunWith; |
Joshua Tsuji | 4accf598 | 2019-04-22 17:36:11 -0400 | [diff] [blame] | 38 | import org.mockito.Mockito; |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 39 | import org.mockito.Spy; |
| 40 | |
Joshua Tsuji | 33c0e9c | 2019-05-14 16:45:39 -0400 | [diff] [blame] | 41 | import java.util.concurrent.CountDownLatch; |
| 42 | import java.util.concurrent.TimeUnit; |
| 43 | |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 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; |
Joshua Tsuji | 33c0e9c | 2019-05-14 16:45:39 -0400 | [diff] [blame] | 52 | private Runnable mCheckStartPosSet; |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 53 | |
| 54 | @Before |
| 55 | public void setUp() throws Exception { |
| 56 | super.setUp(); |
Joshua Tsuji | c36ee6f | 2019-05-28 17:00:16 -0400 | [diff] [blame] | 57 | mLayout.setActiveController(mStackController); |
Joshua Tsuji | cb97a11 | 2019-05-29 16:20:41 -0400 | [diff] [blame] | 58 | addOneMoreThanBubbleLimitBubbles(); |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 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 |
Joshua Tsuji | e567f07 | 2019-04-03 12:37:26 -0400 | [diff] [blame] | 67 | @Ignore("Flaking") |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 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 |
Joshua Tsuji | 87ebd74 | 2019-01-25 16:01:26 -0500 | [diff] [blame] | 101 | @Ignore("Sporadically failing due to DynamicAnimation not settling.") |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 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 |
Joshua Tsuji | 87ebd74 | 2019-01-25 16:01:26 -0500 | [diff] [blame] | 132 | @Ignore("Sporadically failing due to DynamicAnimation not settling.") |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 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 |
Joshua Tsuji | b8a6717 | 2019-05-18 23:44:17 -0400 | [diff] [blame] | 161 | @Ignore("Flaking") |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 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 | |
Joshua Tsuji | 33c0e9c | 2019-05-14 16:45:39 -0400 | [diff] [blame] | 174 | waitForStartPosToBeSet(); |
| 175 | waitForLayoutMessageQueue(); |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 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 |
Joshua Tsuji | ed386f2 | 2019-03-06 23:59:22 -0500 | [diff] [blame] | 191 | @Ignore("Occasionally flakes, ignoring pending investigation.") |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 192 | public void testChildRemoved() throws InterruptedException { |
Joshua Tsuji | a08b6d3 | 2019-01-29 16:15:52 -0500 | [diff] [blame] | 193 | assertEquals(0, mLayout.getTransientViewCount()); |
| 194 | |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 195 | final View firstView = mLayout.getChildAt(0); |
| 196 | mLayout.removeView(firstView); |
| 197 | |
Joshua Tsuji | a08b6d3 | 2019-01-29 16:15:52 -0500 | [diff] [blame] | 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)); |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 201 | |
| 202 | waitForPropertyAnimations(DynamicAnimation.ALPHA); |
| 203 | waitForLayoutMessageQueue(); |
| 204 | |
Joshua Tsuji | a08b6d3 | 2019-01-29 16:15:52 -0500 | [diff] [blame] | 205 | // The view should now be gone entirely, no transient views left. |
| 206 | assertEquals(0, mLayout.getTransientViewCount()); |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 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 | |
Joshua Tsuji | e13b4fc | 2019-02-28 18:39:57 -0500 | [diff] [blame] | 212 | @Test |
Joshua Tsuji | 92e7f78 | 2019-04-02 11:56:21 -0400 | [diff] [blame] | 213 | @Ignore("Flaky") |
Joshua Tsuji | e13b4fc | 2019-02-28 18:39:57 -0500 | [diff] [blame] | 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(); |
Joshua Tsuji | c110843 | 2019-02-22 16:10:12 -0500 | [diff] [blame] | 224 | |
| 225 | waitForLayoutMessageQueue(); |
| 226 | |
Joshua Tsuji | e13b4fc | 2019-02-28 18:39:57 -0500 | [diff] [blame] | 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 | |
Joshua Tsuji | 4accf598 | 2019-04-22 17:36:11 -0400 | [diff] [blame] | 236 | @Test |
Joshua Tsuji | 16a4abf | 2019-05-22 13:03:58 -0400 | [diff] [blame] | 237 | @Ignore("Flaky") |
Joshua Tsuji | 4accf598 | 2019-04-22 17:36:11 -0400 | [diff] [blame] | 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 |
Joshua Tsuji | b8a6717 | 2019-05-18 23:44:17 -0400 | [diff] [blame] | 257 | @Ignore("Flaking") |
Joshua Tsuji | 4accf598 | 2019-04-22 17:36:11 -0400 | [diff] [blame] | 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 | |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 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 | */ |
| 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 | |
Joshua Tsuji | 33c0e9c | 2019-05-14 16:45:39 -0400 | [diff] [blame] | 305 | /** Waits up to 2 seconds for the initial stack position to be initialized. */ |
| 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 | |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 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 |
Joshua Tsuji | e13b4fc | 2019-02-28 18:39:57 -0500 | [diff] [blame] | 332 | protected void flingThenSpringFirstBubbleWithStackFollowing( |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 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 | } |
Joshua Tsuji | 4accf598 | 2019-04-22 17:36:11 -0400 | [diff] [blame] | 339 | |
| 340 | @Override |
| 341 | protected void springFirstBubbleWithStackFollowing(DynamicAnimation.ViewProperty property, |
Joshua Tsuji | af8df2d | 2019-08-01 16:08:01 -0400 | [diff] [blame] | 342 | SpringForce spring, float vel, float finalPosition, Runnable... after) { |
Joshua Tsuji | 4accf598 | 2019-04-22 17:36:11 -0400 | [diff] [blame] | 343 | mMainThreadHandler.post(() -> |
| 344 | super.springFirstBubbleWithStackFollowing( |
Joshua Tsuji | af8df2d | 2019-08-01 16:08:01 -0400 | [diff] [blame] | 345 | property, spring, vel, finalPosition, after)); |
Joshua Tsuji | 4accf598 | 2019-04-22 17:36:11 -0400 | [diff] [blame] | 346 | } |
Joshua Tsuji | b1a796b | 2019-01-16 15:43:12 -0800 | [diff] [blame] | 347 | } |
| 348 | } |