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