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