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