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