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