1 /* 2 * Copyright (C) 2022 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.accessibility.floatingmenu; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 import static org.mockito.Mockito.any; 22 import static org.mockito.Mockito.doReturn; 23 import static org.mockito.Mockito.mock; 24 import static org.mockito.Mockito.spy; 25 import static org.mockito.Mockito.verify; 26 import static org.mockito.Mockito.verifyZeroInteractions; 27 28 import android.graphics.PointF; 29 import android.testing.AndroidTestingRunner; 30 import android.testing.TestableLooper; 31 import android.view.View; 32 import android.view.ViewPropertyAnimator; 33 import android.view.WindowManager; 34 import android.view.accessibility.AccessibilityManager; 35 36 import androidx.dynamicanimation.animation.DynamicAnimation; 37 import androidx.dynamicanimation.animation.FlingAnimation; 38 import androidx.dynamicanimation.animation.SpringAnimation; 39 import androidx.dynamicanimation.animation.SpringForce; 40 import androidx.test.filters.SmallTest; 41 42 import com.android.systemui.Prefs; 43 import com.android.systemui.SysuiTestCase; 44 import com.android.systemui.util.settings.SecureSettings; 45 46 import org.junit.After; 47 import org.junit.Before; 48 import org.junit.Rule; 49 import org.junit.Test; 50 import org.junit.runner.RunWith; 51 import org.mockito.ArgumentCaptor; 52 import org.mockito.Mock; 53 import org.mockito.junit.MockitoJUnit; 54 import org.mockito.junit.MockitoRule; 55 56 import java.util.Optional; 57 58 /** Tests for {@link MenuAnimationController}. */ 59 @RunWith(AndroidTestingRunner.class) 60 @TestableLooper.RunWithLooper(setAsMainLooper = true) 61 @SmallTest 62 public class MenuAnimationControllerTest extends SysuiTestCase { 63 64 private boolean mLastIsMoveToTucked; 65 private ArgumentCaptor<DynamicAnimation.OnAnimationEndListener> mEndListenerCaptor; 66 private ViewPropertyAnimator mViewPropertyAnimator; 67 private MenuView mMenuView; 68 private TestMenuAnimationController mMenuAnimationController; 69 70 @Rule 71 public MockitoRule mockito = MockitoJUnit.rule(); 72 73 @Mock 74 private AccessibilityManager mAccessibilityManager; 75 76 @Before setUp()77 public void setUp() throws Exception { 78 final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); 79 final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, 80 stubWindowManager); 81 final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager, 82 mock(SecureSettings.class)); 83 84 mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance)); 85 mViewPropertyAnimator = spy(mMenuView.animate()); 86 doReturn(mViewPropertyAnimator).when(mMenuView).animate(); 87 88 mMenuAnimationController = new TestMenuAnimationController(mMenuView); 89 mLastIsMoveToTucked = Prefs.getBoolean(mContext, 90 Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED, /* defaultValue= */ false); 91 mEndListenerCaptor = ArgumentCaptor.forClass(DynamicAnimation.OnAnimationEndListener.class); 92 } 93 94 @After tearDown()95 public void tearDown() throws Exception { 96 Prefs.putBoolean(mContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED, 97 mLastIsMoveToTucked); 98 mEndListenerCaptor.getAllValues().clear(); 99 } 100 101 @Test moveToPosition_matchPosition()102 public void moveToPosition_matchPosition() { 103 final PointF destination = new PointF(50, 60); 104 105 mMenuAnimationController.moveToPosition(destination); 106 107 assertThat(mMenuView.getTranslationX()).isEqualTo(50); 108 assertThat(mMenuView.getTranslationY()).isEqualTo(60); 109 } 110 111 @Test startShrinkAnimation_verifyAnimationEndAction()112 public void startShrinkAnimation_verifyAnimationEndAction() { 113 mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(View.VISIBLE)); 114 115 verify(mViewPropertyAnimator).withEndAction(any(Runnable.class)); 116 } 117 118 @Test startGrowAnimation_menuCompletelyOpaque()119 public void startGrowAnimation_menuCompletelyOpaque() { 120 mMenuAnimationController.startShrinkAnimation(/* endAction= */ null); 121 122 mMenuAnimationController.startGrowAnimation(); 123 124 assertThat(mMenuView.getAlpha()).isEqualTo(/* completelyOpaque */ 1.0f); 125 } 126 127 @Test moveToEdgeAndHide_untucked_expectedSharedPreferenceValue()128 public void moveToEdgeAndHide_untucked_expectedSharedPreferenceValue() { 129 Prefs.putBoolean(mContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED, /* value= */ 130 false); 131 132 mMenuAnimationController.moveToEdgeAndHide(); 133 final boolean isMoveToTucked = Prefs.getBoolean(mContext, 134 Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED, /* defaultValue= */ false); 135 136 assertThat(isMoveToTucked).isTrue(); 137 } 138 139 @Test moveOutEdgeAndShow_tucked_expectedSharedPreferenceValue()140 public void moveOutEdgeAndShow_tucked_expectedSharedPreferenceValue() { 141 Prefs.putBoolean(mContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED, /* value= */ 142 true); 143 144 mMenuAnimationController.moveOutEdgeAndShow(); 145 final boolean isMoveToTucked = Prefs.getBoolean(mContext, 146 Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED, /* defaultValue= */ true); 147 148 assertThat(isMoveToTucked).isFalse(); 149 } 150 151 @Test startTuckedAnimationPreview_hasAnimation()152 public void startTuckedAnimationPreview_hasAnimation() { 153 mMenuView.clearAnimation(); 154 155 mMenuAnimationController.startTuckedAnimationPreview(); 156 157 assertThat(mMenuView.getAnimation()).isNotNull(); 158 } 159 160 @Test startSpringAnimationsAndEndOneAnimation_notTriggerEndAction()161 public void startSpringAnimationsAndEndOneAnimation_notTriggerEndAction() { 162 final Runnable onSpringAnimationsEndCallback = mock(Runnable.class); 163 mMenuAnimationController.setSpringAnimationsEndAction(onSpringAnimationsEndCallback); 164 165 setupAndRunSpringAnimations(); 166 final Optional<DynamicAnimation> anyAnimation = 167 mMenuAnimationController.mPositionAnimations.values().stream().findAny(); 168 anyAnimation.ifPresent(this::skipAnimationToEnd); 169 170 verifyZeroInteractions(onSpringAnimationsEndCallback); 171 } 172 173 @Test startAndEndSpringAnimations_triggerEndAction()174 public void startAndEndSpringAnimations_triggerEndAction() { 175 final Runnable onSpringAnimationsEndCallback = mock(Runnable.class); 176 mMenuAnimationController.setSpringAnimationsEndAction(onSpringAnimationsEndCallback); 177 178 setupAndRunSpringAnimations(); 179 mMenuAnimationController.mPositionAnimations.values().forEach(this::skipAnimationToEnd); 180 181 verify(onSpringAnimationsEndCallback).run(); 182 } 183 184 @Test flingThenSpringAnimationsAreEnded_triggerEndAction()185 public void flingThenSpringAnimationsAreEnded_triggerEndAction() { 186 final Runnable onSpringAnimationsEndCallback = mock(Runnable.class); 187 mMenuAnimationController.setSpringAnimationsEndAction(onSpringAnimationsEndCallback); 188 189 mMenuAnimationController.flingMenuThenSpringToEdge(/* x= */ 0, /* velocityX= */ 190 100, /* velocityY= */ 100); 191 mMenuAnimationController.mPositionAnimations.values() 192 .forEach(animation -> verify((FlingAnimation) animation).addEndListener( 193 mEndListenerCaptor.capture())); 194 mEndListenerCaptor.getAllValues() 195 .forEach(listener -> listener.onAnimationEnd(mock(DynamicAnimation.class), 196 /* canceled */ false, /* endValue */ 0, /* endVelocity */ 0)); 197 mMenuAnimationController.mPositionAnimations.values().forEach(this::skipAnimationToEnd); 198 199 verify(onSpringAnimationsEndCallback).run(); 200 } 201 202 @Test existFlingIsRunningAndTheOtherAreEnd_notTriggerEndAction()203 public void existFlingIsRunningAndTheOtherAreEnd_notTriggerEndAction() { 204 final Runnable onSpringAnimationsEndCallback = mock(Runnable.class); 205 mMenuAnimationController.setSpringAnimationsEndAction(onSpringAnimationsEndCallback); 206 207 mMenuAnimationController.flingMenuThenSpringToEdge(/* x= */ 0, /* velocityX= */ 208 200, /* velocityY= */ 200); 209 mMenuAnimationController.mPositionAnimations.values() 210 .forEach(animation -> verify((FlingAnimation) animation).addEndListener( 211 mEndListenerCaptor.capture())); 212 final Optional<DynamicAnimation.OnAnimationEndListener> anyAnimation = 213 mEndListenerCaptor.getAllValues().stream().findAny(); 214 anyAnimation.ifPresent( 215 listener -> listener.onAnimationEnd(mock(DynamicAnimation.class), /* canceled */ 216 false, /* endValue */ 0, /* endVelocity */ 0)); 217 mMenuAnimationController.mPositionAnimations.values() 218 .stream() 219 .filter(animation -> animation instanceof SpringAnimation) 220 .forEach(this::skipAnimationToEnd); 221 222 verifyZeroInteractions(onSpringAnimationsEndCallback); 223 } 224 setupAndRunSpringAnimations()225 private void setupAndRunSpringAnimations() { 226 final float stiffness = 700f; 227 final float dampingRatio = 0.85f; 228 final float velocity = 100f; 229 final float finalPosition = 300f; 230 231 mMenuAnimationController.springMenuWith(DynamicAnimation.TRANSLATION_X, new SpringForce() 232 .setStiffness(stiffness) 233 .setDampingRatio(dampingRatio), velocity, finalPosition); 234 mMenuAnimationController.springMenuWith(DynamicAnimation.TRANSLATION_Y, new SpringForce() 235 .setStiffness(stiffness) 236 .setDampingRatio(dampingRatio), velocity, finalPosition); 237 } 238 skipAnimationToEnd(DynamicAnimation animation)239 private void skipAnimationToEnd(DynamicAnimation animation) { 240 final SpringAnimation springAnimation = ((SpringAnimation) animation); 241 // The doAnimationFrame function is used for skipping animation to the end. 242 springAnimation.doAnimationFrame(100); 243 springAnimation.skipToEnd(); 244 springAnimation.doAnimationFrame(200); 245 } 246 247 /** 248 * Wrapper class for testing. 249 */ 250 private static class TestMenuAnimationController extends MenuAnimationController { TestMenuAnimationController(MenuView menuView)251 TestMenuAnimationController(MenuView menuView) { 252 super(menuView); 253 } 254 255 @Override createFlingAnimation(MenuView menuView, MenuPositionProperty menuPositionProperty)256 FlingAnimation createFlingAnimation(MenuView menuView, 257 MenuPositionProperty menuPositionProperty) { 258 return spy(super.createFlingAnimation(menuView, menuPositionProperty)); 259 } 260 } 261 } 262