• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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 android.view;
18 
19 import static android.view.WindowInsets.Type.ime;
20 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN;
21 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
22 import static android.window.BackEvent.EDGE_LEFT;
23 
24 import static org.mockito.ArgumentMatchers.any;
25 import static org.mockito.ArgumentMatchers.anyBoolean;
26 import static org.mockito.ArgumentMatchers.anyFloat;
27 import static org.mockito.ArgumentMatchers.anyInt;
28 import static org.mockito.ArgumentMatchers.anyLong;
29 import static org.mockito.ArgumentMatchers.eq;
30 import static org.mockito.Mockito.mock;
31 import static org.mockito.Mockito.never;
32 import static org.mockito.Mockito.times;
33 import static org.mockito.Mockito.verify;
34 import static org.mockito.Mockito.when;
35 import static org.testng.Assert.assertEquals;
36 
37 import android.app.WindowConfiguration;
38 import android.content.Context;
39 import android.graphics.Insets;
40 import android.platform.test.annotations.Presubmit;
41 import android.util.SparseArray;
42 import android.view.animation.BackGestureInterpolator;
43 import android.view.animation.Interpolator;
44 import android.view.inputmethod.InputMethodManager;
45 import android.widget.TextView;
46 import android.window.BackEvent;
47 
48 import androidx.test.ext.junit.runners.AndroidJUnit4;
49 import androidx.test.platform.app.InstrumentationRegistry;
50 
51 import org.junit.Before;
52 import org.junit.Test;
53 import org.junit.runner.RunWith;
54 import org.mockito.ArgumentCaptor;
55 import org.mockito.Mock;
56 import org.mockito.Mockito;
57 import org.mockito.MockitoAnnotations;
58 
59 import java.lang.reflect.Field;
60 
61 /**
62  * Tests for {@link ImeBackAnimationController}.
63  *
64  * <p>Build/Install/Run:
65  * atest FrameworksCoreTests:ImeBackAnimationControllerTest
66  *
67  * <p>This test class is a part of Window Manager Service tests and specified in
68  * {@link com.android.server.wm.test.filters.FrameworksTestsFilter}.
69  */
70 @Presubmit
71 @RunWith(AndroidJUnit4.class)
72 public class ImeBackAnimationControllerTest {
73 
74     private static final float PEEK_FRACTION = 0.1f;
75     private static final Interpolator BACK_GESTURE = new BackGestureInterpolator();
76     private static final int IME_HEIGHT = 200;
77     private static final Insets IME_INSETS = Insets.of(0, 0, 0, IME_HEIGHT);
78 
79     @Mock
80     private InsetsController mInsetsController;
81     @Mock
82     private WindowInsetsAnimationController mWindowInsetsAnimationController;
83     @Mock
84     private ViewRootInsetsControllerHost mViewRootInsetsControllerHost;
85 
86     private ViewRootImpl mViewRoot;
87     private ImeBackAnimationController mBackAnimationController;
88 
89     @Before
setup()90     public void setup() {
91         MockitoAnnotations.initMocks(this);
92         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
93             Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
94             InputMethodManager inputMethodManager = context.getSystemService(
95                     InputMethodManager.class);
96             // cannot mock ViewRootImpl since it's final.
97             mViewRoot = new ViewRootImpl(context, context.getDisplayNoVerify());
98             try {
99                 mViewRoot.setView(new TextView(context), new WindowManager.LayoutParams(), null);
100             } catch (WindowManager.BadTokenException e) {
101                 // activity isn't running, we will ignore BadTokenException.
102             }
103             mViewRoot.setOnContentApplyWindowInsetsListener(
104                     mock(Window.OnContentApplyWindowInsetsListener.class));
105             mBackAnimationController = new ImeBackAnimationController(mViewRoot, mInsetsController);
106             mViewRoot.mContext.getResources().getConfiguration().windowConfiguration
107                     .setWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN);
108 
109             when(mWindowInsetsAnimationController.getHiddenStateInsets()).thenReturn(Insets.NONE);
110             when(mWindowInsetsAnimationController.getShownStateInsets()).thenReturn(IME_INSETS);
111             when(mWindowInsetsAnimationController.getCurrentInsets()).thenReturn(IME_INSETS);
112             when(mInsetsController.getHost()).thenReturn(mViewRootInsetsControllerHost);
113             when(mViewRootInsetsControllerHost.getInputMethodManager()).thenReturn(
114                     inputMethodManager);
115             try {
116                 Field field = InsetsController.class.getDeclaredField("mSourceConsumers");
117                 field.setAccessible(true);
118                 field.set(mInsetsController, new SparseArray<InsetsSourceConsumer>());
119             } catch (NoSuchFieldException | IllegalAccessException e) {
120                 throw new RuntimeException("Unable to set mSourceConsumers", e);
121             }
122         });
123         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
124     }
125 
126     @Test
testAdjustResizeWithAppWindowInsetsListenerPlaysAnim()127     public void testAdjustResizeWithAppWindowInsetsListenerPlaysAnim() {
128         // setup ViewRoot with InsetsAnimationCallback and softInputMode=adjustResize
129         mViewRoot.getView()
130                 .setWindowInsetsAnimationCallback(mock(WindowInsetsAnimation.Callback.class));
131         mViewRoot.mWindowAttributes.softInputMode = SOFT_INPUT_ADJUST_RESIZE;
132         // start back gesture
133         mBackAnimationController.onBackStarted(new BackEvent(0f, 0f, 0f, EDGE_LEFT));
134         // verify that ImeBackAnimationController takes control over IME insets
135         verify(mInsetsController, times(1)).controlWindowInsetsAnimation(anyInt(), any(), any(),
136                 anyBoolean(), anyLong(), any(), anyInt(), anyBoolean());
137     }
138 
139     @Test
testAdjustResizeWithEdgeToEdgePlaysAnim()140     public void testAdjustResizeWithEdgeToEdgePlaysAnim() {
141         // set OnContentApplyWindowInsetsListener to null (to simulate edge-to-edge enabled) and
142         // softInputMode=adjustResize
143         mViewRoot.mWindowAttributes.softInputMode = SOFT_INPUT_ADJUST_RESIZE;
144         mViewRoot.setOnContentApplyWindowInsetsListener(null);
145         // start back gesture
146         mBackAnimationController.onBackStarted(new BackEvent(0f, 0f, 0f, EDGE_LEFT));
147         // verify that ImeBackAnimationController takes control over IME insets
148         verify(mInsetsController, times(1)).controlWindowInsetsAnimation(anyInt(), any(), any(),
149                 anyBoolean(), anyLong(), any(), anyInt(), anyBoolean());
150     }
151 
152     @Test
testAdjustResizeWithoutAppWindowInsetsListenerNotPlayingAnim()153     public void testAdjustResizeWithoutAppWindowInsetsListenerNotPlayingAnim() {
154         // setup ViewRoot with softInputMode=adjustResize
155         mViewRoot.mWindowAttributes.softInputMode = SOFT_INPUT_ADJUST_RESIZE;
156         // start back gesture
157         mBackAnimationController.onBackStarted(new BackEvent(0f, 0f, 0f, EDGE_LEFT));
158         // progress back gesture
159         mBackAnimationController.onBackProgressed(new BackEvent(100f, 0f, 0.5f, EDGE_LEFT));
160         // commit back gesture
161         mBackAnimationController.onBackInvoked();
162         // verify that InputMethodManager#notifyImeHidden is called (which is the case whenever
163         // getInputMethodManager is called from ImeBackAnimationController)
164         verify(mViewRootInsetsControllerHost, times(2)).getInputMethodManager();
165         // verify that ImeBackAnimationController does not take control over IME insets
166         verify(mInsetsController, never()).controlWindowInsetsAnimation(anyInt(), any(), any(),
167                 anyBoolean(), anyLong(), any(), anyInt(), anyBoolean());
168     }
169 
170     @Test
testMultiWindowModeNotPlayingAnim()171     public void testMultiWindowModeNotPlayingAnim() {
172         // setup ViewRoot with WINDOWING_MODE_MULTI_WINDOW
173         mViewRoot.mContext.getResources().getConfiguration().windowConfiguration.setWindowingMode(
174                 WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW);
175         // start back gesture
176         mBackAnimationController.onBackStarted(new BackEvent(0f, 0f, 0f, EDGE_LEFT));
177         // progress back gesture
178         mBackAnimationController.onBackProgressed(new BackEvent(100f, 0f, 0.5f, EDGE_LEFT));
179         // commit back gesture
180         mBackAnimationController.onBackInvoked();
181         // verify that InputMethodManager#notifyImeHidden is called (which is the case whenever
182         // getInputMethodManager is called from ImeBackAnimationController)
183         verify(mViewRootInsetsControllerHost, times(2)).getInputMethodManager();
184         // verify that ImeBackAnimationController does not take control over IME insets
185         verify(mInsetsController, never()).controlWindowInsetsAnimation(anyInt(), any(), any(),
186                 anyBoolean(), anyLong(), any(), anyInt(), anyBoolean());
187     }
188 
189     @Test
testAdjustPanScrollsViewRoot()190     public void testAdjustPanScrollsViewRoot() {
191         // simulate view root being panned upwards by 50px
192         int appPan = -50;
193         mViewRoot.setScrollY(appPan);
194         // setup ViewRoot with softInputMode=adjustPan
195         mViewRoot.mWindowAttributes.softInputMode = SOFT_INPUT_ADJUST_PAN;
196 
197         // start back gesture
198         WindowInsetsAnimationControlListener animationControlListener = startBackGesture();
199         // simulate ImeBackAnimationController receiving control
200         animationControlListener.onReady(mWindowInsetsAnimationController, ime());
201 
202         // progress back gesture
203         float progress = 0.5f;
204         mBackAnimationController.onBackProgressed(new BackEvent(100f, 0f, progress, EDGE_LEFT));
205 
206         // verify that view root is scrolled by expected amount
207         float interpolatedProgress = BACK_GESTURE.getInterpolation(progress);
208         int expectedViewRootScroll =
209                 (int) (appPan * (1 - interpolatedProgress * PEEK_FRACTION));
210         assertEquals(mViewRoot.getScrollY(), expectedViewRootScroll);
211     }
212 
213     @Test
testNewGestureAfterCancelSeamlessTakeover()214     public void testNewGestureAfterCancelSeamlessTakeover() {
215         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
216             // start back gesture
217             WindowInsetsAnimationControlListener animationControlListener = startBackGesture();
218             // simulate ImeBackAnimationController receiving control
219             animationControlListener.onReady(mWindowInsetsAnimationController, ime());
220             // verify initial animation insets are set
221             verify(mWindowInsetsAnimationController, times(1)).setInsetsAndAlpha(
222                     eq(Insets.of(0, 0, 0, IME_HEIGHT)), eq(1f), anyFloat());
223 
224             // progress back gesture
225             mBackAnimationController.onBackProgressed(new BackEvent(100f, 0f, 0.5f, EDGE_LEFT));
226 
227             // cancel back gesture
228             mBackAnimationController.onBackCancelled();
229             // verify that InsetsController does not notified of a hide-anim (because the gesture
230             // was cancelled)
231             verify(mInsetsController, never()).setPredictiveBackImeHideAnimInProgress(eq(true));
232 
233             Mockito.clearInvocations(mWindowInsetsAnimationController);
234             // restart back gesture
235             mBackAnimationController.onBackStarted(new BackEvent(0f, 0f, 0f, EDGE_LEFT));
236             // verify that animation controller is reused and initial insets are set immediately
237             verify(mWindowInsetsAnimationController, times(1)).setInsetsAndAlpha(
238                     eq(Insets.of(0, 0, 0, IME_HEIGHT)), eq(1f), anyFloat());
239         });
240     }
241 
242     @Test
testImeInsetsManipulationCurve()243     public void testImeInsetsManipulationCurve() {
244         // start back gesture
245         WindowInsetsAnimationControlListener animationControlListener = startBackGesture();
246         // simulate ImeBackAnimationController receiving control
247         animationControlListener.onReady(mWindowInsetsAnimationController, ime());
248         // verify initial animation insets are set
249         verify(mWindowInsetsAnimationController, times(1)).setInsetsAndAlpha(
250                 eq(Insets.of(0, 0, 0, IME_HEIGHT)), eq(1f), anyFloat());
251 
252         Mockito.clearInvocations(mWindowInsetsAnimationController);
253         // progress back gesture
254         float progress = 0.5f;
255         mBackAnimationController.onBackProgressed(new BackEvent(100f, 0f, progress, EDGE_LEFT));
256         // verify correct ime insets manipulation
257         verify(mWindowInsetsAnimationController, times(1)).setInsetsAndAlpha(
258                 eq(Insets.of(0, 0, 0, getImeHeight(progress))), eq(1f), anyFloat());
259     }
260 
261     @Test
testOnReadyAfterGestureFinished()262     public void testOnReadyAfterGestureFinished() {
263         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
264             // start back gesture
265             WindowInsetsAnimationControlListener animationControlListener = startBackGesture();
266 
267             // progress back gesture
268             float progress = 0.5f;
269             mBackAnimationController.onBackProgressed(new BackEvent(100f, 0f, progress, EDGE_LEFT));
270 
271             // commit back gesture
272             mBackAnimationController.onBackInvoked();
273 
274             // verify setInsetsAndAlpha never called due to onReady delayed
275             verify(mWindowInsetsAnimationController, never()).setInsetsAndAlpha(any(), anyInt(),
276                     anyFloat());
277             verify(mInsetsController, never()).setPredictiveBackImeHideAnimInProgress(eq(true));
278 
279             // simulate ImeBackAnimationController receiving control
280             animationControlListener.onReady(mWindowInsetsAnimationController, ime());
281 
282             // verify setInsetsAndAlpha immediately called
283             verify(mWindowInsetsAnimationController, times(1)).setInsetsAndAlpha(
284                     eq(Insets.of(0, 0, 0, getImeHeight(progress))), eq(1f), anyFloat());
285             // verify post-commit hide anim has started
286             verify(mInsetsController, times(1)).setPredictiveBackImeHideAnimInProgress(eq(true));
287         });
288     }
289 
290     @Test
testOnBackInvokedHidesImeEvenIfInsetsControlCancelled()291     public void testOnBackInvokedHidesImeEvenIfInsetsControlCancelled() {
292         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
293             // start back gesture
294             WindowInsetsAnimationControlListener animationControlListener = startBackGesture();
295 
296             // simulate ImeBackAnimationController not receiving control (e.g. due to split screen)
297             animationControlListener.onCancelled(mWindowInsetsAnimationController);
298 
299             // commit back gesture
300             mBackAnimationController.onBackInvoked();
301             // verify that InputMethodManager#notifyImeHidden is called (which is the case whenever
302             // getInputMethodManager is called from ImeBackAnimationController)
303             verify(mViewRootInsetsControllerHost, times(2)).getInputMethodManager();
304         });
305     }
306 
startBackGesture()307     private WindowInsetsAnimationControlListener startBackGesture() {
308         // start back gesture
309         mBackAnimationController.onBackStarted(new BackEvent(0f, 0f, 0f, EDGE_LEFT));
310 
311         // verify controlWindowInsetsAnimation is called and capture animationControlListener
312         ArgumentCaptor<WindowInsetsAnimationControlListener> animationControlListener =
313                 ArgumentCaptor.forClass(WindowInsetsAnimationControlListener.class);
314         verify(mInsetsController, times(1)).controlWindowInsetsAnimation(anyInt(), any(),
315                 animationControlListener.capture(), anyBoolean(), anyLong(), any(), anyInt(),
316                 anyBoolean());
317 
318         return animationControlListener.getValue();
319     }
320 
getImeHeight(float gestureProgress)321     private int getImeHeight(float gestureProgress) {
322         float interpolatedProgress = BACK_GESTURE.getInterpolation(gestureProgress);
323         return (int) (IME_HEIGHT - interpolatedProgress * PEEK_FRACTION * IME_HEIGHT);
324     }
325 }
326