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