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