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