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