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