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.view.cts;
18 
19 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
20 
21 import static org.junit.Assert.assertEquals;
22 import static org.junit.Assert.assertFalse;
23 import static org.junit.Assert.assertNotNull;
24 import static org.junit.Assert.assertSame;
25 import static org.junit.Assert.assertTrue;
26 
27 import android.content.Context;
28 import android.graphics.Point;
29 import android.graphics.Rect;
30 import android.os.CancellationSignal;
31 import android.platform.test.annotations.Presubmit;
32 import android.view.ScrollCaptureCallback;
33 import android.view.ScrollCaptureSession;
34 import android.view.ScrollCaptureTarget;
35 import android.view.View;
36 import android.view.ViewGroup;
37 
38 import androidx.annotation.NonNull;
39 import androidx.test.filters.MediumTest;
40 import androidx.test.filters.SmallTest;
41 
42 import org.junit.Test;
43 import org.junit.runner.RunWith;
44 import org.mockito.junit.MockitoJUnitRunner;
45 
46 import java.util.ArrayList;
47 import java.util.List;
48 import java.util.function.Consumer;
49 
50 /**
51  * Exercises Scroll Capture (long screenshot) APIs in {@link ViewGroup}.
52  */
53 @Presubmit
54 @SmallTest
55 @RunWith(MockitoJUnitRunner.class)
56 public class ViewGroup_ScrollCaptureTest {
57 
58     private static class Receiver<T> implements Consumer<T> {
59         private final List<T> mValues = new ArrayList<>();
60 
61         @Override
accept(T target)62         public void accept(T target) {
63             mValues.add(target);
64         }
65 
getAllValues()66         public List<T> getAllValues() {
67             return mValues;
68         }
69 
getValue()70         public T getValue() {
71             if (mValues.isEmpty()) {
72                 throw new IllegalStateException("No values received");
73             }
74             return mValues.get(mValues.size() - 1);
75         }
76 
hasValue()77         public boolean hasValue() {
78             return !mValues.isEmpty();
79         }
80     }
81 
82     /** Make sure the hint flags are saved and loaded correctly. */
83     @Test
testSetScrollCaptureHint()84     public void testSetScrollCaptureHint() {
85         final Context context = getInstrumentation().getContext();
86         final MockViewGroup viewGroup = new MockViewGroup(context);
87 
88         assertNotNull(viewGroup);
89         assertEquals("Default scroll capture hint flags should be [SCROLL_CAPTURE_HINT_AUTO]",
90                 ViewGroup.SCROLL_CAPTURE_HINT_AUTO, viewGroup.getScrollCaptureHint());
91 
92         viewGroup.setScrollCaptureHint(View.SCROLL_CAPTURE_HINT_INCLUDE);
93         assertEquals("The scroll capture hint was not stored correctly.",
94                 ViewGroup.SCROLL_CAPTURE_HINT_INCLUDE, viewGroup.getScrollCaptureHint());
95 
96         viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE);
97         assertEquals("The scroll capture hint was not stored correctly.",
98                 ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE, viewGroup.getScrollCaptureHint());
99 
100         viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS);
101         assertEquals("The scroll capture hint was not stored correctly.",
102                 ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS,
103                 viewGroup.getScrollCaptureHint());
104 
105         viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_INCLUDE
106                 | ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS);
107         assertEquals("The scroll capture hint was not stored correctly.",
108                 ViewGroup.SCROLL_CAPTURE_HINT_INCLUDE
109                         | ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS,
110                 viewGroup.getScrollCaptureHint());
111 
112         viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE
113                 | ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS);
114         assertEquals("The scroll capture hint was not stored correctly.",
115                 ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE
116                         | ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS,
117                 viewGroup.getScrollCaptureHint());
118     }
119 
120     /** Make sure the hint flags are saved and loaded correctly. */
121     @Test
testSetScrollCaptureHint_mutuallyExclusiveFlags()122     public void testSetScrollCaptureHint_mutuallyExclusiveFlags() {
123         final Context context = getInstrumentation().getContext();
124         final MockViewGroup viewGroup = new MockViewGroup(context);
125 
126         viewGroup.setScrollCaptureHint(
127                 View.SCROLL_CAPTURE_HINT_INCLUDE | View.SCROLL_CAPTURE_HINT_EXCLUDE);
128         assertEquals("Mutually exclusive flags were not resolved correctly",
129                 ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE, viewGroup.getScrollCaptureHint());
130     }
131 
132     /**
133      * No target is returned because MockViewGroup does not emulate a scrolling container.
134      */
135     @SmallTest
136     @Test
testDispatchScrollCaptureSearch()137     public void testDispatchScrollCaptureSearch() {
138         final Context context = getInstrumentation().getContext();
139         final MockViewGroup viewGroup =
140                 new MockViewGroup(context, 0, 0, 200, 200, View.SCROLL_CAPTURE_HINT_AUTO);
141 
142         Rect localVisibleRect = new Rect(0, 0, 200, 200);
143         Point windowOffset = new Point();
144 
145         Receiver<ScrollCaptureTarget> receiver = new Receiver<>();
146         viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, receiver);
147         assertFalse("No target was expected", receiver.hasValue());
148     }
149 
150     /**
151      * Ensure that a ViewGroup with 'scrollCaptureHint=auto', and a scroll capture callback
152      * produces a correct target for that handler.
153      */
154     @MediumTest
155     @Test
testDispatchScrollCaptureSearch_withCallback()156     public void testDispatchScrollCaptureSearch_withCallback() {
157         final Context context = getInstrumentation().getContext();
158         MockViewGroup viewGroup =
159                 new MockViewGroup(context, 0, 0, 200, 200, View.SCROLL_CAPTURE_HINT_AUTO);
160 
161         MockScrollCaptureCallback callback = new MockScrollCaptureCallback();
162         viewGroup.setScrollCaptureCallback(callback);
163 
164         Rect localVisibleRect = new Rect(0, 0, 200, 200);
165         Point windowOffset = new Point();
166 
167         Receiver<ScrollCaptureTarget> receiver = new Receiver<>();
168         viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, receiver);
169         callOnScrollCaptureSearch(receiver);
170         callback.completeSearchRequest(new Rect(1, 2, 3, 4));
171         assertTrue("A target was expected", receiver.hasValue());
172 
173         ScrollCaptureTarget target = receiver.getValue();
174         assertNotNull("Target not found", target);
175         assertSame("Target has the wrong callback", callback, target.getCallback());
176         assertEquals("Target has the wrong bounds", new Rect(1, 2, 3, 4), target.getScrollBounds());
177 
178         assertSame("Target has the wrong View", viewGroup, target.getContainingView());
179         assertEquals("Target hint is incorrect", View.SCROLL_CAPTURE_HINT_AUTO,
180                 target.getContainingView().getScrollCaptureHint());
181     }
182 
183     /**
184      * Dispatch skips this view entirely due to the exclude hint, despite a callback being set.
185      * Exclude takes precedence.
186      */
187     @MediumTest
188     @Test
testDispatchScrollCaptureSearch_withCallback_hintExclude()189     public void testDispatchScrollCaptureSearch_withCallback_hintExclude() {
190         final Context context = getInstrumentation().getContext();
191         final MockViewGroup viewGroup =
192                 new MockViewGroup(context, 0, 0, 200, 200, View.SCROLL_CAPTURE_HINT_EXCLUDE);
193 
194         MockScrollCaptureCallback callback = new MockScrollCaptureCallback();
195         viewGroup.setScrollCaptureCallback(callback);
196 
197         Rect localVisibleRect = new Rect(0, 0, 200, 200);
198         Point windowOffset = new Point();
199 
200         Receiver<ScrollCaptureTarget> receiver = new Receiver<>();
201         viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, receiver);
202         callback.verifyZeroInteractions();
203         assertFalse("No target expected.", receiver.hasValue());
204     }
205 
nullOrEmpty(Rect r)206     private static boolean nullOrEmpty(Rect r) {
207         return r == null || r.isEmpty();
208     }
209 
210     /**
211      * Test scroll capture search dispatch to child views.
212      * <p>
213      * Verifies computation of child visible bounds.
214      * TODO: with scrollX / scrollY, split up into discrete tests
215      */
216     @MediumTest
217     @Test
testDispatchScrollCaptureSearch_toChildren()218     public void testDispatchScrollCaptureSearch_toChildren() throws Exception {
219         final Context context = getInstrumentation().getContext();
220         final MockViewGroup viewGroup = new MockViewGroup(context, 0, 0, 200, 200);
221 
222         Rect localVisibleRect = new Rect(25, 50, 175, 150);
223         Point windowOffset = new Point(0, 0);
224 
225         //        visible area
226         //       |<- l=25,    |
227         //       |    r=175 ->|
228         // +--------------------------+
229         // | view1 (0, 0, 200, 25)    |
230         // +---------------+----------+
231         // |               |          |
232         // | view2         | view4    | --+
233         // | (0, 25,       |    (inv) |   | visible area
234         // |      150, 100)|          |   |
235         // +---------------+----------+   | t=50, b=150
236         // | view3         | view5    |   |
237         // | (0, 100       |(150, 100 | --+
238         // |     200, 200) | 200, 200)|
239         // |               |          |
240         // |               |          |
241         // +---------------+----------+ (200,200)
242 
243         // View 1 is fully clipped and not visible.
244         final MockView view1 = new MockView(context, 0, 0, 200, 25);
245         viewGroup.addView(view1);
246 
247         // View 2 is partially visible. (75x75), but not scrollable
248         final MockView view2 = new MockView(context, 0, 25, 150, 100);
249         viewGroup.addView(view2);
250 
251         // View 3 is partially visible (175x50)
252         // Pretend View3 can scroll by providing a callback to handle it here
253         MockScrollCaptureCallback view3Callback = new MockScrollCaptureCallback();
254         final MockView view3 = new MockView(context, 0, 100, 200, 200);
255         view3.setScrollCaptureCallback(view3Callback);
256         viewGroup.addView(view3);
257 
258         // View 4 is invisible and should be ignored.
259         final MockView view4 = new MockView(context, 150, 25, 200, 100, View.INVISIBLE);
260         viewGroup.addView(view4);
261 
262         MockScrollCaptureCallback view5Callback = new MockScrollCaptureCallback();
263 
264         // View 5 is partially visible and explicitly included via flag. (25x50)
265         final MockView view5 = new MockView(context, 150, 100, 200, 200);
266         view5.setScrollCaptureCallback(view5Callback);
267         view5.setScrollCaptureHint(View.SCROLL_CAPTURE_HINT_INCLUDE);
268         viewGroup.addView(view5);
269 
270         Receiver<ScrollCaptureTarget> receiver = new Receiver<>();
271 
272         // Dispatch to the ViewGroup
273         viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, receiver);
274         callOnScrollCaptureSearch(receiver);
275         view3Callback.completeSearchRequest(new Rect(0, 0, 200, 100));
276         view5Callback.completeSearchRequest(new Rect(0, 0, 50, 100));
277 
278         // View 1 is entirely clipped by the parent and not visible, dispatch
279         // skips this view entirely.
280         view1.assertDispatchScrollCaptureSearchCount(0);
281 
282         // View 2, verify the computed localVisibleRect and windowOffset are correctly transformed
283         // to the child coordinate space
284         view2.assertDispatchScrollCaptureSearchCount(1);
285         view2.assertDispatchScrollCaptureSearchLastArgs(
286                 new Rect(25, 25, 150, 75), new Point(0, 25));
287 
288         // View 3, verify the computed localVisibleRect and windowOffset are correctly transformed
289         // to the child coordinate space
290         view3.assertDispatchScrollCaptureSearchCount(1);
291         view3.assertDispatchScrollCaptureSearchLastArgs(
292                 new Rect(25, 0, 175, 50), new Point(0, 100));
293 
294         // view4 is invisible, so it should be skipped entirely.
295         view4.assertDispatchScrollCaptureSearchCount(0);
296 
297         // view5 is partially visible
298         view5.assertDispatchScrollCaptureSearchCount(1);
299         view5.assertDispatchScrollCaptureSearchLastArgs(
300                 new Rect(0, 0, 25, 50), new Point(150, 100));
301 
302         assertTrue(receiver.hasValue());
303         assertEquals("expected two targets", 2, receiver.getAllValues().size());
304     }
305 
306     /**
307      * Test stand-in for ScrollCaptureSearchResults which is not part the public API. This
308      * dispatches a request each potential target's handler and collects the results
309      * synchronously on the calling thread. Use with caution!
310      *
311      * @param receiver the result consumer
312      */
callOnScrollCaptureSearch(Receiver<ScrollCaptureTarget> receiver)313     private void callOnScrollCaptureSearch(Receiver<ScrollCaptureTarget> receiver) {
314         CancellationSignal signal = new CancellationSignal();
315         receiver.getAllValues().forEach(target ->
316                 target.getCallback().onScrollCaptureSearch(signal, (scrollBounds) -> {
317                     if (!nullOrEmpty(scrollBounds)) {
318                         target.setScrollBounds(scrollBounds);
319                         target.updatePositionInWindow();
320                     }
321                 }));
322     }
323 
324     /**
325      * Tests the effect of padding on scroll capture search dispatch.
326      * <p>
327      * Verifies computation of child visible bounds with padding.
328      */
329     @MediumTest
330     @Test
testOnScrollCaptureSearch_withPadding()331     public void testOnScrollCaptureSearch_withPadding() {
332         final Context context = getInstrumentation().getContext();
333 
334         Rect windowBounds = new Rect(0, 0, 200, 200);
335         Point windowOffset = new Point(0, 0);
336 
337         final MockViewGroup parent = new MockViewGroup(context, 0, 0, 200, 200);
338         parent.setPadding(25, 50, 25, 50);
339         parent.setClipToPadding(true); // (default)
340 
341         final MockView view1 = new MockView(context, 0, -100, 200, 100);
342         parent.addView(view1);
343 
344         final MockView view2 = new MockView(context, 0, 0, 200, 200);
345         parent.addView(view2);
346 
347         final MockViewGroup view3 = new MockViewGroup(context, 0, 100, 200, 300);
348         parent.addView(view3);
349         view3.setPadding(25, 25, 25, 25);
350         view3.setClipToPadding(true);
351 
352         // Where targets are added
353         Receiver<ScrollCaptureTarget> receiver = new Receiver<>();
354 
355         // Dispatch to the ViewGroup
356         parent.dispatchScrollCaptureSearch(windowBounds, windowOffset, receiver);
357 
358         // Verify padding (with clipToPadding) is subtracted from visibleBounds
359         parent.assertOnScrollCaptureSearchLastArgs(new Rect(25, 50, 175, 150), new Point(0, 0));
360 
361         view1.assertOnScrollCaptureSearchLastArgs(
362                 new Rect(25, 150, 175, 200), new Point(0, -100));
363 
364         view2.assertOnScrollCaptureSearchLastArgs(
365                 new Rect(25, 50, 175, 150), new Point(0, 0));
366 
367         // Account for padding on view3 as well (top == 25px)
368         view3.assertOnScrollCaptureSearchLastArgs(
369                 new Rect(25, 25, 175, 50), new Point(0, 100));
370     }
371 
372     public static final class MockView extends View {
373 
374         private int mDispatchScrollCaptureSearchNumCalls;
375         private Rect mDispatchScrollCaptureSearchLastLocalVisibleRect;
376         private Point mDispatchScrollCaptureSearchLastWindowOffset;
377         private int mCreateScrollCaptureCallbackInternalCount;
378         private Rect mOnScrollCaptureSearchLastLocalVisibleRect;
379         private Point mOnScrollCaptureSearchLastWindowOffset;
380 
MockView(Context context)381         MockView(Context context) {
382             this(context, /* left */ 0, /* top */0, /* right */ 0, /* bottom */0);
383         }
384 
MockView(Context context, int left, int top, int right, int bottom)385         MockView(Context context, int left, int top, int right, int bottom) {
386             this(context, left, top, right, bottom, View.VISIBLE);
387         }
388 
MockView(Context context, int left, int top, int right, int bottom, int visibility)389         MockView(Context context, int left, int top, int right, int bottom, int visibility) {
390             super(context);
391             setVisibility(visibility);
392             setLeftTopRightBottom(left, top, right, bottom);
393         }
394 
assertDispatchScrollCaptureSearchCount(int count)395         void assertDispatchScrollCaptureSearchCount(int count) {
396             assertEquals("Unexpected number of calls to dispatchScrollCaptureSearch",
397                     count, mDispatchScrollCaptureSearchNumCalls);
398         }
399 
assertDispatchScrollCaptureSearchLastArgs(Rect localVisibleRect, Point windowOffset)400         void assertDispatchScrollCaptureSearchLastArgs(Rect localVisibleRect, Point windowOffset) {
401             assertEquals("arg localVisibleRect was incorrect.",
402                     localVisibleRect, mDispatchScrollCaptureSearchLastLocalVisibleRect);
403             assertEquals("arg windowOffset was incorrect.",
404                     windowOffset, mDispatchScrollCaptureSearchLastWindowOffset);
405         }
406 
reset()407         void reset() {
408             mDispatchScrollCaptureSearchNumCalls = 0;
409             mDispatchScrollCaptureSearchLastWindowOffset = null;
410             mDispatchScrollCaptureSearchLastLocalVisibleRect = null;
411             mCreateScrollCaptureCallbackInternalCount = 0;
412 
413         }
414 
415         @Override
onScrollCaptureSearch(Rect localVisibleRect, Point windowOffset, Consumer<ScrollCaptureTarget> targets)416         public void onScrollCaptureSearch(Rect localVisibleRect, Point windowOffset,
417                 Consumer<ScrollCaptureTarget> targets) {
418             super.onScrollCaptureSearch(localVisibleRect, windowOffset, targets);
419             mOnScrollCaptureSearchLastLocalVisibleRect = new Rect(localVisibleRect);
420             mOnScrollCaptureSearchLastWindowOffset = new Point(windowOffset);
421         }
422 
assertOnScrollCaptureSearchLastArgs(Rect localVisibleRect, Point windowOffset)423         void assertOnScrollCaptureSearchLastArgs(Rect localVisibleRect, Point windowOffset) {
424             assertEquals("arg localVisibleRect was incorrect.",
425                     localVisibleRect, mOnScrollCaptureSearchLastLocalVisibleRect);
426             assertEquals("arg windowOffset was incorrect.",
427                     windowOffset, mOnScrollCaptureSearchLastWindowOffset);
428         }
429 
430         @Override
dispatchScrollCaptureSearch(Rect localVisibleRect, Point windowOffset, Consumer<ScrollCaptureTarget> results)431         public void dispatchScrollCaptureSearch(Rect localVisibleRect, Point windowOffset,
432                 Consumer<ScrollCaptureTarget> results) {
433             mDispatchScrollCaptureSearchNumCalls++;
434             mDispatchScrollCaptureSearchLastLocalVisibleRect = new Rect(localVisibleRect);
435             mDispatchScrollCaptureSearchLastWindowOffset = new Point(windowOffset);
436             super.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, results);
437         }
438     }
439 
440     static class CallbackStub implements ScrollCaptureCallback {
441 
442         @Override
onScrollCaptureSearch(@onNull CancellationSignal signal, @NonNull Consumer<Rect> onReady)443         public void onScrollCaptureSearch(@NonNull CancellationSignal signal,
444                 @NonNull Consumer<Rect> onReady) {
445         }
446 
447         @Override
onScrollCaptureStart(@onNull ScrollCaptureSession session, @NonNull CancellationSignal signal, @NonNull Runnable onReady)448         public void onScrollCaptureStart(@NonNull ScrollCaptureSession session,
449                 @NonNull CancellationSignal signal, @NonNull Runnable onReady) {
450         }
451 
452         @Override
onScrollCaptureImageRequest(@onNull ScrollCaptureSession session, @NonNull CancellationSignal signal, @NonNull Rect captureArea, Consumer<Rect> onComplete)453         public void onScrollCaptureImageRequest(@NonNull ScrollCaptureSession session,
454                 @NonNull CancellationSignal signal, @NonNull Rect captureArea,
455                 Consumer<Rect> onComplete) {
456         }
457 
458         @Override
onScrollCaptureEnd(@onNull Runnable onReady)459         public void onScrollCaptureEnd(@NonNull Runnable onReady) {
460         }
461     };
462 
463     public static final class MockViewGroup extends ViewGroup {
464         private Rect mOnScrollCaptureSearchLastLocalVisibleRect;
465         private Point mOnScrollCaptureSearchLastWindowOffset;
466 
MockViewGroup(Context context)467         MockViewGroup(Context context) {
468             this(context, /* left */ 0, /* top */0, /* right */ 0, /* bottom */0);
469         }
470 
MockViewGroup(Context context, int left, int top, int right, int bottom)471         MockViewGroup(Context context, int left, int top, int right, int bottom) {
472             this(context, left, top, right, bottom, View.SCROLL_CAPTURE_HINT_AUTO);
473         }
474 
MockViewGroup(Context context, int left, int top, int right, int bottom, int scrollCaptureHint)475         MockViewGroup(Context context, int left, int top, int right, int bottom,
476                 int scrollCaptureHint) {
477             super(context);
478             setScrollCaptureHint(scrollCaptureHint);
479             setLeftTopRightBottom(left, top, right, bottom);
480         }
481 
482         @Override
onScrollCaptureSearch(Rect localVisibleRect, Point windowOffset, Consumer<ScrollCaptureTarget> targets)483         public void onScrollCaptureSearch(Rect localVisibleRect, Point windowOffset,
484                 Consumer<ScrollCaptureTarget> targets) {
485             super.onScrollCaptureSearch(localVisibleRect, windowOffset, targets);
486             mOnScrollCaptureSearchLastLocalVisibleRect = new Rect(localVisibleRect);
487             mOnScrollCaptureSearchLastWindowOffset = new Point(windowOffset);
488         }
489 
assertOnScrollCaptureSearchLastArgs(Rect localVisibleRect, Point windowOffset)490         void assertOnScrollCaptureSearchLastArgs(Rect localVisibleRect, Point windowOffset) {
491             assertEquals("arg localVisibleRect was incorrect.",
492                     localVisibleRect, mOnScrollCaptureSearchLastLocalVisibleRect);
493             assertEquals("arg windowOffset was incorrect.",
494                     windowOffset, mOnScrollCaptureSearchLastWindowOffset);
495         }
496 
497         @Override
onLayout(boolean changed, int l, int t, int r, int b)498         protected void onLayout(boolean changed, int l, int t, int r, int b) {
499             // We don't layout this view.
500         }
501     }
502 }
503