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