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