1 /* 2 * Copyright (C) 2015 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 package com.google.android.apps.common.testing.accessibility.framework.integrations.espresso; 15 16 import static com.google.common.base.Preconditions.checkArgument; 17 import static com.google.common.base.Preconditions.checkNotNull; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.graphics.Bitmap; 22 import android.os.Build; 23 import android.os.StrictMode; 24 import android.util.Log; 25 import android.view.View; 26 import androidx.test.platform.io.PlatformTestStorage; 27 import androidx.test.services.storage.TestStorage; 28 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset; 29 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPresetAndroid; 30 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult; 31 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType; 32 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultDescriptor; 33 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils; 34 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult; 35 import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewHierarchyCheck; 36 import com.google.android.apps.common.testing.accessibility.framework.Parameters; 37 import com.google.android.apps.common.testing.accessibility.framework.ViewChecker; 38 import com.google.android.apps.common.testing.accessibility.framework.utils.contrast.BitmapImage; 39 import com.google.android.apps.common.testing.accessibility.framework.utils.contrast.Image; 40 import com.google.common.annotations.VisibleForTesting; 41 import com.google.common.collect.FluentIterable; 42 import com.google.common.collect.ImmutableList; 43 import com.google.errorprone.annotations.CanIgnoreReturnValue; 44 import java.io.BufferedOutputStream; 45 import java.io.IOException; 46 import java.util.ArrayList; 47 import java.util.HashMap; 48 import java.util.List; 49 import java.util.Locale; 50 import java.util.Objects; 51 import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 52 import org.checkerframework.checker.nullness.qual.Nullable; 53 import org.hamcrest.Matcher; 54 55 /** 56 * A configurable executor for the {@link AccessibilityViewHierarchyCheck}s designed for use with 57 * Espresso or Robolectric tests. Clients can call {@link #checkAndReturnResults} on a {@link View} 58 * to run all of the checks with the options specified in this object. 59 */ 60 public final class AccessibilityValidator { 61 62 private static final String TAG = "AccessibilityValidator"; 63 64 65 private AccessibilityCheckPreset preset = AccessibilityCheckPreset.LATEST; 66 private boolean runChecksFromRootView = false; 67 68 @VisibleForTesting Screenshotter screenshotter = new Screenshotter(); 69 @VisibleForTesting BitmapWriter bitmapWriter = new BitmapWriter(); 70 private @MonotonicNonNull PlatformTestStorage testStorage; 71 72 private boolean captureScreenshots = true; 73 private @Nullable Boolean saveScreenshots; 74 private @Nullable Boolean saveViewImages; 75 private int screenshotsCaptured = 0; 76 77 private @Nullable AccessibilityCheckResultType throwExceptionFor = 78 AccessibilityCheckResultType.ERROR; 79 80 private AccessibilityCheckResultDescriptor resultDescriptor = 81 new AccessibilityCheckResultDescriptor(); 82 83 /** TextView.addExtraDataToAccessibilityNodeInfo throws NPE when shadows are used. */ 84 private static final ViewChecker viewChecker = 85 new ViewChecker().setObtainCharacterLocations(!isRobolectric()); 86 87 private @Nullable Matcher<? super AccessibilityViewCheckResult> suppressingMatcher = null; 88 private final List<AccessibilityCheckListener> checkListeners = new ArrayList<>(); 89 private final List<CheckResultsListener> checkResultsListeners = new ArrayList<>(); 90 private Parameters parameters = new Parameters(); 91 AccessibilityValidator()92 public AccessibilityValidator() { 93 } 94 95 /** 96 * Runs accessibility checks with default parameters. The default parameters can be set using 97 * {@link #setParameters(Parameters)}. 98 * 99 * @param view the {@link View} to check 100 */ check(View view)101 public final void check(View view) { 102 check(view, parameters); 103 } 104 105 /** 106 * Runs accessibility checks. 107 * 108 * @param view the {@link View} to check 109 * @param parameters supplemental input data and preferences 110 */ check(View view, Parameters parameters)111 public final void check(View view, Parameters parameters) { 112 ImmutableList<AccessibilityViewCheckResult> unused = checkAndReturnResults(view, parameters); 113 } 114 115 /** 116 * Runs accessibility checks with default parameters and returns the list of results. If the 117 * result is not needed, call {@link #check(View)} instead. The default parameters can be set 118 * using {@link #setParameters(Parameters)}. 119 * 120 * @param view the {@link View} to check 121 * @return an immutable list of the resulting {@link AccessibilityViewCheckResult}s 122 */ checkAndReturnResults(View view)123 public final List<AccessibilityViewCheckResult> checkAndReturnResults(View view) { 124 return checkAndReturnResults(view, parameters); 125 } 126 127 /** 128 * Runs accessibility checks and returns the list of results. If the result is not needed, call 129 * {@link #check(View, Parameters)} instead. 130 * 131 * @param view the {@link View} to check 132 * @param parameters supplemental input data and preferences 133 * @return an immutable list of the resulting {@link AccessibilityViewCheckResult}s 134 */ checkAndReturnResults( View view, Parameters parameters)135 private final ImmutableList<AccessibilityViewCheckResult> checkAndReturnResults( 136 View view, Parameters parameters) { 137 checkNotNull(view); 138 checkNotNull(parameters); 139 140 View viewToCheck = runChecksFromRootView ? view.getRootView() : view; 141 return runAccessibilityChecks(viewToCheck, parameters); 142 } 143 144 145 /** 146 * Specify the set of checks to be run. The default is {link AccessibilityCheckPreset.LATEST}. 147 * 148 * @param preset The preset specifying the group of checks to run. 149 * @return this 150 */ 151 @CanIgnoreReturnValue setCheckPreset(AccessibilityCheckPreset preset)152 public AccessibilityValidator setCheckPreset(AccessibilityCheckPreset preset) { 153 this.preset = preset; 154 return this; 155 } 156 157 /** 158 * @param runChecksFromRootView {@code true} to check all views in the hierarchy, {@code false} to 159 * check only views in the hierarchy rooted at the passed in view. Default: {@code false} 160 * @return this 161 */ 162 @CanIgnoreReturnValue setRunChecksFromRootView(boolean runChecksFromRootView)163 public AccessibilityValidator setRunChecksFromRootView(boolean runChecksFromRootView) { 164 this.runChecksFromRootView = runChecksFromRootView; 165 return this; 166 } 167 168 /** 169 * Specifies a preference for whether screenshots should be captured. When enabled, a screenshot 170 * will be captured each time {@link #checkAndReturnResults} is called, and the screenshot will be 171 * provided to the ATF checks. This allows more through testing by some checks - for example, in 172 * heavyweight contrast checking - but incurs additional overhead. 173 * 174 * <p>Default: {@code true} 175 * 176 * @return this 177 * @see #setSaveImages(boolean, boolean) 178 */ 179 @CanIgnoreReturnValue setCaptureScreenshots(boolean capture)180 public AccessibilityValidator setCaptureScreenshots(boolean capture) { 181 captureScreenshots = capture; 182 return this; 183 } 184 185 /** 186 * Specify a preference for whether screenshots and images of Views that produce results should be 187 * retained after check evaluation. These can be useful for debugging, but produce more test 188 * artifacts. 189 * 190 * <p>This is syntactic sugar for {@link #setSaveImages(boolean, boolean)}. 191 */ 192 @CanIgnoreReturnValue setSaveImages(boolean save)193 public AccessibilityValidator setSaveImages(boolean save) { 194 return setSaveImages(save, save); 195 } 196 197 /** 198 * Specify a preference for whether screenshots and images of Views that produce results should be 199 * retained after check evaluation. These can be useful for debugging, but produce more test 200 * artifacts. These settings have no effect unless screenshot capture has been enabled. 201 * 202 * @param saveScreenshots whether screenshots should be saved after evaluation. By default, these 203 * images are saved when the checks produce any result other than {@code NOT_RUN}. 204 * @param saveViewImages whether an image should be saved of each View to which heavyweight 205 * contrast checking is applied. By default, these images are saved when the checks produce 206 * findings based on the images. 207 */ 208 @CanIgnoreReturnValue setSaveImages(boolean saveScreenshots, boolean saveViewImages)209 public AccessibilityValidator setSaveImages(boolean saveScreenshots, boolean saveViewImages) { 210 this.saveScreenshots = saveScreenshots; 211 this.saveViewImages = saveViewImages; 212 return this; 213 } 214 215 /** 216 * Suppresses all results that match the given matcher. Suppressed results will not be included in 217 * any logs or cause any {@code Exception} to be thrown 218 * 219 * @param resultMatcher a matcher that specifies result to be suppressed. If {@code null}, then 220 * any previously set matcher will be removed and the default behavior will be restored. 221 * @return this 222 */ 223 @CanIgnoreReturnValue setSuppressingResultMatcher( @ullable Matcher<? super AccessibilityViewCheckResult> resultMatcher)224 public AccessibilityValidator setSuppressingResultMatcher( 225 @Nullable Matcher<? super AccessibilityViewCheckResult> resultMatcher) { 226 suppressingMatcher = resultMatcher; 227 return this; 228 } 229 230 /** 231 * @param throwExceptionForErrors {@code true} to throw an {@code Exception} when there is at 232 * least one error result, {@code false} to just log the error results to logcat. Default: 233 * {@code true} 234 * @return this 235 * @deprecated Use {@link #setThrowExceptionFor} 236 */ 237 @CanIgnoreReturnValue 238 @Deprecated setThrowExceptionForErrors(boolean throwExceptionForErrors)239 public AccessibilityValidator setThrowExceptionForErrors(boolean throwExceptionForErrors) { 240 return setThrowExceptionFor( 241 throwExceptionForErrors ? AccessibilityCheckResultType.ERROR : null); 242 } 243 244 /** 245 * Specifies the types of results that should produce a thrown exception. 246 * 247 * <ul> 248 * If the value is: 249 * <li>{@link AccessibilityCheckResultType#ERROR}, an exception will be thrown for any ERROR 250 * <li>{@link AccessibilityCheckResultType#WARNING}, an exception will be thrown for any ERROR 251 * or WARNING 252 * <li>{@link AccessibilityCheckResultType#INFO}, an exception will be thrown for any ERROR, 253 * WARNING or INFO 254 * <li>{@code null}, no exception will be thrown 255 * </ul> 256 * 257 * The default is {@code ERROR}. 258 * 259 * @return this 260 */ 261 @CanIgnoreReturnValue setThrowExceptionFor( @ullable AccessibilityCheckResultType throwFor)262 public AccessibilityValidator setThrowExceptionFor( 263 @Nullable AccessibilityCheckResultType throwFor) { 264 checkArgument( 265 (throwFor == AccessibilityCheckResultType.ERROR) 266 || (throwFor == AccessibilityCheckResultType.WARNING) 267 || (throwFor == AccessibilityCheckResultType.INFO) 268 || (throwFor == null), 269 "Argument was %s but expected ERROR, WARNING, INFO or null.", 270 throwFor); 271 throwExceptionFor = throwFor; 272 return this; 273 } 274 275 /** 276 * Sets the {@link AccessibilityCheckResultDescriptor} that is used to convert results to readable 277 * messages in exceptions and logcat statements. 278 * 279 * @return this 280 */ 281 @CanIgnoreReturnValue setResultDescriptor( AccessibilityCheckResultDescriptor resultDescriptor)282 public AccessibilityValidator setResultDescriptor( 283 AccessibilityCheckResultDescriptor resultDescriptor) { 284 this.resultDescriptor = checkNotNull(resultDescriptor); 285 return this; 286 } 287 288 /** 289 * Adds a listener to receive all {@link AccessibilityCheckResult}s after suppression. Listeners 290 * will be called in the order they are added and before any {@link 291 * AccessibilityViewCheckException} would be thrown. 292 * 293 * @return this 294 */ 295 @CanIgnoreReturnValue addCheckListener(AccessibilityCheckListener listener)296 public AccessibilityValidator addCheckListener(AccessibilityCheckListener listener) { 297 checkNotNull(listener); 298 checkListeners.add(listener); 299 return this; 300 } 301 302 /** 303 * Adds a listener to receive a callback after checks have been evaluated. Listeners will be 304 * called in the order they are added and before any {@link AccessibilityViewCheckException} would 305 * be thrown. 306 * 307 * @return this 308 */ 309 @CanIgnoreReturnValue addCheckListener(CheckResultsListener listener)310 public AccessibilityValidator addCheckListener(CheckResultsListener listener) { 311 checkResultsListeners.add(listener); 312 return this; 313 } 314 315 /** 316 * Sets preferences to used by when evaluating checks unless explicitly provided by argument. 317 * 318 * @return this 319 */ 320 @CanIgnoreReturnValue setParameters(Parameters parameters)321 public AccessibilityValidator setParameters(Parameters parameters) { 322 this.parameters = parameters; 323 return this; 324 } 325 326 327 /** 328 * Runs accessibility checks on a {@code View} hierarchy 329 * 330 * @param view the {@link View} to check 331 * @return a list of the results of the checks 332 */ runAccessibilityChecks( View view, Parameters parameters)333 private ImmutableList<AccessibilityViewCheckResult> runAccessibilityChecks( 334 View view, Parameters parameters) { 335 try { 336 parameters = parameters.clone(); 337 } catch (CloneNotSupportedException e) { 338 throw new RuntimeException("Could not clone parameters", e); 339 } 340 Bitmap screenshot = null; 341 if (captureScreenshots) { 342 screenshot = screenshotter.getScreenshot(view.getRootView()); 343 if (screenshot != null) { 344 parameters.putScreenCapture(new BitmapImage(screenshot)); 345 if ((parameters.getSaveViewImages() == null) && !Boolean.FALSE.equals(saveViewImages)) { 346 parameters.setSaveViewImages(true); 347 } 348 screenshotsCaptured++; 349 } 350 } 351 352 return processResults( 353 view.getContext(), 354 viewChecker.runViewChecksOnView( 355 AccessibilityCheckPresetAndroid.getViewChecksForPreset(preset), view, parameters), 356 screenshot); 357 } 358 isRobolectric()359 private static boolean isRobolectric() { 360 return Objects.equals(Build.FINGERPRINT, "robolectric"); 361 } 362 363 /** Returns the number of times that this instance has captured a screenshot. */ 364 @VisibleForTesting getScreenshotsCaptured()365 int getScreenshotsCaptured() { 366 return screenshotsCaptured; 367 } 368 369 /** 370 * If any of the {@code results} include images of the Views associated with the results, this 371 * method will write those images out to files. 372 * 373 * <p>The name of the output files will be "View-{R}-{S}.png" where {S} is the one-based index of 374 * the screenshot taken during the test, and {R} is an identifier of the View associated with the 375 * result. The identifier may be the name of the resource used to construct the View, or some 376 * other string if the View, View ID or resource name cannot be determined. Since there may be 377 * more than one result in a screenshot with the same View ID, a single letter ("b", "c", etc.) 378 * may be appended to {R} to avoid overwritting data. 379 */ saveResultImages( PlatformTestStorage testStorage, List<AccessibilityViewCheckResult> results)380 private void saveResultImages( 381 PlatformTestStorage testStorage, List<AccessibilityViewCheckResult> results) { 382 HashMap<String, Integer> resourceIdCounts = new HashMap<>(); 383 for (AccessibilityViewCheckResult result : results) { 384 Image viewImage = result.getViewImage(); 385 if (viewImage instanceof BitmapImage) { 386 Bitmap bitmap = ((BitmapImage) viewImage).getBitmap(); 387 String resourceId = getResourceIdentifier(result); 388 Integer resourceIdCount = resourceIdCounts.get(resourceId); 389 resourceIdCount = (resourceIdCount == null) ? 0 : (resourceIdCount + 1); 390 resourceIdCounts.put(resourceId, resourceIdCount); 391 String outputPath = 392 String.format( 393 Locale.ENGLISH, 394 "View-%s%s-%d.png", 395 resourceId, 396 (((resourceIdCount > 0) && (resourceIdCount < 26)) 397 ? Character.toString((char) ('a' + resourceIdCount)) 398 : ""), 399 screenshotsCaptured); 400 bitmapWriter.write(testStorage, bitmap, outputPath); 401 } 402 } 403 } 404 405 /** 406 * Returns a String that identifies the View associated with this result. The identifier may be 407 * the name of the resource used to construct the View, or some other string if the View, View ID 408 * or resource name cannot be determined. 409 */ getResourceIdentifier(AccessibilityViewCheckResult result)410 private static String getResourceIdentifier(AccessibilityViewCheckResult result) { 411 View view = result.getView(); 412 if (view == null) { 413 return "NO_VIEW"; 414 } 415 int viewId = view.getId(); 416 if ((viewId == View.NO_ID) || (viewId == 0)) { 417 return "NO_ID"; 418 } 419 if (view.getResources() != null && !isViewIdGenerated(viewId)) { 420 try { 421 return view.getResources().getResourceEntryName(viewId); 422 } catch (Resources.NotFoundException ignore) { 423 // Do nothing. 424 } 425 } 426 return Integer.toString(viewId); 427 } 428 429 /** 430 * IDs generated by {@link View#generateViewId} will fail if used as a resource ID in attempted 431 * resources lookups. This now logs an error in API 28, causing test failures. This method is 432 * taken from {@link View#isViewIdGenerated} to prevent resource lookup to check if a view id was 433 * generated. 434 */ isViewIdGenerated(int id)435 private static boolean isViewIdGenerated(int id) { 436 return (id & 0xFF000000) == 0 && (id & 0x00FFFFFF) != 0; 437 } 438 439 /** 440 * Reports the given check results. Any result matching {@link #suppressingMatcher} is replaced 441 * with a copy whose type is set to SUPPRESSED. 442 * 443 * <ol> 444 * <li>Calls {@link AccessibilityCheckListener#onResults} for any registered listeners. 445 * <li>Throws an {@link AccessibilityViewCheckException} containing all severe results, 446 * depending on the value of {@link #throwExceptionFor}. 447 * <li>Results of type {@code INFO}, {@code WARNING} and {@code ERROR} will be logged to logcat. 448 * </ol> 449 * 450 * @param screenshot screenshot image, if one was captured 451 * @return The same values as in {@code results}, except that any result that matches {@link 452 * #suppressingMatcher} will be replaced with a copy whose type is SUPPRESSED. 453 */ 454 @VisibleForTesting processResults( Context context, ImmutableList<AccessibilityViewCheckResult> results, @Nullable Bitmap screenshot)455 ImmutableList<AccessibilityViewCheckResult> processResults( 456 Context context, 457 ImmutableList<AccessibilityViewCheckResult> results, 458 @Nullable Bitmap screenshot) { 459 460 ImmutableList<AccessibilityViewCheckResult> processedResults = 461 suppressMatchingResults(results, suppressingMatcher); 462 for (AccessibilityCheckListener checkListener : checkListeners) { 463 checkListener.onResults(context, processedResults); 464 } 465 466 List<AccessibilityViewCheckResult> infos = 467 AccessibilityCheckResultUtils.getResultsForType( 468 processedResults, AccessibilityCheckResultType.INFO); 469 List<AccessibilityViewCheckResult> warnings = 470 AccessibilityCheckResultUtils.getResultsForType( 471 processedResults, AccessibilityCheckResultType.WARNING); 472 List<AccessibilityViewCheckResult> errors = 473 AccessibilityCheckResultUtils.getResultsForType( 474 processedResults, AccessibilityCheckResultType.ERROR); 475 476 List<AccessibilityViewCheckResult> severeResults = getSevereResults(errors, warnings, infos); 477 478 String screenshotPath = null; 479 CheckResultsCallback checkResultsCallback = 480 CheckResultsCallback.builder() 481 .setAccessibilityViewCheckResults(processedResults) 482 .setScreenshotPath(screenshotPath) 483 .build(); 484 for (CheckResultsListener checkResultsListener : checkResultsListeners) { 485 checkResultsListener.onResults(checkResultsCallback); 486 } 487 488 if (!severeResults.isEmpty()) { 489 throw new AccessibilityViewCheckException(severeResults, resultDescriptor); 490 } 491 492 for (AccessibilityViewCheckResult result : infos) { 493 Log.i(TAG, describeResult(result)); 494 } 495 for (AccessibilityViewCheckResult result : warnings) { 496 Log.w(TAG, describeResult(result)); 497 } 498 for (AccessibilityViewCheckResult result : errors) { 499 Log.w(TAG, describeResult(result)); 500 } 501 return processedResults; 502 } 503 describeResult(AccessibilityViewCheckResult result)504 private String describeResult(AccessibilityViewCheckResult result) { 505 return resultDescriptor.describeResult(result); 506 } 507 508 /** 509 * Returns a copy of the list where any result that matches the given matcher is replaced by a 510 * copy of the result with the type set to {@code SUPPRESSED}. 511 * 512 * @param results a list of {@code AccessibilityCheckResult}s to be matched against 513 * @param matcher a Matcher that determines whether a given {@code AccessibilityCheckResult} 514 * should be suppressed 515 */ 516 @VisibleForTesting suppressMatchingResults( ImmutableList<AccessibilityViewCheckResult> results, @Nullable Matcher<? super AccessibilityViewCheckResult> matcher)517 static ImmutableList<AccessibilityViewCheckResult> suppressMatchingResults( 518 ImmutableList<AccessibilityViewCheckResult> results, 519 @Nullable Matcher<? super AccessibilityViewCheckResult> matcher) { 520 if (matcher == null) { 521 return results; 522 } 523 524 return FluentIterable.from(results) 525 .transform(result -> matcher.matches(result) ? result.getSuppressedResultCopy() : result) 526 .toList(); 527 } 528 529 /** Returns {@code true} iff there is any result that is not of type {@code NOT_RUN}. */ hasRunResult(ImmutableList<AccessibilityViewCheckResult> results)530 private static boolean hasRunResult(ImmutableList<AccessibilityViewCheckResult> results) { 531 for (AccessibilityViewCheckResult result : results) { 532 if (result.getType() != AccessibilityCheckResultType.NOT_RUN) { 533 return true; 534 } 535 } 536 return false; 537 } 538 539 /** 540 * Returns the list of those results that should cause an exception to be thrown, depending upon 541 * the value of {@link #throwExceptionFor}. 542 */ getSevereResults( List<AccessibilityViewCheckResult> errors, List<AccessibilityViewCheckResult> warnings, List<AccessibilityViewCheckResult> infos)543 private List<AccessibilityViewCheckResult> getSevereResults( 544 List<AccessibilityViewCheckResult> errors, 545 List<AccessibilityViewCheckResult> warnings, 546 List<AccessibilityViewCheckResult> infos) { 547 if (throwExceptionFor != null) { 548 switch (throwExceptionFor) { 549 case ERROR: 550 if (!errors.isEmpty()) { 551 return errors; 552 } 553 break; 554 case WARNING: 555 if (!(errors.isEmpty() && warnings.isEmpty())) { 556 return new ImmutableList.Builder<AccessibilityViewCheckResult>() 557 .addAll(errors) 558 .addAll(warnings) 559 .build(); 560 } 561 break; 562 case INFO: 563 if (!(errors.isEmpty() && warnings.isEmpty() && infos.isEmpty())) { 564 return new ImmutableList.Builder<AccessibilityViewCheckResult>() 565 .addAll(errors) 566 .addAll(warnings) 567 .addAll(infos) 568 .build(); 569 } 570 break; 571 default: 572 } 573 } 574 return ImmutableList.<AccessibilityViewCheckResult>of(); 575 } 576 577 /** Interface for receiving callbacks when results have been obtained. */ 578 public static interface AccessibilityCheckListener { 579 /** 580 * @param results results from the evaluation of checks. This may include results whose {@code 581 * getType} returns {@code NOT_RUN} or {@code SUPPRESSED} if a suppressing result matcher 582 * was specified. 583 */ onResults(Context context, List<? extends AccessibilityViewCheckResult> results)584 void onResults(Context context, List<? extends AccessibilityViewCheckResult> results); 585 } 586 587 /** Interface to receive a callback after checks have been evaluated. */ 588 public static interface CheckResultsListener { onResults(CheckResultsCallback callback)589 void onResults(CheckResultsCallback callback); 590 } 591 592 /** Utility to write a Bitmap to a test output file. */ 593 @VisibleForTesting 594 static class BitmapWriter { 595 /** 596 * Writes the bitmap out to a file that will be included in the test outputs. 597 * 598 * <p>This is an expensive, synchronous operation performed on the UI thread. We really 599 * shouldn't be doing this, but don't have any convenient alternatives. 600 * 601 * @return whether bitmap was successfully written to path 602 */ 603 @CanIgnoreReturnValue write(PlatformTestStorage testStorage, Bitmap bitmap, String path)604 boolean write(PlatformTestStorage testStorage, Bitmap bitmap, String path) { 605 // StrictMode.permitCustomSlowCalls is needed to use Bitmap.compress. Normally, this operation 606 // should not be performed on the UI thread. But it is permissible here because this code 607 // should only be used for testing, and it must finish before the end of the test's lifecycle. 608 StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); 609 StrictMode.setThreadPolicy( 610 new StrictMode.ThreadPolicy.Builder(oldPolicy).permitCustomSlowCalls().build()); 611 try (BufferedOutputStream stream = 612 new BufferedOutputStream(testStorage.openOutputFile(path))) { 613 614 bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); 615 return true; 616 } catch (IOException e) { 617 Log.w(TAG, "Error writing bitmap to file", e); 618 return false; 619 } finally { 620 StrictMode.setThreadPolicy(oldPolicy); 621 } 622 } 623 } 624 625 } 626