1 /* 2 * Copyright (C) 2020 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.server.wm; 18 19 import static android.graphics.Insets.NONE; 20 import static android.view.WindowInsets.Type.ime; 21 import static android.view.WindowInsets.Type.navigationBars; 22 import static android.view.WindowInsets.Type.statusBars; 23 import static android.view.WindowInsets.Type.systemBars; 24 import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; 25 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 26 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN; 27 28 import static org.junit.Assert.assertEquals; 29 import static org.junit.Assert.assertNotNull; 30 import static org.junit.Assert.assertTrue; 31 import static org.mockito.ArgumentMatchers.any; 32 import static org.mockito.ArgumentMatchers.argThat; 33 import static org.mockito.ArgumentMatchers.eq; 34 import static org.mockito.Mockito.atLeast; 35 import static org.mockito.Mockito.inOrder; 36 import static org.mockito.Mockito.spy; 37 38 import android.graphics.Insets; 39 import android.os.Bundle; 40 import android.os.SystemClock; 41 import android.server.wm.WindowInsetsAnimationTestBase.AnimCallback.AnimationStep; 42 import android.util.ArraySet; 43 import android.view.View; 44 import android.view.WindowInsets; 45 import android.view.WindowInsetsAnimation; 46 import android.widget.EditText; 47 import android.widget.LinearLayout; 48 import android.widget.TextView; 49 50 import androidx.annotation.NonNull; 51 52 import com.android.compatibility.common.util.OverrideAnimationScaleRule; 53 54 import org.junit.Assert; 55 import org.junit.Rule; 56 import org.mockito.InOrder; 57 58 import java.util.ArrayList; 59 import java.util.List; 60 import java.util.function.BiPredicate; 61 import java.util.function.Function; 62 import java.util.function.Predicate; 63 64 /** 65 * Base class for tests in {@link WindowInsetsAnimation} and {@link WindowInsetsAnimation.Callback}. 66 */ 67 public class WindowInsetsAnimationTestBase extends WindowManagerTestBase { 68 69 @Rule 70 public final OverrideAnimationScaleRule mOverrideAnimationScaleRule = 71 new OverrideAnimationScaleRule(1.0f); 72 73 protected TestActivity mActivity; 74 protected View mRootView; 75 commonAnimationAssertions(TestActivity activity, WindowInsets before, boolean show, int types)76 protected void commonAnimationAssertions(TestActivity activity, WindowInsets before, 77 boolean show, int types) { 78 79 AnimCallback callback = activity.mCallback; 80 81 InOrder inOrder = inOrder(activity.mCallback, activity.mListener); 82 83 WindowInsets after = activity.mLastWindowInsets; 84 inOrder.verify(callback).onPrepare(eq(callback.lastAnimation)); 85 inOrder.verify(activity.mListener).onApplyWindowInsets(any(), any()); 86 87 inOrder.verify(callback).onStart(eq(callback.lastAnimation), argThat( 88 argument -> argument.getLowerBound().equals(NONE) 89 && argument.getUpperBound().equals(show 90 ? after.getInsets(types) 91 : before.getInsets(types)))); 92 93 inOrder.verify(callback, atLeast(2)).onProgress(any(), argThat( 94 argument -> argument.size() == 1 && argument.get(0) == callback.lastAnimation)); 95 inOrder.verify(callback).onEnd(eq(callback.lastAnimation)); 96 97 if ((types & systemBars()) != 0) { 98 assertTrue((callback.lastAnimation.getTypeMask() & systemBars()) != 0); 99 } 100 if ((types & ime()) != 0) { 101 assertTrue((callback.lastAnimation.getTypeMask() & ime()) != 0); 102 } 103 assertTrue(callback.lastAnimation.getDurationMillis() > 0); 104 assertNotNull(callback.lastAnimation.getInterpolator()); 105 assertBeforeAfterState(callback.animationSteps, before, after); 106 assertAnimationSteps(callback.animationSteps, show /* increasing */); 107 } 108 assertBeforeAfterState(ArrayList<AnimationStep> steps, WindowInsets before, WindowInsets after)109 private void assertBeforeAfterState(ArrayList<AnimationStep> steps, WindowInsets before, 110 WindowInsets after) { 111 assertEquals(before, steps.get(0).insets); 112 assertEquals(after, steps.get(steps.size() - 1).insets); 113 } 114 hasWindowInsets(View rootView, int types)115 protected static boolean hasWindowInsets(View rootView, int types) { 116 return Insets.NONE != rootView.getRootWindowInsets().getInsetsIgnoringVisibility(types); 117 } 118 assertAnimationSteps(ArrayList<AnimationStep> steps, boolean showAnimation)119 protected void assertAnimationSteps(ArrayList<AnimationStep> steps, boolean showAnimation) { 120 assertAnimationSteps(steps, showAnimation, systemBars()); 121 } assertAnimationSteps(ArrayList<AnimationStep> steps, boolean showAnimation, final int types)122 protected void assertAnimationSteps(ArrayList<AnimationStep> steps, boolean showAnimation, 123 final int types) { 124 assertTrue(steps.size() >= 2); 125 assertEquals(0f, steps.get(0).fraction, 0f); 126 assertEquals(0f, steps.get(0).interpolatedFraction, 0f); 127 assertEquals(1f, steps.get(steps.size() - 1).fraction, 0f); 128 assertEquals(1f, steps.get(steps.size() - 1).interpolatedFraction, 0f); 129 if (showAnimation) { 130 assertEquals(1f, steps.get(steps.size() - 1).alpha, 0f); 131 } else { 132 assertEquals(1f, steps.get(0).alpha, 0f); 133 } 134 135 assertListElements(steps, step -> step.fraction, 136 (current, next) -> next >= current); 137 assertListElements(steps, step -> step.interpolatedFraction, 138 (current, next) -> next >= current); 139 assertListElements(steps, step -> step.alpha, alpha -> alpha >= 0f); 140 assertListElements(steps, step -> step.insets, compareInsets(types, showAnimation)); 141 } 142 compareInsets(int types, boolean showAnimation)143 private BiPredicate<WindowInsets, WindowInsets> compareInsets(int types, 144 boolean showAnimation) { 145 if (showAnimation) { 146 return (current, next) -> 147 next.getInsets(types).left >= current.getInsets(types).left 148 && next.getInsets(types).top >= current.getInsets(types).top 149 && next.getInsets(types).right >= current.getInsets(types).right 150 && next.getInsets(types).bottom >= current.getInsets(types).bottom; 151 } else { 152 return (current, next) -> 153 next.getInsets(types).left <= current.getInsets(types).left 154 && next.getInsets(types).top <= current.getInsets(types).top 155 && next.getInsets(types).right <= current.getInsets(types).right 156 && next.getInsets(types).bottom <= current.getInsets(types).bottom; 157 } 158 } 159 assertListElements(ArrayList<T> list, Function<T, V> getter, Predicate<V> predicate)160 private <T, V> void assertListElements(ArrayList<T> list, Function<T, V> getter, 161 Predicate<V> predicate) { 162 for (int i = 0; i <= list.size() - 1; i++) { 163 V value = getter.apply(list.get(i)); 164 assertTrue("Predicate.test failed i=" + i + " value=" 165 + value, predicate.test(value)); 166 } 167 } 168 assertListElements(ArrayList<T> list, Function<T, V> getter, BiPredicate<V, V> comparator)169 private <T, V> void assertListElements(ArrayList<T> list, Function<T, V> getter, 170 BiPredicate<V, V> comparator) { 171 for (int i = 0; i <= list.size() - 2; i++) { 172 V current = getter.apply(list.get(i)); 173 V next = getter.apply(list.get(i + 1)); 174 assertTrue(comparator.test(current, next)); 175 } 176 } 177 178 public static class AnimCallback extends WindowInsetsAnimation.Callback { 179 180 public static class AnimationStep { 181 AnimationStep(WindowInsets insets, float fraction, float interpolatedFraction, float alpha)182 AnimationStep(WindowInsets insets, float fraction, float interpolatedFraction, 183 float alpha) { 184 this.insets = insets; 185 this.fraction = fraction; 186 this.interpolatedFraction = interpolatedFraction; 187 this.alpha = alpha; 188 } 189 190 WindowInsets insets; 191 float fraction; 192 float interpolatedFraction; 193 float alpha; 194 } 195 196 WindowInsetsAnimation lastAnimation; 197 volatile boolean animationDone; 198 final ArrayList<AnimationStep> animationSteps = new ArrayList<>(); 199 AnimCallback(int dispatchMode)200 public AnimCallback(int dispatchMode) { 201 super(dispatchMode); 202 } 203 204 @Override onPrepare(WindowInsetsAnimation animation)205 public void onPrepare(WindowInsetsAnimation animation) { 206 animationSteps.clear(); 207 lastAnimation = animation; 208 } 209 210 @Override onStart( WindowInsetsAnimation animation, WindowInsetsAnimation.Bounds bounds)211 public WindowInsetsAnimation.Bounds onStart( 212 WindowInsetsAnimation animation, WindowInsetsAnimation.Bounds bounds) { 213 return bounds; 214 } 215 216 @Override onProgress(WindowInsets insets, List<WindowInsetsAnimation> runningAnimations)217 public WindowInsets onProgress(WindowInsets insets, 218 List<WindowInsetsAnimation> runningAnimations) { 219 animationSteps.add(new AnimationStep(insets, lastAnimation.getFraction(), 220 lastAnimation.getInterpolatedFraction(), lastAnimation.getAlpha())); 221 return WindowInsets.CONSUMED; 222 } 223 224 @Override onEnd(WindowInsetsAnimation animation)225 public void onEnd(WindowInsetsAnimation animation) { 226 animationDone = true; 227 } 228 } 229 230 protected static class MultiAnimCallback extends WindowInsetsAnimation.Callback { 231 232 WindowInsetsAnimation statusBarAnim; 233 WindowInsetsAnimation navBarAnim; 234 WindowInsetsAnimation imeAnim; 235 volatile boolean imeAnimStarted; 236 volatile boolean animationDone; 237 final ArrayList<AnimationStep> statusAnimSteps = new ArrayList<>(); 238 final ArrayList<AnimationStep> navAnimSteps = new ArrayList<>(); 239 final ArrayList<AnimationStep> imeAnimSteps = new ArrayList<>(); 240 Runnable startRunnable; 241 final ArraySet<WindowInsetsAnimation> runningAnims = new ArraySet<>(); 242 MultiAnimCallback()243 public MultiAnimCallback() { 244 super(DISPATCH_MODE_STOP); 245 } 246 247 @Override onPrepare(WindowInsetsAnimation animation)248 public void onPrepare(WindowInsetsAnimation animation) { 249 if ((animation.getTypeMask() & statusBars()) != 0) { 250 statusBarAnim = animation; 251 } 252 if ((animation.getTypeMask() & navigationBars()) != 0) { 253 navBarAnim = animation; 254 } 255 if ((animation.getTypeMask() & ime()) != 0) { 256 imeAnim = animation; 257 } 258 } 259 260 @Override onStart( WindowInsetsAnimation animation, WindowInsetsAnimation.Bounds bounds)261 public WindowInsetsAnimation.Bounds onStart( 262 WindowInsetsAnimation animation, WindowInsetsAnimation.Bounds bounds) { 263 if (startRunnable != null) { 264 startRunnable.run(); 265 } 266 runningAnims.add(animation); 267 if (animation.equals(imeAnim)) { 268 imeAnimStarted = true; 269 } 270 return bounds; 271 } 272 273 @Override onProgress(WindowInsets insets, List<WindowInsetsAnimation> runningAnimations)274 public WindowInsets onProgress(WindowInsets insets, 275 List<WindowInsetsAnimation> runningAnimations) { 276 if (statusBarAnim != null) { 277 statusAnimSteps.add(new AnimationStep(insets, statusBarAnim.getFraction(), 278 statusBarAnim.getInterpolatedFraction(), statusBarAnim.getAlpha())); 279 } 280 if (navBarAnim != null) { 281 navAnimSteps.add(new AnimationStep(insets, navBarAnim.getFraction(), 282 navBarAnim.getInterpolatedFraction(), navBarAnim.getAlpha())); 283 } 284 if (imeAnim != null) { 285 imeAnimSteps.add(new AnimationStep(insets, imeAnim.getFraction(), 286 imeAnim.getInterpolatedFraction(), imeAnim.getAlpha())); 287 } 288 289 assertEquals(runningAnims.size(), runningAnimations.size()); 290 for (int i = runningAnimations.size() - 1; i >= 0; i--) { 291 Assert.assertNotEquals(-1, 292 runningAnims.indexOf(runningAnimations.get(i))); 293 } 294 295 return WindowInsets.CONSUMED; 296 } 297 298 @Override onEnd(WindowInsetsAnimation animation)299 public void onEnd(WindowInsetsAnimation animation) { 300 runningAnims.remove(animation); 301 if (runningAnims.isEmpty()) { 302 animationDone = true; 303 } 304 } 305 } 306 307 public static class TestActivity extends FocusableActivity { 308 309 private final String mEditTextMarker = 310 "android.server.wm.WindowInsetsAnimationTestBase.TestActivity" 311 + SystemClock.elapsedRealtimeNanos(); 312 313 AnimCallback mCallback = 314 spy(new AnimCallback(WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP)); 315 WindowInsets mLastWindowInsets; 316 /** 317 * Save the WindowInsets when animation done. Acoid to mLastWindowInsets 318 * always be updated after windowinsets animation done on low-ram devices. 319 */ 320 WindowInsets mLastPendingWindowInsets; 321 322 View.OnApplyWindowInsetsListener mListener; 323 LinearLayout mView; 324 View mChild; 325 EditText mEditor; 326 327 public class InsetsListener implements View.OnApplyWindowInsetsListener { 328 329 @Override onApplyWindowInsets(View v, WindowInsets insets)330 public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { 331 /** 332 * Do not update mLastWindowInsets and save the latest WindowInsets to 333 * mLastPendingWindowInsets. 334 */ 335 if (mCallback.animationDone) { 336 mLastPendingWindowInsets = insets; 337 return WindowInsets.CONSUMED; 338 } 339 mLastWindowInsets = insets; 340 mLastPendingWindowInsets = null; 341 return WindowInsets.CONSUMED; 342 } 343 } 344 345 @NonNull getEditTextMarker()346 String getEditTextMarker() { 347 return mEditTextMarker; 348 } 349 350 @Override onCreate(Bundle savedInstanceState)351 protected void onCreate(Bundle savedInstanceState) { 352 super.onCreate(savedInstanceState); 353 mListener = spy(new InsetsListener()); 354 mView = new LinearLayout(this); 355 mView.setWindowInsetsAnimationCallback(mCallback); 356 mView.setOnApplyWindowInsetsListener(mListener); 357 mChild = new TextView(this); 358 mEditor = new EditText(this); 359 mEditor.setPrivateImeOptions(mEditTextMarker); 360 mView.addView(mChild); 361 mView.addView(mEditor); 362 363 setContentView(mView); 364 365 getWindow().setDecorFitsSystemWindows(false); 366 getWindow().getAttributes().layoutInDisplayCutoutMode = 367 LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 368 getWindow().setSoftInputMode(SOFT_INPUT_STATE_HIDDEN); 369 getWindow().getDecorView().getWindowInsetsController().setSystemBarsBehavior( 370 BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); 371 mEditor.requestFocus(); 372 } 373 resetAnimationDone()374 public void resetAnimationDone() { 375 mCallback.animationDone = false; 376 /** 377 * Do not update mLastWindowInsets and save the latest WindowInsets to 378 * mLastPendingWindowInsets. 379 */ 380 if (mLastPendingWindowInsets != null) { 381 mLastWindowInsets = new WindowInsets(mLastPendingWindowInsets); 382 mLastPendingWindowInsets = null; 383 } 384 } 385 } 386 } 387