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