| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.bubbles.animation; |
| |
| import static org.mockito.Mockito.when; |
| |
| import android.content.Context; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.view.DisplayCutout; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.WindowInsets; |
| import android.widget.FrameLayout; |
| |
| import androidx.dynamicanimation.animation.DynamicAnimation; |
| import androidx.dynamicanimation.animation.SpringForce; |
| |
| import com.android.systemui.R; |
| import com.android.systemui.SysuiTestCase; |
| |
| import org.junit.Before; |
| import org.mockito.Mock; |
| import org.mockito.MockitoAnnotations; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * Test case for tests that involve the {@link PhysicsAnimationLayout}. This test case constructs a |
| * testable version of the layout, and provides some helpful methods to add views to the layout and |
| * wait for physics animations to finish running. |
| * |
| * See physics-animation-testing.md. |
| */ |
| public class PhysicsAnimationLayoutTestCase extends SysuiTestCase { |
| TestablePhysicsAnimationLayout mLayout; |
| List<View> mViews = new ArrayList<>(); |
| |
| Handler mMainThreadHandler; |
| |
| int mSystemWindowInsetSize = 50; |
| int mCutoutInsetSize = 100; |
| |
| int mWidth = 1000; |
| int mHeight = 1000; |
| |
| @Mock |
| private WindowInsets mWindowInsets; |
| |
| @Mock |
| private DisplayCutout mCutout; |
| |
| private int mMaxBubbles; |
| |
| @Before |
| public void setUp() throws Exception { |
| MockitoAnnotations.initMocks(this); |
| |
| mLayout = new TestablePhysicsAnimationLayout(mContext); |
| mLayout.setLeft(0); |
| mLayout.setRight(mWidth); |
| mLayout.setTop(0); |
| mLayout.setBottom(mHeight); |
| |
| mMaxBubbles = |
| getContext().getResources().getInteger(R.integer.bubbles_max_rendered); |
| mMainThreadHandler = new Handler(Looper.getMainLooper()); |
| |
| when(mWindowInsets.getSystemWindowInsetTop()).thenReturn(mSystemWindowInsetSize); |
| when(mWindowInsets.getSystemWindowInsetBottom()).thenReturn(mSystemWindowInsetSize); |
| when(mWindowInsets.getSystemWindowInsetLeft()).thenReturn(mSystemWindowInsetSize); |
| when(mWindowInsets.getSystemWindowInsetRight()).thenReturn(mSystemWindowInsetSize); |
| |
| when(mWindowInsets.getDisplayCutout()).thenReturn(mCutout); |
| when(mCutout.getSafeInsetTop()).thenReturn(mCutoutInsetSize); |
| when(mCutout.getSafeInsetBottom()).thenReturn(mCutoutInsetSize); |
| when(mCutout.getSafeInsetLeft()).thenReturn(mCutoutInsetSize); |
| when(mCutout.getSafeInsetRight()).thenReturn(mCutoutInsetSize); |
| } |
| |
| /** Add one extra bubble over the limit, so we can make sure it's gone/chains appropriately. */ |
| void addOneMoreThanBubbleLimitBubbles() throws InterruptedException { |
| for (int i = 0; i < mMaxBubbles + 1; i++) { |
| final View newView = new FrameLayout(mContext); |
| mLayout.addView(newView, 0); |
| mViews.add(0, newView); |
| |
| newView.setTranslationX(0); |
| newView.setTranslationY(0); |
| } |
| } |
| |
| /** |
| * Uses a {@link java.util.concurrent.CountDownLatch} to wait for the given properties' |
| * animations to finish before allowing the test to proceed. |
| */ |
| void waitForPropertyAnimations(DynamicAnimation.ViewProperty... properties) |
| throws InterruptedException { |
| final CountDownLatch animLatch = new CountDownLatch(properties.length); |
| for (DynamicAnimation.ViewProperty property : properties) { |
| mLayout.setTestEndActionForProperty(animLatch::countDown, property); |
| } |
| |
| animLatch.await(2, TimeUnit.SECONDS); |
| } |
| |
| /** Uses a latch to wait for the main thread message queue to finish. */ |
| void waitForLayoutMessageQueue() throws InterruptedException { |
| CountDownLatch layoutLatch = new CountDownLatch(1); |
| mMainThreadHandler.post(layoutLatch::countDown); |
| layoutLatch.await(2, TimeUnit.SECONDS); |
| } |
| |
| /** |
| * Testable subclass of the PhysicsAnimationLayout that ensures methods that trigger animations |
| * are run on the main thread, which is a requirement of DynamicAnimation. |
| */ |
| protected class TestablePhysicsAnimationLayout extends PhysicsAnimationLayout { |
| public TestablePhysicsAnimationLayout(Context context) { |
| super(context); |
| } |
| |
| @Override |
| protected boolean isActiveController(PhysicsAnimationController controller) { |
| // Return true since otherwise all test controllers will be seen as inactive since they |
| // are wrapped by MainThreadAnimationControllerWrapper. |
| return true; |
| } |
| |
| @Override |
| public boolean post(Runnable action) { |
| return mMainThreadHandler.post(action); |
| } |
| |
| @Override |
| public boolean postDelayed(Runnable action, long delayMillis) { |
| return mMainThreadHandler.postDelayed(action, delayMillis); |
| } |
| |
| @Override |
| public void setActiveController(PhysicsAnimationController controller) { |
| runOnMainThreadAndBlock( |
| () -> super.setActiveController( |
| new MainThreadAnimationControllerWrapper(controller))); |
| } |
| |
| @Override |
| public void cancelAllAnimations() { |
| mMainThreadHandler.post(super::cancelAllAnimations); |
| } |
| |
| @Override |
| public void cancelAnimationsOnView(View view) { |
| mMainThreadHandler.post(() -> super.cancelAnimationsOnView(view)); |
| } |
| |
| @Override |
| public WindowInsets getRootWindowInsets() { |
| return mWindowInsets; |
| } |
| |
| @Override |
| public void addView(View child, int index) { |
| child.setTag(R.id.physics_animator_tag, new TestablePhysicsPropertyAnimator(child)); |
| super.addView(child, index); |
| } |
| |
| @Override |
| public void addView(View child, int index, ViewGroup.LayoutParams params) { |
| child.setTag(R.id.physics_animator_tag, new TestablePhysicsPropertyAnimator(child)); |
| super.addView(child, index, params); |
| } |
| |
| /** |
| * Sets an end action that will be called after the 'real' end action that was already set. |
| */ |
| private void setTestEndActionForProperty( |
| Runnable action, DynamicAnimation.ViewProperty property) { |
| final Runnable realEndAction = mEndActionForProperty.get(property); |
| |
| setEndActionForProperty(() -> { |
| if (realEndAction != null) { |
| realEndAction.run(); |
| } |
| |
| action.run(); |
| }, property); |
| } |
| |
| /** PhysicsPropertyAnimator that posts its animations to the main thread. */ |
| protected class TestablePhysicsPropertyAnimator extends PhysicsPropertyAnimator { |
| public TestablePhysicsPropertyAnimator(View view) { |
| super(view); |
| } |
| |
| @Override |
| protected void animateValueForChild(DynamicAnimation.ViewProperty property, View view, |
| float value, float startVel, long startDelay, float stiffness, |
| float dampingRatio, Runnable[] afterCallbacks) { |
| mMainThreadHandler.post(() -> super.animateValueForChild( |
| property, view, value, startVel, startDelay, stiffness, dampingRatio, |
| afterCallbacks)); |
| } |
| } |
| |
| /** |
| * Wrapper around an animation controller that dispatches methods that could start |
| * animations to the main thread. |
| */ |
| protected class MainThreadAnimationControllerWrapper extends PhysicsAnimationController { |
| |
| private final PhysicsAnimationController mWrappedController; |
| |
| protected MainThreadAnimationControllerWrapper(PhysicsAnimationController controller) { |
| mWrappedController = controller; |
| } |
| |
| @Override |
| protected void setLayout(PhysicsAnimationLayout layout) { |
| mWrappedController.setLayout(layout); |
| } |
| |
| @Override |
| protected PhysicsAnimationLayout getLayout() { |
| return mWrappedController.getLayout(); |
| } |
| |
| @Override |
| Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { |
| return mWrappedController.getAnimatedProperties(); |
| } |
| |
| @Override |
| int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) { |
| return mWrappedController.getNextAnimationInChain(property, index); |
| } |
| |
| @Override |
| float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) { |
| return mWrappedController.getOffsetForChainedPropertyAnimation(property); |
| } |
| |
| @Override |
| SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) { |
| return mWrappedController.getSpringForce(property, view); |
| } |
| |
| @Override |
| void onChildAdded(View child, int index) { |
| runOnMainThreadAndBlock(() -> mWrappedController.onChildAdded(child, index)); |
| } |
| |
| @Override |
| void onChildRemoved(View child, int index, Runnable finishRemoval) { |
| runOnMainThreadAndBlock( |
| () -> mWrappedController.onChildRemoved(child, index, finishRemoval)); |
| } |
| |
| @Override |
| void onChildReordered(View child, int oldIndex, int newIndex) { |
| runOnMainThreadAndBlock( |
| () -> mWrappedController.onChildReordered(child, oldIndex, newIndex)); |
| } |
| |
| @Override |
| void onActiveControllerForLayout(PhysicsAnimationLayout layout) { |
| runOnMainThreadAndBlock( |
| () -> mWrappedController.onActiveControllerForLayout(layout)); |
| } |
| |
| @Override |
| protected PhysicsPropertyAnimator animationForChild(View child) { |
| PhysicsPropertyAnimator animator = |
| (PhysicsPropertyAnimator) child.getTag(R.id.physics_animator_tag); |
| |
| if (!(animator instanceof TestablePhysicsPropertyAnimator)) { |
| animator = new TestablePhysicsPropertyAnimator(child); |
| child.setTag(R.id.physics_animator_tag, animator); |
| } |
| |
| return animator; |
| } |
| } |
| } |
| |
| /** |
| * Posts the given Runnable on the main thread, and blocks the calling thread until it's run. |
| */ |
| private void runOnMainThreadAndBlock(Runnable action) { |
| final CountDownLatch latch = new CountDownLatch(1); |
| mMainThreadHandler.post(() -> { |
| action.run(); |
| latch.countDown(); |
| }); |
| |
| try { |
| latch.await(5, TimeUnit.SECONDS); |
| } catch (InterruptedException e) { |
| e.printStackTrace(); |
| } |
| } |
| } |