public abstract class AccessibilityHierarchyCheck extends AccessibilityCheck {

  // Template for the URL of a Google Accessibility Help article, with a placeholder for a topic ID.
  private static final String ANDROID_A11Y_HELP_URL =
      "https://support.google.com/accessibility/android/answer/%s";

  /**
   * Returns the identifier of a Google Accessibility Help article related to the issues identified
   * by this check, or {@code null} if there is not yet an article on these issues.
   */
  protected abstract @Nullable String getHelpTopic();

  /**
   * Returns the string representation of a URL for a Google Accessibility Help article related to
   * the issues identified by this check, or {@code null} if there is not yet an article on these
   * issues.
   */
  public @Nullable String getHelpUrl() {
    String topic = getHelpTopic();
    return (topic == null) ? null : String.format(ANDROID_A11Y_HELP_URL, getHelpTopic());
  }

  /** Indicates the category to which the check belongs. */
  public abstract Category getCategory();

  /**
   * Returns the {@link QuestionHandler} associated with this check. Default returns {@code null} if
   * no QuestionHandler has been declared for this check
   */
  @Beta
  public @Nullable QuestionHandler getQuestionHandler() {
    return null;
  }

  /**
   * Generates a static human-readable title message for the check, supporting standard HTML
   * formatting markup.
   *
   * @return a localized {@link CharSequence} of the check title message
   */
  public abstract String getTitleMessage(Locale locale);

  /**
   * Generates a human-readable message describing a given result, supporting standard HTML
   * formatting markup.
   *
   * <p>NOTE: This method should be implemented such that a message can be generated for any result
   * that may be emitted as a {@link AccessibilityHierarchyCheckResult} from the check's {@link
   * #runCheckOnHierarchy} implementation.
   *
   * @param locale desired locale for the message
   * @param result the {@link AccessibilityHierarchyCheckResult} for which to get the message
   * @return a localized message for the result
   */
  public String getMessageForResult(Locale locale, AccessibilityHierarchyCheckResult result) {
    return getMessageForResultData(locale, result.getResultId(), result.getMetadata());
  }

  /**
   * Generates a human-readable message describing a given result id and any associated metadata,
   * supporting standard HTML formatting markup.
   *
   * <p>NOTE: This method should be implemented such that a message can be generated for any result
   * data that may be emitted as a {@link AccessibilityHierarchyCheckResult} from the check's {@link
   * #runCheckOnHierarchy} implementation.
   *
   * @param locale desired locale for the message
   * @param resultId the result ID with which an {@link AccessibilityHierarchyCheckResult} was
   *     created
   * @param metadata metadata from an {@link AccessibilityHierarchyCheckResult}
   * @return a localized message for the result data
   */
  public abstract String getMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata);

  /**
   * Generates a concise human-readable message describing a given result, supporting standard HTML
   * formatting markup.
   *
   * <p>NOTE: This method should be implemented such that a message can be generated for any result
   * that may be emitted as a {@link AccessibilityHierarchyCheckResult} from the check's {@link
   * #runCheckOnHierarchy} implementation.
   *
   * @param locale desired locale for the message
   * @param result the {@link AccessibilityHierarchyCheckResult} for which to get the abbreviated
   *     message
   * @return a localized {@link CharSequence} of the abbreviated message for the result
   */
  public String getShortMessageForResult(Locale locale, AccessibilityHierarchyCheckResult result) {
    return getShortMessageForResultData(locale, result.getResultId(), result.getMetadata());
  }

  /**
   * Generates a concise human-readable message describing a given result id and any associated
   * metadata, supporting standard HTML formatting markup.
   *
   * <p>NOTE: This method should be implemented such that a message can be generated for any result
   * data that may be emitted as a {@link AccessibilityHierarchyCheckResult} from the check's {@link
   * #runCheckOnHierarchy} implementation.
   *
   * @param locale desired locale for the message
   * @param resultId the result ID with which an {@link AccessibilityHierarchyCheckResult} was
   *     created
   * @param metadata metadata from an {@link AccessibilityHierarchyCheckResult}
   * @return a localized {@link CharSequence} of the abbreviated message for the result datq
   */
  public abstract String getShortMessageForResultData(
      Locale locale, int resultId, @Nullable ResultMetadata metadata);

  /**
   * Run the check on a hierarchy of ViewHierarchyElements. Because these elements support partial
   * initialization, they any of their getters may return null. Checks extending this class must
   * therefore be robust to that uncertainty.
   *
   * <p>Any extending class must define constant result IDs which correspond to a distinct result
   * emitted from that class. These IDs are permanent and cannot be removed or changed to correspond
   * to a semantically different result.
   *
   * <p>Similarly, each class may define constant metadata keys which correspond to a distinct piece
   * of result metadata stored by this class. These IDs are permanent and cannot be removed or
   * changed to correspond to semantically different metadata or a different type of metadata.
   *
   * @param hierarchy The hierarchy to be checked.
   * @param fromRoot An optional {@link ViewHierarchyElement} root from which the {@code hierarchy}
   *     should be evaluated. All elements within {@code hierarchy} will be available to the check,
   *     but only elements within {@code fromRoot}'s subtree will be explicitly evaluated. If {@code
   *     null}, checks will evaluate all elements within {@code hierarchy}'s active window.
   * @param parameters Optional input data or preferences.
   * @return A list of interesting results encountered while running the check. The list will be
   *     empty if the check passes without incident.
   */
  public abstract List<AccessibilityHierarchyCheckResult> runCheckOnHierarchy(
      AccessibilityHierarchy hierarchy,
      @Nullable ViewHierarchyElement fromRoot,
      @Nullable Parameters parameters);

  /**
   * @see AccessibilityHierarchyCheck#runCheckOnHierarchy(AccessibilityHierarchy,
   *      ViewHierarchyElement, Metadata)
   */
  public List<AccessibilityHierarchyCheckResult> runCheckOnHierarchy(
      AccessibilityHierarchy hierarchy) {
    return runCheckOnHierarchy(hierarchy, null);
  }

  /**
   * @see AccessibilityHierarchyCheck#runCheckOnHierarchy(AccessibilityHierarchy,
   *      ViewHierarchyElement, Metadata)
   */
  public List<AccessibilityHierarchyCheckResult> runCheckOnHierarchy(
      AccessibilityHierarchy hierarchy, @Nullable ViewHierarchyElement fromRoot) {
    return runCheckOnHierarchy(hierarchy, fromRoot, null);
  }

  /**
   * Gets a secondary priority for a result. This is a meaningless value that indicates a relative
   * importance of results that are within the same category and have the same {@link
   * AccessibilityCheckResult.AccessibilityCheckResultType}. Larger values have a greater
   * importance.
   */
  public @Nullable Double getSecondaryPriority(AccessibilityHierarchyCheckResult result) {
    return null;
  }

  /**
   * Determines the {@link List} of {@link ViewHierarchyElement}s that should be evaluated based on
   * arguments provided to {@link
   * AccessibilityHierarchyCheck#runCheckOnHierarchy(AccessibilityHierarchy, ViewHierarchyElement,
   * Metadata)} If {@code fromRoot} is {@code null}, all element's within {@code hierarchy}'s active
   * window are returned.
   *
   * @param fromRoot the element from which evaluation should occur, or {@code null} if no such
   *     element was provided
   * @param hierarchy the non-{@code null} {@link AccessibilityHierarchy} under evaluation
   * @return a {@link List} of {@link ViewHierarchyElement}s that should be evaluated, in
   *     depth-first ordering
   */
  protected static List<? extends ViewHierarchyElement> getElementsToEvaluate(
      @Nullable ViewHierarchyElement fromRoot, AccessibilityHierarchy hierarchy) {
    return (fromRoot != null) ? fromRoot.getSelfAndAllDescendants()
        : hierarchy.getActiveWindow().getAllViews();
  }

  /** Indicates whether the locale recorded in the {@link DeviceState} was English. */
  protected static boolean isEnglish(AccessibilityHierarchy hierarchy) {
    return hierarchy
        .getDeviceState()
        .getLocale()
        .getLanguage()
        .equals(Locale.ENGLISH.getLanguage());
  }
} public class AccessibilityHierarchyCheckResult extends AccessibilityCheckResult {

  private final int resultId;
  private final @Nullable ViewHierarchyElement element;
  private final @Nullable ResultMetadata metadata;
  private final ImmutableList<Answer> answers;

  /**
   * Constructor when there are {@link Answer} elements associated with this result
   *
   * @param checkClass The class of the check reporting the error
   * @param type The type of result
   * @param element The element that the result pertains to
   * @param resultId an integer unique to all results emitted from a single class
   * @param metadata extra data about this result
   * @param answers the answers about this result
   */
  @Beta
  public AccessibilityHierarchyCheckResult(
      Class<? extends AccessibilityHierarchyCheck> checkClass,
      AccessibilityCheckResultType type,
      @Nullable ViewHierarchyElement element,
      int resultId,
      @Nullable ResultMetadata metadata,
      ImmutableList<Answer> answers) {
    super(checkNotNull(checkClass), checkNotNull(type), null);
    this.element = element;
    this.resultId = resultId;
    this.metadata = metadata;
    this.answers = answers;
  }

  /**
   * @param checkClass The class of the check reporting the error
   * @param type The type of result
   * @param element The element that the result pertains to
   * @param resultId an integer unique to all results emitted from a single class
   * @param metadata extra data about this result
   */
  public AccessibilityHierarchyCheckResult(
      Class<? extends AccessibilityHierarchyCheck> checkClass,
      AccessibilityCheckResultType type,
      @Nullable ViewHierarchyElement element,
      int resultId,
      @Nullable ResultMetadata metadata) {
    this(checkClass, type, element, resultId, metadata, ImmutableList.of());
  }

  /**
   * Returns a copy of this result, but with the AccessibilityCheckResultType changed to SUPPRESSED.
   */
  public AccessibilityHierarchyCheckResult getSuppressedResultCopy() {
    return new AccessibilityHierarchyCheckResult(
        getSourceCheckClass().asSubclass(AccessibilityHierarchyCheck.class),
        AccessibilityCheckResultType.SUPPRESSED,
        getElement(),
        resultId,
        metadata,
        answers);
  }

  /**
   * Creates an {@link AccessibilityHierarchyCheckResult} from its protocol buffer format
   *
   * @param proto The protocol buffer representation of a result, created with {@link #toProto()}
   * @param associatedHierarchy The {@link AccessibilityHierarchy} that was evaluated to produce
   *     this result
   */
  public static AccessibilityHierarchyCheckResult fromProto(
      AccessibilityHierarchyCheckResultProto proto, AccessibilityHierarchy associatedHierarchy) {
    AccessibilityHierarchyCheck check =
        AccessibilityCheckPreset.getHierarchyCheckForClassName(proto.getSourceCheckClass());
    checkNotNull(check, "Failed to resolve check class: %s", proto.getSourceCheckClass());
    int resultId = proto.getResultId();
    AccessibilityCheckResultType type =
        AccessibilityCheckResultType.fromProto(proto.getResultType());
    HashMapResultMetadata metadata =
        proto.hasMetadata() ? HashMapResultMetadata.fromProto(proto.getMetadata()) : null;
    ViewHierarchyElement element =
        proto.hasHierarchySourceId()
            ? associatedHierarchy.getViewById(proto.getHierarchySourceId())
            : null;
    ImmutableList.Builder<Answer> answersBuilder = ImmutableList.builder();

    for (AnswerProto answer : proto.getAnswersList()) {
      answersBuilder.add(Answer.fromProto(answer, associatedHierarchy));
    }

    return new AccessibilityHierarchyCheckResult(
        check.getClass(), type, element, resultId, metadata, answersBuilder.build());
  }

  /**
   * Returns a general static "title" message for the {@link AccessibilityHierarchyCheck} that
   * generated this result, which may contain formatting markup.
   *
   * @param locale desired locale for the message
   */
  public String getRawTitleMessage(Locale locale) {
    return getCheck().getTitleMessage(locale);
  }

  /**
   * Returns a general static "title" message for the {@link AccessibilityHierarchyCheck} that
   * generated this result, without formatting markup.
   *
   * @param locale desired locale for the message
   */
  public CharSequence getTitleMessage(Locale locale) {
    return Jsoup.parse(getRawTitleMessage(locale)).text();
  }

  /**
   * Returns a human-readable message representing the result identified by the check, which may
   * contain formatting markup
   *
   * @param locale desired locale for the message
   */
  public String getRawMessage(Locale locale) {
    return getCheck().getMessageForResult(locale, this);
  }

  @Override
  public CharSequence getMessage(Locale locale) {
    return Jsoup.parse(getRawMessage(locale)).text();
  }

  /**
   * Returns a concise human-readable message representing the result identified by the check,
   * which may contain formatting markup
   *
   * @param locale desired locale for the message
   */
  public String getRawShortMessage(Locale locale) {
    return getCheck().getShortMessageForResult(locale, this);
  }

  /**
   * Returns a concise human-readable message representing the result identified by the check,
   * which may contain formatting markup.
   *
   * @param locale desired locale for the message
   */
  public CharSequence getShortMessage(Locale locale) {
    return Jsoup.parse(getRawShortMessage(locale)).text();
  }

  /**
   * Returns the integer id of this result. This id is unique within the class of the
   * {@link AccessibilityHierarchyCheck} class associated with this result, and is used to
   * differentiate different types of issues identified by a single check.
   * @return the id of this result.
   */
  public int getResultId() {
    return resultId;
  }

  /**
   * Returns the list of {@link Answer} to questions asked and answered for this result.
   *
   * @return the list of answers to asked questions
   */
  @Beta
  public ImmutableList<Answer> getAnswers() {
    return answers;
  }

  /**
   * Retrieve the metadata stored in this result. This metadata is data computed during check
   * execution and is used to describe the specifics of a result or provide additional details about
   * a particular finding. The metadata keys for a given {@link AccessibilityHierarchyCheck} are
   * defined as constants in each {@code AccessibilityHierarchyCheck} class, and are unique within
   * that class.
   *
   * @return a metadata for this result, or {@code null} if none was provided
   */
  @Pure
  public @Nullable ResultMetadata getMetadata() {
    return metadata;
  }

  /**
   * The {@link ViewHierarchyElement} within a view hierarchy to which this result refers
   *
   * @return the element associated with this result, or {@code null} if this result does not refer
   *     to a specific element within a view hierarchy.
   */
  @Pure
  public @Nullable ViewHierarchyElement getElement() {
    return element;
  }

  /**
   * Gets a secondary priority for the result.
   *
   * @see AccessibilityHierarchyCheck#getSecondaryPriority
   */
  public @Nullable Double getSecondaryPriority() {
    return getCheck().getSecondaryPriority(this);
  }

  public AccessibilityHierarchyCheckResultProto toProto() {
    AccessibilityHierarchyCheckResultProto.Builder builder =
        AccessibilityHierarchyCheckResultProto.newBuilder();
    builder.setResultId(getResultId());
    if (getSourceCheckClass() != null) {
      builder.setSourceCheckClass(getSourceCheckClass().getName());
    }
    if (getType() != null) {
      builder.setResultType(getType().toProto());
    }
    if (getMetadata() instanceof HashMapResultMetadata) {
      builder.setMetadata(((HashMapResultMetadata) getMetadata()).toProto());
    }
    if (getElement() != null) {
      builder.setHierarchySourceId(getElement().getCondensedUniqueId());
    }
    for (Answer answer : getAnswers()) {
      builder.addAnswers(answer.toProto());
    }

    return builder.build();
  }

  @Override
  public boolean equals(@Nullable Object o) {
    if (this == o) {
      return true;
    }
    if (!(o instanceof AccessibilityHierarchyCheckResult)) {
      return false;
    }

    AccessibilityHierarchyCheckResult that = (AccessibilityHierarchyCheckResult) o;

    if (getType() != that.getType()) {
      return false;
    }
    if (getResultId() != that.getResultId()) {
      return false;
    }
    if (getSourceCheckClass() != that.getSourceCheckClass()) {
      return false;
    }
    ViewHierarchyElement thatElement = that.getElement();
    if (getElement() != null) {
      if ((thatElement == null)
          || (getElement().getCondensedUniqueId() != thatElement.getCondensedUniqueId())) {
        return false;
      }
    } else if (thatElement != null) {
      return false;
    }
    if (!Objects.equals(getMetadata(), that.getMetadata())) {
      return false;
    }
    return getAnswers().equals(that.getAnswers());
  }

  @Override
  public int hashCode() {
    return Objects.hash(
        getType(), getResultId(), getSourceCheckClass(), getElement(), getMetadata(), getAnswers());
  }

  // For debugging
  @Override
  public String toString() {
    return String.format(
        "AccessibilityHierarchyCheckResult %s %s %s %s %s num_answers:%d",
        getType(),
        getSourceCheckClass().getSimpleName(),
        getResultId(),
        getElement(),
        getMetadata(),
        getAnswers().size());
  }

  @SuppressWarnings("unchecked") // all Hierarchy results have Hierarchy check classes
  private AccessibilityHierarchyCheck getCheck() {
    AccessibilityHierarchyCheck check =
        AccessibilityCheckPreset.getHierarchyCheckForClass(
            (Class<? extends AccessibilityHierarchyCheck>) getSourceCheckClass());
    return checkNotNull(check, "Failed to resolve check class: %s", getSourceCheckClass());
  }
} public class Parameters {

  @Nullable private Image screenCapture;
  @Nullable private Double customTextContrastRatio;
  @Nullable private Double customImageContrastRatio;
  @Nullable private Integer customTouchTargetSize;
  @Nullable private Boolean enableEnhancedContrastEvaluation;
  @Nullable private Boolean saveViewImage;

  public Parameters() {
    super();
  }

  /**
   * Gets screen capture data.
   *
   * @return {@link Image} containing screen capture data, or {@code null} if no screen capture data
   *     was set.
   * @see #putScreenCapture(Image)
   */
  public @Nullable Image getScreenCapture() {
    return screenCapture;
  }

  /**
   * Sets screen capture data in {@code parameters}. This is typically used by checks that assess
   * properties of the visual appearance of an interface. For example, contrast evaluation samples
   * regions of the display to heuristically evaluate foreground/background contrast ratios.
   *
   * <p>Expected data: A full image of the device's default display at the time check execution
   * occurs. The image should have dimensions which match the DisplayMetrics obtained from
   * WindowManager's default display. The screen area should not be altered, cropped, or adjusted in
   * any way.
   *
   * @param image {@link Image} containing screen capture data
   */
  public void putScreenCapture(Image image) {
    screenCapture = checkNotNull(image);
  }

  /**
   * Specifies a preference for whether images of the subject Views should be preserved. These may
   * be useful for debugging.
   *
   * <p>Presently, this is only used for heuristic contrast evaluation.
   */
  public void setSaveViewImages(boolean save) {
    saveViewImage = save;
  }

  /** Gets the preference for whether images of the subject Views should be preserved. */
  public @Nullable Boolean getSaveViewImages() {
    return saveViewImage;
  }

  /**
   * Gets a user-defined minimum contrast ratio.
   *
   * @return The user-defined minimum contrast ratio, or {@code null} if a user-defined value has
   *     not been set.
   * @see #putCustomContrastRatio(double)
   * @deprecated Use {@link #getCustomTextContrastRatio} and {@link #getCustomImageContrastRatio}
   */
  @Deprecated
  public @Nullable Double getCustomContrastRatio() {
    throw new UnsupportedOperationException();
  }

  /**
   * Gets a user-defined minimum text contrast ratio.
   *
   * @return The user-defined minimum text contrast ratio, or {@code null} if a user-defined value
   *     has not been set.
   * @see #putCustomTextContrastRatio(double)
   */
  public @Nullable Double getCustomTextContrastRatio() {
    return customTextContrastRatio;
  }

  /**
   * Gets a user-defined minimum image contrast ratio.
   *
   * @return The user-defined minimum image contrast ratio, or {@code null} if a user-defined value
   *     has not been set.
   * @see #putCustomImageContrastRatio(double)
   */
  public @Nullable Double getCustomImageContrastRatio() {
    return customImageContrastRatio;
  }

  /**
   * Sets a user-defined minimum contrast ratio for use by {@link
   * com.google.android.apps.common.testing.accessibility.framework.checks.ImageContrastCheck} or
   * {@link
   * com.google.android.apps.common.testing.accessibility.framework.checks.TextContrastCheck}. A
   * value set here should override the default value used by those checks.
   *
   * @param contrastRatio a user-defined minimum contrast ratio
   * @deprecated Use {@link #putCustomTextContrastRatio(double)} and {@link
   *     #putCustomImageContrastRatio(double)}
   */
  @Deprecated
  public void putCustomContrastRatio(double contrastRatio) {
    putCustomTextContrastRatio(contrastRatio);
    putCustomImageContrastRatio(contrastRatio);
  }

  /**
   * Sets a user-defined minimum text contrast ratio for use by {@link
   * com.google.android.apps.common.testing.accessibility.framework.checks.TextContrastCheck}. A
   * value set here should override the default value used by this check.
   *
   * @param textContrastRatio a user-defined minimum text contrast ratio
   */
  public void putCustomTextContrastRatio(double textContrastRatio) {
    customTextContrastRatio = textContrastRatio;
  }

  /**
   * Sets a user-defined minimum contrast ratio for use by {@link
   * com.google.android.apps.common.testing.accessibility.framework.checks.ImageContrastCheck}. A
   * value set here should override the default value used by this check.
   *
   * @param imageContrastRatio a user-defined minimum image contrast ratio
   */
  public void putCustomImageContrastRatio(double imageContrastRatio) {
    customImageContrastRatio = imageContrastRatio;
  }

  /**
   * Gets a user-defined minimum touch target size from {@code parameters}.
   *
   * @return The user-defined minimum touch target size, or {@code null} if a user-defined value has
   *     not been set.
   * @see #putCustomTouchTargetSize(int)
   */
  public @Nullable Integer getCustomTouchTargetSize() {
    return customTouchTargetSize;
  }

  /**
   * Sets a user-defined minimum touch target size for use by {@link
   * com.google.android.apps.common.testing.accessibility.framework.checks.TouchTargetSizeCheck}. A
   * value set here should override the default value used by the check.
   *
   * @param touchTargetSize a user-defined minimum touch target size in pixels
   */
  public void putCustomTouchTargetSize(int touchTargetSize) {
    customTouchTargetSize = touchTargetSize;
  }

  /**
   * Gets an user-defined boolean value for enabling enhanced contrast evaluation from {@code
   * parameters}.
   *
   * @return The boolean value that turns on/off an enhanced contrast evaluation, or {@code null} if
   *     a user-defined value has not been set.
   * @see #putEnhancedContrastEvaluationMode(boolean)
   */
  public @Nullable Boolean getEnableEnhancedContrastEvaluation() {
    return enableEnhancedContrastEvaluation;
  }

  /**
   * Sets a user-defined boolean value for enabling enhanced contrast evaluation use by {@link
   * com.google.android.apps.common.testing.accessibility.framework.checks.ImageContrastCheck} and
   * {@link com.google.android.apps.common.testing.accessibility.framework.checks.TextContrastCheck}
   * that evaluates multiple foreground colors. A value set here should override the default value
   * used by the check.
   *
   * @param enableEnhancedContrastEvaluation {@code true} to enable enhanced contrast evaluation,
   *     {@code false} to disable.
   */
  public void putEnableEnhancedContrastEvaluation(boolean enableEnhancedContrastEvaluation) {
    this.enableEnhancedContrastEvaluation = enableEnhancedContrastEvaluation;
  }
} 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 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 AccessibilityHierarchy {
  /* A representation of the device's state at the time a hierarchy is initially captured */
  private final DeviceState deviceState;

  /* The id of each window corresponds to its position in this list */
  private final List<? extends WindowHierarchyElement> windowHierarchyElements;

  /* A reference to the 'active' window. Exactly one such window exists in any hierarchy. */
  private final WindowHierarchyElement activeWindow;

  /* A nested class that stores all unique view element class names. */
  protected final ViewElementClassNames viewElementClassNames;

  protected AccessibilityHierarchy(
      DeviceState deviceState,
      List<? extends WindowHierarchyElement> windowHierarchyElements,
      WindowHierarchyElement activeWindow,
      ViewElementClassNames viewElementClassNames) {
    this.deviceState = deviceState;
    this.windowHierarchyElements = windowHierarchyElements;
    this.activeWindow = activeWindow;
    this.viewElementClassNames = viewElementClassNames;
  }

  /**
   * @return the {@link DeviceState} representing certain properties of the device at the time an
   *         {@link AccessibilityHierarchy} was originally captured.
   */
  public DeviceState getDeviceState() {
    return deviceState;
  }

  /**
   * Get all {@link WindowHierarchyElement}s in this hierarchy.
   *
   * @return An unmodifiable collection of all windows in hierarchy
   */
  public Collection<? extends WindowHierarchyElement> getAllWindows() {
    return Collections.unmodifiableCollection(windowHierarchyElements);
  }

  /**
   * @return a {@link WindowHierarchyElement} representing the active window in this hierarchy. If
   *     this hierarchy was originally constructed from a {@code AccessibilityNodeInfo} or {@code
   *     View}, this returns the default {@link WindowHierarchyElement} that was implicitly created
   *     to hold the hierarchy.
   */
  public WindowHierarchyElement getActiveWindow() {
    return activeWindow;
  }

  /**
   * @param id The identifier for the desired {@link WindowHierarchyElement}, as returned by
   *        {@link WindowHierarchyElement#getId()}.
   * @return The {@link WindowHierarchyElement} identified by {@code id} in this hierarchy
   * @throws NoSuchElementException if no window within this hierarchy matches the provided
   *         {@code id}
   */
  public WindowHierarchyElement getWindowById(int id) {
    if ((id < 0) || (id >= windowHierarchyElements.size())) {
      throw new NoSuchElementException();
    }
    return windowHierarchyElements.get(id);
  }

  /**
   * @param condensedUniqueId The identifier for the desired {@link ViewHierarchyElement}, as
   *        returned by {@link ViewHierarchyElement#getCondensedUniqueId()}
   * @return The {@link ViewHierarchyElement} identified by {@code id} in this hierarchy
   * @throws NoSuchElementException if no view within this hierarchy matches the provided
   *         {@code condensedUniqueId}
   */
  public ViewHierarchyElement getViewById(long condensedUniqueId) {
    int windowId = (int) (condensedUniqueId >>> 32);
    int viewId = (int) condensedUniqueId;
    return getWindowById(windowId).getViewById(viewId);
  }

  /**
   * @return an {@link AccessibilityHierarchyProto} protocol buffer representation of this hierarchy
   */
  public AccessibilityHierarchyProto toProto() {
    AccessibilityHierarchyProto.Builder builder = AccessibilityHierarchyProto.newBuilder();
    builder
        .setDeviceState(deviceState.toProto())
        .setActiveWindowId(activeWindow.getId())
        .setViewElementClassNames(viewElementClassNames.toProto());
    for (WindowHierarchyElement window : windowHierarchyElements) {
      builder.addWindows(window.toProto());
    }

    return builder.build();
  }

  /** Set backpointers from the windows to the accessibility hierarchy. */
  private void setAccessibilityHierarchy() {
    for (WindowHierarchyElement window : this.windowHierarchyElements) {
      window.setAccessibilityHierarchy(this);
    }
  }

  /**
   * Returns a new builder that can build an AccessibilityHierarchy from a proto.
   *
   * @param proto A protocol buffer representation of a hierarchy
   */
  public static Builder newBuilder(AccessibilityHierarchyProto proto) {
    Builder builder = new Builder();
    builder.proto = checkNotNull(proto);
    return builder;
  }

  /** Returns a {@link ViewElementClassNames} that contains view class name and identifiers. */
  ViewElementClassNames getViewElementClassNames() {
    return viewElementClassNames;
  }

  /**
   * Maintains a bidirectional mapping between View class names and a unique integer identifier for
   * each of those classes.
   */
  static class ViewElementClassNames {
    protected ImmutableBiMap<String, Integer> uniqueViewElementsClassNames;

    protected ViewElementClassNames() {}

    /** Populate bimap from a given map. */
    public ViewElementClassNames(Map<String, Integer> viewElementsMap) {
      this.uniqueViewElementsClassNames = ImmutableBiMap.copyOf(viewElementsMap);
    }

    /** Populate bimap from a given proto. */
    public ViewElementClassNames(ViewElementClassNamesProto proto) {
      this.uniqueViewElementsClassNames = ImmutableBiMap.copyOf(proto.getClassNameMap());
    }

    /** Returns an identifier associated with a given class name. Returns null if not found. */
    @Nullable
    public Integer getIdentifierForClassName(String className) {
      return uniqueViewElementsClassNames.get(className);
    }

    /** Returns a class name associated with a given identifier. Returns null if not found. */
    @Nullable
    public String getClassNameForIdentifier(int id) {
      return uniqueViewElementsClassNames.inverse().get(id);
    }

    ViewElementClassNamesProto toProto() {
      return ViewElementClassNamesProto.newBuilder()
          .putAllClassName(uniqueViewElementsClassNames)
          .build();
    }

    ImmutableBiMap<String, Integer> getMap() {
      return uniqueViewElementsClassNames;
    }
  }

  /**
   * A builder for {@link AccessibilityHierarchy}; obtained using {@link
   * AccessibilityHierarchy#builder}.
   */
  public static class Builder {
    @Nullable protected AccessibilityHierarchyProto proto;

    public AccessibilityHierarchy build() {
      AccessibilityHierarchy result;

      if (proto != null) {
        result = buildHierarchyFromProto(proto);
      } else {
        throw new IllegalStateException("Nothing from which to build");
      }
      return result;
    }

    private AccessibilityHierarchy buildHierarchyFromProto(AccessibilityHierarchyProto proto) {
      DeviceState deviceState = new DeviceState(proto.getDeviceState());
      int activeWindowId = proto.getActiveWindowId();

      List<WindowHierarchyElement> windowHierarchyElements =
          new ArrayList<>(proto.getWindowsCount());
      for (WindowHierarchyElementProto windowProto : proto.getWindowsList()) {
        windowHierarchyElements.add(WindowHierarchyElement.newBuilder(windowProto).build());
      }
      checkState(
          !windowHierarchyElements.isEmpty(), "Hierarchies must contain at least one window.");
      WindowHierarchyElement activeWindow = windowHierarchyElements.get(activeWindowId);
      ViewElementClassNames viewElementClassNames =
          new ViewElementClassNames(proto.getViewElementClassNames());

      AccessibilityHierarchy hierarchy =
          new AccessibilityHierarchy(
              deviceState, windowHierarchyElements, activeWindow, viewElementClassNames);
      hierarchy.setAccessibilityHierarchy();
      return hierarchy;
    }
  }
} public class ViewHierarchyElement {
  protected final int id;
  protected final @Nullable Integer parentId;

  // Created lazily, because many views are leafs.
  protected @MonotonicNonNull List<Integer> childIds;

  // This field is set to a non-null value after construction.
  private @MonotonicNonNull WindowHierarchyElement windowElement;

  protected final @Nullable CharSequence packageName;
  protected final @Nullable CharSequence className;
  protected final @Nullable CharSequence accessibilityClassName;
  protected final @Nullable String resourceName;
  protected final @Nullable SpannableString contentDescription;
  protected final @Nullable SpannableString text;
  protected final boolean importantForAccessibility;
  protected final @Nullable Boolean visibleToUser;
  protected final boolean clickable;
  protected final boolean longClickable;
  protected final boolean focusable;
  protected final @Nullable Boolean editable;
  protected final @Nullable Boolean scrollable;
  protected final @Nullable Boolean canScrollForward;
  protected final @Nullable Boolean canScrollBackward;
  protected final @Nullable Boolean checkable;
  protected final @Nullable Boolean checked;
  protected final @Nullable Boolean hasTouchDelegate;
  protected final @Nullable Rect boundsInScreen;
  protected final @Nullable Integer nonclippedHeight;
  protected final @Nullable Integer nonclippedWidth;
  protected final @Nullable Float textSize;
  protected final @Nullable Integer textColor;
  protected final @Nullable Integer backgroundDrawableColor;
  protected final @Nullable Integer typefaceStyle;
  protected final boolean enabled;
  protected final @Nullable Integer drawingOrder;
  protected final ImmutableList<ViewHierarchyAction> actionList;
  protected final @Nullable LayoutParams layoutParams;
  protected final @Nullable SpannableString hintText; // only for TextView
  protected final @Nullable Integer hintTextColor; // only for TextView
  protected final List<Rect> textCharacterLocations;

  // Populated only after a hierarchy is constructed
  protected @Nullable Long labeledById;
  protected @Nullable Long accessibilityTraversalBeforeId;
  protected @Nullable Long accessibilityTraversalAfterId;
  protected List<Rect> touchDelegateBounds;

  // A list of identifiers that represents all the superclasses of the corresponding view element.
  protected final List<Integer> superclassViews;

  protected ViewHierarchyElement(
      int id,
      @Nullable Integer parentId,
      List<Integer> childIds,
      @Nullable CharSequence packageName,
      @Nullable CharSequence className,
      @Nullable CharSequence accessibilityClassName,
      @Nullable String resourceName,
      @Nullable SpannableString contentDescription,
      @Nullable SpannableString text,
      boolean importantForAccessibility,
      @Nullable Boolean visibleToUser,
      boolean clickable,
      boolean longClickable,
      boolean focusable,
      @Nullable Boolean editable,
      @Nullable Boolean scrollable,
      @Nullable Boolean canScrollForward,
      @Nullable Boolean canScrollBackward,
      @Nullable Boolean checkable,
      @Nullable Boolean checked,
      @Nullable Boolean hasTouchDelegate,
      List<Rect> touchDelegateBounds,
      @Nullable Rect boundsInScreen,
      @Nullable Integer nonclippedHeight,
      @Nullable Integer nonclippedWidth,
      @Nullable Float textSize,
      @Nullable Integer textColor,
      @Nullable Integer backgroundDrawableColor,
      @Nullable Integer typefaceStyle,
      boolean enabled,
      @Nullable Long labeledById,
      @Nullable Long accessibilityTraversalBeforeId,
      @Nullable Long accessibilityTraversalAfterId,
      @Nullable Integer drawingOrder,
      List<Integer> superclassViews,
      List<? extends ViewHierarchyAction> actionList,
      @Nullable LayoutParams layoutParams,
      @Nullable SpannableString hintText,
      @Nullable Integer hintTextColor,
      List<Rect> textCharacterLocations) {
    this.id = id;
    this.parentId = parentId;
    if (!childIds.isEmpty()) {
      this.childIds = new ArrayList<>(childIds.size());
      this.childIds.addAll(childIds);
    }
    this.packageName = packageName;
    this.className = className;
    this.accessibilityClassName = accessibilityClassName;
    this.resourceName = resourceName;
    this.contentDescription = contentDescription;
    this.text = text;
    this.importantForAccessibility = importantForAccessibility;
    this.visibleToUser = visibleToUser;
    this.clickable = clickable;
    this.longClickable = longClickable;
    this.focusable = focusable;
    this.editable = editable;
    this.scrollable = scrollable;
    this.canScrollForward = canScrollForward;
    this.canScrollBackward = canScrollBackward;
    this.checkable = checkable;
    this.checked = checked;
    this.hasTouchDelegate = hasTouchDelegate;
    this.touchDelegateBounds = touchDelegateBounds;
    this.boundsInScreen = boundsInScreen;
    this.nonclippedHeight = nonclippedHeight;
    this.nonclippedWidth = nonclippedWidth;
    this.textSize = textSize;
    this.textColor = textColor;
    this.backgroundDrawableColor = backgroundDrawableColor;
    this.typefaceStyle = typefaceStyle;
    this.enabled = enabled;
    this.labeledById = labeledById;
    this.accessibilityTraversalBeforeId = accessibilityTraversalBeforeId;
    this.accessibilityTraversalAfterId = accessibilityTraversalAfterId;
    this.drawingOrder = drawingOrder;
    this.superclassViews = superclassViews;
    if (actionList != null && !actionList.isEmpty()) {
      this.actionList = ImmutableList.copyOf(actionList);
    } else {
      this.actionList = ImmutableList.of();
    }
    this.layoutParams = layoutParams;
    this.hintText = hintText;
    this.hintTextColor = hintTextColor;
    this.textCharacterLocations = textCharacterLocations;
  }

  ViewHierarchyElement(ViewHierarchyElementProto proto) {
    checkNotNull(proto);

    // Bookkeeping
    this.id = proto.getId();
    this.parentId = (proto.getParentId() != -1) ? proto.getParentId() : null;
    if (proto.getChildIdsCount() > 0) {
      this.childIds = new ArrayList<>(proto.getChildIdsCount());
      this.childIds.addAll(proto.getChildIdsList());
    }

    packageName = proto.hasPackageName() ? proto.getPackageName() : null;
    className = proto.hasClassName() ? proto.getClassName() : null;
    accessibilityClassName =
        proto.hasAccessibilityClassName() ? proto.getAccessibilityClassName() : null;
    resourceName = proto.hasResourceName() ? proto.getResourceName() : null;
    contentDescription =
        proto.hasContentDescription() ? new SpannableString(proto.getContentDescription()) : null;
    text = proto.hasText() ? new SpannableString(proto.getText()) : null;
    importantForAccessibility = proto.getImportantForAccessibility();
    visibleToUser = proto.hasVisibleToUser() ? proto.getVisibleToUser() : null;
    clickable = proto.getClickable();
    longClickable = proto.getLongClickable();
    focusable = proto.getFocusable();
    editable = proto.hasEditable() ? proto.getEditable() : null;
    scrollable = proto.hasScrollable() ? proto.getScrollable() : null;
    canScrollForward = proto.hasCanScrollForward() ? proto.getCanScrollForward() : null;
    canScrollBackward = proto.hasCanScrollBackward() ? proto.getCanScrollBackward() : null;
    checkable = proto.hasCheckable() ? proto.getCheckable() : null;
    checked = proto.hasChecked() ? proto.getChecked() : null;
    hasTouchDelegate = proto.hasHasTouchDelegate() ? proto.getHasTouchDelegate() : null;
    if (proto.getTouchDelegateBoundsCount() > 0) {
      ImmutableList.Builder<Rect> builder = ImmutableList.<Rect>builder();
      for (int i = 0; i < proto.getTouchDelegateBoundsCount(); ++i) {
        builder.add(new Rect(proto.getTouchDelegateBounds(i)));
      }
      touchDelegateBounds = builder.build();
    } else {
      touchDelegateBounds = ImmutableList.of();
    }
    boundsInScreen = proto.hasBoundsInScreen() ? new Rect(proto.getBoundsInScreen()) : null;
    nonclippedHeight = proto.hasNonclippedHeight() ? proto.getNonclippedHeight() : null;
    nonclippedWidth = proto.hasNonclippedWidth() ? proto.getNonclippedWidth() : null;
    textSize = proto.hasTextSize() ? proto.getTextSize() : null;
    textColor = proto.hasTextColor() ? proto.getTextColor() : null;
    backgroundDrawableColor =
        proto.hasBackgroundDrawableColor() ? proto.getBackgroundDrawableColor() : null;
    typefaceStyle = proto.hasTypefaceStyle() ? proto.getTypefaceStyle() : null;
    enabled = proto.getEnabled();
    labeledById = proto.hasLabeledById() ? proto.getLabeledById() : null;
    accessibilityTraversalBeforeId =
        proto.hasAccessibilityTraversalBeforeId()
            ? proto.getAccessibilityTraversalBeforeId()
            : null;
    accessibilityTraversalAfterId =
        proto.hasAccessibilityTraversalAfterId() ? proto.getAccessibilityTraversalAfterId() : null;
    superclassViews = proto.getSuperclassesList();
    drawingOrder = proto.hasDrawingOrder() ? proto.getDrawingOrder() : null;
    ImmutableList.Builder<ViewHierarchyAction> actionBuilder = new ImmutableList.Builder<>();
    for (ViewHierarchyActionProto actionProto : proto.getActionsList()) {
      actionBuilder.add(new ViewHierarchyAction(actionProto));
    }
    actionList = actionBuilder.build();
    layoutParams = proto.hasLayoutParams() ? new LayoutParams(proto.getLayoutParams()) : null;
    hintText = proto.hasHintText() ? new SpannableString(proto.getHintText()) : null;
    hintTextColor = proto.hasHintTextColor() ? proto.getHintTextColor() : null;
    ImmutableList.Builder<Rect> characterLocations = ImmutableList.<Rect>builder();
    if (proto.getTextCharacterLocationsCount() > 0) {
      for (int i = 0; i < proto.getTextCharacterLocationsCount(); ++i) {
        characterLocations.add(new Rect(proto.getTextCharacterLocations(i)));
      }
    }
    textCharacterLocations = characterLocations.build();
  }

  /**
   * Returns the value uniquely identifying this window within the context of its containing {@link
   * WindowHierarchyElement}.
   */
  public int getId() {
    return id;
  }

  /**
   * @return a value uniquely representing this {@link ViewHierarchyElement} and its containing
   *         {@link WindowHierarchyElement} in the context of it's containing
   *         {@link AccessibilityHierarchy}.
   */
  public long getCondensedUniqueId() {
    return (((long) getWindow().getId() << 32) | getId());
  }

  /**
   * @return The parent {@link ViewHierarchyElement} of this view, or {@code null} if this is a root
   *     view.
   * @see android.view.accessibility.AccessibilityNodeInfo#getParent()
   * @see android.view.View#getParent()
   */
  @Pure
  public @Nullable ViewHierarchyElement getParentView() {
    Integer parentIdtmp = parentId;
    return (parentIdtmp != null) ? getWindow().getViewById(parentIdtmp) : null;
  }

  /**
   * @return The number of child {@link ViewHierarchyElement}s rooted at this view
   * @see android.view.accessibility.AccessibilityNodeInfo#getChildCount()
   * @see android.view.ViewGroup#getChildCount()
   */
  public int getChildViewCount() {
    return (childIds == null) ? 0 : childIds.size();
  }

  /**
   * @param atIndex The index of the child {@link ViewHierarchyElement} to obtain. Must be &ge 0 and
   *        &lt {@link #getChildViewCount()}.
   * @return The requested child, or {@code null} if no such child exists at the given
   *         {@code atIndex}
   * @throws NoSuchElementException if {@code atIndex} is less than 0 or greater than
   *         {@code getChildViewCount() - 1}
   */
  public ViewHierarchyElement getChildView(int atIndex) {
    if ((atIndex < 0) || (childIds == null) || (atIndex >= childIds.size())) {
      throw new NoSuchElementException();
    }
    return getWindow().getViewById(childIds.get(atIndex));
  }

  /**
   * @return an unmodifiable {@link List} containing this {@link ViewHierarchyElement} and any
   *     descendants, direct or indirect, in depth-first ordering.
   */
  public List<? extends ViewHierarchyElement> getSelfAndAllDescendants() {
    List<ViewHierarchyElement> listToPopulate = new ArrayList<>();
    listToPopulate.add(this);
    for (int i = 0; i < getChildViewCount(); ++i) {
      listToPopulate.addAll(getChildView(i).getSelfAndAllDescendants());
    }

    return Collections.unmodifiableList(listToPopulate);
  }

  /**
   * @return The containing {@link WindowHierarchyElement} of this view.
   */
  public WindowHierarchyElement getWindow() {

    // The type is explicit because the @MonotonicNonNull field is not read as @Nullable.
    return Preconditions.<@Nullable WindowHierarchyElement>checkNotNull(windowElement);
  }

  /**
   * @return The package name to which this view belongs, or {@code null} if one cannot be
   *     determined
   * @see android.view.accessibility.AccessibilityNodeInfo#getPackageName()
   * @see android.content.Context#getPackageName()
   */
  public @Nullable CharSequence getPackageName() {
    return packageName;
  }

  /**
   * @return The class name to which this view belongs, or {@code null} if one cannot be determined
   * @see android.view.accessibility.AccessibilityNodeInfo#getPackageName()
   * @see android.view.View#getClass()
   */
  public @Nullable CharSequence getClassName() {
    return className;
  }

  /**
   * @return The view id's associated resource name, or {@code null} if one cannot be determined or
   *     is not available
   * @see android.view.accessibility.AccessibilityNodeInfo#getViewIdResourceName()
   * @see android.view.View#getId()
   * @see android.content.res.Resources#getResourceName(int)
   */
  @Pure
  public @Nullable String getResourceName() {
    return resourceName;
  }

  /**
   * Check if the {@link android.view.View} this element represents matches a particular class.
   *
   * @param referenceClassName the name of the class to check against the class of this element.
   * @return true if the {@code android.view.View} this element represents is an instance of the
   *     class whose name is {@code referenceClassName}. False if it does not.
   */
  public boolean checkInstanceOf(String referenceClassName) {
    AccessibilityHierarchy hierarchy = getWindow().getAccessibilityHierarchy();
    Integer id = hierarchy.getViewElementClassNames().getIdentifierForClassName(referenceClassName);
    if (id == null) {
      return false;
    }
    return superclassViews.contains(id);
  }

  /**
   * Check if the {@link android.view.View} this element represents matches one of the classes.
   *
   * @param referenceClassNameList the list of names of classes to check against the class of this
   *     element.
   * @return true if the {@code android.view.View} this element represents is an instance of at
   *     least one of the class names in {@code referenceClassNameList}. False if it does not.
   */
  public boolean checkInstanceOfAny(List<String> referenceClassNameList) {
    for (String referenceClassName : referenceClassNameList) {
      if (checkInstanceOf(referenceClassName)) {
        return true;
      }
    }
    return false;
  }

  /**
   * @return This view's content description, or {@code null} if one is not present
   * @see android.view.accessibility.AccessibilityNodeInfo#getContentDescription()
   * @see android.view.View#getContentDescription()
   */
  public @Nullable SpannableString getContentDescription() {
    return contentDescription;
  }

  /**
   * Indicates whether the element is important for accessibility and would be reported to
   * accessibility services.
   *
   * @see android.view.View#isImportantForAccessibility()
   */
  public boolean isImportantForAccessibility() {
    return importantForAccessibility;
  }

  /**
   * @return This view's text content, or {@code null} if none is present
   * @see android.view.accessibility.AccessibilityNodeInfo#getText()
   * @see android.widget.TextView#getText()
   * @see android.widget.TextView#getHint()
   */
  public @Nullable SpannableString getText() {
    return text;
  }

  /**
   * @return {@link Boolean#TRUE} if the element is visible to the user, {@link Boolean#FALSE} if
   *     not, or {@code null} if this cannot be determined.
   * @see android.view.accessibility.AccessibilityNodeInfo#isVisibleToUser()
   */
  public @Nullable Boolean isVisibleToUser() {
    return visibleToUser;
  }

  /**
   * Indicates whether this view reports that it reacts to click events or not.
   *
   * @see android.view.accessibility.AccessibilityNodeInfo#isClickable()
   * @see android.view.View#isClickable()
   */
  public boolean isClickable() {
    return clickable;
  }

  /**
   * Indicates whether this view reports that it reacts to long click events or not.
   *
   * @see android.view.accessibility.AccessibilityNodeInfo#isLongClickable()
   * @see android.view.View#isLongClickable()
   */
  public boolean isLongClickable() {
    return longClickable;
  }

  /**
   * Indicates whether this view reports that it is currently able to take focus.
   *
   * @see android.view.accessibility.AccessibilityNodeInfo#isFocusable()
   * @see android.view.View#isFocusable()
   */
  public boolean isFocusable() {
    return focusable;
  }

  /**
   * @return {@link Boolean#TRUE} if the element is editable, {@link Boolean#FALSE} if not, or
   *     {@code null} if this cannot be determined.
   */
  public @Nullable Boolean isEditable() {
    return editable;
  }

  /**
   * @return {@link Boolean#TRUE} if the element is potentially scrollable or indicated as a
   *     scrollable container, {@link Boolean#FALSE} if not, or {@code null} if this cannot be
   *     determined. Scrollable in this context refers only to a element's potential for being
   *     scrolled, and doesn't indicate if the container holds enough wrapped content to scroll. To
   *     determine if an element is actually scrollable based on contents use {@link
   *     #canScrollForward} or {@link #canScrollBackward}.
   */
  public @Nullable Boolean isScrollable() {
    return scrollable;
  }

  /**
   * @return {@link Boolean#TRUE} if the element is scrollable in the "forward" direction, typically
   *         meaning either vertically downward or horizontally to the right (in left-to-right
   *         locales), {@link Boolean#FALSE} if not, or {@code null if this cannot be determined.
   */
  public @Nullable Boolean canScrollForward() {
    return canScrollForward;
  }

  /**
   * @return {@link Boolean#TRUE} if the element is scrollable in the "backward" direction,
   *         typically meaning either vertically downward or horizontally to the right (in
   *         left-to-right locales), {@link Boolean#FALSE} if not, or {@code null if this cannot be
   *         determined.
   */
  public @Nullable Boolean canScrollBackward() {
    return canScrollBackward;
  }

  /**
   * @return {@link Boolean#TRUE} if the element is checkable, {@link Boolean#FALSE} if not, or
   *     {@code null} if this cannot be determined.
   */
  public @Nullable Boolean isCheckable() {
    return checkable;
  }

  /**
   * @return {@link Boolean#TRUE} if the element is checked, {@link Boolean#FALSE} if not, or {@code
   *     null} if this cannot be determined.
   */
  public @Nullable Boolean isChecked() {
    return checked;
  }

  /**
   * Returns {@link Boolean#TRUE} if the element has a {@link android.view.TouchDelegate}, {@link
   * Boolean#FALSE} if not, or {@code null} if this cannot be determined. This indicates only that
   * this element may be responsible for delegating its touches to another element.
   */
  public @Nullable Boolean hasTouchDelegate() {
    return hasTouchDelegate;
  }

  /**
   * Returns a list of the touchable bounds of this element if rectangular {@link
   * android.view.TouchDelegate}s are used to modify this delegatee's hit region from another
   * delegating element.
   *
   * <p>NOTE: This is distinct from {@link #hasTouchDelegate()}, that indicates whether the element
   * may be a delegator of touches.
   */
  public List<Rect> getTouchDelegateBounds() {
    return touchDelegateBounds;
  }

  /** Returns a list of character locations in screen coordinates. */
  public List<Rect> getTextCharacterLocations() {
    return textCharacterLocations;
  }

  /**
   * Retrieves the visible bounds of this element in absolute screen coordinates.
   *
   * <p>NOTE: This method provides dimensions that may be reduced in size due to clipping effects
   * from parent elements. To determine nonclipped dimensions, consider using {@link
   * #getNonclippedHeight()} and {@link #getNonclippedWidth}.
   *
   * @return the view's bounds, or {@link Rect#EMPTY} if the view's bounds are unavailable, such as
   *     when it is positioned off-screen.
   * @see android.view.accessibility.AccessibilityNodeInfo#getBoundsInScreen(android.graphics.Rect)
   * @see android.view.View#getGlobalVisibleRect(android.graphics.Rect)
   */
  public Rect getBoundsInScreen() {
    return (boundsInScreen != null) ? boundsInScreen : Rect.EMPTY;
  }

  /**
   * @return the height of this element (in raw pixels) not taking into account clipping effects
   *     applied by parent elements.
   * @see android.view.View#getHeight()
   */
  public @Nullable Integer getNonclippedHeight() {
    return nonclippedHeight;
  }

  /**
   * @return the width of this element (in raw pixels) not taking into account clipping effects
   *     applied by parent elements.
   * @see android.view.View#getWidth()
   */
  public @Nullable Integer getNonclippedWidth() {
    return nonclippedWidth;
  }

  /**
   * @return The size (in raw pixels) of the default text appearing in this view, or {@code null} if
   *     this cannot be determined
   * @see android.widget.TextView#getTextSize()
   */
  public @Nullable Float getTextSize() {
    return textSize;
  }

  /**
   * @return The color of the default text appearing in this view, or {@code null} if this cannot be
   *     determined
   * @see android.widget.TextView#getCurrentTextColor()
   */
  public @Nullable Integer getTextColor() {
    return textColor;
  }

  /**
   * @return The color of this View's background drawable, or {@code null} if the view does not have
   *     a {@link android.graphics.drawable.ColorDrawable} background
   * @see android.view.View#getBackground()
   * @see android.graphics.drawable.ColorDrawable#getColor()
   */
  public @Nullable Integer getBackgroundDrawableColor() {
    return backgroundDrawableColor;
  }

  /**
   * Returns The style attributes of the {@link android.graphics.Typeface} of the default text
   * appearing in this view, or @code null} if this cannot be determined.
   *
   * @see android.widget.TextView#getTypeface()
   * @see android.graphics.Typeface#getStyle()
   * @see android.graphics.Typeface#NORMAL
   * @see android.graphics.Typeface#BOLD
   * @see android.graphics.Typeface#ITALIC
   * @see android.graphics.Typeface#BOLD_ITALIC
   */
  @Pure
  public @Nullable Integer getTypefaceStyle() {
    return typefaceStyle;
  }

  /**
   * Returns the enabled status for this view.
   *
   * @see android.view.View#isEnabled()
   * @see android.view.accessibility.AccessibilityNodeInfo#isEnabled()
   */
  public boolean isEnabled() {
    return enabled;
  }

  /**
   * @return The class name as reported to accessibility services, or {@code null} if this cannot be
   *     determined
   *     <p>NOTE: Unavailable for instances originally created from a {@link android.view.View}
   * @see android.view.accessibility.AccessibilityNodeInfo#getClassName()
   */
  public @Nullable CharSequence getAccessibilityClassName() {
    return accessibilityClassName;
  }

  /**
   * Returns the {@link ViewHierarchyElement} which acts as a label for this element, or {@code
   * null} if this element is not labeled by another
   *
   * @see android.view.accessibility.AccessibilityNodeInfo#getLabeledBy()
   * @see android.view.accessibility.AccessibilityNodeInfo#getLabelFor()
   * @see android.view.View#getLabelFor()
   */
  @Pure
  public @Nullable ViewHierarchyElement getLabeledBy() {
    return getViewHierarchyElementById(labeledById);
  }

  /**
   * @return a view before which this one is visited in accessibility traversal
   * @see android.view.accessibility.AccessibilityNodeInfo#getTraversalBefore()
   * @see android.view.View#getAccessibilityTraversalBefore()
   */
  public @Nullable ViewHierarchyElement getAccessibilityTraversalBefore() {
    return getViewHierarchyElementById(accessibilityTraversalBeforeId);
  }

  /**
   * @return a view after which this one is visited in accessibility traversal
   * @see android.view.accessibility.AccessibilityNodeInfo#getTraversalAfter()
   * @see android.view.View#getAccessibilityTraversalAfter()
   */
  public @Nullable ViewHierarchyElement getAccessibilityTraversalAfter() {
    return getViewHierarchyElementById(accessibilityTraversalAfterId);
  }

  /**
   * Returns this element's drawing order within its parent, or {@code null} if the element's
   * drawing order is not available.
   *
   * @see android.view.accessibility.AccessibilityNodeInfo#getDrawingOrder()
   */
  public @Nullable Integer getDrawingOrder() {
    return drawingOrder;
  }

  /**
   * Returns how the element wants to be laid out in its parent, or {@code null} if the info is not
   * available.
   *
   * @see android.view.ViewGroup.LayoutParams
   */
  public @Nullable LayoutParams getLayoutParams() {
    return layoutParams;
  }

  /**
   * Returns the hint that is displayed when this view is a TextView and the text of the TextView is
   * empty, or {@code null} if the view is not a TextView, or if the info in not available.
   *
   * @see android.widget.TextView#getHint()
   */
  public @Nullable SpannableString getHintText() {
    return hintText;
  }

  /**
   * Returns the current color selected to paint the hint text, or {@code null} if this cannot be
   * determined.
   *
   * @see android.widget.TextView#getCurrentHintTextColor()
   */
  public @Nullable Integer getHintTextColor() {
    return hintTextColor;
  }

  /**
   * Returns {@code true} if this element {@link #isVisibleToUser} and its visible bounds are
   * adjacent to the scrollable edge of a scrollable container. This would indicate that the element
   * may be partially obscured by the container.
   */
  public boolean isAgainstScrollableEdge() {
    return TRUE.equals(isVisibleToUser()) && isAgaistScrollableEdgeOfAncestor(this);
  }

  private boolean isAgaistScrollableEdgeOfAncestor(ViewHierarchyElement view) {
    ViewHierarchyElement ancestor = view.getParentView();
    if (ancestor == null) {
      return false;
    }

    // See if this element is at the top or left edge of a scrollable container that can be scrolled
    // backward.
    if (TRUE.equals(ancestor.canScrollBackward())) {
      Rect scrollableBounds = ancestor.getBoundsInScreen();
      Rect descendantBounds = this.getBoundsInScreen();

      if ((descendantBounds.getTop() <= scrollableBounds.getTop())
          || (descendantBounds.getLeft() <= scrollableBounds.getLeft())) {
        return true;
      }
    }

    // See if this element is at the bottom or right edge of a scrollable container that can be
    // scrolled forward.
    if (TRUE.equals(ancestor.canScrollForward())) {
      Rect scrollableBounds = ancestor.getBoundsInScreen();
      Rect descendantBounds = this.getBoundsInScreen();

      if ((descendantBounds.getBottom() >= scrollableBounds.getBottom())
          || (descendantBounds.getRight() >= scrollableBounds.getRight())) {
        return true;
      }
    }

    // Recurse for ancestors.
    return isAgaistScrollableEdgeOfAncestor(ancestor);
  }

  @Override
  public int hashCode() {
    return getId();
  }

  @Override
  public boolean equals(@Nullable Object object) {
    if (object == this) {
      return true;
    }
    if (!(object instanceof ViewHierarchyElement)) {
      return false;
    }

    ViewHierarchyElement element = (ViewHierarchyElement) object;
    if (!propertiesEquals((ViewHierarchyElement) object)) {
      return false;
    }
    for (int i = 0; i < getChildViewCount(); i++) {
      if (!getChildView(i).equals(element.getChildView(i))) {
        return false;
      }
    }
    return true;
  }

  /**
   * Returns a list of identifiers that represents all the superclasses of the corresponding view
   * element.
   */
  List<Integer> getSuperclassList() {
    return superclassViews;
  }

  /** Add a view class id to superclass list. */
  void addIdToSuperclassViewList(int id) {
    this.superclassViews.add(id);
  }

  /** Returns a list of actions exposed by this view element. */
  ImmutableList<ViewHierarchyAction> getActionList() {
    return actionList;
  }

  ViewHierarchyElementProto toProto() {
    ViewHierarchyElementProto.Builder builder = ViewHierarchyElementProto.newBuilder();
    // Bookkeeping
    builder.setId(id);
    if (parentId != null) {
      builder.setParentId(parentId);
    }
    if ((childIds != null) && !childIds.isEmpty()) {
      builder.addAllChildIds(childIds);
    }

    // View properties
    if (!TextUtils.isEmpty(packageName)) {
      builder.setPackageName(packageName.toString());
    }
    if (!TextUtils.isEmpty(className)) {
      builder.setClassName(className.toString());
    }
    if (!TextUtils.isEmpty(resourceName)) {
      builder.setResourceName(resourceName);
    }
    if (!TextUtils.isEmpty(contentDescription)) {
      builder.setContentDescription(contentDescription.toProto());
    }
    if (!TextUtils.isEmpty(text)) {
      builder.setText(text.toProto());
    }
    builder.setImportantForAccessibility(importantForAccessibility);
    if (visibleToUser != null) {
      builder.setVisibleToUser(visibleToUser);
    }
    builder.setClickable(clickable).setLongClickable(longClickable).setFocusable(focusable);
    if (editable != null) {
      builder.setEditable(editable);
    }
    if (scrollable != null) {
      builder.setScrollable(scrollable);
    }
    if (canScrollForward != null) {
      builder.setCanScrollForward(canScrollForward);
    }
    if (canScrollBackward != null) {
      builder.setCanScrollBackward(canScrollBackward);
    }
    if (checkable != null) {
      builder.setCheckable(checkable);
    }
    if (checked != null) {
      builder.setChecked(checked);
    }
    if (hasTouchDelegate != null) {
      builder.setHasTouchDelegate(hasTouchDelegate);
    }
    for (Rect bounds : touchDelegateBounds) {
      builder.addTouchDelegateBounds(bounds.toProto());
    }
    if (boundsInScreen != null) {
      builder.setBoundsInScreen(boundsInScreen.toProto());
    }
    if (nonclippedHeight != null) {
      builder.setNonclippedHeight(nonclippedHeight);
    }
    if (nonclippedWidth != null) {
      builder.setNonclippedWidth(nonclippedWidth);
    }
    if (textSize != null) {
      builder.setTextSize(textSize);
    }
    if (textColor != null) {
      builder.setTextColor(textColor);
    }
    if (backgroundDrawableColor != null) {
      builder.setBackgroundDrawableColor(backgroundDrawableColor);
    }
    if (typefaceStyle != null) {
      builder.setTypefaceStyle(typefaceStyle);
    }
    builder.setEnabled(enabled);
    if (labeledById != null) {
      builder.setLabeledById(labeledById);
    }
    if (accessibilityClassName != null) {
      builder.setAccessibilityClassName(accessibilityClassName.toString());
    }
    if (accessibilityTraversalBeforeId != null) {
      builder.setAccessibilityTraversalBeforeId(accessibilityTraversalBeforeId);
    }
    if (accessibilityTraversalAfterId != null) {
      builder.setAccessibilityTraversalAfterId(accessibilityTraversalAfterId);
    }
    if (drawingOrder != null) {
      builder.setDrawingOrder(drawingOrder);
    }
    if (layoutParams != null) {
      builder.setLayoutParams(layoutParams.toProto());
    }
    if (!TextUtils.isEmpty(hintText)) {
      builder.setHintText(hintText.toProto());
    }
    if (hintTextColor != null) {
      builder.setHintTextColor(hintTextColor);
    }

    builder.addAllSuperclasses(superclassViews);
    for (ViewHierarchyAction action : actionList) {
      builder.addActions(action.toProto());
    }
    for (Rect rect : textCharacterLocations) {
      builder.addTextCharacterLocations(rect.toProto());
    }
    return builder.build();
  }

  /** Set the containing {@link WindowHierarchyElement} of this view. */
  void setWindow(WindowHierarchyElement window) {
    this.windowElement = window;
  }

  /**
   * @param child The child {@link ViewHierarchyElement} to add as a child of this view
   */
  void addChild(ViewHierarchyElement child) {
    if (childIds == null) {
      childIds = new ArrayList<>();
    }
    childIds.add(child.id);
  }

  /**
   * Denotes that {@code labelingElement} acts as a label for this element
   *
   * @param labelingElement The element that labels this element, or {@code null} if this element is
   *        not labeled by another
   */
  void setLabeledBy(ViewHierarchyElement labelingElement) {
    labeledById = (labelingElement != null) ? labelingElement.getCondensedUniqueId() : null;
  }

  /**
   * Sets a view before which this one is visited in accessibility traversal. A screen-reader must
   * visit the content of this view before the content of the one it precedes.
   */
  void setAccessibilityTraversalBefore(ViewHierarchyElement element) {
    accessibilityTraversalBeforeId = element.getCondensedUniqueId();
  }

  /**
   * Sets a view after which this one is visited in accessibility traversal. A screen-reader must
   * visit the content of the other view before the content of this one.
   */
  void setAccessibilityTraversalAfter(ViewHierarchyElement element) {
    accessibilityTraversalAfterId = element.getCondensedUniqueId();
  }

  /**
   * Adds a Rect to this element's list of {@code TouchDelegate} hit-Rect bounds, indicating it may
   * receive touches via another element within these bounds.
   *
   * <p>NOTE: This should be invoked only on instances from an AccessibilityHierarchy built from
   * {@link android.view.accessibility.AccessibilityNodeInfo}. Hierarchies built from other
   * structures expect this field to be Immutable after construction.
   */
  void addTouchDelegateBounds(Rect bounds) {
    touchDelegateBounds.add(bounds);
  }

  private @Nullable ViewHierarchyElement getViewHierarchyElementById(@Nullable Long id) {
    return (id != null) ? getWindow().getAccessibilityHierarchy().getViewById(id) : null;
  }

  private boolean propertiesEquals(ViewHierarchyElement element) {
    return (getCondensedUniqueId() == element.getCondensedUniqueId())
        && (getChildViewCount() == element.getChildViewCount())
        && TextUtils.equals(getPackageName(), element.getPackageName())
        && TextUtils.equals(getClassName(), element.getClassName())
        && TextUtils.equals(getResourceName(), element.getResourceName())
        && isImportantForAccessibility() == element.isImportantForAccessibility()
        && TextUtils.equals(getContentDescription(), element.getContentDescription())
        && TextUtils.equals(getText(), element.getText())
        && Objects.equals(getTextColor(), element.getTextColor())
        && Objects.equals(getBackgroundDrawableColor(), element.getBackgroundDrawableColor())
        && Objects.equals(isVisibleToUser(), element.isVisibleToUser())
        && (isClickable() == element.isClickable())
        && (isLongClickable() == element.isLongClickable())
        && (isFocusable() == element.isFocusable())
        && Objects.equals(isEditable(), element.isEditable())
        && Objects.equals(isScrollable(), element.isScrollable())
        && Objects.equals(canScrollForward(), element.canScrollForward())
        && Objects.equals(canScrollBackward(), element.canScrollBackward())
        && Objects.equals(isCheckable(), element.isCheckable())
        && Objects.equals(isChecked(), element.isChecked())
        && Objects.equals(hasTouchDelegate(), element.hasTouchDelegate())
        && Objects.equals(getTouchDelegateBounds(), element.getTouchDelegateBounds())
        && Objects.equals(getBoundsInScreen(), element.getBoundsInScreen())
        && Objects.equals(getNonclippedWidth(), element.getNonclippedWidth())
        && Objects.equals(getNonclippedHeight(), element.getNonclippedHeight())
        && Objects.equals(getTextSize(), element.getTextSize())
        && Objects.equals(getTypefaceStyle(), element.getTypefaceStyle())
        && (isEnabled() == element.isEnabled())
        && condensedUniqueIdEquals(getLabeledBy(), element.getLabeledBy())
        && TextUtils.equals(getAccessibilityClassName(), element.getAccessibilityClassName())
        && condensedUniqueIdEquals(
            getAccessibilityTraversalAfter(), element.getAccessibilityTraversalAfter())
        && condensedUniqueIdEquals(
            getAccessibilityTraversalBefore(), element.getAccessibilityTraversalBefore())
        && Objects.equals(getDrawingOrder(), element.getDrawingOrder())
        && Objects.equals(getLayoutParams(), element.getLayoutParams())
        && TextUtils.equals(getHintText(), element.getHintText())
        && Objects.equals(getHintTextColor(), element.getHintTextColor());
  }

  private static boolean condensedUniqueIdEquals(
      @Nullable ViewHierarchyElement ve1, @Nullable ViewHierarchyElement ve2) {
    return (ve1 == null)
        ? (ve2 == null)
        : ((ve2 != null) && (ve1.getCondensedUniqueId() == ve2.getCondensedUniqueId()));
  }
}
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheck;
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityHierarchyCheckResult;
import com.google.android.apps.common.testing.accessibility.framework.Parameters;
import com.google.android.apps.common.testing.accessibility.framework.checks.EditableContentDescCheck;
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.uielement.AccessibilityHierarchy;
import com.google.android.apps.common.testing.accessibility.framework.uielement.ViewHierarchyElement;
import com.google.common.annotations.Beta;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.checkerframework.checker.nullness.qual.Nullable;
package com.google.android.apps.common.testing.accessibility.framework.suggestions;


/**
 * Provides an immutable list of {@link FixSuggestion} for a {@link
 * AccessibilityHierarchyCheckResult}.
 */
@Beta
public final class FixSuggestionPreset {

  private static final ImmutableMap<
          Class<? extends AccessibilityHierarchyCheck>, FixSuggestionsProvider>
      CHECK_CLASS_TO_FIX_SUGGESTION_PROVIDER =
          ImmutableMap.of(
              EditableContentDescCheck.class,