public class ClassNameCheck extends AccessibilityHierarchyCheck {

  /** Result when the view is not visible */
  public static final int RESULT_ID_NOT_VISIBLE = 1;
  /** Result when the view is not {@code importantForAccessibility}. */
  public static final int RESULT_ID_NOT_IMPORTANT_FOR_ACCESSIBILITY = 2;
  /** Result when the view's {@code CharSequence} class name is null. */
  public static final int RESULT_ID_CLASS_NAME_UNKNOWN = 3;
  /** Result when the view's {@code CharSequence} class name is empty. */
  public static final int RESULT_ID_CLASS_NAME_IS_EMPTY = 4;
  /**
   * Result when the view's {@code CharSequence} class name is not supported by the accessibility
   * service.
   */
  public static final int RESULT_ID_CLASS_NAME_NOT_SUPPORTED = 5;

  /**
   * Result metadata key for the {@code String} accessibility class name for an element. Populated
   * in results with {@link #RESULT_ID_CLASS_NAME_NOT_SUPPORTED}
   */
  public static final String KEY_ACCESSIBILITY_CLASS_NAME = "KEY_ACCESSIBILITY_CLASS_NAME";

  private static final ImmutableSet<String> VALID_UI_PACKAGE_NAME_PREFIXES =
      ImmutableSet.of(
          "android.app",
          "android.appwidget",
          "android.inputmethodservice",
          "android.support",
          "android.view",
          "android.webkit",
          "android.widget",
          "androidx");

  @Override
  protected @Nullable String getHelpTopic() {
    return "7661305";
  }

  @Override
  public Category getCategory() {
    return Category.IMPLEMENTATION;
  }

  @Override
  public List<AccessibilityHierarchyCheckResult> runCheckOnHierarchy(
      AccessibilityHierarchy hierarchy,
      @Nullable ViewHierarchyElement fromRoot,
      @Nullable Parameters parameters) {
    List<AccessibilityHierarchyCheckResult> results = new ArrayList<>();

    List<? extends ViewHierarchyElement> viewsToEval = getElementsToEvaluate(fromRoot, hierarchy);
    for (ViewHierarchyElement view : viewsToEval) {
      if (!view.isImportantForAccessibility()) {
        results.add(
            new AccessibilityHierarchyCheckResult(
                this.getClass(),
                AccessibilityCheckResultType.NOT_RUN,
                view,
                RESULT_ID_NOT_IMPORTANT_FOR_ACCESSIBILITY,
                null));
        continue;
      }

      if (!TRUE.equals(view.isVisibleToUser())) {
        results.add(
            new AccessibilityHierarchyCheckResult(
                this.getClass(),
                AccessibilityCheckResultType.NOT_RUN,
                view,
                RESULT_ID_NOT_VISIBLE,
                null));
        continue;
      }

      CharSequence className = view.getAccessibilityClassName();
      if (className == null) {
        results.add(
            new AccessibilityHierarchyCheckResult(
                this.getClass(),
                AccessibilityCheckResultType.NOT_RUN,
                view,
                RESULT_ID_CLASS_NAME_UNKNOWN,
                null));
        continue;
      }

      if (TextUtils.isEmpty(className)) {
        results.add(
            new AccessibilityHierarchyCheckResult(
                this.getClass(),
                AccessibilityCheckResultType.WARNING,
                view,
                RESULT_ID_CLASS_NAME_IS_EMPTY,
                null));
        continue;
      }

      boolean isValidUiClass = false;
      for (String packageName : VALID_UI_PACKAGE_NAME_PREFIXES) {
        if (className.toString().startsWith(packageName)) {
          isValidUiClass = true;
          break;
        }
      }

      if (!isValidUiClass) {
        ResultMetadata resultMetadata = new HashMapResultMetadata();
        resultMetadata.putString(KEY_ACCESSIBILITY_CLASS_NAME, className.toString());
        results.add(
            new AccessibilityHierarchyCheckResult(
                this.getClass(),
                AccessibilityCheckResultType.WARNING,
                view,
                RESULT_ID_CLASS_NAME_NOT_SUPPORTED,
                resultMetadata));
      }
    }
    return results;
  }

  @Override
  public String getMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    switch (resultId) {
      case RESULT_ID_NOT_VISIBLE:
        return StringManager.getString(locale, "result_message_not_visible");
      case RESULT_ID_NOT_IMPORTANT_FOR_ACCESSIBILITY:
        return StringManager.getString(locale, "result_message_not_important_for_accessibility");
      case RESULT_ID_CLASS_NAME_UNKNOWN:
        return StringManager.getString(locale, "result_message_class_name_is_unknown");
      case RESULT_ID_CLASS_NAME_IS_EMPTY:
        return StringManager.getString(locale, "result_message_class_name_is_empty");
      case RESULT_ID_CLASS_NAME_NOT_SUPPORTED:
        // Metadata will have been set for this result ID
        checkNotNull(metadata);
        return String.format(
            StringManager.getString(locale, "result_message_class_name_not_supported_detail"),
            checkNotNull(metadata.getString(KEY_ACCESSIBILITY_CLASS_NAME)));
      default:
        throw new IllegalStateException("Unsupported result id");
    }
  }

  @Override
  public String getShortMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    switch (resultId) {
      case RESULT_ID_NOT_VISIBLE:
        return StringManager.getString(locale, "result_message_not_visible");
      case RESULT_ID_NOT_IMPORTANT_FOR_ACCESSIBILITY:
        return StringManager.getString(locale, "result_message_not_important_for_accessibility");
      case RESULT_ID_CLASS_NAME_UNKNOWN:
        return StringManager.getString(locale, "result_message_class_name_is_unknown");
      case RESULT_ID_CLASS_NAME_IS_EMPTY:
      case RESULT_ID_CLASS_NAME_NOT_SUPPORTED:
        return StringManager.getString(locale, "result_message_class_name_not_supported_brief");
      default:
        throw new IllegalStateException("Unsupported result id");
    }
  }

  @Override
  public String getTitleMessage(Locale locale) {
    return StringManager.getString(locale, "check_title_class_name_not_supported");
  }
} public class ClickableSpanCheck extends AccessibilityHierarchyCheck {
  /** Result when the view type could not be determined. */
  public static final int RESULT_ID_NO_DETERMINED_TYPE = 1;
  /** Result when the view is not a {@link TextView}. */
  public static final int RESULT_ID_NOT_TEXT_VIEW = 2;
  /** Result when the view contains a {@link URLSpan} with a {@code null} URL. */
  public static final int RESULT_ID_NULL_URL = 3;
  /** Result when the view contains a {@link URLSpan} with a relative URL. */
  public static final int RESULT_ID_RELATIVE_LINK = 4;
  /** Result when the view contains a {@link ClickableSpan} that is not a {@link URLSpan}. */
  public static final int RESULT_ID_CLICKABLE_SPAN = 5;
  /**
   * Result when the Android SDK version {@link DeviceState#sdkVersion} is greater than or equal to
   * Android version 8.0 (SDK 26).
   */
  public static final int RESULT_ID_VERSION_NOT_APPLICABLE = 6;

  /** From Android 8.0 O+ (SDK 26), ClickableSpans work properly with accessibility services. */
  private static final int APPLICABLE_UNTIL_ANDROID_SDK_VERSION = 26;

  @Override
  protected String getHelpTopic() {
    return "6378148"; // Clickable links
  }

  @Override
  public Category getCategory() {
    return Category.IMPLEMENTATION;
  }

  @Override
  public List<AccessibilityHierarchyCheckResult> runCheckOnHierarchy(
      AccessibilityHierarchy hierarchy,
      @Nullable ViewHierarchyElement fromRoot,
      @Nullable Parameters parameters) {
    List<AccessibilityHierarchyCheckResult> results = new ArrayList<>();
    if (hierarchy.getDeviceState().getSdkVersion() >= APPLICABLE_UNTIL_ANDROID_SDK_VERSION) {
      results.add(
          new AccessibilityHierarchyCheckResult(
              this.getClass(),
              AccessibilityCheckResultType.NOT_RUN,
              null,
              RESULT_ID_VERSION_NOT_APPLICABLE,
              null));
      return results;
    }

    List<? extends ViewHierarchyElement> viewsToEval = getElementsToEvaluate(fromRoot, hierarchy);
    for (ViewHierarchyElement element : viewsToEval) {
      if (!element.checkInstanceOf(ViewHierarchyElementUtils.TEXT_VIEW_CLASS_NAME)) {
        results.add(new AccessibilityHierarchyCheckResult(this.getClass(),
            AccessibilityCheckResultType.NOT_RUN, element, RESULT_ID_NOT_TEXT_VIEW, null));
        continue;
      } else { // Element is a TextView
        SpannableString text = element.getText();
        if (text != null) {
          for (Span span : text.getSpans()) {
            if (span instanceof Spans.URLSpan) {
              String url = ((Spans.URLSpan) span).getUrl();
              if (url == null) {
                results.add(new AccessibilityHierarchyCheckResult(this.getClass(),
                    AccessibilityCheckResultType.ERROR, element, RESULT_ID_NULL_URL, null));
              } else {
                Uri uri = new Uri(url);
                if (uri.isRelative()) {
                  // Relative URIs cannot be resolved.
                  results.add(new AccessibilityHierarchyCheckResult(this.getClass(),
                      AccessibilityCheckResultType.ERROR, element, RESULT_ID_RELATIVE_LINK, null));
                }
              }
            } else if (span instanceof Spans.ClickableSpan) { // Non-URLSpan ClickableSpan
              results.add(new AccessibilityHierarchyCheckResult(this.getClass(),
                  AccessibilityCheckResultType.ERROR, element, RESULT_ID_CLICKABLE_SPAN, null));
            }
          }
        }
      }
    }
    return results;
  }

  @Override
  public String getMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    return generateMessageForResult(locale, resultId);
  }

  @Override
  public String getShortMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    return generateMessageForResult(locale, resultId);
  }

  @Override
  public String getTitleMessage(Locale locale) {
    return StringManager.getString(locale, "check_title_clickablespan");
  }

  private static String generateMessageForResult(Locale locale, int resultId) {
    switch (resultId) {
      case RESULT_ID_VERSION_NOT_APPLICABLE:
        return String.format(
            locale,
            StringManager.getString(locale, "result_message_sdk_version_not_applicable"),
            "8.0");

      case RESULT_ID_NO_DETERMINED_TYPE:
        return StringManager.getString(locale, "result_message_clickablespan_no_determined_type");

      case RESULT_ID_NOT_TEXT_VIEW:
        return StringManager.getString(locale, "result_message_not_text_view");

      case RESULT_ID_NULL_URL:
      case RESULT_ID_RELATIVE_LINK:
        return StringManager.getString(locale, "result_message_urlspan_invalid_url");

      case RESULT_ID_CLICKABLE_SPAN:
        return StringManager.getString(locale, "result_message_urlspan_not_clickablespan");

      default:
        throw new IllegalStateException("Unsupported result id");
    }
  }
} public class DuplicateClickableBoundsCheck extends AccessibilityHierarchyCheck {
  /** Result when the view has the same bounds as another view. */
  public static final int RESULT_ID_SAME_BOUNDS = 1;
  /** [Legacy] Result to inform the user of a view's bounds .*/
  public static final int RESULT_ID_VIEW_BOUNDS = 2;

  /** Result metadata key for a {@code boolean} of whether both views were clickable. */
  public static final String KEY_CONFLICTS_BECAUSE_CLICKABLE =
      "KEY_CONFLICTS_BECAUSE_CLICKABLE";
  /** Result metadata key for a {@code boolean} of whether both views were long clickable. */
  public static final String KEY_CONFLICTS_BECAUSE_LONG_CLICKABLE =
      "KEY_CONFLICTS_BECAUSE_LONG_CLICKABLE";
  /** Result metadata key for the {@code int} number of other views with the same bounds. */
  public static final String KEY_CONFLICTING_VIEW_COUNT = "KEY_CONFLICTING_VIEW_COUNT";
  /**
   * Result metadata key for the {@code int} of the left coordinate of the location of views with
   * the same bounds.
   */
  public static final String KEY_CONFLICTING_LOCATION_LEFT = "KEY_CONFLICTING_LOCATION_LEFT";
  /**
   * Result metadata key for the {@code int} of the top coordinate of the location of views with
   * the same bounds.
   */
  public static final String KEY_CONFLICTING_LOCATION_TOP = "KEY_CONFLICTING_LOCATION_TOP";
  /**
   * Result metadata key for the {@code int} of the right coordinate of the location of views with
   * the same bounds.
   */
  public static final String KEY_CONFLICTING_LOCATION_RIGHT = "KEY_CONFLICTING_LOCATION_RIGHT";
  /**
   * Result metadata key for the {@code int} of the bottom coordinate of the location of views with
   * the same bounds.
   */
  public static final String KEY_CONFLICTING_LOCATION_BOTTOM = "KEY_CONFLICTING_LOCATION_BOTTOM";

  /** A bitmask for a clickable view. */
  public static final byte ELEMENT_MASK_CLICKABLE = 0x1;
  /** A bitmask for a long-clickable view. */
  public static final byte ELEMENT_MASK_LONG_CLICKABLE = 0x2;

  @Override
  protected String getHelpTopic() {
    return "6378943"; // Duplicate clickable Views
  }

  @Override
  public Category getCategory() {
    return Category.IMPLEMENTATION;
  }

  @Override
  public List<AccessibilityHierarchyCheckResult> runCheckOnHierarchy(
      AccessibilityHierarchy hierarchy,
      @Nullable ViewHierarchyElement fromRoot,
      @Nullable Parameters parameters) {
    List<AccessibilityHierarchyCheckResult> results = new ArrayList<>();

    /* Find all bounds and the clickable views that have those bounds within the full hierarchy */
    Map<ViewLocationActionHolder, List<ViewHierarchyElement>> locationActionToViewMap =
        getLocationActionToViewMap(hierarchy.getActiveWindow().getAllViews());

    /* Deal with any duplicate bounds within our set of elements to evaluate */
    List<? extends ViewHierarchyElement> viewsToEval =
        (fromRoot != null) ? fromRoot.getSelfAndAllDescendants() : null;
    for (List<? extends ViewHierarchyElement> elements : locationActionToViewMap.values()) {
      if (elements.size() < 2) {
        continue; // Bounds are not duplicated
      }

      for (ViewHierarchyElement culprit : elements) {
        if ((viewsToEval == null) || viewsToEval.contains(culprit)) {
          ResultMetadata resultMetadata = new HashMapResultMetadata();
          resultMetadata.putBoolean(KEY_CONFLICTS_BECAUSE_CLICKABLE, culprit.isClickable());
          resultMetadata
              .putBoolean(KEY_CONFLICTS_BECAUSE_LONG_CLICKABLE, culprit.isLongClickable());
          resultMetadata.putInt(KEY_CONFLICTING_VIEW_COUNT, elements.size() - 1);
          setBoundsInMetadata(culprit.getBoundsInScreen(), resultMetadata);
          results.add(new AccessibilityHierarchyCheckResult(
              this.getClass(),
              AccessibilityCheckResultType.ERROR,
              culprit,
              RESULT_ID_SAME_BOUNDS,
              resultMetadata));
          break;
        }
      }
    }

    return results;
  }

  @Override
  public String getMessageForResult(Locale locale, AccessibilityHierarchyCheckResult result) {
    int resultId = result.getResultId();
    if ((resultId == RESULT_ID_SAME_BOUNDS) || (resultId == RESULT_ID_VIEW_BOUNDS)) {
      ResultMetadata metadata = result.getMetadata();
      Rect bounds = getBoundsFromMetadata(metadata);
      if ((bounds == null) && (result.getElement() != null)) {
        // For legacy results, remap hierarchy element bounds to metadata
        ViewHierarchyElement culprit = result.getElement();
        ResultMetadata updatedMetadata =
            (metadata != null) ? metadata.clone() : new HashMapResultMetadata();
        setBoundsInMetadata(culprit.getBoundsInScreen(), updatedMetadata);
        AccessibilityHierarchyCheckResult updatedResult =
            new AccessibilityHierarchyCheckResult(
                this.getClass(),
                result.getType(),
                culprit,
                resultId,
                updatedMetadata);
        return super.getMessageForResult(locale, updatedResult);
      }
    }

    return super.getMessageForResult(locale, result);
  }

  @Override
  public String getMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    // For each of the following result IDs, metadata will have been set on the result, and metadata
    // will contain bounds.
    checkNotNull(metadata);
    Rect bounds = checkNotNull(getBoundsFromMetadata(metadata));
    String actionString = getActionString(
        locale,
        metadata.getBoolean(KEY_CONFLICTS_BECAUSE_CLICKABLE, false),
        metadata.getBoolean(KEY_CONFLICTS_BECAUSE_LONG_CLICKABLE, false));
    switch(resultId) {
      case RESULT_ID_SAME_BOUNDS:
        return String.format(locale,
            StringManager.getString(locale, "result_message_same_view_bounds"), actionString,
            bounds.toShortString(), metadata.getInt(KEY_CONFLICTING_VIEW_COUNT));

      // Legacy
      case RESULT_ID_VIEW_BOUNDS:
        return String.format(locale,
            StringManager.getString(locale, "result_message_view_bounds"), actionString,
            bounds.toShortString());
      default:
        throw new IllegalStateException("Unsupported result id");
    }
  }

  @Override
  public String getShortMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    // For each of the following result IDs, metadata will have been set on the result.
    checkNotNull(metadata);
    String actionString = getShortActionString(
        locale,
        metadata.getBoolean(KEY_CONFLICTS_BECAUSE_CLICKABLE),
        metadata.getBoolean(KEY_CONFLICTS_BECAUSE_LONG_CLICKABLE));
    switch(resultId) {
      case RESULT_ID_SAME_BOUNDS:
      case RESULT_ID_VIEW_BOUNDS:
        return String.format(
            locale,
            StringManager.getString(locale, "result_message_brief_same_view_bounds"),
            actionString);
      default:
        throw new IllegalStateException("Unsupported result id");
    }
  }

  @Override
  public String getTitleMessage(Locale locale) {
    return StringManager.getString(locale, "check_title_duplicate_clickable_bounds");
  }

  /**
   * @param allViews Set of views to index by their speakable text
   * @return map from speakable text to all views with that speakable text
   */
  private Map<ViewLocationActionHolder, List<ViewHierarchyElement>> getLocationActionToViewMap(
      Collection<? extends ViewHierarchyElement> allViews) {
    Map<ViewLocationActionHolder, List<ViewHierarchyElement>> locationActionToViewMap =
        new HashMap<>();

    for (ViewHierarchyElement view : allViews) {
      if (!Boolean.TRUE.equals(view.isVisibleToUser())) {
        continue;
      }

      boolean clickable = view.isClickable();
      boolean longClickable = view.isLongClickable();
      Rect bounds = view.getBoundsInScreen();
      if (view.isImportantForAccessibility() && (clickable || longClickable)) {
        ViewLocationActionHolder viewLocationActionHolder =
            new ViewLocationActionHolder(bounds, clickable, longClickable);
        if (!locationActionToViewMap.containsKey(viewLocationActionHolder)) {
          locationActionToViewMap.put(viewLocationActionHolder,
              new ArrayList<ViewHierarchyElement>());
        }
        locationActionToViewMap.get(viewLocationActionHolder).add(view);
      }
    }
    return locationActionToViewMap;
  }

  private static String getActionString(Locale locale, boolean clickable, boolean longClickable) {
    if (clickable && longClickable) {
      return StringManager.getString(locale, "clickable_and_long_clickable");
    } else if (clickable) {
      return StringManager.getString(locale, "clickable");
    } else if (longClickable) {
      return StringManager.getString(locale, "long_clickable");
    } else {
      // Consider getShortActionString if making updates to logic
      return "";
    }
  }

  private static String getShortActionString(
      Locale locale, boolean clickable, boolean longClickable) {
    if (clickable && longClickable) {
      return StringManager.getString(locale, "actionable");
    }
    return getActionString(locale, clickable, longClickable);
  }

  private static void setBoundsInMetadata(Rect rect, ResultMetadata metadata) {
    metadata.putInt(KEY_CONFLICTING_LOCATION_LEFT, rect.getLeft());
    metadata.putInt(KEY_CONFLICTING_LOCATION_TOP, rect.getTop());
    metadata.putInt(KEY_CONFLICTING_LOCATION_RIGHT, rect.getRight());
    metadata.putInt(KEY_CONFLICTING_LOCATION_BOTTOM, rect.getBottom());
  }

  private static @Nullable Rect getBoundsFromMetadata(@Nullable ResultMetadata metadata) {
    if ((metadata != null)
        && metadata.containsKey(KEY_CONFLICTING_LOCATION_LEFT)
        && metadata.containsKey(KEY_CONFLICTING_LOCATION_TOP)
        && metadata.containsKey(KEY_CONFLICTING_LOCATION_RIGHT)
        && metadata.containsKey(KEY_CONFLICTING_LOCATION_BOTTOM)) {
      return new Rect(
          metadata.getInt(KEY_CONFLICTING_LOCATION_LEFT),
          metadata.getInt(KEY_CONFLICTING_LOCATION_TOP),
          metadata.getInt(KEY_CONFLICTING_LOCATION_RIGHT),
          metadata.getInt(KEY_CONFLICTING_LOCATION_BOTTOM));
    }
    return null;
  }

  private static class ViewLocationActionHolder {
    private final Rect location;
    private final boolean clickable;
    private final boolean longClickable;

    public ViewLocationActionHolder(Rect location, boolean clickable, boolean longClickable) {
      this.location = location;
      this.clickable = clickable;
      this.longClickable = longClickable;
    }

    @Override
    public int hashCode() {
      // We explicitly ignore the action mask when computing the hash so equals() can compare
      // individual masked actions.
      return location.hashCode();
    }

    @Override
    public boolean equals(@Nullable Object obj) {
      if (this == obj) {
        return true;
      }

      if (!(obj instanceof ViewLocationActionHolder)) {
        return false;
      }

      ViewLocationActionHolder other = (ViewLocationActionHolder) obj;
      if (!location.equals(other.location)) {
        return false;
      }

      // Consider any shared action as equivalent.
      return (clickable && other.clickable) || (longClickable && other.longClickable);
    }
  }
} public class DuplicateSpeakableTextCheck extends AccessibilityHierarchyCheck {
  /** Result when two clickable views have the same speakable text. */
  public static final int RESULT_ID_CLICKABLE_SAME_SPEAKABLE_TEXT = 1;
  /** Result when two non-clickable views have the same speakable text. */
  public static final int RESULT_ID_NON_CLICKABLE_SAME_SPEAKABLE_TEXT = 2;
  /** [Legacy] Info result containing a clickable view's speakable text. */
  public static final int RESULT_ID_CLICKABLE_SPEAKABLE_TEXT = 3;
  /** [Legacy] Info result containing a non-clickable view's speakable text. */
  public static final int RESULT_ID_NON_CLICKABLE_SPEAKABLE_TEXT = 4;

  /** Result metadata key for a view's speakable text {@link String} */
  public static final String KEY_SPEAKABLE_TEXT = "KEY_SPEAKABLE_TEXT";
  /** Result metadata key for the {@code int} number of other views with the same speakable text. */
  public static final String KEY_CONFLICTING_VIEW_COUNT = "KEY_CONFLICTING_VIEW_COUNT";

  @Override
  protected String getHelpTopic() {
    return "7102513"; // Duplicate descriptions
  }

  @Override
  public Category getCategory() {
    return Category.CONTENT_LABELING;
  }

  @Override
  public List<AccessibilityHierarchyCheckResult> runCheckOnHierarchy(
      AccessibilityHierarchy hierarchy,
      @Nullable ViewHierarchyElement fromRoot,
      @Nullable Parameters parameters) {
    List<AccessibilityHierarchyCheckResult> results = new ArrayList<>();

    /* Find all text and the views that have that text throughout the full hierarchy */
    Map<String, List<ViewHierarchyElement>> textToViewMap = getSpeakableTextToViewMap(
        hierarchy.getActiveWindow().getAllViews());

    /* Deal with any duplicated text */
    for (String speakableText : textToViewMap.keySet()) {
      if (textToViewMap.get(speakableText).size() < 2) {
        continue; // Text is not duplicated
      }

      // We've found duplicated text. Sort the Views into clickable and non-clickable if they're
      // within scope for evaluation.
      List<ViewHierarchyElement> clickableViews = new ArrayList<>();
      List<ViewHierarchyElement> nonClickableViews = new ArrayList<>();
      List<? extends ViewHierarchyElement> viewsToEval =
          (fromRoot != null) ? fromRoot.getSelfAndAllDescendants() : null;
      for (ViewHierarchyElement view : textToViewMap.get(speakableText)) {
        if ((viewsToEval == null) || viewsToEval.contains(view)) {
          if (Boolean.TRUE.equals(view.isClickable())) {
            clickableViews.add(view);
          } else {
            nonClickableViews.add(view);
          }
        }
      }

      if (!clickableViews.isEmpty()) {
        /* Display warning */
        ResultMetadata resultMetadata = new HashMapResultMetadata();
        resultMetadata.putString(
            KEY_SPEAKABLE_TEXT, speakableText);
        resultMetadata.putInt(KEY_CONFLICTING_VIEW_COUNT,
            (clickableViews.size() + nonClickableViews.size() - 1));
        results.add(new AccessibilityHierarchyCheckResult(
            this.getClass(),
            AccessibilityCheckResultType.WARNING,
            clickableViews.get(0),
            RESULT_ID_CLICKABLE_SAME_SPEAKABLE_TEXT,
            resultMetadata));
        clickableViews.remove(0);
      } else if (!nonClickableViews.isEmpty()) {
        /* Only duplication is on non-clickable views */
        ResultMetadata resultMetadata = new HashMapResultMetadata();
        resultMetadata.putString(
            KEY_SPEAKABLE_TEXT, speakableText);
        resultMetadata.putInt(KEY_CONFLICTING_VIEW_COUNT,
            (clickableViews.size() + nonClickableViews.size() - 1));
        results.add(new AccessibilityHierarchyCheckResult(
            this.getClass(),
            AccessibilityCheckResultType.INFO,
            nonClickableViews.get(0),
            RESULT_ID_NON_CLICKABLE_SAME_SPEAKABLE_TEXT,
            resultMetadata));
        nonClickableViews.remove(0);
      }
    }

    return results;
  }

  @Override
  public String getMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    // For each of the following result IDs, metadata will have been set on the result.
    checkNotNull(metadata);
    switch (resultId) {
      case RESULT_ID_CLICKABLE_SAME_SPEAKABLE_TEXT:
        return String.format(locale,
            StringManager.getString(locale, "result_message_same_speakable_text"),
            StringManager.getString(locale, "clickable"),
            metadata.getString(KEY_SPEAKABLE_TEXT),
            metadata.getInt(KEY_CONFLICTING_VIEW_COUNT));
      case RESULT_ID_NON_CLICKABLE_SAME_SPEAKABLE_TEXT:
        return String.format(locale,
            StringManager.getString(locale, "result_message_same_speakable_text"),
            StringManager.getString(locale, "non_clickable"),
            metadata.getString(KEY_SPEAKABLE_TEXT),
            metadata.getInt(KEY_CONFLICTING_VIEW_COUNT));
      // Legacy
      case RESULT_ID_CLICKABLE_SPEAKABLE_TEXT:
        return String.format(locale,
            StringManager.getString(locale, "result_message_speakable_text"),
            StringManager.getString(locale, "clickable"),
            metadata.getString(KEY_SPEAKABLE_TEXT));
      case RESULT_ID_NON_CLICKABLE_SPEAKABLE_TEXT:
        return String.format(locale,
            StringManager.getString(locale, "result_message_speakable_text"),
            StringManager.getString(locale, "non_clickable"),
            metadata.getString(KEY_SPEAKABLE_TEXT));
      default:
        throw new IllegalStateException("Unsupported result id");
    }
  }

  @Override
  public String getShortMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    switch (resultId) {
      case RESULT_ID_CLICKABLE_SAME_SPEAKABLE_TEXT:
      case RESULT_ID_NON_CLICKABLE_SAME_SPEAKABLE_TEXT:
      case RESULT_ID_CLICKABLE_SPEAKABLE_TEXT:
      case RESULT_ID_NON_CLICKABLE_SPEAKABLE_TEXT:
        return StringManager.getString(locale, "result_message_brief_same_speakable_text");
      default:
        throw new IllegalStateException("Unsupported result id");
    }
  }

  /**
   * Calculates a secondary priority for a duplicate speakable text result.
   *
   * <p>The result is the number of other views with the same text. Thus, the greater the number of
   * repetitions, the higher the priority.
   */
  @Override
  public @Nullable Double getSecondaryPriority(AccessibilityHierarchyCheckResult result) {
    ResultMetadata metadata = result.getMetadata();
    switch (result.getResultId()) {
      case RESULT_ID_CLICKABLE_SAME_SPEAKABLE_TEXT:
      case RESULT_ID_NON_CLICKABLE_SAME_SPEAKABLE_TEXT:
        return (double) checkNotNull(metadata).getInt(KEY_CONFLICTING_VIEW_COUNT, 0);
      default:
        return null;
    }
  }

  @Override
  public String getTitleMessage(Locale locale) {
    return StringManager.getString(locale, "check_title_duplicate_speakable_text");
  }

  /**
   * @param allViews Set of views to index by their speakable text
   * @return map from speakable text to all views with that speakable text
   */
  private Map<String, List<ViewHierarchyElement>> getSpeakableTextToViewMap(
      Collection<? extends ViewHierarchyElement> allViews) {
    Map<String, List<ViewHierarchyElement>> textToViewMap = new HashMap<>();

    for (ViewHierarchyElement view : allViews) {
      if (!ViewHierarchyElementUtils.shouldFocusView(view)) {
        // If the screen reader won't focus the control, the description is unimportant
        continue;
      }

      String speakableText =
          ViewHierarchyElementUtils.getSpeakableTextForElement(view).toString().trim();
      if (TextUtils.isEmpty(speakableText)) {
        continue;
      }

      if (!textToViewMap.containsKey(speakableText)) {
        textToViewMap.put(speakableText, new ArrayList<ViewHierarchyElement>());
      }
      textToViewMap.get(speakableText).add(view);
    }
    return textToViewMap;
  }
} public class EditableContentDescCheck extends AccessibilityHierarchyCheck {

  /** Result when thew view is not {@code importantForAccessibility} */
  public static final int RESULT_ID_NOT_IMPORTANT_FOR_ACCESSIBILITY = 1;
  /** Result when the view is an editable {@link TextView} with a {@code contentDescription}. */
  public static final int RESULT_ID_EDITABLE_TEXTVIEW_CONTENT_DESC = 2;
  /** Result when the view is not an editable {@link TextView}. */
  public static final int RESULT_ID_NOT_EDITABLE_TEXTVIEW = 3;

  @Override
  protected String getHelpTopic() {
    return "6378120"; // Editable View labels
  }

  @Override
  public Category getCategory() {
    return Category.IMPLEMENTATION;
  }

  @Override
  public List<AccessibilityHierarchyCheckResult> runCheckOnHierarchy(
      AccessibilityHierarchy hierarchy,
      @Nullable ViewHierarchyElement fromRoot,
      @Nullable Parameters parameters) {
    List<AccessibilityHierarchyCheckResult> results = new ArrayList<>();
    List<? extends ViewHierarchyElement> viewsToEval = getElementsToEvaluate(fromRoot, hierarchy);
    for (ViewHierarchyElement view : viewsToEval) {
      if (!view.isImportantForAccessibility()) {
        results.add(new AccessibilityHierarchyCheckResult(
            this.getClass(),
            AccessibilityCheckResultType.NOT_RUN,
            view,
            RESULT_ID_NOT_IMPORTANT_FOR_ACCESSIBILITY,
            null));
        continue;
      }

      if (TRUE.equals(view.isEditable())
          || view.checkInstanceOf(ViewHierarchyElementUtils.EDIT_TEXT_CLASS_NAME)) {
        if (!TextUtils.isEmpty(view.getContentDescription())) {
          results.add(new AccessibilityHierarchyCheckResult(this.getClass(),
              AccessibilityCheckResultType.ERROR,
              view,
              RESULT_ID_EDITABLE_TEXTVIEW_CONTENT_DESC,
              null));
        }
      } else {
        results.add(new AccessibilityHierarchyCheckResult(this.getClass(),
            AccessibilityCheckResultType.NOT_RUN,
            view,
            RESULT_ID_NOT_EDITABLE_TEXTVIEW,
            null));
      }
    }

    return results;
  }

  @Override
  public String getMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    return generateMessageForResultId(locale, resultId);
  }

  @Override
  public String getShortMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    return generateMessageForResultId(locale, resultId);
  }

  @Override
  public String getTitleMessage(Locale locale) {
    return StringManager.getString(locale, "check_title_editable_content_desc");
  }

  private static String generateMessageForResultId(Locale locale, int resultId) {
    switch(resultId) {
      case RESULT_ID_NOT_IMPORTANT_FOR_ACCESSIBILITY:
        return StringManager.getString(locale, "result_message_not_important_for_accessibility");
      case RESULT_ID_EDITABLE_TEXTVIEW_CONTENT_DESC:
        return StringManager.getString(locale, "result_message_editable_textview_content_desc");
      case RESULT_ID_NOT_EDITABLE_TEXTVIEW:
        return StringManager.getString(locale, "result_message_not_editable_textview");
      default:
        throw new IllegalStateException("Unsupported result id");
    }
  }
} public class ImageContrastCheck extends AccessibilityHierarchyCheck {

  /** Result when the evaluated element was determined to be not visible to the user */
  public static final int RESULT_ID_NOT_VISIBLE = 1;
  /** Result when the evaluated element was determined to be a view type other than an ImageView */
  public static final int RESULT_ID_NOT_IMAGEVIEW = 2;
  /** Result when we did not receive screen capture information */
  public static final int RESULT_ID_NO_SCREENCAPTURE = 3;
  /** Result when the evaluated element was not within the bounds of the provided capture image */
  public static final int RESULT_ID_VIEW_NOT_WITHIN_SCREENCAPTURE = 4;
  /** Result when the evaluated element's contrast ratio falls below the suggested value */
  public static final int RESULT_ID_IMAGE_CONTRAST_NOT_SUFFICIENT = 5;
  /** Result when the evaluated element was determined to be not enabled */
  public static final int RESULT_ID_NOT_ENABLED = 6;
  /** Result when the evaluated element's screen capture data was determined to be hidden */
  public static final int RESULT_ID_SCREENCAPTURE_DATA_HIDDEN = 7;
  /**
   * Result when the evaluated element's contrast ratio falls below the user-defined contrast ratio
   */
  public static final int RESULT_ID_CUSTOMIZED_IMAGE_CONTRAST_NOT_SUFFICIENT = 8;
  /** Result when the evaluated element's screen capture has a uniform color */
  public static final int RESULT_ID_SCREENCAPTURE_UNIFORM_COLOR = 9;

  /** Result metadata key for the {@code int} background color of the view. */
  public static final String KEY_BACKGROUND_COLOR = "KEY_BACKGROUND_COLOR";
  /** Result metadata key for the {@code double} computed contrast ratio. */
  public static final String KEY_CONTRAST_RATIO = "KEY_CONTRAST_RATIO";
  /** Result metadata key for the {@code int} foreground color of the view. */
  public static final String KEY_FOREGROUND_COLOR = "KEY_FOREGROUND_COLOR";
  /** Result metadata key for the {@code String} bounds of the screenshot. */
  public static final String KEY_SCREENSHOT_BOUNDS_STRING = "KEY_SCREENSHOT_BOUNDS_STRING";
  /** Result metadata key for the {@code String} bounds of the view. */
  public static final String KEY_VIEW_BOUNDS_STRING = "KEY_VIEW_BOUNDS_STRING";
  /** Result metadata key for the {@code double} user-defined heuristic contrast ratio. */
  public static final String KEY_CUSTOMIZED_HEURISTIC_CONTRAST_RATIO =
      "KEY_CUSTOMIZED_HEURISTIC_CONTRAST_RATIO";
  /** Result metadata key for whether the view may be obscured by other on-screen content. */
  public static final String KEY_IS_POTENTIALLY_OBSCURED = "KEY_IS_POTENTIALLY_OBSCURED";
  /**
   * Result metadata key for a {@code boolean} which is {@code true} iff the view is determined to
   * be touching the scrollable edge of a scrollable container.
   */
  public static final String KEY_IS_AGAINST_SCROLLABLE_EDGE = "KEY_IS_AGAINST_SCROLLABLE_EDGE";

  /** Result metadata key for the {@code ArrayList<String>} foreground colors of the view. */
  public static final String KEY_ADDITIONAL_FOREGROUND_COLORS = "KEY_ADDITIONAL_FOREGROUND_COLORS";
  /** Result metadata key for the {@code ArrayList<String>} computed contrast ratio. */
  public static final String KEY_ADDITIONAL_CONTRAST_RATIOS = "KEY_ADDITIONAL_CONTRAST_RATIOS";

  /** The amount by which a view's computed contrast ratio may fall below defined thresholds */
  public static final double CONTRAST_TOLERANCE = 0.01;

  private static final Class<? extends AccessibilityHierarchyCheck> CHECK_CLASS =
      ImageContrastCheck.class;

  @Override
  protected String getHelpTopic() {
    return "7158390"; // Color contrast
  }

  @Override
  public Category getCategory() {
    return Category.LOW_CONTRAST;
  }

  @Override
  public List<AccessibilityHierarchyCheckResult> runCheckOnHierarchy(
      AccessibilityHierarchy hierarchy,
      @Nullable ViewHierarchyElement fromRoot,
      @Nullable Parameters parameters) {
    List<AccessibilityHierarchyCheckResult> results = new ArrayList<>();
    List<? extends ViewHierarchyElement> viewsToEval = getElementsToEvaluate(fromRoot, hierarchy);
    for (ViewHierarchyElement view : viewsToEval) {
      if (!Boolean.TRUE.equals(view.isVisibleToUser())) {
        results.add(
            new AccessibilityHierarchyCheckResult(
                CHECK_CLASS,
                AccessibilityCheckResultType.NOT_RUN,
                view,
                RESULT_ID_NOT_VISIBLE,
                null));
        continue;
      }

      if (!view.checkInstanceOf(ViewHierarchyElementUtils.IMAGE_VIEW_CLASS_NAME)) {
        results.add(
            new AccessibilityHierarchyCheckResult(
                CHECK_CLASS,
                AccessibilityCheckResultType.NOT_RUN,
                view,
                RESULT_ID_NOT_IMAGEVIEW,
                null));
        continue;
      }

      if (!view.isEnabled()) {
        results.add(
            new AccessibilityHierarchyCheckResult(
                CHECK_CLASS,
                AccessibilityCheckResultType.NOT_RUN,
                view,
                RESULT_ID_NOT_ENABLED,
                null));
        continue;
      }

      AccessibilityHierarchyCheckResult heavyweightResult =
          attemptHeavyweightEval(view, parameters);
      if (heavyweightResult != null) {
        results.add(heavyweightResult);
      }
    }

    return results;
  }

  /**
   * Performs heavyweight contrast evaluation on the provided {@code view}. During heavyweight
   * evaluation, we examine the contents of a screen capture to heuristically determine foreground
   * and background colors, and from there, compute a contrast ratio.
   *
   * @param view The {@link ViewHierarchyElement} to evaluate
   * @param parameters Optional check input parameters
   * @return an {@link AccessibilityHierarchyCheckResult} describing the results of the heavyweight
   *     evaluation, or {@code null} if there is no problem detected.
   */
  @Nullable
  private AccessibilityHierarchyCheckResult attemptHeavyweightEval(
      ViewHierarchyElement view, @Nullable Parameters parameters) {
    Image screenCapture = (parameters == null) ? null : parameters.getScreenCapture();
    if (screenCapture == null) {
      return new AccessibilityHierarchyCheckResult(
          CHECK_CLASS,
          AccessibilityCheckResultType.NOT_RUN,
          view,
          RESULT_ID_NO_SCREENCAPTURE,
          null);
    }

    Rect screenCaptureBounds = new Rect(0, 0, screenCapture.getWidth(), screenCapture.getHeight());
    Rect viewBounds = view.getBoundsInScreen();
    if (viewBounds.isEmpty() || !screenCaptureBounds.contains(viewBounds)) {
      // If an off-screen view reports itself as visible, we shouldn't evaluate it.
      ResultMetadata resultMetadata = new HashMapResultMetadata();
      resultMetadata.putString(KEY_VIEW_BOUNDS_STRING, viewBounds.toShortString());
      resultMetadata.putString(KEY_SCREENSHOT_BOUNDS_STRING, screenCaptureBounds.toShortString());
      return new AccessibilityHierarchyCheckResult(
          CHECK_CLASS,
          AccessibilityCheckResultType.NOT_RUN,
          view,
          RESULT_ID_VIEW_NOT_WITHIN_SCREENCAPTURE,
          resultMetadata);
    }

    // viewBounds cannot be out of bounds because the bounds were checked above.
    Image viewImage = crop(screenCapture, viewBounds);
    ContrastSwatch contrastSwatch =
        getContrastSwatch(
            viewImage,
            (parameters == null) ? null : parameters.getEnableEnhancedContrastEvaluation());
    ResultMetadata resultMetadata = new HashMapResultMetadata();
    if (view.isAgainstScrollableEdge()) {
      resultMetadata.putBoolean(KEY_IS_AGAINST_SCROLLABLE_EDGE, true);
    }
    int foreground = contrastSwatch.getForegroundColors().get(0);
    int background = contrastSwatch.getBackgroundColor();

    if (background == foreground) {
      // In the case the foreground and background colors from ContrastSwatch are the same, and
      // they match the secure window censor color, we can assume the system has intentionally
      // censored our screen capture data, and we should not evaluate this content.
      return new AccessibilityHierarchyCheckResult(
          CHECK_CLASS,
          AccessibilityCheckResultType.NOT_RUN,
          view,
          (foreground == ContrastUtils.COLOR_SECURE_WINDOW_CENSOR)
              ? RESULT_ID_SCREENCAPTURE_DATA_HIDDEN
              : RESULT_ID_SCREENCAPTURE_UNIFORM_COLOR,
          resultMetadata);
    }

    List<Integer> foregroundColors = contrastSwatch.getForegroundColors();
    List<Double> contrastRatios = contrastSwatch.getContrastRatios();
    ArrayList<Integer> lowForegroundColors = new ArrayList<>();
    ArrayList<Double> lowContrastRatios = new ArrayList<>();

    // Lower confidence in heuristics for ImageViews, so we'll report only warnings and use the
    // more permissive threshold ratio since images are generally large.
    Double customizedHeuristicContrastRatio =
        (parameters == null) ? null : parameters.getCustomImageContrastRatio();
    if (customizedHeuristicContrastRatio != null) {

      for (int i = 0; i < contrastRatios.size(); i++) {
        if (customizedHeuristicContrastRatio - contrastRatios.get(i) > CONTRAST_TOLERANCE) {
          lowForegroundColors.add(foregroundColors.get(i));
          lowContrastRatios.add(contrastRatios.get(i));
        }
      }
      if (!lowContrastRatios.isEmpty()) {
        if (isPotentiallyObscured(view)) {
          resultMetadata.putBoolean(KEY_IS_POTENTIALLY_OBSCURED, true);
        }
        resultMetadata.putDouble(
            KEY_CUSTOMIZED_HEURISTIC_CONTRAST_RATIO, customizedHeuristicContrastRatio);
        storeColorsAndContrastRatios(
            resultMetadata, background, lowForegroundColors, lowContrastRatios);
        return resultPossiblyWithImage(
            AccessibilityCheckResultType.WARNING,
            view,
            RESULT_ID_CUSTOMIZED_IMAGE_CONTRAST_NOT_SUFFICIENT,
            resultMetadata,
            parameters,
            viewImage);
      }
    } else {
      for (int i = 0; i < contrastRatios.size(); i++) {
        if ((ContrastUtils.CONTRAST_RATIO_WCAG_LARGE_TEXT - contrastRatios.get(i))
            > CONTRAST_TOLERANCE) {
          lowForegroundColors.add(foregroundColors.get(i));
          lowContrastRatios.add(contrastRatios.get(i));
        }
      }
      if (!lowContrastRatios.isEmpty()) {
        if (isPotentiallyObscured(view)) {
          resultMetadata.putBoolean(KEY_IS_POTENTIALLY_OBSCURED, true);
        }
        storeColorsAndContrastRatios(
            resultMetadata, background, lowForegroundColors, lowContrastRatios);
        return resultPossiblyWithImage(
            AccessibilityCheckResultType.WARNING,
            view,
            RESULT_ID_IMAGE_CONTRAST_NOT_SUFFICIENT,
            resultMetadata,
            parameters,
            viewImage);
      }
    }
    return null;
  }

  @Override
  public String getMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    String generated = generateMessageForResultId(locale, resultId);
    if (generated != null) {
      return generated;
    }

    // For each of the following result IDs, metadata will have been set on the result.

    StringBuilder builder;
    checkNotNull(metadata);
    switch (resultId) {
      case RESULT_ID_VIEW_NOT_WITHIN_SCREENCAPTURE:
        return String.format(
            locale,
            StringManager.getString(locale, "result_message_view_not_within_screencapture"),
            metadata.getString(KEY_VIEW_BOUNDS_STRING),
            metadata.getString(KEY_SCREENSHOT_BOUNDS_STRING));
      case RESULT_ID_IMAGE_CONTRAST_NOT_SUFFICIENT:
        builder =
            new StringBuilder(
                String.format(
                    locale,
                    StringManager.getString(locale, "result_message_image_contrast_not_sufficient"),
                    metadata.getDouble(KEY_CONTRAST_RATIO),
                    ContrastUtils.CONTRAST_RATIO_WCAG_LARGE_TEXT,
                    metadata.getInt(KEY_FOREGROUND_COLOR) & 0xFFFFFF,
                    metadata.getInt(KEY_BACKGROUND_COLOR) & 0xFFFFFF));
        appendMetadataStringsToMessageIfNeeded(locale, metadata, builder);
        return builder.toString();
      case RESULT_ID_CUSTOMIZED_IMAGE_CONTRAST_NOT_SUFFICIENT:
        builder =
            new StringBuilder(
                String.format(
                    locale,
                    StringManager.getString(
                        locale, "result_message_image_customized_contrast_not_sufficient"),
                    metadata.getDouble(KEY_CONTRAST_RATIO),
                    metadata.getDouble(KEY_CUSTOMIZED_HEURISTIC_CONTRAST_RATIO),
                    metadata.getInt(KEY_FOREGROUND_COLOR) & 0xFFFFFF,
                    metadata.getInt(KEY_BACKGROUND_COLOR) & 0xFFFFFF));
        appendMetadataStringsToMessageIfNeeded(locale, metadata, builder);
        return builder.toString();
      default:
        throw new IllegalStateException("Unsupported result id");
    }
  }

  @Override
  public String getShortMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    String generated = generateMessageForResultId(locale, resultId);
    if (generated != null) {
      return generated;
    }

    switch (resultId) {
      case RESULT_ID_VIEW_NOT_WITHIN_SCREENCAPTURE:
        return StringManager.getString(locale, "result_message_no_screencapture"); // Close enough
      case RESULT_ID_IMAGE_CONTRAST_NOT_SUFFICIENT:
      case RESULT_ID_CUSTOMIZED_IMAGE_CONTRAST_NOT_SUFFICIENT:
        return StringManager.getString(
            locale, "result_message_brief_image_contrast_not_sufficient");
      default:
        throw new IllegalStateException("Unsupported result id");
    }
  }

  /**
   * Calculates a secondary priority for an image contrast result.
   *
   * <p>The result is the amount that the actual contrast ratio is below the required ratio. Thus,
   * for a given required contrast ratio, decreasing contrast gives a higher priority.
   */
  @Override
  public @Nullable Double getSecondaryPriority(AccessibilityHierarchyCheckResult result) {
    ResultMetadata metadata = result.getMetadata();
    switch (result.getResultId()) {
      case RESULT_ID_IMAGE_CONTRAST_NOT_SUFFICIENT:
        return ContrastUtils.CONTRAST_RATIO_WCAG_LARGE_TEXT
            - checkNotNull(metadata).getDouble(KEY_CONTRAST_RATIO, 0.0);
      case RESULT_ID_CUSTOMIZED_IMAGE_CONTRAST_NOT_SUFFICIENT:
        return checkNotNull(metadata).getDouble(KEY_CUSTOMIZED_HEURISTIC_CONTRAST_RATIO, 0)
            - checkNotNull(metadata).getDouble(KEY_CONTRAST_RATIO, 0.0);
      default:
        return null;
    }
  }

  @Override
  public String getTitleMessage(Locale locale) {
    return StringManager.getString(locale, "check_title_image_contrast");
  }

  @VisibleForTesting
  ContrastSwatch getContrastSwatch(
      Image image, @Nullable Boolean enableEnhancedContrastEvaluation) {
    return new ContrastSwatch(
        image,
        (enableEnhancedContrastEvaluation == null) ? false : enableEnhancedContrastEvaluation);
  }

  private static Image crop(Image screenCapture, Rect viewBounds) {
    return screenCapture.crop(
        viewBounds.getLeft(), viewBounds.getTop(), viewBounds.getWidth(), viewBounds.getHeight());
  }

  /**
   * Returns a new instance of AccessibilityHierarchyCheckResultWithImage if {@code viewImage} is
   * not {@code null} and {@code parameters} specifies that view images should be saved. Otherwise,
   * returns a new instance of AccessibilityHierarchyCheckResult.
   */
  private static AccessibilityHierarchyCheckResult resultPossiblyWithImage(
      AccessibilityCheckResultType type,
      ViewHierarchyElement view,
      int resultId,
      ResultMetadata metadata,
      @Nullable Parameters parameters,
      @Nullable Image viewImage) {
    if ((viewImage != null)
        && (parameters != null)
        && Boolean.TRUE.equals(parameters.getSaveViewImages())) {
      return new AccessibilityHierarchyCheckResultWithImage(
          CHECK_CLASS, type, view, resultId, metadata, viewImage);
    } else {
      return new AccessibilityHierarchyCheckResult(CHECK_CLASS, type, view, resultId, metadata);
    }
  }

  /** Stores extracted or estimated colors and contrast ratios in metadata. */
  private static void storeColorsAndContrastRatios(
      ResultMetadata resultMetadata,
      int background,
      List<Integer> foregroundColors,
      List<Double> contrastRatios) {
    resultMetadata.putInt(KEY_BACKGROUND_COLOR, background);
    storeForegroundColors(resultMetadata, foregroundColors);
    storeContrastRatios(resultMetadata, contrastRatios);
  }

  /**
   * Stores foreground colors in metadata. The first color is stored with the key {@link
   * #KEY_FOREGROUND_COLOR}. If {@code foregroundColors} has more than one value, all but the first
   * value will be stored with the key {@link #KEY_ADDITIONAL_FOREGROUND_COLORS}.
   */
  private static void storeForegroundColors(
      ResultMetadata resultMetadata, List<Integer> foregroundColors) {
    resultMetadata.putInt(KEY_FOREGROUND_COLOR, foregroundColors.get(0));
    if (foregroundColors.size() > 1) {
      resultMetadata.putStringList(
          KEY_ADDITIONAL_FOREGROUND_COLORS,
          Lists.transform(
              foregroundColors.subList(1, foregroundColors.size()), integer -> integer.toString()));
    }
  }

  /**
   * Stores contrast ratios in metadata. The first ratio is stored with the key {@link
   * #KEY_CONTRAST_RATIO}. If {@code contrastRatios} has more than one value, all but the first
   * value will be stored with the key {@link #KEY_ADDITIONAL_CONTRAST_RATIOS}.
   */
  private static void storeContrastRatios(
      ResultMetadata resultMetadata, List<Double> contrastRatios) {
    resultMetadata.putDouble(KEY_CONTRAST_RATIO, contrastRatios.get(0));
    if (contrastRatios.size() > 1) {
      resultMetadata.putStringList(
          KEY_ADDITIONAL_CONTRAST_RATIOS,
          Lists.transform(
              contrastRatios.subList(1, contrastRatios.size()), ratio -> ratio.toString()));
    }
  }

  /**
   * Appends messages for {@link #KEY_IS_POTENTIALLY_OBSCURED} and {@link
   * #KEY_IS_AGAINST_SCROLLABLE_EDGE} to the provided {@code builder} if the relevant keys are set
   * in the given {@code resultMetadata}.
   *
   * @param builder the {@link StringBuilder} to which result messages should be appended
   */
  @SuppressWarnings("unused") // {@code locale} and {@code builder} may not be used
  private static void appendMetadataStringsToMessageIfNeeded(
      Locale locale, ResultMetadata resultMetadata, StringBuilder builder) {
    if (resultMetadata.getBoolean(KEY_IS_POTENTIALLY_OBSCURED, false)) {
      builder
          .append(' ')
          .append(
              StringManager.getString(locale, "result_message_addendum_view_potentially_obscured"));
    }
    if (resultMetadata.getBoolean(KEY_IS_AGAINST_SCROLLABLE_EDGE, false)) {
      builder
          .append(' ')
          .append(
              StringManager.getString(locale, "result_message_addendum_against_scrollable_edge"));
    }
  }

  private static @Nullable String generateMessageForResultId(Locale locale, int resultId) {
    switch (resultId) {
      case RESULT_ID_NOT_VISIBLE:
        return StringManager.getString(locale, "result_message_not_visible");
      case RESULT_ID_NOT_IMAGEVIEW:
        return StringManager.getString(locale, "result_message_not_imageview");
      case RESULT_ID_NOT_ENABLED:
        return StringManager.getString(locale, "result_message_not_enabled");
      case RESULT_ID_NO_SCREENCAPTURE:
        return StringManager.getString(locale, "result_message_no_screencapture");
      case RESULT_ID_SCREENCAPTURE_DATA_HIDDEN:
        return StringManager.getString(locale, "result_message_screencapture_data_hidden");
      case RESULT_ID_SCREENCAPTURE_UNIFORM_COLOR:
        return StringManager.getString(locale, "result_message_screencapture_uniform_color");
      default:
        return null;
    }
  }
} public class LinkPurposeUnclearCheck extends AccessibilityHierarchyCheck {

  /** Result when the locale is not English. */
  public static final int RESULT_ID_ENGLISH_LOCALE_ONLY = 1;
  /** Result when the view is not a {@link TextView}. */
  public static final int RESULT_ID_NOT_TEXT_VIEW = 2;
  /** Result when the view contains a {@link URLSpan} with link text that is not descriptive. */
  public static final int RESULT_ID_LINK_TEXT_NOT_DESCRIPTIVE = 3;

  /** Result metadata key for a view's speakable text {@link String} */
  public static final String KEY_LINK_TEXT = "KEY_LINK_TEXT";

  /*
   * From src/audits/LinkWithUnclearPurpose.js
   * in https://github.com/GoogleChrome/accessibility-developer-tools
   */
  private static final ImmutableList<String> ENGLISH_STOPWORDS =
      ImmutableList.of(
          "click", "tap", "go", "here", "learn", "more", "this", "page", "link", "about");

  private static final Pattern WORD_PATTERN = Pattern.compile("\\w+");

  @Override
  @Nullable
  protected String getHelpTopic() {
    return "9663312"; // Link purpose unclear
  }

  @Override
  public Category getCategory() {
    return Category.CONTENT_LABELING;
  }

  @Override
  public List<AccessibilityHierarchyCheckResult> runCheckOnHierarchy(
      AccessibilityHierarchy hierarchy,
      @Nullable ViewHierarchyElement fromRoot,
      @Nullable Parameters parameters) {
    List<AccessibilityHierarchyCheckResult> results = new ArrayList<>();

    if (!isEnglish(hierarchy)) {
      results.add(
          new AccessibilityHierarchyCheckResult(
              this.getClass(),
              AccessibilityCheckResultType.NOT_RUN,
              null,
              RESULT_ID_ENGLISH_LOCALE_ONLY,
              null));
      return results;
    }

    List<? extends ViewHierarchyElement> viewsToEval = getElementsToEvaluate(fromRoot, hierarchy);
    for (ViewHierarchyElement element : viewsToEval) {
      if (!element.checkInstanceOf(ViewHierarchyElementUtils.TEXT_VIEW_CLASS_NAME)) {
        results.add(
            new AccessibilityHierarchyCheckResult(
                this.getClass(),
                AccessibilityCheckResultType.NOT_RUN,
                element,
                RESULT_ID_NOT_TEXT_VIEW,
                null));
      } else { // Element is a TextView
        SpannableString text = element.getText();
        if (text != null) {
          for (Span span : text.getSpans()) {
            if (span instanceof Spans.ClickableSpan) {
              CharSequence linkText = text.subSequence(span.getStart(), span.getEnd());
              if (hasOnlyStopwords(linkText)) {
                ResultMetadata resultMetadata = new HashMapResultMetadata();
                resultMetadata.putString(KEY_LINK_TEXT, linkText.toString());
                results.add(
                    new AccessibilityHierarchyCheckResult(
                        this.getClass(),
                        AccessibilityCheckResultType.WARNING,
                        element,
                        RESULT_ID_LINK_TEXT_NOT_DESCRIPTIVE,
                        resultMetadata));
              }
            }
          }
        }
      }
    }
    return results;
  }

  /** Indicates whether all of the word characters are in the form of stopwords. */
  @VisibleForTesting
  static boolean hasOnlyStopwords(CharSequence linkText) {
    Matcher m = WORD_PATTERN.matcher(linkText);
    while (m.find()) {
      if (!isStopword(m.group())) {
        return false;
      }
    }

    return true;
  }

  /** Is this one of the ENGLISH_STOPWORDS, ignoring case? */

  private static boolean isStopword(String term) {
    for (String word : ENGLISH_STOPWORDS) {
      if (Ascii.equalsIgnoreCase(word, term)) {
        return true;
      }
    }
    return false;
  }

  @Override
  public String getMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    String generated = generateMessageForResultId(locale, resultId);
    if (generated != null) {
      return generated;
    }

    // For each of the following result IDs, metadata will have been set on the result.
    checkNotNull(metadata);
    switch (resultId) {
      case RESULT_ID_LINK_TEXT_NOT_DESCRIPTIVE:
        return String.format(
            locale,
            StringManager.getString(locale, "result_message_link_text_not_descriptive"),
            metadata.getString(KEY_LINK_TEXT));
      default:
        throw new IllegalStateException("Unsupported result id");
    }
  }

  @Override
  public String getShortMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    String generated = generateMessageForResultId(locale, resultId);
    if (generated != null) {
      return generated;
    }

    switch (resultId) {
      case RESULT_ID_LINK_TEXT_NOT_DESCRIPTIVE:
        return StringManager.getString(locale, "result_message_brief_link_text_not_descriptive");
      default:
        throw new IllegalStateException("Unsupported result id");
    }
  }

  @Override
  public String getTitleMessage(Locale locale) {
    return StringManager.getString(locale, "check_title_link_test");
  }

  @Nullable
  private static String generateMessageForResultId(Locale locale, int resultId) {
    switch (resultId) {
      case RESULT_ID_ENGLISH_LOCALE_ONLY:
        return StringManager.getString(locale, "result_message_english_locale_only");
      case RESULT_ID_NOT_TEXT_VIEW:
        return StringManager.getString(locale, "result_message_not_text_view");
      default:
        return null;
    }
  }
} public class RedundantDescriptionCheck extends AccessibilityHierarchyCheck {

  /** [Legacy] Result when the locale is not English. */
  public static final int RESULT_ID_ENGLISH_LOCALE_ONLY = 1;
  /** Result when the view is not {@code importantForAccessibility}. */
  public static final int RESULT_ID_NOT_IMPORTANT_FOR_ACCESSIBILITY = 2;
  /** Result when the view does not have a {@code contentDescription} */
  public static final int RESULT_ID_NO_CONTENT_DESC = 3;
  /** [Legacy] Result when the view's {@code contentDescription} ends with the view's type. */
  public static final int RESULT_ID_CONTENT_DESC_ENDS_WITH_VIEW_TYPE = 4;
  /** Result when the view's {@code contentDescription} contains the view's type. */
  public static final int RESULT_ID_CONTENT_DESC_CONTAINS_ITEM_TYPE = 5;
  /** Result when thew view is not visible. */
  public static final int RESULT_ID_NOT_VISIBLE = 6;
  /** Result when the view's {@code contentDescription} contains a term indicating state. */
  public static final int RESULT_ID_CONTENT_DESC_CONTAINS_STATE = 7;
  /** Result when the view's {@code contentDescription} contains the name of an action. */
  public static final int RESULT_ID_CONTENT_DESC_CONTAINS_ACTION = 8;

  /**
   * Result metadata key for a view's content description as a {@code String}. Populated in results
   * with {@link #RESULT_ID_CONTENT_DESC_CONTAINS_ITEM_TYPE}, {@link
   * #RESULT_ID_CONTENT_DESC_ENDS_WITH_VIEW_TYPE}, {@link #RESULT_ID_CONTENT_DESC_CONTAINS_STATE} or
   * {@link #RESULT_ID_CONTENT_DESC_CONTAINS_ACTION}.
   */
  public static final String KEY_CONTENT_DESCRIPTION = "KEY_CONTENT_DESCRIPTION";

  /**
   * Result metadata key for a possibly inappropriate word in the view's content description, as a
   * {@code String}. Populated in results with {@link #RESULT_ID_CONTENT_DESC_CONTAINS_ITEM_TYPE},
   * {@link #RESULT_ID_CONTENT_DESC_CONTAINS_STATE} or {@link
   * #RESULT_ID_CONTENT_DESC_CONTAINS_ACTION}.
   */
  public static final String KEY_REDUNDANT_WORD = "KEY_REDUNDANT_WORD";

  /** Keys of String resources that are item types. */
  private static final ImmutableList<String> ITEM_TYPE_WORD_KEYS =
      ImmutableList.of(
          "button_item_type", "checkbox_item_type", "checkbox_item_type_separate_words");

  /** Keys of String resources that are item states. */
  private static final ImmutableList<String> STATE_WORD_KEYS =

      ImmutableList.of("checked_state", "unchecked_state", "selected_state", "unselected_state");

  /** Keys of String resources that are names of actions. */
  private static final ImmutableList<String> ACTION_WORD_KEYS =
      ImmutableList.of("click_action", "swipe_action", "tap_action");

  @Override
  protected String getHelpTopic() {
    return "6378990"; // Items labeled with type or state
  }

  @Override
  public Category getCategory() {
    return Category.CONTENT_LABELING;
  }

  @Override
  public List<AccessibilityHierarchyCheckResult> runCheckOnHierarchy(
      AccessibilityHierarchy hierarchy,
      @Nullable ViewHierarchyElement fromRoot,
      @Nullable Parameters parameters) {
    List<AccessibilityHierarchyCheckResult> results = new ArrayList<>();
    Locale recordedLocale = getRecordedLocale(hierarchy);

    List<? extends ViewHierarchyElement> viewsToEval = getElementsToEvaluate(fromRoot, hierarchy);
    for (ViewHierarchyElement view : viewsToEval) {
      if (!Boolean.TRUE.equals(view.isVisibleToUser())) {
        results.add(
            new AccessibilityHierarchyCheckResult(
                this.getClass(),
                AccessibilityCheckResultType.NOT_RUN,
                view,
                RESULT_ID_NOT_VISIBLE,
                null));
        continue;
      }
      if (!view.isImportantForAccessibility()) {
        results.add(
            new AccessibilityHierarchyCheckResult(
                this.getClass(),
                AccessibilityCheckResultType.NOT_RUN,
                view,
                RESULT_ID_NOT_IMPORTANT_FOR_ACCESSIBILITY,
                null));
        continue;
      }

      CharSequence contentDescription = view.getContentDescription();
      if (TextUtils.isEmpty(contentDescription)) {
        results.add(
            new AccessibilityHierarchyCheckResult(
                this.getClass(),
                AccessibilityCheckResultType.NOT_RUN,
                view,
                RESULT_ID_NO_CONTENT_DESC,
                null));
        continue;
      }
      // This can potentially produce multiple results for one element.
      checkForWords(
          RESULT_ID_CONTENT_DESC_CONTAINS_ITEM_TYPE,
          ITEM_TYPE_WORD_KEYS,
          recordedLocale,
          view,
          results);
      checkForWords(
          RESULT_ID_CONTENT_DESC_CONTAINS_STATE, STATE_WORD_KEYS, recordedLocale, view, results);
      checkForWords(
          RESULT_ID_CONTENT_DESC_CONTAINS_ACTION, ACTION_WORD_KEYS, recordedLocale, view, results);
    }
    return results;
  }

  /**
   * Checks to see if the view's {@code contentDescription} contains any of the localized words
   * indicated by {@code wordKeys}. If found, a WARNING is added to {@code results} with the
   * specified result ID.
   */
  private void checkForWords(
      int resultId,
      ImmutableList<String> wordKeys,
      Locale locale,
      ViewHierarchyElement view,
      List<AccessibilityHierarchyCheckResult> results) {
    CharSequence contentDescription = checkNotNull(view.getContentDescription());
    for (String wordKey : wordKeys) {
      CharSequence word = StringManager.getString(locale, wordKey);
      if (containsWordIgnoreCase(contentDescription, word)) {
        ResultMetadata resultMetadata = new HashMapResultMetadata();
        resultMetadata.putString(KEY_CONTENT_DESCRIPTION, contentDescription.toString());
        resultMetadata.putString(KEY_REDUNDANT_WORD, word.toString());
        results.add(
            new AccessibilityHierarchyCheckResult(
                this.getClass(),
                AccessibilityCheckResultType.WARNING,
                view,
                resultId,
                resultMetadata));
      }
    }
  }

  @Override
  public String getMessageForResult(Locale locale, AccessibilityHierarchyCheckResult result) {
    if (result.getResultId() == RESULT_ID_CONTENT_DESC_ENDS_WITH_VIEW_TYPE) {
      ResultMetadata metadata = result.getMetadata();
      if (((metadata == null) || !metadata.containsKey(KEY_CONTENT_DESCRIPTION))
          && (result.getElement() != null)) {
        // For legacy results, remap hierarchy element content description to metadata
        ViewHierarchyElement culprit = result.getElement();
        ResultMetadata updatedMetadata =
            (metadata != null) ? metadata.clone() : new HashMapResultMetadata();
        updatedMetadata.putString(
            KEY_CONTENT_DESCRIPTION, checkNotNull(culprit.getContentDescription()).toString());
        AccessibilityHierarchyCheckResult updatedResult =
            new AccessibilityHierarchyCheckResult(
                this.getClass(), result.getType(), culprit, result.getResultId(), updatedMetadata);
        return super.getMessageForResult(locale, updatedResult);
      }
    }

    return super.getMessageForResult(locale, result);
  }

  @Override
  public String getMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    String generated = generateMessageForResultId(locale, resultId);
    if (generated != null) {
      return generated;
    }

    // Metadata will have been set for these result IDs.
    checkNotNull(metadata);
    switch (resultId) {
      case RESULT_ID_CONTENT_DESC_CONTAINS_ITEM_TYPE:
        return String.format(
            locale,
            StringManager.getString(locale, "result_message_content_desc_contains_redundant_word"),
            metadata.getString(KEY_CONTENT_DESCRIPTION),
            metadata.getString(KEY_REDUNDANT_WORD));
      case RESULT_ID_CONTENT_DESC_ENDS_WITH_VIEW_TYPE:
        return String.format(
            locale,
            StringManager.getString(locale, "result_message_content_desc_ends_with_view_type"),
            metadata.getString(KEY_CONTENT_DESCRIPTION));
      case RESULT_ID_CONTENT_DESC_CONTAINS_STATE:
        return String.format(
            locale,
            StringManager.getString(locale, "result_message_content_desc_contains_state"),
            metadata.getString(KEY_CONTENT_DESCRIPTION),
            metadata.getString(KEY_REDUNDANT_WORD));
      case RESULT_ID_CONTENT_DESC_CONTAINS_ACTION:
        return String.format(
            locale,
            StringManager.getString(locale, "result_message_content_desc_contains_action"),
            metadata.getString(KEY_CONTENT_DESCRIPTION),
            metadata.getString(KEY_REDUNDANT_WORD));
      default:
        throw new IllegalStateException("Unsupported result id");
    }
  }

  @Override
  public String getShortMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    String generated = generateMessageForResultId(locale, resultId);
    if (generated != null) {
      return generated;
    }

    switch (resultId) {
      case RESULT_ID_CONTENT_DESC_CONTAINS_ITEM_TYPE:
      case RESULT_ID_CONTENT_DESC_ENDS_WITH_VIEW_TYPE:
      case RESULT_ID_CONTENT_DESC_CONTAINS_STATE:
      case RESULT_ID_CONTENT_DESC_CONTAINS_ACTION:
        return StringManager.getString(
            locale, "result_message_brief_content_desc_contains_redundant_word");
      default:
        throw new IllegalStateException("Unsupported result id");
    }
  }

  @Override
  public String getTitleMessage(Locale locale) {
    return StringManager.getString(locale, "check_title_redundant_description");
  }

  private static boolean containsWordIgnoreCase(CharSequence sequence, CharSequence word) {
    // Dot . generally matches any character, but it excludes \n, \r, \u0085, \u2028, and \u2029.
    // (?s) at the beginning makes . matches any character without exception.
    // (?i) is used for ignore case match. \b matches word boundary.
    return Pattern.matches("(?s).*\\b(?i)" + word + "\\b.*", sequence.toString());
  }

  /** Indicates the locale recorded in the {@link DeviceState}. */
  private static Locale getRecordedLocale(AccessibilityHierarchy hierarchy) {
    return hierarchy.getDeviceState().getLocale();
  }

  private static @Nullable String generateMessageForResultId(Locale locale, int resultId) {
    switch (resultId) {
      case RESULT_ID_NOT_VISIBLE:
        return StringManager.getString(locale, "result_message_not_visible");
      case RESULT_ID_ENGLISH_LOCALE_ONLY:
        return StringManager.getString(locale, "result_message_english_locale_only");
      case RESULT_ID_NOT_IMPORTANT_FOR_ACCESSIBILITY:
        return StringManager.getString(locale, "result_message_not_important_for_accessibility");
      case RESULT_ID_NO_CONTENT_DESC:
        return StringManager.getString(locale, "result_message_no_content_desc");
      default:
        return null;
    }
  }
} public class SpeakableTextPresentCheck extends AccessibilityHierarchyCheck {

  /** Result when thew view is not visible. */
  public static final int RESULT_ID_NOT_VISIBLE = 1;
  /** Result when the view is not {@code importantForAccessibility}. */
  public static final int RESULT_ID_NOT_IMPORTANT_FOR_ACCESSIBILITY = 2;
  /** Result when the view should not be focused by a screen reader. */
  public static final int RESULT_ID_SHOULD_NOT_FOCUS = 3;
  /** Result when the view is missing speakable text. */
  public static final int RESULT_ID_MISSING_SPEAKABLE_TEXT = 4;
  /** Result when the view type is excluded. */
  public static final int RESULT_ID_WEB_CONTENT = 5;

  @Override
  protected String getHelpTopic() {
    return "7158690"; // Content labels
  }

  @Override
  public Category getCategory() {
    return Category.CONTENT_LABELING;
  }

  @Override
  public List<AccessibilityHierarchyCheckResult> runCheckOnHierarchy(
      AccessibilityHierarchy hierarchy,
      @Nullable ViewHierarchyElement fromRoot,
      @Nullable Parameters parameters) {
    List<AccessibilityHierarchyCheckResult> results = new ArrayList<>();
    List<? extends ViewHierarchyElement> viewsToEval = getElementsToEvaluate(fromRoot, hierarchy);
    for (ViewHierarchyElement element : viewsToEval) {
      if (!TRUE.equals(element.isVisibleToUser())) {
        results.add(new AccessibilityHierarchyCheckResult(
            this.getClass(),
            AccessibilityCheckResultType.NOT_RUN,
            element,
            RESULT_ID_NOT_VISIBLE,
            null));
        continue;
      }

      if (!element.isImportantForAccessibility()) {
        results.add(new AccessibilityHierarchyCheckResult(
            this.getClass(),
            AccessibilityCheckResultType.NOT_RUN,
            element,
            RESULT_ID_NOT_IMPORTANT_FOR_ACCESSIBILITY,
            null));
        continue;
      }

      if (element.checkInstanceOf(ViewHierarchyElementUtils.WEB_VIEW_CLASS_NAME)
          && element.getChildViewCount() == 0) {
        results.add(new AccessibilityHierarchyCheckResult(
            this.getClass(),
            AccessibilityCheckResultType.NOT_RUN,
            element,
            RESULT_ID_WEB_CONTENT,
            null));
        continue;
      }

      if (!ViewHierarchyElementUtils.shouldFocusView(element)) {
        results.add(new AccessibilityHierarchyCheckResult(
            this.getClass(),
            AccessibilityCheckResultType.NOT_RUN,
            element,
            RESULT_ID_SHOULD_NOT_FOCUS,
            null));
        continue;
      }

      if (TextUtils.isEmpty(ViewHierarchyElementUtils.getSpeakableTextForElement(element))) {
        results.add(new AccessibilityHierarchyCheckResult(
            this.getClass(),
            AccessibilityCheckResultType.ERROR,
            element,
            RESULT_ID_MISSING_SPEAKABLE_TEXT,
            null));
      }
    }
    return results;
  }

  @Override
  public String getMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    return generateMessageForResultId(locale, resultId);
  }

  @Override
  public String getShortMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    return generateMessageForResultId(locale, resultId);
  }

  @Override
  public String getTitleMessage(Locale locale) {
    return StringManager.getString(locale, "check_title_speakable_text_present");
  }

  private static String generateMessageForResultId(Locale locale, int resultId) {
    switch(resultId) {
      case RESULT_ID_NOT_VISIBLE:
        return StringManager.getString(locale, "result_message_not_visible");
      case RESULT_ID_NOT_IMPORTANT_FOR_ACCESSIBILITY:
        return StringManager.getString(locale, "result_message_not_important_for_accessibility");
      case RESULT_ID_SHOULD_NOT_FOCUS:
        return StringManager.getString(locale, "result_message_should_not_focus");
      case RESULT_ID_MISSING_SPEAKABLE_TEXT:
        return StringManager.getString(locale, "result_message_missing_speakable_text");
      case RESULT_ID_WEB_CONTENT:
        return StringManager.getString(locale, "result_message_web_content");
      default:
        throw new IllegalStateException("Unsupported result id");
    }
  }
} public class TextContrastCheck extends AccessibilityHierarchyCheck {

  /** Result when the view is not visible. */
  public static final int RESULT_ID_NOT_VISIBLE = 1;
  /** Result when the view is not a {@link TextView}. */
  public static final int RESULT_ID_NOT_TEXT_VIEW = 2;
  /** Result when the {@link TextView} is empty. */
  public static final int RESULT_ID_TEXTVIEW_EMPTY = 3;
  /** Result when the view's text color could not be obtained. */
  public static final int RESULT_ID_COULD_NOT_GET_TEXT_COLOR = 4;
  /** Result when the view's background color could not be obtained. */
  public static final int RESULT_ID_COULD_NOT_GET_BACKGROUND_COLOR = 5;
  /** Result when the view's text is not opaque. */
  public static final int RESULT_ID_TEXT_MUST_BE_OPAQUE = 6;
  /** Result when the view's backgorund is not opaque. */
  public static final int RESULT_ID_BACKGROUND_MUST_BE_OPAQUE = 7;
  /** Result when the view's contrast is insufficient based on opaque text/background. */
  public static final int RESULT_ID_TEXTVIEW_CONTRAST_NOT_SUFFICIENT = 8;
  /** Result when the heuristic evaluation could not obtain a screenshot. */
  public static final int RESULT_ID_HEURISTIC_COULD_NOT_GET_SCREENCAPTURE = 9;
  /** Result when the view is not completely within the screenshot. */
  public static final int RESULT_ID_VIEW_NOT_WITHIN_SCREENCAPTURE = 10;
  /** Result when the view's contrast is insufficient based on heuristic evaluation. */
  public static final int RESULT_ID_TEXTVIEW_HEURISTIC_CONTRAST_NOT_SUFFICIENT = 11;
  /** Result when the view's contrast is between the small text ratio and the large text ratio. */
  public static final int RESULT_ID_TEXTVIEW_HEURISTIC_CONTRAST_BORDERLINE = 12;
  /** Result when the evaluated element was determined to be not enabled */
  public static final int RESULT_ID_NOT_ENABLED = 13;
  /** Result when the evaluated element's screen capture data was determined to be hidden */
  public static final int RESULT_ID_SCREENCAPTURE_DATA_HIDDEN = 14;
  /**
   * Result when the view's contrast is insufficient based on heuristic evaluation which uses
   * user-defined heuristic contrast ratio.
   */
  public static final int RESULT_ID_CUSTOMIZED_TEXTVIEW_HEURISTIC_CONTRAST_NOT_SUFFICIENT = 15;
  /** Result when the evaluated element's screen capture has a uniform color */
  public static final int RESULT_ID_SCREENCAPTURE_UNIFORM_COLOR = 16;

  /** Result metadata key for the {@code int} color of the view's background. */
  public static final String KEY_BACKGROUND_COLOR = "KEY_BACKGROUND_COLOR";
  /** Result metadata key for the {@code int} (0-100)% opacity of the view's background. */
  public static final String KEY_BACKGROUND_OPACITY = "KEY_BACKGROUND_OPACITY";
  /** Result metadata key for the {@code double} computed contrast ratio of the view. */
  public static final String KEY_CONTRAST_RATIO = "KEY_CONTRAST_RATIO";
  /** Result metadata key for the {@code int} color of the view's foreground. */
  public static final String KEY_FOREGROUND_COLOR = "KEY_FOREGROUND_COLOR";
  /** Result metadata key for the {@code double} required contrast ratio for this view. */
  public static final String KEY_REQUIRED_CONTRAST_RATIO = "KEY_REQUIRED_CONTRAST_RATIO";
  /**
   * Result metadata key for the {@code double} user-defined heuristic contrast ratio for this view.
   */
  public static final String KEY_CUSTOMIZED_HEURISTIC_CONTRAST_RATIO =
      "KEY_CUSTOMIZED_HEURISTIC_CONTRAST_RATIO";
  /** Result metadata key for the {@code String} bounds of the screenshot. */
  public static final String KEY_SCREENSHOT_BOUNDS_STRING = "KEY_SCREENSHOT_BOUNDS_STRING";
  /** Result metadata key for the {@code int} color of the view's text. */
  public static final String KEY_TEXT_COLOR = "KEY_TEXT_COLOR";
  /** Result metadata key for the {@code int} (0-100)% opacity of the view's text. */
  public static final String KEY_TEXT_OPACITY = "KEY_TEXT_OPACITY";
  /** Result metadata key for {@code double} required contrast ratio for large text. */
  public static final String KEY_TOLERANT_CONTRAST_RATIO = "KEY_TOLERANT_CONTRAST_RATIO";
  /** Result metadata key for the {@code String} bounds of the view. */
  public static final String KEY_VIEW_BOUNDS_STRING = "KEY_VIEW_BOUNDS_STRING";
  /**
   * Result metadata key for a {@code boolean} which is {@code true} iff the view is determined to
   * be touching the scrollable edge of a scrollable container.
   */
  public static final String KEY_IS_AGAINST_SCROLLABLE_EDGE = "KEY_IS_AGAINST_SCROLLABLE_EDGE";

  /** Result metadata key for the {@code ArrayList<String>} foreground colors of the view. */
  public static final String KEY_ADDITIONAL_FOREGROUND_COLORS = "KEY_ADDITIONAL_FOREGROUND_COLORS";
  /** Result metadata key for the {@code ArrayList<String>} computed contrast ratio. */
  public static final String KEY_ADDITIONAL_CONTRAST_RATIOS = "KEY_ADDITIONAL_CONTRAST_RATIOS";
  /** Result metadata key for whether the view may be obscured by other on-screen content. */
  public static final String KEY_IS_POTENTIALLY_OBSCURED = "KEY_IS_POTENTIALLY_OBSCURED";

  /** The amount by which a view's computed contrast ratio may fall below defined thresholds */
  public static final double CONTRAST_TOLERANCE = 0.01;

  private static final Class<? extends AccessibilityHierarchyCheck> CHECK_CLASS =
      TextContrastCheck.class;

  /** The constants must match the values in {@link android.graphics.Typeface}. */
  private static final int TYPEFACE_NORMAL = 0;

  private static final int TYPEFACE_BOLD = 1;

  @Override
  protected String getHelpTopic() {
    return "7158390"; // Color contrast
  }

  @Override
  public Category getCategory() {
    return Category.LOW_CONTRAST;
  }

  @Override
  public List<AccessibilityHierarchyCheckResult> runCheckOnHierarchy(
      AccessibilityHierarchy hierarchy,
      @Nullable ViewHierarchyElement fromRoot,
      @Nullable Parameters parameters) {
    List<AccessibilityHierarchyCheckResult> results = new ArrayList<>();
    List<? extends ViewHierarchyElement> viewsToEval = getElementsToEvaluate(fromRoot, hierarchy);
    for (ViewHierarchyElement view : viewsToEval) {
      if (!Boolean.TRUE.equals(view.isVisibleToUser())) {
        results.add(
            new AccessibilityHierarchyCheckResult(
                CHECK_CLASS,
                AccessibilityCheckResultType.NOT_RUN,
                view,
                RESULT_ID_NOT_VISIBLE,
                null));
        continue;
      }

      if (!view.checkInstanceOf(ViewHierarchyElementUtils.TEXT_VIEW_CLASS_NAME)
          || (view.checkInstanceOf(ViewHierarchyElementUtils.SWITCH_CLASS_NAME)
              && view.getTextCharacterLocations().isEmpty())) {
        // Only evaluate contrast for instances of TextView, or instances of Switch when character
        // locations are known.
        results.add(
            new AccessibilityHierarchyCheckResult(
                CHECK_CLASS,
                AccessibilityCheckResultType.NOT_RUN,
                view,
                RESULT_ID_NOT_TEXT_VIEW,
                null));
        continue;
      }

      if (TextUtils.isEmpty(view.getText()) && TextUtils.isEmpty(view.getHintText())) {
        results.add(
            new AccessibilityHierarchyCheckResult(
                CHECK_CLASS,
                AccessibilityCheckResultType.NOT_RUN,
                view,
                RESULT_ID_TEXTVIEW_EMPTY,
                null));
        continue;
      }

      if (!view.isEnabled()) {
        results.add(
            new AccessibilityHierarchyCheckResult(
                CHECK_CLASS,
                AccessibilityCheckResultType.NOT_RUN,
                view,
                RESULT_ID_NOT_ENABLED,
                null));
        continue;
      }

      AccessibilityHierarchyCheckResult lightweightResult = attemptLightweightEval(view);
      if (lightweightResult != null) {
        results.add(lightweightResult);
        if (lightweightResult.getType() == AccessibilityCheckResultType.NOT_RUN) {
          // Lightweight evaluation didn't run successfully.
          AccessibilityHierarchyCheckResult heavyweightResult =
              attemptHeavyweightEval(view, parameters);
          if (heavyweightResult != null) {
            // Heavyweight evaluation found a noteworthy issue.
            results.add(heavyweightResult);
          }
        }
      }
    }

    return results;
  }

  @Override
  public String getMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    String generated = generateMessageForResultId(locale, resultId);
    if (generated != null) {
      return generated;
    }

    // For each of the following result IDs, metadata will have been set on the result.
    checkNotNull(metadata);
    StringBuilder builder;
    switch (resultId) {
      case RESULT_ID_TEXT_MUST_BE_OPAQUE:
        builder =
            new StringBuilder()
                .append(StringManager.getString(locale, "result_message_text_must_be_opaque"))
                .append(" ")
                .append(
                    String.format(
                        locale,
                        StringManager.getString(
                            locale, "result_message_addendum_opacity_description"),
                        metadata.getFloat(KEY_TEXT_OPACITY)));
        return builder.toString();
      case RESULT_ID_BACKGROUND_MUST_BE_OPAQUE:
        builder =
            new StringBuilder()
                .append(StringManager.getString(locale, "result_message_background_must_be_opaque"))
                .append(" ")
                .append(
                    String.format(
                        locale,
                        StringManager.getString(
                            locale, "result_message_addendum_opacity_description"),
                        metadata.getFloat(KEY_BACKGROUND_OPACITY)));
        return builder.toString();
      case RESULT_ID_TEXTVIEW_CONTRAST_NOT_SUFFICIENT:
        builder =
            new StringBuilder(
                String.format(
                    locale,
                    StringManager.getString(
                        locale, "result_message_textview_contrast_not_sufficient"),
                    metadata.getDouble(KEY_CONTRAST_RATIO),
                    metadata.getInt(KEY_TEXT_COLOR) & 0xFFFFFF,
                    metadata.getInt(KEY_BACKGROUND_COLOR) & 0xFFFFFF,
                    metadata.getDouble(KEY_REQUIRED_CONTRAST_RATIO)));
        appendMetadataStringsToMessageIfNeeded(locale, metadata, builder);
        return builder.toString();
      case RESULT_ID_VIEW_NOT_WITHIN_SCREENCAPTURE:
        return String.format(
            locale,
            StringManager.getString(locale, "result_message_view_not_within_screencapture"),
            metadata.getString(KEY_VIEW_BOUNDS_STRING),
            metadata.getString(KEY_SCREENSHOT_BOUNDS_STRING));
      case RESULT_ID_TEXTVIEW_HEURISTIC_CONTRAST_NOT_SUFFICIENT:
        builder =
            new StringBuilder(
                String.format(
                    locale,
                    StringManager.getString(
                        locale, "result_message_textview_heuristic_contrast_not_sufficient"),
                    metadata.getDouble(KEY_CONTRAST_RATIO),
                    metadata.getInt(KEY_FOREGROUND_COLOR) & 0xFFFFFF,
                    metadata.getInt(KEY_BACKGROUND_COLOR) & 0xFFFFFF,
                    ContrastUtils.CONTRAST_RATIO_WCAG_NORMAL_TEXT, /* Suggested for small text */
                    metadata.getDouble(
                        KEY_REQUIRED_CONTRAST_RATIO) /* Suggested for large text */));
        appendMetadataStringsToMessageIfNeeded(locale, metadata, builder);
        return builder.toString();
      case RESULT_ID_TEXTVIEW_HEURISTIC_CONTRAST_BORDERLINE:
        builder =
            new StringBuilder(
                String.format(
                    locale,
                    StringManager.getString(
                        locale, "result_message_textview_heuristic_contrast_not_sufficient"),
                    metadata.getDouble(KEY_CONTRAST_RATIO),
                    metadata.getInt(KEY_FOREGROUND_COLOR) & 0xFFFFFF,
                    metadata.getInt(KEY_BACKGROUND_COLOR) & 0xFFFFFF,
                    metadata.getDouble(KEY_REQUIRED_CONTRAST_RATIO), /* Suggested for small text */
                    metadata.getDouble(
                        KEY_TOLERANT_CONTRAST_RATIO) /* Suggested for large text */));
        appendMetadataStringsToMessageIfNeeded(locale, metadata, builder);
        return builder.toString();
      case RESULT_ID_CUSTOMIZED_TEXTVIEW_HEURISTIC_CONTRAST_NOT_SUFFICIENT:
        builder =
            new StringBuilder(
                String.format(
                    locale,
                    StringManager.getString(
                        locale,
                        "result_message_textview_heuristic_customized_contrast_not_sufficient"),
                    metadata.getDouble(KEY_CONTRAST_RATIO),
                    metadata.getInt(KEY_FOREGROUND_COLOR) & 0xFFFFFF,
                    metadata.getInt(KEY_BACKGROUND_COLOR) & 0xFFFFFF,
                    metadata.getDouble(KEY_CUSTOMIZED_HEURISTIC_CONTRAST_RATIO)));
        appendMetadataStringsToMessageIfNeeded(locale, metadata, builder);
        return builder.toString();
      default:
        throw new IllegalStateException("Unsupported result id");
    }
  }

  @Override
  public String getShortMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    String generated = generateMessageForResultId(locale, resultId);
    if (generated != null) {
      return generated;
    }

    switch (resultId) {
      case RESULT_ID_TEXT_MUST_BE_OPAQUE:
        return StringManager.getString(locale, "result_message_text_must_be_opaque");
      case RESULT_ID_BACKGROUND_MUST_BE_OPAQUE:
        return StringManager.getString(locale, "result_message_background_must_be_opaque");
      case RESULT_ID_VIEW_NOT_WITHIN_SCREENCAPTURE:
        return StringManager.getString(locale, "result_message_no_screencapture"); // Close enough
      case RESULT_ID_TEXTVIEW_CONTRAST_NOT_SUFFICIENT:
      case RESULT_ID_TEXTVIEW_HEURISTIC_CONTRAST_NOT_SUFFICIENT:
      case RESULT_ID_TEXTVIEW_HEURISTIC_CONTRAST_BORDERLINE:
      case RESULT_ID_CUSTOMIZED_TEXTVIEW_HEURISTIC_CONTRAST_NOT_SUFFICIENT:
        return StringManager.getString(locale, "result_message_brief_text_contrast_not_sufficient");
      default:
        throw new IllegalStateException("Unsupported result id");
    }
  }

  /**
   * Calculates a secondary priority for a text contrast result.
   *
   * <p>The result is the amount that the actual contrast ratio is below the required ratio. Thus,
   * for a given required contrast ratio, decreasing contrast gives a higher priority.
   */
  @Override
  public @Nullable Double getSecondaryPriority(AccessibilityHierarchyCheckResult result) {
    ResultMetadata metadata = result.getMetadata();
    switch (result.getResultId()) {
      case RESULT_ID_TEXTVIEW_CONTRAST_NOT_SUFFICIENT:
      case RESULT_ID_TEXTVIEW_HEURISTIC_CONTRAST_NOT_SUFFICIENT:
      case RESULT_ID_TEXTVIEW_HEURISTIC_CONTRAST_BORDERLINE:
        return checkNotNull(metadata).getDouble(KEY_REQUIRED_CONTRAST_RATIO, 0.0)
            - checkNotNull(metadata).getDouble(KEY_CONTRAST_RATIO, 0.0);
      case RESULT_ID_CUSTOMIZED_TEXTVIEW_HEURISTIC_CONTRAST_NOT_SUFFICIENT:
        return checkNotNull(metadata).getDouble(KEY_CUSTOMIZED_HEURISTIC_CONTRAST_RATIO, 0)
            - checkNotNull(metadata).getDouble(KEY_CONTRAST_RATIO, 0.0);
      default:
        return null;
    }
  }

  @Override
  public String getTitleMessage(Locale locale) {
    return StringManager.getString(locale, "check_title_text_contrast");
  }

  private static @Nullable String generateMessageForResultId(Locale locale, int resultId) {
    switch (resultId) {
      case RESULT_ID_NOT_VISIBLE:
        return StringManager.getString(locale, "result_message_not_visible");
      case RESULT_ID_NOT_TEXT_VIEW:
        return StringManager.getString(locale, "result_message_not_text_view");
      case RESULT_ID_NOT_ENABLED:
        return StringManager.getString(locale, "result_message_not_enabled");
      case RESULT_ID_TEXTVIEW_EMPTY:
        return StringManager.getString(locale, "result_message_textview_empty");
      case RESULT_ID_COULD_NOT_GET_TEXT_COLOR:
        return StringManager.getString(locale, "result_message_could_not_get_text_color");
      case RESULT_ID_COULD_NOT_GET_BACKGROUND_COLOR:
        return StringManager.getString(locale, "result_message_could_not_get_background_color");
      case RESULT_ID_HEURISTIC_COULD_NOT_GET_SCREENCAPTURE:
        return StringManager.getString(locale, "result_message_no_screencapture");
      case RESULT_ID_SCREENCAPTURE_DATA_HIDDEN:
        return StringManager.getString(locale, "result_message_screencapture_data_hidden");
      case RESULT_ID_SCREENCAPTURE_UNIFORM_COLOR:
        return StringManager.getString(locale, "result_message_screencapture_uniform_color");
      default:
        return null;
    }
  }

  /**
   * Performs lightweight contrast evaluation on the provided {@code view}. During lightweight
   * evaluation, we examine text color and background Drawables to extract properties about
   * component colors.
   *
   * @param view The {@link ViewHierarchyElement} to evaluate
   * @return an {@link AccessibilityHierarchyCheckResult} describing the result of the lightweight
   *     evaluation, or {@code null} if there is sufficient text contrast.
   */
  private @Nullable AccessibilityHierarchyCheckResult attemptLightweightEval(
      ViewHierarchyElement view) {
    Integer textColor = getForegroundColor(view);
    Integer backgroundDrawableColor = view.getBackgroundDrawableColor();
    if (textColor == null) {
      return new AccessibilityHierarchyCheckResult(
          CHECK_CLASS,
          AccessibilityCheckResultType.NOT_RUN,
          view,
          RESULT_ID_COULD_NOT_GET_TEXT_COLOR,
          null);
    }

    if (backgroundDrawableColor == null) {
      return new AccessibilityHierarchyCheckResult(
          CHECK_CLASS,
          AccessibilityCheckResultType.NOT_RUN,
          view,
          RESULT_ID_COULD_NOT_GET_BACKGROUND_COLOR,
          null);
    }

    int textAlpha = Color.alpha(textColor);
    if (textAlpha < 255) {
      ResultMetadata resultMetadata = new HashMapResultMetadata();
      resultMetadata.putFloat(KEY_TEXT_OPACITY, (textAlpha / 255f) * 100);

      return new AccessibilityHierarchyCheckResult(
          CHECK_CLASS,
          AccessibilityCheckResultType.NOT_RUN,
          view,
          RESULT_ID_TEXT_MUST_BE_OPAQUE,
          resultMetadata);
    }

    int backgroundAlpha = Color.alpha(backgroundDrawableColor);
    if (backgroundAlpha < 255) {
      ResultMetadata resultMetadata = new HashMapResultMetadata();
      resultMetadata.putFloat(KEY_BACKGROUND_OPACITY, (backgroundAlpha / 255f) * 100);

      return new AccessibilityHierarchyCheckResult(
          CHECK_CLASS,
          AccessibilityCheckResultType.NOT_RUN,
          view,
          RESULT_ID_BACKGROUND_MUST_BE_OPAQUE,
          resultMetadata);
    }

    double contrastRatio = ContrastUtils.calculateContrastRatio(textColor, backgroundDrawableColor);
    double requiredContrast =
        isLargeText(view)
            ? ContrastUtils.CONTRAST_RATIO_WCAG_LARGE_TEXT
            : ContrastUtils.CONTRAST_RATIO_WCAG_NORMAL_TEXT;
    if ((requiredContrast - contrastRatio) > CONTRAST_TOLERANCE) {
      ResultMetadata resultMetadata = new HashMapResultMetadata();
      resultMetadata.putDouble(KEY_REQUIRED_CONTRAST_RATIO, requiredContrast);
      resultMetadata.putDouble(KEY_CONTRAST_RATIO, contrastRatio);
      resultMetadata.putInt(KEY_TEXT_COLOR, textColor);
      resultMetadata.putInt(KEY_BACKGROUND_COLOR, backgroundDrawableColor);

      return new AccessibilityHierarchyCheckResult(
          CHECK_CLASS,
          AccessibilityCheckResultType.ERROR,
          view,
          RESULT_ID_TEXTVIEW_CONTRAST_NOT_SUFFICIENT,
          resultMetadata);
    }

    // Sufficient contrast
    return null;
  }

  /**
   * Performs heavyweight contrast evaluation on the provided {@code view}. During heavyweight
   * evaluation, we examine the contents of a screen capture to heuristically determine foreground
   * and background colors, and from there, compute a contrast ratio.
   *
   * @param view The {@link ViewHierarchyElement} to evaluate
   * @param parameters Optional check input parameters
   * @return an {@link AccessibilityHierarchyCheckResult} describing the results of the heavyweight
   *     evaluation, or {@code null} if there is sufficient text contrast.
   */
  private @Nullable AccessibilityHierarchyCheckResult attemptHeavyweightEval(
      ViewHierarchyElement view, @Nullable Parameters parameters) {
    Image screenCapture = (parameters == null) ? null : parameters.getScreenCapture();
    if (screenCapture == null) {
      return new AccessibilityHierarchyCheckResult(
          CHECK_CLASS,
          AccessibilityCheckResultType.NOT_RUN,
          view,
          RESULT_ID_HEURISTIC_COULD_NOT_GET_SCREENCAPTURE,
          null);
    }
    Rect screenCaptureBounds = new Rect(0, 0, screenCapture.getWidth(), screenCapture.getHeight());
    Rect viewBounds = view.getBoundsInScreen();
    Rect textCharacterBounds = getTextCharacterBounds(view);
    if (!textCharacterBounds.isEmpty() && screenCaptureBounds.contains(textCharacterBounds)) {
      // Extracts foreground/background colors from the region which contains the text characters.
      viewBounds = textCharacterBounds;
    }

    if (viewBounds.isEmpty() || !screenCaptureBounds.contains(viewBounds)) {
      // If an off-screen view reports itself as visible, we shouldn't evaluate it.
      ResultMetadata resultMetadata = new HashMapResultMetadata();
      resultMetadata.putString(KEY_VIEW_BOUNDS_STRING, viewBounds.toShortString());
      resultMetadata.putString(KEY_SCREENSHOT_BOUNDS_STRING, screenCaptureBounds.toShortString());

      return new AccessibilityHierarchyCheckResult(
          CHECK_CLASS,
          AccessibilityCheckResultType.NOT_RUN,
          view,
          RESULT_ID_VIEW_NOT_WITHIN_SCREENCAPTURE,
          resultMetadata);
    }

    // viewBounds cannot be out of bounds because the bounds were checked above.
    Image viewImage = crop(screenCapture, viewBounds);
    ContrastSwatch contrastSwatch =
        getContrastSwatch(
            viewImage,
            (parameters == null) ? null : parameters.getEnableEnhancedContrastEvaluation());
    ResultMetadata resultMetadata = new HashMapResultMetadata();
    if (view.isAgainstScrollableEdge()) {
      resultMetadata.putBoolean(KEY_IS_AGAINST_SCROLLABLE_EDGE, true);
    }
    int foreground = contrastSwatch.getForegroundColors().get(0);
    int background = contrastSwatch.getBackgroundColor();

    if (background == foreground) {
      // In the case the foreground and background colors from ContrastSwatch are the same, and
      // they match the secure window censor color, we can assume the system has intentionally
      // censored our screen capture data, and we should not evaluate this content.
      return new AccessibilityHierarchyCheckResult(
          CHECK_CLASS,
          AccessibilityCheckResultType.NOT_RUN,
          view,
          (foreground == ContrastUtils.COLOR_SECURE_WINDOW_CENSOR)
              ? RESULT_ID_SCREENCAPTURE_DATA_HIDDEN
              : RESULT_ID_SCREENCAPTURE_UNIFORM_COLOR,
          resultMetadata);
    }

    List<Integer> foregroundColors = contrastSwatch.getForegroundColors();
    List<Double> contrastRatios = contrastSwatch.getContrastRatios();
    ArrayList<Integer> lowForegroundColors = new ArrayList<>();
    ArrayList<Double> lowContrastRatios = new ArrayList<>();

    Double customizedHeuristicContrastRatio =
        (parameters == null) ? null : parameters.getCustomTextContrastRatio();
    if (customizedHeuristicContrastRatio != null) {
      for (int i = 0; i < contrastRatios.size(); i++) {
        if (customizedHeuristicContrastRatio - contrastRatios.get(i) > CONTRAST_TOLERANCE) {
          lowForegroundColors.add(foregroundColors.get(i));
          lowContrastRatios.add(contrastRatios.get(i));
        }
      }
      if (!lowContrastRatios.isEmpty()) {
        if (isPotentiallyObscured(view)) {
          resultMetadata.putBoolean(KEY_IS_POTENTIALLY_OBSCURED, true);
        }
        resultMetadata.putDouble(
            KEY_CUSTOMIZED_HEURISTIC_CONTRAST_RATIO, customizedHeuristicContrastRatio);
        storeColorsAndContrastRatios(
            resultMetadata, background, lowForegroundColors, lowContrastRatios);
        return resultPossiblyWithImage(
            AccessibilityCheckResultType.WARNING,
            view,
            RESULT_ID_CUSTOMIZED_TEXTVIEW_HEURISTIC_CONTRAST_NOT_SUFFICIENT,
            resultMetadata,
            parameters,
            viewImage);
      }
    } else {
      for (int i = 0; i < contrastRatios.size(); i++) {
        if ((ContrastUtils.CONTRAST_RATIO_WCAG_LARGE_TEXT - contrastRatios.get(i))
            > CONTRAST_TOLERANCE) {
          lowForegroundColors.add(foregroundColors.get(i));
          lowContrastRatios.add(contrastRatios.get(i));
        }
      }
      if (!lowContrastRatios.isEmpty()) {
        if (isPotentiallyObscured(view)) {
          resultMetadata.putBoolean(KEY_IS_POTENTIALLY_OBSCURED, true);
        }
        resultMetadata.putDouble(
            KEY_REQUIRED_CONTRAST_RATIO, ContrastUtils.CONTRAST_RATIO_WCAG_LARGE_TEXT);
        storeColorsAndContrastRatios(
            resultMetadata, background, lowForegroundColors, lowContrastRatios);
        return resultPossiblyWithImage(
            AccessibilityCheckResultType.WARNING,
            view,
            RESULT_ID_TEXTVIEW_HEURISTIC_CONTRAST_NOT_SUFFICIENT,
            resultMetadata,
            parameters,
            viewImage);
      } else {
        for (int i = 0; i < contrastRatios.size(); i++) {
          if ((ContrastUtils.CONTRAST_RATIO_WCAG_NORMAL_TEXT - contrastRatios.get(i))
              > CONTRAST_TOLERANCE) {
            lowForegroundColors.add(foregroundColors.get(i));
            lowContrastRatios.add(contrastRatios.get(i));
          }
        }
        if (!lowContrastRatios.isEmpty()) {
          if (isPotentiallyObscured(view)) {
            resultMetadata.putBoolean(KEY_IS_POTENTIALLY_OBSCURED, true);
          }
          resultMetadata.putDouble(
              KEY_REQUIRED_CONTRAST_RATIO, ContrastUtils.CONTRAST_RATIO_WCAG_NORMAL_TEXT);
          resultMetadata.putDouble(
              KEY_TOLERANT_CONTRAST_RATIO, ContrastUtils.CONTRAST_RATIO_WCAG_LARGE_TEXT);
          storeColorsAndContrastRatios(
              resultMetadata, background, lowForegroundColors, lowContrastRatios);
          return resultPossiblyWithImage(
              AccessibilityCheckResultType.WARNING,
              view,
              RESULT_ID_TEXTVIEW_HEURISTIC_CONTRAST_BORDERLINE,
              resultMetadata,
              parameters,
              viewImage);
        }
      }
    }

    // Sufficient contrast
    return null;
  }

  @VisibleForTesting
  ContrastSwatch getContrastSwatch(
      Image image, @Nullable Boolean enableEnhancedContrastEvaluation) {
    return new ContrastSwatch(image, Boolean.TRUE.equals(enableEnhancedContrastEvaluation));
  }

  private Rect getTextCharacterBounds(ViewHierarchyElement view) {
    List<Rect> characterLocations = view.getTextCharacterLocations();
    if (characterLocations.isEmpty()) {
      return Rect.EMPTY;
    }

    int minLeft = Integer.MAX_VALUE;
    int minTop = Integer.MAX_VALUE;
    int maxRight = Integer.MIN_VALUE;
    int maxBottom = Integer.MIN_VALUE;
    for (Rect rect : characterLocations) {
      minLeft = min(minLeft, rect.getLeft());
      minTop = min(minTop, rect.getTop());
      maxRight = max(maxRight, rect.getRight());
      maxBottom = max(maxBottom, rect.getBottom());
    }
    return new Rect(minLeft, minTop, maxRight, maxBottom);
  }

  private Image crop(Image screenCapture, Rect viewBounds) {
    return screenCapture.crop(
        viewBounds.getLeft(), viewBounds.getTop(), viewBounds.getWidth(), viewBounds.getHeight());
  }

  /**
   * Returns a new instance of AccessibilityHierarchyCheckResultWithImage if {@code viewImage} is
   * not {@code null} and {@code parameters} specifies that view images should be saved. Otherwise,
   * returns a new instance of AccessibilityHierarchyCheckResult.
   */
  private AccessibilityHierarchyCheckResult resultPossiblyWithImage(
      AccessibilityCheckResultType type,
      ViewHierarchyElement view,
      int resultId,
      ResultMetadata metadata,
      @Nullable Parameters parameters,
      @Nullable Image viewImage) {
    if ((viewImage != null)
        && (parameters != null)
        && Boolean.TRUE.equals(parameters.getSaveViewImages())) {
      return new AccessibilityHierarchyCheckResultWithImage(
          CHECK_CLASS, type, view, resultId, metadata, viewImage);
    } else {
      return new AccessibilityHierarchyCheckResult(CHECK_CLASS, type, view, resultId, metadata);
    }
  }

  /** Stores extracted or estimated colors and contrast ratios in metadata. */
  private void storeColorsAndContrastRatios(
      ResultMetadata resultMetadata,
      int background,
      List<Integer> foregroundColors,
      List<Double> contrastRatios) {
    resultMetadata.putInt(KEY_BACKGROUND_COLOR, background);
    storeForegroundColors(resultMetadata, foregroundColors);
    storeContrastRatios(resultMetadata, contrastRatios);
  }

  /**
   * Stores foreground colors in metadata. The first color is stored with the key {@link
   * #KEY_FOREGROUND_COLOR}. If {@code foregroundColors} has more than one value, all but the first
   * value will be stored with the key {@link #KEY_ADDITIONAL_FOREGROUND_COLORS}.
   */
  private void storeForegroundColors(
      ResultMetadata resultMetadata, List<Integer> foregroundColors) {
    resultMetadata.putInt(KEY_FOREGROUND_COLOR, foregroundColors.get(0));
    if (foregroundColors.size() > 1) {
      resultMetadata.putStringList(
          KEY_ADDITIONAL_FOREGROUND_COLORS,
          Lists.transform(
              foregroundColors.subList(1, foregroundColors.size()), integer -> integer.toString()));
    }
  }

  /**
   * Stores contrast ratios in metadata. The first ratio is stored with the key {@link
   * #KEY_CONTRAST_RATIO}. If {@code contrastRatios} has more than one value, all but the first
   * value will be stored with the key {@link #KEY_ADDITIONAL_CONTRAST_RATIOS}.
   */
  private void storeContrastRatios(ResultMetadata resultMetadata, List<Double> contrastRatios) {
    resultMetadata.putDouble(KEY_CONTRAST_RATIO, contrastRatios.get(0));
    if (contrastRatios.size() > 1) {
      resultMetadata.putStringList(
          KEY_ADDITIONAL_CONTRAST_RATIOS,
          Lists.transform(
              contrastRatios.subList(1, contrastRatios.size()), ratio -> ratio.toString()));
    }
  }

  /**
   * Appends messages for {@link #KEY_IS_POTENTIALLY_OBSCURED} and {@link
   * #KEY_IS_AGAINST_SCROLLABLE_EDGE} to the provided {@code builder} if the relevant keys are set
   * in the given {@code resultMetadata}.
   *
   * @param builder the {@link StringBuilder} to which result messages should be appended
   */
  @SuppressWarnings("unused") // {@code locale} and {@code builder} may not be used
  private static void appendMetadataStringsToMessageIfNeeded(
      Locale locale, ResultMetadata resultMetadata, StringBuilder builder) {
    if (resultMetadata.getBoolean(KEY_IS_POTENTIALLY_OBSCURED, false)) {
      builder
          .append(' ')
          .append(
              StringManager.getString(locale, "result_message_addendum_view_potentially_obscured"));
    }
    if (resultMetadata.getBoolean(KEY_IS_AGAINST_SCROLLABLE_EDGE, false)) {
      builder
          .append(' ')
          .append(
              StringManager.getString(locale, "result_message_addendum_against_scrollable_edge"));
    }
  }

  private static boolean isLargeText(ViewHierarchyElement view) {
    float density =
        view.getWindow()
            .getAccessibilityHierarchy()
            .getDeviceState()
            .getDefaultDisplayInfo()
            .getMetricsWithoutDecoration()
            .getScaledDensity();
    Float textSize = view.getTextSize();
    float dpSize = (textSize != null) ? textSize / density : 0;
    int style = (view.getTypefaceStyle() != null) ? view.getTypefaceStyle() : TYPEFACE_NORMAL;
    return (dpSize >= ContrastUtils.WCAG_LARGE_TEXT_MIN_SIZE)
        || ((dpSize >= ContrastUtils.WCAG_LARGE_BOLD_TEXT_MIN_SIZE)
            && ((style & TYPEFACE_BOLD) != 0));
  }

  /**
   * Returns the current color selected to paint the text or the hint of the given {@link
   * ViewHierarchyElement}.
   */
  private static @Nullable Integer getForegroundColor(ViewHierarchyElement view) {
    return TextUtils.isEmpty(view.getText()) ? view.getHintTextColor() : view.getTextColor();
  }
} public class TouchTargetSizeCheck extends AccessibilityHierarchyCheck {

  /** Result when the view is not clickable. */
  public static final int RESULT_ID_NOT_CLICKABLE = 1;
  /** Result when the view is not visible. */
  public static final int RESULT_ID_NOT_VISIBLE = 2;
  /** Result when the view's height and width are both too small. */
  public static final int RESULT_ID_SMALL_TOUCH_TARGET_WIDTH_AND_HEIGHT = 3;
  /** Result when the view's height is too small. */
  public static final int RESULT_ID_SMALL_TOUCH_TARGET_HEIGHT = 4;
  /** Result when the view's width is too small. */
  public static final int RESULT_ID_SMALL_TOUCH_TARGET_WIDTH = 5;
  /**
   * Result when the view's height and width are both smaller than the user-defined touch target
   * size.
   */
  public static final int RESULT_ID_CUSTOMIZED_SMALL_TOUCH_TARGET_WIDTH_AND_HEIGHT = 6;
  /** Result when the view's height is smaller than the user-defined touch target size. */
  public static final int RESULT_ID_CUSTOMIZED_SMALL_TOUCH_TARGET_HEIGHT = 7;
  /** Result when the view's width is smaller than the user-defined touch target size. */
  public static final int RESULT_ID_CUSTOMIZED_SMALL_TOUCH_TARGET_WIDTH = 8;

  /**
   * Result metadata key for a {@code boolean} which is {@code true} iff the view has a {@link
   * android.view.TouchDelegate} that may be handling touches on the view's behalf, but that
   * delegate's hit-Rect is not available.
   */
  public static final String KEY_HAS_TOUCH_DELEGATE = "KEY_HAS_TOUCH_DELEGATE";
  /**
   * Result metadata key for a {@code boolean} which is {@code true} iff the view has a {@link
   * android.view.TouchDelegate} with a hit-Rect available. When this key is set to {@code true},
   * {@link #KEY_HIT_RECT_WIDTH} and {@link #KEY_HIT_RECT_HEIGHT} are also provided within the
   * result metadata.
   */
  public static final String KEY_HAS_TOUCH_DELEGATE_WITH_HIT_RECT =
      "KEY_HAS_TOUCH_DELEGATE_WITH_HIT_RECT";
  /**
   * Result metadata key for a {@code boolean} which is {@code true} iff the view has an ancestor
   * (of a suitable size) which may be handling click actions on behalf of the view.
   */
  public static final String KEY_HAS_CLICKABLE_ANCESTOR = "KEY_HAS_CLICKABLE_ANCESTOR";
  /**
   * Result metadata key for a {@code boolean} which is {@code true} iff the view is determined to
   * be touching the scrollable edge of a scrollable container.
   */
  public static final String KEY_IS_AGAINST_SCROLLABLE_EDGE = "KEY_IS_AGAINST_SCROLLABLE_EDGE";
  /**
   * Result metadata key for a {@code boolean} which is {@code true} iff the view has a reduced
   * visible size because it is clipped by a parent view.  When this key is set to {@code true},
   * {@link #KEY_NONCLIPPED_HEIGHT} and {@link #KEY_NONCLIPPED_WIDTH} are also provided within the
   * result metadata.
   */
  public static final String KEY_IS_CLIPPED_BY_ANCESTOR = "KEY_IS_CLIPPED_BY_ANCESTOR";
  /** Result metadata key for the {@code int} height of the view. */
  public static final String KEY_HEIGHT = "KEY_HEIGHT";
  /** Result metadata key for the {@code int} width of the view. */
  public static final String KEY_WIDTH = "KEY_WIDTH";
  /**
   * Result metadata key for the {@code int} height of the view not considering clipping effects
   * applied by parent views. This value is populated only when {@link #KEY_IS_CLIPPED_BY_ANCESTOR}
   * is set to {@code true}.
   */
  public static final String KEY_NONCLIPPED_HEIGHT = "KEY_NONCLIPPED_HEIGHT";
  /**
   * Result metadata key for the {@code int} width of the view not considering clipping effects
   * applied by parent views. This value is populated only when {@link #KEY_IS_CLIPPED_BY_ANCESTOR}
   * is set to {@code true}.
   */
  public static final String KEY_NONCLIPPED_WIDTH = "KEY_NONCLIPPED_WIDTH";
  /** Result metadata key for the {@code int} required height of the view */
  public static final String KEY_REQUIRED_HEIGHT = "KEY_REQUIRED_HEIGHT";
  /** Result metadata key for the {@code int} required width of the view */
  public static final String KEY_REQUIRED_WIDTH = "KEY_REQUIRED_WIDTH";
  /** Result metadata key for the {@code int} user-defined minimum width of the view */
  public static final String KEY_CUSTOMIZED_REQUIRED_WIDTH = "KEY_CUSTOMIZED_REQUIRED_WIDTH";
  /** Result metadata key for the {@code int} user-defined minimum height of the view */
  public static final String KEY_CUSTOMIZED_REQUIRED_HEIGHT = "KEY_CUSTOMIZED_REQUIRED_HEIGHT";
  /**
   * Result metadata key for the {@code int} conveying the width of the largest {@link
   * android.view.TouchDelegate} hit-Rect of the view
   */
  public static final String KEY_HIT_RECT_WIDTH = "KEY_HIT_RECT_WIDTH";
  /**
   * Result metadata key for the {@code int} conveying the height of the largest {@link
   * android.view.TouchDelegate} hit-Rect of the view
   */
  public static final String KEY_HIT_RECT_HEIGHT = "KEY_HIT_RECT_HEIGHT";

  /**
   * Value of android.view.accessibility.AccessibilityWindowInfo.TYPE_INPUT_METHOD. This avoids a
   * dependency upon Android libraries.
   */
  @VisibleForTesting static final int TYPE_INPUT_METHOD = 2;

  /**
   * Minimum height and width are set according to
   * <a href="http://developer.android.com/design/patterns/accessibility.html"></a>
   *
   * With the modification that targets against the edge of the screen may be narrower.
   */
  private static final int TOUCH_TARGET_MIN_HEIGHT = 48;
  private static final int TOUCH_TARGET_MIN_WIDTH = 48;
  private static final int TOUCH_TARGET_MIN_HEIGHT_ON_EDGE = 32;
  private static final int TOUCH_TARGET_MIN_WIDTH_ON_EDGE = 32;
  private static final int TOUCH_TARGET_MIN_HEIGHT_IME_CONTAINER = 32;
  private static final int TOUCH_TARGET_MIN_WIDTH_IME_CONTAINER = 32;

  @Override
  protected String getHelpTopic() {
    return "7101858"; // Touch target size
  }

  @Override
  public Category getCategory() {
    return Category.TOUCH_TARGET_SIZE;
  }

  @Override
  public List<AccessibilityHierarchyCheckResult> runCheckOnHierarchy(
      AccessibilityHierarchy hierarchy,
      @Nullable ViewHierarchyElement fromRoot,
      @Nullable Parameters parameters) {
    List<AccessibilityHierarchyCheckResult> results = new ArrayList<>();

    DisplayInfo defaultDisplay = hierarchy.getDeviceState().getDefaultDisplayInfo();
    DisplayInfo.Metrics metricsWithoutDecorations = defaultDisplay.getMetricsWithoutDecoration();
    List<? extends ViewHierarchyElement> viewsToEval = getElementsToEvaluate(fromRoot, hierarchy);
    for (ViewHierarchyElement view : viewsToEval) {
      if (!(TRUE.equals(view.isClickable())
          || TRUE.equals(view.isLongClickable()))) {
        results.add(new AccessibilityHierarchyCheckResult(
            this.getClass(),
            AccessibilityCheckResultType.NOT_RUN,
            view,
            RESULT_ID_NOT_CLICKABLE,
            null));
        continue;
      }

      if (!TRUE.equals(view.isVisibleToUser())) {
        results.add(new AccessibilityHierarchyCheckResult(
            this.getClass(),
            AccessibilityCheckResultType.NOT_RUN,
            view,
            RESULT_ID_NOT_VISIBLE,
            null));
        continue;
      }

      Rect bounds = view.getBoundsInScreen();
      Point requiredSize = getMinimumAllowableSizeForView(view, parameters);
      float density = metricsWithoutDecorations.getDensity();
      int actualHeight = Math.round(bounds.getHeight() / density);
      int actualWidth = Math.round(bounds.getWidth() / density);

      if (!meetsRequiredSize(bounds, requiredSize, density)) {
        // Before we know a view fails this check, we must check if another View may be handling
        // touches on its behalf. One mechanism for this is a TouchDelegate.
        boolean hasDelegate = false;
        Rect largestDelegateHitRect = null;
        // There are two approaches to detecting such a delegate.  One (on Android Q+) allows us
        // access to the hit-Rect.  Since this is the most precise signal, we try to use this first.
        if (hasTouchDelegateWithHitRects(view)) {
          hasDelegate = true;
          if (hasTouchDelegateOfRequiredSize(view, requiredSize, density)) {
            // Emit no result if a delegate's hit-Rect is above the required size
            continue;
          }
          // If no associated hit-Rect is of the required size, reference the largest one for
          // inclusion in the result message.
          largestDelegateHitRect = getLargestTouchDelegateHitRect(view);
        } else {
          // Without hit-Rects, another approach is to check (View) ancestors for the presence of
          // any TouchDelegate, which indicates that the element may have its hit-Rect adjusted,
          // but does not tell us what its size is.
          hasDelegate = hasAncestorWithTouchDelegate(view);
        }
        // Another approach is to have the parent handle touches for smaller child views, such as a
        // android.widget.Switch, which retains its clickable state for a "handle drag" effect. In
        // these cases, the parent must perform the same action as the child, which is beyond the
        // scope of this test.  We append this important exception message to the result by setting
        // KEY_HAS_CLICKABLE_ANCESTOR within the result metadata.
        boolean hasClickableAncestor = hasQualifyingClickableAncestor(view, parameters);
        // When evaluating a View-based hierarchy, we can check if the visible size of the view is
        // less than the drawing (nonclipped) size, which indicates an ancestor may scroll,
        // expand/collapse, or otherwise constrain the size of the clickable item.
        boolean isClippedByAncestor = hasQualifyingClippingAncestor(view, requiredSize, density);

        // In each of these cases, with the exception of when we have precise hit-Rect coordinates,
        // we cannot determine how exactly click actions are being handled by the underlying
        // application, so to avoid false positives, we will demote ERROR to WARNING.
        AccessibilityCheckResultType resultType =
            ((hasDelegate && (largestDelegateHitRect == null))
                    || hasClickableAncestor
                    || isClippedByAncestor)
                ? AccessibilityCheckResultType.WARNING
                : AccessibilityCheckResultType.ERROR;

        // We must also detect the case where an item is indicated as a small target because it
        // appears along the scrollable edge of a scrolling container.  In this case, we cannot
        // determine the native nonclipped bounds of the view, so we demote to NOT_RUN.
        boolean isAtScrollableEdge = view.isAgainstScrollableEdge();
        resultType = isAtScrollableEdge ? AccessibilityCheckResultType.NOT_RUN : resultType;

        ResultMetadata resultMetadata = new HashMapResultMetadata();
        resultMetadata.putInt(KEY_HEIGHT, actualHeight);
        resultMetadata.putInt(KEY_WIDTH, actualWidth);
        if (hasDelegate) {
          if (largestDelegateHitRect != null) {
            resultMetadata.putBoolean(KEY_HAS_TOUCH_DELEGATE_WITH_HIT_RECT, true);
            resultMetadata.putInt(
                KEY_HIT_RECT_WIDTH, Math.round(largestDelegateHitRect.getWidth() / density));
            resultMetadata.putInt(
                KEY_HIT_RECT_HEIGHT, Math.round(largestDelegateHitRect.getHeight() / density));
          } else {
            resultMetadata.putBoolean(KEY_HAS_TOUCH_DELEGATE, true);
          }
        }
        if (hasClickableAncestor) {
          resultMetadata.putBoolean(KEY_HAS_CLICKABLE_ANCESTOR, true);
        }
        if (isAtScrollableEdge) {
          resultMetadata.putBoolean(KEY_IS_AGAINST_SCROLLABLE_EDGE, true);
        }
        if (isClippedByAncestor) {
          // If the view is clipped by an ancestor, add the nonclipped dimensions to metadata.
          // The non-clipped height and width cannot be null if isClippedByAncestor is true.
          resultMetadata.putBoolean(KEY_IS_CLIPPED_BY_ANCESTOR, true);
          resultMetadata.putInt(KEY_NONCLIPPED_HEIGHT, checkNotNull(view.getNonclippedHeight()));
          resultMetadata.putInt(KEY_NONCLIPPED_WIDTH, checkNotNull(view.getNonclippedWidth()));
        }

        Integer customizedTouchTargetSize =
            (parameters == null) ? null : parameters.getCustomTouchTargetSize();
        if (customizedTouchTargetSize != null) {
          resultMetadata.putInt(KEY_CUSTOMIZED_REQUIRED_WIDTH, requiredSize.getX());
          resultMetadata.putInt(KEY_CUSTOMIZED_REQUIRED_HEIGHT, requiredSize.getY());
        } else {
          resultMetadata.putInt(KEY_REQUIRED_HEIGHT, requiredSize.getY());
          resultMetadata.putInt(KEY_REQUIRED_WIDTH, requiredSize.getX());
        }

        if ((actualHeight < requiredSize.getY()) && (actualWidth < requiredSize.getX())) {
          // Neither wide enough nor tall enough
          results.add(
              new AccessibilityHierarchyCheckResult(
                  this.getClass(),
                  resultType,
                  view,
                  (customizedTouchTargetSize == null)
                      ? RESULT_ID_SMALL_TOUCH_TARGET_WIDTH_AND_HEIGHT
                      : RESULT_ID_CUSTOMIZED_SMALL_TOUCH_TARGET_WIDTH_AND_HEIGHT,
                  resultMetadata));
        } else if (actualHeight < requiredSize.getY()) {
          // Not tall enough
          results.add(
              new AccessibilityHierarchyCheckResult(
                  this.getClass(),
                  resultType,
                  view,
                  (customizedTouchTargetSize == null)
                      ? RESULT_ID_SMALL_TOUCH_TARGET_HEIGHT
                      : RESULT_ID_CUSTOMIZED_SMALL_TOUCH_TARGET_HEIGHT,
                  resultMetadata));
        } else {
          // Not wide enough
          results.add(
              new AccessibilityHierarchyCheckResult(
                  this.getClass(),
                  resultType,
                  view,
                  (customizedTouchTargetSize == null)
                      ? RESULT_ID_SMALL_TOUCH_TARGET_WIDTH
                      : RESULT_ID_CUSTOMIZED_SMALL_TOUCH_TARGET_WIDTH,
                  resultMetadata));
        }
      }
    }
    return results;
  }

  @Override
  public String getMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    String generated = generateMessageForResultId(locale, resultId);
    if (generated != null) {
      return generated;
    }

    // For each of the following result IDs, metadata will have been set on the result.
    checkNotNull(metadata);
    StringBuilder builder = new StringBuilder();
    int requiredHeight = metadata.getInt(KEY_REQUIRED_HEIGHT, TOUCH_TARGET_MIN_HEIGHT);
    int requiredWidth = metadata.getInt(KEY_REQUIRED_WIDTH, TOUCH_TARGET_MIN_WIDTH);
    switch (resultId) {
      case RESULT_ID_SMALL_TOUCH_TARGET_WIDTH_AND_HEIGHT:
        builder.append(String.format(locale,
            StringManager.getString(locale, "result_message_small_touch_target_width_and_height"),
            metadata.getInt(KEY_WIDTH), metadata.getInt(KEY_HEIGHT), requiredWidth,
            requiredHeight));
        appendMetadataStringsToMessageIfNeeded(locale, metadata, builder);
        return builder.toString();
      case RESULT_ID_SMALL_TOUCH_TARGET_HEIGHT:
        builder.append(String.format(locale,
            StringManager.getString(locale, "result_message_small_touch_target_height"),
            metadata.getInt(KEY_HEIGHT), requiredHeight));
        appendMetadataStringsToMessageIfNeeded(locale, metadata, builder);
        return builder.toString();
      case RESULT_ID_SMALL_TOUCH_TARGET_WIDTH:
        builder.append(String.format(locale,
            StringManager.getString(locale, "result_message_small_touch_target_width"),
            metadata.getInt(KEY_WIDTH), requiredWidth));
        appendMetadataStringsToMessageIfNeeded(locale, metadata, builder);
        return builder.toString();
      case RESULT_ID_CUSTOMIZED_SMALL_TOUCH_TARGET_WIDTH_AND_HEIGHT:
        builder.append(
            String.format(
                locale,
                StringManager.getString(
                    locale, "result_message_customized_small_touch_target_width_and_height"),
                metadata.getInt(KEY_WIDTH),
                metadata.getInt(KEY_HEIGHT),
                metadata.getInt(KEY_CUSTOMIZED_REQUIRED_WIDTH),
                metadata.getInt(KEY_CUSTOMIZED_REQUIRED_HEIGHT)));
        appendMetadataStringsToMessageIfNeeded(locale, metadata, builder);
        return builder.toString();
      case RESULT_ID_CUSTOMIZED_SMALL_TOUCH_TARGET_HEIGHT:
        builder.append(
            String.format(
                locale,
                StringManager.getString(
                    locale, "result_message_customized_small_touch_target_height"),
                metadata.getInt(KEY_HEIGHT),
                metadata.getInt(KEY_CUSTOMIZED_REQUIRED_HEIGHT)));
        appendMetadataStringsToMessageIfNeeded(locale, metadata, builder);
        return builder.toString();
      case RESULT_ID_CUSTOMIZED_SMALL_TOUCH_TARGET_WIDTH:
        builder.append(
            String.format(
                locale,
                StringManager.getString(
                    locale, "result_message_customized_small_touch_target_width"),
                metadata.getInt(KEY_WIDTH),
                metadata.getInt(KEY_CUSTOMIZED_REQUIRED_WIDTH)));
        appendMetadataStringsToMessageIfNeeded(locale, metadata, builder);
        return builder.toString();
      default:
        throw new IllegalStateException("Unsupported result id");
    }
  }

  @Override
  public String getShortMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    String generated = generateMessageForResultId(locale, resultId);
    if (generated != null) {
      return generated;
    }

    switch (resultId) {
      case RESULT_ID_SMALL_TOUCH_TARGET_WIDTH_AND_HEIGHT:
      case RESULT_ID_SMALL_TOUCH_TARGET_HEIGHT:
      case RESULT_ID_SMALL_TOUCH_TARGET_WIDTH:
      case RESULT_ID_CUSTOMIZED_SMALL_TOUCH_TARGET_WIDTH_AND_HEIGHT:
      case RESULT_ID_CUSTOMIZED_SMALL_TOUCH_TARGET_WIDTH:
      case RESULT_ID_CUSTOMIZED_SMALL_TOUCH_TARGET_HEIGHT:
        return StringManager.getString(locale, "result_message_brief_small_touch_target");
      default:
        throw new IllegalStateException("Unsupported result id");
    }
  }

  /**
   * Calculates a secondary priority for a touch target result.
   *
   * <p>The primary influence on this priority is the minimum touch target dimension in the result.
   * For example, any result that has a minimum dimension of 2dp (ex. 2dp x 5dp, 45dp x 2dp, or just
   * 2dp wide) should have a greater priority than any result that has a minimum dimension of 3dp
   * (ex. 3dp x 3dp, 36dp x 3dp, or just 3dp high).
   *
   * <p>The secondary influence on this priority is the maximum touch target dimension in the
   * result. If a result only has one dimension, the other is regarded as infinite. For example,
   * among results with a 3dp minimum threshold, 3dp x 3dp would have the highest priority, 3dp x
   * 5dp (or 5dp x 3dp) would be lower, and just 3dp wide (or just 3dp high) would have the lowest
   * priority.
   */

  @Override
  public @Nullable Double getSecondaryPriority(AccessibilityHierarchyCheckResult result) {
    ResultMetadata meta = result.getMetadata();
    if (meta == null) {
      return null;
    }

    int width = meta.getInt(KEY_WIDTH, Integer.MAX_VALUE);
    int height = meta.getInt(KEY_HEIGHT, Integer.MAX_VALUE);
    double primary = Math.min(width, height);
    if (primary == Integer.MAX_VALUE) {
      return null; // Neither width nor height is present.
    }
    // The divisor of 30 delays the exponential expression from reaching its max value.
    double secondary = 1.0 / Math.exp(Math.max(width, height) / 30.0d);
    return -(primary - secondary);
  }

  @Override
  public String getTitleMessage(Locale locale) {
    return StringManager.getString(locale, "check_title_touch_target_size");
  }

  private static @Nullable String generateMessageForResultId(Locale locale, int resultId) {
    switch (resultId) {
      case RESULT_ID_NOT_CLICKABLE:
        return StringManager.getString(locale, "result_message_not_clickable");
      case RESULT_ID_NOT_VISIBLE:
        return StringManager.getString(locale, "result_message_not_visible");
      default:
        return null;
    }
  }

  /**
   * Derives the minimum allowable size for the given {@code view} in dp
   *
   * @param view the {@link ViewHierarchyElement} to evaluate
   * @param parameters Optional check input parameters
   * @return a {@link Point} representing the minimum allowable size for {@code view} in dp units
   */
  private static Point getMinimumAllowableSizeForView(
      ViewHierarchyElement view, @Nullable Parameters parameters) {
    Rect bounds = view.getBoundsInScreen();
    Metrics realMetrics = view.getWindow().getAccessibilityHierarchy().getDeviceState()
        .getDefaultDisplayInfo().getRealMetrics();

    final int touchTargetMinWidth;
    final int touchTargetMinHeight;
    final int touchTargetMinWidthImeContainer;
    final int touchTargetMinHeightImeContainer;
    final int touchTargetMinWidthOnEdge;
    final int touchTargetMinHeightOnEdge;
    Integer customizedTargetSize =
        (parameters == null) ? null : parameters.getCustomTouchTargetSize();
    if (customizedTargetSize != null) {
      float targetSize = (float) customizedTargetSize;
      touchTargetMinWidth = customizedTargetSize;
      touchTargetMinHeight = customizedTargetSize;
      touchTargetMinHeightImeContainer =
          Math.round(TOUCH_TARGET_MIN_HEIGHT_IME_CONTAINER * targetSize / TOUCH_TARGET_MIN_HEIGHT);
      touchTargetMinWidthImeContainer =
          Math.round(TOUCH_TARGET_MIN_WIDTH_IME_CONTAINER * targetSize / TOUCH_TARGET_MIN_WIDTH);
      touchTargetMinHeightOnEdge =
          Math.round(TOUCH_TARGET_MIN_HEIGHT_ON_EDGE * targetSize / TOUCH_TARGET_MIN_HEIGHT);
      touchTargetMinWidthOnEdge =
          Math.round(TOUCH_TARGET_MIN_WIDTH_ON_EDGE * targetSize / TOUCH_TARGET_MIN_WIDTH);
    } else {
      touchTargetMinWidth = TOUCH_TARGET_MIN_WIDTH;
      touchTargetMinHeight = TOUCH_TARGET_MIN_HEIGHT;
      touchTargetMinHeightImeContainer = TOUCH_TARGET_MIN_HEIGHT_IME_CONTAINER;
      touchTargetMinWidthImeContainer = TOUCH_TARGET_MIN_WIDTH_IME_CONTAINER;
      touchTargetMinHeightOnEdge = TOUCH_TARGET_MIN_HEIGHT_ON_EDGE;
      touchTargetMinWidthOnEdge = TOUCH_TARGET_MIN_WIDTH_ON_EDGE;
    }

    final int requiredWidth;
    final int requiredHeight;
    Integer windowType = view.getWindow().getType();
    if ((windowType != null) && (windowType == TYPE_INPUT_METHOD)) {
      // Contents of input method windows may be smaller
      requiredWidth = touchTargetMinWidthImeContainer;
      requiredHeight = touchTargetMinHeightImeContainer;
    } else if (realMetrics != null) { // JB MR1 and above
      // Views against the edge of the screen may be smaller in the neighboring dimension
      boolean viewAgainstSide =
          (bounds.getLeft() == 0) || (bounds.getRight() == realMetrics.getWidthPixels());
      boolean viewAgainstTopOrBottom =
          (bounds.getTop() == 0) || (bounds.getBottom() == realMetrics.getHeightPixels());

      requiredWidth = viewAgainstSide ? touchTargetMinWidthOnEdge : touchTargetMinWidth;
      requiredHeight = viewAgainstTopOrBottom ? touchTargetMinHeightOnEdge : touchTargetMinHeight;
    } else {
      // Before JB MR1, we can't get the real size of the screen and thus can't be sure that a
      // view is against an edge. In that case, we only enforce that the view is above the most
      // lenient threshold.
      requiredWidth = Math.min(touchTargetMinWidthOnEdge, touchTargetMinWidth);
      requiredHeight = Math.min(touchTargetMinHeightOnEdge, touchTargetMinHeight);
    }

    return new Point(requiredWidth, requiredHeight);
  }

  /**
   * Determines if {@code boundingRectInPx} is at least as large in both dimensions as the size
   * denoted by {@code requiredSizeInDp}. Handles conversion between px and dp based on {@code
   * density}, rounding the result of such conversion.
   */
  private static boolean meetsRequiredSize(
      Rect boundingRectInPx, Point requiredSizeInDp, float density) {
    return (Math.round(boundingRectInPx.getWidth() / density) >= requiredSizeInDp.getX())
        && (Math.round(boundingRectInPx.getHeight() / density) >= requiredSizeInDp.getY());
  }

  /**
   * Returns {@code true} if {@code view} has a {@link android.view.TouchDelegate} with hit-Rects of
   * a known size, {@code false} otherwise
   */
  private static boolean hasTouchDelegateWithHitRects(ViewHierarchyElement view) {
    return !view.getTouchDelegateBounds().isEmpty();
  }

  /**
   * Determines if any of the {@link android.view.TouchDelegate} hit-Rects delegated to {@code view}
   * meet the required size represented by {@code requiredSizeInDp}
   */
  private static boolean hasTouchDelegateOfRequiredSize(
      ViewHierarchyElement view, Point requiredSizeInDp, float density) {
    for (Rect hitRect : view.getTouchDelegateBounds()) {
      if (meetsRequiredSize(hitRect, requiredSizeInDp, density)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Returns the largest hit-Rect (by area) in screen coordinates (px units) associated with {@code
   * view}, or {@code null} if no hit-Rects are used
   */
  @Nullable
  private static Rect getLargestTouchDelegateHitRect(ViewHierarchyElement view) {
    int largestArea = -1;
    Rect largestHitRect = null;
    for (Rect hitRect : view.getTouchDelegateBounds()) {
      int area = hitRect.getWidth() * hitRect.getHeight();
      if (area > largestArea) {
        largestArea = area;
        largestHitRect = hitRect;
      }
    }
    return largestHitRect;
  }

  /**
   * Determines if any view in the hierarchy above the provided {@code view} has a {@link
   * android.view.TouchDelegate} set.
   *
   * @param view the {@link ViewHierarchyElement} to evaluate
   * @return {@code true} if an ancestor has a {@link android.view.TouchDelegate} set, {@code false}
   * if no delegate is set or if this could not be determined.
   */
  private static boolean hasAncestorWithTouchDelegate(ViewHierarchyElement view) {
    for (ViewHierarchyElement evalView = view.getParentView(); evalView != null;
        evalView = evalView.getParentView()) {
      if (TRUE.equals(evalView.hasTouchDelegate())) {
        return true;
      }
    }
    return false;
  }

  /**
   * Determines if any view in the hierarchy above the provided {@code view} matches {@code view}'s
   * clickability and meets its minimum allowable size.
   *
   * @param view the {@link ViewHierarchyElement} to evaluate
   * @param parameters Optional check input parameters
   * @return {@code true} if any view in {@code view}'s ancestry that is clickable and/or
   *     long-clickable and meets its minimum allowable size.
   */
  private static boolean hasQualifyingClickableAncestor(
      ViewHierarchyElement view, @Nullable Parameters parameters) {
    boolean isTargetClickable = TRUE.equals(view.isClickable());
    boolean isTargetLongClickable = TRUE.equals(view.isLongClickable());
    ViewHierarchyElement evalView = view.getParentView();

    while (evalView != null) {
      if ((TRUE.equals(evalView.isClickable()) && isTargetClickable)
          || (TRUE.equals(evalView.isLongClickable()) && isTargetLongClickable)) {
        Point requiredSize = getMinimumAllowableSizeForView(evalView, parameters);
        Rect bounds = evalView.getBoundsInScreen();
        if (!evalView.checkInstanceOf(ViewHierarchyElementUtils.ABS_LIST_VIEW_CLASS_NAME)
            && (bounds.getHeight() >= requiredSize.getY())
            && (bounds.getWidth() >= requiredSize.getX())) {
          return true;
        }
      }
      evalView = evalView.getParentView();
    }
    return false;
  }

  /**
   * Determines if the provided {@code view} is possibly clipped by one of its ancestor views in
   * such a way that it may be sufficiently sized if the view were not clipped.
   *
   * @param view the {@link ViewHierarchyElement} to evaluate
   * @param requiredSize a {@link Point} representing the minimum required size of {@code view}
   * @param density the display density
   * @return {@code true} if {@code view}'s size is reduced due to the size of one of its ancestor
   * views, or {@code false} if it is not or this could not be determined.
   */
  private static boolean hasQualifyingClippingAncestor(ViewHierarchyElement view,
      Point requiredSize, float density) {
    Integer rawNonclippedHeight = view.getNonclippedHeight();
    Integer rawNonclippedWidth = view.getNonclippedWidth();
    if ((rawNonclippedHeight == null) || (rawNonclippedWidth == null)) {
      return false;
    }

    Rect clippedBounds = view.getBoundsInScreen();
    int clippedHeight = (int) (clippedBounds.getHeight() / density);
    int clippedWidth = (int) (clippedBounds.getWidth() / density);
    int nonclippedHeight = (int) (rawNonclippedHeight / density);
    int nonclippedWidth = (int) (rawNonclippedWidth / density);
    boolean clippedTooSmallY = clippedHeight < requiredSize.getY();
    boolean clippedTooSmallX = clippedWidth < requiredSize.getX();
    boolean nonclippedTooSmallY = nonclippedHeight < requiredSize.getY();
    boolean nonclippedTooSmallX = nonclippedWidth < requiredSize.getX();

    return (clippedTooSmallY && !nonclippedTooSmallY) || (clippedTooSmallX && !nonclippedTooSmallX);
  }

  /**
   * Appends result messages for {@link #KEY_HAS_TOUCH_DELEGATE} and
   * {@link #KEY_HAS_CLICKABLE_ANCESTOR} to the provided {@code builder} if the relevant keys are
   * set in the given {@code resultMetadata}.
   *
   * @param builder the {@link StringBuilder} to which result messages should be appended
   */
  private static void appendMetadataStringsToMessageIfNeeded(
      Locale locale, ResultMetadata resultMetadata, StringBuilder builder) {
    boolean hasDelegate = resultMetadata.getBoolean(KEY_HAS_TOUCH_DELEGATE, false);
    boolean hasDelegateWithHitRect =
        resultMetadata.getBoolean(KEY_HAS_TOUCH_DELEGATE_WITH_HIT_RECT, false);
    boolean hasClickableAncestor = resultMetadata.getBoolean(KEY_HAS_CLICKABLE_ANCESTOR, false);
    boolean isClippedByAncestor = resultMetadata.getBoolean(KEY_IS_CLIPPED_BY_ANCESTOR, false);
    boolean isAgainstScrollableEdge =
        resultMetadata.getBoolean(KEY_IS_AGAINST_SCROLLABLE_EDGE, false);

    if (hasDelegateWithHitRect) {
      builder
          .append(' ')
          .append(
              String.format(
                  locale,
                  StringManager.getString(
                      locale, "result_message_addendum_touch_delegate_with_hit_rect"),
                  resultMetadata.getInt(KEY_HIT_RECT_WIDTH),
                  resultMetadata.getInt(KEY_HIT_RECT_HEIGHT)));
    } else if (hasDelegate) {
      builder.append(' ')
          .append(StringManager.getString(locale, "result_message_addendum_touch_delegate"));
    }
    if (hasClickableAncestor) {
      builder.append(' ')
          .append(StringManager.getString(locale, "result_message_addendum_clickable_ancestor"));
    }
    if (isClippedByAncestor) {
      builder.append(' ').append(String.format(locale,
          StringManager.getString(locale, "result_message_addendum_clipped_by_ancestor"),
          resultMetadata.getInt(KEY_NONCLIPPED_WIDTH),
          resultMetadata.getInt(KEY_NONCLIPPED_HEIGHT)));
    }
    if (isAgainstScrollableEdge) {
      builder
          .append(' ')
          .append(
              StringManager.getString(locale, "result_message_addendum_against_scrollable_edge"));
    }
  }
} public class TraversalOrderCheck extends AccessibilityHierarchyCheck {

  /** Result when thew view is not visible. */
  public static final int RESULT_ID_NOT_VISIBLE = 1;
  /** Result when thew view is not {@code importantForAccessibility} */
  public static final int RESULT_ID_NOT_IMPORTANT_FOR_ACCESSIBILITY = 2;
  /** Result when the view has an element involved in an accessibilityTraversalBefore loop. */
  public static final int RESULT_ID_TRAVERSAL_BEFORE_CYCLE = 3;
  /** Result when the view has an element involved in an accessibilityTraversalAfter loop. */
  public static final int RESULT_ID_TRAVERSAL_AFTER_CYCLE = 4;
  /** Result when the view has conflicting accessibilityTraversalBefore/After constraints. */
  public static final int RESULT_ID_TRAVERSAL_OVER_CONSTRAINED = 5;

  @Override
  protected @Nullable String getHelpTopic() {
    return "7664232";
  }

  @Override
  public Category getCategory() {
    return Category.IMPLEMENTATION;
  }

  @Override
  public List<AccessibilityHierarchyCheckResult> runCheckOnHierarchy(
      AccessibilityHierarchy hierarchy,
      @Nullable ViewHierarchyElement fromRoot,
      @Nullable Parameters parameters) {
    List<AccessibilityHierarchyCheckResult> results = new ArrayList<>();
    List<? extends ViewHierarchyElement> viewsToEval = getElementsToEvaluate(fromRoot, hierarchy);
    for (ViewHierarchyElement view : viewsToEval) {
      if (!TRUE.equals(view.isVisibleToUser())) {
        results.add(new AccessibilityHierarchyCheckResult(
            this.getClass(),
            AccessibilityCheckResultType.NOT_RUN,
            view,
            RESULT_ID_NOT_VISIBLE,
            null));
        continue;
      }

      if (!view.isImportantForAccessibility()) {
        results.add(new AccessibilityHierarchyCheckResult(
            this.getClass(),
            AccessibilityCheckResultType.NOT_RUN,
            view,
            RESULT_ID_NOT_IMPORTANT_FOR_ACCESSIBILITY,
            null));
        continue;
      }

      // See if view is involved in an accessibilityTraversalBefore cycle or an
      // accessibilityTraversalAfter cycle.
      List<ViewHierarchyElement> beforeChain;
      List<ViewHierarchyElement> afterChain;
      try {
        beforeChain = buildNodeChain(view, (el) -> el.getAccessibilityTraversalBefore());
      } catch (CycleException e) {
        results.add(
            new AccessibilityHierarchyCheckResult(
                this.getClass(),
                AccessibilityCheckResultType.WARNING,
                view,
                RESULT_ID_TRAVERSAL_BEFORE_CYCLE,
                null));
        continue;
      }
      try {
        afterChain = buildNodeChain(view, (el) -> el.getAccessibilityTraversalAfter());
      } catch (CycleException e) {
        results.add(
            new AccessibilityHierarchyCheckResult(
                this.getClass(),
                AccessibilityCheckResultType.WARNING,
                view,
                RESULT_ID_TRAVERSAL_AFTER_CYCLE,
                null));
        continue;
      }

      // See if view is involved in over constraint by before and after.
      Set<ViewHierarchyElement> intersection = intersectionOf(beforeChain, afterChain);
      intersection.remove(view);
      if (!intersection.isEmpty()) {
        results.add(
            new AccessibilityHierarchyCheckResult(
                this.getClass(),
                AccessibilityCheckResultType.WARNING,
                view,
                RESULT_ID_TRAVERSAL_OVER_CONSTRAINED,
                null));
      }
    }

    return results;
  }

  @Override
  public String getMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    return generateMessageForResultId(locale, resultId);
  }

  @Override
  public String getShortMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata) {
    switch(resultId) {
      case RESULT_ID_NOT_VISIBLE:
        return StringManager.getString(locale, "result_message_not_visible");
      case RESULT_ID_NOT_IMPORTANT_FOR_ACCESSIBILITY:
        return StringManager.getString(locale, "result_message_not_important_for_accessibility");
      default:
        return StringManager.getString(locale, "result_message_brief_unpredictable_traversal");
    }
  }

  @Override
  public String getTitleMessage(Locale locale) {
    return StringManager.getString(locale, "check_title_accessibility_traversal");
  }

  private static String generateMessageForResultId(Locale locale, int resultId) {
    switch(resultId) {
      case RESULT_ID_NOT_VISIBLE:
        return StringManager.getString(locale, "result_message_not_visible");
      case RESULT_ID_NOT_IMPORTANT_FOR_ACCESSIBILITY:
        return StringManager.getString(locale, "result_message_not_important_for_accessibility");
      case RESULT_ID_TRAVERSAL_BEFORE_CYCLE:
        return String.format(
            locale,
            StringManager.getString(locale, "result_message_traversal_cycle"),
            "android:accessibilityTraversalBefore");
      case RESULT_ID_TRAVERSAL_AFTER_CYCLE:
        return String.format(
            locale,
            StringManager.getString(locale, "result_message_traversal_cycle"),
            "android:accessibilityTraversalAfter");
      case RESULT_ID_TRAVERSAL_OVER_CONSTRAINED:
        return StringManager.getString(locale, "result_message_traversal_over_constrained");
      default:
        throw new IllegalStateException("Unsupported result id");
    }
  }

  /**
   * Build a sequence of elements starting from {@code start} by repeatedly applying {@code
   * nextElementFunction}.
   *
   * @throws CycleException if a loop is detected
   */
  private static List<ViewHierarchyElement> buildNodeChain(
      ViewHierarchyElement start, NextElementFunction nextElementFunction) throws CycleException {
    List<ViewHierarchyElement> chain = new ArrayList<>();
    chain.add(start);
    ViewHierarchyElement ptr = start;
    while (true) {
      ptr = nextElementFunction.apply(ptr);
      if (ptr == null) {
        return chain;
      }
      if (chain.contains(ptr)) {
        throw new CycleException(chain);
      }
      chain.add(ptr);
    }
  }

  /** Find the elements that two lists have in common. */
  private static <T> Set<T> intersectionOf(List<T> list1, List<T> list2) {
    Set<T> intersection = new HashSet<T>(list1);
    intersection.retainAll(new HashSet<T>(list2));
    return intersection;
  }

  private interface NextElementFunction {
    @Nullable ViewHierarchyElement apply(ViewHierarchyElement el);
  }

  private static class CycleException extends Exception {
    private final List<ViewHierarchyElement> elements;

    CycleException(List<ViewHierarchyElement> elements) {
      this.elements = elements;
    }

    List<ViewHierarchyElement> getElements() {
      return elements;
    }
  }
}
import com.google.common.collect.ImmutableSet;
import java.util.Map;
import org.checkerframework.checker.nullness.qual.Nullable;
import com.google.android.apps.common.testing.accessibility.framework.checks.ClassNameCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.ClickableSpanCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.DuplicateClickableBoundsCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.DuplicateSpeakableTextCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.EditableContentDescCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.ImageContrastCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.LinkPurposeUnclearCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.RedundantDescriptionCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.TextContrastCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.TouchTargetSizeCheck;
import com.google.android.apps.common.testing.accessibility.framework.checks.TraversalOrderCheck;
import com.google.common.collect.ImmutableClassToInstanceMap;
import com.google.common.collect.ImmutableMap;
/*
 * Copyright (C) 2014 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.google.android.apps.common.testing.accessibility.framework;


/**
 * Pre-sets for check configurations used with {@code getConfigForPreset}
 */
public enum AccessibilityCheckPreset {
  /** The latest set of checks (in general the most comprehensive list */
  LATEST,

  /** The set of checks available in the 1.0 release of the framework */
  VERSION_1_0_CHECKS,

  /** The set of checks available in the 2.0 release of the framework */
  VERSION_2_0_CHECKS,

  /** The set of checks available in the 3.0 release of the framework */
  VERSION_3_0_CHECKS,

  /** The set of checks available in the 3.1 release of the framework */
  VERSION_3_1_CHECKS,

  /** Don't check anything */
  NO_CHECKS,

  /**
   * Preset used occasionally to hold checks that are about to be part of {@code LATEST}.
   * Includes all checks in {@code LATEST}.
   */
  PRERELEASE;

  private static final ImmutableClassToInstanceMap<AccessibilityHierarchyCheck>
      CLASS_TO_HIERARCHY_CHECK =
          new ImmutableClassToInstanceMap.Builder<AccessibilityHierarchyCheck>()
              .put(SpeakableTextPresentCheck.class, new SpeakableTextPresentCheck())
              .put(EditableContentDescCheck.class, new EditableContentDescCheck())
              .put(TouchTargetSizeCheck.class, new TouchTargetSizeCheck())
              .put(DuplicateSpeakableTextCheck.class, new DuplicateSpeakableTextCheck())
              .put(TextContrastCheck.class, new TextContrastCheck())
              .put(ClickableSpanCheck.class, new ClickableSpanCheck())
              .put(DuplicateClickableBoundsCheck.class, new DuplicateClickableBoundsCheck())
              .put(RedundantDescriptionCheck.class, new RedundantDescriptionCheck())
              .put(ImageContrastCheck.class, new ImageContrastCheck())