public static boolean isPotentiallyObscured(ViewHierarchyElement viewHierarchyElement) {
  return isIntersectedByOverlayWindow(viewHierarchyElement)
      || isIntersectedByOverlayView(viewHierarchyElement);
} public enum AccessibilityCheckResultType {
  /** Clearly an accessibility bug, for example no speakable text on a clicked button */
  ERROR(ResultTypeProto.ERROR),
  /**
   * Potentially an accessibility bug, for example finding another view with the same speakable
   * text as a clicked view
   */
  WARNING(ResultTypeProto.WARNING),
  /**
   * Information that may be helpful when evaluating accessibility, for example a listing of all
   * speakable text in a view hierarchy in the traversal order used by an accessibility service.
   */
  INFO(ResultTypeProto.INFO),
  /**
   * Indication that a potential issue was identified, but it was resolved as not an accessibility
   * problem.
   */
  RESOLVED(ResultTypeProto.RESOLVED),
  /** A signal that the check was not run at all (ex. because the API level was too low) */
  NOT_RUN(ResultTypeProto.NOT_RUN),
  /**
   * A result that has been explicitly suppressed from throwing any Exceptions, used to allow for
   * known issues.
   */
  SUPPRESSED(ResultTypeProto.SUPPRESSED);

  private static final Map<Integer, AccessibilityCheckResultType> PROTO_NUMBER_MAP =
      new HashMap<>();

  static {
    for (AccessibilityCheckResultType type : values()) {
      PROTO_NUMBER_MAP.put(type.protoNumber, type);
    }
  }

  final int protoNumber;

  private AccessibilityCheckResultType(ResultTypeProto proto) {
    this.protoNumber = proto.getNumber();
  }

  public static AccessibilityCheckResultType fromProto(ResultTypeProto proto) {
    AccessibilityCheckResultType type = PROTO_NUMBER_MAP.get(proto.getNumber());
    checkArgument(
        (type != null),
        "Failed to create AccessibilityCheckResultType from proto with unknown value: %s",
        proto.getNumber());
    return checkNotNull(type);
  }

  public ResultTypeProto toProto() {
    return ResultTypeProto.forNumber(protoNumber);
  }
} 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 AccessibilityHierarchyCheckResultWithImage extends AccessibilityHierarchyCheckResult {

  private final Image viewImage;

  /**
   * @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 viewImage An image of the visible region of the view that is the subject of the result
   */
  public AccessibilityHierarchyCheckResultWithImage(
      Class<? extends AccessibilityHierarchyCheck> checkClass,
      AccessibilityCheckResultType type,
      ViewHierarchyElement element,
      int resultId,
      ResultMetadata metadata,
      Image viewImage) {
    super(checkClass, type, element, resultId, metadata);
    this.viewImage = viewImage;
  }

  @Override
  public AccessibilityHierarchyCheckResultWithImage getSuppressedResultCopy() {
    return new AccessibilityHierarchyCheckResultWithImage(
        getSourceCheckClass().asSubclass(AccessibilityHierarchyCheck.class),
        AccessibilityCheckResultType.SUPPRESSED,
        checkNotNull(getElement()),
        getResultId(),
        checkNotNull(getMetadata()),
        viewImage);
  }

  /**
   * Returns an image of the visible region of the View that is the subject of the result, if an
   * image was given.
   */
  public Image getViewImage() {
    return viewImage;
  }
} public class HashMapResultMetadata implements ResultMetadata {

  protected final Map<String, TypedValue> map;

  public HashMapResultMetadata() {
    map = new HashMap<>();
  }

  /**
   * Creates an instance containing a copy of the mappings from the given instance.
   *
   * @param metadata The instance from which to copy mappings
   */
  protected HashMapResultMetadata(HashMapResultMetadata metadata) {
    map = new HashMap<>(metadata.map);
  }

  @Override
  public HashMapResultMetadata clone() {
    return new HashMapResultMetadata(this);
  }

  @Override
  public boolean getBoolean(String key) {
    TypedValue tv = map.get(key);
    if (tv == null) {
      throw invalidKeyException(key);
    } else if (TypedValue.Type.BOOLEAN != tv.type) {
      throw invalidTypeException(key, TypedValue.Type.BOOLEAN, tv.type);
    }

    return (boolean) tv.value;
  }

  @Override
  public boolean getBoolean(String key, boolean defaultValue) {
    TypedValue tv = map.get(key);
    if (tv == null) {
      return defaultValue;
    } else if (TypedValue.Type.BOOLEAN != tv.type) {
      throw invalidTypeException(key, TypedValue.Type.BOOLEAN, tv.type);
    }

    return (boolean) tv.value;
  }

  @Override
  public void putBoolean(String key, boolean value) {
    map.put(key, new TypedValue(TypedValue.Type.BOOLEAN, value));
  }

  @Override
  public byte getByte(String key) {
    TypedValue tv = map.get(key);
    if (tv == null) {
      throw invalidKeyException(key);
    } else if (TypedValue.Type.BYTE != tv.type) {
      throw invalidTypeException(key, TypedValue.Type.BYTE, tv.type);
    }

    return (byte) tv.value;
  }

  @Override
  public byte getByte(String key, byte defaultValue) {
    TypedValue tv = map.get(key);
    if (tv == null) {
      return defaultValue;
    } else if (TypedValue.Type.BYTE != tv.type) {
      throw invalidTypeException(key, TypedValue.Type.BYTE, tv.type);
    }

    return (byte) tv.value;
  }

  @Override
  public void putByte(String key, byte value) {
    map.put(key, new TypedValue(TypedValue.Type.BYTE, value));
  }

  @Override
  public short getShort(String key) {
    TypedValue tv = map.get(key);
    if (tv == null) {
      throw invalidKeyException(key);
    } else if (TypedValue.Type.SHORT != tv.type) {
      throw invalidTypeException(key, TypedValue.Type.SHORT, tv.type);
    }

    return (short) tv.value;
  }

  @Override
  public short getShort(String key, short defaultValue) {
    TypedValue tv = map.get(key);
    if (tv == null) {
      return defaultValue;
    } else if (TypedValue.Type.SHORT != tv.type) {
      throw invalidTypeException(key, TypedValue.Type.SHORT, tv.type);
    }

    return (short) tv.value;
  }

  @Override
  public void putShort(String key, short value) {
    map.put(key, new TypedValue(TypedValue.Type.SHORT, value));
  }

  @Override
  public char getChar(String key) {
    TypedValue tv = map.get(key);
    if (tv == null) {
      throw invalidKeyException(key);
    } else if (TypedValue.Type.CHAR != tv.type) {
      throw invalidTypeException(key, TypedValue.Type.CHAR, tv.type);
    }

    return (char) tv.value;
  }

  @Override
  public char getChar(String key, char defaultValue) {
    TypedValue tv = map.get(key);
    if (tv == null) {
      return defaultValue;
    } else if (TypedValue.Type.CHAR != tv.type) {
      throw invalidTypeException(key, TypedValue.Type.CHAR, tv.type);
    }

    return (char) tv.value;
  }

  @Override
  public void putChar(String key, char value) {
    map.put(key, new TypedValue(TypedValue.Type.CHAR, value));
  }

  @Override
  public int getInt(String key) {
    TypedValue tv = map.get(key);
    if (tv == null) {
      throw invalidKeyException(key);
    } else if (TypedValue.Type.INT != tv.type) {
      throw invalidTypeException(key, TypedValue.Type.INT, tv.type);
    }

    return (int) tv.value;
  }

  @Override
  public int getInt(String key, int defaultValue) {
    TypedValue tv = map.get(key);
    if (tv == null) {
      return defaultValue;
    } else if (TypedValue.Type.INT != tv.type) {
      throw invalidTypeException(key, TypedValue.Type.INT, tv.type);
    }

    return (int) tv.value;
  }

  @Override
  public void putInt(String key, int value) {
    map.put(key, new TypedValue(TypedValue.Type.INT, value));
  }

  @Override
  public float getFloat(String key) {
    TypedValue tv = map.get(key);
    if (tv == null) {
      throw invalidKeyException(key);
    } else if (TypedValue.Type.FLOAT != tv.type) {
      throw invalidTypeException(key, TypedValue.Type.FLOAT, tv.type);
    }

    return (float) tv.value;
  }

  @Override
  public float getFloat(String key, float defaultValue) {
    TypedValue tv = map.get(key);
    if (tv == null) {
      return defaultValue;
    } else if (TypedValue.Type.FLOAT != tv.type) {
      throw invalidTypeException(key, TypedValue.Type.FLOAT, tv.type);
    }

    return (float) tv.value;
  }

  @Override
  public void putFloat(String key, float value) {
    map.put(key, new TypedValue(TypedValue.Type.FLOAT, value));
  }

  @Override
  public long getLong(String key) {
    TypedValue tv = map.get(key);
    if (tv == null) {
      throw invalidKeyException(key);
    } else if (TypedValue.Type.LONG != tv.type) {
      throw invalidTypeException(key, TypedValue.Type.LONG, tv.type);
    }

    return (long) tv.value;
  }

  @Override
  public long getLong(String key, long defaultValue) {
    TypedValue tv = map.get(key);
    if (tv == null) {
      return defaultValue;
    } else if (TypedValue.Type.LONG != tv.type) {
      throw invalidTypeException(key, TypedValue.Type.LONG, tv.type);
    }

    return (long) tv.value;
  }

  @Override
  public void putLong(String key, long value) {
    map.put(key, new TypedValue(TypedValue.Type.LONG, value));
  }

  @Override
  public double getDouble(String key) {
    TypedValue tv = map.get(key);
    if (tv == null) {
      throw invalidKeyException(key);
    } else if (TypedValue.Type.DOUBLE != tv.type) {
      throw invalidTypeException(key, TypedValue.Type.DOUBLE, tv.type);
    }

    return (double) tv.value;
  }

  @Override
  public double getDouble(String key, double defaultValue) {
    TypedValue tv = map.get(key);
    if (tv == null) {
      return defaultValue;
    } else if (TypedValue.Type.DOUBLE != tv.type) {
      throw invalidTypeException(key, TypedValue.Type.DOUBLE, tv.type);
    }

    return (double) tv.value;
  }

  @Override
  public void putDouble(String key, double value) {
    map.put(key, new TypedValue(TypedValue.Type.DOUBLE, value));
  }

  @Override
  public String getString(String key) {
    TypedValue tv = map.get(key);
    if (tv == null) {
      throw invalidKeyException(key);
    } else if (TypedValue.Type.STRING != tv.type) {
      throw invalidTypeException(key, TypedValue.Type.STRING, tv.type);
    }

    return (String) tv.value;
  }

  @Override
  public String getString(String key, String defaultValue) {
    TypedValue tv = map.get(key);
    if (tv == null) {
      return defaultValue;
    } else if (TypedValue.Type.STRING != tv.type) {
      throw invalidTypeException(key, TypedValue.Type.STRING, tv.type);
    }

    return (String) tv.value;
  }

  @Override
  public void putString(String key, String value) {
    map.put(key, new TypedValue(TypedValue.Type.STRING, value));
  }

  @SuppressWarnings("unchecked") // Safe specification in TypedValue
  @Override
  public ImmutableList<String> getStringList(String key) {
    TypedValue tv = map.get(key);
    if (tv == null) {
      throw invalidKeyException(key);
    } else if (TypedValue.Type.STRING_LIST != tv.type) {
      throw invalidTypeException(key, TypedValue.Type.STRING_LIST, tv.type);
    }

    return (ImmutableList<String>) tv.value;
  }

  @SuppressWarnings("unchecked") // Safe specification in TypedValue
  @Override
  public ImmutableList<String> getStringList(String key, ImmutableList<String> defaultValue) {
    TypedValue tv = map.get(key);
    if (tv == null) {
      return defaultValue;
    } else if (TypedValue.Type.STRING_LIST != tv.type) {
      throw invalidTypeException(key, TypedValue.Type.STRING_LIST, tv.type);
    }

    return (ImmutableList<String>) tv.value;
  }

  @Override
  public void putStringList(String key, List<String> value) {
    checkArgument(!value.isEmpty());
    map.put(key, new TypedValue(TypedValue.Type.STRING_LIST, ImmutableList.copyOf(value)));
  }

  @Override
  public ImmutableList<Integer> getIntegerList(String key) {
    return getValue(key, TypedValue.Type.INTEGER_LIST, null);
  }

  @Override
  public ImmutableList<Integer> getIntegerList(String key, ImmutableList<Integer> defaultValue) {
    return getValue(key, TypedValue.Type.INTEGER_LIST, defaultValue);
  }

  @Override
  public void putIntegerList(String key, List<Integer> value) {
    checkArgument(!value.isEmpty());
    map.put(key, new TypedValue(TypedValue.Type.INTEGER_LIST, ImmutableList.copyOf(value)));
  }

  @Override
  public boolean containsKey(String key) {
    return map.containsKey(key);
  }

  @Override
  public boolean isEmpty() {
    return map.isEmpty();
  }

  public static HashMapResultMetadata fromProto(MetadataProto proto) {
    HashMapResultMetadata metadata = new HashMapResultMetadata();
    for (Entry<String, TypedValueProto> entry : proto.getMetadataMapMap().entrySet()) {
      metadata.map.put(entry.getKey(), new TypedValue(entry.getValue()));
    }

    return metadata;
  }

  public MetadataProto toProto() {
    MetadataProto.Builder builder = MetadataProto.newBuilder();
    for (Entry<String, TypedValue> entry : map.entrySet()) {
      builder.putMetadataMap(entry.getKey(), entry.getValue().toProto());
    }

    return builder.build();
  }

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

    HashMapResultMetadata metadata = (HashMapResultMetadata) o;

    return map.equals(metadata.map);
  }

  @Override
  public int hashCode() {
    return map.hashCode();
  }

  // For debugging
  @Override
  public String toString() {
    return new TreeMap<String, TypedValue>(map).toString();
  }

  @SuppressWarnings("unchecked") // Safe specification in TypedValue
  private <T> T getValue(String key, TypedValue.Type type, @Nullable T defaultValue) {
    TypedValue tv = map.get(key);
    if (tv == null) {
      if (defaultValue != null) {
        return defaultValue;
      } else {
        throw invalidKeyException(key);
      }
    } else if (type != tv.type) {
      throw invalidTypeException(key, type, tv.type);
    }

    return (T) tv.value;
  }

  private static NoSuchElementException invalidKeyException(String key) {
    return new NoSuchElementException(
        "No HashMapResultMetadata element found for key '" + key + "'.");
  }

  private static ClassCastException invalidTypeException(
      String key, TypedValue.Type requestedType, TypedValue.Type foundType) {
    return new ClassCastException(
        "Invalid type '"
            + requestedType.name()
            + "' requested from HashMapResultMetadata for key '"
            + key
            + "'.  Found type '"
            + foundType.name()
            + "' instead.");
  }

  /**
   * Used internally to track the value types of entries in metadata's {@code map}. Explicitly
   * storing types in this fashion allows for simpler serialization and deserialization.
   */
  protected static class TypedValue {

    /** Mapping between in memory and serializable types. */
    private static final EnumBiMap<Type, TypeProto> TYPE_MAP =
        EnumBiMap.create(
            ImmutableMap.<Type, TypeProto>builder()
                .put(Type.BOOLEAN, TypeProto.BOOLEAN)
                .put(Type.BYTE, TypeProto.BYTE)
                .put(Type.SHORT, TypeProto.SHORT)
                .put(Type.CHAR, TypeProto.CHAR)
                .put(Type.INT, TypeProto.INT)
                .put(Type.FLOAT, TypeProto.FLOAT)
                .put(Type.LONG, TypeProto.LONG)
                .put(Type.DOUBLE, TypeProto.DOUBLE)
                .put(Type.STRING, TypeProto.STRING)
                .put(Type.STRING_LIST, TypeProto.STRING_LIST)
                .put(Type.INTEGER_LIST, TypeProto.INT_LIST)
                .build());

    /**
     * The supported types for entries within metadata maps
     *
     * <p>CONTRACT: Any Type defined here must have a corresponding {@code TypeProto} in {@link
     * #TYPE_MAP}.
     *
     * <p>CONTRACT: All Types must provide:
     *
     * <ul>
     *   <li>An implementation to serialize its typed data to a protocol buffer format within {@link
     *       TypedValue#toProto()}
     *   <li>An implementation to deserialize its typed data from a protocol buffer format within
     *       {@link TypedValue}'s protocol buffer constructor
     * </ul>
     */
    public enum Type {
      BOOLEAN,
      BYTE,
      SHORT,
      CHAR,
      INT,
      FLOAT,
      LONG,
      DOUBLE,
      STRING,
      STRING_LIST,
      INTEGER_LIST;

      public static Type fromProto(TypeProto proto) {
        return checkNotNull(TYPE_MAP.inverse().get(proto));
      }

      public TypeProto toProto() {
        return checkNotNull(TYPE_MAP.get(this));
      }
    }

    public Type type;
    public Object value;

    public TypedValue(Type type, Object value) {
      this.type = type;
      this.value = value;
    }

    public TypedValue(TypedValueProto proto) {
      type = Type.fromProto(proto.getType());
      switch (type) {
        case BOOLEAN:
          value = proto.getBooleanValue();
          break;
        case BYTE:
          value = proto.getByteValue().asReadOnlyByteBuffer().get();
          break;
        case SHORT:
          value = proto.getShortValue().asReadOnlyByteBuffer().getShort();
          break;
        case CHAR:
          value = proto.getCharValue().asReadOnlyByteBuffer().getChar();
          break;
        case INT:
          value = proto.getIntValue();
          break;
        case FLOAT:
          value = proto.getFloatValue();
          break;
        case LONG:
          value = proto.getLongValue();
          break;
        case DOUBLE:
          value = proto.getDoubleValue();
          break;
        case STRING:
          value = proto.getStringValue();
          break;
        case STRING_LIST:
          value = ImmutableList.copyOf(proto.getStringListValue().getValuesList());
          break;
        case INTEGER_LIST:
          value = ImmutableList.copyOf(proto.getIntListValue().getValuesList());
          break;
      }
    }

    @SuppressWarnings("unchecked") // Safe specification in TypedValue
    public TypedValueProto toProto() {
      TypedValueProto.Builder builder = TypedValueProto.newBuilder();
      builder.setType(type.toProto());
      switch (type) {
        case BOOLEAN:
          builder.setBooleanValue((boolean) value);
          break;
        case BYTE:
          ByteBuffer byteBuffer = ByteBuffer.allocate(1);
          byteBuffer.put((byte) value).flip();
          builder.setByteValue(ByteString.copyFrom(byteBuffer));
          break;
        case SHORT:
          ByteBuffer shortBuffer = ByteBuffer.allocate(2);
          shortBuffer.putShort((short) value).flip();
          builder.setShortValue(ByteString.copyFrom(shortBuffer));
          break;
        case CHAR:
          ByteBuffer charBuffer = ByteBuffer.allocate(2);
          charBuffer.putChar((char) value).flip();
          builder.setCharValue(ByteString.copyFrom(charBuffer));
          break;
        case INT:
          builder.setIntValue((int) value);
          break;
        case FLOAT:
          builder.setFloatValue((float) value);
          break;
        case LONG:
          builder.setLongValue((long) value);
          break;
        case DOUBLE:
          builder.setDoubleValue((double) value);
          break;
        case STRING:
          builder.setStringValue((String) value);
          break;
        case STRING_LIST:
          builder.setStringListValue(
              StringListProto.newBuilder().addAllValues((ImmutableList<String>) value).build());
          break;
        case INTEGER_LIST:
          builder.setIntListValue(
              IntListProto.newBuilder().addAllValues((ImmutableList<Integer>) value).build());
          break;
      }
      return builder.build();
    }

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

      TypedValue that = (TypedValue) o;

      if (type != that.type) {
        return false;
      }
      return value.equals(that.value);
    }

    @Override
    public int hashCode() {
      int result = type.hashCode();
      result = 31 * result + value.hashCode();
      return result;
    }

    // For debugging
    @Override
    public String toString() {
      return String.valueOf(value);
    }
  }
} 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;
  }
} @Beta
public abstract class QuestionHandler {
  /** Returns whether the result has an unanswered {@link Question} */
  public boolean hasQuestion(AccessibilityHierarchyCheckResult result) {
    return getNextQuestion(result) != null;
  }

  /**
   * Returns the next {@link Question} if the {@link AccessibilityHierarchyCheckResult} has one,
   * else returns {@code null}
   */
  public abstract @Nullable Question getNextQuestion(AccessibilityHierarchyCheckResult result);

  /**
   * Returns the string for the phrasing of the passed question
   *
   * @param question the question needing to be phrased
   * @param locale the desired locale of the message
   * @return a human-readable String representing the phrasing of the Question
   */
  public abstract String getQuestionMessage(Question question, Locale locale);

  /**
   * Returns zero to many results to replace the original result based on an answer to a question
   *
   * @param answer the {@link Answer} (which contains its associated {@link Question} and original
   *     {@link AccessibilityHierarchyCheckResult}) that will be used to update the result
   * @return a potentially empty list of new result(s) to replace the original result based on the
   *     information in the original result and the answer list
   */
  public abstract ImmutableList<AccessibilityHierarchyCheckResult> updateResult(Answer answer);

  /**
   * Returns an {@link AccessibilityHierarchyCheckResult} with the information of the original
   * result and the new answer appended to list of answers
   */
  // originalResult is an {@link AccessibilityHierarchyCheckResult} so the sourceCheckClass is
  // guaranteed to be from a {@link AccessibilityHierarchyCheck} subclass
  @SuppressWarnings("unchecked")
  protected static ImmutableList<AccessibilityHierarchyCheckResult> updateResultByAppendingAnswer(
      Answer answer) {
    AccessibilityHierarchyCheckResult originalResult = answer.getQuestion().getOriginalResult();
    ImmutableList<Answer> answers =
        ImmutableList.<Answer>builder().addAll(originalResult.getAnswers()).add(answer).build();
    return ImmutableList.of(
        new AccessibilityHierarchyCheckResult(
            (Class<? extends AccessibilityHierarchyCheck>) originalResult.getSourceCheckClass(),
            originalResult.getType(),
            originalResult.getElement(),
            originalResult.getResultId(),
            originalResult.getMetadata(),
            answers));
  }

  /**
   * Return a {@link Answer} that answers the question of questionId, if it has been answered, else
   * returns {@code null}. Assumes answers are kept in an ordered list.
   */
  protected static @Nullable Answer getFirstAnswerForQuestionId(
      AccessibilityHierarchyCheckResult result, int questionId) {
    for (Answer answer : result.getAnswers()) {
      if (answer.getQuestion().getQuestionId() == questionId) {
        return answer;
      }
    }
    return null;
  }

  /**
   * Return a collection of {@link Answer} objects that answers the question of questionId. The
   * collection will be empty if the question is unanswered.
   */
  protected static ImmutableList<Answer> getAnswersForQuestionId(
      AccessibilityHierarchyCheckResult result, int questionId) {
    ImmutableList.Builder<Answer> answersBuilder = ImmutableList.builder();
    for (Answer answer : result.getAnswers()) {
      if (answer.getQuestion().getQuestionId() == questionId) {
        answersBuilder.add(answer);
      }
    }
    return answersBuilder.build();
  }

  /** Returns whether a question with questionId has been asked about a given result */
  protected static boolean haveAskedQuestion(
      AccessibilityHierarchyCheckResult result, int questionId) {
    return getFirstAnswerForQuestionId(result, questionId) != null;
  }
} public interface ResultMetadata {

  /**
   * Retrieves a {@code boolean} value stored within metadata. A {@link RuntimeException} is thrown
   * in the case the provided {@code key} is not stored within metadata or is stored as another
   * type.
   *
   * @param key The key of the value to retrieve
   * @return The {@code boolean} value associated with {@code key}
   */
  boolean getBoolean(String key);

  /**
   * Retrieves a {@code boolean} value stored within metadata. If the metadata does not contain the
   * provided {@code key}, {@code defaultValue} is returned. A {@link RuntimeException} is thrown in
   * the case the provided {@code key} is stored as another type.
   *
   * @param key The key of the value to retrieve
   * @param defaultValue The value to return if this metadata does not contain {@code key}
   * @return The {@code boolean} value associated with {@code key}, or {@code defaultValue} if the
   *     provided key does not exist
   */
  boolean getBoolean(String key, boolean defaultValue);

  /**
   * Stores a {@code boolean} value within metadata.
   *
   * @param key The key of the value to store
   * @param value The value to store associated with {@code key}
   */
  void putBoolean(String key, boolean value);

  /**
   * Retrieves a {@code byte} value stored within metadata. A {@link RuntimeException} is thrown in
   * the case the provided {@code key} is not stored within metadata or is stored as another type.
   *
   * @param key The key of the value to retrieve
   * @return The {@code byte} value associated with {@code key}
   */
  byte getByte(String key);

  /**
   * Retrieves a {@code byte} value stored within metadata. If the metadata does not contain the
   * provided {@code key}, {@code defaultValue} is returned. A {@link RuntimeException} is thrown in
   * the case the provided {@code key} is stored as another type.
   *
   * @param key The key of the value to retrieve
   * @param defaultValue The value to return if this metadata does not contain {@code key}
   * @return The {@code byte} value associated with {@code key}, or {@code defaultValue} if the
   *     provided key does not exist
   */
  byte getByte(String key, byte defaultValue);

  /**
   * Stores a {@code byte} value within metadata.
   *
   * @param key The key of the value to store
   * @param value The value to store associated with {@code key}
   */
  void putByte(String key, byte value);

  /**
   * Retrieves a {@code short} value stored within metadata. A {@link RuntimeException} is thrown in
   * the case the provided {@code key} is not stored within metadata or is stored as another type.
   *
   * @param key The key of the value to retrieve
   * @return The {@code short} value associated with {@code key}
   */
  short getShort(String key);

  /**
   * Retrieves a {@code short} value stored within metadata. If the metadata does not contain the
   * provided {@code key}, {@code defaultValue} is returned. A {@link RuntimeException} is thrown in
   * the case the provided {@code key} is stored as another type.
   *
   * @param key The key of the value to retrieve
   * @param defaultValue The value to return if this metadata does not contain {@code key}
   * @return The {@code short} value associated with {@code key}, or {@code defaultValue} if the
   *     provided key does not exist
   */
  short getShort(String key, short defaultValue);

  /**
   * Stores a {@code short} value within metadata.
   *
   * @param key The key of the value to store
   * @param value The value to store associated with {@code key}
   */
  void putShort(String key, short value);

  /**
   * Retrieves a {@code char} value stored within metadata. A {@link RuntimeException} is thrown in
   * the case the provided {@code key} is not stored within metadata or is stored as another type.
   *
   * @param key The key of the value to retrieve
   * @return The {@code char} value associated with {@code key}
   */
  char getChar(String key);

  /**
   * Retrieves a {@code char} value stored within metadata. If the metadata does not contain the
   * provided {@code key}, {@code defaultValue} is returned. A {@link RuntimeException} is thrown in
   * the case the provided {@code key} is stored as another type.
   *
   * @param key The key of the value to retrieve
   * @param defaultValue The value to return if this metadata does not contain {@code key}
   * @return The {@code char} value associated with {@code key}, or {@code defaultValue} if the
   *     provided key does not exist
   */
  char getChar(String key, char defaultValue);

  /**
   * Stores a {@code char} value within metadata.
   *
   * @param key The key of the value to store
   * @param value The value to store associated with {@code key}
   */
  void putChar(String key, char value);

  /**
   * Retrieves a {@code int} value stored within metadata. A {@link RuntimeException} is thrown in
   * the case the provided {@code key} is not stored within metadata or is stored as another type.
   *
   * @param key The key of the value to retrieve
   * @return The {@code int} value associated with {@code key}
   */
  int getInt(String key);

  /**
   * Retrieves a {@code int} value stored within metadata. If the metadata does not contain the
   * provided {@code key}, {@code defaultValue} is returned. A {@link RuntimeException} is thrown in
   * the case the provided {@code key} is stored as another type.
   *
   * @param key The key of the value to retrieve
   * @param defaultValue The value to return if this metadata does not contain {@code key}
   * @return The {@code int} value associated with {@code key}, or {@code defaultValue} if the
   *     provided key does not exist
   */
  int getInt(String key, int defaultValue);

  /**
   * Stores a {@code int} value within metadata.
   *
   * @param key The key of the value to store
   * @param value The value to store associated with {@code key}
   */
  void putInt(String key, int value);

  /**
   * Retrieves a {@code float} value stored within metadata. A {@link RuntimeException} is thrown in
   * the case the provided {@code key} is not stored within metadata or is stored as another type.
   *
   * @param key The key of the value to retrieve
   * @return The {@code float} value associated with {@code key}
   */
  float getFloat(String key);

  /**
   * Retrieves a {@code float} value stored within metadata. If the metadata does not contain the
   * provided {@code key}, {@code defaultValue} is returned. A {@link RuntimeException} is thrown in
   * the case the provided {@code key} is stored as another type.
   *
   * @param key The key of the value to retrieve
   * @param defaultValue The value to return if this metadata does not contain {@code key}
   * @return The {@code float} value associated with {@code key}, or {@code defaultValue} if the
   *     provided key does not exist
   */
  float getFloat(String key, float defaultValue);

  /**
   * Stores a {@code float} value within metadata.
   *
   * @param key The key of the value to store
   * @param value The value to store associated with {@code key}
   */
  void putFloat(String key, float value);

  /**
   * Retrieves a {@code long} value stored within metadata. A {@link RuntimeException} is thrown in
   * the case the provided {@code key} is not stored within metadata or is stored as another type.
   *
   * @param key The key of the value to retrieve
   * @return The {@code long} value associated with {@code key}
   */
  long getLong(String key);

  /**
   * Retrieves a {@code long} value stored within metadata. If the metadata does not contain the
   * provided {@code key}, {@code defaultValue} is returned. A {@link RuntimeException} is thrown in
   * the case the provided {@code key} is stored as another type.
   *
   * @param key The key of the value to retrieve
   * @param defaultValue The value to return if this metadata does not contain {@code key}
   * @return The {@code long} value associated with {@code key}, or {@code defaultValue} if the
   *     provided key does not exist
   */
  long getLong(String key, long defaultValue);

  /**
   * Stores a {@code long} value within metadata.
   *
   * @param key The key of the value to store
   * @param value The value to store associated with {@code key}
   */
  void putLong(String key, long value);

  /**
   * Retrieves a {@code double} value stored within metadata. A {@link RuntimeException} is thrown
   * in the case the provided {@code key} is not stored within metadata or is stored as another
   * type.
   *
   * @param key The key of the value to retrieve
   * @return The {@code double} value associated with {@code key}
   */
  double getDouble(String key);

  /**
   * Retrieves a {@code double} value stored within metadata. If the metadata does not contain the
   * provided {@code key}, {@code defaultValue} is returned. A {@link RuntimeException} is thrown in
   * the case the provided {@code key} is stored as another type.
   *
   * @param key The key of the value to retrieve
   * @param defaultValue The value to return if this metadata does not contain {@code key}
   * @return The {@code double} value associated with {@code key}, or {@code defaultValue} if the
   *     provided key does not exist
   */
  double getDouble(String key, double defaultValue);

  /**
   * Stores a {@code double} value within metadata.
   *
   * @param key The key of the value to store
   * @param value The value to store associated with {@code key}
   */
  void putDouble(String key, double value);

  /**
   * Retrieves a {@code String} value stored within metadata. A {@link RuntimeException} is thrown
   * in the case the provided {@code key} is not stored within metadata or is stored as another
   * type.
   *
   * @param key The key of the value to retrieve
   * @return The {@code String} value associated with {@code key}
   */
  String getString(String key);

  /**
   * Retrieves a {@code String} value stored within metadata. If the metadata does not contain the
   * provided {@code key}, {@code defaultValue} is returned. A {@link RuntimeException} is thrown in
   * the case the provided {@code key} is stored as another type.
   *
   * @param key The key of the value to retrieve
   * @param defaultValue The value to return if this metadata does not contain {@code key}
   * @return The {@code String} value associated with {@code key}, or {@code defaultValue} if the
   *     provided key does not exist
   */
  String getString(String key, String defaultValue);

  /**
   * Stores a {@code String} value within metadata.
   *
   * @param key The key of the value to store
   * @param value The value to store associated with {@code key}
   */
  void putString(String key, String value);

  /**
   * Retrieves a List of String values stored within metadata. A {@link RuntimeException} is thrown
   * in the case the provided {@code key} is not stored within metadata or is stored as another
   * type.
   *
   * @param key The key of the value to retrieve
   * @return The values associated with {@code key}
   */
  ImmutableList<String> getStringList(String key);

  /**
   * Retrieves a List of String values stored within metadata. If the metadata does not contain the
   * provided {@code key}, {@code defaultValue} is returned. A {@link RuntimeException} is thrown in
   * the case the provided {@code key} is stored as another type.
   *
   * @param key The key of the value to retrieve
   * @param defaultValue The value to return if this metadata does not contain {@code key}
   * @return The values associated with {@code key}, or {@code defaultValue} if the provided key
   *     does not exist
   */
  ImmutableList<String> getStringList(String key, ImmutableList<String> defaultValue);

  /**
   * Stores a non-empty List of String values within metadata, replacing any existing value for the
   * given key.
   *
   * @param key The key of the value to store
   * @param value The value to store associated with {@code key}
   */
  void putStringList(String key, List<String> value);

  /**
   * Retrieve a List of Integer values stored within metadata. A {@link RuntimeException} is thrown
   * in the case the provided {@code key} is not stored within metadata or is stored as another
   * type.
   *
   * @param key The key of the value to retrieve
   * @return The values associated with {@code key}
   */
  ImmutableList<Integer> getIntegerList(String key);

  /**
   * Retrieves a List of Integer values stored within metadata. If the metadata does not contain the
   * provided {@code key}, {@code defaultValue} is returned. A {@link RuntimeException} is thrown in
   * the case the provided {@code key} is stored as another type.
   *
   * @param key The key of the value to retrieve
   * @param defaultValue The value to return if this metadata does not contain {@code key}
   * @return The {@code ImmutableList<Integer>} value associated with {@code key}, or {@code
   *     defaultValue} if the provided key does not exist
   */
  ImmutableList<Integer> getIntegerList(String key, ImmutableList<Integer> defaultValue);

  /**
   * Stores a non-empty List of Integer values within metadata, replacing any existing value for the
   * given key.
   *
   * @param key The key of the value to store
   * @param value The value to store associated with {@code key}
   */
  void putIntegerList(String key, List<Integer> value);

  /** See {@link Map#containsKey(Object)}. */
  boolean containsKey(String key);

  /** Returns {@code true} if this map contains no key-value mappings. */
  boolean isEmpty();

  /** Creates a shallow copy of this metadata */
  ResultMetadata clone();
} public final class ViewHierarchyElementUtils {
  public static final String ABS_LIST_VIEW_CLASS_NAME = "android.widget.AbsListView";
  public static final String ADAPTER_VIEW_CLASS_NAME = "android.widget.AdapterView";
  public static final String SCROLL_VIEW_CLASS_NAME = "android.widget.ScrollView";
  public static final String HORIZONTAL_SCROLL_VIEW_CLASS_NAME =
      "android.widget.HorizontalScrollView";
  public static final String SPINNER_CLASS_NAME = "android.widget.Spinner";
  public static final String TEXT_VIEW_CLASS_NAME = "android.widget.TextView";
  public static final String EDIT_TEXT_CLASS_NAME = "android.widget.EditText";
  public static final String IMAGE_VIEW_CLASS_NAME = "android.widget.ImageView";
  public static final String WEB_VIEW_CLASS_NAME = "android.webkit.WebView";
  public static final String SWITCH_CLASS_NAME = "android.widget.Switch";

  static final ImmutableList<String> SCROLLABLE_CONTAINER_CLASS_NAME_LIST =
      ImmutableList.of(
          ADAPTER_VIEW_CLASS_NAME, SCROLL_VIEW_CLASS_NAME, HORIZONTAL_SCROLL_VIEW_CLASS_NAME);

  private ViewHierarchyElementUtils() {}

  /**
   * Determine what text would be spoken by a screen reader for an element.
   *
   * @param element The element whose spoken text is desired. If it or its children are only
   *     partially initialized, this method may return additional text that would not be spoken.
   * @return An approximation of what a screen reader would speak for the element
   */
  public static SpannableString getSpeakableTextForElement(ViewHierarchyElement element) {
    if (element.isImportantForAccessibility()) {
      // Determine if this element is labeled by another element
      if (element.getLabeledBy() != null) {
        return getSpeakableTextFromElementSubtree(element.getLabeledBy());
      }
    }
    return getSpeakableTextFromElementSubtree(element);
  }

  /**
   * Determine what text would be spoken by a screen reader for an element and its subtree,
   * disregarding other labeling relationships within the hierarchy.
   *
   * @param element The element whose spoken text is desired
   * @return An approximation of what a screen reader would speak for the element and its subtree
   */
  private static SpannableString getSpeakableTextFromElementSubtree(ViewHierarchyElement element) {
    SpannableStringBuilder returnStringBuilder = new SpannableStringBuilder();
    if (element.isImportantForAccessibility()) {
      // Content descriptions override everything else -- including children
      SpannableString contentDescription = element.getContentDescription();
      if (!TextUtils.isEmpty(contentDescription)) {
        return returnStringBuilder.appendWithSeparator(contentDescription).build();
      }

      SpannableString text = element.getText();
      if (!TextUtils.isEmpty(text) && (TextUtils.getTrimmedLength(text) > 0)) {
        returnStringBuilder.appendWithSeparator(text);
      } else {
        SpannableString hint = element.getHintText();
        if (!TextUtils.isEmpty(hint) && (TextUtils.getTrimmedLength(hint) > 0)) {
          returnStringBuilder.appendWithSeparator(hint);
        }
      }

      if (TRUE.equals(element.isCheckable())) {
        if (TRUE.equals(element.isChecked())) {
          returnStringBuilder.appendWithSeparator("Checked");
        } else if (FALSE.equals(element.isChecked())) {
          returnStringBuilder.appendWithSeparator("Not checked");
        }
      }

      if (element.checkInstanceOf(ABS_LIST_VIEW_CLASS_NAME) && element.getChildViewCount() == 0) {
        returnStringBuilder.appendWithSeparator("List showing 0 items");
      }
    }

    /* Collect speakable text from children */
    for (int i = 0; i < element.getChildViewCount(); ++i) {
      ViewHierarchyElement child = element.getChildView(i);
      if (!FALSE.equals(child.isVisibleToUser()) && !isActionableForAccessibility(child)) {
        SpannableString childDesc = getSpeakableTextFromElementSubtree(child);
        if (!TextUtils.isEmpty(childDesc)) {
          returnStringBuilder.appendWithSeparator(childDesc);
        }
      }
    }

    return returnStringBuilder.build();
  }

  /**
   * Determines if the supplied {@link ViewHierarchyElement} would be focused during navigation
   * operations with a screen reader.
   *
   * @param view The {@link ViewHierarchyElement} to evaluate
   * @return {@code true} if a screen reader would choose to place accessibility focus on {@code
   *     view}, {@code false} otherwise.
   */
  public static boolean shouldFocusView(ViewHierarchyElement view) {
    if (!TRUE.equals(view.isVisibleToUser())) {
      // We don't focus views that are not visible
      return false;
    }

    if (isAccessibilityFocusable(view)) {
      if (!hasAnyImportantDescendant(view)) {
        // Leaves that are accessibility focusable always gain focus regardless of presence of a
        // spoken description. This allows unlabeled, but still actionable, widgets to be activated
        // by the user.
        return true;
      } else if (isSpeakingView(view)) {
        // The view (or its grouped non-actionable children) have content to speak.
        return true;
      }

      return false;
    }

    if (hasText(view) && view.isImportantForAccessibility() && !hasFocusableAncestor(view)) {
      return true;
    }

    return false;
  }

  /**
   * Check if an element would correspond to an {@link
   * android.view.accessibility.AccessibilityNodeInfo} that would be deemed actionable for
   * accessibility.
   *
   * @param element The element to check
   * @return {@code true} if the element is actionable, {@code false} if not.
   */
  private static boolean isActionableForAccessibility(ViewHierarchyElement element) {
    return element.isClickable() || element.isFocusable() || element.isLongClickable();
  }

  /**
   * Determines if the supplied {@link ViewHierarchyElement} has an ancestor which meets the
   * criteria for gaining accessibility focus.
   *
   * <p>NOTE: This method only evaluates ancestors which may be considered important for
   * accessibility and explicitly does not evaluate the supplied {@code view}.
   *
   * @param view The {@link ViewHierarchyElement} to evaluate
   * @return {@code true} if an ancestor of {@code view} may gain accessibility focus, {@code false}
   *     otherwise
   */
  private static boolean hasFocusableAncestor(ViewHierarchyElement view) {
    ViewHierarchyElement parent = getImportantForAccessibilityAncestor(view);
    if (parent == null) {
      return false;
    }

    if (isAccessibilityFocusable(parent)) {
      return true;
    }

    return hasFocusableAncestor(parent);
  }

  /**
   * Determines if the supplied {@link ViewHierarchyElement} meets the criteria for gaining
   * accessibility focus.
   *
   * @param view The {@link ViewHierarchyElement} to evaluate
   * @return {@code true} if it is possible for {@code view} to gain accessibility focus, {@code
   *     false} otherwise.
   */
  private static boolean isAccessibilityFocusable(ViewHierarchyElement view) {
    if (!TRUE.equals(view.isVisibleToUser())) {
      return false;
    }

    if (!view.isImportantForAccessibility()) {
      return false;
    }

    if (isActionableForAccessibility(view)) {
      return true;
    }

    return isChildOfScrollableContainer(view) && isSpeakingView(view);
  }

  /**
   * Determines if the supplied {@link ViewHierarchyElement} is a top-level item within a scrollable
   * container.
   *
   * @param view The {@link ViewHierarchyElement} to evaluate
   * @return {@code true} if {@code view} is a top-level view within a scrollable container, {@code
   * false} otherwise
   */
  private static boolean isChildOfScrollableContainer(ViewHierarchyElement view) {

    // Identify the nearest importantForAccessibility parent
    ViewHierarchyElement parent = getImportantForAccessibilityAncestor(view);

    if (parent == null) {
      return false;
    }

    if (TRUE.equals(parent.isScrollable())) {
      return true;
    }

    // Specifically check for parents that are AdapterView, ScrollView, or HorizontalScrollView, but
    // exclude Spinners, which are a special case of AdapterView.  TalkBack explicitly identifies
    // views with parents matching these classes as direct children of a scrollable container.
    if (parent.checkInstanceOf(SPINNER_CLASS_NAME)) {
      return false;
    }

    return parent.checkInstanceOfAny(SCROLLABLE_CONTAINER_CLASS_NAME_LIST);
  }

  /**
   * Determines if the supplied {@link ViewHierarchyElement} is one which would produce speech if it
   * were to gain accessibility focus. <p> NOTE: This method also evaluates the subtree of the
   * {@code view} for children that should be included in {@code view}'s spoken description.
   *
   * @param view The {@link ViewHierarchyElement} to evaluate
   * @return {@code true} if a spoken description for {@code view} was determined, {@code false}
   * otherwise.
   */
  private static boolean isSpeakingView(ViewHierarchyElement view) {
    if (hasText(view)) {
      return true;
    } else if (TRUE.equals(view.isCheckable())) {
      // Special case for checkable items, which screen readers may describe without text
      return true;
    } else if (hasNonFocusableSpeakingChildren(view)) {
      return true;
    }

    return false;
  }

  /**
   * Determines if the supplied {@link ViewHierarchyElement} has child view(s) which are not
   * independently accessibility focusable and also have a spoken description. Put another way, this
   * method determines if {@code view} has at least one child which should be included in {@code
   * view}'s spoken description if {@code view} were to be accessibility focused.
   *
   * @param view The {@link ViewHierarchyElement} to evaluate
   * @return {@code true} if {@code view} has non-actionable speaking children within its subtree
   */
  private static boolean hasNonFocusableSpeakingChildren(ViewHierarchyElement view) {
    for (int i = 0; i < view.getChildViewCount(); ++i) {
      ViewHierarchyElement child = view.getChildView(i);
      if ((child == null)
          || !TRUE.equals(child.isVisibleToUser())
          || isAccessibilityFocusable(child)) {
        continue;
      }

      if (child.isImportantForAccessibility() && isSpeakingView(child)) {
        return true;
      }
    }

    return false;
  }

  /**
   * Determines if the supplied {@link ViewHierarchyElement} has a contentDescription, text or hint.
   *
   * @param view The {@link ViewHierarchyElement} to evaluate
   * @return {@code true} if {@code view} has a contentDescription, text or hint, {@code false}
   *     otherwise.
   */
  private static boolean hasText(ViewHierarchyElement view) {
    return !TextUtils.isEmpty(view.getText())
        || !TextUtils.isEmpty(view.getContentDescription())
        || !TextUtils.isEmpty(view.getHintText());
  }

  /**
   * Returns the nearest ancestor in the provided {@code view}'s lineage that is important for
   * accessibility.
   *
   * @param view The {@link ViewHierarchyElement} to evaluate
   * @return The first important for accessibility {@link ViewHierarchyElement} in {@code view}'s
   * lineage, or {@code null} if no such ancestor exists.
   */
  private static @Nullable ViewHierarchyElement getImportantForAccessibilityAncestor(
      ViewHierarchyElement view) {
    ViewHierarchyElement parent = view.getParentView();
    while ((parent != null) && !view.isImportantForAccessibility()) {
      parent = parent.getParentView();
    }

    return parent;
  }

  /**
   * Determines if the provided {@code element} has any descendant, direct or indirect, which is
   * considered important for accessibility.  This is useful in determining whether or not the
   * Android framework will attempt to reparent any child in the subtree as a direct descendant of
   * {@code element} while converting the hierarchy to an accessibility API representation.
   *
   * @param element the {@link ViewHierarchyElement} to evaluate
   * @return {@code true} if any child in {@code element}'s subtree is considered important for
   *         accessibility, {@code false} otherwise
   */
  private static boolean hasAnyImportantDescendant(ViewHierarchyElement element) {
    for (int i = 0; i < element.getChildViewCount(); ++i) {
      ViewHierarchyElement child = element.getChildView(i);
      if (child.isImportantForAccessibility()) {
        return true;
      }

      if (child.getChildViewCount() > 0) {
        if (hasAnyImportantDescendant(child)) {
          return true;
        }
      }
    }

    return false;
  }

  /**
   * Determines whether the provided {@code viewHierarchyElement} on the active window is
   * intersected by any overlay {@link WindowHierarchyElement} whose z-order is greater than the
   * z-order of the active window.
   *
   * @param viewHierarchyElement the element to check
   * @return {@code true} if the {@code viewHierarchyElement} is intersected by any overlay {@link
   *     WindowHierarchyElement}, otherwise {@code false}
   */
  public static boolean isIntersectedByOverlayWindow(ViewHierarchyElement viewHierarchyElement) {
    AccessibilityHierarchy hierarchy = viewHierarchyElement.getWindow().getAccessibilityHierarchy();
    Integer activeWindowLayer = hierarchy.getActiveWindow().getLayer();
    if (activeWindowLayer == null) {
      return false;
    }

    for (WindowHierarchyElement window : hierarchy.getAllWindows()) {
      if ((window.getLayer() != null) && (checkNotNull(window.getLayer()) > activeWindowLayer)) {
        if (Rect.intersects(viewHierarchyElement.getBoundsInScreen(), window.getBoundsInScreen())) {
          return true;
        }
      }
    }
    return false;
  }

  /**
   * Determines whether the {@code element} on the active window is known to have an intersecting
   * overlay {@link ViewHierarchyElement} based upon their drawing orders in their parent views.
   *
   * @param element the element to check
   * @return {@code true} if the element is known to have an intersecting overlay element based upon
   *     their drawing orders, otherwise {@code false}
   * @see android.view.accessibility.AccessibilityNodeInfo#getDrawingOrder()
   */
  @SuppressWarnings("ReferenceEquality")
  public static boolean isIntersectedByOverlayView(ViewHierarchyElement element) {
    if (element.getDrawingOrder() == null) {
      return false;
    }

    AccessibilityHierarchy hierarchy = element.getWindow().getAccessibilityHierarchy();
    ViewHierarchyElement rootView = hierarchy.getActiveWindow().getRootView();

    ViewHierarchyElement view = element;
    while (view != rootView) {
      ViewHierarchyElement parentView = checkNotNull(view.getParentView());
      for (int i = 0; i < parentView.getChildViewCount(); i++) {
        ViewHierarchyElement siblingView = parentView.getChildView(i);
        if (checkNotNull(siblingView.getDrawingOrder()) > checkNotNull(view.getDrawingOrder())
            && Rect.intersects(element.getBoundsInScreen(), siblingView.getBoundsInScreen())) {
          return true;
        }
      }
      view = parentView;
    }

    return false;
  }

  /**
   * Determines whether the provided {@code viewHierarchyElement} on the active window may be
   * obscured by other on-screen content.
   *
   * @param viewHierarchyElement the element to check
   * @return {@code true} if the {@code viewHierarchyElement} may be obscured by other on-screen
   *     content, otherwise {@code false}
   */
  public static boolean isPotentiallyObscured(ViewHierarchyElement viewHierarchyElement) {
    return isIntersectedByOverlayWindow(viewHierarchyElement)
        || isIntersectedByOverlayView(viewHierarchyElement);
  }
} public final class Rect {

  public static final Rect EMPTY = new Rect(0, 0, 0, 0);

  private final int left;
  private final int top;
  private final int right;
  private final int bottom;

  public Rect(int left, int top, int right, int bottom) {
    // Ensure coordinate ordering is sensible
    this.left = min(left, right);
    this.top = min(top, bottom);
    this.right = max(left, right);
    this.bottom = max(top, bottom);
  }

  public Rect(RectProto rectProto) {
    this(rectProto.getLeft(), rectProto.getTop(), rectProto.getRight(), rectProto.getBottom());
  }

  /** See {@link android.graphics.Rect#left} */
  public final int getLeft() {
    return left;
  }

  /** See {@link android.graphics.Rect#top} */
  public final int getTop() {
    return top;
  }

  /** See {@link android.graphics.Rect#right} */
  public final int getRight() {
    return right;
  }

  /** See {@link android.graphics.Rect#bottom} */
  public final int getBottom() {
    return bottom;
  }

  /** See {@link android.graphics.Rect#width()} */
  public final int getWidth() {
    return right - left;
  }

  /** See {@link android.graphics.Rect#height()} */
  public final int getHeight() {
    return bottom - top;
  }

  /** See {@link android.graphics.Rect#contains(android.graphics.Rect)} */
  public boolean contains(Rect r) {
    return !isEmpty()
        && (this.left <= r.left)
        && (this.top <= r.top)
        && (this.right >= r.right)
        && (this.bottom >= r.bottom);
  }

  /** See {@link android.graphics.Rect#contains(int, int)} */
  public boolean contains(int x, int y) {
    return !isEmpty() && x >= left && x < right && y >= top && y < bottom;
  }

  /** See {@link android.graphics.Rect#isEmpty()} */
  public boolean isEmpty() {
    return (left == right) || (top == bottom);
  }

  /**
   * Returns a {@link Rect} which encloses this rectangle and the specified rectangle. If the
   * specified rectangle is empty, return {@code this}. If this rectangle is empty, return the
   * specified rectangle.
   *
   * <p>See {@link android.graphics.Rect#union(android.graphics.Rect)}
   *
   * @param r The rectangle being unioned with this rectangle
   */
  public Rect union(Rect r) {
    return isEmpty() ? r : union(r.left, r.top, r.right, r.bottom);
  }

  /**
   * Returns a {@link Rect} which encloses this rectangle and the specified rectangle. If the
   * specified rectangle is empty, return {@code this}. If this rectangle is empty, return the
   * specified rectangle.
   *
   * <p>See {@link android.graphics.Rect#union(int, int, int, int)}
   *
   * @param left The left edge being unioned with this rectangle
   * @param top The top edge being unioned with this rectangle
   * @param right The right edge being unioned with this rectangle
   * @param bottom The bottom edge being unioned with this rectangle
   */
  public Rect union(int left, int top, int right, int bottom) {
    if ((right <= left) || (bottom <= top)) {
      return this;
    }

    if (isEmpty()) {
      return new Rect(left, top, right, bottom);
    } else {
      return new Rect(
          min(left, this.left),
          min(top, this.top),
          max(right, this.right),
          max(bottom, this.bottom));
    }
  }

  public RectProto toProto() {
    return RectProto.newBuilder()
        .setLeft(left)
        .setTop(top)
        .setRight(right)
        .setBottom(bottom)
        .build();
  }

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

    if (o == null || Rect.class != o.getClass()) {
      return false;
    }

    Rect r = (Rect) o;
    return ((left == r.left) && (top == r.top) && (right == r.right) && (bottom == r.bottom));
  }

  @Override
  public int hashCode() {
    int result = left;
    result = 31 * result + top;
    result = 31 * result + right;
    result = 31 * result + bottom;
    return result;
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder(32);
    sb.append("Rect(");
    sb.append(left);
    sb.append(", ");
    sb.append(top);
    sb.append(" - ");
    sb.append(right);
    sb.append(", ");
    sb.append(bottom);
    sb.append(")");
    return sb.toString();
  }

  /** See {@link android.graphics.Rect#toShortString()} */
  public String toShortString() {
    StringBuilder sb = new StringBuilder(32);
    sb.append('[');
    sb.append(left);
    sb.append(',');
    sb.append(top);
    sb.append("][");
    sb.append(right);
    sb.append(',');
    sb.append(bottom);
    sb.append(']');
    return sb.toString();
  }

  /** See {@link android.graphics.Rect#intersects(android.graphics.Rect, android.graphics.Rect)} */
  public static boolean intersects(Rect a, Rect b) {
    return (a.top < b.bottom) && (b.top < a.bottom) && (a.left < b.right) && (b.left < a.right);
  }
} public class TextUtils {
  private TextUtils() {
    // Not instantiable
  }

  /**
   * @see android.text.TextUtils#isEmpty(CharSequence)
   */
  @EnsuresNonNullIf(expression = "#1", result = false)
  public static boolean isEmpty(@Nullable CharSequence str) {
    return (str == null) || (str.length() == 0);
  }

  /** See {@link android.text.TextUtils#getTrimmedLength(CharSequence)}. */
  public static int getTrimmedLength(CharSequence str) {
    return str.toString().trim().length();
  }

  /** See {@link android.text.TextUtils#equals(CharSequence, CharSequence)}. */
  public static boolean equals(@Nullable CharSequence s1, @Nullable CharSequence s2) {
    if (s1 == s2) {
      return true;
    }

    if ((s1 != null) && (s2 != null) && (s1.length() == s2.length())) {
      if (s1 instanceof String && s2 instanceof String) {
        return ((String) s1).equals((String) s2);
      } else {
        for (int i = 0; i < s1.length(); i++) {
          if (s1.charAt(i) != s2.charAt(i)) {
            return false;
          }
        }
        return true;
      }
    }
    return false;
  }
} public final class StringManager {

  private static final String STRINGS_PACKAGE_NAME =
      "com.google.android.apps.common.testing.accessibility.framework";
  private static final String STRINGS_FILE_NAME = "strings";
  private static @Nullable ResourceBundleProvider resourceBundleProvider;

  private StringManager() {
  }

  /**
   * Override the default resource bundle used by {@link StringManager}. If no provider is set, a
   * default ResourceBundle will be used.
   *
   * <p>Example usage : If Accessibility Testing Framework is used outside the
   * AndroidXmlResourceBundle environment or if the ClassLoader does not recognize the xml in
   * specific package {@link #STRINGS_PACKAGE_NAME}, use this to override the resource provider.
   */
  public static void setResourceBundleProvider(ResourceBundleProvider provider) {
    resourceBundleProvider = provider;
  }

  /**
   * @param locale the desired locale
   * @param name the name of the {@link String} to get from properties
   * @return a localized {@link String} corresponding to the given name and locale
   * @throws MissingResourceException if the string is not found
   */
  public static String getString(Locale locale, String name) {
    return getResourceBundle(locale).getString(name);
  }

  private static ResourceBundle getResourceBundle(Locale locale) {
    if (resourceBundleProvider != null) {
      return resourceBundleProvider.getResourceBundle(locale);
    }

    // ResourceBundle handles necessary caching.
    try {
      // Try to load the resources from under the package directory. This is location used for
      // resource files in JAR files within Google.
      return ResourceBundle.getBundle(
          AndroidXMLResourceBundle.Control.getBaseName(STRINGS_PACKAGE_NAME, STRINGS_FILE_NAME),
          locale,
          checkNotNull(StringManager.class.getClassLoader()),
          new AndroidXMLResourceBundle.Control());
    } catch (MissingResourceException e) {

      // If that doesn't work, try again, this time without the package name. This is the location
      // used for assets in an AAR file.
      return ResourceBundle.getBundle(
          AndroidXMLResourceBundle.Control.getBaseName("", STRINGS_FILE_NAME),
          locale,
          checkNotNull(StringManager.class.getClassLoader()),
          new AndroidXMLResourceBundle.Control());
    }
  }

  /**
   * Provider for resource bundle. Use this to override the default resource bundle to use in {@link
   * StringManager}.
   */
  public interface ResourceBundleProvider {

    /** Returns {@link ResourceBundle} that is suitable for the env, and the ClassLoader. */
    ResourceBundle getResourceBundle(Locale locale);
  }
} 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()));
  }
} public final class Color {

  public static final int BLACK = 0xFF000000;

  private Color() {
    // Not instantiable
  }

  /** @see android.graphics.Color#alpha(int) */
  public static int alpha(int color) {
    return color >>> 24;
  }

  /** @see android.graphics.Color#red(int) */
  public static int red(int color) {
    return (color >> 16) & 0xFF;
  }

  /** @see android.graphics.Color#green(int) */
  public static int green(int color) {
    return (color >> 8) & 0xFF;
  }

  /** @see android.graphics.Color#blue(int) */
  public static int blue(int color) {
    return color & 0xFF;
  }
} public class ContrastSwatch {

  public static final int MAX_FOREGROUND_COLOR = 5;

  private static final double COLOR_DIFFERENCE_LIMIT = 2.0;

  private static final double COLOR_SIGNIFICANCE_PERCENTAGE = 0.03;

  private static final double COLOR_CUTOFF_PERCENTAGE = 0.01;

  private final SeparatedColors separatedColors;

  /**
   * Constructs a ContrastSwatch, also extracting certain properties from the bitmap related to
   * contrast and luminance.
   *
   * <p>NOTE: Invoking this constructor performs image processing tasks, which are relatively
   * heavyweight. Callers should create these objects off the UI thread.
   *
   * @param image {@link Image} to be evaluated
   * @param multipleForegroundColors if multiple foreground colors should be identified
   */
  public ContrastSwatch(Image image, boolean multipleForegroundColors) {
    separatedColors = processSwatch(image, multipleForegroundColors);
  }

  /** Compute the background and foreground colors and luminance for the image. */
  private static SeparatedColors processSwatch(Image image, boolean multipleForegroundColors) {
    ColorHistogram colorHistogram = new ColorHistogram(image);
    int imageSize = image.getWidth() * image.getHeight();
    return separateColors(colorHistogram, imageSize, multipleForegroundColors);
  }

  /**
   * Separates the colors in the color histogram into foreground and background colors, and computes
   * luminance values.
   *
   * @param imageSize total number of pixels in the image
   */
  private static SeparatedColors separateColors(
      ColorHistogram colorHistogram, int imageSize, boolean multipleForegroundColors) {
    if (colorHistogram.getColors().isEmpty()) {
      // An empty histogram indicates we've encountered a 0px area image.
      return new SeparatedColors(Color.BLACK);
    }

    if (colorHistogram.getColors().size() == 1) {
      // Deal with views that only contain a single color
      int singleColor = colorHistogram.getColors().iterator().next();
      return new SeparatedColors(singleColor);
    }

    double averageLuminance = colorHistogram.calculateAverageLuminance();

    if (multipleForegroundColors) {
      return separateColorsUsingMultipleForegroundMethod(
          colorHistogram, averageLuminance, imageSize);
    }

    return separateColorsUsingSingleForegroundMethod(colorHistogram, averageLuminance);
  }

  private static SeparatedColors separateColorsUsingMultipleForegroundMethod(
      ColorHistogram colorHistogram, double averageLuminance, int imageSize) {

    // Reduce the color histogram.
    ColorHistogram dominantColorHistogram =
        reduceColors(colorHistogram, imageSize, COLOR_CUTOFF_PERCENTAGE);

    // Treat as single color image if cannot find a second dominant color.

    if (dominantColorHistogram.getColors().size() < 2) {
      int singleColor = dominantColorHistogram.getColors().iterator().next();
      return new SeparatedColors(singleColor);
    }
    // Sort colors with a max heap.
    final PriorityQueue<Map.Entry<Integer, Integer>> frequencyMaxHeap =
        new PriorityQueue<>(
            dominantColorHistogram.getColors().size(), (a, b) -> (b.getValue() - a.getValue()));
    for (Map.Entry<Integer, Integer> entry : dominantColorHistogram.entrySet()) {
      frequencyMaxHeap.offer(entry);
    }
    // The color with the most numbers of pixels is considered the background.
    int backgroundColor = checkNotNull(frequencyMaxHeap.poll()).getKey();

    // The other dominant colors are considered as the foregrounds.
    List<Integer> foregroundColors =
        extractDominantForegroundColors(
            backgroundColor, frequencyMaxHeap, averageLuminance, imageSize);

    // Treat as single color image if cannot find a second dominant color that has opposite
    // luminance or covers enough area of the image.
    if (foregroundColors.isEmpty()) {
      return new SeparatedColors(backgroundColor);
    }
    return new SeparatedColors(backgroundColor, foregroundColors);
  }

  private static SeparatedColors separateColorsUsingSingleForegroundMethod(
      ColorHistogram colorHistogram, double averageLuminance) {

    // Select the most common color with above average luminance, and the most common color with
    // below average luminance as our background and foreground colors.
    int lowLuminanceColor = -1;
    int highLuminanceColor = -1;
    int maxLowLuminanceFrequency = 0;
    int maxHighLuminanceFrequency = 0;
    for (Map.Entry<Integer, Integer> entry : colorHistogram.entrySet()) {
      final int color = entry.getKey();
      final double luminanceValue = ContrastUtils.calculateLuminance(color);
      final int frequency = entry.getValue();
      if ((luminanceValue < averageLuminance) && (frequency > maxLowLuminanceFrequency)) {
        maxLowLuminanceFrequency = frequency;
        lowLuminanceColor = color;
      } else if ((luminanceValue >= averageLuminance) && (frequency > maxHighLuminanceFrequency)) {
        maxHighLuminanceFrequency = frequency;
        highLuminanceColor = color;
      }
    }
    // Background luminance is that which occurs more frequently
    int backgroundColor;
    int foregroundColor;
    if (maxHighLuminanceFrequency > maxLowLuminanceFrequency) {
      backgroundColor = highLuminanceColor;
      foregroundColor = lowLuminanceColor;
    } else {
      backgroundColor = lowLuminanceColor;
      foregroundColor = highLuminanceColor;
    }
    return new SeparatedColors(backgroundColor, foregroundColor);
  }

  /**
   * Reduces and combines colors based on a cutoff percentage and perceptual color difference Delta
   * E.
   *
   * @param colorHistogram the color histogram to be processed.
   * @param imageSize total number of pixels in the image
   * @param cutoff the percentage value below which all the colors are ignored.
   * @return a new histogram that contains the dominant colors from the given histogram.
   */
  private static ColorHistogram reduceColors(
      ColorHistogram colorHistogram, int imageSize, double cutoff) {
    Map<Integer, Integer> dominantColorHistogram = new HashMap<>();
    ArrayList<Map.Entry<Integer, Integer>> dominantColorsList = new ArrayList<>();

    // Remove noises and maintain iteration order
    for (Map.Entry<Integer, Integer> entry : colorHistogram.entrySet()) {
      if (entry.getValue() >= imageSize * cutoff) {
        dominantColorsList.add(entry);
      }
    }
    Collections.sort(dominantColorsList, (a, b) -> (b.getValue() - a.getValue()));

    // Combine similar colors
    for (Map.Entry<Integer, Integer> entry : dominantColorsList) {
      int color = entry.getKey();
      int colorCount = entry.getValue();
      for (int dominantColor : dominantColorHistogram.keySet()) {
        if (ContrastUtils.colorDifference(dominantColor, entry.getKey()) < COLOR_DIFFERENCE_LIMIT) {
          color = dominantColor;
          colorCount += dominantColorHistogram.get(dominantColor);
          break;
        }
      }
      dominantColorHistogram.put(color, colorCount);
    }
    return new ColorHistogram(dominantColorHistogram);
  }

  /**
   * Extract dominant foreground colors. Colors that have opposite luminance values are prioritized
   * in the list.
   *
   * @param frequencyMaxHeap a max heap of all the foreground colors.
   * @param averageLuminance the average Luminance of the image.
   * @param imageSize total number of pixels in the image
   */
  private static List<Integer> extractDominantForegroundColors(
      int backgroundColor,
      PriorityQueue<Map.Entry<Integer, Integer>> frequencyMaxHeap,
      double averageLuminance,
      int imageSize) {
    double backgroundLuminance = ContrastUtils.calculateLuminance(backgroundColor);
    boolean backgroundLuminanceBelowAverage = (backgroundLuminance < averageLuminance);
    List<Integer> foregroundColors = new ArrayList<>();
    int priorityIndex = 0;
    while (!frequencyMaxHeap.isEmpty() && (foregroundColors.size() < MAX_FOREGROUND_COLOR)) {
      Map.Entry<Integer, Integer> entry = frequencyMaxHeap.poll();
      int newColor = checkNotNull(entry).getKey();
      double newLuminance = ContrastUtils.calculateLuminance(newColor);
      boolean newLuminanceBelowAverage = newLuminance <= averageLuminance;
      boolean oppositeLuminance = backgroundLuminanceBelowAverage != newLuminanceBelowAverage;

      if (oppositeLuminance) {
        foregroundColors.add(priorityIndex++, newColor);
      } else if (entry.getValue() > imageSize * COLOR_SIGNIFICANCE_PERCENTAGE) {
        foregroundColors.add(newColor);
      }
    }
    return foregroundColors;
  }

  public int getBackgroundColor() {
    return separatedColors.getBackgroundColor();
  }

  /**
   * Returns the foreground colors in the image. The list contains at least one and up to {@link
   * MAX_FOREGROUND_COLOR} elements.
   */
  public ImmutableList<Integer> getForegroundColors() {
    return separatedColors.getForegroundColors();
  }

  public double getBackgroundLuminance() {
    return ContrastUtils.calculateLuminance(getBackgroundColor());
  }

  /**
   * Returns luminances of the foreground colors in the image. Each element in the list corresponds
   * to the luminance of a color in {@link #getForegroundColors()} with the same index.
   */
  public ImmutableList<Double> getForegroundLuminances() {
    ImmutableList.Builder<Double> foregroundLuminances = ImmutableList.builder();
    for (Integer color : getForegroundColors()) {
      foregroundLuminances.add(ContrastUtils.calculateLuminance(checkNotNull(color)));
    }
    return foregroundLuminances.build();
  }

  /**
   * Returns a {@link List<Double>} representing the contrast ratios of the image. Each element in
   * the list corresponds to the contrast ratio between {@code backgroundColor} and the color in
   * {@code foregroundColors} with the same index.
   */
  public ImmutableList<Double> getContrastRatios() {
    ImmutableList.Builder<Double> contrastRatios = ImmutableList.builder();

    // Two-decimal digits of precision for the contrast ratio
    double backgroundLuminance = getBackgroundLuminance();
    List<Double> foregroundLuminances = getForegroundLuminances();
    for (Double element : foregroundLuminances) {
      contrastRatios.add(
          Math.round(ContrastUtils.calculateContrastRatio(backgroundLuminance, element) * 100.0d)
              / 100.0d);
    }

    return contrastRatios.build();
  }

  @Override
  public String toString() {
    return "{contrast:1:"
        + getContrastRatios().get(0)
        + ", background:"
        + ContrastUtils.colorToHexString(separatedColors.getBackgroundColor())
        + ", foreground:"
        + ContrastUtils.colorToHexString(separatedColors.getForegroundColors().get(0))
        + "}";
  }

  private static class SeparatedColors {

    private final int backgroundColor;
    private final ImmutableList<Integer> foregroundColors;

    /** Constructs an instance with the foreground and background having the same color. */
    SeparatedColors(int singleColor) {
      this(singleColor, singleColor);
    }

    /** Constructs an instance with a background color and a single foreground color. */
    SeparatedColors(int backgroundColor, int foregroundColor) {
      this(backgroundColor, ImmutableList.of(foregroundColor));
    }

    /** Constructs an instance with a background color and multiple foreground colors. */
    SeparatedColors(int backgroundColor, List<Integer> foregroundColors) {
      this.backgroundColor = backgroundColor;
      this.foregroundColors = ImmutableList.copyOf(foregroundColors);
    }

    int getBackgroundColor() {
      return backgroundColor;
    }

    ImmutableList<Integer> getForegroundColors() {
      return foregroundColors;
    }
  }

  /**
   * The set of colors from an image, and the count (number of pixels) associated with each color.
   */
  private static class ColorHistogram {

    private final ImmutableMap<Integer, Integer> colorHistogram;

    ColorHistogram(Image image) {
      colorHistogram = processLuminanceData(image);
    }

    private ColorHistogram(Map<Integer, Integer> map) {
      colorHistogram = ImmutableMap.copyOf(map);
    }

    ImmutableSet<Integer> getColors() {
      return colorHistogram.keySet();
    }

    /** Returns each color paired with its count. The keys are colors, */
    ImmutableSet<Map.Entry<Integer, Integer>> entrySet() {
      return colorHistogram.entrySet();
    }

    @Nullable
    Integer getCount(int color) {
      return colorHistogram.get(color);
    }

    /**
     * Finds the average luminance value within the set of colors for purposes of splitting colors
     * into high-luminance and low-luminance buckets. This is explicitly not a weighted average.
     */
    double calculateAverageLuminance() {
      double luminanceSum = 0;
      for (int color : getColors()) {
        luminanceSum += ContrastUtils.calculateLuminance(color);
      }
      return luminanceSum / getColors().size();
    }

    /**
     * @return a map where the keys are colors that are present in the image, and the values are the
     *     number of pixels with each color.
     */
    private static ImmutableMap<Integer, Integer> processLuminanceData(Image image) {
      final int width = image.getWidth();
      final int height = image.getHeight();
      if ((width * height) == 0) {
        return ImmutableMap.of();
      }
      int[] pixels = image.getPixels();
      Arrays.sort(pixels);

      ImmutableMap.Builder<Integer, Integer> colorHistogram = ImmutableMap.builder();
      int currentColor = pixels[0];
      int currentColorCount = 1;
      for (int i = 1; i < pixels.length; i++) {
        int color = pixels[i];
        if (color == currentColor) {
          currentColorCount++;
        } else {
          colorHistogram.put(currentColor, currentColorCount);
          currentColor = color;
          currentColorCount = 1;
        }
      }
      // Catch the last unprocessed color
      colorHistogram.put(currentColor, currentColorCount);
      return colorHistogram.build();
    }
  }
} public final class ContrastUtils {

  /**
   * The minimum text size considered large for contrast checking purposes, as taken from the WCAG
   * standards at http://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html
   */
  public static final int WCAG_LARGE_TEXT_MIN_SIZE = 18;

  /**
   * The minimum text size for bold text to be considered large for contrast checking purposes, as
   * taken from the WCAG standards at
   * http://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html
   */
  public static final int WCAG_LARGE_BOLD_TEXT_MIN_SIZE = 14;

  /** The color value used to censor secure windows from screen capture */
  public static final int COLOR_SECURE_WINDOW_CENSOR = Color.BLACK;

  public static final double CONTRAST_RATIO_WCAG_NORMAL_TEXT = 4.5;

  public static final double CONTRAST_RATIO_WCAG_LARGE_TEXT = 3.0;

  private ContrastUtils() {
    // Not instantiable
  }

  /**
   * Calculates the luminance value of a given {@code int} representation of {@link Color}.
   *
   * <p>Derived from formula at http://gmazzocato.altervista.org/colorwheel/algo.php
   *
   * @param color The {@link Color} to evaluate
   * @return the luminance value of the given color
   */
  public static double calculateLuminance(int color) {
    double r = linearColor(Color.red(color));
    double g = linearColor(Color.green(color));
    double b = linearColor(Color.blue(color));
    return 0.2126d * r + 0.7152d * g + 0.0722d * b;
  }

  private static double linearColor(int component) {
    double sRGB = component / 255.0d;
    if (sRGB <= 0.03928d) {
      return sRGB / 12.92d;
    } else {
      return Math.pow(((sRGB + 0.055d) / 1.055d), 2.4d);
    }
  }

  /**
   * Calculates the Delta E of CIE-94 perceived color difference of two color ints.
   *
   * <p>Derived from formula at https://www.easyrgb.com/en/math.php and
   * https://en.wikipedia.org/wiki/Color_difference
   *
   * @return the perceived color diffrence Delta E.
   */
  public static double colorDifference(int color1, int color2) {
    double[] lab1 = rgb2lab(color1);
    double[] lab2 = rgb2lab(color2);

    double deltaL = lab1[0] - lab2[0];
    double deltaA = lab1[1] - lab2[1];
    double deltaB = lab1[2] - lab2[2];
    double c1 = Math.hypot(lab1[1], lab1[2]);
    double c2 = Math.hypot(lab2[1], lab2[2]);
    double deltaC = c1 - c2;
    double deltaH = deltaA * deltaA + deltaB * deltaB - deltaC * deltaC;
    deltaH = deltaH < 0 ? 0 : Math.sqrt(deltaH);
    double sc = 1.0 + 0.045d * c1;
    double sh = 1.0 + 0.015d * c1;
    double deltaLKlsl = deltaL / 1.0d;
    double deltaCkcsc = deltaC / sc;
    double deltaHkhsh = deltaH / sh;
    return Math.sqrt(deltaLKlsl * deltaLKlsl + deltaCkcsc * deltaCkcsc + deltaHkhsh * deltaHkhsh);
  }

  /**
   * Convert linear RGB to CIE-L*ab color space.
   *
   * <p>Derived from formula at https://www.easyrgb.com/en/math.php
   *
   * @param color The int representation of an sRGB color.
   * @return the three values for L*, a* and b*
   */
  public static double[] rgb2lab(int color) {
    double r = linearColor(Color.red(color));
    double g = linearColor(Color.green(color));
    double b = linearColor(Color.blue(color));
    double x = (r * 0.4124d + g * 0.3576d + b * 0.1805d) / 0.95047d;
    double y = (r * 0.2126d + g * 0.7152d + b * 0.0722d) / 1.00000d;
    double z = (r * 0.0193d + g * 0.1192d + b * 0.9505d) / 1.08883d;
    x = (x > 0.008856d) ? Math.cbrt(x) : (7.787d * x) + 16.0d / 116;
    y = (y > 0.008856d) ? Math.cbrt(y) : (7.787d * y) + 16.0d / 116;
    z = (z > 0.008856d) ? Math.cbrt(z) : (7.787d * z) + 16.0d / 116;
    return new double[] {(116 * y) - 16, 500 * (x - y), 200 * (y - z)};
  }

  /**
   * Calculates the contrast ratio of two color ints.
   */
  public static double calculateContrastRatio(int color1, int color2) {
    return calculateContrastRatio(calculateLuminance(color1), calculateLuminance(color2));
  }

  /**
   * Calculates the contrast ratio of two order-independent luminance values.
   *
   * <p>Derived from formula at http://gmazzocato.altervista.org/colorwheel/algo.php
   *
   * @param lum1 The first luminance value
   * @param lum2 The second luminance value
   * @return The contrast ratio of the luminance values
   * @throws IllegalArgumentException if luminance values are < 0
   */
  public static double calculateContrastRatio(double lum1, double lum2) {
    if ((lum1 < 0.0d) || (lum2 < 0.0d)) {
      throw new IllegalArgumentException("Luminance values may not be negative.");
    }

    return (Math.max(lum1, lum2) + 0.05d) / (Math.min(lum1, lum2) + 0.05d);
  }

  /**
   * Converts an {@code int} representation of a {@link Color} to a hex string.
   *
   * @param color The {@link Color} value to convert
   * @return The hex string representation of the color
   */
  public static String colorToHexString(int color) {
    return String.format("#%06X", (0xFFFFFF & color));
  }
} public interface Image {

  /** Returns the image's height */
  int getHeight();

  /** Returns the image's width */
  int getWidth();

  /**
   * Returns an image representing a rectangular region within this image. The result will be a view
   * into the original image, so its contents will change if the original is modified.
   *
   * @param left The leftmost coordinate of the region
   * @param top The toptmost coordinate of the region
   * @param width The width of the region
   * @param height The height of the region
   */
  Image crop(int left, int top, int width, int height);

  /**
   * Returns a copy of the data within the image. Each value is a packed int representing a Color.
   */
  int[] getPixels();
}
import static com.google.android.apps.common.testing.accessibility.framework.ViewHierarchyElementUtils.isPotentiallyObscured;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.Math.max;
import static java.lang.Math.min;
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType;
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.AccessibilityHierarchyCheckResultWithImage;
import com.google.android.apps.common.testing.accessibility.framework.HashMapResultMetadata;
import com.google.android.apps.common.testing.accessibility.framework.Parameters;
import com.google.android.apps.common.testing.accessibility.framework.QuestionHandler;
import com.google.android.apps.common.testing.accessibility.framework.ResultMetadata;
import com.google.android.apps.common.testing.accessibility.framework.ViewHierarchyElementUtils;
import com.google.android.apps.common.testing.accessibility.framework.replacements.Rect;
import com.google.android.apps.common.testing.accessibility.framework.replacements.TextUtils;
import com.google.android.apps.common.testing.accessibility.framework.strings.StringManager;
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.android.apps.common.testing.accessibility.framework.utils.contrast.Color;
import com.google.android.apps.common.testing.accessibility.framework.utils.contrast.ContrastSwatch;
import com.google.android.apps.common.testing.accessibility.framework.utils.contrast.ContrastUtils;
import com.google.android.apps.common.testing.accessibility.framework.utils.contrast.Image;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.checkerframework.checker.nullness.qual.Nullable;
/*
 * Copyright (C) 2015 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

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



/** Check that ensures text content has sufficient contrast against its background */
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) {