• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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