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.util.Log; 44 import android.view.View; 45 import android.view.WindowInsets; 46 import android.view.WindowInsetsAnimation; 47 import android.widget.EditText; 48 import android.widget.LinearLayout; 49 import android.widget.TextView; 50 51 import androidx.annotation.NonNull; 52 53 import com.android.compatibility.common.util.OverrideAnimationScaleRule; 54 55 import org.junit.Assert; 56 import org.junit.Rule; 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 public WindowInsets insets; 192 public float fraction; 193 public float interpolatedFraction; 194 public float alpha; 195 } 196 197 WindowInsetsAnimation lastAnimation; 198 public 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 public WindowInsetsAnimation statusBarAnim; 234 public WindowInsetsAnimation navBarAnim; 235 public WindowInsetsAnimation imeAnim; 236 public volatile boolean imeAnimStarted; 237 public volatile boolean animationDone; 238 public final ArrayList<AnimationStep> statusAnimSteps = new ArrayList<>(); 239 public final ArrayList<AnimationStep> navAnimSteps = new ArrayList<>(); 240 public final ArrayList<AnimationStep> imeAnimSteps = new ArrayList<>(); 241 public Runnable startRunnable; 242 public final ArraySet<WindowInsetsAnimation> runningAnims = new ArraySet<>(); 243 MultiAnimCallback()244 public MultiAnimCallback() { 245 super(DISPATCH_MODE_STOP); 246 } 247 248 @Override onPrepare(WindowInsetsAnimation animation)249 public void onPrepare(WindowInsetsAnimation animation) { 250 if ((animation.getTypeMask() & statusBars()) != 0) { 251 statusBarAnim = animation; 252 } 253 if ((animation.getTypeMask() & navigationBars()) != 0) { 254 navBarAnim = animation; 255 } 256 if ((animation.getTypeMask() & ime()) != 0) { 257 imeAnim = animation; 258 } 259 } 260 261 @Override onStart( WindowInsetsAnimation animation, WindowInsetsAnimation.Bounds bounds)262 public WindowInsetsAnimation.Bounds onStart( 263 WindowInsetsAnimation animation, WindowInsetsAnimation.Bounds bounds) { 264 if (startRunnable != null) { 265 startRunnable.run(); 266 } 267 runningAnims.add(animation); 268 if (animation.equals(imeAnim)) { 269 imeAnimStarted = true; 270 } 271 return bounds; 272 } 273 274 @Override onProgress(WindowInsets insets, List<WindowInsetsAnimation> runningAnimations)275 public WindowInsets onProgress(WindowInsets insets, 276 List<WindowInsetsAnimation> runningAnimations) { 277 if (statusBarAnim != null) { 278 statusAnimSteps.add(new AnimationStep(insets, statusBarAnim.getFraction(), 279 statusBarAnim.getInterpolatedFraction(), statusBarAnim.getAlpha())); 280 } 281 if (navBarAnim != null) { 282 navAnimSteps.add(new AnimationStep(insets, navBarAnim.getFraction(), 283 navBarAnim.getInterpolatedFraction(), navBarAnim.getAlpha())); 284 } 285 if (imeAnim != null) { 286 imeAnimSteps.add(new AnimationStep(insets, imeAnim.getFraction(), 287 imeAnim.getInterpolatedFraction(), imeAnim.getAlpha())); 288 } 289 290 assertEquals(runningAnims.size(), runningAnimations.size()); 291 for (int i = runningAnimations.size() - 1; i >= 0; i--) { 292 Assert.assertNotEquals(-1, 293 runningAnims.indexOf(runningAnimations.get(i))); 294 } 295 296 return WindowInsets.CONSUMED; 297 } 298 299 @Override onEnd(WindowInsetsAnimation animation)300 public void onEnd(WindowInsetsAnimation animation) { 301 runningAnims.remove(animation); 302 if (runningAnims.isEmpty()) { 303 animationDone = true; 304 } 305 } 306 } 307 308 public static class TestActivity extends FocusableActivity { 309 310 private final String mEditTextMarker = 311 "android.server.wm.WindowInsetsAnimationTestBase.TestActivity" 312 + SystemClock.elapsedRealtimeNanos(); 313 314 public AnimCallback mCallback = 315 spy(new AnimCallback(WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP)); 316 public WindowInsets mLastWindowInsets; 317 /** 318 * Save the WindowInsets when animation done. Acoid to mLastWindowInsets 319 * always be updated after windowinsets animation done on low-ram devices. 320 */ 321 public WindowInsets mLastPendingWindowInsets; 322 323 public View.OnApplyWindowInsetsListener mListener; 324 public LinearLayout mView; 325 public View mChild; 326 public EditText mEditor; 327 328 public class InsetsListener implements View.OnApplyWindowInsetsListener { 329 330 @Override onApplyWindowInsets(View v, WindowInsets insets)331 public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { 332 Log.d("TestActivity", "onApplyWindowInsets insets=" + insets); 333 334 /** 335 * Do not update mLastWindowInsets and save the latest WindowInsets to 336 * mLastPendingWindowInsets. 337 */ 338 if (mCallback.animationDone) { 339 mLastPendingWindowInsets = insets; 340 return WindowInsets.CONSUMED; 341 } 342 mLastWindowInsets = insets; 343 mLastPendingWindowInsets = null; 344 return WindowInsets.CONSUMED; 345 } 346 } 347 348 @NonNull getEditTextMarker()349 public String getEditTextMarker() { 350 return mEditTextMarker; 351 } 352 353 @Override onCreate(Bundle savedInstanceState)354 protected void onCreate(Bundle savedInstanceState) { 355 super.onCreate(savedInstanceState); 356 mListener = spy(new InsetsListener()); 357 mView = new LinearLayout(this); 358 mView.setWindowInsetsAnimationCallback(mCallback); 359 mView.setOnApplyWindowInsetsListener(mListener); 360 mChild = new TextView(this); 361 mEditor = new EditText(this); 362 mEditor.setPrivateImeOptions(mEditTextMarker); 363 mView.addView(mChild); 364 mView.addView(mEditor); 365 366 setContentView(mView); 367 368 getWindow().setDecorFitsSystemWindows(false); 369 getWindow().getAttributes().layoutInDisplayCutoutMode = 370 LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 371 getWindow().setSoftInputMode(SOFT_INPUT_STATE_HIDDEN); 372 getWindow().getDecorView().getWindowInsetsController().setSystemBarsBehavior( 373 BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); 374 mEditor.requestFocus(); 375 } 376 resetAnimationDone()377 public void resetAnimationDone() { 378 mCallback.animationDone = false; 379 /** 380 * Do not update mLastWindowInsets and save the latest WindowInsets to 381 * mLastPendingWindowInsets. 382 */ 383 if (mLastPendingWindowInsets != null) { 384 mLastWindowInsets = new WindowInsets(mLastPendingWindowInsets); 385 mLastPendingWindowInsets = null; 386 } 387 } 388 } 389 } 390