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